changeset 8395:482e163caccd i18n

Merge closed stable-i18n Everything has already landed.
author Mads Kiilerich <mads@kiilerich.com>
date Mon, 04 May 2020 19:24:04 +0200
parents a67945aec3df (diff) c7e672477aee (current diff)
children c893a23fe267
files kallithea/i18n/be/LC_MESSAGES/kallithea.po kallithea/i18n/bg/LC_MESSAGES/kallithea.po kallithea/i18n/cs/LC_MESSAGES/kallithea.po kallithea/i18n/da/LC_MESSAGES/kallithea.po kallithea/i18n/de/LC_MESSAGES/kallithea.po kallithea/i18n/el/LC_MESSAGES/kallithea.po kallithea/i18n/es/LC_MESSAGES/kallithea.po kallithea/i18n/fr/LC_MESSAGES/kallithea.po kallithea/i18n/hu/LC_MESSAGES/kallithea.po kallithea/i18n/ja/LC_MESSAGES/kallithea.po kallithea/i18n/kallithea.pot kallithea/i18n/lb/LC_MESSAGES/kallithea.po kallithea/i18n/nb_NO/LC_MESSAGES/kallithea.po kallithea/i18n/nl_BE/LC_MESSAGES/kallithea.po kallithea/i18n/pl/LC_MESSAGES/kallithea.po kallithea/i18n/pt_BR/LC_MESSAGES/kallithea.po kallithea/i18n/ru/LC_MESSAGES/kallithea.po kallithea/i18n/sk/LC_MESSAGES/kallithea.po kallithea/i18n/tr/LC_MESSAGES/kallithea.po kallithea/i18n/uk/LC_MESSAGES/kallithea.po kallithea/i18n/zh_CN/LC_MESSAGES/kallithea.po kallithea/i18n/zh_TW/LC_MESSAGES/kallithea.po
diffstat 357 files changed, 20584 insertions(+), 20145 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.eslintrc.js	Mon May 04 19:24:04 2020 +0200
@@ -0,0 +1,21 @@
+module.exports = {
+    "env": {
+        "browser": true,
+        "es6": true,
+        "jquery": true
+    },
+    "extends": "eslint:recommended",
+    "globals": {
+        "Atomics": "readonly",
+        "SharedArrayBuffer": "readonly"
+    },
+    "parserOptions": {
+        "ecmaVersion": 2018,
+        "sourceType": "module"
+    },
+    "plugins": [
+        "html"
+    ],
+    "rules": {
+    }
+};
--- a/.hgtags	Mon May 04 18:25:09 2020 +0200
+++ b/.hgtags	Mon May 04 19:24:04 2020 +0200
@@ -74,3 +74,5 @@
 19086c5de05f4984d7a90cd31624c45dd893f6bb 0.4.0
 da65398a62fff50f3d241796cbf17acdea2092ef 0.4.1
 bfa0b0a814644f0af3f492d17a9ed169cc3b89fe 0.5.0
+d01a8e92936dbd62c76505432f60efba432e9397 0.5.1
+aa0a637fa6f635a5e024fa56b19ed2a2dacca857 0.5.2
--- a/CONTRIBUTORS	Mon May 04 18:25:09 2020 +0200
+++ b/CONTRIBUTORS	Mon May 04 19:24:04 2020 +0200
@@ -1,11 +1,13 @@
 List of contributors to Kallithea project:
 
+    Thomas De Schampheleire <thomas.de_schampheleire@nokia.com> 2014-2020
+    Mads Kiilerich <mads@kiilerich.com> 2016-2020
+    Dennis Fink <dennis.fink@c3l.lu> 2020
     Andrej Shadura <andrew@shadura.me> 2012 2014-2017 2019
-    Thomas De Schampheleire <thomas.de_schampheleire@nokia.com> 2014-2019
     Étienne Gilli <etienne.gilli@gmail.com> 2015-2017 2019
-    Mads Kiilerich <mads@kiilerich.com> 2016-2019
     Allan Nordhøy <epost@anotheragency.no> 2017-2019
     ssantos <ssantos@web.de> 2018-2019
+    Adi Kriegisch <adi@cg.tuwien.ac.at> 2019
     Danni Randeris <danniranderis@gmail.com> 2019
     Edmund Wong <ewong@crazy-cat.org> 2019
     Elizabeth Sherrock <lizzyd710@gmail.com> 2019
@@ -15,6 +17,7 @@
     Mateusz Mendel <mendelm9@gmail.com> 2019
     Nathan <bonnemainsnathan@gmail.com> 2019
     Oleksandr Shtalinberg <o.shtalinberg@gmail.com> 2019
+    Private <adamantine.sword@gmail.com> 2019
     THANOS SIOURDAKIS <siourdakisthanos@gmail.com> 2019
     Wolfgang Scherer <wolfgang.scherer@gmx.de> 2019
     Христо Станев <hstanev@gmail.com> 2019
--- a/Jenkinsfile	Mon May 04 18:25:09 2020 +0200
+++ b/Jenkinsfile	Mon May 04 19:24:04 2020 +0200
@@ -9,10 +9,10 @@
                               daysToKeepStr: '',
                               numToKeepStr: '']]]);
     if (isUnix()) {
-        createvirtualenv = 'rm -r $JENKINS_HOME/venv/$JOB_NAME || true && virtualenv $JENKINS_HOME/venv/$JOB_NAME'
+        createvirtualenv = 'rm -r $JENKINS_HOME/venv/$JOB_NAME || true && python3 -m venv $JENKINS_HOME/venv/$JOB_NAME'
         activatevirtualenv = '. $JENKINS_HOME/venv/$JOB_NAME/bin/activate'
     } else {
-        createvirtualenv = 'rmdir /s /q %JENKINS_HOME%\\venv\\%JOB_NAME% || true && virtualenv %JENKINS_HOME%\\venv\\%JOB_NAME%'
+        createvirtualenv = 'rmdir /s /q %JENKINS_HOME%\\venv\\%JOB_NAME% || true && python3 -m venv %JENKINS_HOME%\\venv\\%JOB_NAME%'
         activatevirtualenv = 'call %JENKINS_HOME%\\venv\\%JOB_NAME%\\Scripts\\activate.bat'
     }
 
--- a/README.rst	Mon May 04 18:25:09 2020 +0200
+++ b/README.rst	Mon May 04 19:24:04 2020 +0200
@@ -24,8 +24,8 @@
 Installation
 ------------
 
-Kallithea requires Python_ 2.7 and it is recommended to install it in a
-virtualenv_. Official releases of Kallithea can be installed with::
+Kallithea requires Python_ 3 and it is recommended to install it in a
+virtualenv. Official releases of Kallithea can be installed with::
 
     pip install kallithea
 
@@ -173,7 +173,6 @@
 of Kallithea.
 
 
-.. _virtualenv: http://pypi.python.org/pypi/virtualenv
 .. _Python: http://www.python.org/
 .. _Sphinx: http://sphinx.pocoo.org/
 .. _Mercurial: http://mercurial.selenic.com/
--- a/conftest.py	Mon May 04 18:25:09 2020 +0200
+++ b/conftest.py	Mon May 04 19:24:04 2020 +0200
@@ -2,10 +2,19 @@
 
 import mock
 import pytest
+import tg
 
 
 here = os.path.dirname(__file__)
 
+# HACK:
+def pytest_configure():
+    # Register global dummy tg.context to avoid "TypeError: No object (name: context) has been registered for this thread"
+    tg.request_local.context._push_object(tg.util.bunch.Bunch())
+    # could be removed again after use with
+    # tg.request_local.context._pop_object ... but we keep it around forever as
+    # a reasonable sentinel
+
 def pytest_ignore_collect(path):
     # ignore all files outside the 'kallithea' directory
     if not str(path).startswith(os.path.join(here, 'kallithea')):
@@ -36,3 +45,10 @@
     m = __import__(request.module.__name__, globals(), locals(), [None], 0)
     with mock.patch.object(m, '_', lambda s: s):
         yield
+
+if getattr(pytest, 'register_assert_rewrite', None):
+    # make sure that all asserts under kallithea/tests benefit from advanced
+    # assert reporting with pytest-3.0.0+, including api/api_base.py,
+    # models/common.py etc.
+    # See also: https://docs.pytest.org/en/latest/assert.html#advanced-assertion-introspection
+    pytest.register_assert_rewrite('kallithea.tests')
--- a/dev_requirements.txt	Mon May 04 18:25:09 2020 +0200
+++ b/dev_requirements.txt	Mon May 04 19:24:04 2020 +0200
@@ -1,8 +1,9 @@
-pytest >= 4.6.6, < 4.7
+pytest >= 4.6.6, < 5.4
 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, < 3.1
-Sphinx >= 1.8.0, < 1.9
-WebTest >= 2.0.3, < 2.1
+mock >= 3.0.0, < 4.1
+Sphinx >= 1.8.0, < 2.4
+WebTest >= 2.0.6, < 2.1
 isort == 4.3.21
+pyflakes == 2.1.1
--- a/development.ini	Mon May 04 18:25:09 2020 +0200
+++ b/development.ini	Mon May 04 19:24:04 2020 +0200
@@ -1,10 +1,10 @@
-################################################################################
-################################################################################
-# Kallithea - config file generated with kallithea-config                      #
-#                                                                              #
-# The %(here)s variable will be replaced with the parent directory of this file#
-################################################################################
-################################################################################
+###################################################################################
+###################################################################################
+## Kallithea config file generated with kallithea-config                         ##
+##                                                                               ##
+## The %(here)s variable will be replaced with the parent directory of this file ##
+###################################################################################
+###################################################################################
 
 [DEFAULT]
 
@@ -54,11 +54,11 @@
 ## For "SSL", use smtp_use_ssl = true and smtp_port = 465.
 ## For "STARTTLS", use smtp_use_tls = true and smtp_port = 587.
 smtp_server =
-#smtp_username =
-#smtp_password =
+smtp_username =
+smtp_password =
 smtp_port =
-#smtp_use_ssl = false
-#smtp_use_tls = false
+smtp_use_ssl = false
+smtp_use_tls = false
 
 ## Entry point for 'gearbox serve'
 [server:main]
@@ -90,10 +90,12 @@
 static_files = true
 
 ## Internationalization (see setup documentation for details)
-## By default, the language requested by the browser is used if available.
-#i18n.enabled = false
-## Fallback language, empty for English (valid values are the names of subdirectories in kallithea/i18n):
-i18n.lang =
+## By default, the languages requested by the browser are used if available, with English as default.
+## Set i18n.enabled=false to disable automatic language choice.
+#i18n.enabled = true
+## To Force a language, set i18n.enabled=false and specify the language in i18n.lang.
+## Valid values are the names of subdirectories in kallithea/i18n with a LC_MESSAGES/kallithea.mo
+#i18n.lang = en
 
 cache_dir = %(here)s/data
 index_dir = %(here)s/data/index
@@ -124,7 +126,7 @@
 ## used, which is correct in many cases but for example not when using uwsgi.
 ## If you change this setting, you should reinstall the Git hooks via
 ## Admin > Settings > Remap and Rescan.
-# git_hook_interpreter = /srv/kallithea/venv/bin/python2
+#git_hook_interpreter = /srv/kallithea/venv/bin/python3
 
 ## path to git executable
 git_path = git
@@ -196,7 +198,7 @@
 ## issue_pat, issue_server_link and issue_sub can have suffixes to specify
 ## multiple patterns, to other issues server, wiki or others
 ## below an example how to create a wiki pattern
-# wiki-some-id -> https://wiki.example.com/some-id
+## wiki-some-id -> https://wiki.example.com/some-id
 
 #issue_pat_wiki = wiki-(\S+)
 #issue_server_link_wiki = https://wiki.example.com/\1
@@ -214,12 +216,12 @@
 allow_custom_hooks_settings = True
 
 ## extra extensions for indexing, space separated and without the leading '.'.
-# index.extensions =
+#index.extensions =
 #    gemfile
 #    lock
 
 ## extra filenames for indexing, space separated
-# index.filenames =
+#index.filenames =
 #    .dockerignore
 #    .editorconfig
 #    INSTALL
@@ -248,25 +250,23 @@
 ###        CELERY CONFIG        ####
 ####################################
 
+## Note: Celery doesn't support Windows.
 use_celery = false
 
-## Example: connect to the virtual host 'rabbitmqhost' on localhost as rabbitmq:
-broker.url = amqp://rabbitmq:qewqew@localhost:5672/rabbitmqhost
+## Celery config settings from https://docs.celeryproject.org/en/4.4.0/userguide/configuration.html prefixed with 'celery.'.
 
-celery.imports = kallithea.lib.celerylib.tasks
-celery.accept.content = pickle
-celery.result.backend = amqp
-celery.result.dburi = amqp://
-celery.result.serialier = json
+## Example: use the message queue on the local virtual host 'kallitheavhost' as the RabbitMQ user 'kallithea':
+celery.broker_url = amqp://kallithea:thepassword@localhost:5672/kallitheavhost
 
-#celery.send.task.error.emails = true
+celery.result.backend = db+sqlite:///celery-results.db
+
 #celery.amqp.task.result.expires = 18000
 
-celeryd.concurrency = 2
-celeryd.max.tasks.per.child = 1
+celery.worker_concurrency = 2
+celery.worker_max_tasks_per_child = 1
 
 ## If true, tasks will never be sent to the queue, but executed locally instead.
-celery.always.eager = false
+celery.task_always_eager = false
 
 ####################################
 ###         BEAKER CACHE        ####
@@ -275,19 +275,15 @@
 beaker.cache.data_dir = %(here)s/data/cache/data
 beaker.cache.lock_dir = %(here)s/data/cache/lock
 
-beaker.cache.regions = short_term,long_term,sql_cache_short
-
-beaker.cache.short_term.type = memory
-beaker.cache.short_term.expire = 60
-beaker.cache.short_term.key_length = 256
+beaker.cache.regions = long_term,long_term_file
 
 beaker.cache.long_term.type = memory
 beaker.cache.long_term.expire = 36000
 beaker.cache.long_term.key_length = 256
 
-beaker.cache.sql_cache_short.type = memory
-beaker.cache.sql_cache_short.expire = 10
-beaker.cache.sql_cache_short.key_length = 256
+beaker.cache.long_term_file.type = file
+beaker.cache.long_term_file.expire = 604800
+beaker.cache.long_term_file.key_length = 256
 
 ####################################
 ###       BEAKER SESSION        ####
@@ -322,24 +318,33 @@
 #session.sa.url = postgresql://postgres:qwe@localhost/kallithea
 #session.table_name = db_session
 
-############################
-## ERROR HANDLING SYSTEMS ##
-############################
+####################################
+###       ERROR HANDLING        ####
+####################################
+
+## Show a nice error page for application HTTP errors and exceptions (default true)
+#errorpage.enabled = true
 
-# Propagate email settings to ErrorReporter of TurboGears2
-# You do not normally need to change these lines
-get trace_errors.error_email = email_to
+## Enable Backlash client-side interactive debugger (default false)
+## WARNING: *THIS MUST BE false IN PRODUCTION ENVIRONMENTS!!!*
+## This debug mode will allow all visitors to execute malicious code.
+#debug = false
+debug = true
+
+## Enable Backlash server-side error reporting (unless debug mode handles it client-side) (default true)
+#trace_errors.enable = true
+## Errors will be reported by mail if trace_errors.error_email is set.
+
+## Propagate email settings to ErrorReporter of TurboGears2
+## You do not normally need to change these lines
 get trace_errors.smtp_server = smtp_server
 get trace_errors.smtp_port = smtp_port
 get trace_errors.from_address = error_email_from
+get trace_errors.error_email = email_to
+get trace_errors.smtp_username = smtp_username
+get trace_errors.smtp_password = smtp_password
+get trace_errors.smtp_use_tls = smtp_use_tls
 
-################################################################################
-## WARNING: *DEBUG MODE MUST BE OFF IN A PRODUCTION ENVIRONMENT*              ##
-## Debug mode will enable the interactive debugging tool, allowing ANYONE to  ##
-## execute malicious code after an exception is raised.                       ##
-################################################################################
-#debug = false
-debug = true
 
 ##################################
 ###       LOGVIEW CONFIG       ###
@@ -353,10 +358,10 @@
 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG    ###
 #########################################################
 
-# SQLITE [default]
+## SQLITE [default]
 sqlalchemy.url = sqlite:///%(here)s/kallithea.db?timeout=60
 
-# see sqlalchemy docs for others
+## see sqlalchemy docs for other backends
 
 sqlalchemy.pool_recycle = 3600
 
@@ -387,9 +392,8 @@
 [logger_root]
 level = NOTSET
 #handlers = console
+## For coloring based on log level:
 handlers = console_color
-# For coloring based on log level:
-# handlers = console_color
 
 [logger_routes]
 #level = WARN
@@ -432,10 +436,10 @@
 level = WARN
 handlers =
 qualname = sqlalchemy.engine
-# For coloring based on log level and pretty printing of SQL:
-# level = INFO
-# handlers = console_color_sql
-# propagate = 0
+## For coloring based on log level and pretty printing of SQL:
+#level = INFO
+#handlers = console_color_sql
+#propagate = 0
 
 [logger_whoosh_indexer]
 #level = WARN
@@ -463,13 +467,13 @@
 formatter = generic
 
 [handler_console_color]
-# ANSI color coding based on log level
+## ANSI color coding based on log level
 class = StreamHandler
 args = (sys.stderr,)
 formatter = color_formatter
 
 [handler_console_color_sql]
-# ANSI color coding and pretty printing of SQL statements
+## ANSI color coding and pretty printing of SQL statements
 class = StreamHandler
 args = (sys.stderr,)
 formatter = color_formatter_sql
@@ -500,16 +504,16 @@
 ## SSH LOGGING ##
 #################
 
-# The default loggers use 'handler_console' that uses StreamHandler with
-# destination 'sys.stderr'. In the context of the SSH server process, these log
-# messages would be sent to the client, which is normally not what you want.
-# By default, when running ssh-serve, just use NullHandler and disable logging
-# completely. For other logging options, see:
-# https://docs.python.org/2/library/logging.handlers.html
+## The default loggers use 'handler_console' that uses StreamHandler with
+## destination 'sys.stderr'. In the context of the SSH server process, these log
+## messages would be sent to the client, which is normally not what you want.
+## By default, when running ssh-serve, just use NullHandler and disable logging
+## completely. For other logging options, see:
+## https://docs.python.org/2/library/logging.handlers.html
 
 [ssh_serve:logger_root]
 level = CRITICAL
 handlers = null
 
-# Note: If logging is configured with other handlers, they might need similar
-# muting for ssh-serve too.
+## Note: If logging is configured with other handlers, they might need similar
+## muting for ssh-serve too.
--- a/docs/administrator_guide/auth.rst	Mon May 04 18:25:09 2020 +0200
+++ b/docs/administrator_guide/auth.rst	Mon May 04 19:24:04 2020 +0200
@@ -135,10 +135,10 @@
 .. _Custom CA Certificates:
 
 Custom CA Certificates : optional
-    Directory used by OpenSSL to find CAs for validating the LDAP server certificate.
-    Python 2.7.10 and later default to using the system certificate store, and
-    this should thus not be necessary when using certificates signed by a CA
-    trusted by the system.
+    Directory used by OpenSSL to find CAs for validating the LDAP server
+    certificate. It defaults to using the system certificate store, and it
+    should thus not be necessary to specify *Custom CA Certificates* when using
+    certificates signed by a CA trusted by the system.
     It can be set to something like `/etc/openldap/cacerts` on older systems or
     if using self-signed certificates.
 
--- a/docs/api/models.rst	Mon May 04 18:25:09 2020 +0200
+++ b/docs/api/models.rst	Mon May 04 19:24:04 2020 +0200
@@ -13,9 +13,6 @@
 .. automodule:: kallithea.model.permission
    :members:
 
-.. automodule:: kallithea.model.repo_permission
-   :members:
-
 .. automodule:: kallithea.model.repo
    :members:
 
--- a/docs/conf.py	Mon May 04 18:25:09 2020 +0200
+++ b/docs/conf.py	Mon May 04 19:24:04 2020 +0200
@@ -46,8 +46,8 @@
 master_doc = 'index'
 
 # General information about the project.
-project = u'Kallithea'
-copyright = u'2010-2019 by various authors, licensed as GPLv3.'
+project = 'Kallithea'
+copyright = '2010-2020 by various authors, licensed as GPLv3.'
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
@@ -187,8 +187,8 @@
 # Grouping the document tree into LaTeX files. List of tuples
 # (source start file, target name, title, author, documentclass [howto/manual]).
 latex_documents = [
-  ('index', 'Kallithea.tex', u'Kallithea Documentation',
-   u'Kallithea Developers', 'manual'),
+  ('index', 'Kallithea.tex', 'Kallithea Documentation',
+   'Kallithea Developers', 'manual'),
 ]
 
 # The name of an image file (relative to this directory) to place at the top of
@@ -220,8 +220,8 @@
 # One entry per manual page. List of tuples
 # (source start file, name, description, authors, manual section).
 man_pages = [
-    ('index', 'kallithea', u'Kallithea Documentation',
-     [u'Kallithea Developers'], 1)
+    ('index', 'kallithea', 'Kallithea Documentation',
+     ['Kallithea Developers'], 1)
 ]
 
 
--- a/docs/contributing.rst	Mon May 04 18:25:09 2020 +0200
+++ b/docs/contributing.rst	Mon May 04 19:24:04 2020 +0200
@@ -32,7 +32,7 @@
 
         hg clone https://kallithea-scm.org/repos/kallithea
         cd kallithea
-        virtualenv ../kallithea-venv
+        python3 -m venv ../kallithea-venv
         source ../kallithea-venv/bin/activate
         pip install --upgrade pip setuptools
         pip install --upgrade -e . -r dev_requirements.txt python-ldap python-pam
@@ -92,8 +92,7 @@
 and the test suite creates repositories in the temporary directory. Linux
 systems with /tmp mounted noexec will thus fail.
 
-You can also use ``tox`` to run the tests with all supported Python versions
-(currently only Python 2.7).
+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
 in `kallithea/tests/conftest.py` and populates the SQLite database specified
@@ -199,8 +198,7 @@
 consistency with existing code. Run ``scripts/run-all-cleanup`` before
 committing to ensure some basic code formatting consistency.
 
-We currently only support Python 2.7.x and nothing else. For now we don't care
-about Python 3 compatibility.
+We support Python 3.6 and later.
 
 We try to support the most common modern web browsers. IE9 is still supported
 to the extent it is feasible, IE8 is not.
@@ -238,8 +236,8 @@
 as in an independent database transaction). ``Session`` is the session manager
 and factory. ``Session()`` will create a new session on-demand or return the
 current session for the active thread. Many database operations are methods on
-such session instances - only ``Session.remove()`` should be called directly on
-the manager.
+such session instances. The session will generally be removed by
+TurboGears automatically.
 
 Database model objects
 (almost) always belong to a particular SQLAlchemy session, which means
@@ -268,6 +266,20 @@
 a freshly created model object (before flushing, the ID attribute will
 be ``None``).
 
+Debugging
+^^^^^^^^^
+
+A good way to trace what Kallithea is doing is to keep an eye on the output on
+stdout/stderr of the server process. Perhaps change ``my.ini`` to log at
+``DEBUG`` or ``INFO`` level, especially ``[logger_kallithea]``, but perhaps
+also other loggers. It is often easier to add additional ``log`` or ``print``
+statements than to use a Python debugger.
+
+Sometimes it is simpler to disable ``errorpage.enabled`` and perhaps also
+``trace_errors.enable`` to expose raw errors instead of adding extra
+processing. Enabling ``debug`` can be helpful for showing and exploring
+tracebacks in the browser, but is also insecure and will add extra processing.
+
 TurboGears2 DebugBar
 ^^^^^^^^^^^^^^^^^^^^
 
--- a/docs/index.rst	Mon May 04 18:25:09 2020 +0200
+++ b/docs/index.rst	Mon May 04 19:24:04 2020 +0200
@@ -78,7 +78,6 @@
    dev/dbmigrations
 
 
-.. _virtualenv: http://pypi.python.org/pypi/virtualenv
 .. _python: http://www.python.org/
 .. _django: http://www.djangoproject.com/
 .. _mercurial: https://www.mercurial-scm.org/
--- a/docs/installation.rst	Mon May 04 18:25:09 2020 +0200
+++ b/docs/installation.rst	Mon May 04 19:24:04 2020 +0200
@@ -35,12 +35,12 @@
 For Debian and Ubuntu, the following command will ensure that a reasonable
 set of dependencies is installed::
 
-    sudo apt-get install build-essential git python-pip python-virtualenv libffi-dev python-dev
+    sudo apt-get install build-essential git libffi-dev python3-dev
 
 For Fedora and RHEL-derivatives, the following command will ensure that a
 reasonable set of dependencies is installed::
 
-    sudo yum install gcc git python-pip python-virtualenv libffi-devel python-devel
+    sudo yum install gcc git libffi-devel python3-devel
 
 .. _installation-source:
 
@@ -48,16 +48,16 @@
 Installation from repository source
 -----------------------------------
 
-To install Kallithea in a virtualenv_ using the stable branch of the development
+To install Kallithea in a virtualenv using the stable branch of the development
 repository, follow the instructions below::
 
         hg clone https://kallithea-scm.org/repos/kallithea -u stable
         cd kallithea
-        virtualenv ../kallithea-venv
+        python3 -m venv ../kallithea-venv
         . ../kallithea-venv/bin/activate
         pip install --upgrade pip setuptools
         pip install --upgrade -e .
-        python2 setup.py compile_catalog   # for translation of the UI
+        python3 setup.py compile_catalog   # for translation of the UI
 
 You can now proceed to :ref:`setup`.
 
@@ -67,18 +67,18 @@
 Installing a released version in a virtualenv
 ---------------------------------------------
 
-It is highly recommended to use a separate virtualenv_ for installing Kallithea.
+It is highly recommended to use a separate virtualenv for installing Kallithea.
 This way, all libraries required by Kallithea will be installed separately from your
 main Python installation and other applications and things will be less
 problematic when upgrading the system or Kallithea.
-An additional benefit of virtualenv_ is that it doesn't require root privileges.
+An additional benefit of virtualenv is that it doesn't require root privileges.
 
-- Assuming you have installed virtualenv_, create a new virtual environment
-  for example, in `/srv/kallithea/venv`, using the virtualenv command::
+- Assuming you have installed virtualenv, create a new virtual environment
+  for example, in `/srv/kallithea/venv`, using the venv command::
 
-    virtualenv /srv/kallithea/venv
+    python3 -m venv /srv/kallithea/venv
 
-- Activate the virtualenv_ in your current shell session and make sure the
+- Activate the virtualenv in your current shell session and make sure the
   basic requirements are up-to-date by running::
 
     . /srv/kallithea/venv/bin/activate
@@ -133,6 +133,3 @@
     pip install --user kallithea
 
 You can now proceed to :ref:`setup`.
-
-
-.. _virtualenv: http://pypi.python.org/pypi/virtualenv
--- a/docs/installation_iis.rst	Mon May 04 18:25:09 2020 +0200
+++ b/docs/installation_iis.rst	Mon May 04 19:24:04 2020 +0200
@@ -1,5 +1,7 @@
 .. _installation_iis:
 
+.. warning:: This section is outdated and needs updating for Python 3.
+
 =====================================================================
 Installing Kallithea on Microsoft Internet Information Services (IIS)
 =====================================================================
@@ -66,7 +68,7 @@
 has been generated, it is necessary to run the following command due to the way
 that ISAPI-WSGI is made::
 
-    python2 dispatch.py install
+    python3 dispatch.py install
 
 This accomplishes two things: generating an ISAPI compliant DLL file,
 ``_dispatch.dll``, and installing a script map handler into IIS for the
@@ -119,7 +121,7 @@
 In order to dump output from WSGI using ``win32traceutil`` it is sufficient to
 type the following in a console window::
 
-    python2 -m win32traceutil
+    python3 -m win32traceutil
 
 and any exceptions occurring in the WSGI layer and below (i.e. in the Kallithea
 application itself) that are uncaught, will be printed here complete with stack
--- a/docs/installation_win.rst	Mon May 04 18:25:09 2020 +0200
+++ b/docs/installation_win.rst	Mon May 04 19:24:04 2020 +0200
@@ -1,5 +1,7 @@
 .. _installation_win:
 
+.. warning:: This section is outdated and needs updating for Python 3.
+
 ====================================================
 Installation on Windows (7/Server 2008 R2 and newer)
 ====================================================
@@ -17,18 +19,16 @@
 Step 1 -- Install Python
 ^^^^^^^^^^^^^^^^^^^^^^^^
 
-Install Python 2.7.x. Latest version is recommended. If you need another version, they can run side by side.
+Install Python 3. Latest version is recommended. If you need another version, they can run side by side.
 
-.. warning:: Python 3.x is not supported.
-
-- Download Python 2.7.x from http://www.python.org/download/
+- Download Python 3 from http://www.python.org/download/
 - Choose and click on the version
 - Click on "Windows X86-64 Installer" for x64 or "Windows x86 MSI installer" for Win32.
 - Disable UAC or run the installer with admin privileges. If you chose to disable UAC, do not forget to reboot afterwards.
 
-While writing this guide, the latest version was v2.7.9.
+While writing this guide, the latest version was v3.8.1.
 Remember the specific major and minor versions installed, because they will
-be needed in the next step. In this case, it is "2.7".
+be needed in the next step. In this case, it is "3.8".
 
 Step 2 -- Python BIN
 ^^^^^^^^^^^^^^^^^^^^
@@ -42,7 +42,7 @@
   SETX PATH "%PATH%;[your-python-path]" /M
 
 Please substitute [your-python-path] with your Python installation
-path. Typically this is ``C:\\Python27``.
+path. Typically this is ``C:\\Python38``.
 
 Step 3 -- Install pywin32 extensions
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -52,38 +52,14 @@
 
 - Click on "pywin32" folder
 - Click on the first folder (in this case, Build 219, maybe newer when you try)
-- Choose the file ending with ".amd64-py2.x.exe" (".win32-py2.x.exe"
+- Choose the file ending with ".amd64-py3.x.exe" (".win32-py3.x.exe"
   for Win32) where x is the minor version of Python you installed.
   When writing this guide, the file was:
-  http://sourceforge.net/projects/pywin32/files/pywin32/Build%20219/pywin32-219.win-amd64-py2.7.exe/download
+  http://sourceforge.net/projects/pywin32/files/pywin32/Build%20219/pywin32-219.win-amd64-py3.8.exe/download
   (x64)
-  http://sourceforge.net/projects/pywin32/files/pywin32/Build%20219/pywin32-219.win32-py2.7.exe/download
+  http://sourceforge.net/projects/pywin32/files/pywin32/Build%20219/pywin32-219.win32-py3.8.exe/download
   (Win32)
 
-Step 4 -- Install pip
-^^^^^^^^^^^^^^^^^^^^^
-
-pip is a package management system for Python. You will need it to install Kallithea and its dependencies.
-
-If you installed Python 2.7.9+, you already have it (as long as you ran the installer with admin privileges or disabled UAC).
-
-If it was not installed or if you are using Python < 2.7.9:
-
-- Go to https://bootstrap.pypa.io
-- Right-click on get-pip.py and choose Saves as...
-- Run "python2 get-pip.py" in the folder where you downloaded get-pip.py (may require admin access).
-
-.. note::
-
-   See http://stackoverflow.com/questions/4750806/how-to-install-pip-on-windows
-   for details and alternative methods.
-
-Note that pip.exe will be placed inside your Python installation's
-Scripts folder, which is likely not on your path. To correct this,
-open a CMD and type::
-
-  SETX PATH "%PATH%;[your-python-path]\Scripts" /M
-
 Step 5 -- Kallithea folder structure
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
@@ -108,24 +84,18 @@
    A python virtual environment will allow for isolation between the Python packages of your system and those used for Kallithea.
    It is strongly recommended to use it to ensure that Kallithea does not change a dependency that other software uses or vice versa.
 
-In a command prompt type::
-
-  pip install virtualenv
-
-Virtualenv will now be inside your Python Scripts path (C:\\Python27\\Scripts or similar).
-
 To create a virtual environment, run::
 
-  virtualenv C:\Kallithea\Env
+  python3 -m venv C:\Kallithea\Env
 
 Step 7 -- Install Kallithea
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 In order to install Kallithea, you need to be able to run "pip install kallithea". It will use pip to install the Kallithea Python package and its dependencies.
 Some Python packages use managed code and need to be compiled.
-This can be done on Linux without any special steps. On Windows, you will need to install Microsoft Visual C++ compiler for Python 2.7.
+This can be done on Linux without any special steps. On Windows, you will need to install Microsoft Visual C++ compiler for Python 3.8.
 
-Download and install "Microsoft Visual C++ Compiler for Python 2.7" from http://aka.ms/vcpython27
+Download and install "Microsoft Visual C++ Compiler for Python 3.8" from http://aka.ms/vcpython27
 
 .. note::
   You can also install the dependencies using already compiled Windows binaries packages. A good source of compiled Python packages is http://www.lfd.uci.edu/~gohlke/pythonlibs/. However, not all of the necessary packages for Kallithea are on this site and some are hard to find, so we will stick with using the compiler.
--- a/docs/installation_win_old.rst	Mon May 04 18:25:09 2020 +0200
+++ b/docs/installation_win_old.rst	Mon May 04 19:24:04 2020 +0200
@@ -1,5 +1,7 @@
 .. _installation_win_old:
 
+.. warning:: This section is outdated and needs updating for Python 3.
+
 ==========================================================
 Installation on Windows (XP/Vista/Server 2003/Server 2008)
 ==========================================================
@@ -60,14 +62,11 @@
 Step 2 -- Install Python
 ^^^^^^^^^^^^^^^^^^^^^^^^
 
-Install Python 2.7.x x86 version (32-bit). DO NOT USE A 3.x version.
-Download Python 2.7.x from:
+Install Python 3.8.x from:
 http://www.python.org/download/
 
-Choose "Windows Installer" (32-bit version) not "Windows X86-64
-Installer". While writing this guide, the latest version was v2.7.3.
 Remember the specific major and minor version installed, because it will
-be needed in the next step. In this case, it is "2.7".
+be needed in the next step. In this case, it is "3.8".
 
 .. note::
 
@@ -80,17 +79,17 @@
 http://sourceforge.net/projects/pywin32/files/
 
 - Click on "pywin32" folder
-- Click on the first folder (in this case, Build 217, maybe newer when you try)
-- Choose the file ending with ".win32-py2.x.exe" -> x being the minor
+- Click on the first folder (in this case, Build 218, maybe newer when you try)
+- Choose the file ending with ".win32-py3.x.exe" -> x being the minor
   version of Python you installed (in this case, 7)
   When writing this guide, the file was:
-  http://sourceforge.net/projects/pywin32/files/pywin32/Build%20217/pywin32-217.win32-py2.7.exe/download
+  http://sourceforge.net/projects/pywin32/files/pywin32/Build%20218/pywin32-218.win-amd64-py3.8.exe/download
 
   .. note::
 
      64-bit: Download and install the 64-bit version.
      At the time of writing you can find this at:
-     http://sourceforge.net/projects/pywin32/files/pywin32/Build%20218/pywin32-218.win-amd64-py2.7.exe/download
+     http://sourceforge.net/projects/pywin32/files/pywin32/Build%20218/pywin32-218.win-amd64-py3.8.exe/download
 
 Step 4 -- Python BIN
 ^^^^^^^^^^^^^^^^^^^^
@@ -117,7 +116,7 @@
     SETX PATH "%PATH%;[your-python-path]" /M
 
   Please substitute [your-python-path] with your Python installation path.
-  Typically: C:\\Python27
+  Typically: C:\\Python38
 
 Step 5 -- Kallithea folder structure
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -139,22 +138,10 @@
 Step 6 -- Install virtualenv
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-Install Virtual Env for Python
-
-Navigate to: http://www.virtualenv.org/en/latest/index.html#installation
-Right click on "virtualenv.py" file and choose "Save link as...".
-Download to C:\\Kallithea (or whatever you want)
-(the file is located at
-https://raw.github.com/pypa/virtualenv/master/virtualenv.py)
+Create a virtual Python environment in C:\\Kallithea\\Env (or similar). To
+do so, open a CMD (Python Path should be included in Step3), and write::
 
-Create a virtual Python environment in C:\\Kallithea\\Env (or similar). To
-do so, open a CMD (Python Path should be included in Step3), navigate
-where you downloaded "virtualenv.py", and write::
-
-  python2 virtualenv.py C:\Kallithea\Env
-
-(--no-site-packages is now the default behaviour of virtualenv, no need
-to include it)
+  python3 -m venv C:\Kallithea\Env
 
 Step 7 -- Install Kallithea
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^
--- a/docs/overview.rst	Mon May 04 18:25:09 2020 +0200
+++ b/docs/overview.rst	Mon May 04 19:24:04 2020 +0200
@@ -12,7 +12,7 @@
 ------------------
 
 **Kallithea** is written entirely in Python_ and requires Python version
-2.7 or higher. Python 3.x is currently not supported.
+3.6 or higher.
 
 Given a Python installation, there are different ways of providing the
 environment for running Python applications. Each of them pretty much
@@ -30,7 +30,7 @@
 - Packages could also be installed in ``~/.local`` ... but that is probably
   only a good idea if using a dedicated user per application or instance.
 
-- Finally, it can be installed in a virtualenv_. That is a very lightweight
+- Finally, it can be installed in a virtualenv. That is a very lightweight
   "container" where each Kallithea instance can get its own dedicated and
   self-contained virtual environment.
 
@@ -98,7 +98,7 @@
   installed with all dependencies using ``pip install kallithea``.
 
   With this method, Kallithea is installed in the Python environment as any
-  other package, usually as a ``.../site-packages/Kallithea-X-py2.7.egg/``
+  other package, usually as a ``.../site-packages/Kallithea-X-py3.8.egg/``
   directory with Python files and everything else that is needed.
 
   (``pip install kallithea`` from a source tree will do pretty much the same
@@ -165,7 +165,6 @@
 .. _Python: http://www.python.org/
 .. _Gunicorn: http://gunicorn.org/
 .. _Waitress: http://waitress.readthedocs.org/en/latest/
-.. _virtualenv: http://pypi.python.org/pypi/virtualenv
 .. _Gearbox: http://turbogears.readthedocs.io/en/latest/turbogears/gearbox.html
 .. _PyPI: https://pypi.python.org/pypi
 .. _Apache httpd: http://httpd.apache.org/
--- a/docs/setup.rst	Mon May 04 18:25:09 2020 +0200
+++ b/docs/setup.rst	Mon May 04 19:24:04 2020 +0200
@@ -80,13 +80,12 @@
 language, as indicated by the browser. Thus, different users may see the
 application in different languages. If the requested language is not available
 (because the translation file for that language does not yet exist or is
-incomplete), the language specified in setting ``i18n.lang`` in the Kallithea
-configuration file is used as fallback. If no fallback language is explicitly
-specified, English is used.
+incomplete), English is used.
 
 If you want to disable automatic language detection and instead configure a
 fixed language regardless of user preference, set ``i18n.enabled = false`` and
-set ``i18n.lang`` to the desired language (or leave empty for English).
+specify another language by setting ``i18n.lang`` in the Kallithea
+configuration file.
 
 
 Using Kallithea with SSH
@@ -154,6 +153,16 @@
     process, the server process will raise an exception each time it attempts to
     write the ``authorized_keys`` file.
 
+.. note:: It is possible to configure the SSH server to look for authorized
+   keys in multiple files, for example reserving ``ssh/authorized_keys`` to be
+   used for normal SSH and with Kallithea using
+   ``.ssh/authorized_keys_kallithea``. In ``/etc/ssh/sshd_config`` set
+   ``AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys_kallithea``
+   and restart sshd, and in ``my.ini`` set ``ssh_authorized_keys =
+   /home/kallithea/.ssh/authorized_keys_kallithea``. Note that this new
+   location will apply to all system users, and that multiple entries for the
+   same SSH key will shadow each other.
+
 .. warning:: The handling of SSH access is steered directly by the command
     specified in the ``authorized_keys`` file. There is no interaction with the
     web UI.  Once SSH access is correctly configured and enabled, it will work
@@ -333,11 +342,11 @@
 
   use_celery = true
 
-and add or change the ``celery.*`` and ``broker.*`` configuration variables.
+and add or change the ``celery.*`` configuration variables.
 
-Remember that the ini files use the format with '.' and not with '_' like
-Celery. So for example setting `BROKER_HOST` in Celery means setting
-`broker.host` in the configuration file.
+Configuration settings are prefixed with 'celery.', so for example setting
+`broker_url` in Celery means setting `celery.broker_url` in the configuration
+file.
 
 To start the Celery process, run::
 
@@ -558,11 +567,11 @@
       os.chdir('/srv/kallithea/')
 
       import site
-      site.addsitedir("/srv/kallithea/venv/lib/python2.7/site-packages")
+      site.addsitedir("/srv/kallithea/venv/lib/python3.7/site-packages")
 
       ini = '/srv/kallithea/my.ini'
       from logging.config import fileConfig
-      fileConfig(ini)
+      fileConfig(ini, {'__file__': ini, 'here': '/srv/kallithea'})
       from paste.deploy import loadapp
       application = loadapp('config:' + ini)
 
@@ -578,7 +587,7 @@
 
       ini = '/srv/kallithea/kallithea.ini'
       from logging.config import fileConfig
-      fileConfig(ini)
+      fileConfig(ini, {'__file__': ini, 'here': '/srv/kallithea'})
       from paste.deploy import loadapp
       application = loadapp('config:' + ini)
 
@@ -625,7 +634,6 @@
 .. __: https://kallithea-scm.org/repos/kallithea/files/tip/init.d/ .
 
 
-.. _virtualenv: http://pypi.python.org/pypi/virtualenv
 .. _python: http://www.python.org/
 .. _Python regular expression documentation: https://docs.python.org/2/library/re.html
 .. _Mercurial: https://www.mercurial-scm.org/
--- a/docs/upgrade.rst	Mon May 04 18:25:09 2020 +0200
+++ b/docs/upgrade.rst	Mon May 04 19:24:04 2020 +0200
@@ -241,6 +241,3 @@
 .. note::
     Kallithea does not use hooks on Mercurial repositories. This step is thus
     not necessary if you only have Mercurial repositories.
-
-
-.. _virtualenv: http://pypi.python.org/pypi/virtualenv
--- a/docs/usage/troubleshooting.rst	Mon May 04 18:25:09 2020 +0200
+++ b/docs/usage/troubleshooting.rst	Mon May 04 19:24:04 2020 +0200
@@ -8,7 +8,7 @@
 :A: Make sure either to set the ``static_files = true`` in the .ini file or
    double check the root path for your http setup. It should point to
    for example:
-   ``/home/my-virtual-python/lib/python2.7/site-packages/kallithea/public``
+   ``/home/my-virtual-python/lib/python3.7/site-packages/kallithea/public``
 
 |
 
@@ -67,7 +67,6 @@
     you have installed the latest Windows patches (especially KB2789397).
 
 
-.. _virtualenv: http://pypi.python.org/pypi/virtualenv
 .. _python: http://www.python.org/
 .. _mercurial: https://www.mercurial-scm.org/
 .. _celery: http://celeryproject.org/
--- a/kallithea/__init__.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/__init__.py	Mon May 04 19:24:04 2020 +0200
@@ -31,13 +31,16 @@
 import sys
 
 
-VERSION = (0, 5, 0)
+if sys.version_info < (3, 6):
+    raise Exception('Kallithea requires python 3.6 or later')
+
+VERSION = (0, 5, 99)
 BACKENDS = {
     'hg': 'Mercurial repository',
     'git': 'Git repository',
 }
 
-CELERY_ON = False
+CELERY_APP = None  # set to Celery app instance if using Celery
 CELERY_EAGER = False
 
 CONFIG = {}
--- a/kallithea/alembic/env.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/alembic/env.py	Mon May 04 19:24:04 2020 +0200
@@ -15,6 +15,7 @@
 # Alembic migration environment (configuration).
 
 import logging
+import os
 from logging.config import fileConfig
 
 from alembic import context
@@ -43,7 +44,9 @@
 # stamping during "kallithea-cli db-create"), config_file_name is not available,
 # and loggers are assumed to already have been configured.
 if config.config_file_name:
-    fileConfig(config.config_file_name, disable_existing_loggers=False)
+    fileConfig(config.config_file_name,
+        {'__file__': config.config_file_name, 'here': os.path.dirname(config.config_file_name)},
+        disable_existing_loggers=False)
 
 
 def include_in_autogeneration(object, name, type, reflected, compare_to):
--- a/kallithea/alembic/versions/4851d15bc437_db_migration_step_after_95c01895c006_.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/alembic/versions/4851d15bc437_db_migration_step_after_95c01895c006_.py	Mon May 04 19:24:04 2020 +0200
@@ -31,14 +31,21 @@
 
 
 def upgrade():
-    meta = sa.MetaData()
-    meta.reflect(bind=op.get_bind())
+    pass
+    # The following upgrade step turned out to be a bad idea. A later step
+    # "d7ec25b66e47_ssh_drop_usk_public_key_idx_again" will remove the index
+    # again if it exists ... but we shouldn't even try to create it.
 
-    if not any(i.name == 'usk_public_key_idx' for i in meta.tables['user_ssh_keys'].indexes):
-        with op.batch_alter_table('user_ssh_keys', schema=None) as batch_op:
-            batch_op.create_index('usk_public_key_idx', ['public_key'], unique=False)
+    #meta = sa.MetaData()
+    #meta.reflect(bind=op.get_bind())
+
+    #if not any(i.name == 'usk_public_key_idx' for i in meta.tables['user_ssh_keys'].indexes):
+    #    with op.batch_alter_table('user_ssh_keys', schema=None) as batch_op:
+    #        batch_op.create_index('usk_public_key_idx', ['public_key'], unique=False)
 
 
 def downgrade():
-    with op.batch_alter_table('user_ssh_keys', schema=None) as batch_op:
-        batch_op.drop_index('usk_public_key_idx')
+    meta = sa.MetaData()
+    if any(i.name == 'usk_public_key_idx' for i in meta.tables['user_ssh_keys'].indexes):
+        with op.batch_alter_table('user_ssh_keys', schema=None) as batch_op:
+            batch_op.drop_index('usk_public_key_idx')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/kallithea/alembic/versions/a0a1bf09c143_db_add_ui_composite_index_and_drop_.py	Mon May 04 19:24:04 2020 +0200
@@ -0,0 +1,48 @@
+# 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/>.
+
+"""db: add Ui composite index and drop UniqueConstraint on Ui.ui_key
+
+Revision ID: a0a1bf09c143
+Revises: d7ec25b66e47
+Create Date: 2020-03-12 22:41:14.421837
+
+"""
+
+# The following opaque hexadecimal identifiers ("revisions") are used
+# by Alembic to track this migration script and its relations to others.
+revision = 'a0a1bf09c143'
+down_revision = 'd7ec25b66e47'
+branch_labels = None
+depends_on = None
+
+import sqlalchemy as sa
+from alembic import op
+
+
+def upgrade():
+    meta = sa.MetaData()
+    meta.reflect(bind=op.get_bind())
+
+    with op.batch_alter_table('ui', schema=None) as batch_op:
+        batch_op.create_index('ui_ui_section_ui_key_idx', ['ui_section', 'ui_key'], unique=False)
+        if any(i.name == 'uq_ui_ui_key' for i in meta.tables['ui'].constraints):
+            batch_op.drop_constraint('uq_ui_ui_key', type_='unique')
+        elif any(i.name == 'ui_ui_key_key' for i in meta.tables['ui'].constraints):  # table was created with old naming before 1a080d4e926e
+            batch_op.drop_constraint('ui_ui_key_key', type_='unique')
+
+
+def downgrade():
+    with op.batch_alter_table('ui', schema=None) as batch_op:
+        batch_op.create_unique_constraint('uq_ui_ui_key', ['ui_key'])
+        batch_op.drop_index('ui_ui_section_ui_key_idx')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/kallithea/alembic/versions/d7ec25b66e47_ssh_drop_usk_public_key_idx_again.py	Mon May 04 19:24:04 2020 +0200
@@ -0,0 +1,43 @@
+# 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/>.
+
+"""ssh: drop usk_public_key_idx again
+
+Revision ID: d7ec25b66e47
+Revises: 4851d15bc437
+Create Date: 2019-12-29 15:33:10.982003
+
+"""
+
+# The following opaque hexadecimal identifiers ("revisions") are used
+# by Alembic to track this migration script and its relations to others.
+revision = 'd7ec25b66e47'
+down_revision = '4851d15bc437'
+branch_labels = None
+depends_on = None
+
+import sqlalchemy as sa
+from alembic import op
+
+
+def upgrade():
+    meta = sa.MetaData()
+    meta.reflect(bind=op.get_bind())
+
+    if any(i.name == 'usk_public_key_idx' for i in meta.tables['user_ssh_keys'].indexes):
+        with op.batch_alter_table('user_ssh_keys', schema=None) as batch_op:
+            batch_op.drop_index('usk_public_key_idx')
+
+
+def downgrade():
+    pass
--- a/kallithea/bin/base.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/bin/base.py	Mon May 04 19:24:04 2020 +0200
@@ -29,9 +29,10 @@
 import pprint
 import random
 import sys
-import urllib2
+import urllib.request
 
-from kallithea.lib.compat import json
+from kallithea.lib import ext_json
+from kallithea.lib.utils2 import ascii_bytes
 
 
 CONFIG_NAME = '.config/kallithea'
@@ -67,12 +68,12 @@
         raise Exception('please specify method name !')
     apihost = apihost.rstrip('/')
     id_ = random.randrange(1, 9999)
-    req = urllib2.Request('%s/_admin/api' % apihost,
-                      data=json.dumps(_build_data(id_)),
+    req = urllib.request.Request('%s/_admin/api' % apihost,
+                      data=ascii_bytes(ext_json.dumps(_build_data(id_))),
                       headers={'content-type': 'text/plain'})
-    ret = urllib2.urlopen(req)
+    ret = urllib.request.urlopen(req)
     raw_json = ret.read()
-    json_data = json.loads(raw_json)
+    json_data = ext_json.loads(raw_json)
     id_ret = json_data['id']
     if id_ret == id_:
         return json_data
@@ -107,7 +108,7 @@
     def __getitem__(self, key):
         return self._conf[key]
 
-    def __nonzero__(self):
+    def __bool__(self):
         if self._conf:
             return True
         return False
@@ -128,7 +129,7 @@
         if os.path.exists(self._conf_name):
             update = True
         with open(self._conf_name, 'wb') as f:
-            json.dump(config, f, indent=4)
+            ext_json.dump(config, f, indent=4)
             f.write('\n')
 
         if update:
@@ -146,7 +147,7 @@
         config = {}
         try:
             with open(self._conf_name, 'rb') as conf:
-                config = json.load(conf)
+                config = ext_json.load(conf)
         except IOError as e:
             sys.stderr.write(str(e) + '\n')
 
@@ -159,7 +160,7 @@
         """
         try:
             with open(self._conf_name, 'rb') as conf:
-                return json.load(conf)
+                return ext_json.load(conf)
         except IOError as e:
             #sys.stderr.write(str(e) + '\n')
             pass
--- a/kallithea/bin/kallithea_api.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/bin/kallithea_api.py	Mon May 04 19:24:04 2020 +0200
@@ -25,12 +25,11 @@
 :license: GPLv3, see LICENSE.md for more details.
 """
 
-from __future__ import print_function
-
 import argparse
+import json
 import sys
 
-from kallithea.bin.base import FORMAT_JSON, FORMAT_PRETTY, RcConf, api_call, json
+from kallithea.bin.base import FORMAT_JSON, FORMAT_PRETTY, RcConf, api_call
 
 
 def argparser(argv):
@@ -60,7 +59,7 @@
                  'be also `%s`' % (FORMAT_PRETTY, FORMAT_JSON),
             default=FORMAT_PRETTY
     )
-    args, other = parser.parse_known_args()
+    args, other = parser.parse_known_args(args=argv[1:])
     return parser, args, other
 
 
@@ -101,7 +100,7 @@
         parser.error('Please specify method name')
 
     try:
-        margs = dict(map(lambda s: s.split(':', 1), other))
+        margs = dict(s.split(':', 1) for s in other)
     except ValueError:
         sys.stderr.write('Error parsing arguments \n')
         sys.exit()
--- a/kallithea/bin/kallithea_cli.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/bin/kallithea_cli.py	Mon May 04 19:24:04 2020 +0200
@@ -25,3 +25,8 @@
 import kallithea.bin.kallithea_cli_ssh
 # 'cli' is the main entry point for 'kallithea-cli', specified in setup.py as entry_points console_scripts
 from kallithea.bin.kallithea_cli_base import cli
+
+
+# mute pyflakes "imported but unused"
+assert kallithea.bin.kallithea_cli_ssh
+assert cli
--- a/kallithea/bin/kallithea_cli_base.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/bin/kallithea_cli_base.py	Mon May 04 19:24:04 2020 +0200
@@ -12,7 +12,7 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-import cStringIO
+import configparser
 import functools
 import logging.config
 import os
@@ -23,6 +23,7 @@
 import paste.deploy
 
 import kallithea
+import kallithea.config.middleware
 
 
 # kallithea_cli is usually invoked through the 'kallithea-cli' wrapper script
@@ -71,11 +72,12 @@
             def runtime_wrapper(config_file, *args, **kwargs):
                 path_to_ini_file = os.path.realpath(config_file)
                 kallithea.CONFIG = paste.deploy.appconfig('config:' + path_to_ini_file)
-                config_bytes = read_config(path_to_ini_file, strip_section_prefix=annotated.__name__)
-                logging.config.fileConfig(cStringIO.StringIO(config_bytes))
+                cp = configparser.ConfigParser(strict=False)
+                cp.read_string(read_config(path_to_ini_file, strip_section_prefix=annotated.__name__))
+                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_without_logging(kallithea.CONFIG.global_conf, **kallithea.CONFIG.local_conf)
-                    kallithea.lib.utils.setup_cache_regions(kallithea.CONFIG)
+                    kallithea.config.middleware.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_celery.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/bin/kallithea_cli_celery.py	Mon May 04 19:24:04 2020 +0200
@@ -12,6 +12,7 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import celery.bin.worker
 import click
 
 import kallithea
@@ -31,10 +32,9 @@
     by this CLI command.
     """
 
-    if not kallithea.CELERY_ON:
+    if not kallithea.CELERY_APP:
         raise Exception('Please set use_celery = true in .ini config '
                         'file before running this command')
 
-    from kallithea.lib import celerypylons
-    cmd = celerypylons.worker.worker(celerypylons.app)
+    cmd = celery.bin.worker.worker(kallithea.CELERY_APP)
     return cmd.run_from_argv(None, command='celery-run -c CONFIG_FILE --', argv=list(celery_args))
--- a/kallithea/bin/kallithea_cli_config.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/bin/kallithea_cli_config.py	Mon May 04 19:24:04 2020 +0200
@@ -89,6 +89,16 @@
     mako_variable_values.update({
         'uuid': lambda: uuid.uuid4().hex,
     })
+
+    click.echo('Creating config file using:')
+    for key, value in inifile.default_variables.items():
+        if isinstance(value, str):
+            options = inifile.variable_options.get(key)
+            if options:
+                click.echo('  %s=%s  (options: %s)' % (key, mako_variable_values.get(key, value), ', '.join(options)))
+            else:
+                click.echo('  %s=%s' % (key, mako_variable_values.get(key, value)))
+
     try:
         config_file_abs = os.path.abspath(config_file)
         inifile.create(config_file_abs, mako_variable_values, ini_settings)
--- a/kallithea/bin/kallithea_cli_db.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/bin/kallithea_cli_db.py	Mon May 04 19:24:04 2020 +0200
@@ -67,7 +67,7 @@
     Session().commit()
 
     # initial repository scan
-    kallithea.config.middleware.make_app_without_logging(
+    kallithea.config.middleware.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_iis.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/bin/kallithea_cli_iis.py	Mon May 04 19:24:04 2020 +0200
@@ -33,7 +33,8 @@
 def __ExtensionFactory__():
     from paste.deploy import loadapp
     from logging.config import fileConfig
-    fileConfig('%(inifile)s')
+    fileConfig('%(inifile)s', {'__file__': '%(inifile)s', 'here': '%(inifiledir)s'})
+
     application = loadapp('config:%(inifile)s')
 
     def app(environ, start_response):
@@ -66,6 +67,7 @@
 
     try:
         import isapi_wsgi
+        assert isapi_wsgi
     except ImportError:
         sys.stderr.write('missing requirement: isapi-wsgi not installed\n')
         sys.exit(1)
@@ -75,6 +77,7 @@
     with open(dispatchfile, 'w') as f:
         f.write(dispath_py_template % {
             'inifile': config_file_abs.replace('\\', '\\\\'),
+            'inifiledir': os.path.dirname(config_file_abs).replace('\\', '\\\\'),
             'virtualdir': virtualdir,
             })
 
--- a/kallithea/bin/kallithea_cli_index.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/bin/kallithea_cli_index.py	Mon May 04 19:24:04 2020 +0200
@@ -36,7 +36,7 @@
 @click.option('--repo-location', help='Base path of repositories to index. Default: all')
 @click.option('--index-only', help='Comma-separated list of repositories to build index on. Default: all')
 @click.option('--update-only', help='Comma-separated list of repositories to re-build index on. Default: all')
-@click.option('-f', '--full', 'full_index', help='Recreate the index from scratch')
+@click.option('-f', '--full/--no-full', 'full_index', help='Recreate the index from scratch')
 def index_create(repo_location, index_only, update_only, full_index):
     """Create or update full text search index"""
 
--- a/kallithea/bin/kallithea_cli_ishell.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/bin/kallithea_cli_ishell.py	Mon May 04 19:24:04 2020 +0200
@@ -20,12 +20,10 @@
 :license: GPLv3, see LICENSE.md for more details.
 """
 
-from __future__ import print_function
-
 import sys
 
 import kallithea.bin.kallithea_cli_base as cli_base
-from kallithea.model.db import *
+from kallithea.model.db import *  # these names will be directly available in the IPython shell
 
 
 @cli_base.register_command(config_file_initialize_app=True)
--- a/kallithea/bin/kallithea_cli_repo.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/bin/kallithea_cli_repo.py	Mon May 04 19:24:04 2020 +0200
@@ -26,10 +26,11 @@
 
 import click
 
+import kallithea
 import kallithea.bin.kallithea_cli_base as cli_base
 from kallithea.lib.utils import REMOVED_REPO_PAT, repo2db_mapper
-from kallithea.lib.utils2 import ask_ok, safe_str, safe_unicode
-from kallithea.model.db import Repository, Ui
+from kallithea.lib.utils2 import ask_ok
+from kallithea.model.db import Repository
 from kallithea.model.meta import Session
 from kallithea.model.scm import ScmModel
 
@@ -74,7 +75,7 @@
     if not repositories:
         repo_list = Repository.query().all()
     else:
-        repo_names = [safe_unicode(n.strip()) for n in repositories]
+        repo_names = [n.strip() for n in repositories]
         repo_list = list(Repository.query()
                         .filter(Repository.repo_name.in_(repo_names)))
 
@@ -110,7 +111,7 @@
             return
         parts = parts.groupdict()
         time_params = {}
-        for (name, param) in parts.iteritems():
+        for name, param in parts.items():
             if param:
                 time_params[name] = int(param)
         return datetime.timedelta(**time_params)
@@ -125,9 +126,9 @@
         date_part = name[4:19]  # 4:19 since we don't parse milliseconds
         return datetime.datetime.strptime(date_part, '%Y%m%d_%H%M%S')
 
-    repos_location = Ui.get_repos_location()
+    repos_location = kallithea.CONFIG['base_path']
     to_remove = []
-    for dn_, dirs, f in os.walk(safe_str(repos_location)):
+    for dn_, dirs, f in os.walk(repos_location):
         alldirs = list(dirs)
         del dirs[:]
         if ('.hg' in alldirs or
@@ -175,9 +176,8 @@
         remove = True
     else:
         remove = ask_ok('The following repositories will be removed completely:\n%s\n'
-                'Do you want to proceed? [y/n] '
-                % '\n'.join(['%s deleted on %s' % (safe_str(x[0]), safe_str(x[1]))
-                                     for x in to_remove]))
+            'Do you want to proceed? [y/n] ' %
+            '\n'.join('%s deleted on %s' % (path, date_) for path, date_ in to_remove))
 
     if remove:
         for path, date_ in to_remove:
--- a/kallithea/bin/kallithea_cli_ssh.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/bin/kallithea_cli_ssh.py	Mon May 04 19:24:04 2020 +0200
@@ -14,7 +14,6 @@
 
 import logging
 import os
-import re
 import shlex
 import sys
 
@@ -25,7 +24,7 @@
 from kallithea.lib.utils2 import str2bool
 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
+from kallithea.model.ssh_key import SshKeyModel, SshKeyModelException
 
 
 log = logging.getLogger(__name__)
@@ -83,5 +82,8 @@
 
     The file is usually maintained automatically, but this command will also re-write it.
     """
-
-    SshKeyModel().write_authorized_keys()
+    try:
+        SshKeyModel().write_authorized_keys()
+    except SshKeyModelException as e:
+        sys.stderr.write("%s\n" % e)
+        sys.exit(1)
--- a/kallithea/bin/kallithea_gist.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/bin/kallithea_gist.py	Mon May 04 19:24:04 2020 +0200
@@ -25,15 +25,14 @@
 :license: GPLv3, see LICENSE.md for more details.
 """
 
-from __future__ import print_function
-
 import argparse
 import fileinput
+import json
 import os
 import stat
 import sys
 
-from kallithea.bin.base import FORMAT_JSON, FORMAT_PRETTY, RcConf, api_call, json
+from kallithea.bin.base import FORMAT_JSON, FORMAT_PRETTY, RcConf, api_call
 
 
 def argparser(argv):
@@ -69,7 +68,7 @@
                        'be also `%s`' % (FORMAT_PRETTY, FORMAT_JSON),
             default=FORMAT_PRETTY
     )
-    args, other = parser.parse_known_args()
+    args, other = parser.parse_known_args(args=argv[1:])
     return parser, args, other
 
 
--- a/kallithea/bin/ldap_sync.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/bin/ldap_sync.py	Mon May 04 19:24:04 2020 +0200
@@ -25,15 +25,14 @@
 :license: GPLv3, see LICENSE.md for more details.
 """
 
-from __future__ import print_function
-
-import urllib2
+import urllib.request
 import uuid
-from ConfigParser import ConfigParser
+from configparser import ConfigParser
 
 import ldap
 
-from kallithea.lib.compat import json
+from kallithea.lib import ext_json
+from kallithea.lib.utils2 import ascii_bytes
 
 
 config = ConfigParser()
@@ -80,12 +79,12 @@
         uid = str(uuid.uuid1())
         data = self.get_api_data(uid, method, args)
 
-        data = json.dumps(data)
+        data = ascii_bytes(ext_json.dumps(data))
         headers = {'content-type': 'text/plain'}
-        req = urllib2.Request(self.url, data, headers)
+        req = urllib.request.Request(self.url, data, headers)
 
-        response = urllib2.urlopen(req)
-        response = json.load(response)
+        response = urllib.request.urlopen(req)
+        response = ext_json.load(response)
 
         if uid != response["id"]:
             raise InvalidResponseIDError("UUID does not match.")
--- a/kallithea/config/app_cfg.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/config/app_cfg.py	Mon May 04 19:24:04 2020 +0200
@@ -28,20 +28,21 @@
 from alembic.migration import MigrationContext
 from alembic.script.base import ScriptDirectory
 from sqlalchemy import create_engine
-from tg import hooks
 from tg.configuration import AppConfig
 from tg.support.converters import asbool
 
 import kallithea.lib.locale
 import kallithea.model.base
-from kallithea.lib.auth import set_available_permissions
+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, make_ui, set_app_settings, set_indexer_config, set_vcs_config
+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.model import db
 
 
 log = logging.getLogger(__name__)
@@ -98,12 +99,12 @@
         # Disable transaction manager -- currently Kallithea takes care of transactions itself
         self['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()
 
-# TODO still needed as long as we use pylonslib
-sys.modules['pylons'] = tg
-
 # DebugBar, a debug toolbar for TurboGears2.
 # (https://github.com/TurboGears/tgext.debugbar)
 # To enable it, install 'tgext.debugbar' and 'kajiki', and run Kallithea with
@@ -112,6 +113,7 @@
 try:
     from tgext.debugbar import enable_debugbar
     import kajiki # only to check its existence
+    assert kajiki
 except ImportError:
     pass
 else:
@@ -156,15 +158,14 @@
             sys.exit(1)
 
     # store some globals into kallithea
-    kallithea.CELERY_ON = str2bool(config.get('use_celery'))
-    kallithea.CELERY_EAGER = str2bool(config.get('celery.always.eager'))
+    kallithea.DEFAULT_USER_ID = db.User.get_default_user().user_id
+
+    if str2bool(config.get('use_celery')):
+        kallithea.CELERY_APP = celerypylons.make_app()
     kallithea.CONFIG = config
 
     load_rcextensions(root_path=config['here'])
 
-    set_available_permissions(config)
-    repos_path = make_ui().configitems('paths')[0][1]
-    config['base_path'] = repos_path
     set_app_settings(config)
 
     instance_id = kallithea.CONFIG.get('instance_id', '*')
@@ -183,8 +184,10 @@
 
     check_git_version()
 
+    kallithea.model.meta.Session.remove()
 
-hooks.register('configure_new_app', setup_configuration)
+
+tg.hooks.register('configure_new_app', setup_configuration)
 
 
 def setup_application(app):
@@ -208,4 +211,4 @@
     return app
 
 
-hooks.register('before_config', setup_application)
+tg.hooks.register('before_config', setup_application)
--- a/kallithea/config/conf.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/config/conf.py	Mon May 04 19:24:04 2020 +0200
@@ -35,7 +35,7 @@
 # Whoosh index targets
 
 # Extensions we want to index content of using whoosh
-INDEX_EXTENSIONS = LANGUAGES_EXTENSIONS_MAP.keys()
+INDEX_EXTENSIONS = list(LANGUAGES_EXTENSIONS_MAP)
 
 # Filenames we want to index content of using whoosh
 INDEX_FILENAMES = pygmentsutils.get_index_filenames()
--- a/kallithea/config/middleware.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/config/middleware.py	Mon May 04 19:24:04 2020 +0200
@@ -13,8 +13,6 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 """WSGI middleware initialization for the Kallithea application."""
 
-import logging.config
-
 from kallithea.config.app_cfg import base_config
 from kallithea.config.environment import load_environment
 
@@ -26,11 +24,6 @@
 make_base_app = base_config.setup_tg_wsgi_app(load_environment)
 
 
-def make_app_without_logging(global_conf, full_stack=True, **app_conf):
-    """The core of make_app for use from gearbox commands (other than 'serve')"""
-    return make_base_app(global_conf, full_stack=full_stack, **app_conf)
-
-
 def make_app(global_conf, full_stack=True, **app_conf):
     """
     Set up Kallithea with the settings found in the PasteDeploy configuration
@@ -49,5 +42,6 @@
     ``app_conf`` contains all the application-specific settings (those defined
     under ``[app:main]``.
     """
-    logging.config.fileConfig(global_conf['__file__'])
-    return make_app_without_logging(global_conf, full_stack=full_stack, **app_conf)
+    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/config/routing.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/config/routing.py	Mon May 04 19:24:04 2020 +0200
@@ -19,14 +19,34 @@
 refer to the routes manual at http://routes.groovie.org/docs/
 """
 
-from routes import Mapper
+import routes
 from tg import request
 
+from kallithea.lib.utils2 import safe_str
+
 
 # prefix for non repository related links needs to be prefixed with `/`
 ADMIN_PREFIX = '/_admin'
 
 
+class Mapper(routes.Mapper):
+    """
+    Subclassed Mapper with routematch patched to decode "unicode" str url to
+    *real* unicode str before applying matches and invoking controller methods.
+    """
+
+    def routematch(self, url=None, environ=None):
+        """
+        routematch that also decode url from "fake bytes" to real unicode
+        string before matching and invoking controllers.
+        """
+        # Process url like get_path_info does ... but PATH_INFO has already
+        # been retrieved from environ and is passed, so - let's just use that
+        # instead.
+        url = safe_str(url.encode('latin1'))
+        return super().routematch(url=url, environ=environ)
+
+
 def make_map(config):
     """Create, configure and return the routes Mapper"""
     rmap = Mapper(directory=config['paths']['controllers'],
@@ -86,7 +106,7 @@
     #==========================================================================
 
     # MAIN PAGE
-    rmap.connect('home', '/', controller='home', action='index')
+    rmap.connect('home', '/', controller='home')
     rmap.connect('about', '/about', controller='home', action='about')
     rmap.redirect('/favicon.ico', '/images/favicon.ico')
     rmap.connect('repo_switcher_data', '/_repos', controller='home',
@@ -106,7 +126,7 @@
         m.connect("repos", "/repos",
                   action="create", conditions=dict(method=["POST"]))
         m.connect("repos", "/repos",
-                  action="index", conditions=dict(method=["GET"]))
+                  conditions=dict(method=["GET"]))
         m.connect("new_repo", "/create_repository",
                   action="create_repository", conditions=dict(method=["GET"]))
         m.connect("update_repo", "/repos/{repo_name:.*?}",
@@ -121,7 +141,7 @@
         m.connect("repos_groups", "/repo_groups",
                   action="create", conditions=dict(method=["POST"]))
         m.connect("repos_groups", "/repo_groups",
-                  action="index", conditions=dict(method=["GET"]))
+                  conditions=dict(method=["GET"]))
         m.connect("new_repos_group", "/repo_groups/new",
                   action="new", conditions=dict(method=["GET"]))
         m.connect("update_repos_group", "/repo_groups/{group_name:.*?}",
@@ -161,9 +181,9 @@
         m.connect("new_user", "/users/new",
                   action="create", conditions=dict(method=["POST"]))
         m.connect("users", "/users",
-                  action="index", conditions=dict(method=["GET"]))
+                  conditions=dict(method=["GET"]))
         m.connect("formatted_users", "/users.{format}",
-                  action="index", conditions=dict(method=["GET"]))
+                  conditions=dict(method=["GET"]))
         m.connect("new_user", "/users/new",
                   action="new", conditions=dict(method=["GET"]))
         m.connect("update_user", "/users/{id}",
@@ -216,7 +236,7 @@
         m.connect("users_groups", "/user_groups",
                   action="create", conditions=dict(method=["POST"]))
         m.connect("users_groups", "/user_groups",
-                  action="index", conditions=dict(method=["GET"]))
+                  conditions=dict(method=["GET"]))
         m.connect("new_users_group", "/user_groups/new",
                   action="new", conditions=dict(method=["GET"]))
         m.connect("update_users_group", "/user_groups/{id}",
@@ -263,8 +283,7 @@
     # ADMIN DEFAULTS ROUTES
     with rmap.submapper(path_prefix=ADMIN_PREFIX,
                         controller='admin/defaults') as m:
-        m.connect('defaults', '/defaults',
-                  action="index")
+        m.connect('defaults', '/defaults')
         m.connect('defaults_update', 'defaults/{id}/update',
                   action="update", conditions=dict(method=["POST"]))
 
@@ -370,7 +389,7 @@
         m.connect("gists", "/gists",
                   action="create", conditions=dict(method=["POST"]))
         m.connect("gists", "/gists",
-                  action="index", conditions=dict(method=["GET"]))
+                  conditions=dict(method=["GET"]))
         m.connect("new_gist", "/gists/new",
                   action="new", conditions=dict(method=["GET"]))
 
@@ -396,7 +415,7 @@
     # ADMIN MAIN PAGES
     with rmap.submapper(path_prefix=ADMIN_PREFIX,
                         controller='admin/admin') as m:
-        m.connect('admin_home', '', action='index')
+        m.connect('admin_home', '')
         m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9. _-]*}',
                   action='add_repo')
     #==========================================================================
@@ -408,7 +427,7 @@
 
     # USER JOURNAL
     rmap.connect('journal', '%s/journal' % ADMIN_PREFIX,
-                 controller='journal', action='index')
+                 controller='journal')
     rmap.connect('journal_rss', '%s/journal/rss' % ADMIN_PREFIX,
                  controller='journal', action='journal_rss')
     rmap.connect('journal_atom', '%s/journal/atom' % ADMIN_PREFIX,
@@ -475,7 +494,7 @@
     #==========================================================================
     rmap.connect('repo_creating_home', '/{repo_name:.*?}/repo_creating',
                 controller='admin/repos', action='repo_creating')
-    rmap.connect('repo_check_home', '/{repo_name:.*?}/crepo_check',
+    rmap.connect('repo_check_home', '/{repo_name:.*?}/repo_check_creating',
                 controller='admin/repos', action='repo_check')
 
     rmap.connect('summary_home', '/{repo_name:.*?}',
@@ -544,13 +563,6 @@
                  controller='admin/repos', action="edit_advanced_fork",
                  conditions=dict(method=["POST"], function=check_repo))
 
-    rmap.connect("edit_repo_caches", "/{repo_name:.*?}/settings/caches",
-                 controller='admin/repos', action="edit_caches",
-                 conditions=dict(method=["GET"], function=check_repo))
-    rmap.connect("update_repo_caches", "/{repo_name:.*?}/settings/caches",
-                 controller='admin/repos', action="edit_caches",
-                 conditions=dict(method=["POST"], function=check_repo))
-
     rmap.connect("edit_repo_remote", "/{repo_name:.*?}/settings/remote",
                  controller='admin/repos', action="edit_remote",
                  conditions=dict(method=["GET"], function=check_repo))
@@ -602,7 +614,7 @@
 
     rmap.connect('compare_home',
                  '/{repo_name:.*?}/compare',
-                 controller='compare', action='index',
+                 controller='compare',
                  conditions=dict(function=check_repo))
 
     rmap.connect('compare_url',
@@ -616,7 +628,7 @@
 
     rmap.connect('pullrequest_home',
                  '/{repo_name:.*?}/pull-request/new', controller='pullrequests',
-                 action='index', conditions=dict(function=check_repo,
+                 conditions=dict(function=check_repo,
                                                  method=["GET"]))
 
     rmap.connect('pullrequest_repo_info',
@@ -674,7 +686,7 @@
                 controller='changelog', conditions=dict(function=check_repo))
 
     rmap.connect('changelog_file_home', '/{repo_name:.*?}/changelog/{revision}/{f_path:.*}',
-                controller='changelog', f_path=None,
+                controller='changelog',
                 conditions=dict(function=check_repo))
 
     rmap.connect('changelog_details', '/{repo_name:.*?}/changelog_details/{cs}',
@@ -719,8 +731,8 @@
 
     rmap.connect('files_annotate_home',
                  '/{repo_name:.*?}/annotate/{revision}/{f_path:.*}',
-                 controller='files', action='index', revision='tip',
-                 f_path='', annotate=True, conditions=dict(function=check_repo))
+                 controller='files', revision='tip',
+                 f_path='', annotate='1', conditions=dict(function=check_repo))
 
     rmap.connect('files_edit_home',
                  '/{repo_name:.*?}/edit/{revision}/{f_path:.*}',
--- a/kallithea/controllers/admin/admin.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/controllers/admin/admin.py	Mon May 04 19:24:04 2020 +0200
@@ -36,7 +36,6 @@
 from whoosh.qparser.dateparse import DateParserPlugin
 from whoosh.qparser.default import QueryParser
 
-from kallithea.config.routing import url
 from kallithea.lib.auth import HasPermissionAnyDecorator, LoginRequired
 from kallithea.lib.base import BaseController, render
 from kallithea.lib.indexers import JOURNAL_SCHEMA
@@ -61,7 +60,7 @@
     if search_term:
         qp = QueryParser('repository', schema=JOURNAL_SCHEMA)
         qp.add_plugin(DateParserPlugin())
-        qry = qp.parse(unicode(search_term))
+        qry = qp.parse(search_term)
         log.debug('Filtering using parsed query %r', qry)
 
     def wildcard_handler(col, wc_term):
@@ -139,10 +138,8 @@
 
         p = safe_int(request.GET.get('page'), 1)
 
-        def url_generator(**kw):
-            return url.current(filter=c.search_term, **kw)
-
-        c.users_log = Page(users_log, page=p, items_per_page=10, url=url_generator)
+        c.users_log = Page(users_log, page=p, items_per_page=10,
+                           filter=c.search_term)
 
         if request.environ.get('HTTP_X_PARTIAL_XHR'):
             return render('admin/admin_log.html')
--- a/kallithea/controllers/admin/auth_settings.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/controllers/admin/auth_settings.py	Mon May 04 19:24:04 2020 +0200
@@ -37,7 +37,6 @@
 from kallithea.lib import helpers as h
 from kallithea.lib.auth import HasPermissionAnyDecorator, LoginRequired
 from kallithea.lib.base import BaseController, render
-from kallithea.lib.compat import formatted_json
 from kallithea.model.db import Setting
 from kallithea.model.forms import AuthSettingsForm
 from kallithea.model.meta import Session
@@ -87,7 +86,7 @@
         # we want to show , separated list of enabled plugins
         c.defaults['auth_plugins'] = ','.join(c.enabled_plugin_names)
 
-        log.debug(formatted_json(defaults))
+        log.debug('defaults: %s', defaults)
         return formencode.htmlfill.render(
             render('admin/auth/auth_settings.html'),
             defaults=c.defaults,
@@ -103,7 +102,7 @@
     def auth_settings(self):
         """POST create and store auth settings"""
         self.__load_defaults()
-        log.debug("POST Result: %s", formatted_json(dict(request.POST)))
+        log.debug("POST Result: %s", dict(request.POST))
 
         # First, parse only the plugin list (not the plugin settings).
         _auth_plugins_validator = AuthSettingsForm([]).fields['auth_plugins']
--- a/kallithea/controllers/admin/defaults.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/controllers/admin/defaults.py	Mon May 04 19:24:04 2020 +0200
@@ -31,7 +31,6 @@
 import formencode
 from formencode import htmlfill
 from tg import request
-from tg import tmpl_context as c
 from tg.i18n import ugettext as _
 from webob.exc import HTTPFound
 
@@ -69,7 +68,7 @@
 
         try:
             form_result = _form.to_python(dict(request.POST))
-            for k, v in form_result.iteritems():
+            for k, v in form_result.items():
                 setting = Setting.create_or_update(k, v)
             Session().commit()
             h.flash(_('Default settings updated successfully'),
--- a/kallithea/controllers/admin/gists.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/controllers/admin/gists.py	Mon May 04 19:24:04 2020 +0200
@@ -40,7 +40,7 @@
 from kallithea.lib.auth import LoginRequired
 from kallithea.lib.base import BaseController, jsonify, render
 from kallithea.lib.page import Page
-from kallithea.lib.utils2 import safe_int, safe_unicode, time_to_datetime
+from kallithea.lib.utils2 import safe_int, safe_str, time_to_datetime
 from kallithea.lib.vcs.exceptions import NodeNotChangedError, VCSError
 from kallithea.model.db import Gist
 from kallithea.model.forms import GistForm
@@ -71,6 +71,11 @@
         not_default_user = not request.authuser.is_default_user
         c.show_private = request.GET.get('private') and not_default_user
         c.show_public = request.GET.get('public') and not_default_user
+        url_params = {}
+        if c.show_public:
+            url_params['public'] = 1
+        elif c.show_private:
+            url_params['private'] = 1
 
         gists = Gist().query() \
             .filter_by(is_expired=False) \
@@ -97,7 +102,8 @@
 
         c.gists = gists
         p = safe_int(request.GET.get('page'), 1)
-        c.gists_pager = Page(c.gists, page=p, items_per_page=10)
+        c.gists_pager = Page(c.gists, page=p, items_per_page=10,
+                             **url_params)
         return render('admin/gists/index.html')
 
     @LoginRequired()
@@ -176,7 +182,10 @@
             log.error(traceback.format_exc())
             raise HTTPNotFound()
         if format == 'raw':
-            content = '\n\n'.join([f.content for f in c.files if (f_path is None or safe_unicode(f.path) == f_path)])
+            content = '\n\n'.join(
+                safe_str(f.content)
+                for f in c.files if (f_path is None or f.path == f_path)
+            )
             response.content_type = 'text/plain'
             return content
         return render('admin/gists/show.html')
--- a/kallithea/controllers/admin/my_account.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/controllers/admin/my_account.py	Mon May 04 19:24:04 2020 +0200
@@ -279,18 +279,18 @@
             Session().commit()
             SshKeyModel().write_authorized_keys()
             h.flash(_("SSH key %s successfully added") % new_ssh_key.fingerprint, category='success')
-        except SshKeyModelException as errors:
-            h.flash(errors.message, category='error')
+        except SshKeyModelException as e:
+            h.flash(e.args[0], category='error')
         raise HTTPFound(location=url('my_account_ssh_keys'))
 
     @IfSshEnabled
     def my_account_ssh_keys_delete(self):
-        public_key = request.POST.get('del_public_key')
+        fingerprint = request.POST.get('del_public_key_fingerprint')
         try:
-            SshKeyModel().delete(public_key, request.authuser.user_id)
+            SshKeyModel().delete(fingerprint, request.authuser.user_id)
             Session().commit()
             SshKeyModel().write_authorized_keys()
             h.flash(_("SSH key successfully deleted"), category='success')
-        except SshKeyModelException as errors:
-            h.flash(errors.message, category='error')
+        except SshKeyModelException as e:
+            h.flash(e.args[0], category='error')
         raise HTTPFound(location=url('my_account_ssh_keys'))
--- a/kallithea/controllers/admin/repo_groups.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/controllers/admin/repo_groups.py	Mon May 04 19:24:04 2020 +0200
@@ -25,7 +25,6 @@
 :license: GPLv3, see LICENSE.md for more details.
 """
 
-import itertools
 import logging
 import traceback
 
@@ -37,7 +36,6 @@
 from tg.i18n import ungettext
 from webob.exc import HTTPForbidden, HTTPFound, HTTPInternalServerError, HTTPNotFound
 
-import kallithea
 from kallithea.config.routing import url
 from kallithea.lib import helpers as h
 from kallithea.lib.auth import HasPermissionAny, HasRepoGroupPermissionLevel, HasRepoGroupPermissionLevelDecorator, LoginRequired
@@ -93,10 +91,8 @@
         return data
 
     def _revoke_perms_on_yourself(self, form_result):
-        _up = filter(lambda u: request.authuser.username == u[0],
-                     form_result['perms_updates'])
-        _new = filter(lambda u: request.authuser.username == u[0],
-                      form_result['perms_new'])
+        _up = [u for u in form_result['perms_updates'] if request.authuser.username == u[0]]
+        _new = [u for u in form_result['perms_new'] if request.authuser.username == u[0]]
         if _new and _new[0][1] != 'group.admin' or _up and _up[0][1] != 'group.admin':
             return True
         return False
@@ -105,24 +101,20 @@
         _list = RepoGroup.query(sorted=True).all()
         group_iter = RepoGroupList(_list, perm_level='admin')
         repo_groups_data = []
-        total_records = len(group_iter)
         _tmpl_lookup = app_globals.mako_lookup
         template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
 
-        repo_group_name = lambda repo_group_name, children_groups: (
-            template.get_def("repo_group_name")
-            .render(repo_group_name, children_groups, _=_, h=h, c=c)
-        )
-        repo_group_actions = lambda repo_group_id, repo_group_name, gr_count: (
-            template.get_def("repo_group_actions")
-            .render(repo_group_id, repo_group_name, gr_count, _=_, h=h, c=c,
-                    ungettext=ungettext)
-        )
+        def repo_group_name(repo_group_name, children_groups):
+            return template.get_def("repo_group_name") \
+                .render_unicode(repo_group_name, children_groups, _=_, h=h, c=c)
+
+        def repo_group_actions(repo_group_id, repo_group_name, gr_count):
+            return template.get_def("repo_group_actions") \
+                .render_unicode(repo_group_id, repo_group_name, gr_count, _=_, h=h, c=c,
+                        ungettext=ungettext)
 
         for repo_gr in group_iter:
-            children_groups = map(h.safe_unicode,
-                itertools.chain((g.name for g in repo_gr.parents),
-                                (x.name for x in [repo_gr])))
+            children_groups = [g.name for g in repo_gr.parents] + [repo_gr.name]
             repo_count = repo_gr.repositories.count()
             repo_groups_data.append({
                 "raw_name": repo_gr.group_name,
@@ -148,6 +140,7 @@
         # permissions for can create group based on parent_id are checked
         # here in the Form
         repo_group_form = RepoGroupForm(repo_groups=c.repo_groups)
+        form_result = None
         try:
             form_result = repo_group_form.to_python(dict(request.POST))
             gr = RepoGroupModel().create(
@@ -171,6 +164,8 @@
             log.error(traceback.format_exc())
             h.flash(_('Error occurred during creation of repository group %s')
                     % request.POST.get('group_name'), category='error')
+            if form_result is None:
+                raise
             parent_group_id = form_result['parent_group_id']
             # TODO: maybe we should get back to the main view, not the admin one
             raise HTTPFound(location=url('repos_groups', parent_group=parent_group_id))
--- a/kallithea/controllers/admin/repos.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/controllers/admin/repos.py	Mon May 04 19:24:04 2020 +0200
@@ -28,6 +28,7 @@
 import logging
 import traceback
 
+import celery.result
 import formencode
 from formencode import htmlfill
 from tg import request
@@ -35,6 +36,7 @@
 from tg.i18n import ugettext as _
 from webob.exc import HTTPForbidden, HTTPFound, HTTPInternalServerError, HTTPNotFound
 
+import kallithea
 from kallithea.config.routing import url
 from kallithea.lib import helpers as h
 from kallithea.lib.auth import HasPermissionAny, HasRepoPermissionLevelDecorator, LoginRequired, NotAnonymous
@@ -43,7 +45,7 @@
 from kallithea.lib.utils import action_logger
 from kallithea.lib.utils2 import safe_int
 from kallithea.lib.vcs import RepositoryError
-from kallithea.model.db import RepoGroup, Repository, RepositoryField, Setting, User, UserFollowing
+from kallithea.model.db import RepoGroup, Repository, RepositoryField, Setting, UserFollowing
 from kallithea.model.forms import RepoFieldForm, RepoForm, RepoPermsForm
 from kallithea.model.meta import Session
 from kallithea.model.repo import RepoModel
@@ -110,17 +112,11 @@
     @NotAnonymous()
     def create(self):
         self.__load_defaults()
-        form_result = {}
         try:
             # CanWriteGroup validators checks permissions of this POST
             form_result = RepoForm(repo_groups=c.repo_groups,
                                    landing_revs=c.landing_revs_choices)() \
                             .to_python(dict(request.POST))
-
-            # create is done sometimes async on celery, db transaction
-            # management is handled there.
-            task = RepoModel().create(form_result, request.authuser.user_id)
-            task_id = task.task_id
         except formencode.Invalid as errors:
             log.info(errors)
             return htmlfill.render(
@@ -131,6 +127,11 @@
                 force_defaults=False,
                 encoding="UTF-8")
 
+        try:
+            # create is done sometimes async on celery, db transaction
+            # management is handled there.
+            task = RepoModel().create(form_result, request.authuser.user_id)
+            task_id = task.task_id
         except Exception:
             log.error(traceback.format_exc())
             msg = (_('Error creating repository %s')
@@ -181,12 +182,10 @@
         task_id = request.GET.get('task_id')
 
         if task_id and task_id not in ['None']:
-            from kallithea import CELERY_ON
-            from kallithea.lib import celerypylons
-            if CELERY_ON:
-                task = celerypylons.result.AsyncResult(task_id)
-                if task.failed():
-                    raise HTTPInternalServerError(task.traceback)
+            if kallithea.CELERY_APP:
+                task_result = celery.result.AsyncResult(task_id, app=kallithea.CELERY_APP)
+                if task_result.failed():
+                    raise HTTPInternalServerError(task_result.traceback)
 
         repo = Repository.get_by_repo_name(repo_name)
         if repo and repo.repo_state == Repository.STATE_CREATED:
@@ -406,7 +405,7 @@
     @HasRepoPermissionLevelDecorator('admin')
     def edit_advanced(self, repo_name):
         c.repo_info = self._load_repo()
-        c.default_user_id = User.get_default_user().user_id
+        c.default_user_id = kallithea.DEFAULT_USER_ID
         c.in_public_journal = UserFollowing.query() \
             .filter(UserFollowing.user_id == c.default_user_id) \
             .filter(UserFollowing.follows_repository == c.repo_info).scalar()
@@ -443,7 +442,7 @@
 
         try:
             repo_id = Repository.get_by_repo_name(repo_name).repo_id
-            user_id = User.get_default_user().user_id
+            user_id = kallithea.DEFAULT_USER_ID
             self.scm_model.toggle_following_repo(repo_id, user_id)
             h.flash(_('Updated repository visibility in public journal'),
                     category='success')
@@ -471,7 +470,7 @@
                     category='success')
         except RepositoryError as e:
             log.error(traceback.format_exc())
-            h.flash(str(e), category='error')
+            h.flash(e, category='error')
         except Exception as e:
             log.error(traceback.format_exc())
             h.flash(_('An error occurred during this operation'),
@@ -480,24 +479,6 @@
         raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name))
 
     @HasRepoPermissionLevelDecorator('admin')
-    def edit_caches(self, repo_name):
-        c.repo_info = self._load_repo()
-        c.active = 'caches'
-        if request.POST:
-            try:
-                ScmModel().mark_for_invalidation(repo_name)
-                Session().commit()
-                h.flash(_('Cache invalidation successful'),
-                        category='success')
-            except Exception as e:
-                log.error(traceback.format_exc())
-                h.flash(_('An error occurred during cache invalidation'),
-                        category='error')
-
-            raise HTTPFound(location=url('edit_repo_caches', repo_name=c.repo_name))
-        return render('admin/repos/repo_edit.html')
-
-    @HasRepoPermissionLevelDecorator('admin')
     def edit_remote(self, repo_name):
         c.repo_info = self._load_repo()
         c.active = 'remote'
--- a/kallithea/controllers/admin/settings.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/controllers/admin/settings.py	Mon May 04 19:24:04 2020 +0200
@@ -42,7 +42,7 @@
 from kallithea.lib.celerylib import tasks
 from kallithea.lib.exceptions import HgsubversionImportError
 from kallithea.lib.utils import repo2db_mapper, set_app_settings
-from kallithea.lib.utils2 import safe_unicode
+from kallithea.lib.utils2 import safe_str
 from kallithea.lib.vcs import VCSError
 from kallithea.model.db import Repository, Setting, Ui
 from kallithea.model.forms import ApplicationSettingsForm, ApplicationUiSettingsForm, ApplicationVisualisationForm
@@ -120,6 +120,7 @@
                 if sett.ui_active:
                     try:
                         import hgsubversion  # pragma: no cover
+                        assert hgsubversion
                     except ImportError:
                         raise HgsubversionImportError
 
@@ -168,10 +169,10 @@
                                             user=request.authuser.username,
                                             overwrite_git_hooks=overwrite_git_hooks)
             added_msg = h.HTML(', ').join(
-                h.link_to(safe_unicode(repo_name), h.url('summary_home', repo_name=repo_name)) for repo_name in added
+                h.link_to(safe_str(repo_name), h.url('summary_home', repo_name=repo_name)) for repo_name in added
             ) or '-'
             removed_msg = h.HTML(', ').join(
-                safe_unicode(repo_name) for repo_name in removed
+                safe_str(repo_name) for repo_name in removed
             ) or '-'
             h.flash(h.HTML(_('Repositories successfully rescanned. Added: %s. Removed: %s.')) %
                     (added_msg, removed_msg), category='success')
@@ -423,7 +424,7 @@
         import kallithea
         c.ini = kallithea.CONFIG
         server_info = Setting.get_server_info()
-        for key, val in server_info.iteritems():
+        for key, val in server_info.items():
             setattr(c, key, val)
 
         return htmlfill.render(
--- a/kallithea/controllers/admin/user_groups.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/controllers/admin/user_groups.py	Mon May 04 19:24:04 2020 +0200
@@ -32,19 +32,18 @@
 from formencode import htmlfill
 from sqlalchemy.orm import joinedload
 from sqlalchemy.sql.expression import func
-from tg import app_globals, config, request
+from tg import app_globals, request
 from tg import tmpl_context as c
 from tg.i18n import ugettext as _
 from webob.exc import HTTPFound, HTTPInternalServerError
 
-import kallithea
 from kallithea.config.routing import url
 from kallithea.lib import helpers as h
 from kallithea.lib.auth import HasPermissionAnyDecorator, HasUserGroupPermissionLevelDecorator, LoginRequired
 from kallithea.lib.base import BaseController, render
 from kallithea.lib.exceptions import RepoGroupAssignmentError, UserGroupsAssignedException
 from kallithea.lib.utils import action_logger
-from kallithea.lib.utils2 import safe_int, safe_unicode
+from kallithea.lib.utils2 import safe_int, safe_str
 from kallithea.model.db import User, UserGroup, UserGroupRepoGroupToPerm, UserGroupRepoToPerm, UserGroupToPerm
 from kallithea.model.forms import CustomDefaultPermissionsForm, UserGroupForm, UserGroupPermsForm
 from kallithea.model.meta import Session
@@ -61,7 +60,6 @@
     @LoginRequired(allow_default_user=True)
     def _before(self, *args, **kwargs):
         super(UserGroupsController, self)._before(*args, **kwargs)
-        c.available_permissions = config['available_permissions']
 
     def __load_data(self, user_group_id):
         c.group_members_obj = sorted((x.user for x in c.user_group.members),
@@ -88,20 +86,18 @@
                         .all()
         group_iter = UserGroupList(_list, perm_level='admin')
         user_groups_data = []
-        total_records = len(group_iter)
         _tmpl_lookup = app_globals.mako_lookup
         template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
 
-        user_group_name = lambda user_group_id, user_group_name: (
-            template.get_def("user_group_name")
-            .render(user_group_id, user_group_name, _=_, h=h, c=c)
-        )
-        user_group_actions = lambda user_group_id, user_group_name: (
-            template.get_def("user_group_actions")
-            .render(user_group_id, user_group_name, _=_, h=h, c=c)
-        )
+        def user_group_name(user_group_id, user_group_name):
+            return template.get_def("user_group_name") \
+                .render_unicode(user_group_id, user_group_name, _=_, h=h, c=c)
+
+        def user_group_actions(user_group_id, user_group_name):
+            return template.get_def("user_group_actions") \
+                .render_unicode(user_group_id, user_group_name, _=_, h=h, c=c)
+
         for user_gr in group_iter:
-
             user_groups_data.append({
                 "raw_name": user_gr.users_group_name,
                 "group_name": user_group_name(user_gr.users_group_id,
@@ -163,7 +159,7 @@
         c.active = 'settings'
         self.__load_data(id)
 
-        available_members = [safe_unicode(x[0]) for x in c.available_members]
+        available_members = [safe_str(x[0]) for x in c.available_members]
 
         users_group_form = UserGroupForm(edit=True,
                                          old_data=c.user_group.get_dict(),
--- a/kallithea/controllers/admin/users.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/controllers/admin/users.py	Mon May 04 19:24:04 2020 +0200
@@ -31,7 +31,7 @@
 import formencode
 from formencode import htmlfill
 from sqlalchemy.sql.expression import func
-from tg import app_globals, config, request
+from tg import app_globals, request
 from tg import tmpl_context as c
 from tg.i18n import ugettext as _
 from webob.exc import HTTPFound, HTTPNotFound
@@ -63,7 +63,6 @@
     @HasPermissionAnyDecorator('hg.admin')
     def _before(self, *args, **kwargs):
         super(UsersController, self)._before(*args, **kwargs)
-        c.available_permissions = config['available_permissions']
 
     def index(self, format='html'):
         c.users_list = User.query().order_by(User.username) \
@@ -72,19 +71,18 @@
                         .all()
 
         users_data = []
-        total_records = len(c.users_list)
         _tmpl_lookup = app_globals.mako_lookup
         template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
 
         grav_tmpl = '<div class="gravatar">%s</div>'
 
-        username = lambda user_id, username: (
-                template.get_def("user_name")
-                .render(user_id, username, _=_, h=h, c=c))
+        def username(user_id, username):
+            return template.get_def("user_name") \
+                .render_unicode(user_id, username, _=_, h=h, c=c)
 
-        user_actions = lambda user_id, username: (
-                template.get_def("user_actions")
-                .render(user_id, username, _=_, h=h, c=c))
+        def user_actions(user_id, username):
+            return template.get_def("user_actions") \
+                .render_unicode(user_id, username, _=_, h=h, c=c)
 
         for user in c.users_list:
             users_data.append({
@@ -390,7 +388,7 @@
             .filter(UserIpMap.user == c.user).all()
 
         c.default_user_ip_map = UserIpMap.query() \
-            .filter(UserIpMap.user == User.get_default_user()).all()
+            .filter(UserIpMap.user_id == kallithea.DEFAULT_USER_ID).all()
 
         defaults = c.user.get_dict()
         return htmlfill.render(
@@ -454,20 +452,20 @@
             Session().commit()
             SshKeyModel().write_authorized_keys()
             h.flash(_("SSH key %s successfully added") % new_ssh_key.fingerprint, category='success')
-        except SshKeyModelException as errors:
-            h.flash(errors.message, category='error')
+        except SshKeyModelException as e:
+            h.flash(e.args[0], category='error')
         raise HTTPFound(location=url('edit_user_ssh_keys', id=c.user.user_id))
 
     @IfSshEnabled
     def ssh_keys_delete(self, id):
         c.user = self._get_user_or_raise_if_default(id)
 
-        public_key = request.POST.get('del_public_key')
+        fingerprint = request.POST.get('del_public_key_fingerprint')
         try:
-            SshKeyModel().delete(public_key, c.user.user_id)
+            SshKeyModel().delete(fingerprint, c.user.user_id)
             Session().commit()
             SshKeyModel().write_authorized_keys()
             h.flash(_("SSH key successfully deleted"), category='success')
-        except SshKeyModelException as errors:
-            h.flash(errors.message, category='error')
+        except SshKeyModelException as e:
+            h.flash(e.args[0], category='error')
         raise HTTPFound(location=url('edit_user_ssh_keys', id=c.user.user_id))
--- a/kallithea/controllers/api/__init__.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/controllers/api/__init__.py	Mon May 04 19:24:04 2020 +0200
@@ -35,11 +35,11 @@
 from tg import Response, TGController, request, response
 from webob.exc import HTTPError, HTTPException
 
+from kallithea.lib import ext_json
 from kallithea.lib.auth import AuthUser
-from kallithea.lib.base import _get_access_path
 from kallithea.lib.base import _get_ip_addr as _get_ip
-from kallithea.lib.compat import json
-from kallithea.lib.utils2 import safe_str, safe_unicode
+from kallithea.lib.base import get_path_info
+from kallithea.lib.utils2 import ascii_bytes
 from kallithea.model.db import User
 
 
@@ -53,7 +53,7 @@
         super(JSONRPCError, self).__init__()
 
     def __str__(self):
-        return safe_str(self.message)
+        return self.message
 
 
 class JSONRPCErrorResponse(Response, HTTPException):
@@ -121,7 +121,7 @@
         raw_body = environ['wsgi.input'].read(length)
 
         try:
-            json_body = json.loads(raw_body)
+            json_body = ext_json.loads(raw_body)
         except ValueError as e:
             # catch JSON errors Here
             raise JSONRPCErrorResponse(retid=self._req_id,
@@ -166,13 +166,13 @@
 
         # now that we have a method, add self._req_params to
         # self.kargs and dispatch control to WGIController
-        argspec = inspect.getargspec(self._func)
-        arglist = argspec[0][1:]
-        defaults = map(type, argspec[3] or [])
-        default_empty = types.NotImplementedType
+        argspec = inspect.getfullargspec(self._func)
+        arglist = argspec.args[1:]
+        argtypes = [type(arg) for arg in argspec.defaults or []]
+        default_empty = type(NotImplemented)
 
         # kw arguments required by this method
-        func_kwargs = dict(itertools.izip_longest(reversed(arglist), reversed(defaults),
+        func_kwargs = dict(itertools.zip_longest(reversed(arglist), reversed(argtypes),
                                                   fillvalue=default_empty))
 
         # This attribute will need to be first param of a method that uses
@@ -180,7 +180,7 @@
         USER_SESSION_ATTR = 'apiuser'
 
         # get our arglist and check if we provided them as args
-        for arg, default in func_kwargs.iteritems():
+        for arg, default in func_kwargs.items():
             if arg == USER_SESSION_ATTR:
                 # USER_SESSION_ATTR is something translated from API key and
                 # this is checked before so we don't need validate it
@@ -209,7 +209,7 @@
 
         log.info('IP: %s Request to %s time: %.3fs' % (
             self._get_ip_addr(environ),
-            safe_unicode(_get_access_path(environ)), time.time() - start)
+            get_path_info(environ), time.time() - start)
         )
 
         state.set_action(self._rpc_call, [])
@@ -226,28 +226,28 @@
             if isinstance(raw_response, HTTPError):
                 self._error = str(raw_response)
         except JSONRPCError as e:
-            self._error = safe_str(e)
+            self._error = str(e)
         except Exception as e:
             log.error('Encountered unhandled exception: %s',
                       traceback.format_exc(),)
             json_exc = JSONRPCError('Internal server error')
-            self._error = safe_str(json_exc)
+            self._error = str(json_exc)
 
         if self._error is not None:
             raw_response = None
 
         response = dict(id=self._req_id, result=raw_response, error=self._error)
         try:
-            return json.dumps(response)
+            return ascii_bytes(ext_json.dumps(response))
         except TypeError as e:
-            log.error('API FAILED. Error encoding response: %s', e)
-            return json.dumps(
+            log.error('API FAILED. Error encoding response for %s %s: %s\n%s', action, rpc_args, e, traceback.format_exc())
+            return ascii_bytes(ext_json.dumps(
                 dict(
                     id=self._req_id,
                     result=None,
-                    error="Error encoding response"
+                    error="Error encoding response",
                 )
-            )
+            ))
 
     def _find_method(self):
         """
--- a/kallithea/controllers/api/api.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/controllers/api/api.py	Mon May 04 19:24:04 2020 +0200
@@ -32,8 +32,8 @@
 from tg import request
 
 from kallithea.controllers.api import JSONRPCController, JSONRPCError
-from kallithea.lib.auth import (
-    AuthUser, HasPermissionAny, HasPermissionAnyDecorator, HasRepoGroupPermissionLevel, HasRepoPermissionLevel, HasUserGroupPermissionLevel)
+from kallithea.lib.auth import (AuthUser, HasPermissionAny, HasPermissionAnyDecorator, HasRepoGroupPermissionLevel, HasRepoPermissionLevel,
+                                HasUserGroupPermissionLevel)
 from kallithea.lib.exceptions import DefaultUserException, UserGroupsAssignedException
 from kallithea.lib.utils import action_logger, repo2db_mapper
 from kallithea.lib.utils2 import OAttr, Optional
@@ -433,7 +433,7 @@
 
     @HasPermissionAnyDecorator('hg.admin')
     def create_user(self, username, email, password=Optional(''),
-                    firstname=Optional(u''), lastname=Optional(u''),
+                    firstname=Optional(''), lastname=Optional(''),
                     active=Optional(True), admin=Optional(False),
                     extern_type=Optional(User.DEFAULT_AUTH_TYPE),
                     extern_name=Optional('')):
@@ -686,7 +686,7 @@
         ]
 
     @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
-    def create_user_group(self, group_name, description=Optional(u''),
+    def create_user_group(self, group_name, description=Optional(''),
                           owner=Optional(OAttr('apiuser')), active=Optional(True)):
         """
         Creates new user group. This command can be executed only using api_key
@@ -1160,7 +1160,7 @@
             return _map[ret_type]
         except KeyError:
             raise JSONRPCError('ret_type must be one of %s'
-                               % (','.join(_map.keys())))
+                               % (','.join(sorted(_map))))
         except Exception:
             log.error(traceback.format_exc())
             raise JSONRPCError(
@@ -2339,7 +2339,7 @@
                                                  branch_name,
                                                  reverse, max_revisions)]
         except EmptyRepositoryError as e:
-            raise JSONRPCError(e.message)
+            raise JSONRPCError('Repository is empty')
 
     # permission check inside
     def get_changeset(self, repoid, raw_id, with_reviews=Optional(False)):
@@ -2373,7 +2373,7 @@
         return pull_request.get_api_data()
 
     # permission check inside
-    def comment_pullrequest(self, pull_request_id, comment_msg=u'', status=None, close_pr=False):
+    def comment_pullrequest(self, pull_request_id, comment_msg='', status=None, close_pr=False):
         """
         Add comment, close and change status of pull request.
         """
@@ -2400,7 +2400,7 @@
             pull_request=pull_request.pull_request_id,
             f_path=None,
             line_no=None,
-            status_change=(ChangesetStatus.get_status_lbl(status)),
+            status_change=ChangesetStatus.get_status_lbl(status),
             closing_pr=close_pr
         )
         action_logger(apiuser,
--- a/kallithea/controllers/changelog.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/controllers/changelog.py	Mon May 04 19:24:04 2020 +0200
@@ -38,8 +38,8 @@
 from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired
 from kallithea.lib.base import BaseRepoController, render
 from kallithea.lib.graphmod import graph_data
-from kallithea.lib.page import RepoPage
-from kallithea.lib.utils2 import safe_int, safe_str
+from kallithea.lib.page import Page
+from kallithea.lib.utils2 import safe_int
 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, ChangesetError, EmptyRepositoryError, NodeDoesNotExistError, RepositoryError
 
 
@@ -67,7 +67,7 @@
             h.flash(_('There are no changesets yet'), category='error')
         except RepositoryError as e:
             log.error(traceback.format_exc())
-            h.flash(safe_str(e), category='error')
+            h.flash(e, category='error')
         raise HTTPBadRequest()
 
     @LoginRequired(allow_default_user=True)
@@ -111,35 +111,34 @@
                         cs = self.__get_cs(revision, repo_name)
                         collection = cs.get_file_history(f_path)
                     except RepositoryError as e:
-                        h.flash(safe_str(e), category='warning')
+                        h.flash(e, category='warning')
                         raise HTTPFound(location=h.url('changelog_home', repo_name=repo_name))
-                collection = list(reversed(collection))
             else:
                 collection = c.db_repo_scm_instance.get_changesets(start=0, end=revision,
-                                                        branch_name=branch_name)
+                                                        branch_name=branch_name, reverse=True)
             c.total_cs = len(collection)
 
-            c.cs_pagination = RepoPage(collection, page=p, item_count=c.total_cs,
-                                    items_per_page=c.size, branch=branch_name,)
+            c.cs_pagination = Page(collection, page=p, item_count=c.total_cs, items_per_page=c.size,
+                                   branch=branch_name)
 
             page_revisions = [x.raw_id for x in c.cs_pagination]
             c.cs_comments = c.db_repo.get_comments(page_revisions)
             c.cs_statuses = c.db_repo.statuses(page_revisions)
         except EmptyRepositoryError as e:
-            h.flash(safe_str(e), category='warning')
+            h.flash(e, category='warning')
             raise HTTPFound(location=url('summary_home', repo_name=c.repo_name))
         except (RepositoryError, ChangesetDoesNotExistError, Exception) as e:
             log.error(traceback.format_exc())
-            h.flash(safe_str(e), category='error')
+            h.flash(e, category='error')
             raise HTTPFound(location=url('changelog_home', repo_name=c.repo_name))
 
         c.branch_name = branch_name
         c.branch_filters = [('', _('None'))] + \
-            [(k, k) for k in c.db_repo_scm_instance.branches.keys()]
+            [(k, k) for k in c.db_repo_scm_instance.branches]
         if c.db_repo_scm_instance.closed_branches:
             prefix = _('(closed)') + ' '
             c.branch_filters += [('-', '-')] + \
-                [(k, prefix + k) for k in c.db_repo_scm_instance.closed_branches.keys()]
+                [(k, prefix + k) for k in c.db_repo_scm_instance.closed_branches]
         revs = []
         if not f_path:
             revs = [x.revision for x in c.cs_pagination]
--- a/kallithea/controllers/changeset.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/controllers/changeset.py	Mon May 04 19:24:04 2020 +0200
@@ -25,6 +25,7 @@
 :license: GPLv3, see LICENSE.md for more details.
 """
 
+import binascii
 import logging
 import traceback
 from collections import OrderedDict, defaultdict
@@ -32,7 +33,7 @@
 from tg import request, response
 from tg import tmpl_context as c
 from tg.i18n import ugettext as _
-from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPFound, HTTPNotFound
+from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPNotFound
 
 import kallithea.lib.helpers as h
 from kallithea.lib import diffs
@@ -40,7 +41,7 @@
 from kallithea.lib.base import BaseRepoController, jsonify, render
 from kallithea.lib.graphmod import graph_data
 from kallithea.lib.utils import action_logger
-from kallithea.lib.utils2 import safe_unicode
+from kallithea.lib.utils2 import ascii_str, safe_str
 from kallithea.lib.vcs.backends.base import EmptyChangeset
 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError
 from kallithea.model.changeset_status import ChangesetStatusModel
@@ -65,7 +66,7 @@
 
 def get_ignore_ws(fid, GET):
     ig_ws_global = GET.get('ignorews')
-    ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
+    ig_ws = [k for k in GET.getall(fid) if k.startswith('WS')]
     if ig_ws:
         try:
             return int(ig_ws[0].split(':')[-1])
@@ -108,9 +109,9 @@
 def get_line_ctx(fid, GET):
     ln_ctx_global = GET.get('context')
     if fid:
-        ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
+        ln_ctx = [k for k in GET.getall(fid) if k.startswith('C')]
     else:
-        _ln_ctx = filter(lambda k: k.startswith('C'), GET)
+        _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]
@@ -214,7 +215,6 @@
             return {
                'location': h.url('my_pullrequests'), # or repo pr list?
             }
-            raise HTTPFound(location=h.url('my_pullrequests')) # or repo pr list?
         raise HTTPForbidden()
 
     text = request.POST.get('text', '').strip()
@@ -256,7 +256,7 @@
     Session().commit()
 
     data = {
-       'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
+       'target_id': h.safeid(request.POST.get('f_path')),
     }
     if comment is not None:
         c.comment = comment
@@ -395,6 +395,8 @@
             c.changeset = c.cs_ranges[0]
             c.parent_tmpl = ''.join(['# Parent  %s\n' % x.raw_id
                                      for x in c.changeset.parents])
+            c.changeset_graft_source_hash = ascii_str(c.changeset.extra.get(b'source', b''))
+            c.changeset_transplant_source_hash = ascii_str(binascii.hexlify(c.changeset.extra.get(b'transplant_source', b'')))
         if method == 'download':
             response.content_type = 'text/plain'
             response.content_disposition = 'attachment; filename=%s.diff' \
@@ -402,7 +404,7 @@
             return raw_diff
         elif method == 'patch':
             response.content_type = 'text/plain'
-            c.diff = safe_unicode(raw_diff)
+            c.diff = safe_str(raw_diff)
             return render('changeset/patch_changeset.html')
         elif method == 'raw':
             response.content_type = 'text/plain'
--- a/kallithea/controllers/compare.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/controllers/compare.py	Mon May 04 19:24:04 2020 +0200
@@ -30,6 +30,7 @@
 import logging
 import re
 
+import mercurial.unionrepo
 from tg import request
 from tg import tmpl_context as c
 from tg.i18n import ugettext as _
@@ -42,8 +43,7 @@
 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 safe_int, safe_str
-from kallithea.lib.vcs.utils.hgcompat import unionrepo
+from kallithea.lib.utils2 import ascii_bytes, ascii_str, safe_bytes, safe_int
 from kallithea.model.db import Repository
 
 
@@ -97,14 +97,9 @@
         elif alias == 'hg':
             # case two independent repos
             if org_repo != other_repo:
-                try:
-                    hgrepo = unionrepo.makeunionrepository(other_repo.baseui,
-                                                           other_repo.path,
-                                                           org_repo.path)
-                except AttributeError: # makeunionrepository was introduced in Mercurial 4.8 23f2299e9e53
-                    hgrepo = unionrepo.unionrepository(other_repo.baseui,
-                                                       other_repo.path,
-                                                       org_repo.path)
+                hgrepo = mercurial.unionrepo.makeunionrepository(other_repo.baseui,
+                                                       safe_bytes(other_repo.path),
+                                                       safe_bytes(org_repo.path))
                 # all ancestors of other_rev will be in other_repo and
                 # rev numbers from hgrepo can be used in other_repo - org_rev ancestors cannot
 
@@ -112,21 +107,27 @@
             else:
                 hgrepo = other_repo._repo
 
-            ancestors = [hgrepo[ancestor].hex() for ancestor in
-                         hgrepo.revs("id(%s) & ::id(%s)", other_rev, org_rev)]
+            ancestors = [ascii_str(hgrepo[ancestor].hex()) for ancestor in
+                         hgrepo.revs(b"id(%s) & ::id(%s)", ascii_bytes(other_rev), ascii_bytes(org_rev))]
             if ancestors:
                 log.debug("shortcut found: %s is already an ancestor of %s", other_rev, org_rev)
             else:
                 log.debug("no shortcut found: %s is not an ancestor of %s", other_rev, org_rev)
-                ancestors = [hgrepo[ancestor].hex() for ancestor in
-                             hgrepo.revs("heads(::id(%s) & ::id(%s))", org_rev, other_rev)] # FIXME: expensive!
+                ancestors = [ascii_str(hgrepo[ancestor].hex()) for ancestor in
+                             hgrepo.revs(b"heads(::id(%s) & ::id(%s))", ascii_bytes(org_rev), ascii_bytes(other_rev))] # FIXME: expensive!
 
-            other_revs = hgrepo.revs("ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
-                                     other_rev, org_rev, org_rev)
-            other_changesets = [other_repo.get_changeset(rev) for rev in other_revs]
-            org_revs = hgrepo.revs("ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
-                                   org_rev, other_rev, other_rev)
-            org_changesets = [org_repo.get_changeset(hgrepo[rev].hex()) for rev in org_revs]
+            other_changesets = [
+                other_repo.get_changeset(rev)
+                for rev in hgrepo.revs(
+                    b"ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
+                    ascii_bytes(other_rev), ascii_bytes(org_rev), ascii_bytes(org_rev))
+            ]
+            org_changesets = [
+                org_repo.get_changeset(ascii_str(hgrepo[rev].hex()))
+                for rev in hgrepo.revs(
+                    b"ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
+                    ascii_bytes(org_rev), ascii_bytes(other_rev), ascii_bytes(other_rev))
+            ]
 
         elif alias == 'git':
             if org_repo != other_repo:
@@ -134,15 +135,15 @@
                 from dulwich.client import SubprocessGitClient
 
                 gitrepo = Repo(org_repo.path)
-                SubprocessGitClient(thin_packs=False).fetch(safe_str(other_repo.path), gitrepo)
+                SubprocessGitClient(thin_packs=False).fetch(other_repo.path, gitrepo)
 
                 gitrepo_remote = Repo(other_repo.path)
-                SubprocessGitClient(thin_packs=False).fetch(safe_str(org_repo.path), gitrepo_remote)
+                SubprocessGitClient(thin_packs=False).fetch(org_repo.path, gitrepo_remote)
 
                 revs = [
-                    x.commit.id
-                    for x in gitrepo_remote.get_walker(include=[other_rev],
-                                                       exclude=[org_rev])
+                    ascii_str(x.commit.id)
+                    for x in gitrepo_remote.get_walker(include=[ascii_bytes(other_rev)],
+                                                       exclude=[ascii_bytes(org_rev)])
                 ]
                 other_changesets = [other_repo.get_changeset(rev) for rev in reversed(revs)]
                 if other_changesets:
@@ -155,13 +156,13 @@
                 gitrepo_remote.close()
 
             else:
-                so, se = org_repo.run_git_command(
+                so = org_repo.run_git_command(
                     ['log', '--reverse', '--pretty=format:%H',
                      '-s', '%s..%s' % (org_rev, other_rev)]
                 )
                 other_changesets = [org_repo.get_changeset(cs)
                               for cs in re.findall(r'[0-9a-fA-F]{40}', so)]
-                so, se = org_repo.run_git_command(
+                so = org_repo.run_git_command(
                     ['merge-base', org_rev, other_rev]
                 )
                 ancestors = [re.findall(r'[0-9a-fA-F]{40}', so)[0]]
@@ -277,7 +278,7 @@
                                       ignore_whitespace=ignore_whitespace,
                                       context=line_context)
 
-        diff_processor = diffs.DiffProcessor(raw_diff or '', diff_limit=diff_limit)
+        diff_processor = diffs.DiffProcessor(raw_diff, diff_limit=diff_limit)
         c.limited_diff = diff_processor.limited_diff
         c.file_diff_data = []
         c.lines_added = 0
--- a/kallithea/controllers/error.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/controllers/error.py	Mon May 04 19:24:04 2020 +0200
@@ -25,7 +25,7 @@
 :license: GPLv3, see LICENSE.md for more details.
 """
 
-import cgi
+import html
 import logging
 
 from tg import config, expose, request
@@ -41,11 +41,8 @@
 class ErrorController(BaseController):
     """Generates error documents as and when they are required.
 
-    The ErrorDocuments middleware forwards to ErrorController when error
+    The errorpage middleware renders /error/document when error
     related status codes are returned from the application.
-
-    This behavior can be altered by changing the parameters to the
-    ErrorDocuments middleware in your config/middleware.py file.
     """
 
     def _before(self, *args, **kwargs):
@@ -64,8 +61,7 @@
             'protocol': e.get('wsgi.url_scheme'),
             'host': e.get('HTTP_HOST'), }
         if resp:
-            c.error_message = cgi.escape(request.GET.get('code',
-                                                         str(resp.status)))
+            c.error_message = html.escape(request.GET.get('code', str(resp.status)))
             c.error_explanation = self.get_error_explanation(resp.status_int)
         else:
             c.error_message = _('No response')
--- a/kallithea/controllers/feed.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/controllers/feed.py	Mon May 04 19:24:04 2020 +0200
@@ -32,23 +32,19 @@
 from tg import response
 from tg import tmpl_context as c
 from tg.i18n import ugettext as _
-from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
 
 from kallithea import CONFIG
+from kallithea.lib import feeds
 from kallithea.lib import helpers as h
 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_unicode, str2bool
+from kallithea.lib.utils2 import safe_int, safe_str, str2bool
 
 
 log = logging.getLogger(__name__)
 
 
-language = 'en-us'
-ttl = "5"
-
-
 class FeedController(BaseRepoController):
 
     @LoginRequired(allow_default_user=True)
@@ -98,64 +94,41 @@
         desc_msg.extend(changes)
         if str2bool(CONFIG.get('rss_include_diff', False)):
             desc_msg.append('\n\n')
-            desc_msg.append(raw_diff)
+            desc_msg.append(safe_str(raw_diff))
         desc_msg.append('</pre>')
-        return map(safe_unicode, desc_msg)
+        return desc_msg
 
-    def atom(self, repo_name):
-        """Produce an atom-1.0 feed via feedgenerator module"""
+    def _feed(self, repo_name, feeder):
+        """Produce a simple feed"""
 
-        @cache_region('long_term', '_get_feed_from_cache')
+        @cache_region('long_term_file', '_get_feed_from_cache')
         def _get_feed_from_cache(*_cache_keys):  # parameters are not really used - only as caching key
-            feed = Atom1Feed(
+            header = dict(
                 title=_('%s %s feed') % (c.site_name, repo_name),
                 link=h.canonical_url('summary_home', repo_name=repo_name),
                 description=_('Changes on %s repository') % repo_name,
-                language=language,
-                ttl=ttl
             )
 
             rss_items_per_page = safe_int(CONFIG.get('rss_items_per_page', 20))
+            entries=[]
             for cs in reversed(list(c.db_repo_scm_instance[-rss_items_per_page:])):
-                feed.add_item(title=self._get_title(cs),
-                              link=h.canonical_url('changeset_home', repo_name=repo_name,
-                                       revision=cs.raw_id),
-                              author_name=cs.author,
-                              description=''.join(self.__get_desc(cs)),
-                              pubdate=cs.date,
-                              )
+                entries.append(dict(
+                    title=self._get_title(cs),
+                    link=h.canonical_url('changeset_home', repo_name=repo_name, revision=cs.raw_id),
+                    author_email=cs.author_email,
+                    author_name=cs.author_name,
+                    description=''.join(self.__get_desc(cs)),
+                    pubdate=cs.date,
+                ))
+            return feeder.render(header, entries)
 
-            response.content_type = feed.mime_type
-            return feed.writeString('utf-8')
+        response.content_type = feeder.content_type
+        return _get_feed_from_cache(repo_name, feeder.__name__)
 
-        kind = 'ATOM'
-        return _get_feed_from_cache(repo_name, kind, c.db_repo.changeset_cache.get('raw_id'))
+    def atom(self, repo_name):
+        """Produce a simple atom-1.0 feed"""
+        return self._feed(repo_name, feeds.AtomFeed)
 
     def rss(self, repo_name):
-        """Produce an rss2 feed via feedgenerator module"""
-
-        @cache_region('long_term', '_get_feed_from_cache')
-        def _get_feed_from_cache(*_cache_keys):  # parameters are not really used - only as caching key
-            feed = Rss201rev2Feed(
-                title=_('%s %s feed') % (c.site_name, repo_name),
-                link=h.canonical_url('summary_home', repo_name=repo_name),
-                description=_('Changes on %s repository') % repo_name,
-                language=language,
-                ttl=ttl
-            )
-
-            rss_items_per_page = safe_int(CONFIG.get('rss_items_per_page', 20))
-            for cs in reversed(list(c.db_repo_scm_instance[-rss_items_per_page:])):
-                feed.add_item(title=self._get_title(cs),
-                              link=h.canonical_url('changeset_home', repo_name=repo_name,
-                                       revision=cs.raw_id),
-                              author_name=cs.author,
-                              description=''.join(self.__get_desc(cs)),
-                              pubdate=cs.date,
-                             )
-
-            response.content_type = feed.mime_type
-            return feed.writeString('utf-8')
-
-        kind = 'RSS'
-        return _get_feed_from_cache(repo_name, kind, c.db_repo.changeset_cache.get('raw_id'))
+        """Produce a simple rss2 feed"""
+        return self._feed(repo_name, feeds.RssFeed)
--- a/kallithea/controllers/files.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/controllers/files.py	Mon May 04 19:24:04 2020 +0200
@@ -49,10 +49,10 @@
 from kallithea.lib.utils2 import convert_line_endings, detect_mode, safe_int, safe_str, str2bool
 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, NodeDoesNotExistError, NodeError, RepositoryError, VCSError)
+from kallithea.lib.vcs.exceptions import (ChangesetDoesNotExistError, ChangesetError, EmptyRepositoryError, ImproperArchiveTypeError, NodeAlreadyExistsError,
+                                          NodeDoesNotExistError, NodeError, RepositoryError, VCSError)
 from kallithea.lib.vcs.nodes import FileNode
-from kallithea.model.db import Repository
+from kallithea.model import db
 from kallithea.model.repo import RepoModel
 from kallithea.model.scm import ScmModel
 
@@ -90,7 +90,7 @@
             h.flash(msg, category='error')
             raise HTTPNotFound()
         except RepositoryError as e:
-            h.flash(safe_str(e), category='error')
+            h.flash(e, category='error')
             raise HTTPNotFound()
 
     def __get_filenode(self, cs, path):
@@ -110,7 +110,7 @@
             h.flash(msg, category='error')
             raise HTTPNotFound()
         except RepositoryError as e:
-            h.flash(safe_str(e), category='error')
+            h.flash(e, category='error')
             raise HTTPNotFound()
 
         return file_node
@@ -163,7 +163,7 @@
                 c.load_full_history = False
                 # determine if we're on branch head
                 _branches = c.db_repo_scm_instance.branches
-                c.on_branch_head = revision in _branches.keys() + _branches.values()
+                c.on_branch_head = revision in _branches or revision in _branches.values()
                 _hist = []
                 c.file_history = []
                 if c.load_full_history:
@@ -175,7 +175,7 @@
             else:
                 c.authors = c.file_history = []
         except RepositoryError as e:
-            h.flash(safe_str(e), category='error')
+            h.flash(e, category='error')
             raise HTTPNotFound()
 
         if request.environ.get('HTTP_X_PARTIAL_XHR'):
@@ -232,8 +232,8 @@
         cs = self.__get_cs(revision)
         file_node = self.__get_filenode(cs, f_path)
 
-        response.content_disposition = 'attachment; filename=%s' % \
-            safe_str(f_path.split(Repository.url_sep())[-1])
+        response.content_disposition = \
+            'attachment; filename=%s' % f_path.split(db.URL_SEP)[-1]
 
         response.content_type = file_node.mimetype
         return file_node.content
@@ -277,8 +277,7 @@
                 mimetype, dispo = 'text/plain', 'inline'
 
         if dispo == 'attachment':
-            dispo = 'attachment; filename=%s' % \
-                        safe_str(f_path.split(os.sep)[-1])
+            dispo = 'attachment; filename=%s' % f_path.split(os.sep)[-1]
 
         response.content_disposition = dispo
         response.content_type = mimetype
@@ -292,7 +291,7 @@
         # create multiple heads via file editing
         _branches = repo.scm_instance.branches
         # check if revision is a branch name or branch hash
-        if revision not in _branches.keys() + _branches.values():
+        if revision not in _branches and revision not in _branches.values():
             h.flash(_('You can only delete files with revision '
                       'being a valid branch'), category='warning')
             raise HTTPFound(location=h.url('files_home',
@@ -346,7 +345,7 @@
         # create multiple heads via file editing
         _branches = repo.scm_instance.branches
         # check if revision is a branch name or branch hash
-        if revision not in _branches.keys() + _branches.values():
+        if revision not in _branches and revision not in _branches.values():
             h.flash(_('You can only edit files with revision '
                       'being a valid branch'), category='warning')
             raise HTTPFound(location=h.url('files_home',
@@ -365,8 +364,7 @@
         c.f_path = f_path
 
         if r_post:
-
-            old_content = c.file.content
+            old_content = safe_str(c.file.content)
             sl = old_content.splitlines(1)
             first_line = sl[0] if sl else ''
             # modes:  0 - Unix, 1 - Mac, 2 - DOS
@@ -509,8 +507,7 @@
 
         from kallithea import CONFIG
         rev_name = cs.raw_id[:12]
-        archive_name = '%s-%s%s' % (safe_str(repo_name.replace('/', '_')),
-                                    safe_str(rev_name), ext)
+        archive_name = '%s-%s%s' % (repo_name.replace('/', '_'), rev_name, ext)
 
         archive_path = None
         cached_archive_path = None
--- a/kallithea/controllers/forks.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/controllers/forks.py	Mon May 04 19:24:04 2020 +0200
@@ -35,13 +35,14 @@
 from tg.i18n import ugettext as _
 from webob.exc import HTTPFound
 
+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.base import BaseRepoController, render
 from kallithea.lib.page import Page
 from kallithea.lib.utils2 import safe_int
-from kallithea.model.db import Repository, Ui, User, UserFollowing
+from kallithea.model.db import Repository, Ui, UserFollowing
 from kallithea.model.forms import RepoForkForm
 from kallithea.model.repo import RepoModel
 from kallithea.model.scm import AvailableRepoGroupChoices, ScmModel
@@ -76,7 +77,7 @@
             h.not_mapped_error(c.repo_name)
             raise HTTPFound(location=url('repos'))
 
-        c.default_user_id = User.get_default_user().user_id
+        c.default_user_id = kallithea.DEFAULT_USER_ID
         c.in_public_journal = UserFollowing.query() \
             .filter(UserFollowing.user_id == c.default_user_id) \
             .filter(UserFollowing.follows_repository == c.repo_info).scalar()
--- a/kallithea/controllers/home.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/controllers/home.py	Mon May 04 19:24:04 2020 +0200
@@ -37,7 +37,7 @@
 from kallithea.lib import helpers as h
 from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired
 from kallithea.lib.base import BaseController, jsonify, render
-from kallithea.lib.utils import conditional_cache
+from kallithea.lib.utils2 import safe_str
 from kallithea.model.db import RepoGroup, Repository, User, UserGroup
 from kallithea.model.repo import RepoModel
 from kallithea.model.scm import UserGroupList
@@ -67,9 +67,7 @@
     @LoginRequired(allow_default_user=True)
     @jsonify
     def repo_switcher_data(self):
-        # wrapper for conditional cache
-        def _c():
-            log.debug('generating switcher repo/groups list')
+        if request.is_xhr:
             all_repos = Repository.query(sorted=True).all()
             repo_iter = self.scm_model.get_repos(all_repos)
             all_groups = RepoGroup.query(sorted=True).all()
@@ -96,17 +94,16 @@
                     ],
                    }]
 
+            for res_dict in res:
+                for child in (res_dict['children']):
+                    child['obj'].pop('_changeset_cache', None)  # bytes cannot be encoded in json ... but this value isn't relevant on client side at all ...
+
             data = {
                 'more': False,
                 'results': res,
             }
             return data
 
-        if request.is_xhr:
-            condition = False
-            compute = conditional_cache('short_term', 'cache_desc',
-                                        condition=condition, func=_c)
-            return compute()
         else:
             raise HTTPBadRequest()
 
@@ -120,25 +117,25 @@
         if _branches:
             res.append({
                 'text': _('Branch'),
-                'children': [{'id': rev, 'text': name, 'type': 'branch'} for name, rev in _branches]
+                'children': [{'id': safe_str(rev), 'text': safe_str(name), 'type': 'branch'} for name, rev in _branches]
             })
         _closed_branches = repo.closed_branches.items()
         if _closed_branches:
             res.append({
                 'text': _('Closed Branches'),
-                'children': [{'id': rev, 'text': name, 'type': 'closed-branch'} for name, rev in _closed_branches]
+                'children': [{'id': safe_str(rev), 'text': safe_str(name), 'type': 'closed-branch'} for name, rev in _closed_branches]
             })
         _tags = repo.tags.items()
         if _tags:
             res.append({
                 'text': _('Tag'),
-                'children': [{'id': rev, 'text': name, 'type': 'tag'} for name, rev in _tags]
+                'children': [{'id': safe_str(rev), 'text': safe_str(name), 'type': 'tag'} for name, rev in _tags]
             })
         _bookmarks = repo.bookmarks.items()
         if _bookmarks:
             res.append({
                 'text': _('Bookmark'),
-                'children': [{'id': rev, 'text': name, 'type': 'book'} for name, rev in _bookmarks]
+                'children': [{'id': safe_str(rev), 'text': safe_str(name), 'type': 'book'} for name, rev in _bookmarks]
             })
         data = {
             'more': False,
--- a/kallithea/controllers/journal.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/controllers/journal.py	Mon May 04 19:24:04 2020 +0200
@@ -23,7 +23,6 @@
 :author: marcink
 :copyright: (c) 2013 RhodeCode GmbH, and others.
 :license: GPLv3, see LICENSE.md for more details.
-
 """
 
 import logging
@@ -35,12 +34,11 @@
 from tg import request, response
 from tg import tmpl_context as c
 from tg.i18n import ugettext as _
-from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
 from webob.exc import HTTPBadRequest
 
 import kallithea.lib.helpers as h
-from kallithea.config.routing import url
 from kallithea.controllers.admin.admin import _journal_filter
+from kallithea.lib import feeds
 from kallithea.lib.auth import LoginRequired
 from kallithea.lib.base import BaseController, render
 from kallithea.lib.page import Page
@@ -105,22 +103,17 @@
 
         return journal
 
-    def _atom_feed(self, repos, public=True):
+    def _feed(self, repos, feeder, link, desc):
+        response.content_type = feeder.content_type
         journal = self._get_journal_data(repos)
-        if public:
-            _link = h.canonical_url('public_journal_atom')
-            _desc = '%s %s %s' % (c.site_name, _('Public Journal'),
-                                  'atom feed')
-        else:
-            _link = h.canonical_url('journal_atom')
-            _desc = '%s %s %s' % (c.site_name, _('Journal'), 'atom feed')
 
-        feed = Atom1Feed(title=_desc,
-                         link=_link,
-                         description=_desc,
-                         language=language,
-                         ttl=ttl)
+        header = dict(
+            title=desc,
+            link=link,
+            description=desc,
+        )
 
+        entries=[]
         for entry in journal[:feed_nr]:
             user = entry.user
             if user is None:
@@ -131,63 +124,43 @@
             action, action_extra, ico = h.action_parser(entry, feed=True)
             title = "%s - %s %s" % (user.short_contact, action(),
                                     entry.repository.repo_name)
-            desc = action_extra()
             _url = None
             if entry.repository is not None:
                 _url = h.canonical_url('changelog_home',
                            repo_name=entry.repository.repo_name)
 
-            feed.add_item(title=title,
-                          pubdate=entry.action_date,
-                          link=_url or h.canonical_url(''),
-                          author_email=user.email,
-                          author_name=user.full_contact,
-                          description=desc)
+            entries.append(dict(
+                title=title,
+                pubdate=entry.action_date,
+                link=_url or h.canonical_url(''),
+                author_email=user.email,
+                author_name=user.full_name_or_username,
+                description=action_extra(),
+            ))
+
+        return feeder.render(header, entries)
 
-        response.content_type = feed.mime_type
-        return feed.writeString('utf-8')
+    def _atom_feed(self, repos, public=True):
+        if public:
+            link = h.canonical_url('public_journal_atom')
+            desc = '%s %s %s' % (c.site_name, _('Public Journal'),
+                                  'atom feed')
+        else:
+            link = h.canonical_url('journal_atom')
+            desc = '%s %s %s' % (c.site_name, _('Journal'), 'atom feed')
+
+        return self._feed(repos, feeds.AtomFeed, link, desc)
 
     def _rss_feed(self, repos, public=True):
-        journal = self._get_journal_data(repos)
         if public:
-            _link = h.canonical_url('public_journal_atom')
-            _desc = '%s %s %s' % (c.site_name, _('Public Journal'),
+            link = h.canonical_url('public_journal_atom')
+            desc = '%s %s %s' % (c.site_name, _('Public Journal'),
                                   'rss feed')
         else:
-            _link = h.canonical_url('journal_atom')
-            _desc = '%s %s %s' % (c.site_name, _('Journal'), 'rss feed')
-
-        feed = Rss201rev2Feed(title=_desc,
-                         link=_link,
-                         description=_desc,
-                         language=language,
-                         ttl=ttl)
+            link = h.canonical_url('journal_atom')
+            desc = '%s %s %s' % (c.site_name, _('Journal'), 'rss feed')
 
-        for entry in journal[:feed_nr]:
-            user = entry.user
-            if user is None:
-                # fix deleted users
-                user = AttributeDict({'short_contact': entry.username,
-                                      'email': '',
-                                      'full_contact': ''})
-            action, action_extra, ico = h.action_parser(entry, feed=True)
-            title = "%s - %s %s" % (user.short_contact, action(),
-                                    entry.repository.repo_name)
-            desc = action_extra()
-            _url = None
-            if entry.repository is not None:
-                _url = h.canonical_url('changelog_home',
-                           repo_name=entry.repository.repo_name)
-
-            feed.add_item(title=title,
-                          pubdate=entry.action_date,
-                          link=_url or h.canonical_url(''),
-                          author_email=user.email,
-                          author_name=user.full_contact,
-                          description=desc)
-
-        response.content_type = feed.mime_type
-        return feed.writeString('utf-8')
+        return self._feed(repos, feeds.RssFeed, link, desc)
 
     @LoginRequired()
     def index(self):
@@ -201,10 +174,8 @@
 
         journal = self._get_journal_data(c.following)
 
-        def url_generator(**kw):
-            return url.current(filter=c.search_term, **kw)
-
-        c.journal_pager = Page(journal, page=p, items_per_page=20, url=url_generator)
+        c.journal_pager = Page(journal, page=p, items_per_page=20,
+                               filter=c.search_term)
         c.journal_day_aggregate = self._get_daily_aggregate(c.journal_pager)
 
         if request.environ.get('HTTP_X_PARTIAL_XHR'):
@@ -221,9 +192,7 @@
 
     @LoginRequired()
     def journal_atom(self):
-        """
-        Produce an atom-1.0 feed via feedgenerator module
-        """
+        """Produce a simple atom-1.0 feed"""
         following = UserFollowing.query() \
             .filter(UserFollowing.user_id == request.authuser.user_id) \
             .options(joinedload(UserFollowing.follows_repository)) \
@@ -232,9 +201,7 @@
 
     @LoginRequired()
     def journal_rss(self):
-        """
-        Produce an rss feed via feedgenerator module
-        """
+        """Produce a simple rss2 feed"""
         following = UserFollowing.query() \
             .filter(UserFollowing.user_id == request.authuser.user_id) \
             .options(joinedload(UserFollowing.follows_repository)) \
@@ -290,9 +257,7 @@
 
     @LoginRequired(allow_default_user=True)
     def public_journal_atom(self):
-        """
-        Produce an atom-1.0 feed via feedgenerator module
-        """
+        """Produce a simple atom-1.0 feed"""
         c.following = UserFollowing.query() \
             .filter(UserFollowing.user_id == request.authuser.user_id) \
             .options(joinedload(UserFollowing.follows_repository)) \
@@ -302,9 +267,7 @@
 
     @LoginRequired(allow_default_user=True)
     def public_journal_rss(self):
-        """
-        Produce an rss2 feed via feedgenerator module
-        """
+        """Produce a simple rss2 feed"""
         c.following = UserFollowing.query() \
             .filter(UserFollowing.user_id == request.authuser.user_id) \
             .options(joinedload(UserFollowing.follows_repository)) \
--- a/kallithea/controllers/login.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/controllers/login.py	Mon May 04 19:24:04 2020 +0200
@@ -41,7 +41,6 @@
 from kallithea.lib.auth import AuthUser, HasPermissionAnyDecorator
 from kallithea.lib.base import BaseController, log_in_user, render
 from kallithea.lib.exceptions import UserCreationError
-from kallithea.lib.utils2 import safe_str
 from kallithea.model.db import Setting, User
 from kallithea.model.forms import LoginForm, PasswordResetConfirmationForm, PasswordResetRequestForm, RegisterForm
 from kallithea.model.meta import Session
@@ -68,7 +67,7 @@
         return _re.match(came_from) is not None
 
     def index(self):
-        c.came_from = safe_str(request.GET.get('came_from', ''))
+        c.came_from = request.GET.get('came_from', '')
         if c.came_from:
             if not self._validate_came_from(c.came_from):
                 log.error('Invalid came_from (not server-relative): %r', c.came_from)
@@ -80,10 +79,11 @@
             # import Login Form validator class
             login_form = LoginForm()()
             try:
+                # login_form will check username/password using ValidAuth and report failure to the user
                 c.form_result = login_form.to_python(dict(request.POST))
-                # form checks for username/password, now we're authenticated
                 username = c.form_result['username']
-                user = User.get_by_username_or_email(username, case_insensitive=True)
+                user = User.get_by_username_or_email(username)
+                assert user is not None  # the same user get just passed in the form validation
             except formencode.Invalid as errors:
                 defaults = errors.value
                 # remove password from filling in form again
@@ -102,9 +102,11 @@
                 # Exception itself
                 h.flash(e, 'error')
             else:
+                # login_form already validated the password - now set the session cookie accordingly
                 auth_user = log_in_user(user, c.form_result['remember'], is_external_auth=False, ip_addr=request.ip_addr)
-                # TODO: handle auth_user is None as failed authentication?
-                raise HTTPFound(location=c.came_from)
+                if auth_user:
+                    raise HTTPFound(location=c.came_from)
+                h.flash(_('Authentication failed.'), 'error')
         else:
             # redirect if already logged in
             if not request.authuser.is_anonymous:
@@ -210,12 +212,10 @@
 
         # The template needs the email address outside of the form.
         c.email = request.params.get('email')
-
+        c.timestamp = request.params.get('timestamp') or ''
+        c.token = request.params.get('token') or ''
         if not request.POST:
-            return htmlfill.render(
-                render('/password_reset_confirmation.html'),
-                defaults=dict(request.params),
-                encoding='UTF-8')
+            return render('/password_reset_confirmation.html')
 
         form = PasswordResetConfirmationForm()()
         try:
--- a/kallithea/controllers/pullrequests.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/controllers/pullrequests.py	Mon May 04 19:24:04 2020 +0200
@@ -29,6 +29,7 @@
 import traceback
 
 import formencode
+import mercurial.unionrepo
 from tg import request
 from tg import tmpl_context as c
 from tg.i18n import ugettext as _
@@ -42,10 +43,8 @@
 from kallithea.lib.base import BaseRepoController, jsonify, render
 from kallithea.lib.graphmod import graph_data
 from kallithea.lib.page import Page
-from kallithea.lib.utils2 import safe_int
+from kallithea.lib.utils2 import ascii_bytes, safe_bytes, safe_int
 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, EmptyRepositoryError
-from kallithea.lib.vcs.utils import safe_str
-from kallithea.lib.vcs.utils.hgcompat import unionrepo
 from kallithea.model.changeset_status import ChangesetStatusModel
 from kallithea.model.comment import ChangesetCommentsModel
 from kallithea.model.db import ChangesetStatus, PullRequest, PullRequestReviewer, Repository, User
@@ -83,22 +82,15 @@
         # list named branches that has been merged to this named branch - it should probably merge back
         peers = []
 
-        if rev:
-            rev = safe_str(rev)
-
-        if branch:
-            branch = safe_str(branch)
-
         if branch_rev:
-            branch_rev = safe_str(branch_rev)
             # a revset not restricting to merge() would be better
             # (especially because it would get the branch point)
             # ... but is currently too expensive
             # including branches of children could be nice too
             peerbranches = set()
             for i in repo._repo.revs(
-                "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)), -rev)",
-                branch_rev, branch_rev
+                b"sort(parents(branch(id(%s)) and merge()) - branch(id(%s)), -rev)",
+                ascii_bytes(branch_rev), ascii_bytes(branch_rev),
             ):
                 for abranch in repo.get_changeset(i).branches:
                     if abranch not in peerbranches:
@@ -111,7 +103,7 @@
         tipbranch = None
 
         branches = []
-        for abranch, branchrev in repo.branches.iteritems():
+        for abranch, branchrev in repo.branches.items():
             n = 'branch:%s:%s' % (abranch, branchrev)
             desc = abranch
             if branchrev == tiprev:
@@ -135,14 +127,14 @@
                 log.debug('branch %r not found in %s', branch, repo)
 
         bookmarks = []
-        for bookmark, bookmarkrev in repo.bookmarks.iteritems():
+        for bookmark, bookmarkrev in repo.bookmarks.items():
             n = 'book:%s:%s' % (bookmark, bookmarkrev)
             bookmarks.append((n, bookmark))
             if rev == bookmarkrev:
                 selected = n
 
         tags = []
-        for tag, tagrev in repo.tags.iteritems():
+        for tag, tagrev in repo.tags.items():
             if tag == 'tip':
                 continue
             n = 'tag:%s:%s' % (tag, tagrev)
@@ -173,7 +165,7 @@
                 if 'master' in repo.branches:
                     selected = 'branch:master:%s' % repo.branches['master']
                 else:
-                    k, v = repo.branches.items()[0]
+                    k, v = list(repo.branches.items())[0]
                     selected = 'branch:%s:%s' % (k, v)
 
         groups = [(specials, _("Special")),
@@ -201,6 +193,11 @@
     def show_all(self, repo_name):
         c.from_ = request.GET.get('from_') or ''
         c.closed = request.GET.get('closed') or ''
+        url_params = {}
+        if c.from_:
+            url_params['from_'] = 1
+        if c.closed:
+            url_params['closed'] = 1
         p = safe_int(request.GET.get('page'), 1)
 
         q = PullRequest.query(include_closed=c.closed, sorted=True)
@@ -210,7 +207,7 @@
             q = q.filter_by(other_repo=c.db_repo)
         c.pull_requests = q.all()
 
-        c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=100)
+        c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=100, **url_params)
 
         return render('/pullrequests/pullrequest_show_all.html')
 
@@ -335,7 +332,7 @@
         try:
             cmd = CreatePullRequestAction(org_repo, other_repo, org_ref, other_ref, title, description, owner, reviewers)
         except CreatePullRequestAction.ValidationError as e:
-            h.flash(str(e), category='error', logf=log.error)
+            h.flash(e, category='error', logf=log.error)
             raise HTTPNotFound
 
         try:
@@ -358,7 +355,7 @@
         try:
             cmd = CreatePullRequestIterationAction(old_pull_request, new_org_rev, new_other_rev, title, description, owner, reviewers)
         except CreatePullRequestAction.ValidationError as e:
-            h.flash(str(e), category='error', logf=log.error)
+            h.flash(e, category='error', logf=log.error)
             raise HTTPNotFound
 
         try:
@@ -470,14 +467,15 @@
          c.a_rev) = c.pull_request.other_ref.split(':') # a_rev is ancestor
 
         org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!!
-        try:
-            c.cs_ranges = []
-            for x in c.pull_request.revisions:
+        c.cs_ranges = []
+        for x in c.pull_request.revisions:
+            try:
                 c.cs_ranges.append(org_scm_instance.get_changeset(x))
-        except ChangesetDoesNotExistError:
-            c.cs_ranges = []
-            h.flash(_('Revision %s not found in %s') % (x, c.cs_repo.repo_name),
-                'error')
+            except ChangesetDoesNotExistError:
+                c.cs_ranges = []
+                h.flash(_('Revision %s not found in %s') % (x, c.cs_repo.repo_name),
+                    'error')
+                break
         c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
         revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
         c.jsdata = graph_data(org_scm_instance, revs)
@@ -530,14 +528,9 @@
                             # Note: org_scm_instance.path must come first so all
                             # valid revision numbers are 100% org_scm compatible
                             # - both for avail_revs and for revset results
-                            try:
-                                hgrepo = unionrepo.makeunionrepository(org_scm_instance.baseui,
-                                                                       org_scm_instance.path,
-                                                                       other_scm_instance.path)
-                            except AttributeError: # makeunionrepository was introduced in Mercurial 4.8 23f2299e9e53
-                                hgrepo = unionrepo.unionrepository(org_scm_instance.baseui,
-                                                                   org_scm_instance.path,
-                                                                   other_scm_instance.path)
+                            hgrepo = mercurial.unionrepo.makeunionrepository(org_scm_instance.baseui,
+                                                                   safe_bytes(org_scm_instance.path),
+                                                                   safe_bytes(other_scm_instance.path))
                         else:
                             hgrepo = org_scm_instance._repo
                         show = set(hgrepo.revs('::%ld & !::parents(%s) & !::%s',
@@ -587,11 +580,11 @@
         log.debug('running diff between %s and %s in %s',
                   c.a_rev, c.cs_rev, org_scm_instance.path)
         try:
-            raw_diff = diffs.get_diff(org_scm_instance, rev1=safe_str(c.a_rev), rev2=safe_str(c.cs_rev),
+            raw_diff = diffs.get_diff(org_scm_instance, rev1=c.a_rev, rev2=c.cs_rev,
                                       ignore_whitespace=ignore_whitespace, context=line_context)
         except ChangesetDoesNotExistError:
-            raw_diff = _("The diff can't be shown - the PR revisions could not be found.")
-        diff_processor = diffs.DiffProcessor(raw_diff or '', diff_limit=diff_limit)
+            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)
         c.limited_diff = diff_processor.limited_diff
         c.file_diff_data = []
         c.lines_added = 0
--- a/kallithea/controllers/root.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/controllers/root.py	Mon May 04 19:24:04 2020 +0200
@@ -31,5 +31,5 @@
     def __init__(self):
         self.mapper = make_map(config)
 
-        # the following assignment hooks in error handling
+        # The URL '/error/document' (the default TG errorpage.path) should be handled by ErrorController.document
         self.error = ErrorController()
--- a/kallithea/controllers/search.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/controllers/search.py	Mon May 04 19:24:04 2020 +0200
@@ -27,12 +27,10 @@
 
 import logging
 import traceback
-import urllib
 
 from tg import config, request
 from tg import tmpl_context as c
 from tg.i18n import ugettext as _
-from webhelpers2.html.tools import update_params
 from whoosh.index import EmptyIndexError, exists_in, open_dir
 from whoosh.qparser import QueryParser, QueryParserError
 from whoosh.query import Phrase, Prefix
@@ -41,7 +39,7 @@
 from kallithea.lib.base import BaseRepoController, render
 from kallithea.lib.indexers import CHGSET_IDX_NAME, CHGSETS_SCHEMA, IDX_NAME, SCHEMA, WhooshResultWrapper
 from kallithea.lib.page import Page
-from kallithea.lib.utils2 import safe_int, safe_str
+from kallithea.lib.utils2 import safe_int
 from kallithea.model.repo import RepoModel
 
 
@@ -96,9 +94,9 @@
                 if c.repo_name:
                     # use "repository_rawname:" instead of "repository:"
                     # for case-sensitive matching
-                    cur_query = u'repository_rawname:%s %s' % (c.repo_name, cur_query)
+                    cur_query = 'repository_rawname:%s %s' % (c.repo_name, cur_query)
                 try:
-                    query = qp.parse(unicode(cur_query))
+                    query = qp.parse(cur_query)
                     # extract words for highlight
                     if isinstance(query, Phrase):
                         highlight_items.update(query.words)
@@ -119,9 +117,6 @@
                         res_ln, results.runtime
                     )
 
-                    def url_generator(**kw):
-                        q = urllib.quote(safe_str(c.cur_query))
-                        return update_params("?q=%s&type=%s" % (q, safe_str(c.cur_type)), **kw)
                     repo_location = RepoModel().repos_path
                     c.formated_results = Page(
                         WhooshResultWrapper(search_type, searcher, matcher,
@@ -129,7 +124,8 @@
                         page=p,
                         item_count=res_ln,
                         items_per_page=10,
-                        url=url_generator
+                        type=c.cur_type,
+                        q=c.cur_query,
                     )
 
                 except QueryParserError:
--- a/kallithea/controllers/summary.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/controllers/summary.py	Mon May 04 19:24:04 2020 +0200
@@ -38,14 +38,15 @@
 from tg.i18n import ugettext as _
 from webob.exc import HTTPBadRequest
 
+import kallithea.lib.helpers as h
 from kallithea.config.conf import ALL_EXTS, ALL_READMES, LANGUAGES_EXTENSIONS_MAP
+from kallithea.lib import ext_json
 from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired
 from kallithea.lib.base import BaseRepoController, jsonify, render
 from kallithea.lib.celerylib.tasks import get_commits_stats
-from kallithea.lib.compat import json
 from kallithea.lib.markup_renderer import MarkupRenderer
-from kallithea.lib.page import RepoPage
-from kallithea.lib.utils2 import safe_int
+from kallithea.lib.page import Page
+from kallithea.lib.utils2 import safe_int, safe_str
 from kallithea.lib.vcs.backends.base import EmptyChangeset
 from kallithea.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, NodeDoesNotExistError
 from kallithea.lib.vcs.nodes import FileNode
@@ -65,7 +66,7 @@
         repo_name = db_repo.repo_name
         log.debug('Looking for README file')
 
-        @cache_region('long_term', '_get_readme_from_cache')
+        @cache_region('long_term_file', '_get_readme_from_cache')
         def _get_readme_from_cache(*_cache_keys):  # parameters are not really used - only as caching key
             readme_data = None
             readme_file = None
@@ -83,7 +84,7 @@
                         readme_file = f
                         log.debug('Found README file `%s` rendering...',
                                   readme_file)
-                        readme_data = renderer.render(readme.content,
+                        readme_data = renderer.render(safe_str(readme.content),
                                                       filename=f)
                         break
                     except NodeDoesNotExistError:
@@ -104,8 +105,12 @@
     def index(self, repo_name):
         p = safe_int(request.GET.get('page'), 1)
         size = safe_int(request.GET.get('size'), 10)
-        collection = c.db_repo_scm_instance
-        c.cs_pagination = RepoPage(collection, page=p, items_per_page=size)
+        try:
+            collection = c.db_repo_scm_instance.get_changesets(reverse=True)
+        except EmptyRepositoryError as e:
+            h.flash(e, category='warning')
+            collection = []
+        c.cs_pagination = Page(collection, page=p, items_per_page=size)
         page_revisions = [x.raw_id for x in list(c.cs_pagination)]
         c.cs_comments = c.db_repo.get_comments(page_revisions)
         c.cs_statuses = c.db_repo.statuses(page_revisions)
@@ -133,17 +138,13 @@
         c.stats_percentage = 0
 
         if stats and stats.languages:
-            c.no_data = False is c.db_repo.enable_statistics
-            lang_stats_d = json.loads(stats.languages)
-
+            lang_stats_d = ext_json.loads(stats.languages)
             lang_stats = [(x, {"count": y,
                                "desc": LANGUAGES_EXTENSIONS_MAP.get(x, '?')})
                           for x, y in lang_stats_d.items()]
             lang_stats.sort(key=lambda k: (-k[1]['count'], k[0]))
-
             c.trending_languages = lang_stats[:10]
         else:
-            c.no_data = True
             c.trending_languages = []
 
         c.enable_downloads = c.db_repo.enable_downloads
@@ -171,7 +172,7 @@
             c.no_data_msg = _('Statistics are disabled for this repository')
 
         td = date.today() + timedelta(days=1)
-        td_1m = td - timedelta(days=calendar.mdays[td.month])
+        td_1m = td - timedelta(days=calendar.monthrange(td.year, td.month)[1])
         td_1y = td - timedelta(days=365)
 
         ts_min_m = mktime(td_1m.timetuple())
@@ -185,18 +186,16 @@
             .scalar()
         c.stats_percentage = 0
         if stats and stats.languages:
-            c.no_data = False is c.db_repo.enable_statistics
-            lang_stats_d = json.loads(stats.languages)
-            c.commit_data = json.loads(stats.commit_activity)
-            c.overview_data = json.loads(stats.commit_activity_combined)
+            c.commit_data = ext_json.loads(stats.commit_activity)
+            c.overview_data = ext_json.loads(stats.commit_activity_combined)
 
-            lang_stats = ((x, {"count": y,
-                               "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
-                          for x, y in lang_stats_d.items())
+            lang_stats_d = ext_json.loads(stats.languages)
+            lang_stats = [(x, {"count": y,
+                               "desc": LANGUAGES_EXTENSIONS_MAP.get(x, '?')})
+                          for x, y in lang_stats_d.items()]
+            lang_stats.sort(key=lambda k: (-k[1]['count'], k[0]))
+            c.trending_languages = lang_stats[:10]
 
-            c.trending_languages = (
-                sorted(lang_stats, reverse=True, key=lambda k: k[1])[:10]
-            )
             last_rev = stats.stat_on_revision + 1
             c.repo_last_rev = c.db_repo_scm_instance.count() \
                 if c.db_repo_scm_instance.revisions else 0
@@ -208,8 +207,7 @@
         else:
             c.commit_data = {}
             c.overview_data = ([[ts_min_y, 0], [ts_max_y, 10]])
-            c.trending_languages = {}
-            c.no_data = True
+            c.trending_languages = []
 
         recurse_limit = 500  # don't recurse more than 500 times when parsing
         get_commits_stats(c.db_repo.repo_name, ts_min_y, ts_max_y, recurse_limit)
--- a/kallithea/front-end/package-lock.json	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/front-end/package-lock.json	Mon May 04 19:24:04 2020 +0200
@@ -3,18 +3,49 @@
   "requires": true,
   "lockfileVersion": 1,
   "dependencies": {
+    "@babel/code-frame": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
+      "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
+      "dev": true,
+      "requires": {
+        "@babel/highlight": "^7.8.3"
+      }
+    },
+    "@babel/highlight": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz",
+      "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==",
+      "dev": true,
+      "requires": {
+        "chalk": "^2.0.0",
+        "esutils": "^2.0.2",
+        "js-tokens": "^4.0.0"
+      }
+    },
     "abbrev": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
       "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
       "dev": true
     },
+    "acorn": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz",
+      "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==",
+      "dev": true
+    },
+    "acorn-jsx": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz",
+      "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==",
+      "dev": true
+    },
     "ajv": {
       "version": "6.10.2",
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz",
       "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==",
       "dev": true,
-      "optional": true,
       "requires": {
         "fast-deep-equal": "^2.0.1",
         "fast-json-stable-stringify": "^2.0.0",
@@ -28,6 +59,21 @@
       "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
       "dev": true
     },
+    "ansi-escapes": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz",
+      "integrity": "sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==",
+      "dev": true,
+      "requires": {
+        "type-fest": "^0.8.1"
+      }
+    },
+    "ansi-regex": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+      "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
+      "dev": true
+    },
     "ansi-styles": {
       "version": "3.2.1",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
@@ -37,6 +83,15 @@
         "color-convert": "^1.9.0"
       }
     },
+    "argparse": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+      "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+      "dev": true,
+      "requires": {
+        "sprintf-js": "~1.0.2"
+      }
+    },
     "array-find-index": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
@@ -66,6 +121,12 @@
       "dev": true,
       "optional": true
     },
+    "astral-regex": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
+      "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
+      "dev": true
+    },
     "asynckit": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -123,6 +184,12 @@
         "concat-map": "0.0.1"
       }
     },
+    "callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+      "dev": true
+    },
     "caseless": {
       "version": "0.12.0",
       "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
@@ -141,6 +208,12 @@
         "supports-color": "^5.3.0"
       }
     },
+    "chardet": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
+      "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
+      "dev": true
+    },
     "clean-css": {
       "version": "3.4.28",
       "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-3.4.28.tgz",
@@ -162,6 +235,21 @@
         }
       }
     },
+    "cli-cursor": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+      "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+      "dev": true,
+      "requires": {
+        "restore-cursor": "^3.1.0"
+      }
+    },
+    "cli-width": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
+      "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=",
+      "dev": true
+    },
     "clone": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
@@ -220,6 +308,19 @@
       "dev": true,
       "optional": true
     },
+    "cross-spawn": {
+      "version": "6.0.5",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+      "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+      "dev": true,
+      "requires": {
+        "nice-try": "^1.0.4",
+        "path-key": "^2.0.1",
+        "semver": "^5.5.0",
+        "shebang-command": "^1.2.0",
+        "which": "^1.2.9"
+      }
+    },
     "dashdash": {
       "version": "1.14.1",
       "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@@ -262,6 +363,12 @@
       "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=",
       "dev": true
     },
+    "deep-is": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
+      "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
+      "dev": true
+    },
     "delayed-stream": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -279,6 +386,64 @@
         "wrappy": "1"
       }
     },
+    "doctrine": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+      "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+      "dev": true,
+      "requires": {
+        "esutils": "^2.0.2"
+      }
+    },
+    "dom-serializer": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
+      "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==",
+      "dev": true,
+      "requires": {
+        "domelementtype": "^2.0.1",
+        "entities": "^2.0.0"
+      },
+      "dependencies": {
+        "domelementtype": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz",
+          "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==",
+          "dev": true
+        },
+        "entities": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz",
+          "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==",
+          "dev": true
+        }
+      }
+    },
+    "domelementtype": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
+      "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==",
+      "dev": true
+    },
+    "domhandler": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
+      "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
+      "dev": true,
+      "requires": {
+        "domelementtype": "1"
+      }
+    },
+    "domutils": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
+      "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
+      "dev": true,
+      "requires": {
+        "dom-serializer": "0",
+        "domelementtype": "1"
+      }
+    },
     "ecc-jsbn": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
@@ -290,6 +455,18 @@
         "safer-buffer": "^2.1.0"
       }
     },
+    "emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "dev": true
+    },
+    "entities": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
+      "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
+      "dev": true
+    },
     "errno": {
       "version": "0.1.7",
       "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
@@ -306,6 +483,149 @@
       "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
       "dev": true
     },
+    "eslint": {
+      "version": "6.8.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz",
+      "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.0.0",
+        "ajv": "^6.10.0",
+        "chalk": "^2.1.0",
+        "cross-spawn": "^6.0.5",
+        "debug": "^4.0.1",
+        "doctrine": "^3.0.0",
+        "eslint-scope": "^5.0.0",
+        "eslint-utils": "^1.4.3",
+        "eslint-visitor-keys": "^1.1.0",
+        "espree": "^6.1.2",
+        "esquery": "^1.0.1",
+        "esutils": "^2.0.2",
+        "file-entry-cache": "^5.0.1",
+        "functional-red-black-tree": "^1.0.1",
+        "glob-parent": "^5.0.0",
+        "globals": "^12.1.0",
+        "ignore": "^4.0.6",
+        "import-fresh": "^3.0.0",
+        "imurmurhash": "^0.1.4",
+        "inquirer": "^7.0.0",
+        "is-glob": "^4.0.0",
+        "js-yaml": "^3.13.1",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "levn": "^0.3.0",
+        "lodash": "^4.17.14",
+        "minimatch": "^3.0.4",
+        "mkdirp": "^0.5.1",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.8.3",
+        "progress": "^2.0.0",
+        "regexpp": "^2.0.1",
+        "semver": "^6.1.2",
+        "strip-ansi": "^5.2.0",
+        "strip-json-comments": "^3.0.1",
+        "table": "^5.2.3",
+        "text-table": "^0.2.0",
+        "v8-compile-cache": "^2.0.3"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+          "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+          "dev": true,
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "semver": {
+          "version": "6.3.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+          "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+          "dev": true
+        }
+      }
+    },
+    "eslint-plugin-html": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-html/-/eslint-plugin-html-6.0.0.tgz",
+      "integrity": "sha512-PQcGippOHS+HTbQCStmH5MY1BF2MaU8qW/+Mvo/8xTa/ioeMXdSP+IiaBw2+nh0KEMfYQKuTz1Zo+vHynjwhbg==",
+      "dev": true,
+      "requires": {
+        "htmlparser2": "^3.10.1"
+      }
+    },
+    "eslint-scope": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz",
+      "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==",
+      "dev": true,
+      "requires": {
+        "esrecurse": "^4.1.0",
+        "estraverse": "^4.1.1"
+      }
+    },
+    "eslint-utils": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz",
+      "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==",
+      "dev": true,
+      "requires": {
+        "eslint-visitor-keys": "^1.1.0"
+      }
+    },
+    "eslint-visitor-keys": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
+      "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
+      "dev": true
+    },
+    "espree": {
+      "version": "6.1.2",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz",
+      "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==",
+      "dev": true,
+      "requires": {
+        "acorn": "^7.1.0",
+        "acorn-jsx": "^5.1.0",
+        "eslint-visitor-keys": "^1.1.0"
+      }
+    },
+    "esprima": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+      "dev": true
+    },
+    "esquery": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.1.0.tgz",
+      "integrity": "sha512-MxYW9xKmROWF672KqjO75sszsA8Mxhw06YFeS5VHlB98KDHbOSurm3ArsjO60Eaf3QmGMCP1yn+0JQkNLo/97Q==",
+      "dev": true,
+      "requires": {
+        "estraverse": "^4.0.0"
+      }
+    },
+    "esrecurse": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
+      "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
+      "dev": true,
+      "requires": {
+        "estraverse": "^4.1.0"
+      }
+    },
+    "estraverse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+      "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+      "dev": true
+    },
+    "esutils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+      "dev": true
+    },
     "extend": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -313,6 +633,17 @@
       "dev": true,
       "optional": true
     },
+    "external-editor": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
+      "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
+      "dev": true,
+      "requires": {
+        "chardet": "^0.7.0",
+        "iconv-lite": "^0.4.24",
+        "tmp": "^0.0.33"
+      }
+    },
     "extsprintf": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
@@ -324,15 +655,54 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
       "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
-      "dev": true,
-      "optional": true
+      "dev": true
     },
     "fast-json-stable-stringify": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
       "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
+      "dev": true
+    },
+    "fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+      "dev": true
+    },
+    "figures": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
+      "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
       "dev": true,
-      "optional": true
+      "requires": {
+        "escape-string-regexp": "^1.0.5"
+      }
+    },
+    "file-entry-cache": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz",
+      "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==",
+      "dev": true,
+      "requires": {
+        "flat-cache": "^2.0.1"
+      }
+    },
+    "flat-cache": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz",
+      "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==",
+      "dev": true,
+      "requires": {
+        "flatted": "^2.0.0",
+        "rimraf": "2.6.3",
+        "write": "1.0.3"
+      }
+    },
+    "flatted": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz",
+      "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==",
+      "dev": true
     },
     "forever-agent": {
       "version": "0.6.1",
@@ -359,6 +729,12 @@
       "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
       "dev": true
     },
+    "functional-red-black-tree": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+      "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
+      "dev": true
+    },
     "getpass": {
       "version": "0.1.7",
       "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
@@ -383,6 +759,24 @@
         "path-is-absolute": "^1.0.0"
       }
     },
+    "glob-parent": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz",
+      "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==",
+      "dev": true,
+      "requires": {
+        "is-glob": "^4.0.1"
+      }
+    },
+    "globals": {
+      "version": "12.3.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-12.3.0.tgz",
+      "integrity": "sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw==",
+      "dev": true,
+      "requires": {
+        "type-fest": "^0.8.1"
+      }
+    },
     "graceful-fs": {
       "version": "4.2.3",
       "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
@@ -425,6 +819,20 @@
       "integrity": "sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==",
       "dev": true
     },
+    "htmlparser2": {
+      "version": "3.10.1",
+      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
+      "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==",
+      "dev": true,
+      "requires": {
+        "domelementtype": "^1.3.1",
+        "domhandler": "^2.3.0",
+        "domutils": "^1.5.1",
+        "entities": "^1.1.1",
+        "inherits": "^2.0.1",
+        "readable-stream": "^3.1.1"
+      }
+    },
     "http-signature": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
@@ -437,6 +845,21 @@
         "sshpk": "^1.7.0"
       }
     },
+    "iconv-lite": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "dev": true,
+      "requires": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      }
+    },
+    "ignore": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
+      "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
+      "dev": true
+    },
     "image-size": {
       "version": "0.5.5",
       "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
@@ -444,6 +867,22 @@
       "dev": true,
       "optional": true
     },
+    "import-fresh": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
+      "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==",
+      "dev": true,
+      "requires": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      }
+    },
+    "imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
+      "dev": true
+    },
     "inflight": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -460,6 +899,54 @@
       "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
       "dev": true
     },
+    "inquirer": {
+      "version": "7.0.4",
+      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz",
+      "integrity": "sha512-Bu5Td5+j11sCkqfqmUTiwv+tWisMtP0L7Q8WrqA2C/BbBhy1YTdFrvjjlrKq8oagA/tLQBski2Gcx/Sqyi2qSQ==",
+      "dev": true,
+      "requires": {
+        "ansi-escapes": "^4.2.1",
+        "chalk": "^2.4.2",
+        "cli-cursor": "^3.1.0",
+        "cli-width": "^2.0.0",
+        "external-editor": "^3.0.3",
+        "figures": "^3.0.0",
+        "lodash": "^4.17.15",
+        "mute-stream": "0.0.8",
+        "run-async": "^2.2.0",
+        "rxjs": "^6.5.3",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^5.1.0",
+        "through": "^2.3.6"
+      }
+    },
+    "is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+      "dev": true
+    },
+    "is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "dev": true
+    },
+    "is-glob": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+      "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
+      "dev": true,
+      "requires": {
+        "is-extglob": "^2.1.1"
+      }
+    },
+    "is-promise": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
+      "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=",
+      "dev": true
+    },
     "is-typedarray": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
@@ -467,6 +954,12 @@
       "dev": true,
       "optional": true
     },
+    "isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+      "dev": true
+    },
     "isstream": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
@@ -489,6 +982,22 @@
       "resolved": "https://registry.npmjs.org/jquery.flot/-/jquery.flot-0.8.3.tgz",
       "integrity": "sha512-/tEE8J5NjwvStHDaCHkvTJpD7wDS4hE1OEL8xEmhgQfUe0gLUem923PIceNez1mz4yBNx6Hjv7pJcowLNd+nbg=="
     },
+    "js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "dev": true
+    },
+    "js-yaml": {
+      "version": "3.13.1",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+      "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+      "dev": true,
+      "requires": {
+        "argparse": "^1.0.7",
+        "esprima": "^4.0.0"
+      }
+    },
     "jsbn": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
@@ -513,8 +1022,13 @@
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
       "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-      "dev": true,
-      "optional": true
+      "dev": true
+    },
+    "json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+      "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
+      "dev": true
     },
     "json-stringify-safe": {
       "version": "5.0.1",
@@ -562,6 +1076,16 @@
         "clean-css": "^3.0.1"
       }
     },
+    "levn": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+      "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "~1.1.2",
+        "type-check": "~0.3.2"
+      }
+    },
     "license-checker": {
       "version": "25.0.1",
       "resolved": "https://registry.npmjs.org/license-checker/-/license-checker-25.0.1.tgz",
@@ -580,6 +1104,12 @@
         "treeify": "^1.1.0"
       }
     },
+    "lodash": {
+      "version": "4.17.15",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+      "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
+      "dev": true
+    },
     "mime": {
       "version": "1.6.0",
       "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
@@ -604,6 +1134,12 @@
         "mime-db": "1.40.0"
       }
     },
+    "mimic-fn": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+      "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+      "dev": true
+    },
     "minimatch": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
@@ -634,6 +1170,24 @@
       "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
       "dev": true
     },
+    "mute-stream": {
+      "version": "0.0.8",
+      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
+      "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
+      "dev": true
+    },
+    "natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
+      "dev": true
+    },
+    "nice-try": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+      "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
+      "dev": true
+    },
     "nopt": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",
@@ -672,6 +1226,29 @@
         "wrappy": "1"
       }
     },
+    "onetime": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz",
+      "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==",
+      "dev": true,
+      "requires": {
+        "mimic-fn": "^2.1.0"
+      }
+    },
+    "optionator": {
+      "version": "0.8.3",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
+      "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
+      "dev": true,
+      "requires": {
+        "deep-is": "~0.1.3",
+        "fast-levenshtein": "~2.0.6",
+        "levn": "~0.3.0",
+        "prelude-ls": "~1.1.2",
+        "type-check": "~0.3.2",
+        "word-wrap": "~1.2.3"
+      }
+    },
     "os-homedir": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
@@ -694,12 +1271,27 @@
         "os-tmpdir": "^1.0.0"
       }
     },
+    "parent-module": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "dev": true,
+      "requires": {
+        "callsites": "^3.0.0"
+      }
+    },
     "path-is-absolute": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
       "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
       "dev": true
     },
+    "path-key": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+      "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
+      "dev": true
+    },
     "path-parse": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
@@ -713,6 +1305,18 @@
       "dev": true,
       "optional": true
     },
+    "prelude-ls": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+      "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
+      "dev": true
+    },
+    "progress": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+      "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+      "dev": true
+    },
     "promise": {
       "version": "7.3.1",
       "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
@@ -741,8 +1345,7 @@
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
       "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
-      "dev": true,
-      "optional": true
+      "dev": true
     },
     "qs": {
       "version": "6.5.2",
@@ -779,6 +1382,17 @@
         "slash": "^1.0.0"
       }
     },
+    "readable-stream": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+      "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+      "dev": true,
+      "requires": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      }
+    },
     "readdir-scoped-modules": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz",
@@ -791,6 +1405,12 @@
         "once": "^1.3.0"
       }
     },
+    "regexpp": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
+      "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==",
+      "dev": true
+    },
     "request": {
       "version": "2.88.0",
       "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
@@ -829,19 +1449,60 @@
         "path-parse": "^1.0.6"
       }
     },
+    "resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "dev": true
+    },
+    "restore-cursor": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+      "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+      "dev": true,
+      "requires": {
+        "onetime": "^5.1.0",
+        "signal-exit": "^3.0.2"
+      }
+    },
+    "rimraf": {
+      "version": "2.6.3",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
+      "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
+      "dev": true,
+      "requires": {
+        "glob": "^7.1.3"
+      }
+    },
+    "run-async": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
+      "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=",
+      "dev": true,
+      "requires": {
+        "is-promise": "^2.1.0"
+      }
+    },
+    "rxjs": {
+      "version": "6.5.4",
+      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz",
+      "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==",
+      "dev": true,
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
     "safe-buffer": {
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
       "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==",
-      "dev": true,
-      "optional": true
+      "dev": true
     },
     "safer-buffer": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
       "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
-      "dev": true,
-      "optional": true
+      "dev": true
     },
     "select2": {
       "version": "3.5.1",
@@ -859,12 +1520,52 @@
       "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
       "dev": true
     },
+    "shebang-command": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+      "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+      "dev": true,
+      "requires": {
+        "shebang-regex": "^1.0.0"
+      }
+    },
+    "shebang-regex": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+      "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
+      "dev": true
+    },
+    "signal-exit": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
+      "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
+      "dev": true
+    },
     "slash": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
       "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=",
       "dev": true
     },
+    "slice-ansi": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz",
+      "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==",
+      "dev": true,
+      "requires": {
+        "ansi-styles": "^3.2.0",
+        "astral-regex": "^1.0.0",
+        "is-fullwidth-code-point": "^2.0.0"
+      },
+      "dependencies": {
+        "is-fullwidth-code-point": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+          "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+          "dev": true
+        }
+      }
+    },
     "slide": {
       "version": "1.1.6",
       "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz",
@@ -938,6 +1639,12 @@
         "spdx-ranges": "^2.0.0"
       }
     },
+    "sprintf-js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+      "dev": true
+    },
     "sshpk": {
       "version": "1.16.1",
       "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
@@ -956,6 +1663,60 @@
         "tweetnacl": "~0.14.0"
       }
     },
+    "string-width": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+      "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
+      "dev": true,
+      "requires": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "dependencies": {
+        "strip-ansi": {
+          "version": "6.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+          "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^5.0.0"
+          }
+        }
+      }
+    },
+    "string_decoder": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+      "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "~5.2.0"
+      }
+    },
+    "strip-ansi": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+      "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+      "dev": true,
+      "requires": {
+        "ansi-regex": "^4.1.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+          "dev": true
+        }
+      }
+    },
+    "strip-json-comments": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz",
+      "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==",
+      "dev": true
+    },
     "supports-color": {
       "version": "5.5.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -965,6 +1726,64 @@
         "has-flag": "^3.0.0"
       }
     },
+    "table": {
+      "version": "5.4.6",
+      "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz",
+      "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==",
+      "dev": true,
+      "requires": {
+        "ajv": "^6.10.2",
+        "lodash": "^4.17.14",
+        "slice-ansi": "^2.1.0",
+        "string-width": "^3.0.0"
+      },
+      "dependencies": {
+        "emoji-regex": {
+          "version": "7.0.3",
+          "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+          "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
+          "dev": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+          "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+          "dev": true
+        },
+        "string-width": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+          "dev": true,
+          "requires": {
+            "emoji-regex": "^7.0.1",
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^5.1.0"
+          }
+        }
+      }
+    },
+    "text-table": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+      "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
+      "dev": true
+    },
+    "through": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+      "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
+      "dev": true
+    },
+    "tmp": {
+      "version": "0.0.33",
+      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+      "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+      "dev": true,
+      "requires": {
+        "os-tmpdir": "~1.0.2"
+      }
+    },
     "tough-cookie": {
       "version": "2.4.3",
       "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
@@ -991,6 +1810,12 @@
       "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==",
       "dev": true
     },
+    "tslib": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.0.tgz",
+      "integrity": "sha512-BmndXUtiTn/VDDrJzQE7Mm22Ix3PxgLltW9bSNLoeCY31gnG2OPx0QqJnuc9oMIKioYrz487i6K9o4Pdn0j+Kg==",
+      "dev": true
+    },
     "tunnel-agent": {
       "version": "0.6.0",
       "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@@ -1008,16 +1833,36 @@
       "dev": true,
       "optional": true
     },
+    "type-check": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+      "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
+      "dev": true,
+      "requires": {
+        "prelude-ls": "~1.1.2"
+      }
+    },
+    "type-fest": {
+      "version": "0.8.1",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
+      "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+      "dev": true
+    },
     "uri-js": {
       "version": "4.2.2",
       "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
       "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
       "dev": true,
-      "optional": true,
       "requires": {
         "punycode": "^2.1.0"
       }
     },
+    "util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
+      "dev": true
+    },
     "util-extend": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz",
@@ -1031,6 +1876,12 @@
       "dev": true,
       "optional": true
     },
+    "v8-compile-cache": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz",
+      "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==",
+      "dev": true
+    },
     "validate-npm-package-license": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
@@ -1053,11 +1904,35 @@
         "extsprintf": "^1.2.0"
       }
     },
+    "which": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+      "dev": true,
+      "requires": {
+        "isexe": "^2.0.0"
+      }
+    },
+    "word-wrap": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
+      "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+      "dev": true
+    },
     "wrappy": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
       "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
       "dev": true
+    },
+    "write": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz",
+      "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==",
+      "dev": true,
+      "requires": {
+        "mkdirp": "^0.5.1"
+      }
     }
   }
 }
--- a/kallithea/front-end/package.json	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/front-end/package.json	Mon May 04 19:24:04 2020 +0200
@@ -14,6 +14,8 @@
     "select2-bootstrap-css": "1.4.6"
   },
   "devDependencies": {
+    "eslint": "6.8.0",
+    "eslint-plugin-html": "6.0.0",
     "less": "3.10.3",
     "less-plugin-clean-css": "1.5.1",
     "license-checker": "25.0.1"
--- a/kallithea/front-end/style.less	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/front-end/style.less	Mon May 04 19:24:04 2020 +0200
@@ -937,8 +937,8 @@
   background-color: @kallithea-theme-main-color;
   border: 0;
 }
-#content #context-pages .follow .show-following,
-#content #context-pages .following .show-follow {
+#content .follow .show-following,
+#content .following .show-follow {
   display: none;
 }
 
--- a/kallithea/i18n/be/LC_MESSAGES/kallithea.po	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/i18n/be/LC_MESSAGES/kallithea.po	Mon May 04 19:24:04 2020 +0200
@@ -5,7 +5,7 @@
 msgstr ""
 "Project-Id-Version: Kallithea 0.3\n"
 "Report-Msgid-Bugs-To: translations@kallithea-scm.org\n"
-"POT-Creation-Date: 2020-01-05 04:10+0100\n"
+"POT-Creation-Date: 2020-05-04 19:12+0200\n"
 "PO-Revision-Date: 2017-08-20 10:44+0000\n"
 "Last-Translator: Viktar Vauchkevich <victorenator@gmail.com>\n"
 "Language-Team: Belarusian <https://hosted.weblate.org/projects/kallithea/"
@@ -19,14 +19,14 @@
 "X-Generator: Weblate 2.17-dev\n"
 
 #: kallithea/controllers/changelog.py:67
-#: kallithea/controllers/pullrequests.py:250 kallithea/lib/base.py:602
+#: kallithea/controllers/pullrequests.py:247 kallithea/lib/base.py:602
 msgid "There are no changesets yet"
 msgstr "Яшчэ не было змен"
 
 #: kallithea/controllers/admin/permissions.py:64
 #: kallithea/controllers/admin/permissions.py:68
 #: kallithea/controllers/admin/permissions.py:72
-#: kallithea/controllers/changelog.py:137
+#: kallithea/controllers/changelog.py:136
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:7
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:88
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:7
@@ -35,38 +35,38 @@
 msgid "None"
 msgstr "Нічога"
 
-#: kallithea/controllers/changelog.py:140 kallithea/controllers/files.py:189
+#: kallithea/controllers/changelog.py:139 kallithea/controllers/files.py:189
 msgid "(closed)"
 msgstr "(зачынена)"
 
-#: kallithea/controllers/changeset.py:81
+#: kallithea/controllers/changeset.py:82
 msgid "Show whitespace"
 msgstr "Паказваць прабелы"
 
-#: kallithea/controllers/changeset.py:88
-#: kallithea/controllers/changeset.py:95
+#: kallithea/controllers/changeset.py:89
+#: kallithea/controllers/changeset.py:96
 #: kallithea/templates/files/diff_2way.html:55
 msgid "Ignore whitespace"
 msgstr "Ігнараваць прабелы"
 
-#: kallithea/controllers/changeset.py:161
+#: kallithea/controllers/changeset.py:162
 #, python-format
 msgid "Increase diff context to %(num)s lines"
 msgstr "Павялічыць кантэкст да %(num)s радкоў"
 
-#: kallithea/controllers/changeset.py:201
+#: kallithea/controllers/changeset.py:202
 #, fuzzy
 #| msgid "No permission to change pull request status"
 msgid "No permission to change status"
 msgstr "Няма правоў змяняць статус pull-запыту"
 
-#: kallithea/controllers/changeset.py:212
+#: kallithea/controllers/changeset.py:213
 #, python-format
 msgid "Successfully deleted pull request %s"
 msgstr "Pull-запыт %s паспяхова выдалены"
 
 #: kallithea/controllers/changeset.py:319 kallithea/controllers/files.py:89
-#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:700
+#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:697
 msgid "Such revision does not exist for this repository"
 msgstr "Няма такой рэвізіі ў гэтым рэпазітары"
 
@@ -79,48 +79,48 @@
 msgid "Cannot compare repositories of different types"
 msgstr "Немагчыма параўноўваць рэпазітары розных тыпаў"
 
-#: kallithea/controllers/compare.py:246
+#: kallithea/controllers/compare.py:247
 msgid "Cannot show empty diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:248
+#: kallithea/controllers/compare.py:249
 msgid "No ancestor found for merge diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:252
+#: kallithea/controllers/compare.py:253
 msgid "Multiple merge ancestors found for merge compare"
 msgstr ""
 
-#: kallithea/controllers/compare.py:268
+#: kallithea/controllers/compare.py:269
 msgid "Cannot compare repositories without using common ancestor"
 msgstr "Немагчыма параўноўваць рэпазітары без агульнага продка"
 
-#: kallithea/controllers/error.py:71
+#: kallithea/controllers/error.py:67
 msgid "No response"
 msgstr "Няма адказу"
 
-#: kallithea/controllers/error.py:72
+#: kallithea/controllers/error.py:68
 msgid "Unknown error"
 msgstr "Невядомая памылка"
 
-#: kallithea/controllers/error.py:85
+#: kallithea/controllers/error.py:81
 msgid ""
 "The request could not be understood by the server due to malformed syntax."
 msgstr "Запыт не распазнаны серверам з-за няправільнага сінтаксісу."
 
-#: kallithea/controllers/error.py:88
+#: kallithea/controllers/error.py:84
 msgid "Unauthorized access to resource"
 msgstr "Несанкцыянаваны доступ да рэсурсу"
 
-#: kallithea/controllers/error.py:90
+#: kallithea/controllers/error.py:86
 msgid "You don't have permission to view this page"
 msgstr "У вас няма правоў для прагляду гэтай старонкі"
 
-#: kallithea/controllers/error.py:92
+#: kallithea/controllers/error.py:88
 msgid "The resource could not be found"
 msgstr "Рэсурс не знойдзены"
 
-#: kallithea/controllers/error.py:94
+#: kallithea/controllers/error.py:90
 msgid ""
 "The server encountered an unexpected condition which prevented it from "
 "fulfilling the request."
@@ -128,14 +128,14 @@
 "Сервер не можа выканаць запыт з-за нечаканых умоваў, якія ўзніклі падчас "
 "яго спрацавання."
 
-#: kallithea/controllers/feed.py:63
+#: kallithea/controllers/feed.py:59
 #, python-format
 msgid "%s committed on %s"
 msgstr "%s выканаў каміт у %s"
 
-#: kallithea/controllers/feed.py:88
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/controllers/feed.py:84
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/compare/compare_diff.html:95
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
@@ -143,12 +143,12 @@
 msgid "Changeset was too big and was cut off..."
 msgstr "Змены апынуліся занадта вялікімі і былі скарочаныя..."
 
-#: kallithea/controllers/feed.py:111 kallithea/controllers/feed.py:140
+#: kallithea/controllers/feed.py:107
 #, python-format
 msgid "%s %s feed"
 msgstr "Стужка навін %s %s"
 
-#: kallithea/controllers/feed.py:113 kallithea/controllers/feed.py:142
+#: kallithea/controllers/feed.py:109
 #, python-format
 msgid "Changes on %s repository"
 msgstr "Змены ў рэпазітары %s"
@@ -168,106 +168,106 @@
 msgid "%s at %s"
 msgstr "%s (%s)"
 
-#: kallithea/controllers/files.py:296
+#: kallithea/controllers/files.py:295
 msgid "You can only delete files with revision being a valid branch"
 msgstr "Вы можаце выдаляць файлы толькі ў рэвізіі, злучанай з існай галінай"
 
-#: kallithea/controllers/files.py:307
+#: kallithea/controllers/files.py:306
 #, python-format
 msgid "Deleted file %s via Kallithea"
 msgstr "Файл %s выдалены з дапамогай Kallithea"
 
-#: kallithea/controllers/files.py:331
+#: kallithea/controllers/files.py:330
 #, python-format
 msgid "Successfully deleted file %s"
 msgstr "Файл %s выдалены"
 
-#: kallithea/controllers/files.py:335 kallithea/controllers/files.py:394
-#: kallithea/controllers/files.py:469
+#: kallithea/controllers/files.py:334 kallithea/controllers/files.py:392
+#: kallithea/controllers/files.py:467
 msgid "Error occurred during commit"
 msgstr "Падчас каміта адбылася памылка"
 
-#: kallithea/controllers/files.py:350
+#: kallithea/controllers/files.py:349
 msgid "You can only edit files with revision being a valid branch"
 msgstr ""
 "Вы можаце рэдагаваць файлы толькі ў рэвізіі, злучанай з існай галінай"
 
-#: kallithea/controllers/files.py:364
+#: kallithea/controllers/files.py:363
 #, python-format
 msgid "Edited file %s via Kallithea"
 msgstr "Файл %s адрэдагаваны з дапамогай Kallithea"
 
-#: kallithea/controllers/files.py:380
+#: kallithea/controllers/files.py:378
 msgid "No changes"
 msgstr "Без змен"
 
-#: kallithea/controllers/files.py:390 kallithea/controllers/files.py:458
+#: kallithea/controllers/files.py:388 kallithea/controllers/files.py:456
 #, python-format
 msgid "Successfully committed to %s"
 msgstr "Змены захаваныя ў %s"
 
-#: kallithea/controllers/files.py:409
+#: kallithea/controllers/files.py:407
 msgid "Added file via Kallithea"
 msgstr "Файл дададзены з дапамогай Kallithea"
 
-#: kallithea/controllers/files.py:430
+#: kallithea/controllers/files.py:428
 msgid "No content"
 msgstr "Пуста"
 
-#: kallithea/controllers/files.py:434
+#: kallithea/controllers/files.py:432
 msgid "No filename"
 msgstr "Безназоўны"
 
-#: kallithea/controllers/files.py:461
+#: kallithea/controllers/files.py:459
 msgid "Location must be relative path and must not contain .. in path"
 msgstr ""
 "Размяшчэнне павінна быць адносным шляхам, і не можа ўтрымліваць \"..\" у "
 "шляхі"
 
-#: kallithea/controllers/files.py:493
+#: kallithea/controllers/files.py:491
 msgid "Downloads disabled"
 msgstr "Магчымасць спампоўваць адключаная"
 
-#: kallithea/controllers/files.py:504
+#: kallithea/controllers/files.py:502
 #, python-format
 msgid "Unknown revision %s"
 msgstr "Невядомая рэвізія %s"
 
-#: kallithea/controllers/files.py:506
+#: kallithea/controllers/files.py:504
 msgid "Empty repository"
 msgstr "Пусты рэпазітар"
 
-#: kallithea/controllers/files.py:508
+#: kallithea/controllers/files.py:506
 msgid "Unknown archive type"
 msgstr "Невядомы тып архіва"
 
-#: kallithea/controllers/files.py:729
+#: kallithea/controllers/files.py:726
 #: kallithea/templates/changeset/changeset_range.html:9
 #: kallithea/templates/email_templates/pull_request.html:64
 #: kallithea/templates/pullrequests/pullrequest.html:84
 msgid "Changesets"
 msgstr "Набор змен"
 
-#: kallithea/controllers/files.py:730
-#: kallithea/controllers/pullrequests.py:182 kallithea/model/scm.py:676
+#: kallithea/controllers/files.py:727
+#: kallithea/controllers/pullrequests.py:174 kallithea/model/scm.py:663
 msgid "Branches"
 msgstr "Галіны"
 
-#: kallithea/controllers/files.py:731
-#: kallithea/controllers/pullrequests.py:183 kallithea/model/scm.py:687
+#: kallithea/controllers/files.py:728
+#: kallithea/controllers/pullrequests.py:175 kallithea/model/scm.py:674
 msgid "Tags"
 msgstr "Тэгі"
 
-#: kallithea/controllers/forks.py:174
+#: kallithea/controllers/forks.py:175
 #, python-format
 msgid "An error occurred during repository forking %s"
 msgstr "Памылка падчас стварэння форка рэпазітара %s"
 
-#: kallithea/controllers/home.py:79
+#: kallithea/controllers/home.py:77
 msgid "Groups"
 msgstr "Групы"
 
-#: kallithea/controllers/home.py:89
+#: kallithea/controllers/home.py:87
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:90
 #: kallithea/templates/admin/repos/repo_add.html:12
 #: kallithea/templates/admin/repos/repo_add.html:16
@@ -279,48 +279,54 @@
 msgid "Repositories"
 msgstr "Рэпазітары"
 
-#: kallithea/controllers/home.py:122
+#: kallithea/controllers/home.py:119
 #: kallithea/templates/files/files_add.html:32
 #: kallithea/templates/files/files_delete.html:23
 #: kallithea/templates/files/files_edit.html:32
 msgid "Branch"
 msgstr "Галіна"
 
-#: kallithea/controllers/home.py:128
+#: kallithea/controllers/home.py:125
 msgid "Closed Branches"
 msgstr "Зачыненыя галіны"
 
-#: kallithea/controllers/home.py:134
+#: kallithea/controllers/home.py:131
 msgid "Tag"
 msgstr "Тэгі"
 
-#: kallithea/controllers/home.py:140
+#: kallithea/controllers/home.py:137
 msgid "Bookmark"
 msgstr "Закладкі"
 
-#: kallithea/controllers/journal.py:112 kallithea/controllers/journal.py:154
+#: kallithea/controllers/journal.py:146 kallithea/controllers/journal.py:157
 #: kallithea/templates/journal/public_journal.html:4
 #: kallithea/templates/journal/public_journal.html:18
 msgid "Public Journal"
 msgstr "Публічны журнал"
 
-#: kallithea/controllers/journal.py:116 kallithea/controllers/journal.py:158
+#: kallithea/controllers/journal.py:150 kallithea/controllers/journal.py:161
 #: kallithea/templates/base/base.html:290
 #: kallithea/templates/journal/journal.html:5
 #: kallithea/templates/journal/journal.html:13
 msgid "Journal"
 msgstr "Журнал"
 
-#: kallithea/controllers/login.py:140 kallithea/controllers/login.py:185
+#: kallithea/controllers/login.py:109
+#, fuzzy
+#| msgid "Authentication"
+msgid "Authentication failed."
+msgstr "Аўтэнтыфікацыя"
+
+#: kallithea/controllers/login.py:142 kallithea/controllers/login.py:187
 msgid "Bad captcha"
 msgstr "Няслушная капча"
 
-#: kallithea/controllers/login.py:146
+#: kallithea/controllers/login.py:148
 #, python-format
 msgid "You have successfully registered with %s"
 msgstr "Рэгістрацыя ў %s прайшла паспяхова"
 
-#: kallithea/controllers/login.py:190
+#: kallithea/controllers/login.py:192
 msgid "A password reset confirmation code has been sent"
 msgstr "Код для скідання пароля адпраўлены"
 
@@ -333,236 +339,236 @@
 msgid "Successfully updated password"
 msgstr "Пароль абноўлены"
 
-#: kallithea/controllers/pullrequests.py:68
+#: kallithea/controllers/pullrequests.py:67
 #, python-format
 msgid "Invalid reviewer \"%s\" specified"
 msgstr "Няслушны рэцэнзент \"%s\""
 
-#: kallithea/controllers/pullrequests.py:131
+#: kallithea/controllers/pullrequests.py:123
 #, python-format
 msgid "%s (closed)"
 msgstr "%s (зачынена)"
 
-#: kallithea/controllers/pullrequests.py:158
+#: kallithea/controllers/pullrequests.py:150
 #: kallithea/templates/changeset/changeset.html:12
 msgid "Changeset"
 msgstr "Змены"
 
-#: kallithea/controllers/pullrequests.py:179
+#: kallithea/controllers/pullrequests.py:171
 msgid "Special"
 msgstr "Адмысловы"
 
-#: kallithea/controllers/pullrequests.py:180
+#: kallithea/controllers/pullrequests.py:172
 msgid "Peer branches"
 msgstr "Галіны ўдзельніка"
 
-#: kallithea/controllers/pullrequests.py:181 kallithea/model/scm.py:682
+#: kallithea/controllers/pullrequests.py:173 kallithea/model/scm.py:669
 msgid "Bookmarks"
 msgstr "Закладкі"
 
-#: kallithea/controllers/pullrequests.py:318
+#: kallithea/controllers/pullrequests.py:315
 #, python-format
 msgid "Error creating pull request: %s"
 msgstr "Памылка пры стварэнні pull-запыту: %s"
 
-#: kallithea/controllers/pullrequests.py:345
-#: kallithea/controllers/pullrequests.py:368
+#: kallithea/controllers/pullrequests.py:342
+#: kallithea/controllers/pullrequests.py:365
 msgid "Error occurred while creating pull request"
 msgstr "Адбылася памылка пры стварэнні pull-запыту"
 
-#: kallithea/controllers/pullrequests.py:350
+#: kallithea/controllers/pullrequests.py:347
 msgid "Successfully opened new pull request"
 msgstr "Pull-запыт створаны паспяхова"
 
-#: kallithea/controllers/pullrequests.py:373
+#: kallithea/controllers/pullrequests.py:370
 #, fuzzy
 #| msgid "Pull request update created"
 msgid "New pull request iteration created"
 msgstr "Абнаўленне для pull-запыту створана"
 
-#: kallithea/controllers/pullrequests.py:401
+#: kallithea/controllers/pullrequests.py:398
 #, python-format
 msgid "Meanwhile, the following reviewers have been added: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:405
+#: kallithea/controllers/pullrequests.py:402
 #, python-format
 msgid "Meanwhile, the following reviewers have been removed: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:421
-#: kallithea/model/pull_request.py:232
+#: kallithea/controllers/pullrequests.py:418
+#: kallithea/model/pull_request.py:230
 msgid "No description"
 msgstr "Няма апісання"
 
-#: kallithea/controllers/pullrequests.py:430
+#: kallithea/controllers/pullrequests.py:427
 msgid "Pull request updated"
 msgstr "Pull-запыт абноўлены"
 
-#: kallithea/controllers/pullrequests.py:443
+#: kallithea/controllers/pullrequests.py:440
 msgid "Successfully deleted pull request"
 msgstr "Pull-запыт паспяхова выдалены"
 
-#: kallithea/controllers/pullrequests.py:479
+#: kallithea/controllers/pullrequests.py:476
 #, fuzzy, python-format
 #| msgid "Changeset for %s %s not found in %s"
 msgid "Revision %s not found in %s"
 msgstr "Набор змен для %s %s не знойдзены ў %s"
 
-#: kallithea/controllers/pullrequests.py:506
+#: kallithea/controllers/pullrequests.py:504
 #, fuzzy, python-format
 #| msgid "No changesets found for updating this pull request."
 msgid "Error: changesets not found when displaying pull request from %s."
 msgstr "Няма змен для абнаўлення гэтага pull-запыту."
 
-#: kallithea/controllers/pullrequests.py:520
+#: kallithea/controllers/pullrequests.py:518
 #, python-format
 msgid "This pull request has already been merged to %s."
 msgstr "Гэты pull-запыт ужо прыняты на галіну %s."
 
-#: kallithea/controllers/pullrequests.py:522
+#: kallithea/controllers/pullrequests.py:520
 msgid "This pull request has been closed and can not be updated."
 msgstr "Гэты pull-запыт быў зачынены і не можа быць абноўлены."
 
-#: kallithea/controllers/pullrequests.py:546
+#: kallithea/controllers/pullrequests.py:539
 #, fuzzy, python-format
 #| msgid "The following changes are available on %s:"
 msgid "The following additional changes are available on %s:"
 msgstr "Гэтыя змены даступныя на %s:"
 
-#: kallithea/controllers/pullrequests.py:548
-#: kallithea/controllers/pullrequests.py:552
+#: kallithea/controllers/pullrequests.py:541
+#: kallithea/controllers/pullrequests.py:545
 #, fuzzy
 #| msgid "No changesets found for updating this pull request."
 msgid "No additional changesets found for iterating on this pull request."
 msgstr "Няма змен для абнаўлення гэтага pull-запыту."
 
-#: kallithea/controllers/pullrequests.py:560
+#: kallithea/controllers/pullrequests.py:553
 #, python-format
 msgid "Note: Branch %s has another head: %s."
 msgstr "Увага: Галіна %s мае яшчэ адну верхавіну: %s."
 
-#: kallithea/controllers/pullrequests.py:567
+#: kallithea/controllers/pullrequests.py:560
 #, fuzzy
 #| msgid "Git pull requests don't support updates yet."
 msgid "Git pull requests don't support iterating yet."
 msgstr "Абнаўленне pull-запытаў git яшчэ не падтрымліваецца."
 
-#: kallithea/controllers/pullrequests.py:569
+#: kallithea/controllers/pullrequests.py:562
 #, fuzzy, python-format
 #| msgid "No changesets found for updating this pull request."
 msgid ""
 "Error: some changesets not found when displaying pull request from %s."
 msgstr "Няма змен для абнаўлення гэтага pull-запыту."
 
-#: kallithea/controllers/pullrequests.py:593
+#: kallithea/controllers/pullrequests.py:586
 msgid "The diff can't be shown - the PR revisions could not be found."
 msgstr ""
 
-#: kallithea/controllers/search.py:136
+#: kallithea/controllers/search.py:132
 msgid "Invalid search query. Try quoting it."
 msgstr "Недапушчальны пошукавы запыт. Паспрабуйце скласці яго ў двукоссі."
 
-#: kallithea/controllers/search.py:140
+#: kallithea/controllers/search.py:136
 msgid "The server has no search index."
 msgstr ""
 
-#: kallithea/controllers/search.py:143
+#: kallithea/controllers/search.py:139
 msgid "An error occurred during search operation."
 msgstr "Памылка пры выкананні гэтага пошуку."
 
-#: kallithea/controllers/summary.py:168
-#: kallithea/templates/summary/summary.html:412
+#: kallithea/controllers/summary.py:169
+#: kallithea/templates/summary/summary.html:410
 msgid "No data ready yet"
 msgstr "Няма дадзеных"
 
-#: kallithea/controllers/summary.py:171
+#: kallithea/controllers/summary.py:172
 #: kallithea/templates/summary/summary.html:97
 msgid "Statistics are disabled for this repository"
 msgstr "Статыстычныя дадзеныя адключаны для гэтага рэпазітара"
 
-#: kallithea/controllers/admin/auth_settings.py:137
+#: kallithea/controllers/admin/auth_settings.py:136
 msgid "Auth settings updated successfully"
 msgstr "Налады аўтарызацыі паспяхова абноўлены"
 
-#: kallithea/controllers/admin/auth_settings.py:148
+#: kallithea/controllers/admin/auth_settings.py:147
 msgid "error occurred during update of auth settings"
 msgstr "памылка пры абнаўленні налад аўтарызацыі"
 
-#: kallithea/controllers/admin/defaults.py:75
+#: kallithea/controllers/admin/defaults.py:74
 msgid "Default settings updated successfully"
 msgstr "Стандартныя налады паспяхова абноўлены"
 
-#: kallithea/controllers/admin/defaults.py:90
+#: kallithea/controllers/admin/defaults.py:89
 msgid "Error occurred during update of defaults"
 msgstr "Памылка пры абнаўленні стандартных налад"
 
 #: kallithea/controllers/admin/gists.py:59
 #: kallithea/controllers/admin/my_account.py:232
-#: kallithea/controllers/admin/users.py:248
+#: kallithea/controllers/admin/users.py:246
 msgid "Forever"
 msgstr "Назаўжды"
 
 #: kallithea/controllers/admin/gists.py:60
 #: kallithea/controllers/admin/my_account.py:233
-#: kallithea/controllers/admin/users.py:249
+#: kallithea/controllers/admin/users.py:247
 msgid "5 minutes"
 msgstr "5 хвілін"
 
 #: kallithea/controllers/admin/gists.py:61
 #: kallithea/controllers/admin/my_account.py:234
-#: kallithea/controllers/admin/users.py:250
+#: kallithea/controllers/admin/users.py:248
 msgid "1 hour"
 msgstr "1 гадзіна"
 
 #: kallithea/controllers/admin/gists.py:62
 #: kallithea/controllers/admin/my_account.py:235
-#: kallithea/controllers/admin/users.py:251
+#: kallithea/controllers/admin/users.py:249
 msgid "1 day"
 msgstr "1 дзень"
 
 #: kallithea/controllers/admin/gists.py:63
 #: kallithea/controllers/admin/my_account.py:236
-#: kallithea/controllers/admin/users.py:252
+#: kallithea/controllers/admin/users.py:250
 msgid "1 month"
 msgstr "1 месяц"
 
 #: kallithea/controllers/admin/gists.py:67
 #: kallithea/controllers/admin/my_account.py:238
-#: kallithea/controllers/admin/users.py:254
+#: kallithea/controllers/admin/users.py:252
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:65
 #: kallithea/templates/admin/users/user_edit_api_keys.html:65
 msgid "Lifetime"
 msgstr "Тэрмін"
 
-#: kallithea/controllers/admin/gists.py:142
+#: kallithea/controllers/admin/gists.py:148
 msgid "Error occurred during gist creation"
 msgstr "Адбылася памылка падчас стварэння gist-запіса"
 
-#: kallithea/controllers/admin/gists.py:158
+#: kallithea/controllers/admin/gists.py:164
 #, python-format
 msgid "Deleted gist %s"
 msgstr "Gist-запіс %s выдалены"
 
-#: kallithea/controllers/admin/gists.py:198
+#: kallithea/controllers/admin/gists.py:207
 msgid "Unmodified"
 msgstr "Без змен"
 
-#: kallithea/controllers/admin/gists.py:228
+#: kallithea/controllers/admin/gists.py:237
 msgid "Successfully updated gist content"
 msgstr "Gist-запіс абноўлены"
 
-#: kallithea/controllers/admin/gists.py:233
+#: kallithea/controllers/admin/gists.py:242
 msgid "Successfully updated gist data"
 msgstr "Gist-запіс абноўлены"
 
-#: kallithea/controllers/admin/gists.py:236
+#: kallithea/controllers/admin/gists.py:245
 #, python-format
 msgid "Error occurred during update of gist %s"
 msgstr "Памылка пры абнаўленні gist-запісу %s"
 
-#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:211
-#: kallithea/model/user.py:232
+#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:205
+#: kallithea/model/user.py:226
 msgid "You can't edit this user since it's crucial for entire application"
 msgstr ""
 "Вы не можаце змяніць дадзеныя гэтага карыстальніка, паколькі ён важны для "
@@ -573,7 +579,7 @@
 msgstr "Ваш уліковы запіс паспяхова абноўлены"
 
 #: kallithea/controllers/admin/my_account.py:134
-#: kallithea/controllers/admin/users.py:181
+#: kallithea/controllers/admin/users.py:179
 #, python-format
 msgid "Error occurred during update of user %s"
 msgstr "Памылка пры абнаўленні карыстальніка %s"
@@ -583,45 +589,45 @@
 msgstr "Памылка пры абнаўленні пароля"
 
 #: kallithea/controllers/admin/my_account.py:209
-#: kallithea/controllers/admin/users.py:367
+#: kallithea/controllers/admin/users.py:365
 #, python-format
 msgid "Added email %s to user"
 msgstr "Карыстальніку дададзены e-mail %s"
 
 #: kallithea/controllers/admin/my_account.py:215
-#: kallithea/controllers/admin/users.py:373
+#: kallithea/controllers/admin/users.py:371
 msgid "An error occurred during email saving"
 msgstr "Памылка пры захаванні e-mail"
 
 #: kallithea/controllers/admin/my_account.py:224
-#: kallithea/controllers/admin/users.py:383
+#: kallithea/controllers/admin/users.py:381
 msgid "Removed email from user"
 msgstr "E-mail карыстальніка выдалены"
 
 #: kallithea/controllers/admin/my_account.py:248
-#: kallithea/controllers/admin/users.py:271
+#: kallithea/controllers/admin/users.py:269
 msgid "API key successfully created"
 msgstr "API-ключ паспяхова створаны"
 
 #: kallithea/controllers/admin/my_account.py:257
-#: kallithea/controllers/admin/users.py:281
+#: kallithea/controllers/admin/users.py:279
 msgid "API key successfully reset"
 msgstr "API-ключ паспяхова скінуты"
 
 #: kallithea/controllers/admin/my_account.py:261
-#: kallithea/controllers/admin/users.py:285
+#: kallithea/controllers/admin/users.py:283
 msgid "API key successfully deleted"
 msgstr "API-ключ паспяхова выдалены"
 
 #: kallithea/controllers/admin/my_account.py:281
-#: kallithea/controllers/admin/users.py:456
+#: kallithea/controllers/admin/users.py:454
 #, fuzzy, python-format
 #| msgid "API key successfully created"
 msgid "SSH key %s successfully added"
 msgstr "API-ключ паспяхова створаны"
 
 #: kallithea/controllers/admin/my_account.py:293
-#: kallithea/controllers/admin/users.py:470
+#: kallithea/controllers/admin/users.py:468
 #, fuzzy
 #| msgid "API key successfully deleted"
 msgid "SSH key successfully deleted"
@@ -699,11 +705,11 @@
 msgid "Allowed with automatic account activation"
 msgstr "Дазволена, з аўтаматычнай актывацыяй уліковага запісу"
 
-#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1673
+#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1578
 msgid "Manual activation of external account"
 msgstr "Ручная актывацыя вонкавага ўліковага запісу"
 
-#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1674
+#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1579
 msgid "Automatic activation of external account"
 msgstr "Аўтаматычная актывацыя вонкавага ўліковага запісу"
 
@@ -725,186 +731,178 @@
 msgid "Error occurred during update of permissions"
 msgstr "Адбылася памылка падчас абнаўлення прывілеяў"
 
-#: kallithea/controllers/admin/repo_groups.py:172
+#: kallithea/controllers/admin/repo_groups.py:165
 #, python-format
 msgid "Error occurred during creation of repository group %s"
 msgstr "Адбылася памылка пры стварэнні групы рэпазітароў %s"
 
-#: kallithea/controllers/admin/repo_groups.py:177
+#: kallithea/controllers/admin/repo_groups.py:172
 #, python-format
 msgid "Created repository group %s"
 msgstr "Створаная новая група рэпазітароў %s"
 
-#: kallithea/controllers/admin/repo_groups.py:224
+#: kallithea/controllers/admin/repo_groups.py:219
 #, python-format
 msgid "Updated repository group %s"
 msgstr "Група рэпазітароў %s абноўленая"
 
-#: kallithea/controllers/admin/repo_groups.py:240
+#: kallithea/controllers/admin/repo_groups.py:235
 #, python-format
 msgid "Error occurred during update of repository group %s"
 msgstr "Адбылася памылка пры абнаўленні групы рэпазітароў %s"
 
-#: kallithea/controllers/admin/repo_groups.py:250
+#: kallithea/controllers/admin/repo_groups.py:245
 #, python-format
 msgid "This group contains %s repositories and cannot be deleted"
 msgstr "Група ўтрымлівае %s рэпазітароў і не можа быць выдаленая"
 
-#: kallithea/controllers/admin/repo_groups.py:257
+#: kallithea/controllers/admin/repo_groups.py:252
 #, python-format
 msgid "This group contains %s subgroups and cannot be deleted"
 msgstr "Група ўтрымлівае ў сабе %s падгруп і не можа быць выдаленая"
 
-#: kallithea/controllers/admin/repo_groups.py:263
+#: kallithea/controllers/admin/repo_groups.py:258
 #, python-format
 msgid "Removed repository group %s"
 msgstr "Група рэпазітароў %s выдаленая"
 
-#: kallithea/controllers/admin/repo_groups.py:268
+#: kallithea/controllers/admin/repo_groups.py:263
 #, python-format
 msgid "Error occurred during deletion of repository group %s"
 msgstr "Памылка пры выдаленні групы рэпазітароў %s"
 
-#: kallithea/controllers/admin/repo_groups.py:352
-#: kallithea/controllers/admin/repo_groups.py:382
-#: kallithea/controllers/admin/user_groups.py:294
+#: kallithea/controllers/admin/repo_groups.py:347
+#: kallithea/controllers/admin/repo_groups.py:377
+#: kallithea/controllers/admin/user_groups.py:290
 msgid "Cannot revoke permission for yourself as admin"
 msgstr "Адміністратар не можа адклікаць свае прывелеі"
 
-#: kallithea/controllers/admin/repo_groups.py:367
+#: kallithea/controllers/admin/repo_groups.py:362
 msgid "Repository group permissions updated"
 msgstr "Прывілеі групы рэпазітароў абноўленыя"
 
-#: kallithea/controllers/admin/repo_groups.py:399
-#: kallithea/controllers/admin/repos.py:358
-#: kallithea/controllers/admin/user_groups.py:306
+#: kallithea/controllers/admin/repo_groups.py:394
+#: kallithea/controllers/admin/repos.py:357
+#: kallithea/controllers/admin/user_groups.py:302
 msgid "An error occurred during revoking of permission"
 msgstr "Памылка пры водгуку прывелея"
 
-#: kallithea/controllers/admin/repos.py:136
+#: kallithea/controllers/admin/repos.py:137
 #, python-format
 msgid "Error creating repository %s"
 msgstr "Памылка пры стварэнні рэпазітара %s"
 
-#: kallithea/controllers/admin/repos.py:194
+#: kallithea/controllers/admin/repos.py:193
 #, python-format
 msgid "Created repository %s from %s"
 msgstr "Рэпазітар %s створаны з %s"
 
-#: kallithea/controllers/admin/repos.py:203
+#: kallithea/controllers/admin/repos.py:202
 #, python-format
 msgid "Forked repository %s as %s"
 msgstr "Зроблены форк рэпазітара %s на %s"
 
-#: kallithea/controllers/admin/repos.py:206
+#: kallithea/controllers/admin/repos.py:205
 #, python-format
 msgid "Created repository %s"
 msgstr "Рэпазітар %s створаны"
 
-#: kallithea/controllers/admin/repos.py:235
+#: kallithea/controllers/admin/repos.py:234
 #, python-format
 msgid "Repository %s updated successfully"
 msgstr "Рэпазітар %s паспяхова абноўлены"
 
-#: kallithea/controllers/admin/repos.py:255
+#: kallithea/controllers/admin/repos.py:254
 #, python-format
 msgid "Error occurred during update of repository %s"
 msgstr "Памылка падчас абнаўлення рэпазітара %s"
 
-#: kallithea/controllers/admin/repos.py:273
+#: kallithea/controllers/admin/repos.py:272
 #, python-format
 msgid "Detached %s forks"
 msgstr "Форкі %s адлучаныя"
 
-#: kallithea/controllers/admin/repos.py:276
+#: kallithea/controllers/admin/repos.py:275
 #, python-format
 msgid "Deleted %s forks"
 msgstr "Выдаленыя форки рэпазітара %s"
 
-#: kallithea/controllers/admin/repos.py:281
+#: kallithea/controllers/admin/repos.py:280
 #, python-format
 msgid "Deleted repository %s"
 msgstr "Рэпазітар %s выдалены"
 
-#: kallithea/controllers/admin/repos.py:284
+#: kallithea/controllers/admin/repos.py:283
 #, python-format
 msgid "Cannot delete repository %s which still has forks"
 msgstr "Немагчыма выдаліць %s, ён усё яшчэ мае форкі"
 
-#: kallithea/controllers/admin/repos.py:289
+#: kallithea/controllers/admin/repos.py:288
 #, python-format
 msgid "An error occurred during deletion of %s"
 msgstr "Памылка падчас выдалення %s"
 
-#: kallithea/controllers/admin/repos.py:329
+#: kallithea/controllers/admin/repos.py:328
 msgid "Repository permissions updated"
 msgstr "Прывілеі рэпазітара абноўленыя"
 
-#: kallithea/controllers/admin/repos.py:388
+#: kallithea/controllers/admin/repos.py:387
 #, python-format
 msgid "Field validation error: %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:391
+#: kallithea/controllers/admin/repos.py:390
 #, fuzzy, python-format
 #| msgid "An error occurred during creation of field"
 msgid "An error occurred during creation of field: %r"
 msgstr "Памылка пры стварэнні поля"
 
-#: kallithea/controllers/admin/repos.py:402
+#: kallithea/controllers/admin/repos.py:401
 msgid "An error occurred during removal of field"
 msgstr "Памылка пры выдаленні поля"
 
-#: kallithea/controllers/admin/repos.py:416
+#: kallithea/controllers/admin/repos.py:415
 msgid "-- Not a fork --"
 msgstr "-- Не форк --"
 
-#: kallithea/controllers/admin/repos.py:448
+#: kallithea/controllers/admin/repos.py:447
 msgid "Updated repository visibility in public journal"
 msgstr "Бачнасць рэпазітара ў публічным часопісе абноўлена"
 
-#: kallithea/controllers/admin/repos.py:452
+#: kallithea/controllers/admin/repos.py:451
 msgid "An error occurred during setting this repository in public journal"
 msgstr "Памылка пры даданні рэпазітара ў агульнадаступны часопіс"
 
-#: kallithea/controllers/admin/repos.py:468
+#: kallithea/controllers/admin/repos.py:467
 msgid "Nothing"
 msgstr "Нічога"
 
-#: kallithea/controllers/admin/repos.py:470
+#: kallithea/controllers/admin/repos.py:469
 #, python-format
 msgid "Marked repository %s as fork of %s"
 msgstr "Рэпазітар %s адзначаны як форк %s"
 
-#: kallithea/controllers/admin/repos.py:477
+#: kallithea/controllers/admin/repos.py:476
 msgid "An error occurred during this operation"
 msgstr "Памылка пры выкананні аперацыі"
 
-#: kallithea/controllers/admin/repos.py:490
-msgid "Cache invalidation successful"
-msgstr "Кэш скінуты"
-
-#: kallithea/controllers/admin/repos.py:494
-msgid "An error occurred during cache invalidation"
-msgstr "Памылка пры скіданні кэша"
-
-#: kallithea/controllers/admin/repos.py:507
+#: kallithea/controllers/admin/repos.py:488
 msgid "Pulled from remote location"
 msgstr "Занесеныя змены з аддаленага рэпазітара"
 
-#: kallithea/controllers/admin/repos.py:510
+#: kallithea/controllers/admin/repos.py:491
 msgid "An error occurred during pull from remote location"
 msgstr "Памылка пры занясенні змен з аддаленага рэпазітара"
 
-#: kallithea/controllers/admin/repos.py:541
+#: kallithea/controllers/admin/repos.py:522
 msgid "An error occurred during deletion of repository stats"
 msgstr "Адбылася памылка пры выдаленні статыстыкі рэпазітара"
 
-#: kallithea/controllers/admin/settings.py:131
+#: kallithea/controllers/admin/settings.py:132
 msgid "Updated VCS settings"
 msgstr "Абноўлены налады VCS"
 
-#: kallithea/controllers/admin/settings.py:135 kallithea/lib/utils.py:237
+#: kallithea/controllers/admin/settings.py:136
 msgid ""
 "Unable to activate hgsubversion support. The \"hgsubversion\" library is "
 "missing"
@@ -912,158 +910,158 @@
 "Немагчыма ўключыць падтрымку hgsubversion. Бібліятэка hgsubversion "
 "адсутнічае"
 
-#: kallithea/controllers/admin/settings.py:141
-#: kallithea/controllers/admin/settings.py:233
+#: kallithea/controllers/admin/settings.py:142
+#: kallithea/controllers/admin/settings.py:234
 msgid "Error occurred while updating application settings"
 msgstr "Памылка пры абнаўленні наладаў праграмы"
 
-#: kallithea/controllers/admin/settings.py:176
+#: kallithea/controllers/admin/settings.py:177
 #, python-format
 msgid "Repositories successfully rescanned. Added: %s. Removed: %s."
 msgstr "Рэпазітары паспяхова перасканаваныя, дададзена: %s, выдалена: %s."
 
-#: kallithea/controllers/admin/settings.py:188
+#: kallithea/controllers/admin/settings.py:189
 #, python-format
 msgid "Invalidated %s repositories"
 msgstr "Скінуць кэш для %s рэпазітароў"
 
-#: kallithea/controllers/admin/settings.py:229
+#: kallithea/controllers/admin/settings.py:230
 msgid "Updated application settings"
 msgstr "Абноўленыя налады праграмы"
 
-#: kallithea/controllers/admin/settings.py:283
+#: kallithea/controllers/admin/settings.py:284
 msgid "Updated visualisation settings"
 msgstr "Налады візуалізацыі абноўленыя"
 
-#: kallithea/controllers/admin/settings.py:288
+#: kallithea/controllers/admin/settings.py:289
 msgid "Error occurred during updating visualisation settings"
 msgstr "Адбылася памылка пры абнаўленні наладаў візуалізацыі"
 
-#: kallithea/controllers/admin/settings.py:312
+#: kallithea/controllers/admin/settings.py:313
 msgid "Please enter email address"
 msgstr "Калі ласка, увядзіце e-mail-адрас"
 
-#: kallithea/controllers/admin/settings.py:327
+#: kallithea/controllers/admin/settings.py:328
 msgid "Send email task created"
 msgstr "Задача адпраўкі e-mail створаная"
 
-#: kallithea/controllers/admin/settings.py:355
+#: kallithea/controllers/admin/settings.py:356
 #, fuzzy
 #| msgid "No data ready yet"
 msgid "Hook already exists"
 msgstr "Няма дадзеных"
 
-#: kallithea/controllers/admin/settings.py:357
+#: kallithea/controllers/admin/settings.py:358
 msgid "Builtin hooks are read-only. Please use another hook name."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:360
+#: kallithea/controllers/admin/settings.py:361
 msgid "Added new hook"
 msgstr "Дададзены новы хук"
 
-#: kallithea/controllers/admin/settings.py:376
+#: kallithea/controllers/admin/settings.py:377
 msgid "Updated hooks"
 msgstr "Абноўленыя хукі"
 
-#: kallithea/controllers/admin/settings.py:380
+#: kallithea/controllers/admin/settings.py:381
 msgid "Error occurred during hook creation"
 msgstr "Памылка пры стварэнні хука"
 
-#: kallithea/controllers/admin/settings.py:404
+#: kallithea/controllers/admin/settings.py:405
 msgid "Whoosh reindex task scheduled"
 msgstr "Запланаванае пераіндэксаванне базы Whoosh"
 
-#: kallithea/controllers/admin/user_groups.py:138
+#: kallithea/controllers/admin/user_groups.py:134
 #, python-format
 msgid "Created user group %s"
 msgstr "Створана група карыстальнікаў %s"
 
-#: kallithea/controllers/admin/user_groups.py:151
+#: kallithea/controllers/admin/user_groups.py:147
 #, python-format
 msgid "Error occurred during creation of user group %s"
 msgstr "Памылка пры стварэнні групы карыстальнікаў %s"
 
-#: kallithea/controllers/admin/user_groups.py:179
+#: kallithea/controllers/admin/user_groups.py:175
 #, python-format
 msgid "Updated user group %s"
 msgstr "Група карыстальнікаў %s абноўленая"
 
-#: kallithea/controllers/admin/user_groups.py:201
+#: kallithea/controllers/admin/user_groups.py:197
 #, python-format
 msgid "Error occurred during update of user group %s"
 msgstr "Памылка пры абнаўленні групы карыстальнікаў %s"
 
-#: kallithea/controllers/admin/user_groups.py:212
+#: kallithea/controllers/admin/user_groups.py:208
 msgid "Successfully deleted user group"
 msgstr "Група карыстальнікаў паспяхова выдаленая"
 
-#: kallithea/controllers/admin/user_groups.py:217
+#: kallithea/controllers/admin/user_groups.py:213
 msgid "An error occurred during deletion of user group"
 msgstr "Памылка пры выдаленні групы карыстальнікаў"
 
-#: kallithea/controllers/admin/user_groups.py:273
+#: kallithea/controllers/admin/user_groups.py:269
 msgid "Target group cannot be the same"
 msgstr "Мэтавая група не можа быць той жа самай"
 
-#: kallithea/controllers/admin/user_groups.py:279
+#: kallithea/controllers/admin/user_groups.py:275
 msgid "User group permissions updated"
 msgstr "Прывілеі групы карыстальнікаў абноўленыя"
 
-#: kallithea/controllers/admin/user_groups.py:388
-#: kallithea/controllers/admin/users.py:338
+#: kallithea/controllers/admin/user_groups.py:384
+#: kallithea/controllers/admin/users.py:336
 msgid "Updated permissions"
 msgstr "Абноўленыя прывілеі"
 
-#: kallithea/controllers/admin/user_groups.py:392
-#: kallithea/controllers/admin/users.py:342
+#: kallithea/controllers/admin/user_groups.py:388
+#: kallithea/controllers/admin/users.py:340
 msgid "An error occurred during permissions saving"
 msgstr "Памылка пры захаванні прывілеяў"
 
-#: kallithea/controllers/admin/users.py:123
+#: kallithea/controllers/admin/users.py:121
 #, python-format
 msgid "Created user %s"
 msgstr "Карыстальнік %s створаны"
 
-#: kallithea/controllers/admin/users.py:138
+#: kallithea/controllers/admin/users.py:136
 #, python-format
 msgid "Error occurred during creation of user %s"
 msgstr "Памылка пры стварэнні карыстальніка %s"
 
-#: kallithea/controllers/admin/users.py:162
+#: kallithea/controllers/admin/users.py:160
 msgid "User updated successfully"
 msgstr "Карыстальнік паспяхова абноўлены"
 
-#: kallithea/controllers/admin/users.py:190
+#: kallithea/controllers/admin/users.py:188
 msgid "Successfully deleted user"
 msgstr "Карыстальнік паспяхова выдалены"
 
-#: kallithea/controllers/admin/users.py:195
+#: kallithea/controllers/admin/users.py:193
 msgid "An error occurred during deletion of user"
 msgstr "Памылка пры выдаленні карыстальніка"
 
-#: kallithea/controllers/admin/users.py:203
+#: kallithea/controllers/admin/users.py:201
 msgid "The default user cannot be edited"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:409
+#: kallithea/controllers/admin/users.py:407
 #, python-format
 msgid "Added IP address %s to user whitelist"
 msgstr "Дададзены IP %s у белы спіс карыстальніка"
 
-#: kallithea/controllers/admin/users.py:415
+#: kallithea/controllers/admin/users.py:413
 msgid "An error occurred while adding IP address"
 msgstr "Адбылася памылка пры захаванні IP"
 
-#: kallithea/controllers/admin/users.py:427
+#: kallithea/controllers/admin/users.py:425
 msgid "Removed IP address from user whitelist"
 msgstr "Выдалены IP %s з белага спісу карыстальніка"
 
-#: kallithea/lib/auth.py:684
+#: kallithea/lib/auth.py:634
 msgid "You need to be a registered user to perform this action"
 msgstr ""
 "Вы павінны быць зарэгістраваным карыстальнікам, каб выканаць гэта дзеянне"
 
-#: kallithea/lib/auth.py:712
+#: kallithea/lib/auth.py:662
 msgid "You need to be signed in to view this page"
 msgstr "Старонка даступная толькі аўтарызаваным карыстальнікам"
 
@@ -1098,170 +1096,170 @@
 "Набор змены апынуўся занадта вялікімі і быў падрэзаны, выкарыстоўвайце "
 "меню параўнання для паказу выніку параўнання"
 
-#: kallithea/lib/diffs.py:224
+#: kallithea/lib/diffs.py:223
 msgid "No changes detected"
 msgstr "Змен не выяўлена"
 
-#: kallithea/lib/helpers.py:653
+#: kallithea/lib/helpers.py:670
 #, python-format
 msgid "Deleted branch: %s"
 msgstr "Выдаленая галіна: %s"
 
-#: kallithea/lib/helpers.py:655
+#: kallithea/lib/helpers.py:672
 #, python-format
 msgid "Created tag: %s"
 msgstr "Створаны тэг: %s"
 
-#: kallithea/lib/helpers.py:666
+#: kallithea/lib/helpers.py:683
 #, python-format
 msgid "Changeset %s not found"
 msgstr "Набор змен %s не знойдзены"
 
-#: kallithea/lib/helpers.py:715
+#: kallithea/lib/helpers.py:732
 #, python-format
 msgid "Show all combined changesets %s->%s"
 msgstr "Паказаць адрозненні разам %s->%s"
 
-#: kallithea/lib/helpers.py:721
+#: kallithea/lib/helpers.py:738
 msgid "Compare view"
 msgstr "Параўнанне"
 
-#: kallithea/lib/helpers.py:740
+#: kallithea/lib/helpers.py:757
 msgid "and"
 msgstr "і"
 
-#: kallithea/lib/helpers.py:741
+#: kallithea/lib/helpers.py:758
 #, python-format
 msgid "%s more"
 msgstr "на %s больш"
 
-#: kallithea/lib/helpers.py:742
+#: kallithea/lib/helpers.py:759
 #: kallithea/templates/changelog/changelog.html:43
 msgid "revisions"
 msgstr "версіі"
 
-#: kallithea/lib/helpers.py:766
+#: kallithea/lib/helpers.py:783
 #, python-format
 msgid "Fork name %s"
 msgstr "Імя форка %s"
 
-#: kallithea/lib/helpers.py:787
+#: kallithea/lib/helpers.py:804
 #, python-format
 msgid "Pull request %s"
 msgstr "Pull-запыт %s"
 
-#: kallithea/lib/helpers.py:797
+#: kallithea/lib/helpers.py:814
 msgid "[deleted] repository"
 msgstr "[выдалены] рэпазітар"
 
-#: kallithea/lib/helpers.py:799 kallithea/lib/helpers.py:811
+#: kallithea/lib/helpers.py:816 kallithea/lib/helpers.py:828
 msgid "[created] repository"
 msgstr "[створаны] рэпазітар"
 
-#: kallithea/lib/helpers.py:801
+#: kallithea/lib/helpers.py:818
 msgid "[created] repository as fork"
 msgstr "[створаны] рэпазітар як форк"
 
-#: kallithea/lib/helpers.py:803 kallithea/lib/helpers.py:813
+#: kallithea/lib/helpers.py:820 kallithea/lib/helpers.py:830
 msgid "[forked] repository"
 msgstr "[форкнуты] рэпазітар"
 
-#: kallithea/lib/helpers.py:805 kallithea/lib/helpers.py:815
+#: kallithea/lib/helpers.py:822 kallithea/lib/helpers.py:832
 msgid "[updated] repository"
 msgstr "[абноўлены] рэпазітар"
 
-#: kallithea/lib/helpers.py:807
+#: kallithea/lib/helpers.py:824
 msgid "[downloaded] archive from repository"
 msgstr "[загружаны] архіў з рэпазітара"
 
-#: kallithea/lib/helpers.py:809
+#: kallithea/lib/helpers.py:826
 msgid "[delete] repository"
 msgstr "[выдалены] рэпазітар"
 
-#: kallithea/lib/helpers.py:817
+#: kallithea/lib/helpers.py:834
 msgid "[created] user"
 msgstr "[створаны] карыстальнік"
 
-#: kallithea/lib/helpers.py:819
+#: kallithea/lib/helpers.py:836
 msgid "[updated] user"
 msgstr "[абноўлены] карыстальнік"
 
-#: kallithea/lib/helpers.py:821
+#: kallithea/lib/helpers.py:838
 msgid "[created] user group"
 msgstr "[створана] група карыстальнікаў"
 
-#: kallithea/lib/helpers.py:823
+#: kallithea/lib/helpers.py:840
 msgid "[updated] user group"
 msgstr "[абноўлена] група карыстальнікаў"
 
-#: kallithea/lib/helpers.py:825
+#: kallithea/lib/helpers.py:842
 msgid "[commented] on revision in repository"
 msgstr "[каментар] да рэвізіі ў рэпазітары"
 
-#: kallithea/lib/helpers.py:827
+#: kallithea/lib/helpers.py:844
 msgid "[commented] on pull request for"
 msgstr "[каментар] у pull-запыце для"
 
-#: kallithea/lib/helpers.py:829
+#: kallithea/lib/helpers.py:846
 msgid "[closed] pull request for"
 msgstr "[зачынены] pull-запыт для"
 
-#: kallithea/lib/helpers.py:831
+#: kallithea/lib/helpers.py:848
 msgid "[pushed] into"
 msgstr "[адпраўлена] у"
 
-#: kallithea/lib/helpers.py:833
+#: kallithea/lib/helpers.py:850
 msgid "[committed via Kallithea] into repository"
 msgstr "[каміт праз Kallithea] у рэпазітары"
 
-#: kallithea/lib/helpers.py:835
+#: kallithea/lib/helpers.py:852
 msgid "[pulled from remote] into repository"
 msgstr "[занесены з аддаленага рэпазітара] у рэпазітар"
 
-#: kallithea/lib/helpers.py:837
+#: kallithea/lib/helpers.py:854
 msgid "[pulled] from"
 msgstr "[занесены] з"
 
-#: kallithea/lib/helpers.py:839
+#: kallithea/lib/helpers.py:856
 msgid "[started following] repository"
 msgstr "[дададзены ў назіранні] рэпазітар"
 
-#: kallithea/lib/helpers.py:841
+#: kallithea/lib/helpers.py:858
 msgid "[stopped following] repository"
 msgstr "[выдалены з назірання] рэпазітар"
 
-#: kallithea/lib/helpers.py:961
+#: kallithea/lib/helpers.py:975
 #, python-format
 msgid " and %s more"
 msgstr " і на %s больш"
 
-#: kallithea/lib/helpers.py:965
+#: kallithea/lib/helpers.py:979
 #: kallithea/templates/compare/compare_diff.html:69
 #: kallithea/templates/pullrequests/pullrequest_show.html:297
 msgid "No files"
 msgstr "Няма файлаў"
 
-#: kallithea/lib/helpers.py:990
+#: kallithea/lib/helpers.py:1004
 msgid "new file"
 msgstr "новы файл"
 
-#: kallithea/lib/helpers.py:993
+#: kallithea/lib/helpers.py:1007
 msgid "mod"
 msgstr "зменены"
 
-#: kallithea/lib/helpers.py:996
+#: kallithea/lib/helpers.py:1010
 msgid "del"
 msgstr "выдалены"
 
-#: kallithea/lib/helpers.py:999
+#: kallithea/lib/helpers.py:1013
 msgid "rename"
 msgstr "пераназваны"
 
-#: kallithea/lib/helpers.py:1004
+#: kallithea/lib/helpers.py:1018
 msgid "chmod"
 msgstr "chmod"
 
-#: kallithea/lib/helpers.py:1297
+#: kallithea/lib/helpers.py:1323
 #, python-format
 msgid ""
 "%s repository is not mapped to db perhaps it was created or renamed from "
@@ -1272,34 +1270,36 @@
 "пераназваны з файлавай сістэмы. Калі ласка, перазапусціце прыкладанне для "
 "сканавання рэпазітароў"
 
-#: kallithea/lib/ssh.py:73
+#: kallithea/lib/ssh.py:75
 msgid "SSH key is missing"
 msgstr ""
 
-#: kallithea/lib/ssh.py:77
-msgid "Incorrect SSH key - it must have both a key type and a base64 part"
-msgstr ""
-
-#: kallithea/lib/ssh.py:81
+#: kallithea/lib/ssh.py:79
+msgid ""
+"Incorrect SSH key - it must have both a key type and a base64 part, like "
+"'ssh-rsa ASRNeaZu4FA...xlJp='"
+msgstr ""
+
+#: kallithea/lib/ssh.py:83
 msgid "Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'"
 msgstr ""
 
-#: kallithea/lib/ssh.py:84
+#: kallithea/lib/ssh.py:86
 #, python-format
 msgid "Incorrect SSH key - unexpected characters in base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:89
+#: kallithea/lib/ssh.py:91
 #, python-format
 msgid "Incorrect SSH key - failed to decode base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:92
+#: kallithea/lib/ssh.py:94
 #, python-format
 msgid "Incorrect SSH key - base64 part is not %r as claimed but %r"
 msgstr ""
 
-#: kallithea/lib/utils2.py:334
+#: kallithea/lib/utils2.py:253
 #, python-format
 msgid "%d year"
 msgid_plural "%d years"
@@ -1307,7 +1307,7 @@
 msgstr[1] "%d гады"
 msgstr[2] "%d гадоў"
 
-#: kallithea/lib/utils2.py:335
+#: kallithea/lib/utils2.py:254
 #, python-format
 msgid "%d month"
 msgid_plural "%d months"
@@ -1315,7 +1315,7 @@
 msgstr[1] "%d месяцы"
 msgstr[2] "%d месяцаў"
 
-#: kallithea/lib/utils2.py:336
+#: kallithea/lib/utils2.py:255
 #, python-format
 msgid "%d day"
 msgid_plural "%d days"
@@ -1323,7 +1323,7 @@
 msgstr[1] "%d дні"
 msgstr[2] "%d дзён"
 
-#: kallithea/lib/utils2.py:337
+#: kallithea/lib/utils2.py:256
 #, python-format
 msgid "%d hour"
 msgid_plural "%d hours"
@@ -1331,7 +1331,7 @@
 msgstr[1] "%d гадзіны"
 msgstr[2] "%d гадзін"
 
-#: kallithea/lib/utils2.py:338
+#: kallithea/lib/utils2.py:257
 #, python-format
 msgid "%d minute"
 msgid_plural "%d minutes"
@@ -1339,7 +1339,7 @@
 msgstr[1] "%d хвіліны"
 msgstr[2] "%d хвілін"
 
-#: kallithea/lib/utils2.py:339
+#: kallithea/lib/utils2.py:258
 #, python-format
 msgid "%d second"
 msgid_plural "%d seconds"
@@ -1347,27 +1347,27 @@
 msgstr[1] "%d секунды"
 msgstr[2] "%d секунд"
 
-#: kallithea/lib/utils2.py:355
+#: kallithea/lib/utils2.py:274
 #, python-format
 msgid "in %s"
 msgstr "у %s"
 
-#: kallithea/lib/utils2.py:357
+#: kallithea/lib/utils2.py:276
 #, python-format
 msgid "%s ago"
 msgstr "%s назад"
 
-#: kallithea/lib/utils2.py:359
+#: kallithea/lib/utils2.py:278
 #, python-format
 msgid "in %s and %s"
 msgstr "у %s і %s"
 
-#: kallithea/lib/utils2.py:362
+#: kallithea/lib/utils2.py:281
 #, python-format
 msgid "%s and %s ago"
 msgstr "%s і %s назад"
 
-#: kallithea/lib/utils2.py:365
+#: kallithea/lib/utils2.py:284
 msgid "just now"
 msgstr "цяпер"
 
@@ -1376,137 +1376,137 @@
 msgid "on line %s"
 msgstr "на радку %s"
 
-#: kallithea/model/comment.py:221 kallithea/model/pull_request.py:114
+#: kallithea/model/comment.py:219 kallithea/model/pull_request.py:112
 msgid "[Mention]"
 msgstr "[Згадванне]"
 
-#: kallithea/model/db.py:1496
+#: kallithea/model/db.py:1411
 msgid "top level"
 msgstr "верхні ўзровень"
 
-#: kallithea/model/db.py:1637
+#: kallithea/model/db.py:1542
 msgid "Kallithea Administrator"
 msgstr "Адміністратар Kallithea"
 
-#: kallithea/model/db.py:1639
+#: kallithea/model/db.py:1544
 msgid "Default user has no access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1640
+#: kallithea/model/db.py:1545
 #, fuzzy
 msgid "Default user has read access to new repositories"
 msgstr "Несанкцыянаваны доступ да рэсурсу"
 
-#: kallithea/model/db.py:1641
+#: kallithea/model/db.py:1546
 #, fuzzy
 msgid "Default user has write access to new repositories"
 msgstr "Несанкцыянаваны доступ да рэсурсу"
 
-#: kallithea/model/db.py:1642
+#: kallithea/model/db.py:1547
 msgid "Default user has admin access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1644
+#: kallithea/model/db.py:1549
 msgid "Default user has no access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1645
+#: kallithea/model/db.py:1550
 msgid "Default user has read access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1646
+#: kallithea/model/db.py:1551
 msgid "Default user has write access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1647
+#: kallithea/model/db.py:1552
 msgid "Default user has admin access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1649
+#: kallithea/model/db.py:1554
 msgid "Default user has no access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1650
+#: kallithea/model/db.py:1555
 msgid "Default user has read access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1651
+#: kallithea/model/db.py:1556
 msgid "Default user has write access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1652
+#: kallithea/model/db.py:1557
 msgid "Default user has admin access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1654
+#: kallithea/model/db.py:1559
 msgid "Only admins can create repository groups"
 msgstr "Толькі адміністратары могуць ствараць групы репазітароў"
 
-#: kallithea/model/db.py:1655
+#: kallithea/model/db.py:1560
 msgid "Non-admins can create repository groups"
 msgstr "Неадміністратары могуць ствараць групы репазітароў"
 
-#: kallithea/model/db.py:1657
+#: kallithea/model/db.py:1562
 msgid "Only admins can create user groups"
 msgstr "Толькі адміністратары могуць ствараць групы карыстальнікаў"
 
-#: kallithea/model/db.py:1658
+#: kallithea/model/db.py:1563
 msgid "Non-admins can create user groups"
 msgstr "Неадміністратары могуць ствараць групы карыстальнікаў"
 
-#: kallithea/model/db.py:1660
+#: kallithea/model/db.py:1565
 msgid "Only admins can create top level repositories"
 msgstr "Толькі адміністратары могуць ствараць рэпазітары верхняга ўзроўню"
 
-#: kallithea/model/db.py:1661
+#: kallithea/model/db.py:1566
 msgid "Non-admins can create top level repositories"
 msgstr "Неадміністратары могуць ствараць рэпазітары верхняга ўзроўню"
 
-#: kallithea/model/db.py:1663
+#: kallithea/model/db.py:1568
 msgid ""
 "Repository creation enabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1664
+#: kallithea/model/db.py:1569
 msgid ""
 "Repository creation disabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1666
+#: kallithea/model/db.py:1571
 msgid "Only admins can fork repositories"
 msgstr "Месцазнаходжанне рэпазітароў"
 
-#: kallithea/model/db.py:1667
+#: kallithea/model/db.py:1572
 msgid "Non-admins can fork repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1669
+#: kallithea/model/db.py:1574
 msgid "Registration disabled"
 msgstr "Рэгістрацыя адключаная"
 
-#: kallithea/model/db.py:1670
+#: kallithea/model/db.py:1575
 msgid "User registration with manual account activation"
 msgstr "Рэгістрацыя карыстальніка з ручной актывацыяй уліковага запісу"
 
-#: kallithea/model/db.py:1671
+#: kallithea/model/db.py:1576
 msgid "User registration with automatic account activation"
 msgstr "Рэгістрацыя карыстальніка з аўтаматычнай актывацыяй"
 
-#: kallithea/model/db.py:2206
+#: kallithea/model/db.py:1992
 msgid "Not reviewed"
 msgstr "Не прагледжана"
 
-#: kallithea/model/db.py:2207
+#: kallithea/model/db.py:1993
 msgid "Under review"
 msgstr "На разглядзе"
 
-#: kallithea/model/db.py:2208
+#: kallithea/model/db.py:1994
 #, fuzzy
 #| msgid "Approved"
 msgid "Not approved"
 msgstr "Ухвалена"
 
-#: kallithea/model/db.py:2209
+#: kallithea/model/db.py:1995
 msgid "Approved"
 msgstr "Ухвалена"
 
@@ -1532,7 +1532,7 @@
 msgid "Name must not contain only digits"
 msgstr "Імя не можа ўтрымліваць толькі лічбы"
 
-#: kallithea/model/notification.py:164
+#: kallithea/model/notification.py:162
 #, fuzzy, python-format
 #| msgid "[Comment] %(repo_name)s pull request %(pr_nice_id)s from %(ref)s"
 msgid ""
@@ -1540,72 +1540,72 @@
 "%(branch)s"
 msgstr "[пракаментавана] у запыце на занясенне змен для"
 
-#: kallithea/model/notification.py:167
+#: kallithea/model/notification.py:165
 #, python-format
 msgid "New user %(new_username)s registered"
 msgstr "Новы карыстальнік \"%(new_username)s\" зарэгістраваны"
 
-#: kallithea/model/notification.py:169
+#: kallithea/model/notification.py:167
 #, python-format
 msgid ""
 "[Review] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:170
+#: kallithea/model/notification.py:168
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:183
+#: kallithea/model/notification.py:188
 msgid "Closing"
 msgstr "Зачынены"
 
-#: kallithea/model/pull_request.py:73
+#: kallithea/model/pull_request.py:72
 #, python-format
 msgid ""
 "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s"
 msgstr ""
 "%(user)s просіць вас разгледзець pull request %(pr_nice_id)s: %(pr_title)s"
 
-#: kallithea/model/pull_request.py:209
+#: kallithea/model/pull_request.py:207
 #, fuzzy
 #| msgid "Error creating pull request: %s"
 msgid "Cannot create empty pull request"
 msgstr "Памылка пры стварэнні pull-запыту: %s"
 
-#: kallithea/model/pull_request.py:217
+#: kallithea/model/pull_request.py:215
 #, python-format
 msgid ""
 "Cannot create pull request - criss cross merge detected, please merge a "
 "later %s revision to %s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:245 kallithea/model/pull_request.py:380
+#: kallithea/model/pull_request.py:243 kallithea/model/pull_request.py:378
 #, fuzzy
 #| msgid "Confirm to delete this pull request"
 msgid "You are not authorized to create the pull request"
 msgstr "Пацвердзіце выдаленне гэтага pull-request'а"
 
-#: kallithea/model/pull_request.py:339
+#: kallithea/model/pull_request.py:337
 #, fuzzy
 #| msgid "Missing changesets since the previous pull request:"
 msgid "Missing changesets since the previous iteration:"
 msgstr "Адсутныя рэвізіі адносна папярэдняга pull-запыту:"
 
-#: kallithea/model/pull_request.py:346
+#: kallithea/model/pull_request.py:344
 #, fuzzy, python-format
 #| msgid "New changesets on %s %s since the previous pull request:"
 msgid "New changesets on %s %s since the previous iteration:"
 msgstr "Новыя рэвізіі на %s %s адносна папярэдняга pull-запыту:"
 
-#: kallithea/model/pull_request.py:353
+#: kallithea/model/pull_request.py:351
 msgid "Ancestor didn't change - diff since previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:360
+#: kallithea/model/pull_request.py:358
 #, fuzzy, python-format
 msgid ""
 "This iteration is based on another %s revision and there is no simple "
@@ -1613,49 +1613,49 @@
 msgstr ""
 "Гэты pull-запыт заснаваны на іншай рэвізіі %s, просты diff немагчымы."
 
-#: kallithea/model/pull_request.py:362
+#: kallithea/model/pull_request.py:360
 #, fuzzy, python-format
 #| msgid "No changes found on %s %s since previous version."
 msgid "No changes found on %s %s since previous iteration."
 msgstr "Няма змен на %s %s адносна папярэдняй версіі."
 
-#: kallithea/model/pull_request.py:388
+#: kallithea/model/pull_request.py:386
 #, python-format
 msgid "Closed, next iteration: %s ."
 msgstr ""
 
-#: kallithea/model/scm.py:668
+#: kallithea/model/scm.py:655
 msgid "latest tip"
 msgstr "апошняя версія"
 
-#: kallithea/model/ssh_key.py:56
+#: kallithea/model/ssh_key.py:57
 #, python-format
 msgid "SSH key %r is invalid: %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:68
+#: kallithea/model/ssh_key.py:69
 #, python-format
 msgid "SSH key %s is already used by %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:89
+#: kallithea/model/ssh_key.py:88
 #, fuzzy, python-format
 #| msgid "Changeset %s not found"
-msgid "SSH key %r not found"
+msgid "SSH key with fingerprint %r found"
 msgstr "Набор змен %s не знойдзены"
 
-#: kallithea/model/user.py:186
+#: kallithea/model/user.py:180
 msgid "New user registration"
 msgstr "Рэгістрацыя новага карыстальніка"
 
-#: kallithea/model/user.py:250
+#: kallithea/model/user.py:244
 msgid ""
 "You can't remove this user since it is crucial for the entire application"
 msgstr ""
 "Вы не можаце выдаліць карыстальніка, паколькі гэта крытычна для працы "
 "ўсёй праграмы"
 
-#: kallithea/model/user.py:255
+#: kallithea/model/user.py:249
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repositories and cannot be removed. Switch "
@@ -1665,7 +1665,7 @@
 "таму не можа быць выдалены. Змяніце ўладальніка ці выдаліце гэтыя "
 "рэпазітары: %s"
 
-#: kallithea/model/user.py:260
+#: kallithea/model/user.py:254
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repository groups and cannot be removed. Switch "
@@ -1675,7 +1675,7 @@
 "і таму не можа быць выдалены. Змяніце ўладальніка ці выдаліце гэтая "
 "групы: %s"
 
-#: kallithea/model/user.py:267
+#: kallithea/model/user.py:261
 #, python-format
 msgid ""
 "User \"%s\" still owns %s user groups and cannot be removed. Switch "
@@ -1685,36 +1685,36 @@
 "карыстальнікаў і таму не можа быць выдалены. Змяніце ўладальніка ці "
 "выдаліце гэтыя групы: %s"
 
-#: kallithea/model/user.py:361
+#: kallithea/model/user.py:355
 msgid "Password reset link"
 msgstr "Спасылка скіду пароля"
 
-#: kallithea/model/user.py:408
+#: kallithea/model/user.py:402
 msgid "Password reset notification"
 msgstr "Паведамленне пра скіданне пароля"
 
-#: kallithea/model/user.py:409
+#: kallithea/model/user.py:403
 #, python-format
 msgid ""
 "The password to your account %s has been changed using password reset "
 "form."
 msgstr ""
 
-#: kallithea/model/validators.py:52 kallithea/model/validators.py:53
+#: kallithea/model/validators.py:53 kallithea/model/validators.py:54
 msgid "Value cannot be an empty list"
 msgstr "Значэнне не можа быць пустым спісам"
 
-#: kallithea/model/validators.py:72
+#: kallithea/model/validators.py:73
 #, python-format
 msgid "Username \"%(username)s\" already exists"
 msgstr "Карыстальнік з імем \"%(username)s\" ужо існуе"
 
-#: kallithea/model/validators.py:74
+#: kallithea/model/validators.py:75
 #, python-format
 msgid "Username \"%(username)s\" cannot be used"
 msgstr "Імя \"%(username)s\" недапушчальнае"
 
-#: kallithea/model/validators.py:76
+#: kallithea/model/validators.py:77
 msgid ""
 "Username may only contain alphanumeric characters underscores, periods or "
 "dashes and must begin with an alphanumeric character or underscore"
@@ -1723,25 +1723,25 @@
 "падкрэслення, кропкі і працяжнік; а гэтак жа павінна пачынацца з літары, "
 "лічбы або са знака падкрэслення"
 
-#: kallithea/model/validators.py:103
+#: kallithea/model/validators.py:104
 msgid "The input is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:110
+#: kallithea/model/validators.py:111
 #, python-format
 msgid "Username %(username)s is not valid"
 msgstr "Імя \"%(username)s\" недапушчальнае"
 
-#: kallithea/model/validators.py:131
+#: kallithea/model/validators.py:132
 msgid "Invalid user group name"
 msgstr "Няслушнае імя групы карыстальнікаў"
 
-#: kallithea/model/validators.py:132
+#: kallithea/model/validators.py:133
 #, python-format
 msgid "User group \"%(usergroup)s\" already exists"
 msgstr "Група карыстальнікаў \"%(usergroup)s\" ужо існуе"
 
-#: kallithea/model/validators.py:134
+#: kallithea/model/validators.py:135
 msgid ""
 "user group name may only contain alphanumeric characters underscores, "
 "periods or dashes and must begin with alphanumeric character"
@@ -1750,61 +1750,61 @@
 "падкрэслення, кропкі і працяжнік; а гэтак жа павінна пачынацца з літары "
 "ці лічбы"
 
-#: kallithea/model/validators.py:174
+#: kallithea/model/validators.py:175
 msgid "Cannot assign this group as parent"
 msgstr "Немагчыма выкарыстоўваць гэту групу як бацькоўскую"
 
-#: kallithea/model/validators.py:175
+#: kallithea/model/validators.py:176
 #, python-format
 msgid "Group \"%(group_name)s\" already exists"
 msgstr "Група \"%(group_name)s\" ужо існуе"
 
-#: kallithea/model/validators.py:177
+#: kallithea/model/validators.py:178
 #, python-format
 msgid "Repository with name \"%(group_name)s\" already exists"
 msgstr "Рэпазітар з  імем \"%(group_name)s\" ужо існуе"
 
-#: kallithea/model/validators.py:233
+#: kallithea/model/validators.py:230
 msgid "Invalid characters (non-ascii) in password"
 msgstr "Недапушчальныя знакі (не ascii) у паролі"
 
-#: kallithea/model/validators.py:248
+#: kallithea/model/validators.py:245
 msgid "Invalid old password"
 msgstr "Няслушна зададзены стары пароль"
 
-#: kallithea/model/validators.py:264
+#: kallithea/model/validators.py:261
 msgid "Passwords do not match"
 msgstr "Паролі не супадаюць"
 
-#: kallithea/model/validators.py:279
+#: kallithea/model/validators.py:276
 msgid "Invalid username or password"
 msgstr "Няслушнае імя ці пароль"
 
-#: kallithea/model/validators.py:313
+#: kallithea/model/validators.py:310
 #, python-format
 msgid "Repository name %(repo)s is not allowed"
 msgstr "Імя рэпазітара %(repo)s забароненае"
 
-#: kallithea/model/validators.py:315
+#: kallithea/model/validators.py:312
 #, python-format
 msgid "Repository named %(repo)s already exists"
 msgstr "Рэпазітар %(repo)s ужо існуе"
 
-#: kallithea/model/validators.py:316
+#: kallithea/model/validators.py:313
 #, python-format
 msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
 msgstr "Рэпазітар \"%(repo)s\" ужо існуе ў групе \"%(group)s\""
 
-#: kallithea/model/validators.py:318
+#: kallithea/model/validators.py:315
 #, python-format
 msgid "Repository group with name \"%(repo)s\" already exists"
 msgstr "Група рэпазітароў \"%(repo)s\" ужо існуе"
 
-#: kallithea/model/validators.py:404
+#: kallithea/model/validators.py:401
 msgid "Invalid repository URL"
 msgstr "Няслушны URL рэпазітара"
 
-#: kallithea/model/validators.py:405
+#: kallithea/model/validators.py:402
 msgid ""
 "Invalid repository URL. It must be a valid http, https, ssh, svn+http or "
 "svn+https URL"
@@ -1812,40 +1812,40 @@
 "Няслушны URL рэпазітара. Ён мусіць быць карэктным URL http, https, ssh, "
 "svn+http ці svn+https"
 
-#: kallithea/model/validators.py:430
+#: kallithea/model/validators.py:427
 msgid "Fork has to be the same type as parent"
 msgstr "Тып форка будзе супадаць з бацькоўскім"
 
-#: kallithea/model/validators.py:445
+#: kallithea/model/validators.py:442
 msgid "You don't have permissions to create repository in this group"
 msgstr "У вас недастаткова правоў для стварэння рэпазітароў у гэтай групе"
 
-#: kallithea/model/validators.py:447
+#: kallithea/model/validators.py:444
 msgid "no permission to create repository in root location"
 msgstr "недастаткова правоў для стварэння рэпазітара ў каранёвым каталогу"
 
-#: kallithea/model/validators.py:497
+#: kallithea/model/validators.py:494
 msgid "You don't have permissions to create a group in this location"
 msgstr "У Вас недастаткова прывілеяў для стварэння групы ў гэтым месцы"
 
-#: kallithea/model/validators.py:537
+#: kallithea/model/validators.py:534
 msgid "This username or user group name is not valid"
 msgstr "Дадзенае імя карыстальніка ці групы карыстальнікаў недапушчальна"
 
-#: kallithea/model/validators.py:630
+#: kallithea/model/validators.py:627
 msgid "This is not a valid path"
 msgstr "Гэты шлях хібны"
 
-#: kallithea/model/validators.py:647
+#: kallithea/model/validators.py:644
 msgid "This email address is already in use"
 msgstr "Гэты e-mail ужо ўжываецца"
 
-#: kallithea/model/validators.py:667
+#: kallithea/model/validators.py:664
 #, python-format
 msgid "Email address \"%(email)s\" not found"
 msgstr "Email-адрас \"%(email)s\" не знойдзены"
 
-#: kallithea/model/validators.py:704
+#: kallithea/model/validators.py:701
 msgid ""
 "The LDAP Login attribute of the CN must be specified - this is the name "
 "of the attribute that is equivalent to \"username\""
@@ -1853,11 +1853,11 @@
 "Для ўваходу па LDAP павінна быць паказана значэнне атрыбута CN - гэта "
 "эквівалент імя карыстальніка"
 
-#: kallithea/model/validators.py:716
+#: kallithea/model/validators.py:713
 msgid "Please enter a valid IPv4 or IPv6 address"
 msgstr "Калі ласка, увядзіце існы IPv4 ці IPv6 адрас"
 
-#: kallithea/model/validators.py:717
+#: kallithea/model/validators.py:714
 #, python-format
 msgid ""
 "The network size (bits) must be within the range of 0-32 (not %(bits)r)"
@@ -1865,17 +1865,17 @@
 "Значэнне маскі падсеткі павінна быць у межах ад 0 да 32 (%(bits)r - "
 "няслушна)"
 
-#: kallithea/model/validators.py:750
+#: kallithea/model/validators.py:747
 msgid "Key name can only consist of letters, underscore, dash or numbers"
 msgstr ""
 "Ключавое імя можа толькі складацца з літар, знака падкрэслення, працяжнік "
 "ці лікаў"
 
-#: kallithea/model/validators.py:764
+#: kallithea/model/validators.py:761
 msgid "Filename cannot be inside a directory"
 msgstr "Файла няма ў каталогу"
 
-#: kallithea/model/validators.py:780
+#: kallithea/model/validators.py:777
 #, python-format
 msgid "Plugins %(loaded)s and %(next_to_load)s both export the same name"
 msgstr ""
@@ -1936,7 +1936,7 @@
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:60
 #: kallithea/templates/email_templates/pull_request.html:37
 #: kallithea/templates/forks/fork.html:34
-#: kallithea/templates/index_base.html:58
+#: kallithea/templates/index_base.html:59
 #: kallithea/templates/pullrequests/pullrequest.html:33
 #: kallithea/templates/pullrequests/pullrequest_show.html:38
 #: kallithea/templates/pullrequests/pullrequest_show.html:59
@@ -1944,14 +1944,14 @@
 msgid "Description"
 msgstr "Апісанне"
 
-#: kallithea/templates/index_base.html:60
+#: kallithea/templates/index_base.html:61
 msgid "Last Change"
 msgstr "Апошняя змена"
 
 #: kallithea/templates/admin/my_account/my_account_repos.html:15
 #: kallithea/templates/admin/my_account/my_account_watched.html:15
 #: kallithea/templates/admin/repos/repos.html:41
-#: kallithea/templates/index_base.html:62
+#: kallithea/templates/index_base.html:63
 msgid "Tip"
 msgstr "Стан"
 
@@ -1961,7 +1961,7 @@
 #: kallithea/templates/admin/repos/repos.html:42
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:8
 #: kallithea/templates/admin/user_groups/user_groups.html:42
-#: kallithea/templates/index_base.html:63
+#: kallithea/templates/index_base.html:64
 #: kallithea/templates/pullrequests/pullrequest_data.html:16
 #: kallithea/templates/pullrequests/pullrequest_show.html:124
 #: kallithea/templates/pullrequests/pullrequest_show.html:219
@@ -1985,7 +1985,7 @@
 #: kallithea/templates/admin/users/user_edit_profile.html:18
 #: kallithea/templates/admin/users/users.html:37
 #: kallithea/templates/base/base.html:364
-#: kallithea/templates/email_templates/registration.html:11
+#: kallithea/templates/email_templates/registration.html:12
 #: kallithea/templates/login.html:28 kallithea/templates/register.html:31
 msgid "Username"
 msgstr "Імя карыстальніка"
@@ -2111,7 +2111,7 @@
 #: kallithea/templates/admin/settings/settings.html:31
 #: kallithea/templates/admin/users/user_add.html:62
 #: kallithea/templates/admin/users/user_edit_profile.html:25
-#: kallithea/templates/email_templates/registration.html:33
+#: kallithea/templates/email_templates/registration.html:34
 #: kallithea/templates/register.html:66
 msgid "Email"
 msgstr "E-mail"
@@ -2362,7 +2362,7 @@
 msgstr "Стварыць новы gist-запіс"
 
 #: kallithea/templates/admin/gists/index.html:51
-#: kallithea/templates/data_table/_dt_elements.html:78
+#: kallithea/templates/data_table/_dt_elements.html:84
 msgid "Created"
 msgstr "Створаны"
 
@@ -2446,13 +2446,13 @@
 #: kallithea/templates/admin/users/user_edit_ips.html:21
 #: kallithea/templates/changeset/changeset_file_comment.html:30
 #: kallithea/templates/changeset/changeset_file_comment.html:121
-#: kallithea/templates/data_table/_dt_elements.html:69
-#: kallithea/templates/data_table/_dt_elements.html:89
-#: kallithea/templates/data_table/_dt_elements.html:91
-#: kallithea/templates/data_table/_dt_elements.html:101
-#: kallithea/templates/data_table/_dt_elements.html:103
-#: kallithea/templates/data_table/_dt_elements.html:120
-#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:75
+#: kallithea/templates/data_table/_dt_elements.html:95
+#: kallithea/templates/data_table/_dt_elements.html:97
+#: kallithea/templates/data_table/_dt_elements.html:107
+#: kallithea/templates/data_table/_dt_elements.html:109
+#: kallithea/templates/data_table/_dt_elements.html:126
+#: kallithea/templates/data_table/_dt_elements.html:128
 #: kallithea/templates/files/files_source.html:35
 #: kallithea/templates/files/files_source.html:38
 #: kallithea/templates/files/files_source.html:41
@@ -2468,14 +2468,14 @@
 #: kallithea/templates/base/perms_summary.html:44
 #: kallithea/templates/base/perms_summary.html:81
 #: kallithea/templates/base/perms_summary.html:83
-#: kallithea/templates/data_table/_dt_elements.html:63
-#: kallithea/templates/data_table/_dt_elements.html:64
-#: kallithea/templates/data_table/_dt_elements.html:85
-#: kallithea/templates/data_table/_dt_elements.html:86
-#: kallithea/templates/data_table/_dt_elements.html:97
-#: kallithea/templates/data_table/_dt_elements.html:98
-#: kallithea/templates/data_table/_dt_elements.html:116
-#: kallithea/templates/data_table/_dt_elements.html:117
+#: kallithea/templates/data_table/_dt_elements.html:69
+#: kallithea/templates/data_table/_dt_elements.html:70
+#: kallithea/templates/data_table/_dt_elements.html:91
+#: kallithea/templates/data_table/_dt_elements.html:92
+#: kallithea/templates/data_table/_dt_elements.html:103
+#: kallithea/templates/data_table/_dt_elements.html:104
+#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:123
 #: kallithea/templates/files/diff_2way.html:56
 #: kallithea/templates/files/files_source.html:37
 #: kallithea/templates/files/files_source.html:40
@@ -2792,7 +2792,7 @@
 #: kallithea/templates/admin/permissions/permissions_globals.html:27
 #: kallithea/templates/admin/repos/repo_add_base.html:28
 #: kallithea/templates/admin/repos/repo_edit_settings.html:33
-#: kallithea/templates/data_table/_dt_elements.html:134
+#: kallithea/templates/data_table/_dt_elements.html:140
 #: kallithea/templates/forks/fork.html:42
 msgid "Repository group"
 msgstr "Група рэпазітароў"
@@ -2817,7 +2817,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:40
-#: kallithea/templates/data_table/_dt_elements.html:141
+#: kallithea/templates/data_table/_dt_elements.html:147
 msgid "User group"
 msgstr "Група карыстальнікаў"
 
@@ -2994,7 +2994,7 @@
 msgstr "Створана"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:121
+#: kallithea/templates/data_table/_dt_elements.html:127
 #, python-format
 msgid "Confirm to delete this group: %s with %s repository"
 msgid_plural "Confirm to delete this group: %s with %s repositories"
@@ -3169,14 +3169,10 @@
 msgstr "Дадатковыя палі"
 
 #: kallithea/templates/admin/repos/repo_edit.html:37
-msgid "Caches"
-msgstr "Кэшы"
+msgid "Remote"
+msgstr "Выдалены"
 
 #: kallithea/templates/admin/repos/repo_edit.html:40
-msgid "Remote"
-msgstr "Выдалены"
-
-#: kallithea/templates/admin/repos/repo_edit.html:43
 #: kallithea/templates/summary/statistics.html:8
 #: kallithea/templates/summary/summary.html:169
 #: kallithea/templates/summary/summary.html:170
@@ -3216,7 +3212,7 @@
 "публічным журнал."
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:46
-#: kallithea/templates/data_table/_dt_elements.html:68
+#: kallithea/templates/data_table/_dt_elements.html:74
 #, python-format
 msgid "Confirm to delete this repository: %s"
 msgstr "Пацвердзіце выдаленне гэтага рэпазітара: %s"
@@ -3248,43 +3244,14 @@
 "it or restore it."
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:4
-msgid "Invalidate Repository Cache"
-msgstr "Скінуць кэш рэпазітара"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:6
-msgid ""
-"Manually invalidate cache for this repository. On first access, the "
-"repository will be cached again."
-msgstr "Ручное скіданне кэша рэпазітара. Пры першым доступе кэш адновіцца."
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:9
-msgid "List of Cached Values"
-msgstr "Спіс кэшаваных значэнняў"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:12
-msgid "Prefix"
-msgstr "Прэфікс"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:13
+#: kallithea/templates/admin/repos/repo_edit_fields.html:6
+msgid "Label"
+msgstr ""
+
 #: kallithea/templates/admin/repos/repo_edit_fields.html:7
 msgid "Key"
 msgstr "Ключ"
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:14
-#: kallithea/templates/admin/user_groups/user_group_add.html:40
-#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
-#: kallithea/templates/admin/user_groups/user_groups.html:41
-#: kallithea/templates/admin/users/user_add.html:69
-#: kallithea/templates/admin/users/user_edit_profile.html:74
-#: kallithea/templates/admin/users/users.html:42
-msgid "Active"
-msgstr "Актыўны"
-
-#: kallithea/templates/admin/repos/repo_edit_fields.html:6
-msgid "Label"
-msgstr ""
-
 #: kallithea/templates/admin/repos/repo_edit_fields.html:20
 #, python-format
 msgid "Confirm to delete this field: %s"
@@ -3815,6 +3782,15 @@
 msgid "Short, optional description for this user group."
 msgstr "Кароткае дадатковае апісанне для гэтай групы карыстальнікаў."
 
+#: kallithea/templates/admin/user_groups/user_group_add.html:40
+#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
+#: kallithea/templates/admin/user_groups/user_groups.html:41
+#: kallithea/templates/admin/users/user_add.html:69
+#: kallithea/templates/admin/users/user_edit_profile.html:74
+#: kallithea/templates/admin/users/users.html:42
+msgid "Active"
+msgstr "Актыўны"
+
 #: kallithea/templates/admin/user_groups/user_group_edit.html:5
 #, python-format
 msgid "%s user group settings"
@@ -3836,7 +3812,7 @@
 msgstr "Удзельнікі"
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:19
-#: kallithea/templates/data_table/_dt_elements.html:102
+#: kallithea/templates/data_table/_dt_elements.html:108
 #, python-format
 msgid "Confirm to delete this user group: %s"
 msgstr "Пацвердзіце выдаленне наступнай групы карыстальнікаў: %s"
@@ -3910,7 +3886,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:90
+#: kallithea/templates/data_table/_dt_elements.html:96
 #, python-format
 msgid "Confirm to delete this user: %s"
 msgstr "Пацвердзіце выдаленне карыстальніка %s"
@@ -4010,10 +3986,12 @@
 msgstr "Пошук"
 
 #: kallithea/templates/base/base.html:167
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Follow"
 msgstr "Назіраць"
 
 #: kallithea/templates/base/base.html:168
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Unfollow"
 msgstr "Не назіраць"
 
@@ -4431,23 +4409,23 @@
 msgid "Merge"
 msgstr "звесці"
 
-#: kallithea/templates/changeset/changeset.html:96
+#: kallithea/templates/changeset/changeset.html:95
 msgid "Grafted from:"
 msgstr "Перанесена з:"
 
-#: kallithea/templates/changeset/changeset.html:102
+#: kallithea/templates/changeset/changeset.html:100
 msgid "Transplanted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:108
+#: kallithea/templates/changeset/changeset.html:106
 msgid "Replaced by:"
 msgstr "Заменена:"
 
-#: kallithea/templates/changeset/changeset.html:122
+#: kallithea/templates/changeset/changeset.html:120
 msgid "Preceded by:"
 msgstr "Замяняе:"
 
-#: kallithea/templates/changeset/changeset.html:139
+#: kallithea/templates/changeset/changeset.html:137
 #: kallithea/templates/compare/compare_diff.html:59
 #: kallithea/templates/pullrequests/pullrequest_show.html:290
 #, python-format
@@ -4457,7 +4435,7 @@
 msgstr[1] "%s файлы зменена"
 msgstr[2] "%s файлаў зменена"
 
-#: kallithea/templates/changeset/changeset.html:141
+#: kallithea/templates/changeset/changeset.html:139
 #: kallithea/templates/compare/compare_diff.html:61
 #: kallithea/templates/pullrequests/pullrequest_show.html:292
 #, python-format
@@ -4467,8 +4445,8 @@
 msgstr[1] "%s файлы зменена: %s даданні, %s выдаленні"
 msgstr[2] "%s файлаў зменена: %s даданняў, %s выдаленняў"
 
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
 #: kallithea/templates/pullrequests/pullrequest_show.html:333
@@ -4717,23 +4695,23 @@
 msgid "Repository creation in progress..."
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:42
+#: kallithea/templates/data_table/_dt_elements.html:48
 msgid "No changesets yet"
 msgstr "Змен яшчэ не было"
 
-#: kallithea/templates/data_table/_dt_elements.html:48
-#: kallithea/templates/data_table/_dt_elements.html:50
+#: kallithea/templates/data_table/_dt_elements.html:54
+#: kallithea/templates/data_table/_dt_elements.html:56
 #, python-format
 msgid "Subscribe to %s rss feed"
 msgstr "Падпісацца на стужку RSS %s"
 
-#: kallithea/templates/data_table/_dt_elements.html:56
-#: kallithea/templates/data_table/_dt_elements.html:58
+#: kallithea/templates/data_table/_dt_elements.html:62
+#: kallithea/templates/data_table/_dt_elements.html:64
 #, python-format
 msgid "Subscribe to %s atom feed"
 msgstr "Падпісацца на стужку Atom %s"
 
-#: kallithea/templates/data_table/_dt_elements.html:76
+#: kallithea/templates/data_table/_dt_elements.html:82
 msgid "Creating"
 msgstr "Ствараецца"
 
@@ -4771,6 +4749,13 @@
 msgid "by"
 msgstr ""
 
+#: kallithea/templates/email_templates/changeset_comment.html:36
+#: kallithea/templates/email_templates/pull_request_comment.html:43
+#, fuzzy
+#| msgid "Comment"
+msgid "View Comment"
+msgstr "Каментаваць"
+
 #: kallithea/templates/email_templates/comment.html:27
 #, fuzzy
 #| msgid "Status change"
@@ -4783,32 +4768,42 @@
 msgid "The pull request has been closed."
 msgstr "Гэты pull-запыт быў зачынены і не можа быць абноўлены."
 
-#: kallithea/templates/email_templates/password_reset.html:9
+#: kallithea/templates/email_templates/default.html:4
+msgid "Message"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:4
+#, fuzzy
+#| msgid "Password Reset"
+msgid "Password Reset Request"
+msgstr "Скінуць пароль"
+
+#: kallithea/templates/email_templates/password_reset.html:10
 #, python-format
 msgid "Hello %s"
 msgstr "Добры дзень, %s"
 
-#: kallithea/templates/email_templates/password_reset.html:16
+#: kallithea/templates/email_templates/password_reset.html:17
 msgid "We have received a request to reset the password for your account."
 msgstr "Мы атрымалі запыт на скіданне пароля для вашага акаўнта."
 
-#: kallithea/templates/email_templates/password_reset.html:25
+#: kallithea/templates/email_templates/password_reset.html:26
 msgid ""
 "This account is however managed outside this system and the password "
 "cannot be changed here."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:28
+#: kallithea/templates/email_templates/password_reset.html:29
 msgid "To set a new password, click the following link"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:33
+#: kallithea/templates/email_templates/password_reset.html:34
 msgid ""
 "Should you not be able to use the link above, please type the following "
 "code into the password reset form"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:44
+#: kallithea/templates/email_templates/password_reset.html:45
 msgid ""
 "If it weren't you who requested the password reset, just disregard this "
 "message."
@@ -4843,6 +4838,12 @@
 msgid "to"
 msgstr ""
 
+#: kallithea/templates/email_templates/pull_request.html:85
+#, fuzzy
+#| msgid "New Pull Request"
+msgid "View Pull Request"
+msgstr "Новы pull-запыт"
+
 #: kallithea/templates/email_templates/pull_request_comment.html:4
 #, fuzzy, python-format
 #| msgid "%s mentioned you on %s pull request \"%s\""
@@ -4861,12 +4862,24 @@
 msgid "Comment on Pull Request %s \"%s\""
 msgstr "[каментар] у pull-запыце для"
 
-#: kallithea/templates/email_templates/registration.html:22
+#: kallithea/templates/email_templates/registration.html:5
+#, fuzzy
+#| msgid "New user registration"
+msgid "New User Registration"
+msgstr "Рэгістрацыя новага карыстальніка"
+
+#: kallithea/templates/email_templates/registration.html:23
 #, fuzzy
 #| msgid "Group name"
 msgid "Full Name"
 msgstr "Імя групы"
 
+#: kallithea/templates/email_templates/registration.html:42
+#, fuzzy
+#| msgid "View this user here"
+msgid "View User Profile"
+msgstr "Падрабязней пра карыстальніка"
+
 #: kallithea/templates/files/diff_2way.html:15
 #, python-format
 msgid "%s File side-by-side diff"
@@ -5477,45 +5490,45 @@
 msgid "Stats gathered: "
 msgstr "Атрыманая статыстыка: "
 
-#: kallithea/templates/summary/statistics.html:87
-#: kallithea/templates/summary/summary.html:371
+#: kallithea/templates/summary/statistics.html:85
+#: kallithea/templates/summary/summary.html:369
 msgid "files"
 msgstr "файлы"
 
-#: kallithea/templates/summary/statistics.html:111
-#: kallithea/templates/summary/summary.html:401
+#: kallithea/templates/summary/statistics.html:109
+#: kallithea/templates/summary/summary.html:399
 msgid "Show more"
 msgstr "Паказаць яшчэ"
 
-#: kallithea/templates/summary/statistics.html:405
+#: kallithea/templates/summary/statistics.html:395
 msgid "commits"
 msgstr "commit'ы"
 
-#: kallithea/templates/summary/statistics.html:406
+#: kallithea/templates/summary/statistics.html:396
 msgid "files added"
 msgstr "файлы дададзены"
 
-#: kallithea/templates/summary/statistics.html:407
+#: kallithea/templates/summary/statistics.html:397
 msgid "files changed"
 msgstr "файлы зменены"
 
-#: kallithea/templates/summary/statistics.html:408
+#: kallithea/templates/summary/statistics.html:398
 msgid "files removed"
 msgstr "файлы выдалены"
 
-#: kallithea/templates/summary/statistics.html:410
+#: kallithea/templates/summary/statistics.html:400
 msgid "commit"
 msgstr "commit"
 
-#: kallithea/templates/summary/statistics.html:411
+#: kallithea/templates/summary/statistics.html:401
 msgid "file added"
 msgstr "файл выдалены"
 
-#: kallithea/templates/summary/statistics.html:412
+#: kallithea/templates/summary/statistics.html:402
 msgid "file changed"
 msgstr "файл зменены"
 
-#: kallithea/templates/summary/statistics.html:413
+#: kallithea/templates/summary/statistics.html:403
 msgid "file removed"
 msgstr "файл выдалены"
 
@@ -5618,6 +5631,30 @@
 msgid "Download %s as %s"
 msgstr "Спампаваць %s як %s"
 
+#~ msgid "Cache invalidation successful"
+#~ msgstr "Кэш скінуты"
+
+#~ msgid "An error occurred during cache invalidation"
+#~ msgstr "Памылка пры скіданні кэша"
+
+#~ msgid "Caches"
+#~ msgstr "Кэшы"
+
+#~ msgid "Invalidate Repository Cache"
+#~ msgstr "Скінуць кэш рэпазітара"
+
+#~ msgid ""
+#~ "Manually invalidate cache for this repository. On first access, the "
+#~ "repository will be cached again."
+#~ msgstr ""
+#~ "Ручное скіданне кэша рэпазітара. Пры першым доступе кэш адновіцца."
+
+#~ msgid "List of Cached Values"
+#~ msgstr "Спіс кэшаваных значэнняў"
+
+#~ msgid "Prefix"
+#~ msgstr "Прэфікс"
+
 #~ msgid "This repository has been locked by %s on %s"
 #~ msgstr "Рэпазітар заблакаваў %s у %s"
 
@@ -5916,9 +5953,6 @@
 #~ msgid "The comment was made with status"
 #~ msgstr "Каментар пакінуты са статусам"
 
-#~ msgid "View this user here"
-#~ msgstr "Падрабязней пра карыстальніка"
-
 #~ msgid "Repository Size"
 #~ msgstr "Памер рэпазітара"
 
--- a/kallithea/i18n/bg/LC_MESSAGES/kallithea.po	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/i18n/bg/LC_MESSAGES/kallithea.po	Mon May 04 19:24:04 2020 +0200
@@ -7,7 +7,7 @@
 msgstr ""
 "Project-Id-Version: Kallithea 0.4.99\n"
 "Report-Msgid-Bugs-To: translations@kallithea-scm.org\n"
-"POT-Creation-Date: 2020-01-05 04:10+0100\n"
+"POT-Creation-Date: 2020-05-04 19:12+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: Automatically generated\n"
 "Language-Team: none\n"
@@ -18,14 +18,14 @@
 "Generated-By: Babel 2.6.0\n"
 
 #: kallithea/controllers/changelog.py:67
-#: kallithea/controllers/pullrequests.py:250 kallithea/lib/base.py:602
+#: kallithea/controllers/pullrequests.py:247 kallithea/lib/base.py:602
 msgid "There are no changesets yet"
 msgstr ""
 
 #: kallithea/controllers/admin/permissions.py:64
 #: kallithea/controllers/admin/permissions.py:68
 #: kallithea/controllers/admin/permissions.py:72
-#: kallithea/controllers/changelog.py:137
+#: kallithea/controllers/changelog.py:136
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:7
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:88
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:7
@@ -34,36 +34,36 @@
 msgid "None"
 msgstr ""
 
-#: kallithea/controllers/changelog.py:140 kallithea/controllers/files.py:189
+#: kallithea/controllers/changelog.py:139 kallithea/controllers/files.py:189
 msgid "(closed)"
 msgstr ""
 
-#: kallithea/controllers/changeset.py:81
+#: kallithea/controllers/changeset.py:82
 msgid "Show whitespace"
 msgstr ""
 
-#: kallithea/controllers/changeset.py:88
-#: kallithea/controllers/changeset.py:95
+#: kallithea/controllers/changeset.py:89
+#: kallithea/controllers/changeset.py:96
 #: kallithea/templates/files/diff_2way.html:55
 msgid "Ignore whitespace"
 msgstr ""
 
-#: kallithea/controllers/changeset.py:161
+#: kallithea/controllers/changeset.py:162
 #, python-format
 msgid "Increase diff context to %(num)s lines"
 msgstr ""
 
-#: kallithea/controllers/changeset.py:201
+#: kallithea/controllers/changeset.py:202
 msgid "No permission to change status"
 msgstr ""
 
-#: kallithea/controllers/changeset.py:212
+#: kallithea/controllers/changeset.py:213
 #, python-format
 msgid "Successfully deleted pull request %s"
 msgstr ""
 
 #: kallithea/controllers/changeset.py:319 kallithea/controllers/files.py:89
-#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:700
+#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:697
 msgid "Such revision does not exist for this repository"
 msgstr ""
 
@@ -76,61 +76,61 @@
 msgid "Cannot compare repositories of different types"
 msgstr ""
 
-#: kallithea/controllers/compare.py:246
+#: kallithea/controllers/compare.py:247
 msgid "Cannot show empty diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:248
+#: kallithea/controllers/compare.py:249
 msgid "No ancestor found for merge diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:252
+#: kallithea/controllers/compare.py:253
 msgid "Multiple merge ancestors found for merge compare"
 msgstr ""
 
-#: kallithea/controllers/compare.py:268
+#: kallithea/controllers/compare.py:269
 msgid "Cannot compare repositories without using common ancestor"
 msgstr ""
 
-#: kallithea/controllers/error.py:71
+#: kallithea/controllers/error.py:67
 msgid "No response"
 msgstr ""
 
-#: kallithea/controllers/error.py:72
+#: kallithea/controllers/error.py:68
 msgid "Unknown error"
 msgstr ""
 
-#: kallithea/controllers/error.py:85
+#: kallithea/controllers/error.py:81
 msgid ""
 "The request could not be understood by the server due to malformed syntax."
 msgstr ""
 
+#: kallithea/controllers/error.py:84
+msgid "Unauthorized access to resource"
+msgstr ""
+
+#: kallithea/controllers/error.py:86
+msgid "You don't have permission to view this page"
+msgstr ""
+
 #: kallithea/controllers/error.py:88
-msgid "Unauthorized access to resource"
+msgid "The resource could not be found"
 msgstr ""
 
 #: kallithea/controllers/error.py:90
-msgid "You don't have permission to view this page"
-msgstr ""
-
-#: kallithea/controllers/error.py:92
-msgid "The resource could not be found"
-msgstr ""
-
-#: kallithea/controllers/error.py:94
 msgid ""
 "The server encountered an unexpected condition which prevented it from "
 "fulfilling the request."
 msgstr ""
 
-#: kallithea/controllers/feed.py:63
+#: kallithea/controllers/feed.py:59
 #, python-format
 msgid "%s committed on %s"
 msgstr ""
 
-#: kallithea/controllers/feed.py:88
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/controllers/feed.py:84
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/compare/compare_diff.html:95
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
@@ -138,12 +138,12 @@
 msgid "Changeset was too big and was cut off..."
 msgstr ""
 
-#: kallithea/controllers/feed.py:111 kallithea/controllers/feed.py:140
+#: kallithea/controllers/feed.py:107
 #, python-format
 msgid "%s %s feed"
 msgstr ""
 
-#: kallithea/controllers/feed.py:113 kallithea/controllers/feed.py:142
+#: kallithea/controllers/feed.py:109
 #, python-format
 msgid "Changes on %s repository"
 msgstr ""
@@ -161,103 +161,103 @@
 msgid "%s at %s"
 msgstr ""
 
-#: kallithea/controllers/files.py:296
+#: kallithea/controllers/files.py:295
 msgid "You can only delete files with revision being a valid branch"
 msgstr ""
 
-#: kallithea/controllers/files.py:307
+#: kallithea/controllers/files.py:306
 #, python-format
 msgid "Deleted file %s via Kallithea"
 msgstr ""
 
-#: kallithea/controllers/files.py:331
+#: kallithea/controllers/files.py:330
 #, python-format
 msgid "Successfully deleted file %s"
 msgstr ""
 
-#: kallithea/controllers/files.py:335 kallithea/controllers/files.py:394
-#: kallithea/controllers/files.py:469
+#: kallithea/controllers/files.py:334 kallithea/controllers/files.py:392
+#: kallithea/controllers/files.py:467
 msgid "Error occurred during commit"
 msgstr ""
 
-#: kallithea/controllers/files.py:350
+#: kallithea/controllers/files.py:349
 msgid "You can only edit files with revision being a valid branch"
 msgstr ""
 
-#: kallithea/controllers/files.py:364
+#: kallithea/controllers/files.py:363
 #, python-format
 msgid "Edited file %s via Kallithea"
 msgstr ""
 
-#: kallithea/controllers/files.py:380
+#: kallithea/controllers/files.py:378
 msgid "No changes"
 msgstr ""
 
-#: kallithea/controllers/files.py:390 kallithea/controllers/files.py:458
+#: kallithea/controllers/files.py:388 kallithea/controllers/files.py:456
 #, python-format
 msgid "Successfully committed to %s"
 msgstr ""
 
-#: kallithea/controllers/files.py:409
+#: kallithea/controllers/files.py:407
 msgid "Added file via Kallithea"
 msgstr ""
 
-#: kallithea/controllers/files.py:430
+#: kallithea/controllers/files.py:428
 msgid "No content"
 msgstr ""
 
-#: kallithea/controllers/files.py:434
+#: kallithea/controllers/files.py:432
 msgid "No filename"
 msgstr ""
 
-#: kallithea/controllers/files.py:461
+#: kallithea/controllers/files.py:459
 msgid "Location must be relative path and must not contain .. in path"
 msgstr ""
 
-#: kallithea/controllers/files.py:493
+#: kallithea/controllers/files.py:491
 msgid "Downloads disabled"
 msgstr ""
 
+#: kallithea/controllers/files.py:502
+#, python-format
+msgid "Unknown revision %s"
+msgstr ""
+
 #: kallithea/controllers/files.py:504
-#, python-format
-msgid "Unknown revision %s"
+msgid "Empty repository"
 msgstr ""
 
 #: kallithea/controllers/files.py:506
-msgid "Empty repository"
-msgstr ""
-
-#: kallithea/controllers/files.py:508
 msgid "Unknown archive type"
 msgstr ""
 
-#: kallithea/controllers/files.py:729
+#: kallithea/controllers/files.py:726
 #: kallithea/templates/changeset/changeset_range.html:9
 #: kallithea/templates/email_templates/pull_request.html:64
 #: kallithea/templates/pullrequests/pullrequest.html:84
 msgid "Changesets"
 msgstr ""
 
-#: kallithea/controllers/files.py:730
-#: kallithea/controllers/pullrequests.py:182 kallithea/model/scm.py:676
+#: kallithea/controllers/files.py:727
+#: kallithea/controllers/pullrequests.py:174 kallithea/model/scm.py:663
 msgid "Branches"
 msgstr ""
 
-#: kallithea/controllers/files.py:731
-#: kallithea/controllers/pullrequests.py:183 kallithea/model/scm.py:687
+#: kallithea/controllers/files.py:728
+#: kallithea/controllers/pullrequests.py:175 kallithea/model/scm.py:674
 msgid "Tags"
 msgstr ""
 
-#: kallithea/controllers/forks.py:174
+#: kallithea/controllers/forks.py:175
 #, python-format
 msgid "An error occurred during repository forking %s"
 msgstr ""
 
-#: kallithea/controllers/home.py:79
+#: kallithea/controllers/home.py:77
 msgid "Groups"
 msgstr ""
 
-#: kallithea/controllers/home.py:89
+#: kallithea/controllers/home.py:87
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:90
 #: kallithea/templates/admin/repos/repo_add.html:12
 #: kallithea/templates/admin/repos/repo_add.html:16
@@ -269,48 +269,52 @@
 msgid "Repositories"
 msgstr ""
 
-#: kallithea/controllers/home.py:122
+#: kallithea/controllers/home.py:119
 #: kallithea/templates/files/files_add.html:32
 #: kallithea/templates/files/files_delete.html:23
 #: kallithea/templates/files/files_edit.html:32
 msgid "Branch"
 msgstr ""
 
-#: kallithea/controllers/home.py:128
+#: kallithea/controllers/home.py:125
 msgid "Closed Branches"
 msgstr ""
 
-#: kallithea/controllers/home.py:134
+#: kallithea/controllers/home.py:131
 msgid "Tag"
 msgstr ""
 
-#: kallithea/controllers/home.py:140
+#: kallithea/controllers/home.py:137
 msgid "Bookmark"
 msgstr ""
 
-#: kallithea/controllers/journal.py:112 kallithea/controllers/journal.py:154
+#: kallithea/controllers/journal.py:146 kallithea/controllers/journal.py:157
 #: kallithea/templates/journal/public_journal.html:4
 #: kallithea/templates/journal/public_journal.html:18
 msgid "Public Journal"
 msgstr ""
 
-#: kallithea/controllers/journal.py:116 kallithea/controllers/journal.py:158
+#: kallithea/controllers/journal.py:150 kallithea/controllers/journal.py:161
 #: kallithea/templates/base/base.html:290
 #: kallithea/templates/journal/journal.html:5
 #: kallithea/templates/journal/journal.html:13
 msgid "Journal"
 msgstr ""
 
-#: kallithea/controllers/login.py:140 kallithea/controllers/login.py:185
+#: kallithea/controllers/login.py:109
+msgid "Authentication failed."
+msgstr ""
+
+#: kallithea/controllers/login.py:142 kallithea/controllers/login.py:187
 msgid "Bad captcha"
 msgstr ""
 
-#: kallithea/controllers/login.py:146
+#: kallithea/controllers/login.py:148
 #, python-format
 msgid "You have successfully registered with %s"
 msgstr ""
 
-#: kallithea/controllers/login.py:190
+#: kallithea/controllers/login.py:192
 msgid "A password reset confirmation code has been sent"
 msgstr ""
 
@@ -323,226 +327,226 @@
 msgid "Successfully updated password"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:68
+#: kallithea/controllers/pullrequests.py:67
 #, python-format
 msgid "Invalid reviewer \"%s\" specified"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:131
+#: kallithea/controllers/pullrequests.py:123
 #, python-format
 msgid "%s (closed)"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:158
+#: kallithea/controllers/pullrequests.py:150
 #: kallithea/templates/changeset/changeset.html:12
 msgid "Changeset"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:179
+#: kallithea/controllers/pullrequests.py:171
 msgid "Special"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:180
+#: kallithea/controllers/pullrequests.py:172
 msgid "Peer branches"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:181 kallithea/model/scm.py:682
+#: kallithea/controllers/pullrequests.py:173 kallithea/model/scm.py:669
 msgid "Bookmarks"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:318
+#: kallithea/controllers/pullrequests.py:315
 #, python-format
 msgid "Error creating pull request: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:345
-#: kallithea/controllers/pullrequests.py:368
+#: kallithea/controllers/pullrequests.py:342
+#: kallithea/controllers/pullrequests.py:365
 msgid "Error occurred while creating pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:350
+#: kallithea/controllers/pullrequests.py:347
 msgid "Successfully opened new pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:373
+#: kallithea/controllers/pullrequests.py:370
 msgid "New pull request iteration created"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:401
+#: kallithea/controllers/pullrequests.py:398
 #, python-format
 msgid "Meanwhile, the following reviewers have been added: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:405
+#: kallithea/controllers/pullrequests.py:402
 #, python-format
 msgid "Meanwhile, the following reviewers have been removed: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:421
-#: kallithea/model/pull_request.py:232
+#: kallithea/controllers/pullrequests.py:418
+#: kallithea/model/pull_request.py:230
 msgid "No description"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:430
+#: kallithea/controllers/pullrequests.py:427
 msgid "Pull request updated"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:443
+#: kallithea/controllers/pullrequests.py:440
 msgid "Successfully deleted pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:479
+#: kallithea/controllers/pullrequests.py:476
 #, python-format
 msgid "Revision %s not found in %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:506
+#: kallithea/controllers/pullrequests.py:504
 #, python-format
 msgid "Error: changesets not found when displaying pull request from %s."
 msgstr ""
 
+#: kallithea/controllers/pullrequests.py:518
+#, python-format
+msgid "This pull request has already been merged to %s."
+msgstr ""
+
 #: kallithea/controllers/pullrequests.py:520
-#, python-format
-msgid "This pull request has already been merged to %s."
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:522
 msgid "This pull request has been closed and can not be updated."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:546
+#: kallithea/controllers/pullrequests.py:539
 #, python-format
 msgid "The following additional changes are available on %s:"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:548
-#: kallithea/controllers/pullrequests.py:552
+#: kallithea/controllers/pullrequests.py:541
+#: kallithea/controllers/pullrequests.py:545
 msgid "No additional changesets found for iterating on this pull request."
 msgstr ""
 
+#: kallithea/controllers/pullrequests.py:553
+#, python-format
+msgid "Note: Branch %s has another head: %s."
+msgstr ""
+
 #: kallithea/controllers/pullrequests.py:560
-#, python-format
-msgid "Note: Branch %s has another head: %s."
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:567
 msgid "Git pull requests don't support iterating yet."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:569
+#: kallithea/controllers/pullrequests.py:562
 #, python-format
 msgid ""
 "Error: some changesets not found when displaying pull request from %s."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:593
+#: kallithea/controllers/pullrequests.py:586
 msgid "The diff can't be shown - the PR revisions could not be found."
 msgstr ""
 
+#: kallithea/controllers/search.py:132
+msgid "Invalid search query. Try quoting it."
+msgstr ""
+
 #: kallithea/controllers/search.py:136
-msgid "Invalid search query. Try quoting it."
-msgstr ""
-
-#: kallithea/controllers/search.py:140
 msgid "The server has no search index."
 msgstr ""
 
-#: kallithea/controllers/search.py:143
+#: kallithea/controllers/search.py:139
 msgid "An error occurred during search operation."
 msgstr ""
 
-#: kallithea/controllers/summary.py:168
-#: kallithea/templates/summary/summary.html:412
+#: kallithea/controllers/summary.py:169
+#: kallithea/templates/summary/summary.html:410
 msgid "No data ready yet"
 msgstr ""
 
-#: kallithea/controllers/summary.py:171
+#: kallithea/controllers/summary.py:172
 #: kallithea/templates/summary/summary.html:97
 msgid "Statistics are disabled for this repository"
 msgstr ""
 
-#: kallithea/controllers/admin/auth_settings.py:137
+#: kallithea/controllers/admin/auth_settings.py:136
 msgid "Auth settings updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/auth_settings.py:148
+#: kallithea/controllers/admin/auth_settings.py:147
 msgid "error occurred during update of auth settings"
 msgstr ""
 
-#: kallithea/controllers/admin/defaults.py:75
+#: kallithea/controllers/admin/defaults.py:74
 msgid "Default settings updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/defaults.py:90
+#: kallithea/controllers/admin/defaults.py:89
 msgid "Error occurred during update of defaults"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:59
 #: kallithea/controllers/admin/my_account.py:232
-#: kallithea/controllers/admin/users.py:248
+#: kallithea/controllers/admin/users.py:246
 msgid "Forever"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:60
 #: kallithea/controllers/admin/my_account.py:233
-#: kallithea/controllers/admin/users.py:249
+#: kallithea/controllers/admin/users.py:247
 msgid "5 minutes"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:61
 #: kallithea/controllers/admin/my_account.py:234
-#: kallithea/controllers/admin/users.py:250
+#: kallithea/controllers/admin/users.py:248
 msgid "1 hour"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:62
 #: kallithea/controllers/admin/my_account.py:235
-#: kallithea/controllers/admin/users.py:251
+#: kallithea/controllers/admin/users.py:249
 msgid "1 day"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:63
 #: kallithea/controllers/admin/my_account.py:236
-#: kallithea/controllers/admin/users.py:252
+#: kallithea/controllers/admin/users.py:250
 msgid "1 month"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:67
 #: kallithea/controllers/admin/my_account.py:238
-#: kallithea/controllers/admin/users.py:254
+#: kallithea/controllers/admin/users.py:252
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:65
 #: kallithea/templates/admin/users/user_edit_api_keys.html:65
 msgid "Lifetime"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:142
+#: kallithea/controllers/admin/gists.py:148
 msgid "Error occurred during gist creation"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:158
+#: kallithea/controllers/admin/gists.py:164
 #, python-format
 msgid "Deleted gist %s"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:198
+#: kallithea/controllers/admin/gists.py:207
 msgid "Unmodified"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:228
+#: kallithea/controllers/admin/gists.py:237
 msgid "Successfully updated gist content"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:233
+#: kallithea/controllers/admin/gists.py:242
 msgid "Successfully updated gist data"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:236
+#: kallithea/controllers/admin/gists.py:245
 #, python-format
 msgid "Error occurred during update of gist %s"
 msgstr ""
 
-#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:211
-#: kallithea/model/user.py:232
+#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:205
+#: kallithea/model/user.py:226
 msgid "You can't edit this user since it's crucial for entire application"
 msgstr ""
 
@@ -551,7 +555,7 @@
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:134
-#: kallithea/controllers/admin/users.py:181
+#: kallithea/controllers/admin/users.py:179
 #, python-format
 msgid "Error occurred during update of user %s"
 msgstr ""
@@ -561,44 +565,44 @@
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:209
-#: kallithea/controllers/admin/users.py:367
+#: kallithea/controllers/admin/users.py:365
 #, python-format
 msgid "Added email %s to user"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:215
-#: kallithea/controllers/admin/users.py:373
+#: kallithea/controllers/admin/users.py:371
 msgid "An error occurred during email saving"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:224
-#: kallithea/controllers/admin/users.py:383
+#: kallithea/controllers/admin/users.py:381
 msgid "Removed email from user"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:248
-#: kallithea/controllers/admin/users.py:271
+#: kallithea/controllers/admin/users.py:269
 msgid "API key successfully created"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:257
-#: kallithea/controllers/admin/users.py:281
+#: kallithea/controllers/admin/users.py:279
 msgid "API key successfully reset"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:261
-#: kallithea/controllers/admin/users.py:285
+#: kallithea/controllers/admin/users.py:283
 msgid "API key successfully deleted"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:281
-#: kallithea/controllers/admin/users.py:456
+#: kallithea/controllers/admin/users.py:454
 #, python-format
 msgid "SSH key %s successfully added"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:293
-#: kallithea/controllers/admin/users.py:470
+#: kallithea/controllers/admin/users.py:468
 msgid "SSH key successfully deleted"
 msgstr ""
 
@@ -674,11 +678,11 @@
 msgid "Allowed with automatic account activation"
 msgstr ""
 
-#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1673
+#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1578
 msgid "Manual activation of external account"
 msgstr ""
 
-#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1674
+#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1579
 msgid "Automatic activation of external account"
 msgstr ""
 
@@ -700,339 +704,331 @@
 msgid "Error occurred during update of permissions"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:172
+#: kallithea/controllers/admin/repo_groups.py:165
 #, python-format
 msgid "Error occurred during creation of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:177
+#: kallithea/controllers/admin/repo_groups.py:172
 #, python-format
 msgid "Created repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:224
+#: kallithea/controllers/admin/repo_groups.py:219
 #, python-format
 msgid "Updated repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:240
+#: kallithea/controllers/admin/repo_groups.py:235
 #, python-format
 msgid "Error occurred during update of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:250
+#: kallithea/controllers/admin/repo_groups.py:245
 #, python-format
 msgid "This group contains %s repositories and cannot be deleted"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:257
+#: kallithea/controllers/admin/repo_groups.py:252
 #, python-format
 msgid "This group contains %s subgroups and cannot be deleted"
 msgstr ""
 
+#: kallithea/controllers/admin/repo_groups.py:258
+#, python-format
+msgid "Removed repository group %s"
+msgstr ""
+
 #: kallithea/controllers/admin/repo_groups.py:263
 #, python-format
-msgid "Removed repository group %s"
-msgstr ""
-
-#: kallithea/controllers/admin/repo_groups.py:268
-#, python-format
 msgid "Error occurred during deletion of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:352
-#: kallithea/controllers/admin/repo_groups.py:382
-#: kallithea/controllers/admin/user_groups.py:294
+#: kallithea/controllers/admin/repo_groups.py:347
+#: kallithea/controllers/admin/repo_groups.py:377
+#: kallithea/controllers/admin/user_groups.py:290
 msgid "Cannot revoke permission for yourself as admin"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:367
+#: kallithea/controllers/admin/repo_groups.py:362
 msgid "Repository group permissions updated"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:399
-#: kallithea/controllers/admin/repos.py:358
-#: kallithea/controllers/admin/user_groups.py:306
+#: kallithea/controllers/admin/repo_groups.py:394
+#: kallithea/controllers/admin/repos.py:357
+#: kallithea/controllers/admin/user_groups.py:302
 msgid "An error occurred during revoking of permission"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:136
+#: kallithea/controllers/admin/repos.py:137
 #, python-format
 msgid "Error creating repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:194
+#: kallithea/controllers/admin/repos.py:193
 #, python-format
 msgid "Created repository %s from %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:203
+#: kallithea/controllers/admin/repos.py:202
 #, python-format
 msgid "Forked repository %s as %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:206
+#: kallithea/controllers/admin/repos.py:205
 #, python-format
 msgid "Created repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:235
+#: kallithea/controllers/admin/repos.py:234
 #, python-format
 msgid "Repository %s updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:255
+#: kallithea/controllers/admin/repos.py:254
 #, python-format
 msgid "Error occurred during update of repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:273
+#: kallithea/controllers/admin/repos.py:272
 #, python-format
 msgid "Detached %s forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:276
+#: kallithea/controllers/admin/repos.py:275
 #, python-format
 msgid "Deleted %s forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:281
+#: kallithea/controllers/admin/repos.py:280
 #, python-format
 msgid "Deleted repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:284
+#: kallithea/controllers/admin/repos.py:283
 #, python-format
 msgid "Cannot delete repository %s which still has forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:289
+#: kallithea/controllers/admin/repos.py:288
 #, python-format
 msgid "An error occurred during deletion of %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:329
+#: kallithea/controllers/admin/repos.py:328
 msgid "Repository permissions updated"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:388
+#: kallithea/controllers/admin/repos.py:387
 #, python-format
 msgid "Field validation error: %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:391
+#: kallithea/controllers/admin/repos.py:390
 #, python-format
 msgid "An error occurred during creation of field: %r"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:402
+#: kallithea/controllers/admin/repos.py:401
 msgid "An error occurred during removal of field"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:416
+#: kallithea/controllers/admin/repos.py:415
 msgid "-- Not a fork --"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:448
+#: kallithea/controllers/admin/repos.py:447
 msgid "Updated repository visibility in public journal"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:452
+#: kallithea/controllers/admin/repos.py:451
 msgid "An error occurred during setting this repository in public journal"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:468
+#: kallithea/controllers/admin/repos.py:467
 msgid "Nothing"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:470
+#: kallithea/controllers/admin/repos.py:469
 #, python-format
 msgid "Marked repository %s as fork of %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:477
+#: kallithea/controllers/admin/repos.py:476
 msgid "An error occurred during this operation"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:490
-msgid "Cache invalidation successful"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:494
-msgid "An error occurred during cache invalidation"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:507
+#: kallithea/controllers/admin/repos.py:488
 msgid "Pulled from remote location"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:510
+#: kallithea/controllers/admin/repos.py:491
 msgid "An error occurred during pull from remote location"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:541
+#: kallithea/controllers/admin/repos.py:522
 msgid "An error occurred during deletion of repository stats"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:131
+#: kallithea/controllers/admin/settings.py:132
 msgid "Updated VCS settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:135 kallithea/lib/utils.py:237
+#: kallithea/controllers/admin/settings.py:136
 msgid ""
 "Unable to activate hgsubversion support. The \"hgsubversion\" library is "
 "missing"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:141
-#: kallithea/controllers/admin/settings.py:233
+#: kallithea/controllers/admin/settings.py:142
+#: kallithea/controllers/admin/settings.py:234
 msgid "Error occurred while updating application settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:176
+#: kallithea/controllers/admin/settings.py:177
 #, python-format
 msgid "Repositories successfully rescanned. Added: %s. Removed: %s."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:188
+#: kallithea/controllers/admin/settings.py:189
 #, python-format
 msgid "Invalidated %s repositories"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:229
+#: kallithea/controllers/admin/settings.py:230
 msgid "Updated application settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:283
+#: kallithea/controllers/admin/settings.py:284
 msgid "Updated visualisation settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:288
+#: kallithea/controllers/admin/settings.py:289
 msgid "Error occurred during updating visualisation settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:312
+#: kallithea/controllers/admin/settings.py:313
 msgid "Please enter email address"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:327
+#: kallithea/controllers/admin/settings.py:328
 msgid "Send email task created"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:355
+#: kallithea/controllers/admin/settings.py:356
 msgid "Hook already exists"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:357
+#: kallithea/controllers/admin/settings.py:358
 msgid "Builtin hooks are read-only. Please use another hook name."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:360
+#: kallithea/controllers/admin/settings.py:361
 msgid "Added new hook"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:376
+#: kallithea/controllers/admin/settings.py:377
 msgid "Updated hooks"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:380
+#: kallithea/controllers/admin/settings.py:381
 msgid "Error occurred during hook creation"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:404
+#: kallithea/controllers/admin/settings.py:405
 msgid "Whoosh reindex task scheduled"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:138
+#: kallithea/controllers/admin/user_groups.py:134
 #, python-format
 msgid "Created user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:151
+#: kallithea/controllers/admin/user_groups.py:147
 #, python-format
 msgid "Error occurred during creation of user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:179
+#: kallithea/controllers/admin/user_groups.py:175
 #, python-format
 msgid "Updated user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:201
+#: kallithea/controllers/admin/user_groups.py:197
 #, python-format
 msgid "Error occurred during update of user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:212
+#: kallithea/controllers/admin/user_groups.py:208
 msgid "Successfully deleted user group"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:217
+#: kallithea/controllers/admin/user_groups.py:213
 msgid "An error occurred during deletion of user group"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:273
+#: kallithea/controllers/admin/user_groups.py:269
 msgid "Target group cannot be the same"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:279
+#: kallithea/controllers/admin/user_groups.py:275
 msgid "User group permissions updated"
 msgstr ""
 
+#: kallithea/controllers/admin/user_groups.py:384
+#: kallithea/controllers/admin/users.py:336
+msgid "Updated permissions"
+msgstr ""
+
 #: kallithea/controllers/admin/user_groups.py:388
-#: kallithea/controllers/admin/users.py:338
-msgid "Updated permissions"
-msgstr ""
-
-#: kallithea/controllers/admin/user_groups.py:392
-#: kallithea/controllers/admin/users.py:342
+#: kallithea/controllers/admin/users.py:340
 msgid "An error occurred during permissions saving"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:123
+#: kallithea/controllers/admin/users.py:121
 #, python-format
 msgid "Created user %s"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:138
+#: kallithea/controllers/admin/users.py:136
 #, python-format
 msgid "Error occurred during creation of user %s"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:162
+#: kallithea/controllers/admin/users.py:160
 msgid "User updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:190
+#: kallithea/controllers/admin/users.py:188
 msgid "Successfully deleted user"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:195
+#: kallithea/controllers/admin/users.py:193
 msgid "An error occurred during deletion of user"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:203
+#: kallithea/controllers/admin/users.py:201
 msgid "The default user cannot be edited"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:409
+#: kallithea/controllers/admin/users.py:407
 #, python-format
 msgid "Added IP address %s to user whitelist"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:415
+#: kallithea/controllers/admin/users.py:413
 msgid "An error occurred while adding IP address"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:427
+#: kallithea/controllers/admin/users.py:425
 msgid "Removed IP address from user whitelist"
 msgstr ""
 
-#: kallithea/lib/auth.py:684
+#: kallithea/lib/auth.py:634
 msgid "You need to be a registered user to perform this action"
 msgstr ""
 
-#: kallithea/lib/auth.py:712
+#: kallithea/lib/auth.py:662
 msgid "You need to be signed in to view this page"
 msgstr ""
 
@@ -1063,170 +1059,170 @@
 "Changeset was too big and was cut off, use diff menu to display this diff"
 msgstr ""
 
-#: kallithea/lib/diffs.py:224
+#: kallithea/lib/diffs.py:223
 msgid "No changes detected"
 msgstr ""
 
-#: kallithea/lib/helpers.py:653
+#: kallithea/lib/helpers.py:670
 #, python-format
 msgid "Deleted branch: %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:655
+#: kallithea/lib/helpers.py:672
 #, python-format
 msgid "Created tag: %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:666
+#: kallithea/lib/helpers.py:683
 #, python-format
 msgid "Changeset %s not found"
 msgstr ""
 
-#: kallithea/lib/helpers.py:715
+#: kallithea/lib/helpers.py:732
 #, python-format
 msgid "Show all combined changesets %s->%s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:721
+#: kallithea/lib/helpers.py:738
 msgid "Compare view"
 msgstr ""
 
-#: kallithea/lib/helpers.py:740
+#: kallithea/lib/helpers.py:757
 msgid "and"
 msgstr ""
 
-#: kallithea/lib/helpers.py:741
+#: kallithea/lib/helpers.py:758
 #, python-format
 msgid "%s more"
 msgstr ""
 
-#: kallithea/lib/helpers.py:742
+#: kallithea/lib/helpers.py:759
 #: kallithea/templates/changelog/changelog.html:43
 msgid "revisions"
 msgstr ""
 
-#: kallithea/lib/helpers.py:766
+#: kallithea/lib/helpers.py:783
 #, python-format
 msgid "Fork name %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:787
+#: kallithea/lib/helpers.py:804
 #, python-format
 msgid "Pull request %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:797
+#: kallithea/lib/helpers.py:814
 msgid "[deleted] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:799 kallithea/lib/helpers.py:811
+#: kallithea/lib/helpers.py:816 kallithea/lib/helpers.py:828
 msgid "[created] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:801
+#: kallithea/lib/helpers.py:818
 msgid "[created] repository as fork"
 msgstr ""
 
-#: kallithea/lib/helpers.py:803 kallithea/lib/helpers.py:813
+#: kallithea/lib/helpers.py:820 kallithea/lib/helpers.py:830
 msgid "[forked] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:805 kallithea/lib/helpers.py:815
+#: kallithea/lib/helpers.py:822 kallithea/lib/helpers.py:832
 msgid "[updated] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:807
+#: kallithea/lib/helpers.py:824
 msgid "[downloaded] archive from repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:809
+#: kallithea/lib/helpers.py:826
 msgid "[delete] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:817
+#: kallithea/lib/helpers.py:834
 msgid "[created] user"
 msgstr ""
 
-#: kallithea/lib/helpers.py:819
+#: kallithea/lib/helpers.py:836
 msgid "[updated] user"
 msgstr ""
 
-#: kallithea/lib/helpers.py:821
+#: kallithea/lib/helpers.py:838
 msgid "[created] user group"
 msgstr ""
 
-#: kallithea/lib/helpers.py:823
+#: kallithea/lib/helpers.py:840
 msgid "[updated] user group"
 msgstr ""
 
-#: kallithea/lib/helpers.py:825
+#: kallithea/lib/helpers.py:842
 msgid "[commented] on revision in repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:827
+#: kallithea/lib/helpers.py:844
 msgid "[commented] on pull request for"
 msgstr ""
 
-#: kallithea/lib/helpers.py:829
+#: kallithea/lib/helpers.py:846
 msgid "[closed] pull request for"
 msgstr ""
 
-#: kallithea/lib/helpers.py:831
+#: kallithea/lib/helpers.py:848
 msgid "[pushed] into"
 msgstr ""
 
-#: kallithea/lib/helpers.py:833
+#: kallithea/lib/helpers.py:850
 msgid "[committed via Kallithea] into repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:835
+#: kallithea/lib/helpers.py:852
 msgid "[pulled from remote] into repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:837
+#: kallithea/lib/helpers.py:854
 msgid "[pulled] from"
 msgstr ""
 
-#: kallithea/lib/helpers.py:839
+#: kallithea/lib/helpers.py:856
 msgid "[started following] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:841
+#: kallithea/lib/helpers.py:858
 msgid "[stopped following] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:961
+#: kallithea/lib/helpers.py:975
 #, python-format
 msgid " and %s more"
 msgstr ""
 
-#: kallithea/lib/helpers.py:965
+#: kallithea/lib/helpers.py:979
 #: kallithea/templates/compare/compare_diff.html:69
 #: kallithea/templates/pullrequests/pullrequest_show.html:297
 msgid "No files"
 msgstr ""
 
-#: kallithea/lib/helpers.py:990
+#: kallithea/lib/helpers.py:1004
 msgid "new file"
 msgstr ""
 
-#: kallithea/lib/helpers.py:993
+#: kallithea/lib/helpers.py:1007
 msgid "mod"
 msgstr ""
 
-#: kallithea/lib/helpers.py:996
+#: kallithea/lib/helpers.py:1010
 msgid "del"
 msgstr ""
 
-#: kallithea/lib/helpers.py:999
+#: kallithea/lib/helpers.py:1013
 msgid "rename"
 msgstr ""
 
-#: kallithea/lib/helpers.py:1004
+#: kallithea/lib/helpers.py:1018
 msgid "chmod"
 msgstr ""
 
-#: kallithea/lib/helpers.py:1297
+#: kallithea/lib/helpers.py:1323
 #, python-format
 msgid ""
 "%s repository is not mapped to db perhaps it was created or renamed from "
@@ -1234,96 +1230,98 @@
 "repositories"
 msgstr ""
 
-#: kallithea/lib/ssh.py:73
+#: kallithea/lib/ssh.py:75
 msgid "SSH key is missing"
 msgstr ""
 
-#: kallithea/lib/ssh.py:77
-msgid "Incorrect SSH key - it must have both a key type and a base64 part"
-msgstr ""
-
-#: kallithea/lib/ssh.py:81
+#: kallithea/lib/ssh.py:79
+msgid ""
+"Incorrect SSH key - it must have both a key type and a base64 part, like "
+"'ssh-rsa ASRNeaZu4FA...xlJp='"
+msgstr ""
+
+#: kallithea/lib/ssh.py:83
 msgid "Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'"
 msgstr ""
 
-#: kallithea/lib/ssh.py:84
+#: kallithea/lib/ssh.py:86
 #, python-format
 msgid "Incorrect SSH key - unexpected characters in base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:89
+#: kallithea/lib/ssh.py:91
 #, python-format
 msgid "Incorrect SSH key - failed to decode base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:92
+#: kallithea/lib/ssh.py:94
 #, python-format
 msgid "Incorrect SSH key - base64 part is not %r as claimed but %r"
 msgstr ""
 
-#: kallithea/lib/utils2.py:334
+#: kallithea/lib/utils2.py:253
 #, python-format
 msgid "%d year"
 msgid_plural "%d years"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:335
+#: kallithea/lib/utils2.py:254
 #, python-format
 msgid "%d month"
 msgid_plural "%d months"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:336
+#: kallithea/lib/utils2.py:255
 #, python-format
 msgid "%d day"
 msgid_plural "%d days"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:337
+#: kallithea/lib/utils2.py:256
 #, python-format
 msgid "%d hour"
 msgid_plural "%d hours"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:338
+#: kallithea/lib/utils2.py:257
 #, python-format
 msgid "%d minute"
 msgid_plural "%d minutes"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:339
+#: kallithea/lib/utils2.py:258
 #, python-format
 msgid "%d second"
 msgid_plural "%d seconds"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:355
+#: kallithea/lib/utils2.py:274
 #, python-format
 msgid "in %s"
 msgstr ""
 
-#: kallithea/lib/utils2.py:357
+#: kallithea/lib/utils2.py:276
 #, python-format
 msgid "%s ago"
 msgstr ""
 
-#: kallithea/lib/utils2.py:359
+#: kallithea/lib/utils2.py:278
 #, python-format
 msgid "in %s and %s"
 msgstr ""
 
-#: kallithea/lib/utils2.py:362
+#: kallithea/lib/utils2.py:281
 #, python-format
 msgid "%s and %s ago"
 msgstr ""
 
-#: kallithea/lib/utils2.py:365
+#: kallithea/lib/utils2.py:284
 msgid "just now"
 msgstr ""
 
@@ -1332,133 +1330,133 @@
 msgid "on line %s"
 msgstr ""
 
-#: kallithea/model/comment.py:221 kallithea/model/pull_request.py:114
+#: kallithea/model/comment.py:219 kallithea/model/pull_request.py:112
 msgid "[Mention]"
 msgstr ""
 
-#: kallithea/model/db.py:1496
+#: kallithea/model/db.py:1411
 msgid "top level"
 msgstr ""
 
-#: kallithea/model/db.py:1637
+#: kallithea/model/db.py:1542
 msgid "Kallithea Administrator"
 msgstr ""
 
-#: kallithea/model/db.py:1639
+#: kallithea/model/db.py:1544
 msgid "Default user has no access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1640
+#: kallithea/model/db.py:1545
 msgid "Default user has read access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1641
+#: kallithea/model/db.py:1546
 msgid "Default user has write access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1642
+#: kallithea/model/db.py:1547
 msgid "Default user has admin access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1644
+#: kallithea/model/db.py:1549
 msgid "Default user has no access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1645
+#: kallithea/model/db.py:1550
 msgid "Default user has read access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1646
+#: kallithea/model/db.py:1551
 msgid "Default user has write access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1647
+#: kallithea/model/db.py:1552
 msgid "Default user has admin access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1649
+#: kallithea/model/db.py:1554
 msgid "Default user has no access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1650
+#: kallithea/model/db.py:1555
 msgid "Default user has read access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1651
+#: kallithea/model/db.py:1556
 msgid "Default user has write access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1652
+#: kallithea/model/db.py:1557
 msgid "Default user has admin access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1654
+#: kallithea/model/db.py:1559
 msgid "Only admins can create repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1655
+#: kallithea/model/db.py:1560
 msgid "Non-admins can create repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1657
+#: kallithea/model/db.py:1562
 msgid "Only admins can create user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1658
+#: kallithea/model/db.py:1563
 msgid "Non-admins can create user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1660
+#: kallithea/model/db.py:1565
 msgid "Only admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1661
+#: kallithea/model/db.py:1566
 msgid "Non-admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1663
+#: kallithea/model/db.py:1568
 msgid ""
 "Repository creation enabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1664
+#: kallithea/model/db.py:1569
 msgid ""
 "Repository creation disabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1666
+#: kallithea/model/db.py:1571
 msgid "Only admins can fork repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1667
+#: kallithea/model/db.py:1572
 msgid "Non-admins can fork repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1669
+#: kallithea/model/db.py:1574
 msgid "Registration disabled"
 msgstr ""
 
-#: kallithea/model/db.py:1670
+#: kallithea/model/db.py:1575
 msgid "User registration with manual account activation"
 msgstr ""
 
-#: kallithea/model/db.py:1671
+#: kallithea/model/db.py:1576
 msgid "User registration with automatic account activation"
 msgstr ""
 
-#: kallithea/model/db.py:2206
+#: kallithea/model/db.py:1992
 msgid "Not reviewed"
 msgstr ""
 
-#: kallithea/model/db.py:2207
+#: kallithea/model/db.py:1993
 msgid "Under review"
 msgstr ""
 
-#: kallithea/model/db.py:2208
+#: kallithea/model/db.py:1994
 msgid "Not approved"
 msgstr ""
 
-#: kallithea/model/db.py:2209
+#: kallithea/model/db.py:1995
 msgid "Approved"
 msgstr ""
 
@@ -1484,313 +1482,313 @@
 msgid "Name must not contain only digits"
 msgstr ""
 
-#: kallithea/model/notification.py:164
+#: kallithea/model/notification.py:162
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s changeset %(short_id)s \"%(message_short)s\" on "
 "%(branch)s"
 msgstr ""
 
-#: kallithea/model/notification.py:167
+#: kallithea/model/notification.py:165
 #, python-format
 msgid "New user %(new_username)s registered"
 msgstr ""
 
-#: kallithea/model/notification.py:169
+#: kallithea/model/notification.py:167
 #, python-format
 msgid ""
 "[Review] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:170
+#: kallithea/model/notification.py:168
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:183
+#: kallithea/model/notification.py:188
 msgid "Closing"
 msgstr ""
 
-#: kallithea/model/pull_request.py:73
+#: kallithea/model/pull_request.py:72
 #, python-format
 msgid ""
 "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:209
+#: kallithea/model/pull_request.py:207
 msgid "Cannot create empty pull request"
 msgstr ""
 
-#: kallithea/model/pull_request.py:217
+#: kallithea/model/pull_request.py:215
 #, python-format
 msgid ""
 "Cannot create pull request - criss cross merge detected, please merge a "
 "later %s revision to %s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:245 kallithea/model/pull_request.py:380
+#: kallithea/model/pull_request.py:243 kallithea/model/pull_request.py:378
 msgid "You are not authorized to create the pull request"
 msgstr ""
 
-#: kallithea/model/pull_request.py:339
+#: kallithea/model/pull_request.py:337
 msgid "Missing changesets since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:346
+#: kallithea/model/pull_request.py:344
 #, python-format
 msgid "New changesets on %s %s since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:353
+#: kallithea/model/pull_request.py:351
 msgid "Ancestor didn't change - diff since previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:360
+#: kallithea/model/pull_request.py:358
 #, python-format
 msgid ""
 "This iteration is based on another %s revision and there is no simple "
 "diff."
 msgstr ""
 
-#: kallithea/model/pull_request.py:362
+#: kallithea/model/pull_request.py:360
 #, python-format
 msgid "No changes found on %s %s since previous iteration."
 msgstr ""
 
-#: kallithea/model/pull_request.py:388
+#: kallithea/model/pull_request.py:386
 #, python-format
 msgid "Closed, next iteration: %s ."
 msgstr ""
 
-#: kallithea/model/scm.py:668
+#: kallithea/model/scm.py:655
 msgid "latest tip"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:56
+#: kallithea/model/ssh_key.py:57
 #, python-format
 msgid "SSH key %r is invalid: %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:68
+#: kallithea/model/ssh_key.py:69
 #, python-format
 msgid "SSH key %s is already used by %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:89
-#, python-format
-msgid "SSH key %r not found"
-msgstr ""
-
-#: kallithea/model/user.py:186
+#: kallithea/model/ssh_key.py:88
+#, python-format
+msgid "SSH key with fingerprint %r found"
+msgstr ""
+
+#: kallithea/model/user.py:180
 msgid "New user registration"
 msgstr ""
 
-#: kallithea/model/user.py:250
+#: kallithea/model/user.py:244
 msgid ""
 "You can't remove this user since it is crucial for the entire application"
 msgstr ""
 
-#: kallithea/model/user.py:255
+#: kallithea/model/user.py:249
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repositories and cannot be removed. Switch "
 "owners or remove those repositories: %s"
 msgstr ""
 
-#: kallithea/model/user.py:260
+#: kallithea/model/user.py:254
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repository groups and cannot be removed. Switch "
 "owners or remove those repository groups: %s"
 msgstr ""
 
-#: kallithea/model/user.py:267
+#: kallithea/model/user.py:261
 #, python-format
 msgid ""
 "User \"%s\" still owns %s user groups and cannot be removed. Switch "
 "owners or remove those user groups: %s"
 msgstr ""
 
-#: kallithea/model/user.py:361
+#: kallithea/model/user.py:355
 msgid "Password reset link"
 msgstr ""
 
-#: kallithea/model/user.py:408
+#: kallithea/model/user.py:402
 msgid "Password reset notification"
 msgstr ""
 
-#: kallithea/model/user.py:409
+#: kallithea/model/user.py:403
 #, python-format
 msgid ""
 "The password to your account %s has been changed using password reset "
 "form."
 msgstr ""
 
-#: kallithea/model/validators.py:52 kallithea/model/validators.py:53
+#: kallithea/model/validators.py:53 kallithea/model/validators.py:54
 msgid "Value cannot be an empty list"
 msgstr ""
 
-#: kallithea/model/validators.py:72
+#: kallithea/model/validators.py:73
 #, python-format
 msgid "Username \"%(username)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:74
+#: kallithea/model/validators.py:75
 #, python-format
 msgid "Username \"%(username)s\" cannot be used"
 msgstr ""
 
-#: kallithea/model/validators.py:76
+#: kallithea/model/validators.py:77
 msgid ""
 "Username may only contain alphanumeric characters underscores, periods or "
 "dashes and must begin with an alphanumeric character or underscore"
 msgstr ""
 
-#: kallithea/model/validators.py:103
+#: kallithea/model/validators.py:104
 msgid "The input is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:110
+#: kallithea/model/validators.py:111
 #, python-format
 msgid "Username %(username)s is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:131
-msgid "Invalid user group name"
-msgstr ""
-
 #: kallithea/model/validators.py:132
+msgid "Invalid user group name"
+msgstr ""
+
+#: kallithea/model/validators.py:133
 #, python-format
 msgid "User group \"%(usergroup)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:134
+#: kallithea/model/validators.py:135
 msgid ""
 "user group name may only contain alphanumeric characters underscores, "
 "periods or dashes and must begin with alphanumeric character"
 msgstr ""
 
-#: kallithea/model/validators.py:174
-msgid "Cannot assign this group as parent"
-msgstr ""
-
 #: kallithea/model/validators.py:175
+msgid "Cannot assign this group as parent"
+msgstr ""
+
+#: kallithea/model/validators.py:176
 #, python-format
 msgid "Group \"%(group_name)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:177
+#: kallithea/model/validators.py:178
 #, python-format
 msgid "Repository with name \"%(group_name)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:233
+#: kallithea/model/validators.py:230
 msgid "Invalid characters (non-ascii) in password"
 msgstr ""
 
-#: kallithea/model/validators.py:248
+#: kallithea/model/validators.py:245
 msgid "Invalid old password"
 msgstr ""
 
-#: kallithea/model/validators.py:264
+#: kallithea/model/validators.py:261
 msgid "Passwords do not match"
 msgstr ""
 
-#: kallithea/model/validators.py:279
+#: kallithea/model/validators.py:276
 msgid "Invalid username or password"
 msgstr ""
 
+#: kallithea/model/validators.py:310
+#, python-format
+msgid "Repository name %(repo)s is not allowed"
+msgstr ""
+
+#: kallithea/model/validators.py:312
+#, python-format
+msgid "Repository named %(repo)s already exists"
+msgstr ""
+
 #: kallithea/model/validators.py:313
 #, python-format
-msgid "Repository name %(repo)s is not allowed"
+msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
 msgstr ""
 
 #: kallithea/model/validators.py:315
 #, python-format
-msgid "Repository named %(repo)s already exists"
-msgstr ""
-
-#: kallithea/model/validators.py:316
-#, python-format
-msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
-msgstr ""
-
-#: kallithea/model/validators.py:318
-#, python-format
 msgid "Repository group with name \"%(repo)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:404
+#: kallithea/model/validators.py:401
 msgid "Invalid repository URL"
 msgstr ""
 
-#: kallithea/model/validators.py:405
+#: kallithea/model/validators.py:402
 msgid ""
 "Invalid repository URL. It must be a valid http, https, ssh, svn+http or "
 "svn+https URL"
 msgstr ""
 
-#: kallithea/model/validators.py:430
+#: kallithea/model/validators.py:427
 msgid "Fork has to be the same type as parent"
 msgstr ""
 
-#: kallithea/model/validators.py:445
+#: kallithea/model/validators.py:442
 msgid "You don't have permissions to create repository in this group"
 msgstr ""
 
-#: kallithea/model/validators.py:447
+#: kallithea/model/validators.py:444
 msgid "no permission to create repository in root location"
 msgstr ""
 
-#: kallithea/model/validators.py:497
+#: kallithea/model/validators.py:494
 msgid "You don't have permissions to create a group in this location"
 msgstr ""
 
-#: kallithea/model/validators.py:537
+#: kallithea/model/validators.py:534
 msgid "This username or user group name is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:630
+#: kallithea/model/validators.py:627
 msgid "This is not a valid path"
 msgstr ""
 
-#: kallithea/model/validators.py:647
+#: kallithea/model/validators.py:644
 msgid "This email address is already in use"
 msgstr ""
 
-#: kallithea/model/validators.py:667
+#: kallithea/model/validators.py:664
 #, python-format
 msgid "Email address \"%(email)s\" not found"
 msgstr ""
 
-#: kallithea/model/validators.py:704
+#: kallithea/model/validators.py:701
 msgid ""
 "The LDAP Login attribute of the CN must be specified - this is the name "
 "of the attribute that is equivalent to \"username\""
 msgstr ""
 
-#: kallithea/model/validators.py:716
+#: kallithea/model/validators.py:713
 msgid "Please enter a valid IPv4 or IPv6 address"
 msgstr ""
 
-#: kallithea/model/validators.py:717
+#: kallithea/model/validators.py:714
 #, python-format
 msgid ""
 "The network size (bits) must be within the range of 0-32 (not %(bits)r)"
 msgstr ""
 
-#: kallithea/model/validators.py:750
+#: kallithea/model/validators.py:747
 msgid "Key name can only consist of letters, underscore, dash or numbers"
 msgstr ""
 
-#: kallithea/model/validators.py:764
+#: kallithea/model/validators.py:761
 msgid "Filename cannot be inside a directory"
 msgstr ""
 
-#: kallithea/model/validators.py:780
+#: kallithea/model/validators.py:777
 #, python-format
 msgid "Plugins %(loaded)s and %(next_to_load)s both export the same name"
 msgstr ""
@@ -1850,7 +1848,7 @@
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:60
 #: kallithea/templates/email_templates/pull_request.html:37
 #: kallithea/templates/forks/fork.html:34
-#: kallithea/templates/index_base.html:58
+#: kallithea/templates/index_base.html:59
 #: kallithea/templates/pullrequests/pullrequest.html:33
 #: kallithea/templates/pullrequests/pullrequest_show.html:38
 #: kallithea/templates/pullrequests/pullrequest_show.html:59
@@ -1858,14 +1856,14 @@
 msgid "Description"
 msgstr ""
 
-#: kallithea/templates/index_base.html:60
+#: kallithea/templates/index_base.html:61
 msgid "Last Change"
 msgstr ""
 
 #: kallithea/templates/admin/my_account/my_account_repos.html:15
 #: kallithea/templates/admin/my_account/my_account_watched.html:15
 #: kallithea/templates/admin/repos/repos.html:41
-#: kallithea/templates/index_base.html:62
+#: kallithea/templates/index_base.html:63
 msgid "Tip"
 msgstr ""
 
@@ -1875,7 +1873,7 @@
 #: kallithea/templates/admin/repos/repos.html:42
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:8
 #: kallithea/templates/admin/user_groups/user_groups.html:42
-#: kallithea/templates/index_base.html:63
+#: kallithea/templates/index_base.html:64
 #: kallithea/templates/pullrequests/pullrequest_data.html:16
 #: kallithea/templates/pullrequests/pullrequest_show.html:124
 #: kallithea/templates/pullrequests/pullrequest_show.html:219
@@ -1899,7 +1897,7 @@
 #: kallithea/templates/admin/users/user_edit_profile.html:18
 #: kallithea/templates/admin/users/users.html:37
 #: kallithea/templates/base/base.html:364
-#: kallithea/templates/email_templates/registration.html:11
+#: kallithea/templates/email_templates/registration.html:12
 #: kallithea/templates/login.html:28 kallithea/templates/register.html:31
 msgid "Username"
 msgstr ""
@@ -2023,7 +2021,7 @@
 #: kallithea/templates/admin/settings/settings.html:31
 #: kallithea/templates/admin/users/user_add.html:62
 #: kallithea/templates/admin/users/user_edit_profile.html:25
-#: kallithea/templates/email_templates/registration.html:33
+#: kallithea/templates/email_templates/registration.html:34
 #: kallithea/templates/register.html:66
 msgid "Email"
 msgstr ""
@@ -2270,7 +2268,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/gists/index.html:51
-#: kallithea/templates/data_table/_dt_elements.html:78
+#: kallithea/templates/data_table/_dt_elements.html:84
 msgid "Created"
 msgstr ""
 
@@ -2354,13 +2352,13 @@
 #: kallithea/templates/admin/users/user_edit_ips.html:21
 #: kallithea/templates/changeset/changeset_file_comment.html:30
 #: kallithea/templates/changeset/changeset_file_comment.html:121
-#: kallithea/templates/data_table/_dt_elements.html:69
-#: kallithea/templates/data_table/_dt_elements.html:89
-#: kallithea/templates/data_table/_dt_elements.html:91
-#: kallithea/templates/data_table/_dt_elements.html:101
-#: kallithea/templates/data_table/_dt_elements.html:103
-#: kallithea/templates/data_table/_dt_elements.html:120
-#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:75
+#: kallithea/templates/data_table/_dt_elements.html:95
+#: kallithea/templates/data_table/_dt_elements.html:97
+#: kallithea/templates/data_table/_dt_elements.html:107
+#: kallithea/templates/data_table/_dt_elements.html:109
+#: kallithea/templates/data_table/_dt_elements.html:126
+#: kallithea/templates/data_table/_dt_elements.html:128
 #: kallithea/templates/files/files_source.html:35
 #: kallithea/templates/files/files_source.html:38
 #: kallithea/templates/files/files_source.html:41
@@ -2376,14 +2374,14 @@
 #: kallithea/templates/base/perms_summary.html:44
 #: kallithea/templates/base/perms_summary.html:81
 #: kallithea/templates/base/perms_summary.html:83
-#: kallithea/templates/data_table/_dt_elements.html:63
-#: kallithea/templates/data_table/_dt_elements.html:64
-#: kallithea/templates/data_table/_dt_elements.html:85
-#: kallithea/templates/data_table/_dt_elements.html:86
-#: kallithea/templates/data_table/_dt_elements.html:97
-#: kallithea/templates/data_table/_dt_elements.html:98
-#: kallithea/templates/data_table/_dt_elements.html:116
-#: kallithea/templates/data_table/_dt_elements.html:117
+#: kallithea/templates/data_table/_dt_elements.html:69
+#: kallithea/templates/data_table/_dt_elements.html:70
+#: kallithea/templates/data_table/_dt_elements.html:91
+#: kallithea/templates/data_table/_dt_elements.html:92
+#: kallithea/templates/data_table/_dt_elements.html:103
+#: kallithea/templates/data_table/_dt_elements.html:104
+#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:123
 #: kallithea/templates/files/diff_2way.html:56
 #: kallithea/templates/files/files_source.html:37
 #: kallithea/templates/files/files_source.html:40
@@ -2684,7 +2682,7 @@
 #: kallithea/templates/admin/permissions/permissions_globals.html:27
 #: kallithea/templates/admin/repos/repo_add_base.html:28
 #: kallithea/templates/admin/repos/repo_edit_settings.html:33
-#: kallithea/templates/data_table/_dt_elements.html:134
+#: kallithea/templates/data_table/_dt_elements.html:140
 #: kallithea/templates/forks/fork.html:42
 msgid "Repository group"
 msgstr ""
@@ -2705,7 +2703,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:40
-#: kallithea/templates/data_table/_dt_elements.html:141
+#: kallithea/templates/data_table/_dt_elements.html:147
 msgid "User group"
 msgstr ""
 
@@ -2878,7 +2876,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:121
+#: kallithea/templates/data_table/_dt_elements.html:127
 #, python-format
 msgid "Confirm to delete this group: %s with %s repository"
 msgid_plural "Confirm to delete this group: %s with %s repositories"
@@ -3041,14 +3039,10 @@
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit.html:37
-msgid "Caches"
+msgid "Remote"
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit.html:40
-msgid "Remote"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit.html:43
 #: kallithea/templates/summary/statistics.html:8
 #: kallithea/templates/summary/summary.html:169
 #: kallithea/templates/summary/summary.html:170
@@ -3086,7 +3080,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:46
-#: kallithea/templates/data_table/_dt_elements.html:68
+#: kallithea/templates/data_table/_dt_elements.html:74
 #, python-format
 msgid "Confirm to delete this repository: %s"
 msgstr ""
@@ -3117,43 +3111,14 @@
 "it or restore it."
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:4
-msgid "Invalidate Repository Cache"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:6
-msgid ""
-"Manually invalidate cache for this repository. On first access, the "
-"repository will be cached again."
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:9
-msgid "List of Cached Values"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:12
-msgid "Prefix"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:13
+#: kallithea/templates/admin/repos/repo_edit_fields.html:6
+msgid "Label"
+msgstr ""
+
 #: kallithea/templates/admin/repos/repo_edit_fields.html:7
 msgid "Key"
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:14
-#: kallithea/templates/admin/user_groups/user_group_add.html:40
-#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
-#: kallithea/templates/admin/user_groups/user_groups.html:41
-#: kallithea/templates/admin/users/user_add.html:69
-#: kallithea/templates/admin/users/user_edit_profile.html:74
-#: kallithea/templates/admin/users/users.html:42
-msgid "Active"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_fields.html:6
-msgid "Label"
-msgstr ""
-
 #: kallithea/templates/admin/repos/repo_edit_fields.html:20
 #, python-format
 msgid "Confirm to delete this field: %s"
@@ -3666,6 +3631,15 @@
 msgid "Short, optional description for this user group."
 msgstr ""
 
+#: kallithea/templates/admin/user_groups/user_group_add.html:40
+#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
+#: kallithea/templates/admin/user_groups/user_groups.html:41
+#: kallithea/templates/admin/users/user_add.html:69
+#: kallithea/templates/admin/users/user_edit_profile.html:74
+#: kallithea/templates/admin/users/users.html:42
+msgid "Active"
+msgstr ""
+
 #: kallithea/templates/admin/user_groups/user_group_edit.html:5
 #, python-format
 msgid "%s user group settings"
@@ -3687,7 +3661,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:19
-#: kallithea/templates/data_table/_dt_elements.html:102
+#: kallithea/templates/data_table/_dt_elements.html:108
 #, python-format
 msgid "Confirm to delete this user group: %s"
 msgstr ""
@@ -3761,7 +3735,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:90
+#: kallithea/templates/data_table/_dt_elements.html:96
 #, python-format
 msgid "Confirm to delete this user: %s"
 msgstr ""
@@ -3861,10 +3835,12 @@
 msgstr ""
 
 #: kallithea/templates/base/base.html:167
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Follow"
 msgstr ""
 
 #: kallithea/templates/base/base.html:168
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Unfollow"
 msgstr ""
 
@@ -4268,23 +4244,23 @@
 msgid "Merge"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:96
+#: kallithea/templates/changeset/changeset.html:95
 msgid "Grafted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:102
+#: kallithea/templates/changeset/changeset.html:100
 msgid "Transplanted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:108
+#: kallithea/templates/changeset/changeset.html:106
 msgid "Replaced by:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:122
+#: kallithea/templates/changeset/changeset.html:120
 msgid "Preceded by:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:139
+#: kallithea/templates/changeset/changeset.html:137
 #: kallithea/templates/compare/compare_diff.html:59
 #: kallithea/templates/pullrequests/pullrequest_show.html:290
 #, python-format
@@ -4293,7 +4269,7 @@
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/templates/changeset/changeset.html:141
+#: kallithea/templates/changeset/changeset.html:139
 #: kallithea/templates/compare/compare_diff.html:61
 #: kallithea/templates/pullrequests/pullrequest_show.html:292
 #, python-format
@@ -4302,8 +4278,8 @@
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
 #: kallithea/templates/pullrequests/pullrequest_show.html:333
@@ -4532,23 +4508,23 @@
 msgid "Repository creation in progress..."
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:42
+#: kallithea/templates/data_table/_dt_elements.html:48
 msgid "No changesets yet"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:48
-#: kallithea/templates/data_table/_dt_elements.html:50
+#: kallithea/templates/data_table/_dt_elements.html:54
+#: kallithea/templates/data_table/_dt_elements.html:56
 #, python-format
 msgid "Subscribe to %s rss feed"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:56
-#: kallithea/templates/data_table/_dt_elements.html:58
+#: kallithea/templates/data_table/_dt_elements.html:62
+#: kallithea/templates/data_table/_dt_elements.html:64
 #, python-format
 msgid "Subscribe to %s atom feed"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:76
+#: kallithea/templates/data_table/_dt_elements.html:82
 msgid "Creating"
 msgstr ""
 
@@ -4580,6 +4556,11 @@
 msgid "by"
 msgstr ""
 
+#: kallithea/templates/email_templates/changeset_comment.html:36
+#: kallithea/templates/email_templates/pull_request_comment.html:43
+msgid "View Comment"
+msgstr ""
+
 #: kallithea/templates/email_templates/comment.html:27
 msgid "Status change:"
 msgstr ""
@@ -4588,32 +4569,40 @@
 msgid "The pull request has been closed."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:9
+#: kallithea/templates/email_templates/default.html:4
+msgid "Message"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:4
+msgid "Password Reset Request"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:10
 #, python-format
 msgid "Hello %s"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:16
+#: kallithea/templates/email_templates/password_reset.html:17
 msgid "We have received a request to reset the password for your account."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:25
+#: kallithea/templates/email_templates/password_reset.html:26
 msgid ""
 "This account is however managed outside this system and the password "
 "cannot be changed here."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:28
+#: kallithea/templates/email_templates/password_reset.html:29
 msgid "To set a new password, click the following link"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:33
+#: kallithea/templates/email_templates/password_reset.html:34
 msgid ""
 "Should you not be able to use the link above, please type the following "
 "code into the password reset form"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:44
+#: kallithea/templates/email_templates/password_reset.html:45
 msgid ""
 "If it weren't you who requested the password reset, just disregard this "
 "message."
@@ -4644,6 +4633,10 @@
 msgid "to"
 msgstr ""
 
+#: kallithea/templates/email_templates/pull_request.html:85
+msgid "View Pull Request"
+msgstr ""
+
 #: kallithea/templates/email_templates/pull_request_comment.html:4
 #, python-format
 msgid "Mention in Comment on Pull Request %s \"%s\""
@@ -4659,10 +4652,18 @@
 msgid "Comment on Pull Request %s \"%s\""
 msgstr ""
 
-#: kallithea/templates/email_templates/registration.html:22
+#: kallithea/templates/email_templates/registration.html:5
+msgid "New User Registration"
+msgstr ""
+
+#: kallithea/templates/email_templates/registration.html:23
 msgid "Full Name"
 msgstr ""
 
+#: kallithea/templates/email_templates/registration.html:42
+msgid "View User Profile"
+msgstr ""
+
 #: kallithea/templates/files/diff_2way.html:15
 #, python-format
 msgid "%s File side-by-side diff"
@@ -5263,45 +5264,45 @@
 msgid "Stats gathered: "
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:87
-#: kallithea/templates/summary/summary.html:371
+#: kallithea/templates/summary/statistics.html:85
+#: kallithea/templates/summary/summary.html:369
 msgid "files"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:111
-#: kallithea/templates/summary/summary.html:401
+#: kallithea/templates/summary/statistics.html:109
+#: kallithea/templates/summary/summary.html:399
 msgid "Show more"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:405
+#: kallithea/templates/summary/statistics.html:395
 msgid "commits"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:406
+#: kallithea/templates/summary/statistics.html:396
 msgid "files added"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:407
+#: kallithea/templates/summary/statistics.html:397
 msgid "files changed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:408
+#: kallithea/templates/summary/statistics.html:398
 msgid "files removed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:410
+#: kallithea/templates/summary/statistics.html:400
 msgid "commit"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:411
+#: kallithea/templates/summary/statistics.html:401
 msgid "file added"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:412
+#: kallithea/templates/summary/statistics.html:402
 msgid "file changed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:413
+#: kallithea/templates/summary/statistics.html:403
 msgid "file removed"
 msgstr ""
 
--- a/kallithea/i18n/cs/LC_MESSAGES/kallithea.po	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/i18n/cs/LC_MESSAGES/kallithea.po	Mon May 04 19:24:04 2020 +0200
@@ -5,7 +5,7 @@
 msgstr ""
 "Project-Id-Version: Kallithea 0.3\n"
 "Report-Msgid-Bugs-To: translations@kallithea-scm.org\n"
-"POT-Creation-Date: 2020-01-05 04:10+0100\n"
+"POT-Creation-Date: 2020-05-04 19:12+0200\n"
 "PO-Revision-Date: 2015-11-12 08:51+0000\n"
 "Last-Translator: Michal Čihař <michal@cihar.com>\n"
 "Language-Team: Czech <https://hosted.weblate.org/projects/kallithea/"
@@ -18,14 +18,14 @@
 "X-Generator: Weblate 2.5-dev\n"
 
 #: kallithea/controllers/changelog.py:67
-#: kallithea/controllers/pullrequests.py:250 kallithea/lib/base.py:602
+#: kallithea/controllers/pullrequests.py:247 kallithea/lib/base.py:602
 msgid "There are no changesets yet"
 msgstr ""
 
 #: kallithea/controllers/admin/permissions.py:64
 #: kallithea/controllers/admin/permissions.py:68
 #: kallithea/controllers/admin/permissions.py:72
-#: kallithea/controllers/changelog.py:137
+#: kallithea/controllers/changelog.py:136
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:7
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:88
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:7
@@ -34,37 +34,37 @@
 msgid "None"
 msgstr ""
 
-#: kallithea/controllers/changelog.py:140 kallithea/controllers/files.py:189
+#: kallithea/controllers/changelog.py:139 kallithea/controllers/files.py:189
 msgid "(closed)"
 msgstr "(zavřeno)"
 
-#: kallithea/controllers/changeset.py:81
+#: kallithea/controllers/changeset.py:82
 msgid "Show whitespace"
 msgstr ""
 
-#: kallithea/controllers/changeset.py:88
-#: kallithea/controllers/changeset.py:95
+#: kallithea/controllers/changeset.py:89
+#: kallithea/controllers/changeset.py:96
 #: kallithea/templates/files/diff_2way.html:55
 msgid "Ignore whitespace"
 msgstr ""
 
-#: kallithea/controllers/changeset.py:161
+#: kallithea/controllers/changeset.py:162
 #, python-format
 msgid "Increase diff context to %(num)s lines"
 msgstr ""
 
-#: kallithea/controllers/changeset.py:201
+#: kallithea/controllers/changeset.py:202
 #, fuzzy
 msgid "No permission to change status"
 msgstr "Změny"
 
-#: kallithea/controllers/changeset.py:212
+#: kallithea/controllers/changeset.py:213
 #, fuzzy, python-format
 msgid "Successfully deleted pull request %s"
 msgstr "Úspěšně aktualizované heslo"
 
 #: kallithea/controllers/changeset.py:319 kallithea/controllers/files.py:89
-#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:700
+#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:697
 msgid "Such revision does not exist for this repository"
 msgstr "Taková revize neexistuje"
 
@@ -78,62 +78,62 @@
 msgid "Cannot compare repositories of different types"
 msgstr ""
 
-#: kallithea/controllers/compare.py:246
+#: kallithea/controllers/compare.py:247
 msgid "Cannot show empty diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:248
+#: kallithea/controllers/compare.py:249
 msgid "No ancestor found for merge diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:252
+#: kallithea/controllers/compare.py:253
 msgid "Multiple merge ancestors found for merge compare"
 msgstr ""
 
-#: kallithea/controllers/compare.py:268
+#: kallithea/controllers/compare.py:269
 msgid "Cannot compare repositories without using common ancestor"
 msgstr ""
 
-#: kallithea/controllers/error.py:71
+#: kallithea/controllers/error.py:67
 #, fuzzy
 msgid "No response"
 msgstr "Neznámá revize %s"
 
-#: kallithea/controllers/error.py:72
+#: kallithea/controllers/error.py:68
 msgid "Unknown error"
 msgstr ""
 
-#: kallithea/controllers/error.py:85
+#: kallithea/controllers/error.py:81
 msgid ""
 "The request could not be understood by the server due to malformed syntax."
 msgstr ""
 
+#: kallithea/controllers/error.py:84
+msgid "Unauthorized access to resource"
+msgstr ""
+
+#: kallithea/controllers/error.py:86
+msgid "You don't have permission to view this page"
+msgstr "Nemáte oprávnění k zobrazení této stránky"
+
 #: kallithea/controllers/error.py:88
-msgid "Unauthorized access to resource"
+msgid "The resource could not be found"
 msgstr ""
 
 #: kallithea/controllers/error.py:90
-msgid "You don't have permission to view this page"
-msgstr "Nemáte oprávnění k zobrazení této stránky"
-
-#: kallithea/controllers/error.py:92
-msgid "The resource could not be found"
-msgstr ""
-
-#: kallithea/controllers/error.py:94
 msgid ""
 "The server encountered an unexpected condition which prevented it from "
 "fulfilling the request."
 msgstr ""
 
-#: kallithea/controllers/feed.py:63
+#: kallithea/controllers/feed.py:59
 #, python-format
 msgid "%s committed on %s"
 msgstr ""
 
-#: kallithea/controllers/feed.py:88
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/controllers/feed.py:84
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/compare/compare_diff.html:95
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
@@ -141,12 +141,12 @@
 msgid "Changeset was too big and was cut off..."
 msgstr ""
 
-#: kallithea/controllers/feed.py:111 kallithea/controllers/feed.py:140
+#: kallithea/controllers/feed.py:107
 #, python-format
 msgid "%s %s feed"
 msgstr ""
 
-#: kallithea/controllers/feed.py:113 kallithea/controllers/feed.py:142
+#: kallithea/controllers/feed.py:109
 #, python-format
 msgid "Changes on %s repository"
 msgstr "Změny na repozitáři %s"
@@ -166,103 +166,103 @@
 msgid "%s at %s"
 msgstr ""
 
-#: kallithea/controllers/files.py:296
+#: kallithea/controllers/files.py:295
 msgid "You can only delete files with revision being a valid branch"
 msgstr ""
 
-#: kallithea/controllers/files.py:307
+#: kallithea/controllers/files.py:306
 #, python-format
 msgid "Deleted file %s via Kallithea"
 msgstr ""
 
-#: kallithea/controllers/files.py:331
+#: kallithea/controllers/files.py:330
 #, python-format
 msgid "Successfully deleted file %s"
 msgstr ""
 
-#: kallithea/controllers/files.py:335 kallithea/controllers/files.py:394
-#: kallithea/controllers/files.py:469
+#: kallithea/controllers/files.py:334 kallithea/controllers/files.py:392
+#: kallithea/controllers/files.py:467
 msgid "Error occurred during commit"
 msgstr ""
 
-#: kallithea/controllers/files.py:350
+#: kallithea/controllers/files.py:349
 msgid "You can only edit files with revision being a valid branch"
 msgstr ""
 
-#: kallithea/controllers/files.py:364
+#: kallithea/controllers/files.py:363
 #, python-format
 msgid "Edited file %s via Kallithea"
 msgstr ""
 
-#: kallithea/controllers/files.py:380
+#: kallithea/controllers/files.py:378
 msgid "No changes"
 msgstr "Žádné změny"
 
-#: kallithea/controllers/files.py:390 kallithea/controllers/files.py:458
+#: kallithea/controllers/files.py:388 kallithea/controllers/files.py:456
 #, python-format
 msgid "Successfully committed to %s"
 msgstr ""
 
-#: kallithea/controllers/files.py:409
+#: kallithea/controllers/files.py:407
 msgid "Added file via Kallithea"
 msgstr "Přidaný soubor přes Kallithea"
 
-#: kallithea/controllers/files.py:430
+#: kallithea/controllers/files.py:428
 msgid "No content"
 msgstr "Žádný obsah"
 
-#: kallithea/controllers/files.py:434
+#: kallithea/controllers/files.py:432
 msgid "No filename"
 msgstr ""
 
-#: kallithea/controllers/files.py:461
+#: kallithea/controllers/files.py:459
 msgid "Location must be relative path and must not contain .. in path"
 msgstr ""
 
-#: kallithea/controllers/files.py:493
+#: kallithea/controllers/files.py:491
 msgid "Downloads disabled"
 msgstr "Stahování vypnuto"
 
-#: kallithea/controllers/files.py:504
+#: kallithea/controllers/files.py:502
 #, python-format
 msgid "Unknown revision %s"
 msgstr "Neznámá revize %s"
 
-#: kallithea/controllers/files.py:506
+#: kallithea/controllers/files.py:504
 msgid "Empty repository"
 msgstr "Prázdný repozitář"
 
-#: kallithea/controllers/files.py:508
+#: kallithea/controllers/files.py:506
 msgid "Unknown archive type"
 msgstr ""
 
-#: kallithea/controllers/files.py:729
+#: kallithea/controllers/files.py:726
 #: kallithea/templates/changeset/changeset_range.html:9
 #: kallithea/templates/email_templates/pull_request.html:64
 #: kallithea/templates/pullrequests/pullrequest.html:84
 msgid "Changesets"
 msgstr "Změny"
 
-#: kallithea/controllers/files.py:730
-#: kallithea/controllers/pullrequests.py:182 kallithea/model/scm.py:676
+#: kallithea/controllers/files.py:727
+#: kallithea/controllers/pullrequests.py:174 kallithea/model/scm.py:663
 msgid "Branches"
 msgstr "Větve"
 
-#: kallithea/controllers/files.py:731
-#: kallithea/controllers/pullrequests.py:183 kallithea/model/scm.py:687
+#: kallithea/controllers/files.py:728
+#: kallithea/controllers/pullrequests.py:175 kallithea/model/scm.py:674
 msgid "Tags"
 msgstr "Tagy"
 
-#: kallithea/controllers/forks.py:174
+#: kallithea/controllers/forks.py:175
 #, python-format
 msgid "An error occurred during repository forking %s"
 msgstr ""
 
-#: kallithea/controllers/home.py:79
+#: kallithea/controllers/home.py:77
 msgid "Groups"
 msgstr "Skupiny"
 
-#: kallithea/controllers/home.py:89
+#: kallithea/controllers/home.py:87
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:90
 #: kallithea/templates/admin/repos/repo_add.html:12
 #: kallithea/templates/admin/repos/repo_add.html:16
@@ -274,48 +274,52 @@
 msgid "Repositories"
 msgstr "Repozitáře"
 
-#: kallithea/controllers/home.py:122
+#: kallithea/controllers/home.py:119
 #: kallithea/templates/files/files_add.html:32
 #: kallithea/templates/files/files_delete.html:23
 #: kallithea/templates/files/files_edit.html:32
 msgid "Branch"
 msgstr "Větev"
 
-#: kallithea/controllers/home.py:128
+#: kallithea/controllers/home.py:125
 msgid "Closed Branches"
 msgstr ""
 
-#: kallithea/controllers/home.py:134
+#: kallithea/controllers/home.py:131
 msgid "Tag"
 msgstr "Tag"
 
-#: kallithea/controllers/home.py:140
+#: kallithea/controllers/home.py:137
 msgid "Bookmark"
 msgstr "Záložka"
 
-#: kallithea/controllers/journal.py:112 kallithea/controllers/journal.py:154
+#: kallithea/controllers/journal.py:146 kallithea/controllers/journal.py:157
 #: kallithea/templates/journal/public_journal.html:4
 #: kallithea/templates/journal/public_journal.html:18
 msgid "Public Journal"
 msgstr ""
 
-#: kallithea/controllers/journal.py:116 kallithea/controllers/journal.py:158
+#: kallithea/controllers/journal.py:150 kallithea/controllers/journal.py:161
 #: kallithea/templates/base/base.html:290
 #: kallithea/templates/journal/journal.html:5
 #: kallithea/templates/journal/journal.html:13
 msgid "Journal"
 msgstr ""
 
-#: kallithea/controllers/login.py:140 kallithea/controllers/login.py:185
+#: kallithea/controllers/login.py:109
+msgid "Authentication failed."
+msgstr ""
+
+#: kallithea/controllers/login.py:142 kallithea/controllers/login.py:187
 msgid "Bad captcha"
 msgstr "Špatná captcha"
 
-#: kallithea/controllers/login.py:146
+#: kallithea/controllers/login.py:148
 #, python-format
 msgid "You have successfully registered with %s"
 msgstr ""
 
-#: kallithea/controllers/login.py:190
+#: kallithea/controllers/login.py:192
 msgid "A password reset confirmation code has been sent"
 msgstr ""
 
@@ -328,226 +332,226 @@
 msgid "Successfully updated password"
 msgstr "Úspěšně aktualizované heslo"
 
-#: kallithea/controllers/pullrequests.py:68
+#: kallithea/controllers/pullrequests.py:67
 #, python-format
 msgid "Invalid reviewer \"%s\" specified"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:131
+#: kallithea/controllers/pullrequests.py:123
 #, python-format
 msgid "%s (closed)"
 msgstr "%s (zavřené)"
 
-#: kallithea/controllers/pullrequests.py:158
+#: kallithea/controllers/pullrequests.py:150
 #: kallithea/templates/changeset/changeset.html:12
 msgid "Changeset"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:179
+#: kallithea/controllers/pullrequests.py:171
 msgid "Special"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:180
+#: kallithea/controllers/pullrequests.py:172
 msgid "Peer branches"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:181 kallithea/model/scm.py:682
+#: kallithea/controllers/pullrequests.py:173 kallithea/model/scm.py:669
 msgid "Bookmarks"
 msgstr "Záložky"
 
-#: kallithea/controllers/pullrequests.py:318
+#: kallithea/controllers/pullrequests.py:315
 #, python-format
 msgid "Error creating pull request: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:345
-#: kallithea/controllers/pullrequests.py:368
+#: kallithea/controllers/pullrequests.py:342
+#: kallithea/controllers/pullrequests.py:365
 msgid "Error occurred while creating pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:350
+#: kallithea/controllers/pullrequests.py:347
 msgid "Successfully opened new pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:373
+#: kallithea/controllers/pullrequests.py:370
 msgid "New pull request iteration created"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:401
+#: kallithea/controllers/pullrequests.py:398
 #, python-format
 msgid "Meanwhile, the following reviewers have been added: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:405
+#: kallithea/controllers/pullrequests.py:402
 #, python-format
 msgid "Meanwhile, the following reviewers have been removed: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:421
-#: kallithea/model/pull_request.py:232
+#: kallithea/controllers/pullrequests.py:418
+#: kallithea/model/pull_request.py:230
 msgid "No description"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:430
+#: kallithea/controllers/pullrequests.py:427
 msgid "Pull request updated"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:443
+#: kallithea/controllers/pullrequests.py:440
 msgid "Successfully deleted pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:479
+#: kallithea/controllers/pullrequests.py:476
 #, python-format
 msgid "Revision %s not found in %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:506
+#: kallithea/controllers/pullrequests.py:504
 #, python-format
 msgid "Error: changesets not found when displaying pull request from %s."
 msgstr ""
 
+#: kallithea/controllers/pullrequests.py:518
+#, python-format
+msgid "This pull request has already been merged to %s."
+msgstr ""
+
 #: kallithea/controllers/pullrequests.py:520
-#, python-format
-msgid "This pull request has already been merged to %s."
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:522
 msgid "This pull request has been closed and can not be updated."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:546
+#: kallithea/controllers/pullrequests.py:539
 #, python-format
 msgid "The following additional changes are available on %s:"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:548
-#: kallithea/controllers/pullrequests.py:552
+#: kallithea/controllers/pullrequests.py:541
+#: kallithea/controllers/pullrequests.py:545
 msgid "No additional changesets found for iterating on this pull request."
 msgstr ""
 
+#: kallithea/controllers/pullrequests.py:553
+#, python-format
+msgid "Note: Branch %s has another head: %s."
+msgstr ""
+
 #: kallithea/controllers/pullrequests.py:560
-#, python-format
-msgid "Note: Branch %s has another head: %s."
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:567
 msgid "Git pull requests don't support iterating yet."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:569
+#: kallithea/controllers/pullrequests.py:562
 #, python-format
 msgid ""
 "Error: some changesets not found when displaying pull request from %s."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:593
+#: kallithea/controllers/pullrequests.py:586
 msgid "The diff can't be shown - the PR revisions could not be found."
 msgstr ""
 
+#: kallithea/controllers/search.py:132
+msgid "Invalid search query. Try quoting it."
+msgstr ""
+
 #: kallithea/controllers/search.py:136
-msgid "Invalid search query. Try quoting it."
-msgstr ""
-
-#: kallithea/controllers/search.py:140
 msgid "The server has no search index."
 msgstr ""
 
-#: kallithea/controllers/search.py:143
+#: kallithea/controllers/search.py:139
 msgid "An error occurred during search operation."
 msgstr "Došlo k chybě při vyhledávání."
 
-#: kallithea/controllers/summary.py:168
-#: kallithea/templates/summary/summary.html:412
+#: kallithea/controllers/summary.py:169
+#: kallithea/templates/summary/summary.html:410
 msgid "No data ready yet"
 msgstr ""
 
-#: kallithea/controllers/summary.py:171
+#: kallithea/controllers/summary.py:172
 #: kallithea/templates/summary/summary.html:97
 msgid "Statistics are disabled for this repository"
 msgstr ""
 
-#: kallithea/controllers/admin/auth_settings.py:137
+#: kallithea/controllers/admin/auth_settings.py:136
 msgid "Auth settings updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/auth_settings.py:148
+#: kallithea/controllers/admin/auth_settings.py:147
 msgid "error occurred during update of auth settings"
 msgstr ""
 
-#: kallithea/controllers/admin/defaults.py:75
+#: kallithea/controllers/admin/defaults.py:74
 msgid "Default settings updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/defaults.py:90
+#: kallithea/controllers/admin/defaults.py:89
 msgid "Error occurred during update of defaults"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:59
 #: kallithea/controllers/admin/my_account.py:232
-#: kallithea/controllers/admin/users.py:248
+#: kallithea/controllers/admin/users.py:246
 msgid "Forever"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:60
 #: kallithea/controllers/admin/my_account.py:233
-#: kallithea/controllers/admin/users.py:249
+#: kallithea/controllers/admin/users.py:247
 msgid "5 minutes"
 msgstr "5 minut"
 
 #: kallithea/controllers/admin/gists.py:61
 #: kallithea/controllers/admin/my_account.py:234
-#: kallithea/controllers/admin/users.py:250
+#: kallithea/controllers/admin/users.py:248
 msgid "1 hour"
 msgstr "1 hodina"
 
 #: kallithea/controllers/admin/gists.py:62
 #: kallithea/controllers/admin/my_account.py:235
-#: kallithea/controllers/admin/users.py:251
+#: kallithea/controllers/admin/users.py:249
 msgid "1 day"
 msgstr "1 den"
 
 #: kallithea/controllers/admin/gists.py:63
 #: kallithea/controllers/admin/my_account.py:236
-#: kallithea/controllers/admin/users.py:252
+#: kallithea/controllers/admin/users.py:250
 msgid "1 month"
 msgstr "1 měsíc"
 
 #: kallithea/controllers/admin/gists.py:67
 #: kallithea/controllers/admin/my_account.py:238
-#: kallithea/controllers/admin/users.py:254
+#: kallithea/controllers/admin/users.py:252
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:65
 #: kallithea/templates/admin/users/user_edit_api_keys.html:65
 msgid "Lifetime"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:142
+#: kallithea/controllers/admin/gists.py:148
 msgid "Error occurred during gist creation"
 msgstr "Došlo k chybě při vytváření gist"
 
-#: kallithea/controllers/admin/gists.py:158
+#: kallithea/controllers/admin/gists.py:164
 #, python-format
 msgid "Deleted gist %s"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:198
+#: kallithea/controllers/admin/gists.py:207
 msgid "Unmodified"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:228
+#: kallithea/controllers/admin/gists.py:237
 msgid "Successfully updated gist content"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:233
+#: kallithea/controllers/admin/gists.py:242
 msgid "Successfully updated gist data"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:236
+#: kallithea/controllers/admin/gists.py:245
 #, python-format
 msgid "Error occurred during update of gist %s"
 msgstr ""
 
-#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:211
-#: kallithea/model/user.py:232
+#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:205
+#: kallithea/model/user.py:226
 msgid "You can't edit this user since it's crucial for entire application"
 msgstr ""
 
@@ -556,7 +560,7 @@
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:134
-#: kallithea/controllers/admin/users.py:181
+#: kallithea/controllers/admin/users.py:179
 #, python-format
 msgid "Error occurred during update of user %s"
 msgstr ""
@@ -566,44 +570,44 @@
 msgstr "Došlo k chybě při aktualizaci hesla uživatele"
 
 #: kallithea/controllers/admin/my_account.py:209
-#: kallithea/controllers/admin/users.py:367
+#: kallithea/controllers/admin/users.py:365
 #, python-format
 msgid "Added email %s to user"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:215
-#: kallithea/controllers/admin/users.py:373
+#: kallithea/controllers/admin/users.py:371
 msgid "An error occurred during email saving"
 msgstr "Došlo k chybě při ukládání e-mailové adresy"
 
 #: kallithea/controllers/admin/my_account.py:224
-#: kallithea/controllers/admin/users.py:383
+#: kallithea/controllers/admin/users.py:381
 msgid "Removed email from user"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:248
-#: kallithea/controllers/admin/users.py:271
+#: kallithea/controllers/admin/users.py:269
 msgid "API key successfully created"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:257
-#: kallithea/controllers/admin/users.py:281
+#: kallithea/controllers/admin/users.py:279
 msgid "API key successfully reset"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:261
-#: kallithea/controllers/admin/users.py:285
+#: kallithea/controllers/admin/users.py:283
 msgid "API key successfully deleted"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:281
-#: kallithea/controllers/admin/users.py:456
+#: kallithea/controllers/admin/users.py:454
 #, python-format
 msgid "SSH key %s successfully added"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:293
-#: kallithea/controllers/admin/users.py:470
+#: kallithea/controllers/admin/users.py:468
 #, fuzzy
 msgid "SSH key successfully deleted"
 msgstr "Úspěšně aktualizované heslo"
@@ -680,11 +684,11 @@
 msgid "Allowed with automatic account activation"
 msgstr ""
 
-#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1673
+#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1578
 msgid "Manual activation of external account"
 msgstr ""
 
-#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1674
+#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1579
 msgid "Automatic activation of external account"
 msgstr ""
 
@@ -706,341 +710,333 @@
 msgid "Error occurred during update of permissions"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:172
+#: kallithea/controllers/admin/repo_groups.py:165
 #, python-format
 msgid "Error occurred during creation of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:177
+#: kallithea/controllers/admin/repo_groups.py:172
 #, python-format
 msgid "Created repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:224
+#: kallithea/controllers/admin/repo_groups.py:219
 #, python-format
 msgid "Updated repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:240
+#: kallithea/controllers/admin/repo_groups.py:235
 #, python-format
 msgid "Error occurred during update of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:250
+#: kallithea/controllers/admin/repo_groups.py:245
 #, python-format
 msgid "This group contains %s repositories and cannot be deleted"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:257
+#: kallithea/controllers/admin/repo_groups.py:252
 #, python-format
 msgid "This group contains %s subgroups and cannot be deleted"
 msgstr ""
 
+#: kallithea/controllers/admin/repo_groups.py:258
+#, python-format
+msgid "Removed repository group %s"
+msgstr ""
+
 #: kallithea/controllers/admin/repo_groups.py:263
 #, python-format
-msgid "Removed repository group %s"
-msgstr ""
-
-#: kallithea/controllers/admin/repo_groups.py:268
-#, python-format
 msgid "Error occurred during deletion of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:352
-#: kallithea/controllers/admin/repo_groups.py:382
-#: kallithea/controllers/admin/user_groups.py:294
+#: kallithea/controllers/admin/repo_groups.py:347
+#: kallithea/controllers/admin/repo_groups.py:377
+#: kallithea/controllers/admin/user_groups.py:290
 msgid "Cannot revoke permission for yourself as admin"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:367
+#: kallithea/controllers/admin/repo_groups.py:362
 msgid "Repository group permissions updated"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:399
-#: kallithea/controllers/admin/repos.py:358
-#: kallithea/controllers/admin/user_groups.py:306
+#: kallithea/controllers/admin/repo_groups.py:394
+#: kallithea/controllers/admin/repos.py:357
+#: kallithea/controllers/admin/user_groups.py:302
 msgid "An error occurred during revoking of permission"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:136
+#: kallithea/controllers/admin/repos.py:137
 #, python-format
 msgid "Error creating repository %s"
 msgstr "Chyba při vytváření repozitáře %s"
 
-#: kallithea/controllers/admin/repos.py:194
+#: kallithea/controllers/admin/repos.py:193
 #, python-format
 msgid "Created repository %s from %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:203
+#: kallithea/controllers/admin/repos.py:202
 #, python-format
 msgid "Forked repository %s as %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:206
+#: kallithea/controllers/admin/repos.py:205
 #, python-format
 msgid "Created repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:235
+#: kallithea/controllers/admin/repos.py:234
 #, python-format
 msgid "Repository %s updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:255
+#: kallithea/controllers/admin/repos.py:254
 #, python-format
 msgid "Error occurred during update of repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:273
+#: kallithea/controllers/admin/repos.py:272
 #, python-format
 msgid "Detached %s forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:276
+#: kallithea/controllers/admin/repos.py:275
 #, python-format
 msgid "Deleted %s forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:281
+#: kallithea/controllers/admin/repos.py:280
 #, python-format
 msgid "Deleted repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:284
+#: kallithea/controllers/admin/repos.py:283
 #, python-format
 msgid "Cannot delete repository %s which still has forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:289
+#: kallithea/controllers/admin/repos.py:288
 #, python-format
 msgid "An error occurred during deletion of %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:329
+#: kallithea/controllers/admin/repos.py:328
 msgid "Repository permissions updated"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:388
+#: kallithea/controllers/admin/repos.py:387
 #, python-format
 msgid "Field validation error: %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:391
+#: kallithea/controllers/admin/repos.py:390
 #, fuzzy, python-format
 #| msgid "Error occurred during gist creation"
 msgid "An error occurred during creation of field: %r"
 msgstr "Došlo k chybě při vytváření gist"
 
-#: kallithea/controllers/admin/repos.py:402
+#: kallithea/controllers/admin/repos.py:401
 msgid "An error occurred during removal of field"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:416
+#: kallithea/controllers/admin/repos.py:415
 msgid "-- Not a fork --"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:448
+#: kallithea/controllers/admin/repos.py:447
 msgid "Updated repository visibility in public journal"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:452
+#: kallithea/controllers/admin/repos.py:451
 msgid "An error occurred during setting this repository in public journal"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:468
+#: kallithea/controllers/admin/repos.py:467
 msgid "Nothing"
 msgstr "Nic"
 
-#: kallithea/controllers/admin/repos.py:470
+#: kallithea/controllers/admin/repos.py:469
 #, python-format
 msgid "Marked repository %s as fork of %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:477
+#: kallithea/controllers/admin/repos.py:476
 msgid "An error occurred during this operation"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:490
-msgid "Cache invalidation successful"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:494
-msgid "An error occurred during cache invalidation"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:507
+#: kallithea/controllers/admin/repos.py:488
 msgid "Pulled from remote location"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:510
+#: kallithea/controllers/admin/repos.py:491
 msgid "An error occurred during pull from remote location"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:541
+#: kallithea/controllers/admin/repos.py:522
 msgid "An error occurred during deletion of repository stats"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:131
+#: kallithea/controllers/admin/settings.py:132
 msgid "Updated VCS settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:135 kallithea/lib/utils.py:237
+#: kallithea/controllers/admin/settings.py:136
 msgid ""
 "Unable to activate hgsubversion support. The \"hgsubversion\" library is "
 "missing"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:141
-#: kallithea/controllers/admin/settings.py:233
+#: kallithea/controllers/admin/settings.py:142
+#: kallithea/controllers/admin/settings.py:234
 msgid "Error occurred while updating application settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:176
+#: kallithea/controllers/admin/settings.py:177
 #, python-format
 msgid "Repositories successfully rescanned. Added: %s. Removed: %s."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:188
+#: kallithea/controllers/admin/settings.py:189
 #, fuzzy, python-format
 #| msgid "Watched Repositories"
 msgid "Invalidated %s repositories"
 msgstr "Repozitáře"
 
-#: kallithea/controllers/admin/settings.py:229
+#: kallithea/controllers/admin/settings.py:230
 msgid "Updated application settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:283
+#: kallithea/controllers/admin/settings.py:284
 msgid "Updated visualisation settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:288
+#: kallithea/controllers/admin/settings.py:289
 msgid "Error occurred during updating visualisation settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:312
+#: kallithea/controllers/admin/settings.py:313
 msgid "Please enter email address"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:327
+#: kallithea/controllers/admin/settings.py:328
 msgid "Send email task created"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:355
+#: kallithea/controllers/admin/settings.py:356
 msgid "Hook already exists"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:357
+#: kallithea/controllers/admin/settings.py:358
 msgid "Builtin hooks are read-only. Please use another hook name."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:360
+#: kallithea/controllers/admin/settings.py:361
 msgid "Added new hook"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:376
+#: kallithea/controllers/admin/settings.py:377
 msgid "Updated hooks"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:380
+#: kallithea/controllers/admin/settings.py:381
 msgid "Error occurred during hook creation"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:404
+#: kallithea/controllers/admin/settings.py:405
 msgid "Whoosh reindex task scheduled"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:138
+#: kallithea/controllers/admin/user_groups.py:134
 #, python-format
 msgid "Created user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:151
+#: kallithea/controllers/admin/user_groups.py:147
 #, python-format
 msgid "Error occurred during creation of user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:179
+#: kallithea/controllers/admin/user_groups.py:175
 #, python-format
 msgid "Updated user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:201
+#: kallithea/controllers/admin/user_groups.py:197
 #, python-format
 msgid "Error occurred during update of user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:212
+#: kallithea/controllers/admin/user_groups.py:208
 msgid "Successfully deleted user group"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:217
+#: kallithea/controllers/admin/user_groups.py:213
 msgid "An error occurred during deletion of user group"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:273
+#: kallithea/controllers/admin/user_groups.py:269
 msgid "Target group cannot be the same"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:279
+#: kallithea/controllers/admin/user_groups.py:275
 msgid "User group permissions updated"
 msgstr ""
 
+#: kallithea/controllers/admin/user_groups.py:384
+#: kallithea/controllers/admin/users.py:336
+msgid "Updated permissions"
+msgstr ""
+
 #: kallithea/controllers/admin/user_groups.py:388
-#: kallithea/controllers/admin/users.py:338
-msgid "Updated permissions"
-msgstr ""
-
-#: kallithea/controllers/admin/user_groups.py:392
-#: kallithea/controllers/admin/users.py:342
+#: kallithea/controllers/admin/users.py:340
 msgid "An error occurred during permissions saving"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:123
+#: kallithea/controllers/admin/users.py:121
 #, python-format
 msgid "Created user %s"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:138
+#: kallithea/controllers/admin/users.py:136
 #, python-format
 msgid "Error occurred during creation of user %s"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:162
+#: kallithea/controllers/admin/users.py:160
 msgid "User updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:190
+#: kallithea/controllers/admin/users.py:188
 msgid "Successfully deleted user"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:195
+#: kallithea/controllers/admin/users.py:193
 msgid "An error occurred during deletion of user"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:203
+#: kallithea/controllers/admin/users.py:201
 msgid "The default user cannot be edited"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:409
+#: kallithea/controllers/admin/users.py:407
 #, python-format
 msgid "Added IP address %s to user whitelist"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:415
+#: kallithea/controllers/admin/users.py:413
 msgid "An error occurred while adding IP address"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:427
+#: kallithea/controllers/admin/users.py:425
 msgid "Removed IP address from user whitelist"
 msgstr ""
 
-#: kallithea/lib/auth.py:684
+#: kallithea/lib/auth.py:634
 msgid "You need to be a registered user to perform this action"
 msgstr ""
 
-#: kallithea/lib/auth.py:712
+#: kallithea/lib/auth.py:662
 msgid "You need to be signed in to view this page"
 msgstr ""
 
@@ -1071,171 +1067,171 @@
 "Changeset was too big and was cut off, use diff menu to display this diff"
 msgstr ""
 
-#: kallithea/lib/diffs.py:224
+#: kallithea/lib/diffs.py:223
 msgid "No changes detected"
 msgstr ""
 
-#: kallithea/lib/helpers.py:653
+#: kallithea/lib/helpers.py:670
 #, python-format
 msgid "Deleted branch: %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:655
+#: kallithea/lib/helpers.py:672
 #, python-format
 msgid "Created tag: %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:666
+#: kallithea/lib/helpers.py:683
 #, fuzzy, python-format
 #| msgid "Set changeset status"
 msgid "Changeset %s not found"
 msgstr "Změny"
 
-#: kallithea/lib/helpers.py:715
+#: kallithea/lib/helpers.py:732
 #, python-format
 msgid "Show all combined changesets %s->%s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:721
+#: kallithea/lib/helpers.py:738
 msgid "Compare view"
 msgstr ""
 
-#: kallithea/lib/helpers.py:740
+#: kallithea/lib/helpers.py:757
 msgid "and"
 msgstr ""
 
-#: kallithea/lib/helpers.py:741
+#: kallithea/lib/helpers.py:758
 #, python-format
 msgid "%s more"
 msgstr ""
 
-#: kallithea/lib/helpers.py:742
+#: kallithea/lib/helpers.py:759
 #: kallithea/templates/changelog/changelog.html:43
 msgid "revisions"
 msgstr ""
 
-#: kallithea/lib/helpers.py:766
+#: kallithea/lib/helpers.py:783
 #, python-format
 msgid "Fork name %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:787
+#: kallithea/lib/helpers.py:804
 #, python-format
 msgid "Pull request %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:797
+#: kallithea/lib/helpers.py:814
 msgid "[deleted] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:799 kallithea/lib/helpers.py:811
+#: kallithea/lib/helpers.py:816 kallithea/lib/helpers.py:828
 msgid "[created] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:801
+#: kallithea/lib/helpers.py:818
 msgid "[created] repository as fork"
 msgstr ""
 
-#: kallithea/lib/helpers.py:803 kallithea/lib/helpers.py:813
+#: kallithea/lib/helpers.py:820 kallithea/lib/helpers.py:830
 msgid "[forked] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:805 kallithea/lib/helpers.py:815
+#: kallithea/lib/helpers.py:822 kallithea/lib/helpers.py:832
 msgid "[updated] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:807
+#: kallithea/lib/helpers.py:824
 msgid "[downloaded] archive from repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:809
+#: kallithea/lib/helpers.py:826
 msgid "[delete] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:817
+#: kallithea/lib/helpers.py:834
 msgid "[created] user"
 msgstr ""
 
-#: kallithea/lib/helpers.py:819
+#: kallithea/lib/helpers.py:836
 msgid "[updated] user"
 msgstr ""
 
-#: kallithea/lib/helpers.py:821
+#: kallithea/lib/helpers.py:838
 msgid "[created] user group"
 msgstr ""
 
-#: kallithea/lib/helpers.py:823
+#: kallithea/lib/helpers.py:840
 msgid "[updated] user group"
 msgstr ""
 
-#: kallithea/lib/helpers.py:825
+#: kallithea/lib/helpers.py:842
 msgid "[commented] on revision in repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:827
+#: kallithea/lib/helpers.py:844
 msgid "[commented] on pull request for"
 msgstr ""
 
-#: kallithea/lib/helpers.py:829
+#: kallithea/lib/helpers.py:846
 msgid "[closed] pull request for"
 msgstr ""
 
-#: kallithea/lib/helpers.py:831
+#: kallithea/lib/helpers.py:848
 msgid "[pushed] into"
 msgstr ""
 
-#: kallithea/lib/helpers.py:833
+#: kallithea/lib/helpers.py:850
 msgid "[committed via Kallithea] into repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:835
+#: kallithea/lib/helpers.py:852
 msgid "[pulled from remote] into repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:837
+#: kallithea/lib/helpers.py:854
 msgid "[pulled] from"
 msgstr ""
 
-#: kallithea/lib/helpers.py:839
+#: kallithea/lib/helpers.py:856
 msgid "[started following] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:841
+#: kallithea/lib/helpers.py:858
 msgid "[stopped following] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:961
+#: kallithea/lib/helpers.py:975
 #, python-format
 msgid " and %s more"
 msgstr ""
 
-#: kallithea/lib/helpers.py:965
+#: kallithea/lib/helpers.py:979
 #: kallithea/templates/compare/compare_diff.html:69
 #: kallithea/templates/pullrequests/pullrequest_show.html:297
 msgid "No files"
 msgstr ""
 
-#: kallithea/lib/helpers.py:990
+#: kallithea/lib/helpers.py:1004
 msgid "new file"
 msgstr ""
 
-#: kallithea/lib/helpers.py:993
+#: kallithea/lib/helpers.py:1007
 msgid "mod"
 msgstr ""
 
-#: kallithea/lib/helpers.py:996
+#: kallithea/lib/helpers.py:1010
 msgid "del"
 msgstr ""
 
-#: kallithea/lib/helpers.py:999
+#: kallithea/lib/helpers.py:1013
 msgid "rename"
 msgstr ""
 
-#: kallithea/lib/helpers.py:1004
+#: kallithea/lib/helpers.py:1018
 msgid "chmod"
 msgstr ""
 
-#: kallithea/lib/helpers.py:1297
+#: kallithea/lib/helpers.py:1323
 #, python-format
 msgid ""
 "%s repository is not mapped to db perhaps it was created or renamed from "
@@ -1243,34 +1239,36 @@
 "repositories"
 msgstr ""
 
-#: kallithea/lib/ssh.py:73
+#: kallithea/lib/ssh.py:75
 msgid "SSH key is missing"
 msgstr ""
 
-#: kallithea/lib/ssh.py:77
-msgid "Incorrect SSH key - it must have both a key type and a base64 part"
-msgstr ""
-
-#: kallithea/lib/ssh.py:81
+#: kallithea/lib/ssh.py:79
+msgid ""
+"Incorrect SSH key - it must have both a key type and a base64 part, like "
+"'ssh-rsa ASRNeaZu4FA...xlJp='"
+msgstr ""
+
+#: kallithea/lib/ssh.py:83
 msgid "Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'"
 msgstr ""
 
-#: kallithea/lib/ssh.py:84
+#: kallithea/lib/ssh.py:86
 #, python-format
 msgid "Incorrect SSH key - unexpected characters in base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:89
+#: kallithea/lib/ssh.py:91
 #, python-format
 msgid "Incorrect SSH key - failed to decode base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:92
+#: kallithea/lib/ssh.py:94
 #, python-format
 msgid "Incorrect SSH key - base64 part is not %r as claimed but %r"
 msgstr ""
 
-#: kallithea/lib/utils2.py:334
+#: kallithea/lib/utils2.py:253
 #, python-format
 msgid "%d year"
 msgid_plural "%d years"
@@ -1278,7 +1276,7 @@
 msgstr[1] ""
 msgstr[2] ""
 
-#: kallithea/lib/utils2.py:335
+#: kallithea/lib/utils2.py:254
 #, python-format
 msgid "%d month"
 msgid_plural "%d months"
@@ -1286,7 +1284,7 @@
 msgstr[1] ""
 msgstr[2] ""
 
-#: kallithea/lib/utils2.py:336
+#: kallithea/lib/utils2.py:255
 #, python-format
 msgid "%d day"
 msgid_plural "%d days"
@@ -1294,7 +1292,7 @@
 msgstr[1] ""
 msgstr[2] ""
 
-#: kallithea/lib/utils2.py:337
+#: kallithea/lib/utils2.py:256
 #, python-format
 msgid "%d hour"
 msgid_plural "%d hours"
@@ -1302,7 +1300,7 @@
 msgstr[1] ""
 msgstr[2] ""
 
-#: kallithea/lib/utils2.py:338
+#: kallithea/lib/utils2.py:257
 #, python-format
 msgid "%d minute"
 msgid_plural "%d minutes"
@@ -1310,7 +1308,7 @@
 msgstr[1] ""
 msgstr[2] ""
 
-#: kallithea/lib/utils2.py:339
+#: kallithea/lib/utils2.py:258
 #, python-format
 msgid "%d second"
 msgid_plural "%d seconds"
@@ -1318,27 +1316,27 @@
 msgstr[1] ""
 msgstr[2] ""
 
-#: kallithea/lib/utils2.py:355
+#: kallithea/lib/utils2.py:274
 #, python-format
 msgid "in %s"
 msgstr ""
 
-#: kallithea/lib/utils2.py:357
+#: kallithea/lib/utils2.py:276
 #, python-format
 msgid "%s ago"
 msgstr ""
 
-#: kallithea/lib/utils2.py:359
+#: kallithea/lib/utils2.py:278
 #, python-format
 msgid "in %s and %s"
 msgstr ""
 
-#: kallithea/lib/utils2.py:362
+#: kallithea/lib/utils2.py:281
 #, python-format
 msgid "%s and %s ago"
 msgstr ""
 
-#: kallithea/lib/utils2.py:365
+#: kallithea/lib/utils2.py:284
 msgid "just now"
 msgstr ""
 
@@ -1347,135 +1345,135 @@
 msgid "on line %s"
 msgstr ""
 
-#: kallithea/model/comment.py:221 kallithea/model/pull_request.py:114
+#: kallithea/model/comment.py:219 kallithea/model/pull_request.py:112
 msgid "[Mention]"
 msgstr ""
 
-#: kallithea/model/db.py:1496
+#: kallithea/model/db.py:1411
 msgid "top level"
 msgstr ""
 
-#: kallithea/model/db.py:1637
+#: kallithea/model/db.py:1542
 msgid "Kallithea Administrator"
 msgstr ""
 
-#: kallithea/model/db.py:1639
+#: kallithea/model/db.py:1544
 msgid "Default user has no access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1640
+#: kallithea/model/db.py:1545
 msgid "Default user has read access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1641
+#: kallithea/model/db.py:1546
 msgid "Default user has write access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1642
+#: kallithea/model/db.py:1547
 msgid "Default user has admin access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1644
+#: kallithea/model/db.py:1549
 msgid "Default user has no access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1645
+#: kallithea/model/db.py:1550
 msgid "Default user has read access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1646
+#: kallithea/model/db.py:1551
 msgid "Default user has write access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1647
+#: kallithea/model/db.py:1552
 msgid "Default user has admin access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1649
+#: kallithea/model/db.py:1554
 msgid "Default user has no access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1650
+#: kallithea/model/db.py:1555
 msgid "Default user has read access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1651
+#: kallithea/model/db.py:1556
 msgid "Default user has write access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1652
+#: kallithea/model/db.py:1557
 msgid "Default user has admin access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1654
+#: kallithea/model/db.py:1559
 msgid "Only admins can create repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1655
+#: kallithea/model/db.py:1560
 msgid "Non-admins can create repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1657
+#: kallithea/model/db.py:1562
 msgid "Only admins can create user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1658
+#: kallithea/model/db.py:1563
 msgid "Non-admins can create user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1660
+#: kallithea/model/db.py:1565
 msgid "Only admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1661
+#: kallithea/model/db.py:1566
 msgid "Non-admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1663
+#: kallithea/model/db.py:1568
 msgid ""
 "Repository creation enabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1664
+#: kallithea/model/db.py:1569
 msgid ""
 "Repository creation disabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1666
+#: kallithea/model/db.py:1571
 #, fuzzy
 msgid "Only admins can fork repositories"
 msgstr "Chyba při vytváření repozitáře %s"
 
-#: kallithea/model/db.py:1667
+#: kallithea/model/db.py:1572
 #, fuzzy
 msgid "Non-admins can fork repositories"
 msgstr "Chyba při vytváření repozitáře %s"
 
-#: kallithea/model/db.py:1669
+#: kallithea/model/db.py:1574
 msgid "Registration disabled"
 msgstr ""
 
-#: kallithea/model/db.py:1670
+#: kallithea/model/db.py:1575
 msgid "User registration with manual account activation"
 msgstr ""
 
-#: kallithea/model/db.py:1671
+#: kallithea/model/db.py:1576
 msgid "User registration with automatic account activation"
 msgstr ""
 
-#: kallithea/model/db.py:2206
+#: kallithea/model/db.py:1992
 msgid "Not reviewed"
 msgstr ""
 
-#: kallithea/model/db.py:2207
+#: kallithea/model/db.py:1993
 msgid "Under review"
 msgstr ""
 
-#: kallithea/model/db.py:2208
+#: kallithea/model/db.py:1994
 msgid "Not approved"
 msgstr ""
 
-#: kallithea/model/db.py:2209
+#: kallithea/model/db.py:1995
 msgid "Approved"
 msgstr ""
 
@@ -1501,315 +1499,315 @@
 msgid "Name must not contain only digits"
 msgstr ""
 
-#: kallithea/model/notification.py:164
+#: kallithea/model/notification.py:162
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s changeset %(short_id)s \"%(message_short)s\" on "
 "%(branch)s"
 msgstr ""
 
-#: kallithea/model/notification.py:167
+#: kallithea/model/notification.py:165
 #, python-format
 msgid "New user %(new_username)s registered"
 msgstr ""
 
-#: kallithea/model/notification.py:169
+#: kallithea/model/notification.py:167
 #, python-format
 msgid ""
 "[Review] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:170
+#: kallithea/model/notification.py:168
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:183
+#: kallithea/model/notification.py:188
 msgid "Closing"
 msgstr ""
 
-#: kallithea/model/pull_request.py:73
+#: kallithea/model/pull_request.py:72
 #, python-format
 msgid ""
 "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:209
+#: kallithea/model/pull_request.py:207
 msgid "Cannot create empty pull request"
 msgstr ""
 
-#: kallithea/model/pull_request.py:217
+#: kallithea/model/pull_request.py:215
 #, python-format
 msgid ""
 "Cannot create pull request - criss cross merge detected, please merge a "
 "later %s revision to %s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:245 kallithea/model/pull_request.py:380
+#: kallithea/model/pull_request.py:243 kallithea/model/pull_request.py:378
 msgid "You are not authorized to create the pull request"
 msgstr ""
 
-#: kallithea/model/pull_request.py:339
+#: kallithea/model/pull_request.py:337
 msgid "Missing changesets since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:346
+#: kallithea/model/pull_request.py:344
 #, python-format
 msgid "New changesets on %s %s since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:353
+#: kallithea/model/pull_request.py:351
 msgid "Ancestor didn't change - diff since previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:360
+#: kallithea/model/pull_request.py:358
 #, python-format
 msgid ""
 "This iteration is based on another %s revision and there is no simple "
 "diff."
 msgstr ""
 
-#: kallithea/model/pull_request.py:362
+#: kallithea/model/pull_request.py:360
 #, python-format
 msgid "No changes found on %s %s since previous iteration."
 msgstr ""
 
-#: kallithea/model/pull_request.py:388
+#: kallithea/model/pull_request.py:386
 #, python-format
 msgid "Closed, next iteration: %s ."
 msgstr ""
 
-#: kallithea/model/scm.py:668
+#: kallithea/model/scm.py:655
 msgid "latest tip"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:56
+#: kallithea/model/ssh_key.py:57
 #, python-format
 msgid "SSH key %r is invalid: %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:68
+#: kallithea/model/ssh_key.py:69
 #, python-format
 msgid "SSH key %s is already used by %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:89
+#: kallithea/model/ssh_key.py:88
 #, fuzzy, python-format
 #| msgid "Set changeset status"
-msgid "SSH key %r not found"
+msgid "SSH key with fingerprint %r found"
 msgstr "Změny"
 
-#: kallithea/model/user.py:186
+#: kallithea/model/user.py:180
 msgid "New user registration"
 msgstr ""
 
-#: kallithea/model/user.py:250
+#: kallithea/model/user.py:244
 msgid ""
 "You can't remove this user since it is crucial for the entire application"
 msgstr ""
 
-#: kallithea/model/user.py:255
+#: kallithea/model/user.py:249
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repositories and cannot be removed. Switch "
 "owners or remove those repositories: %s"
 msgstr ""
 
-#: kallithea/model/user.py:260
+#: kallithea/model/user.py:254
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repository groups and cannot be removed. Switch "
 "owners or remove those repository groups: %s"
 msgstr ""
 
-#: kallithea/model/user.py:267
+#: kallithea/model/user.py:261
 #, python-format
 msgid ""
 "User \"%s\" still owns %s user groups and cannot be removed. Switch "
 "owners or remove those user groups: %s"
 msgstr ""
 
-#: kallithea/model/user.py:361
+#: kallithea/model/user.py:355
 msgid "Password reset link"
 msgstr ""
 
-#: kallithea/model/user.py:408
+#: kallithea/model/user.py:402
 msgid "Password reset notification"
 msgstr ""
 
-#: kallithea/model/user.py:409
+#: kallithea/model/user.py:403
 #, python-format
 msgid ""
 "The password to your account %s has been changed using password reset "
 "form."
 msgstr ""
 
-#: kallithea/model/validators.py:52 kallithea/model/validators.py:53
+#: kallithea/model/validators.py:53 kallithea/model/validators.py:54
 msgid "Value cannot be an empty list"
 msgstr ""
 
-#: kallithea/model/validators.py:72
+#: kallithea/model/validators.py:73
 #, python-format
 msgid "Username \"%(username)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:74
+#: kallithea/model/validators.py:75
 #, python-format
 msgid "Username \"%(username)s\" cannot be used"
 msgstr ""
 
-#: kallithea/model/validators.py:76
+#: kallithea/model/validators.py:77
 msgid ""
 "Username may only contain alphanumeric characters underscores, periods or "
 "dashes and must begin with an alphanumeric character or underscore"
 msgstr ""
 
-#: kallithea/model/validators.py:103
+#: kallithea/model/validators.py:104
 msgid "The input is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:110
+#: kallithea/model/validators.py:111
 #, python-format
 msgid "Username %(username)s is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:131
-msgid "Invalid user group name"
-msgstr ""
-
 #: kallithea/model/validators.py:132
+msgid "Invalid user group name"
+msgstr ""
+
+#: kallithea/model/validators.py:133
 #, python-format
 msgid "User group \"%(usergroup)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:134
+#: kallithea/model/validators.py:135
 msgid ""
 "user group name may only contain alphanumeric characters underscores, "
 "periods or dashes and must begin with alphanumeric character"
 msgstr ""
 
-#: kallithea/model/validators.py:174
-msgid "Cannot assign this group as parent"
-msgstr ""
-
 #: kallithea/model/validators.py:175
+msgid "Cannot assign this group as parent"
+msgstr ""
+
+#: kallithea/model/validators.py:176
 #, python-format
 msgid "Group \"%(group_name)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:177
+#: kallithea/model/validators.py:178
 #, python-format
 msgid "Repository with name \"%(group_name)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:233
+#: kallithea/model/validators.py:230
 msgid "Invalid characters (non-ascii) in password"
 msgstr ""
 
-#: kallithea/model/validators.py:248
+#: kallithea/model/validators.py:245
 msgid "Invalid old password"
 msgstr ""
 
-#: kallithea/model/validators.py:264
+#: kallithea/model/validators.py:261
 msgid "Passwords do not match"
 msgstr ""
 
-#: kallithea/model/validators.py:279
+#: kallithea/model/validators.py:276
 msgid "Invalid username or password"
 msgstr ""
 
+#: kallithea/model/validators.py:310
+#, python-format
+msgid "Repository name %(repo)s is not allowed"
+msgstr ""
+
+#: kallithea/model/validators.py:312
+#, python-format
+msgid "Repository named %(repo)s already exists"
+msgstr ""
+
 #: kallithea/model/validators.py:313
 #, python-format
-msgid "Repository name %(repo)s is not allowed"
+msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
 msgstr ""
 
 #: kallithea/model/validators.py:315
 #, python-format
-msgid "Repository named %(repo)s already exists"
-msgstr ""
-
-#: kallithea/model/validators.py:316
-#, python-format
-msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
-msgstr ""
-
-#: kallithea/model/validators.py:318
-#, python-format
 msgid "Repository group with name \"%(repo)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:404
+#: kallithea/model/validators.py:401
 #, fuzzy
 msgid "Invalid repository URL"
 msgstr "Prázdný repozitář"
 
-#: kallithea/model/validators.py:405
+#: kallithea/model/validators.py:402
 msgid ""
 "Invalid repository URL. It must be a valid http, https, ssh, svn+http or "
 "svn+https URL"
 msgstr ""
 
-#: kallithea/model/validators.py:430
+#: kallithea/model/validators.py:427
 msgid "Fork has to be the same type as parent"
 msgstr ""
 
-#: kallithea/model/validators.py:445
+#: kallithea/model/validators.py:442
 msgid "You don't have permissions to create repository in this group"
 msgstr ""
 
-#: kallithea/model/validators.py:447
+#: kallithea/model/validators.py:444
 msgid "no permission to create repository in root location"
 msgstr ""
 
-#: kallithea/model/validators.py:497
+#: kallithea/model/validators.py:494
 msgid "You don't have permissions to create a group in this location"
 msgstr ""
 
-#: kallithea/model/validators.py:537
+#: kallithea/model/validators.py:534
 msgid "This username or user group name is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:630
+#: kallithea/model/validators.py:627
 msgid "This is not a valid path"
 msgstr ""
 
-#: kallithea/model/validators.py:647
+#: kallithea/model/validators.py:644
 msgid "This email address is already in use"
 msgstr ""
 
-#: kallithea/model/validators.py:667
+#: kallithea/model/validators.py:664
 #, python-format
 msgid "Email address \"%(email)s\" not found"
 msgstr ""
 
-#: kallithea/model/validators.py:704
+#: kallithea/model/validators.py:701
 msgid ""
 "The LDAP Login attribute of the CN must be specified - this is the name "
 "of the attribute that is equivalent to \"username\""
 msgstr ""
 
-#: kallithea/model/validators.py:716
+#: kallithea/model/validators.py:713
 msgid "Please enter a valid IPv4 or IPv6 address"
 msgstr ""
 
-#: kallithea/model/validators.py:717
+#: kallithea/model/validators.py:714
 #, python-format
 msgid ""
 "The network size (bits) must be within the range of 0-32 (not %(bits)r)"
 msgstr ""
 
-#: kallithea/model/validators.py:750
+#: kallithea/model/validators.py:747
 msgid "Key name can only consist of letters, underscore, dash or numbers"
 msgstr ""
 
-#: kallithea/model/validators.py:764
+#: kallithea/model/validators.py:761
 msgid "Filename cannot be inside a directory"
 msgstr ""
 
-#: kallithea/model/validators.py:780
+#: kallithea/model/validators.py:777
 #, python-format
 msgid "Plugins %(loaded)s and %(next_to_load)s both export the same name"
 msgstr ""
@@ -1869,7 +1867,7 @@
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:60
 #: kallithea/templates/email_templates/pull_request.html:37
 #: kallithea/templates/forks/fork.html:34
-#: kallithea/templates/index_base.html:58
+#: kallithea/templates/index_base.html:59
 #: kallithea/templates/pullrequests/pullrequest.html:33
 #: kallithea/templates/pullrequests/pullrequest_show.html:38
 #: kallithea/templates/pullrequests/pullrequest_show.html:59
@@ -1877,14 +1875,14 @@
 msgid "Description"
 msgstr ""
 
-#: kallithea/templates/index_base.html:60
+#: kallithea/templates/index_base.html:61
 msgid "Last Change"
 msgstr ""
 
 #: kallithea/templates/admin/my_account/my_account_repos.html:15
 #: kallithea/templates/admin/my_account/my_account_watched.html:15
 #: kallithea/templates/admin/repos/repos.html:41
-#: kallithea/templates/index_base.html:62
+#: kallithea/templates/index_base.html:63
 msgid "Tip"
 msgstr ""
 
@@ -1894,7 +1892,7 @@
 #: kallithea/templates/admin/repos/repos.html:42
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:8
 #: kallithea/templates/admin/user_groups/user_groups.html:42
-#: kallithea/templates/index_base.html:63
+#: kallithea/templates/index_base.html:64
 #: kallithea/templates/pullrequests/pullrequest_data.html:16
 #: kallithea/templates/pullrequests/pullrequest_show.html:124
 #: kallithea/templates/pullrequests/pullrequest_show.html:219
@@ -1918,7 +1916,7 @@
 #: kallithea/templates/admin/users/user_edit_profile.html:18
 #: kallithea/templates/admin/users/users.html:37
 #: kallithea/templates/base/base.html:364
-#: kallithea/templates/email_templates/registration.html:11
+#: kallithea/templates/email_templates/registration.html:12
 #: kallithea/templates/login.html:28 kallithea/templates/register.html:31
 msgid "Username"
 msgstr ""
@@ -2042,7 +2040,7 @@
 #: kallithea/templates/admin/settings/settings.html:31
 #: kallithea/templates/admin/users/user_add.html:62
 #: kallithea/templates/admin/users/user_edit_profile.html:25
-#: kallithea/templates/email_templates/registration.html:33
+#: kallithea/templates/email_templates/registration.html:34
 #: kallithea/templates/register.html:66
 msgid "Email"
 msgstr ""
@@ -2291,7 +2289,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/gists/index.html:51
-#: kallithea/templates/data_table/_dt_elements.html:78
+#: kallithea/templates/data_table/_dt_elements.html:84
 msgid "Created"
 msgstr ""
 
@@ -2375,13 +2373,13 @@
 #: kallithea/templates/admin/users/user_edit_ips.html:21
 #: kallithea/templates/changeset/changeset_file_comment.html:30
 #: kallithea/templates/changeset/changeset_file_comment.html:121
-#: kallithea/templates/data_table/_dt_elements.html:69
-#: kallithea/templates/data_table/_dt_elements.html:89
-#: kallithea/templates/data_table/_dt_elements.html:91
-#: kallithea/templates/data_table/_dt_elements.html:101
-#: kallithea/templates/data_table/_dt_elements.html:103
-#: kallithea/templates/data_table/_dt_elements.html:120
-#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:75
+#: kallithea/templates/data_table/_dt_elements.html:95
+#: kallithea/templates/data_table/_dt_elements.html:97
+#: kallithea/templates/data_table/_dt_elements.html:107
+#: kallithea/templates/data_table/_dt_elements.html:109
+#: kallithea/templates/data_table/_dt_elements.html:126
+#: kallithea/templates/data_table/_dt_elements.html:128
 #: kallithea/templates/files/files_source.html:35
 #: kallithea/templates/files/files_source.html:38
 #: kallithea/templates/files/files_source.html:41
@@ -2397,14 +2395,14 @@
 #: kallithea/templates/base/perms_summary.html:44
 #: kallithea/templates/base/perms_summary.html:81
 #: kallithea/templates/base/perms_summary.html:83
-#: kallithea/templates/data_table/_dt_elements.html:63
-#: kallithea/templates/data_table/_dt_elements.html:64
-#: kallithea/templates/data_table/_dt_elements.html:85
-#: kallithea/templates/data_table/_dt_elements.html:86
-#: kallithea/templates/data_table/_dt_elements.html:97
-#: kallithea/templates/data_table/_dt_elements.html:98
-#: kallithea/templates/data_table/_dt_elements.html:116
-#: kallithea/templates/data_table/_dt_elements.html:117
+#: kallithea/templates/data_table/_dt_elements.html:69
+#: kallithea/templates/data_table/_dt_elements.html:70
+#: kallithea/templates/data_table/_dt_elements.html:91
+#: kallithea/templates/data_table/_dt_elements.html:92
+#: kallithea/templates/data_table/_dt_elements.html:103
+#: kallithea/templates/data_table/_dt_elements.html:104
+#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:123
 #: kallithea/templates/files/diff_2way.html:56
 #: kallithea/templates/files/files_source.html:37
 #: kallithea/templates/files/files_source.html:40
@@ -2710,7 +2708,7 @@
 #: kallithea/templates/admin/permissions/permissions_globals.html:27
 #: kallithea/templates/admin/repos/repo_add_base.html:28
 #: kallithea/templates/admin/repos/repo_edit_settings.html:33
-#: kallithea/templates/data_table/_dt_elements.html:134
+#: kallithea/templates/data_table/_dt_elements.html:140
 #: kallithea/templates/forks/fork.html:42
 msgid "Repository group"
 msgstr ""
@@ -2731,7 +2729,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:40
-#: kallithea/templates/data_table/_dt_elements.html:141
+#: kallithea/templates/data_table/_dt_elements.html:147
 msgid "User group"
 msgstr ""
 
@@ -2904,7 +2902,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:121
+#: kallithea/templates/data_table/_dt_elements.html:127
 #, python-format
 msgid "Confirm to delete this group: %s with %s repository"
 msgid_plural "Confirm to delete this group: %s with %s repositories"
@@ -3072,14 +3070,10 @@
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit.html:37
-msgid "Caches"
+msgid "Remote"
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit.html:40
-msgid "Remote"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit.html:43
 #: kallithea/templates/summary/statistics.html:8
 #: kallithea/templates/summary/summary.html:169
 #: kallithea/templates/summary/summary.html:170
@@ -3117,7 +3111,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:46
-#: kallithea/templates/data_table/_dt_elements.html:68
+#: kallithea/templates/data_table/_dt_elements.html:74
 #, python-format
 msgid "Confirm to delete this repository: %s"
 msgstr ""
@@ -3150,43 +3144,14 @@
 "it or restore it."
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:4
-msgid "Invalidate Repository Cache"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:6
-msgid ""
-"Manually invalidate cache for this repository. On first access, the "
-"repository will be cached again."
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:9
-msgid "List of Cached Values"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:12
-msgid "Prefix"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:13
+#: kallithea/templates/admin/repos/repo_edit_fields.html:6
+msgid "Label"
+msgstr ""
+
 #: kallithea/templates/admin/repos/repo_edit_fields.html:7
 msgid "Key"
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:14
-#: kallithea/templates/admin/user_groups/user_group_add.html:40
-#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
-#: kallithea/templates/admin/user_groups/user_groups.html:41
-#: kallithea/templates/admin/users/user_add.html:69
-#: kallithea/templates/admin/users/user_edit_profile.html:74
-#: kallithea/templates/admin/users/users.html:42
-msgid "Active"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_fields.html:6
-msgid "Label"
-msgstr ""
-
 #: kallithea/templates/admin/repos/repo_edit_fields.html:20
 #, python-format
 msgid "Confirm to delete this field: %s"
@@ -3714,6 +3679,15 @@
 msgid "Short, optional description for this user group."
 msgstr ""
 
+#: kallithea/templates/admin/user_groups/user_group_add.html:40
+#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
+#: kallithea/templates/admin/user_groups/user_groups.html:41
+#: kallithea/templates/admin/users/user_add.html:69
+#: kallithea/templates/admin/users/user_edit_profile.html:74
+#: kallithea/templates/admin/users/users.html:42
+msgid "Active"
+msgstr ""
+
 #: kallithea/templates/admin/user_groups/user_group_edit.html:5
 #, python-format
 msgid "%s user group settings"
@@ -3735,7 +3709,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:19
-#: kallithea/templates/data_table/_dt_elements.html:102
+#: kallithea/templates/data_table/_dt_elements.html:108
 #, python-format
 msgid "Confirm to delete this user group: %s"
 msgstr ""
@@ -3809,7 +3783,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:90
+#: kallithea/templates/data_table/_dt_elements.html:96
 #, python-format
 msgid "Confirm to delete this user: %s"
 msgstr ""
@@ -3909,10 +3883,12 @@
 msgstr ""
 
 #: kallithea/templates/base/base.html:167
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Follow"
 msgstr ""
 
 #: kallithea/templates/base/base.html:168
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Unfollow"
 msgstr ""
 
@@ -4320,23 +4296,23 @@
 msgid "Merge"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:96
+#: kallithea/templates/changeset/changeset.html:95
 msgid "Grafted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:102
+#: kallithea/templates/changeset/changeset.html:100
 msgid "Transplanted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:108
+#: kallithea/templates/changeset/changeset.html:106
 msgid "Replaced by:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:122
+#: kallithea/templates/changeset/changeset.html:120
 msgid "Preceded by:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:139
+#: kallithea/templates/changeset/changeset.html:137
 #: kallithea/templates/compare/compare_diff.html:59
 #: kallithea/templates/pullrequests/pullrequest_show.html:290
 #, python-format
@@ -4346,7 +4322,7 @@
 msgstr[1] ""
 msgstr[2] ""
 
-#: kallithea/templates/changeset/changeset.html:141
+#: kallithea/templates/changeset/changeset.html:139
 #: kallithea/templates/compare/compare_diff.html:61
 #: kallithea/templates/pullrequests/pullrequest_show.html:292
 #, python-format
@@ -4356,8 +4332,8 @@
 msgstr[1] ""
 msgstr[2] ""
 
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
 #: kallithea/templates/pullrequests/pullrequest_show.html:333
@@ -4598,23 +4574,23 @@
 msgid "Repository creation in progress..."
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:42
+#: kallithea/templates/data_table/_dt_elements.html:48
 msgid "No changesets yet"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:48
-#: kallithea/templates/data_table/_dt_elements.html:50
+#: kallithea/templates/data_table/_dt_elements.html:54
+#: kallithea/templates/data_table/_dt_elements.html:56
 #, python-format
 msgid "Subscribe to %s rss feed"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:56
-#: kallithea/templates/data_table/_dt_elements.html:58
+#: kallithea/templates/data_table/_dt_elements.html:62
+#: kallithea/templates/data_table/_dt_elements.html:64
 #, python-format
 msgid "Subscribe to %s atom feed"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:76
+#: kallithea/templates/data_table/_dt_elements.html:82
 msgid "Creating"
 msgstr ""
 
@@ -4651,6 +4627,11 @@
 msgid "by"
 msgstr ""
 
+#: kallithea/templates/email_templates/changeset_comment.html:36
+#: kallithea/templates/email_templates/pull_request_comment.html:43
+msgid "View Comment"
+msgstr ""
+
 #: kallithea/templates/email_templates/comment.html:27
 #, fuzzy
 #| msgid "Status change"
@@ -4663,32 +4644,40 @@
 msgid "The pull request has been closed."
 msgstr "Repozitář byl uzamčen"
 
-#: kallithea/templates/email_templates/password_reset.html:9
+#: kallithea/templates/email_templates/default.html:4
+msgid "Message"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:4
+msgid "Password Reset Request"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:10
 #, python-format
 msgid "Hello %s"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:16
+#: kallithea/templates/email_templates/password_reset.html:17
 msgid "We have received a request to reset the password for your account."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:25
+#: kallithea/templates/email_templates/password_reset.html:26
 msgid ""
 "This account is however managed outside this system and the password "
 "cannot be changed here."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:28
+#: kallithea/templates/email_templates/password_reset.html:29
 msgid "To set a new password, click the following link"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:33
+#: kallithea/templates/email_templates/password_reset.html:34
 msgid ""
 "Should you not be able to use the link above, please type the following "
 "code into the password reset form"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:44
+#: kallithea/templates/email_templates/password_reset.html:45
 msgid ""
 "If it weren't you who requested the password reset, just disregard this "
 "message."
@@ -4721,6 +4710,12 @@
 msgid "to"
 msgstr ""
 
+#: kallithea/templates/email_templates/pull_request.html:85
+#, fuzzy
+#| msgid "on pull request"
+msgid "View Pull Request"
+msgstr "Změna stavu-> %s"
+
 #: kallithea/templates/email_templates/pull_request_comment.html:4
 #, python-format
 msgid "Mention in Comment on Pull Request %s \"%s\""
@@ -4737,10 +4732,18 @@
 msgid "Comment on Pull Request %s \"%s\""
 msgstr "Změna stavu-> %s"
 
-#: kallithea/templates/email_templates/registration.html:22
+#: kallithea/templates/email_templates/registration.html:5
+msgid "New User Registration"
+msgstr ""
+
+#: kallithea/templates/email_templates/registration.html:23
 msgid "Full Name"
 msgstr ""
 
+#: kallithea/templates/email_templates/registration.html:42
+msgid "View User Profile"
+msgstr ""
+
 #: kallithea/templates/files/diff_2way.html:15
 #, python-format
 msgid "%s File side-by-side diff"
@@ -5344,45 +5347,45 @@
 msgid "Stats gathered: "
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:87
-#: kallithea/templates/summary/summary.html:371
+#: kallithea/templates/summary/statistics.html:85
+#: kallithea/templates/summary/summary.html:369
 msgid "files"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:111
-#: kallithea/templates/summary/summary.html:401
+#: kallithea/templates/summary/statistics.html:109
+#: kallithea/templates/summary/summary.html:399
 msgid "Show more"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:405
+#: kallithea/templates/summary/statistics.html:395
 msgid "commits"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:406
+#: kallithea/templates/summary/statistics.html:396
 msgid "files added"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:407
+#: kallithea/templates/summary/statistics.html:397
 msgid "files changed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:408
+#: kallithea/templates/summary/statistics.html:398
 msgid "files removed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:410
+#: kallithea/templates/summary/statistics.html:400
 msgid "commit"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:411
+#: kallithea/templates/summary/statistics.html:401
 msgid "file added"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:412
+#: kallithea/templates/summary/statistics.html:402
 msgid "file changed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:413
+#: kallithea/templates/summary/statistics.html:403
 msgid "file removed"
 msgstr ""
 
--- a/kallithea/i18n/da/LC_MESSAGES/kallithea.po	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/i18n/da/LC_MESSAGES/kallithea.po	Mon May 04 19:24:04 2020 +0200
@@ -4,7 +4,7 @@
 msgstr ""
 "Project-Id-Version: Kallithea 0.3.99\n"
 "Report-Msgid-Bugs-To: translations@kallithea-scm.org\n"
-"POT-Creation-Date: 2020-01-05 04:10+0100\n"
+"POT-Creation-Date: 2020-05-04 19:12+0200\n"
 "PO-Revision-Date: 2019-03-14 01:03+0000\n"
 "Last-Translator: Allan Nordhøy <epost@anotheragency.no>\n"
 "Language-Team: Danish <https://hosted.weblate.org/projects/kallithea/"
@@ -17,14 +17,14 @@
 "X-Generator: Weblate 3.5.1\n"
 
 #: kallithea/controllers/changelog.py:67
-#: kallithea/controllers/pullrequests.py:250 kallithea/lib/base.py:602
+#: kallithea/controllers/pullrequests.py:247 kallithea/lib/base.py:602
 msgid "There are no changesets yet"
 msgstr "Der er ingen changesets endnu"
 
 #: kallithea/controllers/admin/permissions.py:64
 #: kallithea/controllers/admin/permissions.py:68
 #: kallithea/controllers/admin/permissions.py:72
-#: kallithea/controllers/changelog.py:137
+#: kallithea/controllers/changelog.py:136
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:7
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:88
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:7
@@ -33,38 +33,38 @@
 msgid "None"
 msgstr "Ingen"
 
-#: kallithea/controllers/changelog.py:140 kallithea/controllers/files.py:189
+#: kallithea/controllers/changelog.py:139 kallithea/controllers/files.py:189
 msgid "(closed)"
 msgstr "(lukket)"
 
-#: kallithea/controllers/changeset.py:81
+#: kallithea/controllers/changeset.py:82
 msgid "Show whitespace"
 msgstr "Vis mellemrum"
 
-#: kallithea/controllers/changeset.py:88
-#: kallithea/controllers/changeset.py:95
+#: kallithea/controllers/changeset.py:89
+#: kallithea/controllers/changeset.py:96
 #: kallithea/templates/files/diff_2way.html:55
 msgid "Ignore whitespace"
 msgstr "Ignorer mellemrum"
 
-#: kallithea/controllers/changeset.py:161
+#: kallithea/controllers/changeset.py:162
 #, python-format
 msgid "Increase diff context to %(num)s lines"
 msgstr "Øg diff konteksten med %(num)s linjer"
 
-#: kallithea/controllers/changeset.py:201
+#: kallithea/controllers/changeset.py:202
 #, fuzzy
 #| msgid "No permission to change pull request status"
 msgid "No permission to change status"
 msgstr "Ingen tilladelse til ændring af status for pull-forespørgsel"
 
-#: kallithea/controllers/changeset.py:212
+#: kallithea/controllers/changeset.py:213
 #, python-format
 msgid "Successfully deleted pull request %s"
 msgstr "Pull-forespørgsel %s slettet successfuldt"
 
 #: kallithea/controllers/changeset.py:319 kallithea/controllers/files.py:89
-#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:700
+#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:697
 msgid "Such revision does not exist for this repository"
 msgstr "En sådan revision findes ikke for dette repository"
 
@@ -77,50 +77,50 @@
 msgid "Cannot compare repositories of different types"
 msgstr "Kan ikke sammenligne repositories af forskellige typer"
 
-#: kallithea/controllers/compare.py:246
+#: kallithea/controllers/compare.py:247
 msgid "Cannot show empty diff"
 msgstr "Kan ikke vise en tom diff"
 
-#: kallithea/controllers/compare.py:248
+#: kallithea/controllers/compare.py:249
 msgid "No ancestor found for merge diff"
 msgstr "Ingen forfader fundet for merge diff"
 
-#: kallithea/controllers/compare.py:252
+#: kallithea/controllers/compare.py:253
 msgid "Multiple merge ancestors found for merge compare"
 msgstr "Flere merge forfædre fundet for merge sammenligning"
 
-#: kallithea/controllers/compare.py:268
+#: kallithea/controllers/compare.py:269
 msgid "Cannot compare repositories without using common ancestor"
 msgstr "Kan ikke sammenligne repositories uden en fælles forfader"
 
-#: kallithea/controllers/error.py:71
+#: kallithea/controllers/error.py:67
 msgid "No response"
 msgstr "Intet svar"
 
-#: kallithea/controllers/error.py:72
+#: kallithea/controllers/error.py:68
 msgid "Unknown error"
 msgstr "Ukendt fejl"
 
-#: kallithea/controllers/error.py:85
+#: kallithea/controllers/error.py:81
 msgid ""
 "The request could not be understood by the server due to malformed syntax."
 msgstr ""
 "Forespørgslen kunne ikke forstås af serveren på grund af fejlformet "
 "syntaks."
 
-#: kallithea/controllers/error.py:88
+#: kallithea/controllers/error.py:84
 msgid "Unauthorized access to resource"
 msgstr "Uautoriseret adgang til ressource"
 
-#: kallithea/controllers/error.py:90
+#: kallithea/controllers/error.py:86
 msgid "You don't have permission to view this page"
 msgstr "Du har ikke tilladelse til at se denne side"
 
-#: kallithea/controllers/error.py:92
+#: kallithea/controllers/error.py:88
 msgid "The resource could not be found"
 msgstr "Kunne ikke finde ressourcen"
 
-#: kallithea/controllers/error.py:94
+#: kallithea/controllers/error.py:90
 msgid ""
 "The server encountered an unexpected condition which prevented it from "
 "fulfilling the request."
@@ -128,14 +128,14 @@
 "Serveren stødte på en uventet tilstand, som forhindrede den i at opfylde "
 "anmodningen."
 
-#: kallithea/controllers/feed.py:63
+#: kallithea/controllers/feed.py:59
 #, python-format
 msgid "%s committed on %s"
 msgstr "%s committed den %s"
 
-#: kallithea/controllers/feed.py:88
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/controllers/feed.py:84
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/compare/compare_diff.html:95
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
@@ -143,12 +143,12 @@
 msgid "Changeset was too big and was cut off..."
 msgstr "Changesettet var for stor og blev afskåret..."
 
-#: kallithea/controllers/feed.py:111 kallithea/controllers/feed.py:140
+#: kallithea/controllers/feed.py:107
 #, python-format
 msgid "%s %s feed"
 msgstr "Feed for %s %s"
 
-#: kallithea/controllers/feed.py:113 kallithea/controllers/feed.py:142
+#: kallithea/controllers/feed.py:109
 #, python-format
 msgid "Changes on %s repository"
 msgstr "Ændringer på repository %s"
@@ -168,78 +168,78 @@
 msgid "%s at %s"
 msgstr "%s fra %s"
 
-#: kallithea/controllers/files.py:296
+#: kallithea/controllers/files.py:295
 msgid "You can only delete files with revision being a valid branch"
 msgstr "Du kan kun slette filer, hvor revisionen er en gyldig branch"
 
-#: kallithea/controllers/files.py:307
+#: kallithea/controllers/files.py:306
 #, python-format
 msgid "Deleted file %s via Kallithea"
 msgstr "Slettet fil %s via Kallithea"
 
-#: kallithea/controllers/files.py:331
+#: kallithea/controllers/files.py:330
 #, python-format
 msgid "Successfully deleted file %s"
 msgstr "Successfuldt slettet filen %s"
 
-#: kallithea/controllers/files.py:335 kallithea/controllers/files.py:394
-#: kallithea/controllers/files.py:469
+#: kallithea/controllers/files.py:334 kallithea/controllers/files.py:392
+#: kallithea/controllers/files.py:467
 msgid "Error occurred during commit"
 msgstr "Fejl opstået under commit"
 
-#: kallithea/controllers/files.py:350
+#: kallithea/controllers/files.py:349
 msgid "You can only edit files with revision being a valid branch"
 msgstr "Du kan kun redigere filer, hvor revisionen er en gyldig branch"
 
-#: kallithea/controllers/files.py:364
+#: kallithea/controllers/files.py:363
 #, python-format
 msgid "Edited file %s via Kallithea"
 msgstr "Redigeret fil %s via Kallithea"
 
-#: kallithea/controllers/files.py:380
+#: kallithea/controllers/files.py:378
 msgid "No changes"
 msgstr "Ingen ændringer"
 
-#: kallithea/controllers/files.py:390 kallithea/controllers/files.py:458
+#: kallithea/controllers/files.py:388 kallithea/controllers/files.py:456
 #, python-format
 msgid "Successfully committed to %s"
 msgstr "Successfuldt committed til %s"
 
-#: kallithea/controllers/files.py:409
+#: kallithea/controllers/files.py:407
 msgid "Added file via Kallithea"
 msgstr "Tilføjet fil via Kallithea"
 
-#: kallithea/controllers/files.py:430
+#: kallithea/controllers/files.py:428
 msgid "No content"
 msgstr "Intet indhold"
 
-#: kallithea/controllers/files.py:434
+#: kallithea/controllers/files.py:432
 msgid "No filename"
 msgstr "Intet filnavn"
 
-#: kallithea/controllers/files.py:461
+#: kallithea/controllers/files.py:459
 msgid "Location must be relative path and must not contain .. in path"
 msgstr ""
 "Placeringen skal være en relativ sti og må ikke indeholde .. i stien"
 
-#: kallithea/controllers/files.py:493
+#: kallithea/controllers/files.py:491
 msgid "Downloads disabled"
 msgstr "Downloads er deaktiveret"
 
-#: kallithea/controllers/files.py:504
+#: kallithea/controllers/files.py:502
 #, python-format
 msgid "Unknown revision %s"
 msgstr "Ukendt revision %s"
 
-#: kallithea/controllers/files.py:506
+#: kallithea/controllers/files.py:504
 msgid "Empty repository"
 msgstr "Tomt repository"
 
-#: kallithea/controllers/files.py:508
+#: kallithea/controllers/files.py:506
 msgid "Unknown archive type"
 msgstr "Ukendt arkivtype"
 
-#: kallithea/controllers/files.py:729
+#: kallithea/controllers/files.py:726
 #: kallithea/templates/changeset/changeset_range.html:9
 #: kallithea/templates/email_templates/pull_request.html:64
 #: kallithea/templates/pullrequests/pullrequest.html:84
@@ -247,27 +247,27 @@
 msgid "Changesets"
 msgstr "Changesets"
 
-#: kallithea/controllers/files.py:730
-#: kallithea/controllers/pullrequests.py:182 kallithea/model/scm.py:676
+#: kallithea/controllers/files.py:727
+#: kallithea/controllers/pullrequests.py:174 kallithea/model/scm.py:663
 #, fuzzy
 msgid "Branches"
 msgstr "Branches"
 
-#: kallithea/controllers/files.py:731
-#: kallithea/controllers/pullrequests.py:183 kallithea/model/scm.py:687
+#: kallithea/controllers/files.py:728
+#: kallithea/controllers/pullrequests.py:175 kallithea/model/scm.py:674
 msgid "Tags"
 msgstr "Tags"
 
-#: kallithea/controllers/forks.py:174
+#: kallithea/controllers/forks.py:175
 #, python-format
 msgid "An error occurred during repository forking %s"
 msgstr "Der opstod en fejl under repository forking %s"
 
-#: kallithea/controllers/home.py:79
+#: kallithea/controllers/home.py:77
 msgid "Groups"
 msgstr "Grupper"
 
-#: kallithea/controllers/home.py:89
+#: kallithea/controllers/home.py:87
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:90
 #: kallithea/templates/admin/repos/repo_add.html:12
 #: kallithea/templates/admin/repos/repo_add.html:16
@@ -280,7 +280,7 @@
 msgid "Repositories"
 msgstr "Repositories"
 
-#: kallithea/controllers/home.py:122
+#: kallithea/controllers/home.py:119
 #: kallithea/templates/files/files_add.html:32
 #: kallithea/templates/files/files_delete.html:23
 #: kallithea/templates/files/files_edit.html:32
@@ -288,41 +288,45 @@
 msgid "Branch"
 msgstr "Branch"
 
-#: kallithea/controllers/home.py:128
+#: kallithea/controllers/home.py:125
 msgid "Closed Branches"
 msgstr "Lukkede Branches"
 
-#: kallithea/controllers/home.py:134
+#: kallithea/controllers/home.py:131
 msgid "Tag"
 msgstr "Tag"
 
-#: kallithea/controllers/home.py:140
+#: kallithea/controllers/home.py:137
 msgid "Bookmark"
 msgstr "Bogmærke"
 
-#: kallithea/controllers/journal.py:112 kallithea/controllers/journal.py:154
+#: kallithea/controllers/journal.py:146 kallithea/controllers/journal.py:157
 #: kallithea/templates/journal/public_journal.html:4
 #: kallithea/templates/journal/public_journal.html:18
 msgid "Public Journal"
 msgstr "Offentlig journal"
 
-#: kallithea/controllers/journal.py:116 kallithea/controllers/journal.py:158
+#: kallithea/controllers/journal.py:150 kallithea/controllers/journal.py:161
 #: kallithea/templates/base/base.html:290
 #: kallithea/templates/journal/journal.html:5
 #: kallithea/templates/journal/journal.html:13
 msgid "Journal"
 msgstr "Journal"
 
-#: kallithea/controllers/login.py:140 kallithea/controllers/login.py:185
+#: kallithea/controllers/login.py:109
+msgid "Authentication failed."
+msgstr ""
+
+#: kallithea/controllers/login.py:142 kallithea/controllers/login.py:187
 msgid "Bad captcha"
 msgstr "Dårlig captcha"
 
-#: kallithea/controllers/login.py:146
+#: kallithea/controllers/login.py:148
 #, python-format
 msgid "You have successfully registered with %s"
 msgstr "Du har succesfuldt registreret med %s"
 
-#: kallithea/controllers/login.py:190
+#: kallithea/controllers/login.py:192
 msgid "A password reset confirmation code has been sent"
 msgstr "En bekræftelseskode til ændring af adgangskode er sendt"
 
@@ -335,117 +339,117 @@
 msgid "Successfully updated password"
 msgstr "Successfuld ændring af adgangskode"
 
-#: kallithea/controllers/pullrequests.py:68
+#: kallithea/controllers/pullrequests.py:67
 #, python-format
 msgid "Invalid reviewer \"%s\" specified"
 msgstr "Ugyldig reviewer \"%s\" angivet"
 
-#: kallithea/controllers/pullrequests.py:131
+#: kallithea/controllers/pullrequests.py:123
 #, python-format
 msgid "%s (closed)"
 msgstr "%s (lukket)"
 
-#: kallithea/controllers/pullrequests.py:158
+#: kallithea/controllers/pullrequests.py:150
 #: kallithea/templates/changeset/changeset.html:12
 #, fuzzy
 msgid "Changeset"
 msgstr "Changeset"
 
-#: kallithea/controllers/pullrequests.py:179
+#: kallithea/controllers/pullrequests.py:171
 msgid "Special"
 msgstr "Speciel"
 
-#: kallithea/controllers/pullrequests.py:180
+#: kallithea/controllers/pullrequests.py:172
 msgid "Peer branches"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:181 kallithea/model/scm.py:682
+#: kallithea/controllers/pullrequests.py:173 kallithea/model/scm.py:669
 msgid "Bookmarks"
 msgstr "Bogmærker"
 
-#: kallithea/controllers/pullrequests.py:318
+#: kallithea/controllers/pullrequests.py:315
 #, python-format
 msgid "Error creating pull request: %s"
 msgstr "Fejl ved oprettelse af pull-forespørgsel: %s"
 
-#: kallithea/controllers/pullrequests.py:345
-#: kallithea/controllers/pullrequests.py:368
+#: kallithea/controllers/pullrequests.py:342
+#: kallithea/controllers/pullrequests.py:365
 msgid "Error occurred while creating pull request"
 msgstr "Der opstod en fejl under oprettelse af pull-forespørgsel"
 
-#: kallithea/controllers/pullrequests.py:350
+#: kallithea/controllers/pullrequests.py:347
 msgid "Successfully opened new pull request"
 msgstr "Åbnede ny pull-forespørgsel med success"
 
-#: kallithea/controllers/pullrequests.py:373
+#: kallithea/controllers/pullrequests.py:370
 msgid "New pull request iteration created"
 msgstr "Ny pull-forespørgsel iteration oprettet"
 
-#: kallithea/controllers/pullrequests.py:401
+#: kallithea/controllers/pullrequests.py:398
 #, python-format
 msgid "Meanwhile, the following reviewers have been added: %s"
 msgstr "I mellemtiden er de følgende reviewers tilføjet: %s"
 
-#: kallithea/controllers/pullrequests.py:405
+#: kallithea/controllers/pullrequests.py:402
 #, python-format
 msgid "Meanwhile, the following reviewers have been removed: %s"
 msgstr "I mellemtiden er de følgende reviewers fjernet: %s"
 
-#: kallithea/controllers/pullrequests.py:421
-#: kallithea/model/pull_request.py:232
+#: kallithea/controllers/pullrequests.py:418
+#: kallithea/model/pull_request.py:230
 msgid "No description"
 msgstr "Ingen beskrivelse"
 
-#: kallithea/controllers/pullrequests.py:430
+#: kallithea/controllers/pullrequests.py:427
 msgid "Pull request updated"
 msgstr "Pull-forespørgsel opdateret"
 
-#: kallithea/controllers/pullrequests.py:443
+#: kallithea/controllers/pullrequests.py:440
 msgid "Successfully deleted pull request"
 msgstr "Slettede pull-forespørgsel med success"
 
-#: kallithea/controllers/pullrequests.py:479
+#: kallithea/controllers/pullrequests.py:476
 #, python-format
 msgid "Revision %s not found in %s"
 msgstr "Revision %s er ikke fundet i %s"
 
-#: kallithea/controllers/pullrequests.py:506
+#: kallithea/controllers/pullrequests.py:504
 #, python-format
 msgid "Error: changesets not found when displaying pull request from %s."
 msgstr ""
 "Fejl: Changesets ikke fundet, ved visning af pull-forespørgsel fra %s."
 
-#: kallithea/controllers/pullrequests.py:520
+#: kallithea/controllers/pullrequests.py:518
 #, python-format
 msgid "This pull request has already been merged to %s."
 msgstr "Denne pull-forespørgsel er allerede merged til %s."
 
-#: kallithea/controllers/pullrequests.py:522
+#: kallithea/controllers/pullrequests.py:520
 msgid "This pull request has been closed and can not be updated."
 msgstr "Denne pull-forespørgsel er lukket og kan ikke opdateres."
 
-#: kallithea/controllers/pullrequests.py:546
+#: kallithea/controllers/pullrequests.py:539
 #, python-format
 msgid "The following additional changes are available on %s:"
 msgstr "Følgende yderligere ændringer er tilgængelige på %s:"
 
-#: kallithea/controllers/pullrequests.py:548
-#: kallithea/controllers/pullrequests.py:552
+#: kallithea/controllers/pullrequests.py:541
+#: kallithea/controllers/pullrequests.py:545
 msgid "No additional changesets found for iterating on this pull request."
 msgstr ""
 "Ingen yderligere changesets fundet ved iteration på denne pull-"
 "forespørgsel."
 
-#: kallithea/controllers/pullrequests.py:560
+#: kallithea/controllers/pullrequests.py:553
 #, python-format
 msgid "Note: Branch %s has another head: %s."
 msgstr "Bemærk: Branch %s har et andet head: %s."
 
-#: kallithea/controllers/pullrequests.py:567
+#: kallithea/controllers/pullrequests.py:560
 msgid "Git pull requests don't support iterating yet."
 msgstr "Git pull-forespørgsler supportere ej iteration endnu."
 
-#: kallithea/controllers/pullrequests.py:569
+#: kallithea/controllers/pullrequests.py:562
 #, python-format
 msgid ""
 "Error: some changesets not found when displaying pull request from %s."
@@ -453,116 +457,116 @@
 "Fejl: Nogle changesets kunne ikke findes ved visning af pull-forespørgsel "
 "fra %s."
 
-#: kallithea/controllers/pullrequests.py:593
+#: kallithea/controllers/pullrequests.py:586
 msgid "The diff can't be shown - the PR revisions could not be found."
 msgstr ""
 "Diff'en kunne ikke vises - pull-forespørgslens revisions kunne ikke "
 "findes."
 
-#: kallithea/controllers/search.py:136
+#: kallithea/controllers/search.py:132
 msgid "Invalid search query. Try quoting it."
 msgstr "Ugyldig søgning. Prøv at citere det."
 
-#: kallithea/controllers/search.py:140
+#: kallithea/controllers/search.py:136
 msgid "The server has no search index."
 msgstr ""
 
-#: kallithea/controllers/search.py:143
+#: kallithea/controllers/search.py:139
 msgid "An error occurred during search operation."
 msgstr "Der opstod en fejl under søgning."
 
-#: kallithea/controllers/summary.py:168
-#: kallithea/templates/summary/summary.html:412
+#: kallithea/controllers/summary.py:169
+#: kallithea/templates/summary/summary.html:410
 msgid "No data ready yet"
 msgstr "Ingen data er klar endnu"
 
-#: kallithea/controllers/summary.py:171
+#: kallithea/controllers/summary.py:172
 #: kallithea/templates/summary/summary.html:97
 msgid "Statistics are disabled for this repository"
 msgstr "Statistik er slået fra for dette repository"
 
-#: kallithea/controllers/admin/auth_settings.py:137
+#: kallithea/controllers/admin/auth_settings.py:136
 msgid "Auth settings updated successfully"
 msgstr "Auth-indstillinger opdateret successfuldt"
 
-#: kallithea/controllers/admin/auth_settings.py:148
+#: kallithea/controllers/admin/auth_settings.py:147
 msgid "error occurred during update of auth settings"
 msgstr "Der opstod en fejl under opdatering af auth-indstillinger"
 
-#: kallithea/controllers/admin/defaults.py:75
+#: kallithea/controllers/admin/defaults.py:74
 msgid "Default settings updated successfully"
 msgstr "Standard-indstillinger opdateret successfuldt"
 
-#: kallithea/controllers/admin/defaults.py:90
+#: kallithea/controllers/admin/defaults.py:89
 msgid "Error occurred during update of defaults"
 msgstr "Der opstod en fejl under opdatering af standarder"
 
 #: kallithea/controllers/admin/gists.py:59
 #: kallithea/controllers/admin/my_account.py:232
-#: kallithea/controllers/admin/users.py:248
+#: kallithea/controllers/admin/users.py:246
 msgid "Forever"
 msgstr "For evigt"
 
 #: kallithea/controllers/admin/gists.py:60
 #: kallithea/controllers/admin/my_account.py:233
-#: kallithea/controllers/admin/users.py:249
+#: kallithea/controllers/admin/users.py:247
 msgid "5 minutes"
 msgstr "5 minutter"
 
 #: kallithea/controllers/admin/gists.py:61
 #: kallithea/controllers/admin/my_account.py:234
-#: kallithea/controllers/admin/users.py:250
+#: kallithea/controllers/admin/users.py:248
 msgid "1 hour"
 msgstr "1 time"
 
 #: kallithea/controllers/admin/gists.py:62
 #: kallithea/controllers/admin/my_account.py:235
-#: kallithea/controllers/admin/users.py:251
+#: kallithea/controllers/admin/users.py:249
 msgid "1 day"
 msgstr "1 dag"
 
 #: kallithea/controllers/admin/gists.py:63
 #: kallithea/controllers/admin/my_account.py:236
-#: kallithea/controllers/admin/users.py:252
+#: kallithea/controllers/admin/users.py:250
 msgid "1 month"
 msgstr "1 måned"
 
 #: kallithea/controllers/admin/gists.py:67
 #: kallithea/controllers/admin/my_account.py:238
-#: kallithea/controllers/admin/users.py:254
+#: kallithea/controllers/admin/users.py:252
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:65
 #: kallithea/templates/admin/users/user_edit_api_keys.html:65
 msgid "Lifetime"
 msgstr "Levetid"
 
-#: kallithea/controllers/admin/gists.py:142
+#: kallithea/controllers/admin/gists.py:148
 msgid "Error occurred during gist creation"
 msgstr "Der opstod en fejl under oprettelse af gist"
 
-#: kallithea/controllers/admin/gists.py:158
+#: kallithea/controllers/admin/gists.py:164
 #, python-format
 msgid "Deleted gist %s"
 msgstr "Slettet gist %s"
 
-#: kallithea/controllers/admin/gists.py:198
+#: kallithea/controllers/admin/gists.py:207
 msgid "Unmodified"
 msgstr "Uændret"
 
-#: kallithea/controllers/admin/gists.py:228
+#: kallithea/controllers/admin/gists.py:237
 msgid "Successfully updated gist content"
 msgstr "Opdateret gist-indhold successfuldt"
 
-#: kallithea/controllers/admin/gists.py:233
+#: kallithea/controllers/admin/gists.py:242
 msgid "Successfully updated gist data"
 msgstr "Opdateret gist-data successfuldt"
 
-#: kallithea/controllers/admin/gists.py:236
+#: kallithea/controllers/admin/gists.py:245
 #, python-format
 msgid "Error occurred during update of gist %s"
 msgstr "Der opstod en fejl under opdatering af gist %s"
 
-#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:211
-#: kallithea/model/user.py:232
+#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:205
+#: kallithea/model/user.py:226
 msgid "You can't edit this user since it's crucial for entire application"
 msgstr ""
 "Du kan ikke redigere denne bruger, da den er afgørende for hele "
@@ -573,7 +577,7 @@
 msgstr "Din konto er blevet opdateret successfuldt"
 
 #: kallithea/controllers/admin/my_account.py:134
-#: kallithea/controllers/admin/users.py:181
+#: kallithea/controllers/admin/users.py:179
 #, python-format
 msgid "Error occurred during update of user %s"
 msgstr "Der opstod en fejl under opdatering af bruger %s"
@@ -583,45 +587,45 @@
 msgstr "Der opstod en fejl under opdatering af bruger adgangskode"
 
 #: kallithea/controllers/admin/my_account.py:209
-#: kallithea/controllers/admin/users.py:367
+#: kallithea/controllers/admin/users.py:365
 #, python-format
 msgid "Added email %s to user"
 msgstr "Tilføjet email %s til bruger"
 
 #: kallithea/controllers/admin/my_account.py:215
-#: kallithea/controllers/admin/users.py:373
+#: kallithea/controllers/admin/users.py:371
 msgid "An error occurred during email saving"
 msgstr "Der opstod en fejl under tilføjelse af email"
 
 #: kallithea/controllers/admin/my_account.py:224
-#: kallithea/controllers/admin/users.py:383
+#: kallithea/controllers/admin/users.py:381
 msgid "Removed email from user"
 msgstr "Fjernet email fra brugeren"
 
 #: kallithea/controllers/admin/my_account.py:248
-#: kallithea/controllers/admin/users.py:271
+#: kallithea/controllers/admin/users.py:269
 msgid "API key successfully created"
 msgstr "API-nøgle oprettet successfuldt"
 
 #: kallithea/controllers/admin/my_account.py:257
-#: kallithea/controllers/admin/users.py:281
+#: kallithea/controllers/admin/users.py:279
 msgid "API key successfully reset"
 msgstr "API-nøgle nulstillet successfuldt"
 
 #: kallithea/controllers/admin/my_account.py:261
-#: kallithea/controllers/admin/users.py:285
+#: kallithea/controllers/admin/users.py:283
 msgid "API key successfully deleted"
 msgstr "API-nøgle slettet successfuldt"
 
 #: kallithea/controllers/admin/my_account.py:281
-#: kallithea/controllers/admin/users.py:456
+#: kallithea/controllers/admin/users.py:454
 #, fuzzy, python-format
 #| msgid "API key successfully created"
 msgid "SSH key %s successfully added"
 msgstr "API-nøgle oprettet successfuldt"
 
 #: kallithea/controllers/admin/my_account.py:293
-#: kallithea/controllers/admin/users.py:470
+#: kallithea/controllers/admin/users.py:468
 #, fuzzy
 #| msgid "API key successfully deleted"
 msgid "SSH key successfully deleted"
@@ -699,11 +703,11 @@
 msgid "Allowed with automatic account activation"
 msgstr "Tilladt med automatisk kontoaktivering"
 
-#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1673
+#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1578
 msgid "Manual activation of external account"
 msgstr "Manuel aktivering af ekstern konto"
 
-#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1674
+#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1579
 msgid "Automatic activation of external account"
 msgstr "Automatisk aktivering af ekstern konto"
 
@@ -725,188 +729,180 @@
 msgid "Error occurred during update of permissions"
 msgstr "Der opstod en fejl under opdatering af tilladelser"
 
-#: kallithea/controllers/admin/repo_groups.py:172
+#: kallithea/controllers/admin/repo_groups.py:165
 #, python-format
 msgid "Error occurred during creation of repository group %s"
 msgstr "Der opstod en fejl under oprettelse af repository-gruppen %s"
 
-#: kallithea/controllers/admin/repo_groups.py:177
+#: kallithea/controllers/admin/repo_groups.py:172
 #, python-format
 msgid "Created repository group %s"
 msgstr "Oprettet repository-gruppen %s"
 
-#: kallithea/controllers/admin/repo_groups.py:224
+#: kallithea/controllers/admin/repo_groups.py:219
 #, python-format
 msgid "Updated repository group %s"
 msgstr "Opdateret repository-gruppen %s"
 
-#: kallithea/controllers/admin/repo_groups.py:240
+#: kallithea/controllers/admin/repo_groups.py:235
 #, python-format
 msgid "Error occurred during update of repository group %s"
 msgstr "Der opstod en fejl under opdatering af repository-gruppen %s"
 
-#: kallithea/controllers/admin/repo_groups.py:250
+#: kallithea/controllers/admin/repo_groups.py:245
 #, python-format
 msgid "This group contains %s repositories and cannot be deleted"
 msgstr "Denne gruppe indeholder %s repositories og kan ikke slettes"
 
-#: kallithea/controllers/admin/repo_groups.py:257
+#: kallithea/controllers/admin/repo_groups.py:252
 #, python-format
 msgid "This group contains %s subgroups and cannot be deleted"
 msgstr "Denne gruppe indeholder %s undergrupper og kan ikke slettes"
 
-#: kallithea/controllers/admin/repo_groups.py:263
+#: kallithea/controllers/admin/repo_groups.py:258
 #, python-format
 msgid "Removed repository group %s"
 msgstr "Fjernet repository-gruppen %s"
 
-#: kallithea/controllers/admin/repo_groups.py:268
+#: kallithea/controllers/admin/repo_groups.py:263
 #, python-format
 msgid "Error occurred during deletion of repository group %s"
 msgstr "Der opstod en fejl under sletning af repository-gruppen %s"
 
-#: kallithea/controllers/admin/repo_groups.py:352
-#: kallithea/controllers/admin/repo_groups.py:382
-#: kallithea/controllers/admin/user_groups.py:294
+#: kallithea/controllers/admin/repo_groups.py:347
+#: kallithea/controllers/admin/repo_groups.py:377
+#: kallithea/controllers/admin/user_groups.py:290
 msgid "Cannot revoke permission for yourself as admin"
 msgstr "Kan ikke tilbagekalde tilladelse for én selv som admin"
 
-#: kallithea/controllers/admin/repo_groups.py:367
+#: kallithea/controllers/admin/repo_groups.py:362
 msgid "Repository group permissions updated"
 msgstr "Repository-gruppe tilladelser opdateret"
 
-#: kallithea/controllers/admin/repo_groups.py:399
-#: kallithea/controllers/admin/repos.py:358
-#: kallithea/controllers/admin/user_groups.py:306
+#: kallithea/controllers/admin/repo_groups.py:394
+#: kallithea/controllers/admin/repos.py:357
+#: kallithea/controllers/admin/user_groups.py:302
 msgid "An error occurred during revoking of permission"
 msgstr "Der opstod en fejl under tilbagekaldelse af tilladelse"
 
-#: kallithea/controllers/admin/repos.py:136
+#: kallithea/controllers/admin/repos.py:137
 #, python-format
 msgid "Error creating repository %s"
 msgstr "Fejl ved oprettelse af repository %s"
 
-#: kallithea/controllers/admin/repos.py:194
+#: kallithea/controllers/admin/repos.py:193
 #, python-format
 msgid "Created repository %s from %s"
 msgstr "Oprettet repository %s fra %s"
 
-#: kallithea/controllers/admin/repos.py:203
+#: kallithea/controllers/admin/repos.py:202
 #, python-format
 msgid "Forked repository %s as %s"
 msgstr "Forked repository %s som %s"
 
-#: kallithea/controllers/admin/repos.py:206
+#: kallithea/controllers/admin/repos.py:205
 #, python-format
 msgid "Created repository %s"
 msgstr "Oprettet repository %s"
 
-#: kallithea/controllers/admin/repos.py:235
+#: kallithea/controllers/admin/repos.py:234
 #, python-format
 msgid "Repository %s updated successfully"
 msgstr "Repository %s opdateret"
 
-#: kallithea/controllers/admin/repos.py:255
+#: kallithea/controllers/admin/repos.py:254
 #, python-format
 msgid "Error occurred during update of repository %s"
 msgstr "Der opstod en fejl under opdatering af repository %s"
 
-#: kallithea/controllers/admin/repos.py:273
+#: kallithea/controllers/admin/repos.py:272
 #, python-format
 msgid "Detached %s forks"
 msgstr "Fraskilt %s forks"
 
-#: kallithea/controllers/admin/repos.py:276
+#: kallithea/controllers/admin/repos.py:275
 #, python-format
 msgid "Deleted %s forks"
 msgstr "Slettet %s forks"
 
-#: kallithea/controllers/admin/repos.py:281
+#: kallithea/controllers/admin/repos.py:280
 #, python-format
 msgid "Deleted repository %s"
 msgstr "Slettet repository %s"
 
-#: kallithea/controllers/admin/repos.py:284
+#: kallithea/controllers/admin/repos.py:283
 #, python-format
 msgid "Cannot delete repository %s which still has forks"
 msgstr "Kan ikke slette repository %s, da den stadig har forks"
 
-#: kallithea/controllers/admin/repos.py:289
+#: kallithea/controllers/admin/repos.py:288
 #, python-format
 msgid "An error occurred during deletion of %s"
 msgstr "Der opstod en fejl under sletning af %s"
 
-#: kallithea/controllers/admin/repos.py:329
+#: kallithea/controllers/admin/repos.py:328
 msgid "Repository permissions updated"
 msgstr "Repository tilladelser opdateret"
 
-#: kallithea/controllers/admin/repos.py:388
+#: kallithea/controllers/admin/repos.py:387
 #, python-format
 msgid "Field validation error: %s"
 msgstr "Feltvaliderings fejl: %s"
 
-#: kallithea/controllers/admin/repos.py:391
+#: kallithea/controllers/admin/repos.py:390
 #, python-format
 msgid "An error occurred during creation of field: %r"
 msgstr "Der opstod en fejl under oprettelse af felt: %r"
 
-#: kallithea/controllers/admin/repos.py:402
+#: kallithea/controllers/admin/repos.py:401
 msgid "An error occurred during removal of field"
 msgstr "Der opstod en fejl under fjernelse af feltet"
 
-#: kallithea/controllers/admin/repos.py:416
+#: kallithea/controllers/admin/repos.py:415
 msgid "-- Not a fork --"
 msgstr "-- Ikke en fork --"
 
-#: kallithea/controllers/admin/repos.py:448
+#: kallithea/controllers/admin/repos.py:447
 msgid "Updated repository visibility in public journal"
 msgstr "Opdateret repository's synlighed i den offentlige journal"
 
-#: kallithea/controllers/admin/repos.py:452
+#: kallithea/controllers/admin/repos.py:451
 msgid "An error occurred during setting this repository in public journal"
 msgstr ""
 "Der opstod en fejl under indstilling af dette repository, i den "
 "offentlige journal"
 
-#: kallithea/controllers/admin/repos.py:468
+#: kallithea/controllers/admin/repos.py:467
 msgid "Nothing"
 msgstr "Intet"
 
-#: kallithea/controllers/admin/repos.py:470
+#: kallithea/controllers/admin/repos.py:469
 #, python-format
 msgid "Marked repository %s as fork of %s"
 msgstr "Mærket repository %s som fork af %s"
 
-#: kallithea/controllers/admin/repos.py:477
+#: kallithea/controllers/admin/repos.py:476
 msgid "An error occurred during this operation"
 msgstr "Der opstod en fejl under denne operation"
 
-#: kallithea/controllers/admin/repos.py:490
-msgid "Cache invalidation successful"
-msgstr "Ugyldiggørelse af cache er succesfuld"
-
-#: kallithea/controllers/admin/repos.py:494
-msgid "An error occurred during cache invalidation"
-msgstr "Der opstod en fejl under cache ugyldiggørelse"
-
-#: kallithea/controllers/admin/repos.py:507
+#: kallithea/controllers/admin/repos.py:488
 #, fuzzy
 msgid "Pulled from remote location"
 msgstr "Pulled fra remote placering"
 
-#: kallithea/controllers/admin/repos.py:510
+#: kallithea/controllers/admin/repos.py:491
 msgid "An error occurred during pull from remote location"
 msgstr "Der opstod en fejl under pull fra remote placering"
 
-#: kallithea/controllers/admin/repos.py:541
+#: kallithea/controllers/admin/repos.py:522
 msgid "An error occurred during deletion of repository stats"
 msgstr "Der opstod en fejl under sletning af repository statistik"
 
-#: kallithea/controllers/admin/settings.py:131
+#: kallithea/controllers/admin/settings.py:132
 msgid "Updated VCS settings"
 msgstr "Opdateret VCS-indstillinger"
 
-#: kallithea/controllers/admin/settings.py:135 kallithea/lib/utils.py:237
+#: kallithea/controllers/admin/settings.py:136
 msgid ""
 "Unable to activate hgsubversion support. The \"hgsubversion\" library is "
 "missing"
@@ -914,157 +910,157 @@
 "Ude af stand til at aktivere hgsubversion understøttelse. \"hgsubversion"
 "\" biblioteket mangler"
 
-#: kallithea/controllers/admin/settings.py:141
-#: kallithea/controllers/admin/settings.py:233
+#: kallithea/controllers/admin/settings.py:142
+#: kallithea/controllers/admin/settings.py:234
 msgid "Error occurred while updating application settings"
 msgstr "Der opstod en fejl ved opdatering af applikationsindstillinger"
 
-#: kallithea/controllers/admin/settings.py:176
+#: kallithea/controllers/admin/settings.py:177
 #, python-format
 msgid "Repositories successfully rescanned. Added: %s. Removed: %s."
 msgstr "Repositories genscannet successfuldt. Tilføjet: %s. Fjernet: %s."
 
-#: kallithea/controllers/admin/settings.py:188
+#: kallithea/controllers/admin/settings.py:189
 #, python-format
 msgid "Invalidated %s repositories"
 msgstr "Ugyldiggjort %s repositories"
 
-#: kallithea/controllers/admin/settings.py:229
+#: kallithea/controllers/admin/settings.py:230
 msgid "Updated application settings"
 msgstr "Opdateret applikationsindstillinger"
 
-#: kallithea/controllers/admin/settings.py:283
+#: kallithea/controllers/admin/settings.py:284
 msgid "Updated visualisation settings"
 msgstr "Opdateret visualiseringsindstillinger"
 
-#: kallithea/controllers/admin/settings.py:288
+#: kallithea/controllers/admin/settings.py:289
 msgid "Error occurred during updating visualisation settings"
 msgstr "Der opstod en fejl under opdatering af visualiseringsindstillinger"
 
-#: kallithea/controllers/admin/settings.py:312
+#: kallithea/controllers/admin/settings.py:313
 msgid "Please enter email address"
 msgstr "Indtast email-adresse"
 
-#: kallithea/controllers/admin/settings.py:327
+#: kallithea/controllers/admin/settings.py:328
 msgid "Send email task created"
 msgstr "Send email-opgave oprettet"
 
-#: kallithea/controllers/admin/settings.py:355
+#: kallithea/controllers/admin/settings.py:356
 #, fuzzy
 #| msgid "No data ready yet"
 msgid "Hook already exists"
 msgstr "Ingen data er klar endnu"
 
-#: kallithea/controllers/admin/settings.py:357
+#: kallithea/controllers/admin/settings.py:358
 msgid "Builtin hooks are read-only. Please use another hook name."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:360
+#: kallithea/controllers/admin/settings.py:361
 msgid "Added new hook"
 msgstr "Tilføjet nyt hook"
 
-#: kallithea/controllers/admin/settings.py:376
+#: kallithea/controllers/admin/settings.py:377
 msgid "Updated hooks"
 msgstr "Opdateret hooks"
 
-#: kallithea/controllers/admin/settings.py:380
+#: kallithea/controllers/admin/settings.py:381
 msgid "Error occurred during hook creation"
 msgstr "Der opstod en fejl under oprettelse af et hook"
 
-#: kallithea/controllers/admin/settings.py:404
+#: kallithea/controllers/admin/settings.py:405
 msgid "Whoosh reindex task scheduled"
 msgstr "Whoosh reindex-opgave skeduleret"
 
-#: kallithea/controllers/admin/user_groups.py:138
+#: kallithea/controllers/admin/user_groups.py:134
 #, python-format
 msgid "Created user group %s"
 msgstr "Oprettet brugergruppe %s"
 
-#: kallithea/controllers/admin/user_groups.py:151
+#: kallithea/controllers/admin/user_groups.py:147
 #, python-format
 msgid "Error occurred during creation of user group %s"
 msgstr "Der opstod en fejl under oprettelse af brugergruppe %s"
 
-#: kallithea/controllers/admin/user_groups.py:179
+#: kallithea/controllers/admin/user_groups.py:175
 #, python-format
 msgid "Updated user group %s"
 msgstr "Opdateret brugergruppe %s"
 
-#: kallithea/controllers/admin/user_groups.py:201
+#: kallithea/controllers/admin/user_groups.py:197
 #, python-format
 msgid "Error occurred during update of user group %s"
 msgstr "Der opstod en fejl under opdatering af brugergruppe %s"
 
-#: kallithea/controllers/admin/user_groups.py:212
+#: kallithea/controllers/admin/user_groups.py:208
 msgid "Successfully deleted user group"
 msgstr "Brugergruppe slettet succesfuldt"
 
-#: kallithea/controllers/admin/user_groups.py:217
+#: kallithea/controllers/admin/user_groups.py:213
 msgid "An error occurred during deletion of user group"
 msgstr "Der opstod en fejl under sletning af brugergruppe"
 
-#: kallithea/controllers/admin/user_groups.py:273
+#: kallithea/controllers/admin/user_groups.py:269
 msgid "Target group cannot be the same"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:279
+#: kallithea/controllers/admin/user_groups.py:275
 msgid "User group permissions updated"
 msgstr "Brugergrupper-tilladelser opdateret"
 
+#: kallithea/controllers/admin/user_groups.py:384
+#: kallithea/controllers/admin/users.py:336
+msgid "Updated permissions"
+msgstr "Tilladelser opdateret"
+
 #: kallithea/controllers/admin/user_groups.py:388
-#: kallithea/controllers/admin/users.py:338
-msgid "Updated permissions"
-msgstr "Tilladelser opdateret"
-
-#: kallithea/controllers/admin/user_groups.py:392
-#: kallithea/controllers/admin/users.py:342
+#: kallithea/controllers/admin/users.py:340
 msgid "An error occurred during permissions saving"
 msgstr "Der opstod en fejl under gemning af tilladelser"
 
-#: kallithea/controllers/admin/users.py:123
+#: kallithea/controllers/admin/users.py:121
 #, python-format
 msgid "Created user %s"
 msgstr "Bruger %s oprettet"
 
-#: kallithea/controllers/admin/users.py:138
+#: kallithea/controllers/admin/users.py:136
 #, python-format
 msgid "Error occurred during creation of user %s"
 msgstr "Der opstod en fejl under oprettelse af bruger %s"
 
-#: kallithea/controllers/admin/users.py:162
+#: kallithea/controllers/admin/users.py:160
 msgid "User updated successfully"
 msgstr "Bruger opdateret"
 
-#: kallithea/controllers/admin/users.py:190
+#: kallithea/controllers/admin/users.py:188
 msgid "Successfully deleted user"
 msgstr "Slettet bruger"
 
-#: kallithea/controllers/admin/users.py:195
+#: kallithea/controllers/admin/users.py:193
 msgid "An error occurred during deletion of user"
 msgstr "Der opstod en fejl under sletning af bruger %s"
 
-#: kallithea/controllers/admin/users.py:203
+#: kallithea/controllers/admin/users.py:201
 msgid "The default user cannot be edited"
 msgstr "Standardbrugeren kan ikke redigeres"
 
-#: kallithea/controllers/admin/users.py:409
+#: kallithea/controllers/admin/users.py:407
 #, python-format
 msgid "Added IP address %s to user whitelist"
 msgstr "Tilføjet IP-adresse %s til bruger-whitelist"
 
-#: kallithea/controllers/admin/users.py:415
+#: kallithea/controllers/admin/users.py:413
 msgid "An error occurred while adding IP address"
 msgstr "Der opstod en fejl under tilføjelse af IP-adresse"
 
-#: kallithea/controllers/admin/users.py:427
+#: kallithea/controllers/admin/users.py:425
 msgid "Removed IP address from user whitelist"
 msgstr "Fjernet IP-adresse fra bruger-whitelist"
 
-#: kallithea/lib/auth.py:684
+#: kallithea/lib/auth.py:634
 msgid "You need to be a registered user to perform this action"
 msgstr "Du skal være registreret bruger for at kunne udføre denne handling"
 
-#: kallithea/lib/auth.py:712
+#: kallithea/lib/auth.py:662
 msgid "You need to be signed in to view this page"
 msgstr "Du skal være logget ind for at se denne side"
 
@@ -1097,170 +1093,170 @@
 "Changeset var for stor, og blev afskåret, brug diff menu for at få vist "
 "denne diff"
 
-#: kallithea/lib/diffs.py:224
+#: kallithea/lib/diffs.py:223
 msgid "No changes detected"
 msgstr "Ingen ændringer fundet"
 
-#: kallithea/lib/helpers.py:653
+#: kallithea/lib/helpers.py:670
 #, python-format
 msgid "Deleted branch: %s"
 msgstr "Slettet branch: %s"
 
-#: kallithea/lib/helpers.py:655
+#: kallithea/lib/helpers.py:672
 #, python-format
 msgid "Created tag: %s"
 msgstr "Oprettet tag: %s"
 
-#: kallithea/lib/helpers.py:666
+#: kallithea/lib/helpers.py:683
 #, python-format
 msgid "Changeset %s not found"
 msgstr "Changeset %s ikke fundet"
 
-#: kallithea/lib/helpers.py:715
+#: kallithea/lib/helpers.py:732
 #, python-format
 msgid "Show all combined changesets %s->%s"
 msgstr "Vis alle kombineret changesets %s->%s"
 
-#: kallithea/lib/helpers.py:721
+#: kallithea/lib/helpers.py:738
 msgid "Compare view"
 msgstr "Sammenlign visning"
 
-#: kallithea/lib/helpers.py:740
+#: kallithea/lib/helpers.py:757
 msgid "and"
 msgstr "og"
 
-#: kallithea/lib/helpers.py:741
+#: kallithea/lib/helpers.py:758
 #, python-format
 msgid "%s more"
 msgstr "%s flere"
 
-#: kallithea/lib/helpers.py:742
+#: kallithea/lib/helpers.py:759
 #: kallithea/templates/changelog/changelog.html:43
 msgid "revisions"
 msgstr "revisioner"
 
-#: kallithea/lib/helpers.py:766
+#: kallithea/lib/helpers.py:783
 #, python-format
 msgid "Fork name %s"
 msgstr "Fork-navn %s"
 
-#: kallithea/lib/helpers.py:787
+#: kallithea/lib/helpers.py:804
 #, python-format
 msgid "Pull request %s"
 msgstr "Pull-forespørgsel %s"
 
-#: kallithea/lib/helpers.py:797
+#: kallithea/lib/helpers.py:814
 msgid "[deleted] repository"
 msgstr "[slettet] repository"
 
-#: kallithea/lib/helpers.py:799 kallithea/lib/helpers.py:811
+#: kallithea/lib/helpers.py:816 kallithea/lib/helpers.py:828
 msgid "[created] repository"
 msgstr "[oprettet] repository"
 
-#: kallithea/lib/helpers.py:801
+#: kallithea/lib/helpers.py:818
 msgid "[created] repository as fork"
 msgstr "[oprettet] repository som fork"
 
-#: kallithea/lib/helpers.py:803 kallithea/lib/helpers.py:813
+#: kallithea/lib/helpers.py:820 kallithea/lib/helpers.py:830
 msgid "[forked] repository"
 msgstr "[forked] repository"
 
-#: kallithea/lib/helpers.py:805 kallithea/lib/helpers.py:815
+#: kallithea/lib/helpers.py:822 kallithea/lib/helpers.py:832
 msgid "[updated] repository"
 msgstr "[opdateret] repository"
 
-#: kallithea/lib/helpers.py:807
+#: kallithea/lib/helpers.py:824
 msgid "[downloaded] archive from repository"
 msgstr "[hentet] arkiv fra repository"
 
-#: kallithea/lib/helpers.py:809
+#: kallithea/lib/helpers.py:826
 msgid "[delete] repository"
 msgstr "[slettet] repository"
 
-#: kallithea/lib/helpers.py:817
+#: kallithea/lib/helpers.py:834
 msgid "[created] user"
 msgstr "[oprettet] bruger"
 
-#: kallithea/lib/helpers.py:819
+#: kallithea/lib/helpers.py:836
 msgid "[updated] user"
 msgstr "[opdateret] bruger"
 
-#: kallithea/lib/helpers.py:821
+#: kallithea/lib/helpers.py:838
 msgid "[created] user group"
 msgstr "[oprettet] brugergruppe"
 
-#: kallithea/lib/helpers.py:823
+#: kallithea/lib/helpers.py:840
 msgid "[updated] user group"
 msgstr "[opdateret] brugergruppe"
 
-#: kallithea/lib/helpers.py:825
+#: kallithea/lib/helpers.py:842
 msgid "[commented] on revision in repository"
 msgstr "[kommenterede] på revision i repository"
 
-#: kallithea/lib/helpers.py:827
+#: kallithea/lib/helpers.py:844
 msgid "[commented] on pull request for"
 msgstr "[kommenterede] på pull-forespørgsel for"
 
-#: kallithea/lib/helpers.py:829
+#: kallithea/lib/helpers.py:846
 msgid "[closed] pull request for"
 msgstr "[lukket] pull-forespørgsel for"
 
-#: kallithea/lib/helpers.py:831
+#: kallithea/lib/helpers.py:848
 msgid "[pushed] into"
 msgstr "[pushed] ind i"
 
-#: kallithea/lib/helpers.py:833
+#: kallithea/lib/helpers.py:850
 msgid "[committed via Kallithea] into repository"
 msgstr "[committed via kallithea] ind i repository"
 
-#: kallithea/lib/helpers.py:835
+#: kallithea/lib/helpers.py:852
 msgid "[pulled from remote] into repository"
 msgstr "[pulled fra remote] ind i repository"
 
-#: kallithea/lib/helpers.py:837
+#: kallithea/lib/helpers.py:854
 msgid "[pulled] from"
 msgstr "[pulled] fra"
 
-#: kallithea/lib/helpers.py:839
+#: kallithea/lib/helpers.py:856
 msgid "[started following] repository"
 msgstr "[begyndt at følge] repository"
 
-#: kallithea/lib/helpers.py:841
+#: kallithea/lib/helpers.py:858
 msgid "[stopped following] repository"
 msgstr "[stoppet at følge] repository"
 
-#: kallithea/lib/helpers.py:961
+#: kallithea/lib/helpers.py:975
 #, python-format
 msgid " and %s more"
 msgstr " og %s flere"
 
-#: kallithea/lib/helpers.py:965
+#: kallithea/lib/helpers.py:979
 #: kallithea/templates/compare/compare_diff.html:69
 #: kallithea/templates/pullrequests/pullrequest_show.html:297
 msgid "No files"
 msgstr "Ingen filer"
 
-#: kallithea/lib/helpers.py:990
+#: kallithea/lib/helpers.py:1004
 msgid "new file"
 msgstr "ny fil"
 
-#: kallithea/lib/helpers.py:993
+#: kallithea/lib/helpers.py:1007
 msgid "mod"
 msgstr "mod"
 
-#: kallithea/lib/helpers.py:996
+#: kallithea/lib/helpers.py:1010
 msgid "del"
 msgstr "del"
 
-#: kallithea/lib/helpers.py:999
+#: kallithea/lib/helpers.py:1013
 msgid "rename"
 msgstr "omdøb"
 
-#: kallithea/lib/helpers.py:1004
+#: kallithea/lib/helpers.py:1018
 msgid "chmod"
 msgstr "chmod"
 
-#: kallithea/lib/helpers.py:1297
+#: kallithea/lib/helpers.py:1323
 #, python-format
 msgid ""
 "%s repository is not mapped to db perhaps it was created or renamed from "
@@ -1270,96 +1266,98 @@
 "%s repository er ikke knyttet til db, måske var det skabt eller omdøbt "
 "fra filsystemet, kør applikationen igen for at scanne repositories"
 
-#: kallithea/lib/ssh.py:73
+#: kallithea/lib/ssh.py:75
 msgid "SSH key is missing"
 msgstr ""
 
-#: kallithea/lib/ssh.py:77
-msgid "Incorrect SSH key - it must have both a key type and a base64 part"
-msgstr ""
-
-#: kallithea/lib/ssh.py:81
+#: kallithea/lib/ssh.py:79
+msgid ""
+"Incorrect SSH key - it must have both a key type and a base64 part, like "
+"'ssh-rsa ASRNeaZu4FA...xlJp='"
+msgstr ""
+
+#: kallithea/lib/ssh.py:83
 msgid "Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'"
 msgstr ""
 
-#: kallithea/lib/ssh.py:84
+#: kallithea/lib/ssh.py:86
 #, python-format
 msgid "Incorrect SSH key - unexpected characters in base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:89
+#: kallithea/lib/ssh.py:91
 #, python-format
 msgid "Incorrect SSH key - failed to decode base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:92
+#: kallithea/lib/ssh.py:94
 #, python-format
 msgid "Incorrect SSH key - base64 part is not %r as claimed but %r"
 msgstr ""
 
-#: kallithea/lib/utils2.py:334
+#: kallithea/lib/utils2.py:253
 #, python-format
 msgid "%d year"
 msgid_plural "%d years"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:335
+#: kallithea/lib/utils2.py:254
 #, python-format
 msgid "%d month"
 msgid_plural "%d months"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:336
+#: kallithea/lib/utils2.py:255
 #, python-format
 msgid "%d day"
 msgid_plural "%d days"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:337
+#: kallithea/lib/utils2.py:256
 #, python-format
 msgid "%d hour"
 msgid_plural "%d hours"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:338
+#: kallithea/lib/utils2.py:257
 #, python-format
 msgid "%d minute"
 msgid_plural "%d minutes"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:339
+#: kallithea/lib/utils2.py:258
 #, python-format
 msgid "%d second"
 msgid_plural "%d seconds"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:355
+#: kallithea/lib/utils2.py:274
 #, python-format
 msgid "in %s"
 msgstr "i %s"
 
-#: kallithea/lib/utils2.py:357
+#: kallithea/lib/utils2.py:276
 #, python-format
 msgid "%s ago"
 msgstr "%s siden"
 
-#: kallithea/lib/utils2.py:359
+#: kallithea/lib/utils2.py:278
 #, python-format
 msgid "in %s and %s"
 msgstr "i %s og %s"
 
-#: kallithea/lib/utils2.py:362
+#: kallithea/lib/utils2.py:281
 #, python-format
 msgid "%s and %s ago"
 msgstr "%s og %s siden"
 
-#: kallithea/lib/utils2.py:365
+#: kallithea/lib/utils2.py:284
 msgid "just now"
 msgstr "lige nu"
 
@@ -1368,136 +1366,136 @@
 msgid "on line %s"
 msgstr "på linje %s"
 
-#: kallithea/model/comment.py:221 kallithea/model/pull_request.py:114
+#: kallithea/model/comment.py:219 kallithea/model/pull_request.py:112
 msgid "[Mention]"
 msgstr "[Omtale]"
 
-#: kallithea/model/db.py:1496
+#: kallithea/model/db.py:1411
 msgid "top level"
 msgstr "top-niveau"
 
-#: kallithea/model/db.py:1637
+#: kallithea/model/db.py:1542
 msgid "Kallithea Administrator"
 msgstr "Kallithea Administrator"
 
-#: kallithea/model/db.py:1639
+#: kallithea/model/db.py:1544
 msgid "Default user has no access to new repositories"
 msgstr "Standard-bruger har ikke adgang til nye repositories"
 
-#: kallithea/model/db.py:1640
+#: kallithea/model/db.py:1545
 msgid "Default user has read access to new repositories"
 msgstr "Standard-bruger har læse-adgang til nye repositories"
 
-#: kallithea/model/db.py:1641
+#: kallithea/model/db.py:1546
 msgid "Default user has write access to new repositories"
 msgstr "Standard-bruger har skrive-adgang til nye repositories"
 
-#: kallithea/model/db.py:1642
+#: kallithea/model/db.py:1547
 msgid "Default user has admin access to new repositories"
 msgstr "Standard-bruger har admin-adgang til nye repositories"
 
-#: kallithea/model/db.py:1644
+#: kallithea/model/db.py:1549
 msgid "Default user has no access to new repository groups"
 msgstr "Standard-bruger har ikke adgang til nye repository-grupper"
 
-#: kallithea/model/db.py:1645
+#: kallithea/model/db.py:1550
 msgid "Default user has read access to new repository groups"
 msgstr "Standard-bruger har læse-adgang til nye repository-grupper"
 
-#: kallithea/model/db.py:1646
+#: kallithea/model/db.py:1551
 msgid "Default user has write access to new repository groups"
 msgstr "Standard-bruger har skrive-adgang til nye repository-grupper"
 
-#: kallithea/model/db.py:1647
+#: kallithea/model/db.py:1552
 msgid "Default user has admin access to new repository groups"
 msgstr "Standard-bruger har admin-adgang til nye repository-grupper"
 
-#: kallithea/model/db.py:1649
+#: kallithea/model/db.py:1554
 msgid "Default user has no access to new user groups"
 msgstr "Standard-bruger har ikke adgang til nye brugergrupper"
 
-#: kallithea/model/db.py:1650
+#: kallithea/model/db.py:1555
 msgid "Default user has read access to new user groups"
 msgstr "Standard-bruger har læse-adgang til nye brugergrupper"
 
-#: kallithea/model/db.py:1651
+#: kallithea/model/db.py:1556
 msgid "Default user has write access to new user groups"
 msgstr "Standard-bruger har skrive-adgang til nye brugergrupper"
 
-#: kallithea/model/db.py:1652
+#: kallithea/model/db.py:1557
 msgid "Default user has admin access to new user groups"
 msgstr "Standard-bruger har admin-adgang til nye brugergrupper"
 
-#: kallithea/model/db.py:1654
+#: kallithea/model/db.py:1559
 msgid "Only admins can create repository groups"
 msgstr "Kun administratorer kan oprette repository-grupper"
 
-#: kallithea/model/db.py:1655
+#: kallithea/model/db.py:1560
 msgid "Non-admins can create repository groups"
 msgstr "Ikke-administratorer kan oprette repository-grupper"
 
-#: kallithea/model/db.py:1657
+#: kallithea/model/db.py:1562
 msgid "Only admins can create user groups"
 msgstr "Kun administratorer kan oprette brugergrupper"
 
-#: kallithea/model/db.py:1658
+#: kallithea/model/db.py:1563
 msgid "Non-admins can create user groups"
 msgstr "Ikke-administratorer kan oprette brugergrupper"
 
-#: kallithea/model/db.py:1660
+#: kallithea/model/db.py:1565
 msgid "Only admins can create top level repositories"
 msgstr "Kun administratorer kan oprette top-niveau repositories"
 
-#: kallithea/model/db.py:1661
+#: kallithea/model/db.py:1566
 msgid "Non-admins can create top level repositories"
 msgstr "Ikke-administratorer kan oprette top-niveau repositories"
 
-#: kallithea/model/db.py:1663
+#: kallithea/model/db.py:1568
 msgid ""
 "Repository creation enabled with write permission to a repository group"
 msgstr ""
 "Repository oprettelse aktiveret med skriveadgang til en repository-gruppe"
 
-#: kallithea/model/db.py:1664
+#: kallithea/model/db.py:1569
 msgid ""
 "Repository creation disabled with write permission to a repository group"
 msgstr ""
 "Repository oprettelse deaktiveret med skriveadgang til en repository-"
 "gruppe"
 
-#: kallithea/model/db.py:1666
+#: kallithea/model/db.py:1571
 msgid "Only admins can fork repositories"
 msgstr "Kun admins kan fork repositories"
 
-#: kallithea/model/db.py:1667
+#: kallithea/model/db.py:1572
 msgid "Non-admins can fork repositories"
 msgstr "Ikke-administratorer kan forke repositories"
 
-#: kallithea/model/db.py:1669
+#: kallithea/model/db.py:1574
 msgid "Registration disabled"
 msgstr "Registrering deaktiveret"
 
-#: kallithea/model/db.py:1670
+#: kallithea/model/db.py:1575
 msgid "User registration with manual account activation"
 msgstr "Brugerregistrering med manuel kontoaktivering"
 
-#: kallithea/model/db.py:1671
+#: kallithea/model/db.py:1576
 msgid "User registration with automatic account activation"
 msgstr "Brugerregistrering med automatisk kontoaktivering"
 
-#: kallithea/model/db.py:2206
+#: kallithea/model/db.py:1992
 msgid "Not reviewed"
 msgstr "Ikke gennemgået"
 
-#: kallithea/model/db.py:2207
+#: kallithea/model/db.py:1993
 msgid "Under review"
 msgstr "Under gennemgang"
 
-#: kallithea/model/db.py:2208
+#: kallithea/model/db.py:1994
 msgid "Not approved"
 msgstr "Ikke godkendt"
 
-#: kallithea/model/db.py:2209
+#: kallithea/model/db.py:1995
 msgid "Approved"
 msgstr "Godkendt"
 
@@ -1523,7 +1521,7 @@
 msgid "Name must not contain only digits"
 msgstr "Navn må ikke kun indeholde cifre"
 
-#: kallithea/model/notification.py:164
+#: kallithea/model/notification.py:162
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s changeset %(short_id)s \"%(message_short)s\" on "
@@ -1532,12 +1530,12 @@
 "[Kommentar] %(repo_name)s changeset %(short_id)s \"%(message_short)s\" på "
 "%(branch)s"
 
-#: kallithea/model/notification.py:167
+#: kallithea/model/notification.py:165
 #, python-format
 msgid "New user %(new_username)s registered"
 msgstr "Ny bruger %(new_username)s registreret"
 
-#: kallithea/model/notification.py:169
+#: kallithea/model/notification.py:167
 #, python-format
 msgid ""
 "[Review] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
@@ -1546,7 +1544,7 @@
 "[Gennemgang] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" fra "
 "%(pr_source_branch)s af %(pr_owner_username)s"
 
-#: kallithea/model/notification.py:170
+#: kallithea/model/notification.py:168
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
@@ -1555,11 +1553,11 @@
 "[Kommentar] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" fra "
 "%(pr_source_branch)s af %(pr_owner_username)s"
 
-#: kallithea/model/notification.py:183
+#: kallithea/model/notification.py:188
 msgid "Closing"
 msgstr "Lukning"
 
-#: kallithea/model/pull_request.py:73
+#: kallithea/model/pull_request.py:72
 #, python-format
 msgid ""
 "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s"
@@ -1567,278 +1565,278 @@
 "%(user)s vil have dig til at gennemgå pull-forespørgsel %(pr_nice_id)s: "
 "%(pr_title)s"
 
-#: kallithea/model/pull_request.py:209
+#: kallithea/model/pull_request.py:207
 msgid "Cannot create empty pull request"
 msgstr "Kan ikke oprette en tom pull-forespørgsel"
 
-#: kallithea/model/pull_request.py:217
+#: kallithea/model/pull_request.py:215
 #, python-format
 msgid ""
 "Cannot create pull request - criss cross merge detected, please merge a "
 "later %s revision to %s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:245 kallithea/model/pull_request.py:380
+#: kallithea/model/pull_request.py:243 kallithea/model/pull_request.py:378
 msgid "You are not authorized to create the pull request"
 msgstr ""
 
-#: kallithea/model/pull_request.py:339
+#: kallithea/model/pull_request.py:337
 msgid "Missing changesets since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:346
+#: kallithea/model/pull_request.py:344
 #, python-format
 msgid "New changesets on %s %s since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:353
+#: kallithea/model/pull_request.py:351
 msgid "Ancestor didn't change - diff since previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:360
+#: kallithea/model/pull_request.py:358
 #, python-format
 msgid ""
 "This iteration is based on another %s revision and there is no simple "
 "diff."
 msgstr ""
 
-#: kallithea/model/pull_request.py:362
+#: kallithea/model/pull_request.py:360
 #, python-format
 msgid "No changes found on %s %s since previous iteration."
 msgstr ""
 
-#: kallithea/model/pull_request.py:388
+#: kallithea/model/pull_request.py:386
 #, python-format
 msgid "Closed, next iteration: %s ."
 msgstr ""
 
-#: kallithea/model/scm.py:668
+#: kallithea/model/scm.py:655
 msgid "latest tip"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:56
+#: kallithea/model/ssh_key.py:57
 #, python-format
 msgid "SSH key %r is invalid: %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:68
+#: kallithea/model/ssh_key.py:69
 #, python-format
 msgid "SSH key %s is already used by %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:89
+#: kallithea/model/ssh_key.py:88
 #, fuzzy, python-format
 #| msgid "Changeset %s not found"
-msgid "SSH key %r not found"
+msgid "SSH key with fingerprint %r found"
 msgstr "Changeset %s ikke fundet"
 
-#: kallithea/model/user.py:186
+#: kallithea/model/user.py:180
 msgid "New user registration"
 msgstr ""
 
-#: kallithea/model/user.py:250
+#: kallithea/model/user.py:244
 msgid ""
 "You can't remove this user since it is crucial for the entire application"
 msgstr ""
 
-#: kallithea/model/user.py:255
+#: kallithea/model/user.py:249
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repositories and cannot be removed. Switch "
 "owners or remove those repositories: %s"
 msgstr ""
 
-#: kallithea/model/user.py:260
+#: kallithea/model/user.py:254
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repository groups and cannot be removed. Switch "
 "owners or remove those repository groups: %s"
 msgstr ""
 
-#: kallithea/model/user.py:267
+#: kallithea/model/user.py:261
 #, python-format
 msgid ""
 "User \"%s\" still owns %s user groups and cannot be removed. Switch "
 "owners or remove those user groups: %s"
 msgstr ""
 
-#: kallithea/model/user.py:361
+#: kallithea/model/user.py:355
 msgid "Password reset link"
 msgstr ""
 
-#: kallithea/model/user.py:408
+#: kallithea/model/user.py:402
 msgid "Password reset notification"
 msgstr ""
 
-#: kallithea/model/user.py:409
+#: kallithea/model/user.py:403
 #, python-format
 msgid ""
 "The password to your account %s has been changed using password reset "
 "form."
 msgstr ""
 
-#: kallithea/model/validators.py:52 kallithea/model/validators.py:53
+#: kallithea/model/validators.py:53 kallithea/model/validators.py:54
 msgid "Value cannot be an empty list"
 msgstr ""
 
-#: kallithea/model/validators.py:72
+#: kallithea/model/validators.py:73
 #, python-format
 msgid "Username \"%(username)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:74
+#: kallithea/model/validators.py:75
 #, python-format
 msgid "Username \"%(username)s\" cannot be used"
 msgstr ""
 
-#: kallithea/model/validators.py:76
+#: kallithea/model/validators.py:77
 msgid ""
 "Username may only contain alphanumeric characters underscores, periods or "
 "dashes and must begin with an alphanumeric character or underscore"
 msgstr ""
 
-#: kallithea/model/validators.py:103
+#: kallithea/model/validators.py:104
 msgid "The input is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:110
+#: kallithea/model/validators.py:111
 #, python-format
 msgid "Username %(username)s is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:131
-msgid "Invalid user group name"
-msgstr ""
-
 #: kallithea/model/validators.py:132
+msgid "Invalid user group name"
+msgstr ""
+
+#: kallithea/model/validators.py:133
 #, python-format
 msgid "User group \"%(usergroup)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:134
+#: kallithea/model/validators.py:135
 msgid ""
 "user group name may only contain alphanumeric characters underscores, "
 "periods or dashes and must begin with alphanumeric character"
 msgstr ""
 
-#: kallithea/model/validators.py:174
-msgid "Cannot assign this group as parent"
-msgstr ""
-
 #: kallithea/model/validators.py:175
+msgid "Cannot assign this group as parent"
+msgstr ""
+
+#: kallithea/model/validators.py:176
 #, python-format
 msgid "Group \"%(group_name)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:177
+#: kallithea/model/validators.py:178
 #, python-format
 msgid "Repository with name \"%(group_name)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:233
+#: kallithea/model/validators.py:230
 msgid "Invalid characters (non-ascii) in password"
 msgstr ""
 
-#: kallithea/model/validators.py:248
+#: kallithea/model/validators.py:245
 msgid "Invalid old password"
 msgstr ""
 
-#: kallithea/model/validators.py:264
+#: kallithea/model/validators.py:261
 msgid "Passwords do not match"
 msgstr ""
 
-#: kallithea/model/validators.py:279
+#: kallithea/model/validators.py:276
 msgid "Invalid username or password"
 msgstr ""
 
+#: kallithea/model/validators.py:310
+#, python-format
+msgid "Repository name %(repo)s is not allowed"
+msgstr ""
+
+#: kallithea/model/validators.py:312
+#, python-format
+msgid "Repository named %(repo)s already exists"
+msgstr ""
+
 #: kallithea/model/validators.py:313
 #, python-format
-msgid "Repository name %(repo)s is not allowed"
+msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
 msgstr ""
 
 #: kallithea/model/validators.py:315
 #, python-format
-msgid "Repository named %(repo)s already exists"
-msgstr ""
-
-#: kallithea/model/validators.py:316
-#, python-format
-msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
-msgstr ""
-
-#: kallithea/model/validators.py:318
-#, python-format
 msgid "Repository group with name \"%(repo)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:404
+#: kallithea/model/validators.py:401
 msgid "Invalid repository URL"
 msgstr ""
 
-#: kallithea/model/validators.py:405
+#: kallithea/model/validators.py:402
 msgid ""
 "Invalid repository URL. It must be a valid http, https, ssh, svn+http or "
 "svn+https URL"
 msgstr ""
 
-#: kallithea/model/validators.py:430
+#: kallithea/model/validators.py:427
 msgid "Fork has to be the same type as parent"
 msgstr ""
 
-#: kallithea/model/validators.py:445
+#: kallithea/model/validators.py:442
 msgid "You don't have permissions to create repository in this group"
 msgstr ""
 
-#: kallithea/model/validators.py:447
+#: kallithea/model/validators.py:444
 msgid "no permission to create repository in root location"
 msgstr ""
 
-#: kallithea/model/validators.py:497
+#: kallithea/model/validators.py:494
 msgid "You don't have permissions to create a group in this location"
 msgstr ""
 
-#: kallithea/model/validators.py:537
+#: kallithea/model/validators.py:534
 msgid "This username or user group name is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:630
+#: kallithea/model/validators.py:627
 msgid "This is not a valid path"
 msgstr ""
 
-#: kallithea/model/validators.py:647
+#: kallithea/model/validators.py:644
 msgid "This email address is already in use"
 msgstr ""
 
-#: kallithea/model/validators.py:667
+#: kallithea/model/validators.py:664
 #, python-format
 msgid "Email address \"%(email)s\" not found"
 msgstr ""
 
-#: kallithea/model/validators.py:704
+#: kallithea/model/validators.py:701
 msgid ""
 "The LDAP Login attribute of the CN must be specified - this is the name "
 "of the attribute that is equivalent to \"username\""
 msgstr ""
 
-#: kallithea/model/validators.py:716
+#: kallithea/model/validators.py:713
 msgid "Please enter a valid IPv4 or IPv6 address"
 msgstr ""
 
-#: kallithea/model/validators.py:717
+#: kallithea/model/validators.py:714
 #, python-format
 msgid ""
 "The network size (bits) must be within the range of 0-32 (not %(bits)r)"
 msgstr ""
 
-#: kallithea/model/validators.py:750
+#: kallithea/model/validators.py:747
 msgid "Key name can only consist of letters, underscore, dash or numbers"
 msgstr ""
 
-#: kallithea/model/validators.py:764
+#: kallithea/model/validators.py:761
 msgid "Filename cannot be inside a directory"
 msgstr ""
 
-#: kallithea/model/validators.py:780
+#: kallithea/model/validators.py:777
 #, python-format
 msgid "Plugins %(loaded)s and %(next_to_load)s both export the same name"
 msgstr ""
@@ -1898,7 +1896,7 @@
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:60
 #: kallithea/templates/email_templates/pull_request.html:37
 #: kallithea/templates/forks/fork.html:34
-#: kallithea/templates/index_base.html:58
+#: kallithea/templates/index_base.html:59
 #: kallithea/templates/pullrequests/pullrequest.html:33
 #: kallithea/templates/pullrequests/pullrequest_show.html:38
 #: kallithea/templates/pullrequests/pullrequest_show.html:59
@@ -1906,14 +1904,14 @@
 msgid "Description"
 msgstr ""
 
-#: kallithea/templates/index_base.html:60
+#: kallithea/templates/index_base.html:61
 msgid "Last Change"
 msgstr ""
 
 #: kallithea/templates/admin/my_account/my_account_repos.html:15
 #: kallithea/templates/admin/my_account/my_account_watched.html:15
 #: kallithea/templates/admin/repos/repos.html:41
-#: kallithea/templates/index_base.html:62
+#: kallithea/templates/index_base.html:63
 msgid "Tip"
 msgstr ""
 
@@ -1923,7 +1921,7 @@
 #: kallithea/templates/admin/repos/repos.html:42
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:8
 #: kallithea/templates/admin/user_groups/user_groups.html:42
-#: kallithea/templates/index_base.html:63
+#: kallithea/templates/index_base.html:64
 #: kallithea/templates/pullrequests/pullrequest_data.html:16
 #: kallithea/templates/pullrequests/pullrequest_show.html:124
 #: kallithea/templates/pullrequests/pullrequest_show.html:219
@@ -1947,7 +1945,7 @@
 #: kallithea/templates/admin/users/user_edit_profile.html:18
 #: kallithea/templates/admin/users/users.html:37
 #: kallithea/templates/base/base.html:364
-#: kallithea/templates/email_templates/registration.html:11
+#: kallithea/templates/email_templates/registration.html:12
 #: kallithea/templates/login.html:28 kallithea/templates/register.html:31
 msgid "Username"
 msgstr ""
@@ -2071,7 +2069,7 @@
 #: kallithea/templates/admin/settings/settings.html:31
 #: kallithea/templates/admin/users/user_add.html:62
 #: kallithea/templates/admin/users/user_edit_profile.html:25
-#: kallithea/templates/email_templates/registration.html:33
+#: kallithea/templates/email_templates/registration.html:34
 #: kallithea/templates/register.html:66
 msgid "Email"
 msgstr ""
@@ -2318,7 +2316,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/gists/index.html:51
-#: kallithea/templates/data_table/_dt_elements.html:78
+#: kallithea/templates/data_table/_dt_elements.html:84
 msgid "Created"
 msgstr ""
 
@@ -2402,13 +2400,13 @@
 #: kallithea/templates/admin/users/user_edit_ips.html:21
 #: kallithea/templates/changeset/changeset_file_comment.html:30
 #: kallithea/templates/changeset/changeset_file_comment.html:121
-#: kallithea/templates/data_table/_dt_elements.html:69
-#: kallithea/templates/data_table/_dt_elements.html:89
-#: kallithea/templates/data_table/_dt_elements.html:91
-#: kallithea/templates/data_table/_dt_elements.html:101
-#: kallithea/templates/data_table/_dt_elements.html:103
-#: kallithea/templates/data_table/_dt_elements.html:120
-#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:75
+#: kallithea/templates/data_table/_dt_elements.html:95
+#: kallithea/templates/data_table/_dt_elements.html:97
+#: kallithea/templates/data_table/_dt_elements.html:107
+#: kallithea/templates/data_table/_dt_elements.html:109
+#: kallithea/templates/data_table/_dt_elements.html:126
+#: kallithea/templates/data_table/_dt_elements.html:128
 #: kallithea/templates/files/files_source.html:35
 #: kallithea/templates/files/files_source.html:38
 #: kallithea/templates/files/files_source.html:41
@@ -2424,14 +2422,14 @@
 #: kallithea/templates/base/perms_summary.html:44
 #: kallithea/templates/base/perms_summary.html:81
 #: kallithea/templates/base/perms_summary.html:83
-#: kallithea/templates/data_table/_dt_elements.html:63
-#: kallithea/templates/data_table/_dt_elements.html:64
-#: kallithea/templates/data_table/_dt_elements.html:85
-#: kallithea/templates/data_table/_dt_elements.html:86
-#: kallithea/templates/data_table/_dt_elements.html:97
-#: kallithea/templates/data_table/_dt_elements.html:98
-#: kallithea/templates/data_table/_dt_elements.html:116
-#: kallithea/templates/data_table/_dt_elements.html:117
+#: kallithea/templates/data_table/_dt_elements.html:69
+#: kallithea/templates/data_table/_dt_elements.html:70
+#: kallithea/templates/data_table/_dt_elements.html:91
+#: kallithea/templates/data_table/_dt_elements.html:92
+#: kallithea/templates/data_table/_dt_elements.html:103
+#: kallithea/templates/data_table/_dt_elements.html:104
+#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:123
 #: kallithea/templates/files/diff_2way.html:56
 #: kallithea/templates/files/files_source.html:37
 #: kallithea/templates/files/files_source.html:40
@@ -2732,7 +2730,7 @@
 #: kallithea/templates/admin/permissions/permissions_globals.html:27
 #: kallithea/templates/admin/repos/repo_add_base.html:28
 #: kallithea/templates/admin/repos/repo_edit_settings.html:33
-#: kallithea/templates/data_table/_dt_elements.html:134
+#: kallithea/templates/data_table/_dt_elements.html:140
 #: kallithea/templates/forks/fork.html:42
 msgid "Repository group"
 msgstr ""
@@ -2753,7 +2751,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:40
-#: kallithea/templates/data_table/_dt_elements.html:141
+#: kallithea/templates/data_table/_dt_elements.html:147
 msgid "User group"
 msgstr ""
 
@@ -2926,7 +2924,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:121
+#: kallithea/templates/data_table/_dt_elements.html:127
 #, python-format
 msgid "Confirm to delete this group: %s with %s repository"
 msgid_plural "Confirm to delete this group: %s with %s repositories"
@@ -3089,14 +3087,10 @@
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit.html:37
-msgid "Caches"
+msgid "Remote"
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit.html:40
-msgid "Remote"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit.html:43
 #: kallithea/templates/summary/statistics.html:8
 #: kallithea/templates/summary/summary.html:169
 #: kallithea/templates/summary/summary.html:170
@@ -3134,7 +3128,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:46
-#: kallithea/templates/data_table/_dt_elements.html:68
+#: kallithea/templates/data_table/_dt_elements.html:74
 #, python-format
 msgid "Confirm to delete this repository: %s"
 msgstr ""
@@ -3165,43 +3159,14 @@
 "it or restore it."
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:4
-msgid "Invalidate Repository Cache"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:6
-msgid ""
-"Manually invalidate cache for this repository. On first access, the "
-"repository will be cached again."
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:9
-msgid "List of Cached Values"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:12
-msgid "Prefix"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:13
+#: kallithea/templates/admin/repos/repo_edit_fields.html:6
+msgid "Label"
+msgstr ""
+
 #: kallithea/templates/admin/repos/repo_edit_fields.html:7
 msgid "Key"
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:14
-#: kallithea/templates/admin/user_groups/user_group_add.html:40
-#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
-#: kallithea/templates/admin/user_groups/user_groups.html:41
-#: kallithea/templates/admin/users/user_add.html:69
-#: kallithea/templates/admin/users/user_edit_profile.html:74
-#: kallithea/templates/admin/users/users.html:42
-msgid "Active"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_fields.html:6
-msgid "Label"
-msgstr ""
-
 #: kallithea/templates/admin/repos/repo_edit_fields.html:20
 #, python-format
 msgid "Confirm to delete this field: %s"
@@ -3714,6 +3679,15 @@
 msgid "Short, optional description for this user group."
 msgstr ""
 
+#: kallithea/templates/admin/user_groups/user_group_add.html:40
+#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
+#: kallithea/templates/admin/user_groups/user_groups.html:41
+#: kallithea/templates/admin/users/user_add.html:69
+#: kallithea/templates/admin/users/user_edit_profile.html:74
+#: kallithea/templates/admin/users/users.html:42
+msgid "Active"
+msgstr ""
+
 #: kallithea/templates/admin/user_groups/user_group_edit.html:5
 #, python-format
 msgid "%s user group settings"
@@ -3735,7 +3709,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:19
-#: kallithea/templates/data_table/_dt_elements.html:102
+#: kallithea/templates/data_table/_dt_elements.html:108
 #, python-format
 msgid "Confirm to delete this user group: %s"
 msgstr ""
@@ -3809,7 +3783,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:90
+#: kallithea/templates/data_table/_dt_elements.html:96
 #, python-format
 msgid "Confirm to delete this user: %s"
 msgstr ""
@@ -3909,10 +3883,12 @@
 msgstr ""
 
 #: kallithea/templates/base/base.html:167
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Follow"
 msgstr ""
 
 #: kallithea/templates/base/base.html:168
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Unfollow"
 msgstr ""
 
@@ -4317,23 +4293,23 @@
 msgid "Merge"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:96
+#: kallithea/templates/changeset/changeset.html:95
 msgid "Grafted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:102
+#: kallithea/templates/changeset/changeset.html:100
 msgid "Transplanted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:108
+#: kallithea/templates/changeset/changeset.html:106
 msgid "Replaced by:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:122
+#: kallithea/templates/changeset/changeset.html:120
 msgid "Preceded by:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:139
+#: kallithea/templates/changeset/changeset.html:137
 #: kallithea/templates/compare/compare_diff.html:59
 #: kallithea/templates/pullrequests/pullrequest_show.html:290
 #, python-format
@@ -4342,7 +4318,7 @@
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/templates/changeset/changeset.html:141
+#: kallithea/templates/changeset/changeset.html:139
 #: kallithea/templates/compare/compare_diff.html:61
 #: kallithea/templates/pullrequests/pullrequest_show.html:292
 #, python-format
@@ -4351,8 +4327,8 @@
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
 #: kallithea/templates/pullrequests/pullrequest_show.html:333
@@ -4583,23 +4559,23 @@
 msgid "Repository creation in progress..."
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:42
+#: kallithea/templates/data_table/_dt_elements.html:48
 msgid "No changesets yet"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:48
-#: kallithea/templates/data_table/_dt_elements.html:50
+#: kallithea/templates/data_table/_dt_elements.html:54
+#: kallithea/templates/data_table/_dt_elements.html:56
 #, python-format
 msgid "Subscribe to %s rss feed"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:56
-#: kallithea/templates/data_table/_dt_elements.html:58
+#: kallithea/templates/data_table/_dt_elements.html:62
+#: kallithea/templates/data_table/_dt_elements.html:64
 #, python-format
 msgid "Subscribe to %s atom feed"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:76
+#: kallithea/templates/data_table/_dt_elements.html:82
 msgid "Creating"
 msgstr ""
 
@@ -4631,6 +4607,13 @@
 msgid "by"
 msgstr ""
 
+#: kallithea/templates/email_templates/changeset_comment.html:36
+#: kallithea/templates/email_templates/pull_request_comment.html:43
+#, fuzzy
+#| msgid "%s committed on %s"
+msgid "View Comment"
+msgstr "%s committed den %s"
+
 #: kallithea/templates/email_templates/comment.html:27
 msgid "Status change:"
 msgstr ""
@@ -4639,32 +4622,40 @@
 msgid "The pull request has been closed."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:9
+#: kallithea/templates/email_templates/default.html:4
+msgid "Message"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:4
+msgid "Password Reset Request"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:10
 #, python-format
 msgid "Hello %s"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:16
+#: kallithea/templates/email_templates/password_reset.html:17
 msgid "We have received a request to reset the password for your account."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:25
+#: kallithea/templates/email_templates/password_reset.html:26
 msgid ""
 "This account is however managed outside this system and the password "
 "cannot be changed here."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:28
+#: kallithea/templates/email_templates/password_reset.html:29
 msgid "To set a new password, click the following link"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:33
+#: kallithea/templates/email_templates/password_reset.html:34
 msgid ""
 "Should you not be able to use the link above, please type the following "
 "code into the password reset form"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:44
+#: kallithea/templates/email_templates/password_reset.html:45
 msgid ""
 "If it weren't you who requested the password reset, just disregard this "
 "message."
@@ -4697,6 +4688,12 @@
 msgid "to"
 msgstr ""
 
+#: kallithea/templates/email_templates/pull_request.html:85
+#, fuzzy
+#| msgid "Pull request %s"
+msgid "View Pull Request"
+msgstr "Pull-forespørgsel %s"
+
 #: kallithea/templates/email_templates/pull_request_comment.html:4
 #, python-format
 msgid "Mention in Comment on Pull Request %s \"%s\""
@@ -4712,10 +4709,18 @@
 msgid "Comment on Pull Request %s \"%s\""
 msgstr ""
 
-#: kallithea/templates/email_templates/registration.html:22
+#: kallithea/templates/email_templates/registration.html:5
+msgid "New User Registration"
+msgstr ""
+
+#: kallithea/templates/email_templates/registration.html:23
 msgid "Full Name"
 msgstr ""
 
+#: kallithea/templates/email_templates/registration.html:42
+msgid "View User Profile"
+msgstr ""
+
 #: kallithea/templates/files/diff_2way.html:15
 #, python-format
 msgid "%s File side-by-side diff"
@@ -5316,45 +5321,45 @@
 msgid "Stats gathered: "
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:87
-#: kallithea/templates/summary/summary.html:371
+#: kallithea/templates/summary/statistics.html:85
+#: kallithea/templates/summary/summary.html:369
 msgid "files"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:111
-#: kallithea/templates/summary/summary.html:401
+#: kallithea/templates/summary/statistics.html:109
+#: kallithea/templates/summary/summary.html:399
 msgid "Show more"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:405
+#: kallithea/templates/summary/statistics.html:395
 msgid "commits"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:406
+#: kallithea/templates/summary/statistics.html:396
 msgid "files added"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:407
+#: kallithea/templates/summary/statistics.html:397
 msgid "files changed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:408
+#: kallithea/templates/summary/statistics.html:398
 msgid "files removed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:410
+#: kallithea/templates/summary/statistics.html:400
 msgid "commit"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:411
+#: kallithea/templates/summary/statistics.html:401
 msgid "file added"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:412
+#: kallithea/templates/summary/statistics.html:402
 msgid "file changed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:413
+#: kallithea/templates/summary/statistics.html:403
 msgid "file removed"
 msgstr ""
 
@@ -5455,6 +5460,12 @@
 msgid "Download %s as %s"
 msgstr ""
 
+#~ msgid "Cache invalidation successful"
+#~ msgstr "Ugyldiggørelse af cache er succesfuld"
+
+#~ msgid "An error occurred during cache invalidation"
+#~ msgstr "Der opstod en fejl under cache ugyldiggørelse"
+
 #~ msgid "This repository has been locked by %s on %s"
 #~ msgstr "Dette repository er låst af %s den %s"
 
--- a/kallithea/i18n/de/LC_MESSAGES/kallithea.po	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/i18n/de/LC_MESSAGES/kallithea.po	Mon May 04 19:24:04 2020 +0200
@@ -4,7 +4,7 @@
 msgstr ""
 "Project-Id-Version: Kallithea 0.3\n"
 "Report-Msgid-Bugs-To: translations@kallithea-scm.org\n"
-"POT-Creation-Date: 2020-01-05 04:10+0100\n"
+"POT-Creation-Date: 2020-05-04 19:12+0200\n"
 "PO-Revision-Date: 2019-05-29 22:52+0000\n"
 "Last-Translator: ssantos <ssantos@web.de>\n"
 "Language-Team: German <https://hosted.weblate.org/projects/kallithea/"
@@ -17,14 +17,14 @@
 "X-Generator: Weblate 3.7-dev\n"
 
 #: kallithea/controllers/changelog.py:67
-#: kallithea/controllers/pullrequests.py:250 kallithea/lib/base.py:602
+#: kallithea/controllers/pullrequests.py:247 kallithea/lib/base.py:602
 msgid "There are no changesets yet"
 msgstr "Es gibt noch keine Änderungssätze"
 
 #: kallithea/controllers/admin/permissions.py:64
 #: kallithea/controllers/admin/permissions.py:68
 #: kallithea/controllers/admin/permissions.py:72
-#: kallithea/controllers/changelog.py:137
+#: kallithea/controllers/changelog.py:136
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:7
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:88
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:7
@@ -33,38 +33,38 @@
 msgid "None"
 msgstr "Keine"
 
-#: kallithea/controllers/changelog.py:140 kallithea/controllers/files.py:189
+#: kallithea/controllers/changelog.py:139 kallithea/controllers/files.py:189
 msgid "(closed)"
 msgstr "(geschlossen)"
 
-#: kallithea/controllers/changeset.py:81
+#: kallithea/controllers/changeset.py:82
 msgid "Show whitespace"
 msgstr "Zeige unsichtbare Zeichen"
 
-#: kallithea/controllers/changeset.py:88
-#: kallithea/controllers/changeset.py:95
+#: kallithea/controllers/changeset.py:89
+#: kallithea/controllers/changeset.py:96
 #: kallithea/templates/files/diff_2way.html:55
 msgid "Ignore whitespace"
 msgstr "Ignoriere unsichtbare Zeichen"
 
-#: kallithea/controllers/changeset.py:161
+#: kallithea/controllers/changeset.py:162
 #, python-format
 msgid "Increase diff context to %(num)s lines"
 msgstr "Erhöhe diff-Kontext auf %(num)s Zeilen"
 
-#: kallithea/controllers/changeset.py:201
+#: kallithea/controllers/changeset.py:202
 #, fuzzy
 #| msgid "No permission to change pull request status"
 msgid "No permission to change status"
 msgstr "Keine Berechtigung zum Ändern des Status des Pull Requests"
 
-#: kallithea/controllers/changeset.py:212
+#: kallithea/controllers/changeset.py:213
 #, python-format
 msgid "Successfully deleted pull request %s"
 msgstr "Pull-Request %s erfolgreich gelöscht"
 
 #: kallithea/controllers/changeset.py:319 kallithea/controllers/files.py:89
-#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:700
+#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:697
 msgid "Such revision does not exist for this repository"
 msgstr "Die angegebene Version existiert nicht in diesem Repository"
 
@@ -82,52 +82,52 @@
 "Ohne einen gemeinsamen Vorfahren ist ein Vergleich der Repositories nicht "
 "möglich"
 
-#: kallithea/controllers/compare.py:246
+#: kallithea/controllers/compare.py:247
 msgid "Cannot show empty diff"
 msgstr "Kann leeren diff nicht anzeigen"
 
-#: kallithea/controllers/compare.py:248
+#: kallithea/controllers/compare.py:249
 msgid "No ancestor found for merge diff"
 msgstr "Es konnte kein Vorfahre für den merge diff gefunden werden"
 
-#: kallithea/controllers/compare.py:252
+#: kallithea/controllers/compare.py:253
 msgid "Multiple merge ancestors found for merge compare"
 msgstr "Es wurden mehrere merge Vorfahren für den merge Vergleich gefunden"
 
-#: kallithea/controllers/compare.py:268
+#: kallithea/controllers/compare.py:269
 msgid "Cannot compare repositories without using common ancestor"
 msgstr ""
 "Ohne einen gemeinsamen Vorfahren ist ein Vergleich der Repositories nicht "
 "möglich"
 
-#: kallithea/controllers/error.py:71
+#: kallithea/controllers/error.py:67
 msgid "No response"
 msgstr "Keine Rückmeldung"
 
-#: kallithea/controllers/error.py:72
+#: kallithea/controllers/error.py:68
 msgid "Unknown error"
 msgstr "Unbekannter Fehler"
 
-#: kallithea/controllers/error.py:85
+#: kallithea/controllers/error.py:81
 msgid ""
 "The request could not be understood by the server due to malformed syntax."
 msgstr ""
 "Die Anfrage konnte wegen ungültiger Syntax vom Server nicht ausgewertet "
 "werden."
 
-#: kallithea/controllers/error.py:88
+#: kallithea/controllers/error.py:84
 msgid "Unauthorized access to resource"
 msgstr "Unauthorisierter Zugang zur Ressource"
 
-#: kallithea/controllers/error.py:90
+#: kallithea/controllers/error.py:86
 msgid "You don't have permission to view this page"
 msgstr "Du hast keine Rechte, um diese Seite zu betrachten"
 
-#: kallithea/controllers/error.py:92
+#: kallithea/controllers/error.py:88
 msgid "The resource could not be found"
 msgstr "Die Ressource konnte nicht gefunden werden"
 
-#: kallithea/controllers/error.py:94
+#: kallithea/controllers/error.py:90
 msgid ""
 "The server encountered an unexpected condition which prevented it from "
 "fulfilling the request."
@@ -135,14 +135,14 @@
 "Aufgrund einer unerwarteten Gegebenheit konnte der Server diese Anfrage "
 "nicht vollenden."
 
-#: kallithea/controllers/feed.py:63
+#: kallithea/controllers/feed.py:59
 #, python-format
 msgid "%s committed on %s"
 msgstr "%s committed am %s"
 
-#: kallithea/controllers/feed.py:88
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/controllers/feed.py:84
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/compare/compare_diff.html:95
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
@@ -150,12 +150,12 @@
 msgid "Changeset was too big and was cut off..."
 msgstr "Der Änderungssatz war zu groß und wurde abgeschnitten..."
 
-#: kallithea/controllers/feed.py:111 kallithea/controllers/feed.py:140
+#: kallithea/controllers/feed.py:107
 #, python-format
 msgid "%s %s feed"
 msgstr "%s %s Feed"
 
-#: kallithea/controllers/feed.py:113 kallithea/controllers/feed.py:142
+#: kallithea/controllers/feed.py:109
 #, python-format
 msgid "Changes on %s repository"
 msgstr "Änderungen im %s Repository"
@@ -175,107 +175,107 @@
 msgid "%s at %s"
 msgstr "%s auf %s"
 
-#: kallithea/controllers/files.py:296
+#: kallithea/controllers/files.py:295
 #, fuzzy
 msgid "You can only delete files with revision being a valid branch"
 msgstr ""
 "Dateien können nur gelöscht werden, deren Revision ein gültiger Branch ist"
 
-#: kallithea/controllers/files.py:307
+#: kallithea/controllers/files.py:306
 #, python-format
 msgid "Deleted file %s via Kallithea"
 msgstr "Datei %s via Kallithea gelöscht"
 
-#: kallithea/controllers/files.py:331
+#: kallithea/controllers/files.py:330
 #, python-format
 msgid "Successfully deleted file %s"
 msgstr "Datei %s erfolgreich gelöscht"
 
-#: kallithea/controllers/files.py:335 kallithea/controllers/files.py:394
-#: kallithea/controllers/files.py:469
+#: kallithea/controllers/files.py:334 kallithea/controllers/files.py:392
+#: kallithea/controllers/files.py:467
 msgid "Error occurred during commit"
 msgstr "Während des Commits trat ein Fehler auf"
 
-#: kallithea/controllers/files.py:350
+#: kallithea/controllers/files.py:349
 #, fuzzy
 msgid "You can only edit files with revision being a valid branch"
 msgstr ""
 "Dateien können nur editiert werden, deren Revision ein gültiger Branch ist"
 
-#: kallithea/controllers/files.py:364
+#: kallithea/controllers/files.py:363
 #, python-format
 msgid "Edited file %s via Kallithea"
 msgstr "Datei %s via Kallithea editiert"
 
-#: kallithea/controllers/files.py:380
+#: kallithea/controllers/files.py:378
 msgid "No changes"
 msgstr "Keine Änderungen"
 
-#: kallithea/controllers/files.py:390 kallithea/controllers/files.py:458
+#: kallithea/controllers/files.py:388 kallithea/controllers/files.py:456
 #, python-format
 msgid "Successfully committed to %s"
 msgstr "Der Commit zu %s war erfolgreich"
 
-#: kallithea/controllers/files.py:409
+#: kallithea/controllers/files.py:407
 msgid "Added file via Kallithea"
 msgstr "Datei via Kallithea hinzugefügt"
 
-#: kallithea/controllers/files.py:430
+#: kallithea/controllers/files.py:428
 msgid "No content"
 msgstr "Kein Inhalt"
 
-#: kallithea/controllers/files.py:434
+#: kallithea/controllers/files.py:432
 msgid "No filename"
 msgstr "Kein Dateiname"
 
-#: kallithea/controllers/files.py:461
+#: kallithea/controllers/files.py:459
 msgid "Location must be relative path and must not contain .. in path"
 msgstr "Der Ort muss ein relativer Pfad sein und darf nicht .. enthalten"
 
-#: kallithea/controllers/files.py:493
+#: kallithea/controllers/files.py:491
 msgid "Downloads disabled"
 msgstr "Downloads gesperrt"
 
-#: kallithea/controllers/files.py:504
+#: kallithea/controllers/files.py:502
 #, python-format
 msgid "Unknown revision %s"
 msgstr "Unbekannte Revision %s"
 
-#: kallithea/controllers/files.py:506
+#: kallithea/controllers/files.py:504
 msgid "Empty repository"
 msgstr "Leeres Repository"
 
-#: kallithea/controllers/files.py:508
+#: kallithea/controllers/files.py:506
 msgid "Unknown archive type"
 msgstr "Unbekannter Archivtyp"
 
-#: kallithea/controllers/files.py:729
+#: kallithea/controllers/files.py:726
 #: kallithea/templates/changeset/changeset_range.html:9
 #: kallithea/templates/email_templates/pull_request.html:64
 #: kallithea/templates/pullrequests/pullrequest.html:84
 msgid "Changesets"
 msgstr "Änderungssätze"
 
-#: kallithea/controllers/files.py:730
-#: kallithea/controllers/pullrequests.py:182 kallithea/model/scm.py:676
+#: kallithea/controllers/files.py:727
+#: kallithea/controllers/pullrequests.py:174 kallithea/model/scm.py:663
 msgid "Branches"
 msgstr "Entwicklungszweige"
 
-#: kallithea/controllers/files.py:731
-#: kallithea/controllers/pullrequests.py:183 kallithea/model/scm.py:687
+#: kallithea/controllers/files.py:728
+#: kallithea/controllers/pullrequests.py:175 kallithea/model/scm.py:674
 msgid "Tags"
 msgstr "Tags"
 
-#: kallithea/controllers/forks.py:174
+#: kallithea/controllers/forks.py:175
 #, python-format
 msgid "An error occurred during repository forking %s"
 msgstr "Während des Forkens des Repositorys trat ein Fehler auf: %s"
 
-#: kallithea/controllers/home.py:79
+#: kallithea/controllers/home.py:77
 msgid "Groups"
 msgstr "Gruppen"
 
-#: kallithea/controllers/home.py:89
+#: kallithea/controllers/home.py:87
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:90
 #: kallithea/templates/admin/repos/repo_add.html:12
 #: kallithea/templates/admin/repos/repo_add.html:16
@@ -287,48 +287,54 @@
 msgid "Repositories"
 msgstr "Repositories"
 
-#: kallithea/controllers/home.py:122
+#: kallithea/controllers/home.py:119
 #: kallithea/templates/files/files_add.html:32
 #: kallithea/templates/files/files_delete.html:23
 #: kallithea/templates/files/files_edit.html:32
 msgid "Branch"
 msgstr "Zweig"
 
-#: kallithea/controllers/home.py:128
+#: kallithea/controllers/home.py:125
 msgid "Closed Branches"
 msgstr "Geschlossene Branches"
 
-#: kallithea/controllers/home.py:134
+#: kallithea/controllers/home.py:131
 msgid "Tag"
 msgstr "Marke"
 
-#: kallithea/controllers/home.py:140
+#: kallithea/controllers/home.py:137
 msgid "Bookmark"
 msgstr "Lesezeichen"
 
-#: kallithea/controllers/journal.py:112 kallithea/controllers/journal.py:154
+#: kallithea/controllers/journal.py:146 kallithea/controllers/journal.py:157
 #: kallithea/templates/journal/public_journal.html:4
 #: kallithea/templates/journal/public_journal.html:18
 msgid "Public Journal"
 msgstr "Öffentliches Logbuch"
 
-#: kallithea/controllers/journal.py:116 kallithea/controllers/journal.py:158
+#: kallithea/controllers/journal.py:150 kallithea/controllers/journal.py:161
 #: kallithea/templates/base/base.html:290
 #: kallithea/templates/journal/journal.html:5
 #: kallithea/templates/journal/journal.html:13
 msgid "Journal"
 msgstr "Logbuch"
 
-#: kallithea/controllers/login.py:140 kallithea/controllers/login.py:185
+#: kallithea/controllers/login.py:109
+#, fuzzy
+#| msgid "Authentication"
+msgid "Authentication failed."
+msgstr "Authentifizierung"
+
+#: kallithea/controllers/login.py:142 kallithea/controllers/login.py:187
 msgid "Bad captcha"
 msgstr "Falsches Captcha"
 
-#: kallithea/controllers/login.py:146
+#: kallithea/controllers/login.py:148
 #, python-format
 msgid "You have successfully registered with %s"
 msgstr "Sie haben sich erfolgreich bei %s registriert"
 
-#: kallithea/controllers/login.py:190
+#: kallithea/controllers/login.py:192
 msgid "A password reset confirmation code has been sent"
 msgstr "Ihr Link um das Passwort zurückzusetzen wurde versendet"
 
@@ -341,240 +347,240 @@
 msgid "Successfully updated password"
 msgstr "Erfolgreich Kennwort geändert"
 
-#: kallithea/controllers/pullrequests.py:68
+#: kallithea/controllers/pullrequests.py:67
 #, python-format
 msgid "Invalid reviewer \"%s\" specified"
 msgstr "Ungültigen Begutachter \"%s\" angegeben"
 
-#: kallithea/controllers/pullrequests.py:131
+#: kallithea/controllers/pullrequests.py:123
 #, python-format
 msgid "%s (closed)"
 msgstr "%s (geschlossen)"
 
-#: kallithea/controllers/pullrequests.py:158
+#: kallithea/controllers/pullrequests.py:150
 #: kallithea/templates/changeset/changeset.html:12
 msgid "Changeset"
 msgstr "Änderungssatz"
 
-#: kallithea/controllers/pullrequests.py:179
+#: kallithea/controllers/pullrequests.py:171
 msgid "Special"
 msgstr "Spezial"
 
-#: kallithea/controllers/pullrequests.py:180
+#: kallithea/controllers/pullrequests.py:172
 msgid "Peer branches"
 msgstr "Branches anderer"
 
-#: kallithea/controllers/pullrequests.py:181 kallithea/model/scm.py:682
+#: kallithea/controllers/pullrequests.py:173 kallithea/model/scm.py:669
 msgid "Bookmarks"
 msgstr "Lesezeichen"
 
-#: kallithea/controllers/pullrequests.py:318
+#: kallithea/controllers/pullrequests.py:315
 #, python-format
 msgid "Error creating pull request: %s"
 msgstr "Fehler beim Erstellen des Pull-Requests: %s"
 
-#: kallithea/controllers/pullrequests.py:345
-#: kallithea/controllers/pullrequests.py:368
+#: kallithea/controllers/pullrequests.py:342
+#: kallithea/controllers/pullrequests.py:365
 msgid "Error occurred while creating pull request"
 msgstr "Während des Erstellens des Pull Requests trat ein Fehler auf"
 
-#: kallithea/controllers/pullrequests.py:350
+#: kallithea/controllers/pullrequests.py:347
 msgid "Successfully opened new pull request"
 msgstr "Es wurde erfolgreich ein neuer Pullrequest eröffnet"
 
-#: kallithea/controllers/pullrequests.py:373
+#: kallithea/controllers/pullrequests.py:370
 #, fuzzy
 #| msgid "Pull request update created"
 msgid "New pull request iteration created"
 msgstr "Pull Request Update erstellt"
 
-#: kallithea/controllers/pullrequests.py:401
+#: kallithea/controllers/pullrequests.py:398
 #, python-format
 msgid "Meanwhile, the following reviewers have been added: %s"
 msgstr "Es wurden inzwischen folgende Begutachter hinzugefügt: %s"
 
-#: kallithea/controllers/pullrequests.py:405
+#: kallithea/controllers/pullrequests.py:402
 #, python-format
 msgid "Meanwhile, the following reviewers have been removed: %s"
 msgstr "Es wurden inzwischen folgende Begutachter entfernt: %s"
 
-#: kallithea/controllers/pullrequests.py:421
-#: kallithea/model/pull_request.py:232
+#: kallithea/controllers/pullrequests.py:418
+#: kallithea/model/pull_request.py:230
 msgid "No description"
 msgstr "Keine Beschreibung"
 
-#: kallithea/controllers/pullrequests.py:430
+#: kallithea/controllers/pullrequests.py:427
 msgid "Pull request updated"
 msgstr "Pull Request aktualisiert"
 
-#: kallithea/controllers/pullrequests.py:443
+#: kallithea/controllers/pullrequests.py:440
 msgid "Successfully deleted pull request"
 msgstr "Erfolgreich Pull-Request gelöscht"
 
-#: kallithea/controllers/pullrequests.py:479
+#: kallithea/controllers/pullrequests.py:476
 #, python-format
 msgid "Revision %s not found in %s"
 msgstr "Die Revision %s konnte in %s nicht gefunden werden"
 
-#: kallithea/controllers/pullrequests.py:506
+#: kallithea/controllers/pullrequests.py:504
 #, fuzzy, python-format
 #| msgid "No changesets found for updating this pull request."
 msgid "Error: changesets not found when displaying pull request from %s."
 msgstr "Keine Changesets gefunden, um den Pull Request zu aktualisieren."
 
-#: kallithea/controllers/pullrequests.py:520
+#: kallithea/controllers/pullrequests.py:518
 #, python-format
 msgid "This pull request has already been merged to %s."
 msgstr "Dieser Pull Request wurde bereits in %s integriert."
 
-#: kallithea/controllers/pullrequests.py:522
+#: kallithea/controllers/pullrequests.py:520
 msgid "This pull request has been closed and can not be updated."
 msgstr ""
 "Dieser Pull Request wurde geschlossen und kann daher nicht aktualisiert "
 "werden."
 
-#: kallithea/controllers/pullrequests.py:546
+#: kallithea/controllers/pullrequests.py:539
 #, fuzzy, python-format
 #| msgid "The following changes are available on %s:"
 msgid "The following additional changes are available on %s:"
 msgstr "Die folgenden Änderungen sind verfügbar unter %s:"
 
-#: kallithea/controllers/pullrequests.py:548
-#: kallithea/controllers/pullrequests.py:552
+#: kallithea/controllers/pullrequests.py:541
+#: kallithea/controllers/pullrequests.py:545
 #, fuzzy
 #| msgid "No changesets found for updating this pull request."
 msgid "No additional changesets found for iterating on this pull request."
 msgstr "Keine Changesets gefunden, um den Pull Request zu aktualisieren."
 
-#: kallithea/controllers/pullrequests.py:560
+#: kallithea/controllers/pullrequests.py:553
 #, python-format
 msgid "Note: Branch %s has another head: %s."
 msgstr "Hinweis: Branch %s hat einen anderen Head: %s."
 
-#: kallithea/controllers/pullrequests.py:567
+#: kallithea/controllers/pullrequests.py:560
 #, fuzzy
 #| msgid "Git pull requests don't support updates yet."
 msgid "Git pull requests don't support iterating yet."
 msgstr "Git Pull Request unterstützen bisher keine Updates."
 
-#: kallithea/controllers/pullrequests.py:569
+#: kallithea/controllers/pullrequests.py:562
 #, fuzzy, python-format
 #| msgid "No changesets found for updating this pull request."
 msgid ""
 "Error: some changesets not found when displaying pull request from %s."
 msgstr "Keine Changesets gefunden, um den Pull Request zu aktualisieren."
 
-#: kallithea/controllers/pullrequests.py:593
+#: kallithea/controllers/pullrequests.py:586
 msgid "The diff can't be shown - the PR revisions could not be found."
 msgstr ""
 "Der diff kann nicht angezeigt werden. Die Pull Request Revisionen konnten "
 "nicht gefunden werden."
 
-#: kallithea/controllers/search.py:136
+#: kallithea/controllers/search.py:132
 msgid "Invalid search query. Try quoting it."
 msgstr ""
 "Ungültige Suchanfrage. Versuchen sie es in Anführungzeichen zu setzen."
 
-#: kallithea/controllers/search.py:140
+#: kallithea/controllers/search.py:136
 msgid "The server has no search index."
 msgstr "Der Server hat keinen Suchindex."
 
-#: kallithea/controllers/search.py:143
+#: kallithea/controllers/search.py:139
 msgid "An error occurred during search operation."
 msgstr "Während der Suchoperation trat ein Fehler auf."
 
-#: kallithea/controllers/summary.py:168
-#: kallithea/templates/summary/summary.html:412
+#: kallithea/controllers/summary.py:169
+#: kallithea/templates/summary/summary.html:410
 msgid "No data ready yet"
 msgstr "Es stehen noch keine Daten zur Verfügung"
 
-#: kallithea/controllers/summary.py:171
+#: kallithea/controllers/summary.py:172
 #: kallithea/templates/summary/summary.html:97
 msgid "Statistics are disabled for this repository"
 msgstr "Statistiken sind deaktiviert für dieses Repository"
 
-#: kallithea/controllers/admin/auth_settings.py:137
+#: kallithea/controllers/admin/auth_settings.py:136
 msgid "Auth settings updated successfully"
 msgstr "Anmeldeeinstellungen erfolgreich geändert"
 
-#: kallithea/controllers/admin/auth_settings.py:148
+#: kallithea/controllers/admin/auth_settings.py:147
 msgid "error occurred during update of auth settings"
 msgstr "Fehler bei der Änderung der Anmeldeeinstellungen aufgetreten"
 
-#: kallithea/controllers/admin/defaults.py:75
+#: kallithea/controllers/admin/defaults.py:74
 msgid "Default settings updated successfully"
 msgstr "Standardeinstellungen erfolgreich geupdated"
 
-#: kallithea/controllers/admin/defaults.py:90
+#: kallithea/controllers/admin/defaults.py:89
 msgid "Error occurred during update of defaults"
 msgstr "Ein Fehler trat beim updaten der Standardeinstellungen auf"
 
 #: kallithea/controllers/admin/gists.py:59
 #: kallithea/controllers/admin/my_account.py:232
-#: kallithea/controllers/admin/users.py:248
+#: kallithea/controllers/admin/users.py:246
 msgid "Forever"
 msgstr "Immer"
 
 #: kallithea/controllers/admin/gists.py:60
 #: kallithea/controllers/admin/my_account.py:233
-#: kallithea/controllers/admin/users.py:249
+#: kallithea/controllers/admin/users.py:247
 msgid "5 minutes"
 msgstr "5 Minuten"
 
 #: kallithea/controllers/admin/gists.py:61
 #: kallithea/controllers/admin/my_account.py:234
-#: kallithea/controllers/admin/users.py:250
+#: kallithea/controllers/admin/users.py:248
 msgid "1 hour"
 msgstr "1 Stunde"
 
 #: kallithea/controllers/admin/gists.py:62
 #: kallithea/controllers/admin/my_account.py:235
-#: kallithea/controllers/admin/users.py:251
+#: kallithea/controllers/admin/users.py:249
 msgid "1 day"
 msgstr "1 Tag"
 
 #: kallithea/controllers/admin/gists.py:63
 #: kallithea/controllers/admin/my_account.py:236
-#: kallithea/controllers/admin/users.py:252
+#: kallithea/controllers/admin/users.py:250
 msgid "1 month"
 msgstr "1 Monat"
 
 #: kallithea/controllers/admin/gists.py:67
 #: kallithea/controllers/admin/my_account.py:238
-#: kallithea/controllers/admin/users.py:254
+#: kallithea/controllers/admin/users.py:252
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:65
 #: kallithea/templates/admin/users/user_edit_api_keys.html:65
 msgid "Lifetime"
 msgstr "Lebenszeit"
 
-#: kallithea/controllers/admin/gists.py:142
+#: kallithea/controllers/admin/gists.py:148
 msgid "Error occurred during gist creation"
 msgstr "Ein fehler trat auf bei der Erstellung des gist"
 
-#: kallithea/controllers/admin/gists.py:158
+#: kallithea/controllers/admin/gists.py:164
 #, python-format
 msgid "Deleted gist %s"
 msgstr "gist %s gelöscht"
 
-#: kallithea/controllers/admin/gists.py:198
+#: kallithea/controllers/admin/gists.py:207
 msgid "Unmodified"
 msgstr "Ungeändert"
 
-#: kallithea/controllers/admin/gists.py:228
+#: kallithea/controllers/admin/gists.py:237
 msgid "Successfully updated gist content"
 msgstr "Erfolgreich Kerninhalt aktualisiert"
 
-#: kallithea/controllers/admin/gists.py:233
+#: kallithea/controllers/admin/gists.py:242
 msgid "Successfully updated gist data"
 msgstr "Erfolgreich Kerndaten aktualisiert"
 
-#: kallithea/controllers/admin/gists.py:236
+#: kallithea/controllers/admin/gists.py:245
 #, python-format
 msgid "Error occurred during update of gist %s"
 msgstr "Fehler beim Aktualisieren der Kerndaten %s"
 
-#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:211
-#: kallithea/model/user.py:232
+#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:205
+#: kallithea/model/user.py:226
 msgid "You can't edit this user since it's crucial for entire application"
 msgstr ""
 "Sie können diesen Benutzer nicht editieren, da er von entscheidender "
@@ -585,7 +591,7 @@
 msgstr "Ihr Account wurde erfolgreich aktualisiert"
 
 #: kallithea/controllers/admin/my_account.py:134
-#: kallithea/controllers/admin/users.py:181
+#: kallithea/controllers/admin/users.py:179
 #, python-format
 msgid "Error occurred during update of user %s"
 msgstr "Fehler beim Aktualisieren der Benutzer %s"
@@ -595,45 +601,45 @@
 msgstr "Fehler bei der Änderung des Kennworts"
 
 #: kallithea/controllers/admin/my_account.py:209
-#: kallithea/controllers/admin/users.py:367
+#: kallithea/controllers/admin/users.py:365
 #, python-format
 msgid "Added email %s to user"
 msgstr "Die EMail Addresse %s wurde zum Benutzer hinzugefügt"
 
 #: kallithea/controllers/admin/my_account.py:215
-#: kallithea/controllers/admin/users.py:373
+#: kallithea/controllers/admin/users.py:371
 msgid "An error occurred during email saving"
 msgstr "Währen der Speicherung der EMail Addresse trat ein Fehler auf"
 
 #: kallithea/controllers/admin/my_account.py:224
-#: kallithea/controllers/admin/users.py:383
+#: kallithea/controllers/admin/users.py:381
 msgid "Removed email from user"
 msgstr "Die EMail Addresse wurde vom Benutzer entfernt"
 
 #: kallithea/controllers/admin/my_account.py:248
-#: kallithea/controllers/admin/users.py:271
+#: kallithea/controllers/admin/users.py:269
 msgid "API key successfully created"
 msgstr "API Key wurde erfolgreich erstellt"
 
 #: kallithea/controllers/admin/my_account.py:257
-#: kallithea/controllers/admin/users.py:281
+#: kallithea/controllers/admin/users.py:279
 msgid "API key successfully reset"
 msgstr "API-Schlüssel erfolgreich zurückgesetzt"
 
 #: kallithea/controllers/admin/my_account.py:261
-#: kallithea/controllers/admin/users.py:285
+#: kallithea/controllers/admin/users.py:283
 msgid "API key successfully deleted"
 msgstr "API-Schlüssel erfolgreich gelöscht"
 
 #: kallithea/controllers/admin/my_account.py:281
-#: kallithea/controllers/admin/users.py:456
+#: kallithea/controllers/admin/users.py:454
 #, fuzzy, python-format
 #| msgid "API key successfully created"
 msgid "SSH key %s successfully added"
 msgstr "API Key wurde erfolgreich erstellt"
 
 #: kallithea/controllers/admin/my_account.py:293
-#: kallithea/controllers/admin/users.py:470
+#: kallithea/controllers/admin/users.py:468
 #, fuzzy
 #| msgid "API key successfully deleted"
 msgid "SSH key successfully deleted"
@@ -711,11 +717,11 @@
 msgid "Allowed with automatic account activation"
 msgstr "Erlaubt mit automatischer Kontoaktivierung"
 
-#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1673
+#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1578
 msgid "Manual activation of external account"
 msgstr "Manuelle Aktivierung externen Kontos"
 
-#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1674
+#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1579
 msgid "Automatic activation of external account"
 msgstr "Automatische Aktivierung externen Kontos"
 
@@ -737,190 +743,182 @@
 msgid "Error occurred during update of permissions"
 msgstr "Fehler bei der Änderung der globalen Berechtigungen"
 
-#: kallithea/controllers/admin/repo_groups.py:172
+#: kallithea/controllers/admin/repo_groups.py:165
 #, python-format
 msgid "Error occurred during creation of repository group %s"
 msgstr "Fehler bei der Erstellung der Repositoriumsgruppe %s"
 
-#: kallithea/controllers/admin/repo_groups.py:177
+#: kallithea/controllers/admin/repo_groups.py:172
 #, python-format
 msgid "Created repository group %s"
 msgstr "Repositoriumsgruppe %s erstellt"
 
-#: kallithea/controllers/admin/repo_groups.py:224
+#: kallithea/controllers/admin/repo_groups.py:219
 #, python-format
 msgid "Updated repository group %s"
 msgstr "Repositoriumsgruppe %s aktualisiert"
 
-#: kallithea/controllers/admin/repo_groups.py:240
+#: kallithea/controllers/admin/repo_groups.py:235
 #, python-format
 msgid "Error occurred during update of repository group %s"
 msgstr "Fehler bei der Aktualisierung der Repositoriumsgruppe %s"
 
-#: kallithea/controllers/admin/repo_groups.py:250
+#: kallithea/controllers/admin/repo_groups.py:245
 #, python-format
 msgid "This group contains %s repositories and cannot be deleted"
 msgstr "Die Gruppe enthält %s Repositorys und kann nicht gelöscht werden"
 
-#: kallithea/controllers/admin/repo_groups.py:257
+#: kallithea/controllers/admin/repo_groups.py:252
 #, python-format
 msgid "This group contains %s subgroups and cannot be deleted"
 msgstr "Diese Gruppe enthält %s Untergruppen und kann nicht gelöscht werden"
 
-#: kallithea/controllers/admin/repo_groups.py:263
+#: kallithea/controllers/admin/repo_groups.py:258
 #, python-format
 msgid "Removed repository group %s"
 msgstr "Repositoriumsgruppe %s entfernt"
 
-#: kallithea/controllers/admin/repo_groups.py:268
+#: kallithea/controllers/admin/repo_groups.py:263
 #, python-format
 msgid "Error occurred during deletion of repository group %s"
 msgstr "Fehler beim Löschen der Repositoriumsgruppe %s"
 
-#: kallithea/controllers/admin/repo_groups.py:352
-#: kallithea/controllers/admin/repo_groups.py:382
-#: kallithea/controllers/admin/user_groups.py:294
+#: kallithea/controllers/admin/repo_groups.py:347
+#: kallithea/controllers/admin/repo_groups.py:377
+#: kallithea/controllers/admin/user_groups.py:290
 msgid "Cannot revoke permission for yourself as admin"
 msgstr "Als Administrator kann man sich keine Berechtigungen entziehen"
 
-#: kallithea/controllers/admin/repo_groups.py:367
+#: kallithea/controllers/admin/repo_groups.py:362
 msgid "Repository group permissions updated"
 msgstr "Berechtigungen der Repositoriumsgruppe aktualisiert"
 
-#: kallithea/controllers/admin/repo_groups.py:399
-#: kallithea/controllers/admin/repos.py:358
-#: kallithea/controllers/admin/user_groups.py:306
+#: kallithea/controllers/admin/repo_groups.py:394
+#: kallithea/controllers/admin/repos.py:357
+#: kallithea/controllers/admin/user_groups.py:302
 msgid "An error occurred during revoking of permission"
 msgstr "Fehler beim Entzug der Berechtigungen"
 
-#: kallithea/controllers/admin/repos.py:136
+#: kallithea/controllers/admin/repos.py:137
 #, python-format
 msgid "Error creating repository %s"
 msgstr "Fehler beim Erstellen des Repositoriums %s"
 
-#: kallithea/controllers/admin/repos.py:194
+#: kallithea/controllers/admin/repos.py:193
 #, python-format
 msgid "Created repository %s from %s"
 msgstr "Repositorium %s von %s erstellt"
 
-#: kallithea/controllers/admin/repos.py:203
+#: kallithea/controllers/admin/repos.py:202
 #, python-format
 msgid "Forked repository %s as %s"
 msgstr "Aufgespaltenes Repositorium %s zu %s"
 
-#: kallithea/controllers/admin/repos.py:206
+#: kallithea/controllers/admin/repos.py:205
 #, python-format
 msgid "Created repository %s"
 msgstr "Repositorium erzeugt %s"
 
-#: kallithea/controllers/admin/repos.py:235
+#: kallithea/controllers/admin/repos.py:234
 #, python-format
 msgid "Repository %s updated successfully"
 msgstr "Repository %s wurde erfolgreich aktualisiert"
 
-#: kallithea/controllers/admin/repos.py:255
+#: kallithea/controllers/admin/repos.py:254
 #, python-format
 msgid "Error occurred during update of repository %s"
 msgstr "Fehler bei der Aktualisierung des Repositoriums %s"
 
-#: kallithea/controllers/admin/repos.py:273
+#: kallithea/controllers/admin/repos.py:272
 #, python-format
 msgid "Detached %s forks"
 msgstr "%s Spaltung abgetrennt"
 
-#: kallithea/controllers/admin/repos.py:276
+#: kallithea/controllers/admin/repos.py:275
 #, python-format
 msgid "Deleted %s forks"
 msgstr "%s Spaltung gelöscht"
 
-#: kallithea/controllers/admin/repos.py:281
+#: kallithea/controllers/admin/repos.py:280
 #, python-format
 msgid "Deleted repository %s"
 msgstr "Repositorium %s gelöscht"
 
-#: kallithea/controllers/admin/repos.py:284
+#: kallithea/controllers/admin/repos.py:283
 #, python-format
 msgid "Cannot delete repository %s which still has forks"
 msgstr "%s konnte nicht gelöscht werden, da es noch Forks besitzt"
 
-#: kallithea/controllers/admin/repos.py:289
+#: kallithea/controllers/admin/repos.py:288
 #, python-format
 msgid "An error occurred during deletion of %s"
 msgstr "Beim Löschen von %s trat ein Fehler auf"
 
-#: kallithea/controllers/admin/repos.py:329
+#: kallithea/controllers/admin/repos.py:328
 msgid "Repository permissions updated"
 msgstr "Repositoriumsberechtigungen aktualisiert"
 
-#: kallithea/controllers/admin/repos.py:388
+#: kallithea/controllers/admin/repos.py:387
 #, python-format
 msgid "Field validation error: %s"
 msgstr "Feldvalidierung fehlgeschlagen: %s"
 
-#: kallithea/controllers/admin/repos.py:391
+#: kallithea/controllers/admin/repos.py:390
 #, fuzzy, python-format
 #| msgid "An error occurred during creation of field"
 msgid "An error occurred during creation of field: %r"
 msgstr "Fehler während der Erzeugung des Feldes"
 
-#: kallithea/controllers/admin/repos.py:402
+#: kallithea/controllers/admin/repos.py:401
 msgid "An error occurred during removal of field"
 msgstr "Fehler beim Entfernen des Feldes"
 
-#: kallithea/controllers/admin/repos.py:416
+#: kallithea/controllers/admin/repos.py:415
 msgid "-- Not a fork --"
 msgstr "-- Keine Abspaltung --"
 
-#: kallithea/controllers/admin/repos.py:448
+#: kallithea/controllers/admin/repos.py:447
 msgid "Updated repository visibility in public journal"
 msgstr "Sichtbarkeit des Repositorys im Öffentlichen Logbuch aktualisiert"
 
-#: kallithea/controllers/admin/repos.py:452
+#: kallithea/controllers/admin/repos.py:451
 msgid "An error occurred during setting this repository in public journal"
 msgstr ""
 "Es trat ein Fehler während der Aktualisierung der Sicherbarkeit dieses "
 "Repositorys im Öffentlichen Logbuch auf"
 
-#: kallithea/controllers/admin/repos.py:468
+#: kallithea/controllers/admin/repos.py:467
 msgid "Nothing"
 msgstr "Nichts"
 
-#: kallithea/controllers/admin/repos.py:470
+#: kallithea/controllers/admin/repos.py:469
 #, python-format
 msgid "Marked repository %s as fork of %s"
 msgstr "Markiere Repository %s als Abzweig von Repository %s"
 
-#: kallithea/controllers/admin/repos.py:477
+#: kallithea/controllers/admin/repos.py:476
 msgid "An error occurred during this operation"
 msgstr "Während dieser operation trat ein Fehler auf"
 
-#: kallithea/controllers/admin/repos.py:490
-msgid "Cache invalidation successful"
-msgstr "Cache Entfernung war erfolgreich"
-
-#: kallithea/controllers/admin/repos.py:494
-msgid "An error occurred during cache invalidation"
-msgstr "Währen der Cache Invalidierung trat ein Fehler auf"
-
-#: kallithea/controllers/admin/repos.py:507
+#: kallithea/controllers/admin/repos.py:488
 msgid "Pulled from remote location"
 msgstr "Von entferntem Ort übertragen"
 
-#: kallithea/controllers/admin/repos.py:510
+#: kallithea/controllers/admin/repos.py:491
 msgid "An error occurred during pull from remote location"
 msgstr ""
 "Es trat ein Fehler auf während das Repository von einem Entfernten "
 "Speicherort übertragen wurde"
 
-#: kallithea/controllers/admin/repos.py:541
+#: kallithea/controllers/admin/repos.py:522
 msgid "An error occurred during deletion of repository stats"
 msgstr "Während des löschens der Repository Statistiken trat ein Fehler auf"
 
-#: kallithea/controllers/admin/settings.py:131
+#: kallithea/controllers/admin/settings.py:132
 msgid "Updated VCS settings"
 msgstr "VCS-Einstellungen aktualisiert"
 
-#: kallithea/controllers/admin/settings.py:135 kallithea/lib/utils.py:237
+#: kallithea/controllers/admin/settings.py:136
 msgid ""
 "Unable to activate hgsubversion support. The \"hgsubversion\" library is "
 "missing"
@@ -928,170 +926,170 @@
 "hgsubversion-Unterstützung konnte nicht aktiviert werden. Die "
 "\"hgsubversion\"-Bibliothek fehlt"
 
-#: kallithea/controllers/admin/settings.py:141
-#: kallithea/controllers/admin/settings.py:233
+#: kallithea/controllers/admin/settings.py:142
+#: kallithea/controllers/admin/settings.py:234
 msgid "Error occurred while updating application settings"
 msgstr ""
 "Ein Fehler ist während der Aktualisierung der Applikationseinstellungen "
 "aufgetreten"
 
-#: kallithea/controllers/admin/settings.py:176
+#: kallithea/controllers/admin/settings.py:177
 #, python-format
 msgid "Repositories successfully rescanned. Added: %s. Removed: %s."
 msgstr ""
 "Die Repositories wurden erfolgreich überprüft. Hinzugefügt: %s. Entfernt: "
 "%s."
 
-#: kallithea/controllers/admin/settings.py:188
+#: kallithea/controllers/admin/settings.py:189
 #, fuzzy, python-format
 #| msgid "Invalidate Repository Cache"
 msgid "Invalidated %s repositories"
 msgstr "Ungültiger Repositorycache"
 
-#: kallithea/controllers/admin/settings.py:229
+#: kallithea/controllers/admin/settings.py:230
 msgid "Updated application settings"
 msgstr "Anwendungseinstellungen aktualisiert"
 
-#: kallithea/controllers/admin/settings.py:283
+#: kallithea/controllers/admin/settings.py:284
 msgid "Updated visualisation settings"
 msgstr "Visualisierungseinstellungen aktualisiert"
 
-#: kallithea/controllers/admin/settings.py:288
+#: kallithea/controllers/admin/settings.py:289
 msgid "Error occurred during updating visualisation settings"
 msgstr ""
 "Es ist ein Fehler während der Aktualisierung der Layouteinstellung "
 "aufgetreten"
 
-#: kallithea/controllers/admin/settings.py:312
+#: kallithea/controllers/admin/settings.py:313
 msgid "Please enter email address"
 msgstr "Bitte gebe eine E-Mailadresse an"
 
-#: kallithea/controllers/admin/settings.py:327
+#: kallithea/controllers/admin/settings.py:328
 msgid "Send email task created"
 msgstr "Task zum Versenden von E-Mails erstellt"
 
-#: kallithea/controllers/admin/settings.py:355
+#: kallithea/controllers/admin/settings.py:356
 #, fuzzy
 #| msgid "No data ready yet"
 msgid "Hook already exists"
 msgstr "Es stehen noch keine Daten zur Verfügung"
 
-#: kallithea/controllers/admin/settings.py:357
+#: kallithea/controllers/admin/settings.py:358
 msgid "Builtin hooks are read-only. Please use another hook name."
 msgstr ""
 "Die eingebauten Hooks sind schreibgeschützt. Bitte verwenden Sie einen "
 "anderen Hook-Namen."
 
-#: kallithea/controllers/admin/settings.py:360
+#: kallithea/controllers/admin/settings.py:361
 msgid "Added new hook"
 msgstr "Neuer Hook hinzugefügt"
 
-#: kallithea/controllers/admin/settings.py:376
+#: kallithea/controllers/admin/settings.py:377
 msgid "Updated hooks"
 msgstr "Die Hooks wurden aktutalisiert"
 
-#: kallithea/controllers/admin/settings.py:380
+#: kallithea/controllers/admin/settings.py:381
 msgid "Error occurred during hook creation"
 msgstr "Während der Erzeugung des Hooks ist ein Fehler aufgetreten"
 
-#: kallithea/controllers/admin/settings.py:404
+#: kallithea/controllers/admin/settings.py:405
 msgid "Whoosh reindex task scheduled"
 msgstr "Whoosh Reindizierungs Aufgabe wurde zur Ausführung geplant"
 
-#: kallithea/controllers/admin/user_groups.py:138
+#: kallithea/controllers/admin/user_groups.py:134
 #, python-format
 msgid "Created user group %s"
 msgstr "Nutzergruppe %s erstellt"
 
-#: kallithea/controllers/admin/user_groups.py:151
+#: kallithea/controllers/admin/user_groups.py:147
 #, python-format
 msgid "Error occurred during creation of user group %s"
 msgstr ""
 "Es ist ein Fehler während der Erstellung der Nutzergruppe %s aufgetreten"
 
-#: kallithea/controllers/admin/user_groups.py:179
+#: kallithea/controllers/admin/user_groups.py:175
 #, python-format
 msgid "Updated user group %s"
 msgstr "Aktualisierte Nutzergruppe %s"
 
-#: kallithea/controllers/admin/user_groups.py:201
+#: kallithea/controllers/admin/user_groups.py:197
 #, python-format
 msgid "Error occurred during update of user group %s"
 msgstr ""
 "Während des Updates der Benutzergruppe %s ist ein Fehler aufgetreten"
 
-#: kallithea/controllers/admin/user_groups.py:212
+#: kallithea/controllers/admin/user_groups.py:208
 msgid "Successfully deleted user group"
 msgstr "Die Nutzergruppe wurde erfolgreich entfernt"
 
-#: kallithea/controllers/admin/user_groups.py:217
+#: kallithea/controllers/admin/user_groups.py:213
 msgid "An error occurred during deletion of user group"
 msgstr "Während des Löschens der Benutzergruppe ist ein Fehler aufgetreten"
 
-#: kallithea/controllers/admin/user_groups.py:273
+#: kallithea/controllers/admin/user_groups.py:269
 msgid "Target group cannot be the same"
 msgstr "Zielgruppe kann nicht die gleiche Gruppe sein"
 
-#: kallithea/controllers/admin/user_groups.py:279
+#: kallithea/controllers/admin/user_groups.py:275
 msgid "User group permissions updated"
 msgstr "Berechtigungen der Benutzergruppe wurden aktualisiert"
 
+#: kallithea/controllers/admin/user_groups.py:384
+#: kallithea/controllers/admin/users.py:336
+msgid "Updated permissions"
+msgstr "Berechtigungen wurden aktualisiert"
+
 #: kallithea/controllers/admin/user_groups.py:388
-#: kallithea/controllers/admin/users.py:338
-msgid "Updated permissions"
-msgstr "Berechtigungen wurden aktualisiert"
-
-#: kallithea/controllers/admin/user_groups.py:392
-#: kallithea/controllers/admin/users.py:342
+#: kallithea/controllers/admin/users.py:340
 msgid "An error occurred during permissions saving"
 msgstr ""
 "Es ist ein Fehler während des Speicherns der Berechtigungen aufgetreten"
 
-#: kallithea/controllers/admin/users.py:123
+#: kallithea/controllers/admin/users.py:121
 #, python-format
 msgid "Created user %s"
 msgstr "Nutzer %s erstellt"
 
-#: kallithea/controllers/admin/users.py:138
+#: kallithea/controllers/admin/users.py:136
 #, python-format
 msgid "Error occurred during creation of user %s"
 msgstr "Während des Erstellens des Benutzers %s ist ein Fehler aufgetreten"
 
-#: kallithea/controllers/admin/users.py:162
+#: kallithea/controllers/admin/users.py:160
 msgid "User updated successfully"
 msgstr "Der Benutzer wurde erfolgreich aktualisiert"
 
-#: kallithea/controllers/admin/users.py:190
+#: kallithea/controllers/admin/users.py:188
 msgid "Successfully deleted user"
 msgstr "Der Nutzer wurde erfolgreich gelöscht"
 
-#: kallithea/controllers/admin/users.py:195
+#: kallithea/controllers/admin/users.py:193
 msgid "An error occurred during deletion of user"
 msgstr "Während der Löschen des Benutzers trat ein Fehler auf"
 
-#: kallithea/controllers/admin/users.py:203
+#: kallithea/controllers/admin/users.py:201
 msgid "The default user cannot be edited"
 msgstr "Der Standard-Benutzer kann nicht bearbeitet werden"
 
-#: kallithea/controllers/admin/users.py:409
+#: kallithea/controllers/admin/users.py:407
 #, python-format
 msgid "Added IP address %s to user whitelist"
 msgstr "Die IP-Adresse %s wurde zur Nutzerwhitelist hinzugefügt"
 
-#: kallithea/controllers/admin/users.py:415
+#: kallithea/controllers/admin/users.py:413
 msgid "An error occurred while adding IP address"
 msgstr "Während des Speicherns der IP-Adresse ist ein Fehler aufgetreten"
 
-#: kallithea/controllers/admin/users.py:427
+#: kallithea/controllers/admin/users.py:425
 msgid "Removed IP address from user whitelist"
 msgstr "IP-Adresse wurde von der Nutzerwhitelist entfernt"
 
-#: kallithea/lib/auth.py:684
+#: kallithea/lib/auth.py:634
 msgid "You need to be a registered user to perform this action"
 msgstr ""
 "Sie müssen ein Registrierter Nutzer sein um diese Aktion durchzuführen"
 
-#: kallithea/lib/auth.py:712
+#: kallithea/lib/auth.py:662
 msgid "You need to be signed in to view this page"
 msgstr "Sie müssen sich anmelden um diese Seite aufzurufen"
 
@@ -1128,171 +1126,171 @@
 "Der Änderungssatz war zu groß und wurde abgeschnitten, benutzen sie das "
 "Diff Menü um die Unterschiede anzuzeigen"
 
-#: kallithea/lib/diffs.py:224
+#: kallithea/lib/diffs.py:223
 msgid "No changes detected"
 msgstr "Keine Änderungen erkannt"
 
-#: kallithea/lib/helpers.py:653
+#: kallithea/lib/helpers.py:670
 #, python-format
 msgid "Deleted branch: %s"
 msgstr "Branch %s gelöscht"
 
-#: kallithea/lib/helpers.py:655
+#: kallithea/lib/helpers.py:672
 #, python-format
 msgid "Created tag: %s"
 msgstr "Tag %s erstellt"
 
-#: kallithea/lib/helpers.py:666
+#: kallithea/lib/helpers.py:683
 #, fuzzy, python-format
 #| msgid "Changeset not found"
 msgid "Changeset %s not found"
 msgstr "Änderungssatz nicht gefunden"
 
-#: kallithea/lib/helpers.py:715
+#: kallithea/lib/helpers.py:732
 #, python-format
 msgid "Show all combined changesets %s->%s"
 msgstr "Zeige alle Kombinierten Änderungensätze %s->%s"
 
-#: kallithea/lib/helpers.py:721
+#: kallithea/lib/helpers.py:738
 msgid "Compare view"
 msgstr "Vergleichsansicht"
 
-#: kallithea/lib/helpers.py:740
+#: kallithea/lib/helpers.py:757
 msgid "and"
 msgstr "und"
 
-#: kallithea/lib/helpers.py:741
+#: kallithea/lib/helpers.py:758
 #, python-format
 msgid "%s more"
 msgstr "%s mehr"
 
-#: kallithea/lib/helpers.py:742
+#: kallithea/lib/helpers.py:759
 #: kallithea/templates/changelog/changelog.html:43
 msgid "revisions"
 msgstr "revisionen"
 
-#: kallithea/lib/helpers.py:766
+#: kallithea/lib/helpers.py:783
 #, python-format
 msgid "Fork name %s"
 msgstr "Fork Name %s"
 
-#: kallithea/lib/helpers.py:787
+#: kallithea/lib/helpers.py:804
 #, python-format
 msgid "Pull request %s"
 msgstr "Pull Request %s"
 
-#: kallithea/lib/helpers.py:797
+#: kallithea/lib/helpers.py:814
 msgid "[deleted] repository"
 msgstr "[gelöscht] Repository"
 
-#: kallithea/lib/helpers.py:799 kallithea/lib/helpers.py:811
+#: kallithea/lib/helpers.py:816 kallithea/lib/helpers.py:828
 msgid "[created] repository"
 msgstr "[erstellt] Repository"
 
-#: kallithea/lib/helpers.py:801
+#: kallithea/lib/helpers.py:818
 msgid "[created] repository as fork"
 msgstr "[erstellt] Repository als Fork"
 
-#: kallithea/lib/helpers.py:803 kallithea/lib/helpers.py:813
+#: kallithea/lib/helpers.py:820 kallithea/lib/helpers.py:830
 msgid "[forked] repository"
 msgstr "[forked] Repository"
 
-#: kallithea/lib/helpers.py:805 kallithea/lib/helpers.py:815
+#: kallithea/lib/helpers.py:822 kallithea/lib/helpers.py:832
 msgid "[updated] repository"
 msgstr "[aktualisiert] Repository"
 
-#: kallithea/lib/helpers.py:807
+#: kallithea/lib/helpers.py:824
 msgid "[downloaded] archive from repository"
 msgstr "Archiv von Repository [heruntergeladen]"
 
-#: kallithea/lib/helpers.py:809
+#: kallithea/lib/helpers.py:826
 msgid "[delete] repository"
 msgstr "Repository [gelöscht]"
 
-#: kallithea/lib/helpers.py:817
+#: kallithea/lib/helpers.py:834
 msgid "[created] user"
 msgstr "Benutzer [erstellt]"
 
-#: kallithea/lib/helpers.py:819
+#: kallithea/lib/helpers.py:836
 msgid "[updated] user"
 msgstr "Benutzer [akutalisiert]"
 
-#: kallithea/lib/helpers.py:821
+#: kallithea/lib/helpers.py:838
 msgid "[created] user group"
 msgstr "Benutzergruppe [erstellt]"
 
-#: kallithea/lib/helpers.py:823
+#: kallithea/lib/helpers.py:840
 msgid "[updated] user group"
 msgstr "Benutzergruppe [aktualisiert]"
 
-#: kallithea/lib/helpers.py:825
+#: kallithea/lib/helpers.py:842
 msgid "[commented] on revision in repository"
 msgstr "Revision [kommentiert] in Repository"
 
-#: kallithea/lib/helpers.py:827
+#: kallithea/lib/helpers.py:844
 msgid "[commented] on pull request for"
 msgstr "Pull Request [kommentiert] für"
 
-#: kallithea/lib/helpers.py:829
+#: kallithea/lib/helpers.py:846
 msgid "[closed] pull request for"
 msgstr "Pull Request [geschlossen] für"
 
-#: kallithea/lib/helpers.py:831
+#: kallithea/lib/helpers.py:848
 msgid "[pushed] into"
 msgstr "[Pushed] in"
 
-#: kallithea/lib/helpers.py:833
+#: kallithea/lib/helpers.py:850
 msgid "[committed via Kallithea] into repository"
 msgstr "[via Kallithea] in Repository [committed]"
 
-#: kallithea/lib/helpers.py:835
+#: kallithea/lib/helpers.py:852
 msgid "[pulled from remote] into repository"
 msgstr "[Pulled von Remote] in Repository"
 
-#: kallithea/lib/helpers.py:837
+#: kallithea/lib/helpers.py:854
 msgid "[pulled] from"
 msgstr "[Pulled] von"
 
-#: kallithea/lib/helpers.py:839
+#: kallithea/lib/helpers.py:856
 msgid "[started following] repository"
 msgstr "[Following gestartet] für Repository"
 
-#: kallithea/lib/helpers.py:841
+#: kallithea/lib/helpers.py:858
 msgid "[stopped following] repository"
 msgstr "[Following gestoppt] für Repository"
 
-#: kallithea/lib/helpers.py:961
+#: kallithea/lib/helpers.py:975
 #, python-format
 msgid " and %s more"
 msgstr " und %s weitere"
 
-#: kallithea/lib/helpers.py:965
+#: kallithea/lib/helpers.py:979
 #: kallithea/templates/compare/compare_diff.html:69
 #: kallithea/templates/pullrequests/pullrequest_show.html:297
 msgid "No files"
 msgstr "Keine Dateien"
 
-#: kallithea/lib/helpers.py:990
+#: kallithea/lib/helpers.py:1004
 msgid "new file"
 msgstr "neue Datei"
 
-#: kallithea/lib/helpers.py:993
+#: kallithea/lib/helpers.py:1007
 msgid "mod"
 msgstr "mod"
 
-#: kallithea/lib/helpers.py:996
+#: kallithea/lib/helpers.py:1010
 msgid "del"
 msgstr "entf"
 
-#: kallithea/lib/helpers.py:999
+#: kallithea/lib/helpers.py:1013
 msgid "rename"
 msgstr "umbenennen"
 
-#: kallithea/lib/helpers.py:1004
+#: kallithea/lib/helpers.py:1018
 msgid "chmod"
 msgstr "chmod"
 
-#: kallithea/lib/helpers.py:1297
+#: kallithea/lib/helpers.py:1323
 #, python-format
 msgid ""
 "%s repository is not mapped to db perhaps it was created or renamed from "
@@ -1303,96 +1301,98 @@
 "es im Dateisystem erstellt oder umbenannt. Bitte starten sie die "
 "Applikation erneut um die Repositories neu zu Indizieren"
 
-#: kallithea/lib/ssh.py:73
+#: kallithea/lib/ssh.py:75
 msgid "SSH key is missing"
 msgstr ""
 
-#: kallithea/lib/ssh.py:77
-msgid "Incorrect SSH key - it must have both a key type and a base64 part"
-msgstr ""
-
-#: kallithea/lib/ssh.py:81
+#: kallithea/lib/ssh.py:79
+msgid ""
+"Incorrect SSH key - it must have both a key type and a base64 part, like "
+"'ssh-rsa ASRNeaZu4FA...xlJp='"
+msgstr ""
+
+#: kallithea/lib/ssh.py:83
 msgid "Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'"
 msgstr ""
 
-#: kallithea/lib/ssh.py:84
+#: kallithea/lib/ssh.py:86
 #, python-format
 msgid "Incorrect SSH key - unexpected characters in base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:89
+#: kallithea/lib/ssh.py:91
 #, python-format
 msgid "Incorrect SSH key - failed to decode base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:92
+#: kallithea/lib/ssh.py:94
 #, python-format
 msgid "Incorrect SSH key - base64 part is not %r as claimed but %r"
 msgstr ""
 
-#: kallithea/lib/utils2.py:334
+#: kallithea/lib/utils2.py:253
 #, python-format
 msgid "%d year"
 msgid_plural "%d years"
 msgstr[0] "%d Jahr"
 msgstr[1] "%d Jahre"
 
-#: kallithea/lib/utils2.py:335
+#: kallithea/lib/utils2.py:254
 #, python-format
 msgid "%d month"
 msgid_plural "%d months"
 msgstr[0] "%d Monat"
 msgstr[1] "%d Monate"
 
-#: kallithea/lib/utils2.py:336
+#: kallithea/lib/utils2.py:255
 #, python-format
 msgid "%d day"
 msgid_plural "%d days"
 msgstr[0] "%d Tag"
 msgstr[1] "%d Tage"
 
-#: kallithea/lib/utils2.py:337
+#: kallithea/lib/utils2.py:256
 #, python-format
 msgid "%d hour"
 msgid_plural "%d hours"
 msgstr[0] "%d Stunde"
 msgstr[1] "%d Stunden"
 
-#: kallithea/lib/utils2.py:338
+#: kallithea/lib/utils2.py:257
 #, python-format
 msgid "%d minute"
 msgid_plural "%d minutes"
 msgstr[0] "%d Minute"
 msgstr[1] "%d Minuten"
 
-#: kallithea/lib/utils2.py:339
+#: kallithea/lib/utils2.py:258
 #, python-format
 msgid "%d second"
 msgid_plural "%d seconds"
 msgstr[0] "%d Sekunde"
 msgstr[1] "%d Sekunden"
 
-#: kallithea/lib/utils2.py:355
+#: kallithea/lib/utils2.py:274
 #, python-format
 msgid "in %s"
 msgstr "in %s"
 
-#: kallithea/lib/utils2.py:357
+#: kallithea/lib/utils2.py:276
 #, python-format
 msgid "%s ago"
 msgstr "vor %s"
 
-#: kallithea/lib/utils2.py:359
+#: kallithea/lib/utils2.py:278
 #, python-format
 msgid "in %s and %s"
 msgstr "in %s und %s"
 
-#: kallithea/lib/utils2.py:362
+#: kallithea/lib/utils2.py:281
 #, python-format
 msgid "%s and %s ago"
 msgstr "%s und %s her"
 
-#: kallithea/lib/utils2.py:365
+#: kallithea/lib/utils2.py:284
 msgid "just now"
 msgstr "jetzt gerade"
 
@@ -1401,140 +1401,140 @@
 msgid "on line %s"
 msgstr "in Zeile %s"
 
-#: kallithea/model/comment.py:221 kallithea/model/pull_request.py:114
+#: kallithea/model/comment.py:219 kallithea/model/pull_request.py:112
 msgid "[Mention]"
 msgstr "[Mention]"
 
-#: kallithea/model/db.py:1496
+#: kallithea/model/db.py:1411
 msgid "top level"
 msgstr "höchste Ebene"
 
-#: kallithea/model/db.py:1637
+#: kallithea/model/db.py:1542
 msgid "Kallithea Administrator"
 msgstr "Kallithea Administrator"
 
-#: kallithea/model/db.py:1639
+#: kallithea/model/db.py:1544
 msgid "Default user has no access to new repositories"
 msgstr "Der Standard-Benutzer hat keinen Zugriff auf neue Repositories"
 
-#: kallithea/model/db.py:1640
+#: kallithea/model/db.py:1545
 msgid "Default user has read access to new repositories"
 msgstr "Der Standard-Benutzer hat Leserechte auf neuen Repositories"
 
-#: kallithea/model/db.py:1641
+#: kallithea/model/db.py:1546
 msgid "Default user has write access to new repositories"
 msgstr "Der Standard-Benutzer hat Schreibrechte auf neuen Repositories"
 
-#: kallithea/model/db.py:1642
+#: kallithea/model/db.py:1547
 msgid "Default user has admin access to new repositories"
 msgstr "Der Standard-Benutzer hat Admin-Rechte auf neuen Repositories"
 
-#: kallithea/model/db.py:1644
+#: kallithea/model/db.py:1549
 msgid "Default user has no access to new repository groups"
 msgstr ""
 "Der Standard-Benutzer hat keinen Zugriff auf neue Repository-Gruppen"
 
-#: kallithea/model/db.py:1645
+#: kallithea/model/db.py:1550
 msgid "Default user has read access to new repository groups"
 msgstr "Der Standard-Benutzer hat Leserechte auf neuen Repository-Gruppen"
 
-#: kallithea/model/db.py:1646
+#: kallithea/model/db.py:1551
 msgid "Default user has write access to new repository groups"
 msgstr "Der Standard-Benutzer Schreibrechte auf neuen Repository-Gruppen"
 
-#: kallithea/model/db.py:1647
+#: kallithea/model/db.py:1552
 msgid "Default user has admin access to new repository groups"
 msgstr "Der Standard-Benutzer Admin-Rechte auf neuen Repository-Gruppen"
 
-#: kallithea/model/db.py:1649
+#: kallithea/model/db.py:1554
 msgid "Default user has no access to new user groups"
 msgstr "Der Standard-Benutzer hat keinen Zugriff auf neue Benutzer-Gruppen"
 
-#: kallithea/model/db.py:1650
+#: kallithea/model/db.py:1555
 msgid "Default user has read access to new user groups"
 msgstr "Der Standard-Benutzer hat Leserechte auf neuen Benutzer-Gruppen"
 
-#: kallithea/model/db.py:1651
+#: kallithea/model/db.py:1556
 msgid "Default user has write access to new user groups"
 msgstr "Der Standard-Benutzer hat Schreibrechte auf neuen Benutzer-Gruppen"
 
-#: kallithea/model/db.py:1652
+#: kallithea/model/db.py:1557
 msgid "Default user has admin access to new user groups"
 msgstr "Der Standard-Benutzer hat Admin-Rechte auf neuen Benutzer-Gruppen"
 
-#: kallithea/model/db.py:1654
+#: kallithea/model/db.py:1559
 msgid "Only admins can create repository groups"
 msgstr "Nur Admins können Repository-Gruppen erstellen"
 
-#: kallithea/model/db.py:1655
+#: kallithea/model/db.py:1560
 msgid "Non-admins can create repository groups"
 msgstr "Nicht-Admins können Repository-Gruppen erstellen"
 
-#: kallithea/model/db.py:1657
+#: kallithea/model/db.py:1562
 msgid "Only admins can create user groups"
 msgstr "Nur Admins können Benutzer-Gruppen erstellen"
 
-#: kallithea/model/db.py:1658
+#: kallithea/model/db.py:1563
 msgid "Non-admins can create user groups"
 msgstr "Nicht-Admins können Benutzer-Gruppen erstellen"
 
-#: kallithea/model/db.py:1660
+#: kallithea/model/db.py:1565
 msgid "Only admins can create top level repositories"
 msgstr "Nur Admins können Repositories auf oberster Ebene erstellen"
 
-#: kallithea/model/db.py:1661
+#: kallithea/model/db.py:1566
 msgid "Non-admins can create top level repositories"
 msgstr "Nicht-Admins können Repositories oberster Ebene erstellen"
 
-#: kallithea/model/db.py:1663
+#: kallithea/model/db.py:1568
 msgid ""
 "Repository creation enabled with write permission to a repository group"
 msgstr ""
 "Erstellung von Repositories mit Schreibzugriff für Repositorygruppe "
 "aktiviert"
 
-#: kallithea/model/db.py:1664
+#: kallithea/model/db.py:1569
 msgid ""
 "Repository creation disabled with write permission to a repository group"
 msgstr ""
 "Erstellung von Repositories mit Schreibzugriff für Repositorygruppe "
 "deaktiviert"
 
-#: kallithea/model/db.py:1666
+#: kallithea/model/db.py:1571
 msgid "Only admins can fork repositories"
 msgstr "Nur Admins können Repositories forken"
 
-#: kallithea/model/db.py:1667
+#: kallithea/model/db.py:1572
 msgid "Non-admins can fork repositories"
 msgstr "Nicht-Admins können Repositorys forken"
 
-#: kallithea/model/db.py:1669
+#: kallithea/model/db.py:1574
 msgid "Registration disabled"
 msgstr "Registrierung deaktiviert"
 
-#: kallithea/model/db.py:1670
+#: kallithea/model/db.py:1575
 msgid "User registration with manual account activation"
 msgstr "Benutzerregistrierung mit manueller Kontoaktivierung"
 
-#: kallithea/model/db.py:1671
+#: kallithea/model/db.py:1576
 msgid "User registration with automatic account activation"
 msgstr "Benutzerregistrierung mit automatischer Kontoaktivierung"
 
-#: kallithea/model/db.py:2206
+#: kallithea/model/db.py:1992
 msgid "Not reviewed"
 msgstr "Nicht Begutachtet"
 
-#: kallithea/model/db.py:2207
+#: kallithea/model/db.py:1993
 msgid "Under review"
 msgstr "In Begutachtung"
 
-#: kallithea/model/db.py:2208
+#: kallithea/model/db.py:1994
 #, fuzzy
 #| msgid "Approved"
 msgid "Not approved"
 msgstr "Akzeptiert"
 
-#: kallithea/model/db.py:2209
+#: kallithea/model/db.py:1995
 msgid "Approved"
 msgstr "Akzeptiert"
 
@@ -1560,7 +1560,7 @@
 msgid "Name must not contain only digits"
 msgstr "Name darf nicht nur Ziffern enthalten"
 
-#: kallithea/model/notification.py:164
+#: kallithea/model/notification.py:162
 #, fuzzy, python-format
 #| msgid "[Comment] %(repo_name)s changeset %(short_id)s on %(branch)s"
 msgid ""
@@ -1570,43 +1570,43 @@
 "Kommentar für %(repo_name)s Changeset %(short_id)s in %(branch)s erstellt "
 "von %(comment_username)s"
 
-#: kallithea/model/notification.py:167
+#: kallithea/model/notification.py:165
 #, python-format
 msgid "New user %(new_username)s registered"
 msgstr "Neuer Benutzer %(new_username)s registriert"
 
-#: kallithea/model/notification.py:169
+#: kallithea/model/notification.py:167
 #, python-format
 msgid ""
 "[Review] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:170
+#: kallithea/model/notification.py:168
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:183
+#: kallithea/model/notification.py:188
 msgid "Closing"
 msgstr "Schließen"
 
-#: kallithea/model/pull_request.py:73
+#: kallithea/model/pull_request.py:72
 #, fuzzy, python-format
 msgid ""
 "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s"
 msgstr ""
 "%(user)s möchte ein Review des Pull Request #%(pr_id)s: %(pr_title)s"
 
-#: kallithea/model/pull_request.py:209
+#: kallithea/model/pull_request.py:207
 #, fuzzy
 #| msgid "Error creating pull request: %s"
 msgid "Cannot create empty pull request"
 msgstr "Fehler beim Erstellen des Pull-Requests: %s"
 
-#: kallithea/model/pull_request.py:217
+#: kallithea/model/pull_request.py:215
 #, python-format
 msgid ""
 "Cannot create pull request - criss cross merge detected, please merge a "
@@ -1615,29 +1615,29 @@
 "Pull-Request kann nicht erstellt werden - Criss Cross Merge erkannt, "
 "bitte eine spätere %s-Revision in %s zusammenführen."
 
-#: kallithea/model/pull_request.py:245 kallithea/model/pull_request.py:380
+#: kallithea/model/pull_request.py:243 kallithea/model/pull_request.py:378
 msgid "You are not authorized to create the pull request"
 msgstr "Sie sind nicht berechtigt, den Pull-Request anzulegen."
 
-#: kallithea/model/pull_request.py:339
+#: kallithea/model/pull_request.py:337
 #, fuzzy
 #| msgid "Missing changesets since the previous pull request:"
 msgid "Missing changesets since the previous iteration:"
 msgstr "Fehlende Changesets seit letztem Pull Request:"
 
-#: kallithea/model/pull_request.py:346
+#: kallithea/model/pull_request.py:344
 #, fuzzy, python-format
 #| msgid "New changesets on %s %s since the previous pull request:"
 msgid "New changesets on %s %s since the previous iteration:"
 msgstr "Neue Changesets in %s %s seit dem letzten Pull Request:"
 
-#: kallithea/model/pull_request.py:353
+#: kallithea/model/pull_request.py:351
 #, fuzzy
 #| msgid "Ancestor didn't change - show diff since previous version:"
 msgid "Ancestor didn't change - diff since previous iteration:"
 msgstr "Vorgänger unverändert - zeige Diff zu lezter Version:"
 
-#: kallithea/model/pull_request.py:360
+#: kallithea/model/pull_request.py:358
 #, fuzzy, python-format
 msgid ""
 "This iteration is based on another %s revision and there is no simple "
@@ -1646,42 +1646,42 @@
 "Dieser Pull Request basiert auf einer anderen %s Revision. Daher ist kein "
 "Simple Diff verfügbar."
 
-#: kallithea/model/pull_request.py:362
+#: kallithea/model/pull_request.py:360
 #, fuzzy, python-format
 #| msgid "No changes found on %s %s since previous version."
 msgid "No changes found on %s %s since previous iteration."
 msgstr "Keine Änderungen seit der letzten Version gefunden in %s %s."
 
-#: kallithea/model/pull_request.py:388
+#: kallithea/model/pull_request.py:386
 #, python-format
 msgid "Closed, next iteration: %s ."
 msgstr "Geschlossen, nächste Iteration: %s ."
 
-#: kallithea/model/scm.py:668
+#: kallithea/model/scm.py:655
 msgid "latest tip"
 msgstr "Letzter Tip"
 
-#: kallithea/model/ssh_key.py:56
+#: kallithea/model/ssh_key.py:57
 #, python-format
 msgid "SSH key %r is invalid: %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:68
+#: kallithea/model/ssh_key.py:69
 #, python-format
 msgid "SSH key %s is already used by %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:89
+#: kallithea/model/ssh_key.py:88
 #, fuzzy, python-format
 #| msgid "Changeset not found"
-msgid "SSH key %r not found"
+msgid "SSH key with fingerprint %r found"
 msgstr "Änderungssatz nicht gefunden"
 
-#: kallithea/model/user.py:186
+#: kallithea/model/user.py:180
 msgid "New user registration"
 msgstr "Neue Benutzerregistrierung"
 
-#: kallithea/model/user.py:250
+#: kallithea/model/user.py:244
 #, fuzzy
 msgid ""
 "You can't remove this user since it is crucial for the entire application"
@@ -1689,7 +1689,7 @@
 "Sie können diesen Benutzer nicht löschen, da er von entscheidender "
 "Bedeutung für die gesamte Applikation ist"
 
-#: kallithea/model/user.py:255
+#: kallithea/model/user.py:249
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repositories and cannot be removed. Switch "
@@ -1699,7 +1699,7 @@
 "nicht entfernt werden. Entweder muss der Besitzer geändert oder das "
 "Repository entfernt werden: %s"
 
-#: kallithea/model/user.py:260
+#: kallithea/model/user.py:254
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repository groups and cannot be removed. Switch "
@@ -1709,7 +1709,7 @@
 "kann daher nicht entfernt werden. Entweder muss der Besitzer geändert "
 "oder die Repositorygruppen müssen entfernt werden: %s"
 
-#: kallithea/model/user.py:267
+#: kallithea/model/user.py:261
 #, python-format
 msgid ""
 "User \"%s\" still owns %s user groups and cannot be removed. Switch "
@@ -1719,16 +1719,16 @@
 "nicht entfernt werden. Entweder muss der Besitzer geändert oder die "
 "Benutzergruppen müssen gelöscht werden: %s"
 
-#: kallithea/model/user.py:361
+#: kallithea/model/user.py:355
 msgid "Password reset link"
 msgstr "Link zum Zurücksetzen des Passworts"
 
-#: kallithea/model/user.py:408
+#: kallithea/model/user.py:402
 #, fuzzy
 msgid "Password reset notification"
 msgstr "Link zum Zurücksetzen des Passworts"
 
-#: kallithea/model/user.py:409
+#: kallithea/model/user.py:403
 #, python-format
 msgid ""
 "The password to your account %s has been changed using password reset "
@@ -1737,21 +1737,21 @@
 "Das Passwort für dein Konto %s wurde mit dem Formular zum Zurücksetzen "
 "des Passworts geändert."
 
-#: kallithea/model/validators.py:52 kallithea/model/validators.py:53
+#: kallithea/model/validators.py:53 kallithea/model/validators.py:54
 msgid "Value cannot be an empty list"
 msgstr "Eine leere Liste ist kein gültiger Wert"
 
-#: kallithea/model/validators.py:72
+#: kallithea/model/validators.py:73
 #, python-format
 msgid "Username \"%(username)s\" already exists"
 msgstr "Benutezrname \"%(username)s\" existiert bereits"
 
-#: kallithea/model/validators.py:74
+#: kallithea/model/validators.py:75
 #, fuzzy, python-format
 msgid "Username \"%(username)s\" cannot be used"
 msgstr "Benutzername \"%(username)s\" ist ungültig"
 
-#: kallithea/model/validators.py:76
+#: kallithea/model/validators.py:77
 #, fuzzy
 msgid ""
 "Username may only contain alphanumeric characters underscores, periods or "
@@ -1761,25 +1761,25 @@
 "oder Bindestriche enthalten und muss mit einem alphanumerischen Zeichen "
 "oder einem Unterstrich beginnen"
 
-#: kallithea/model/validators.py:103
+#: kallithea/model/validators.py:104
 msgid "The input is not valid"
 msgstr "Die Eingabe ist nicht gültig"
 
-#: kallithea/model/validators.py:110
+#: kallithea/model/validators.py:111
 #, python-format
 msgid "Username %(username)s is not valid"
 msgstr "Benutzername \"%(username)s\" ist ungültig"
 
-#: kallithea/model/validators.py:131
+#: kallithea/model/validators.py:132
 msgid "Invalid user group name"
 msgstr "Ungültiger Benutzergruppenname"
 
-#: kallithea/model/validators.py:132
+#: kallithea/model/validators.py:133
 #, python-format
 msgid "User group \"%(usergroup)s\" already exists"
 msgstr "Benutzergruppe \"%(usergroup)s\" existiert bereits"
 
-#: kallithea/model/validators.py:134
+#: kallithea/model/validators.py:135
 msgid ""
 "user group name may only contain alphanumeric characters underscores, "
 "periods or dashes and must begin with alphanumeric character"
@@ -1788,64 +1788,64 @@
 "Unterstriche, Punkte oder Bindestriche enthalten und muss mit einem "
 "alphanumerischen Zeichen beginnen"
 
-#: kallithea/model/validators.py:174
+#: kallithea/model/validators.py:175
 msgid "Cannot assign this group as parent"
 msgstr "Kann diese Gruppe nicht als vorgesetzt setzen"
 
-#: kallithea/model/validators.py:175
+#: kallithea/model/validators.py:176
 #, python-format
 msgid "Group \"%(group_name)s\" already exists"
 msgstr "Gruppe \"%(group_name)s\" existiert bereits"
 
-#: kallithea/model/validators.py:177
+#: kallithea/model/validators.py:178
 #, python-format
 msgid "Repository with name \"%(group_name)s\" already exists"
 msgstr "Es gibt bereits ein Repository mit \"%(group_name)s\""
 
-#: kallithea/model/validators.py:233
+#: kallithea/model/validators.py:230
 msgid "Invalid characters (non-ascii) in password"
 msgstr "Üngültige(nicht ASCII) Zeichen im Passwort"
 
-#: kallithea/model/validators.py:248
+#: kallithea/model/validators.py:245
 msgid "Invalid old password"
 msgstr "Ungültiges altes Passwort"
 
-#: kallithea/model/validators.py:264
+#: kallithea/model/validators.py:261
 msgid "Passwords do not match"
 msgstr "Die Passwörter stimmen nicht überein"
 
-#: kallithea/model/validators.py:279
+#: kallithea/model/validators.py:276
 #, fuzzy
 msgid "Invalid username or password"
 msgstr "Ungültiges Passwort"
 
-#: kallithea/model/validators.py:313
+#: kallithea/model/validators.py:310
 #, fuzzy, python-format
 msgid "Repository name %(repo)s is not allowed"
 msgstr "Repository Name \"%(repo)s\" ist nicht erlaubt"
 
-#: kallithea/model/validators.py:315
+#: kallithea/model/validators.py:312
 #, python-format
 msgid "Repository named %(repo)s already exists"
 msgstr "Es gibt bereits ein Repository mit \"%(repo)s\""
 
-#: kallithea/model/validators.py:316
+#: kallithea/model/validators.py:313
 #, python-format
 msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
 msgstr ""
 "Es gibt bereits ein Repository mit \"%(repo)s\" in der Gruppe \"%(group)s"
 "\""
 
-#: kallithea/model/validators.py:318
+#: kallithea/model/validators.py:315
 #, python-format
 msgid "Repository group with name \"%(repo)s\" already exists"
 msgstr "Eine Repositorygruppe mit dem Namen \"%(repo)s\" existiert bereits"
 
-#: kallithea/model/validators.py:404
+#: kallithea/model/validators.py:401
 msgid "Invalid repository URL"
 msgstr "Ungültige Repository-URL"
 
-#: kallithea/model/validators.py:405
+#: kallithea/model/validators.py:402
 msgid ""
 "Invalid repository URL. It must be a valid http, https, ssh, svn+http or "
 "svn+https URL"
@@ -1853,44 +1853,44 @@
 "Ungültige Repository-URL. Es muss eine gültige http, https, ssh, svn+http "
 "oder svn+https URL sein"
 
-#: kallithea/model/validators.py:430
+#: kallithea/model/validators.py:427
 msgid "Fork has to be the same type as parent"
 msgstr "Forke um den selben typ wie der Vorgesetze zu haben"
 
-#: kallithea/model/validators.py:445
+#: kallithea/model/validators.py:442
 msgid "You don't have permissions to create repository in this group"
 msgstr ""
 "Du hast nicht die erforderlichen Berechtigungen, um in dieser Gruppe ein "
 "Repository zu erzeugen"
 
-#: kallithea/model/validators.py:447
+#: kallithea/model/validators.py:444
 msgid "no permission to create repository in root location"
 msgstr "keine Berechtigung, um ein Repository auf höchster Ebene anzulegen"
 
-#: kallithea/model/validators.py:497
+#: kallithea/model/validators.py:494
 msgid "You don't have permissions to create a group in this location"
 msgstr ""
 "Sie haben keine Berechtigung, um an diesem Ort ein Repository anzulegen"
 
-#: kallithea/model/validators.py:537
+#: kallithea/model/validators.py:534
 msgid "This username or user group name is not valid"
 msgstr "Dieser Benutzername oder Benutzergruppenname ist nicht gültig"
 
-#: kallithea/model/validators.py:630
+#: kallithea/model/validators.py:627
 msgid "This is not a valid path"
 msgstr "Dies ist ein Ungültiger Pfad"
 
-#: kallithea/model/validators.py:647
+#: kallithea/model/validators.py:644
 #, fuzzy
 msgid "This email address is already in use"
 msgstr "Diese E-Mailaddresse ist bereits in Benutzung"
 
-#: kallithea/model/validators.py:667
+#: kallithea/model/validators.py:664
 #, fuzzy, python-format
 msgid "Email address \"%(email)s\" not found"
 msgstr "E-MailAddresse \"%(email)s\" existiert nicht."
 
-#: kallithea/model/validators.py:704
+#: kallithea/model/validators.py:701
 msgid ""
 "The LDAP Login attribute of the CN must be specified - this is the name "
 "of the attribute that is equivalent to \"username\""
@@ -1898,11 +1898,11 @@
 "Das LDAP-Login-Attribut des CN muss angeben werden - Es ist der Name des "
 "Attributes äquivalent zu \"Benutzername\""
 
-#: kallithea/model/validators.py:716
+#: kallithea/model/validators.py:713
 msgid "Please enter a valid IPv4 or IPv6 address"
 msgstr "Bitte eine gültige IPv4- oder IPv6-Adresse angeben"
 
-#: kallithea/model/validators.py:717
+#: kallithea/model/validators.py:714
 #, python-format
 msgid ""
 "The network size (bits) must be within the range of 0-32 (not %(bits)r)"
@@ -1910,17 +1910,17 @@
 "Die Größe (in Bits) des Netzwerks muss im Bereich 0-32 liegen (nicht "
 "%(bits)r)"
 
-#: kallithea/model/validators.py:750
+#: kallithea/model/validators.py:747
 msgid "Key name can only consist of letters, underscore, dash or numbers"
 msgstr ""
 "Der Name eines Schlüssels darf nur aus Buchstaben, Ziffern, Unterstrich "
 "und Bindestrich bestehen"
 
-#: kallithea/model/validators.py:764
+#: kallithea/model/validators.py:761
 msgid "Filename cannot be inside a directory"
 msgstr "Dateiname darf kein Unterverzeichnis enthalten"
 
-#: kallithea/model/validators.py:780
+#: kallithea/model/validators.py:777
 #, python-format
 msgid "Plugins %(loaded)s and %(next_to_load)s both export the same name"
 msgstr ""
@@ -1982,7 +1982,7 @@
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:60
 #: kallithea/templates/email_templates/pull_request.html:37
 #: kallithea/templates/forks/fork.html:34
-#: kallithea/templates/index_base.html:58
+#: kallithea/templates/index_base.html:59
 #: kallithea/templates/pullrequests/pullrequest.html:33
 #: kallithea/templates/pullrequests/pullrequest_show.html:38
 #: kallithea/templates/pullrequests/pullrequest_show.html:59
@@ -1990,14 +1990,14 @@
 msgid "Description"
 msgstr "Beschreibung"
 
-#: kallithea/templates/index_base.html:60
+#: kallithea/templates/index_base.html:61
 msgid "Last Change"
 msgstr "Letzte Änderung"
 
 #: kallithea/templates/admin/my_account/my_account_repos.html:15
 #: kallithea/templates/admin/my_account/my_account_watched.html:15
 #: kallithea/templates/admin/repos/repos.html:41
-#: kallithea/templates/index_base.html:62
+#: kallithea/templates/index_base.html:63
 msgid "Tip"
 msgstr "Tipp"
 
@@ -2007,7 +2007,7 @@
 #: kallithea/templates/admin/repos/repos.html:42
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:8
 #: kallithea/templates/admin/user_groups/user_groups.html:42
-#: kallithea/templates/index_base.html:63
+#: kallithea/templates/index_base.html:64
 #: kallithea/templates/pullrequests/pullrequest_data.html:16
 #: kallithea/templates/pullrequests/pullrequest_show.html:124
 #: kallithea/templates/pullrequests/pullrequest_show.html:219
@@ -2031,7 +2031,7 @@
 #: kallithea/templates/admin/users/user_edit_profile.html:18
 #: kallithea/templates/admin/users/users.html:37
 #: kallithea/templates/base/base.html:364
-#: kallithea/templates/email_templates/registration.html:11
+#: kallithea/templates/email_templates/registration.html:12
 #: kallithea/templates/login.html:28 kallithea/templates/register.html:31
 msgid "Username"
 msgstr "Benutzername"
@@ -2163,7 +2163,7 @@
 #: kallithea/templates/admin/settings/settings.html:31
 #: kallithea/templates/admin/users/user_add.html:62
 #: kallithea/templates/admin/users/user_edit_profile.html:25
-#: kallithea/templates/email_templates/registration.html:33
+#: kallithea/templates/email_templates/registration.html:34
 #: kallithea/templates/register.html:66
 msgid "Email"
 msgstr "E-Mail"
@@ -2421,7 +2421,7 @@
 msgstr "Neuen Gist erstellen"
 
 #: kallithea/templates/admin/gists/index.html:51
-#: kallithea/templates/data_table/_dt_elements.html:78
+#: kallithea/templates/data_table/_dt_elements.html:84
 msgid "Created"
 msgstr "Erstellt"
 
@@ -2507,13 +2507,13 @@
 #: kallithea/templates/admin/users/user_edit_ips.html:21
 #: kallithea/templates/changeset/changeset_file_comment.html:30
 #: kallithea/templates/changeset/changeset_file_comment.html:121
-#: kallithea/templates/data_table/_dt_elements.html:69
-#: kallithea/templates/data_table/_dt_elements.html:89
-#: kallithea/templates/data_table/_dt_elements.html:91
-#: kallithea/templates/data_table/_dt_elements.html:101
-#: kallithea/templates/data_table/_dt_elements.html:103
-#: kallithea/templates/data_table/_dt_elements.html:120
-#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:75
+#: kallithea/templates/data_table/_dt_elements.html:95
+#: kallithea/templates/data_table/_dt_elements.html:97
+#: kallithea/templates/data_table/_dt_elements.html:107
+#: kallithea/templates/data_table/_dt_elements.html:109
+#: kallithea/templates/data_table/_dt_elements.html:126
+#: kallithea/templates/data_table/_dt_elements.html:128
 #: kallithea/templates/files/files_source.html:35
 #: kallithea/templates/files/files_source.html:38
 #: kallithea/templates/files/files_source.html:41
@@ -2529,14 +2529,14 @@
 #: kallithea/templates/base/perms_summary.html:44
 #: kallithea/templates/base/perms_summary.html:81
 #: kallithea/templates/base/perms_summary.html:83
-#: kallithea/templates/data_table/_dt_elements.html:63
-#: kallithea/templates/data_table/_dt_elements.html:64
-#: kallithea/templates/data_table/_dt_elements.html:85
-#: kallithea/templates/data_table/_dt_elements.html:86
-#: kallithea/templates/data_table/_dt_elements.html:97
-#: kallithea/templates/data_table/_dt_elements.html:98
-#: kallithea/templates/data_table/_dt_elements.html:116
-#: kallithea/templates/data_table/_dt_elements.html:117
+#: kallithea/templates/data_table/_dt_elements.html:69
+#: kallithea/templates/data_table/_dt_elements.html:70
+#: kallithea/templates/data_table/_dt_elements.html:91
+#: kallithea/templates/data_table/_dt_elements.html:92
+#: kallithea/templates/data_table/_dt_elements.html:103
+#: kallithea/templates/data_table/_dt_elements.html:104
+#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:123
 #: kallithea/templates/files/diff_2way.html:56
 #: kallithea/templates/files/files_source.html:37
 #: kallithea/templates/files/files_source.html:40
@@ -2867,7 +2867,7 @@
 #: kallithea/templates/admin/permissions/permissions_globals.html:27
 #: kallithea/templates/admin/repos/repo_add_base.html:28
 #: kallithea/templates/admin/repos/repo_edit_settings.html:33
-#: kallithea/templates/data_table/_dt_elements.html:134
+#: kallithea/templates/data_table/_dt_elements.html:140
 #: kallithea/templates/forks/fork.html:42
 msgid "Repository group"
 msgstr "Repository Gruppe"
@@ -2891,7 +2891,7 @@
 msgstr "Berechtigungen des Vorgabe-Benutzers für neue Repository-Gruppen."
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:40
-#: kallithea/templates/data_table/_dt_elements.html:141
+#: kallithea/templates/data_table/_dt_elements.html:147
 msgid "User group"
 msgstr "Benutzergruppe"
 
@@ -3079,7 +3079,7 @@
 msgstr "Erstellt am"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:121
+#: kallithea/templates/data_table/_dt_elements.html:127
 #, python-format
 msgid "Confirm to delete this group: %s with %s repository"
 msgid_plural "Confirm to delete this group: %s with %s repositories"
@@ -3261,14 +3261,10 @@
 msgstr "Extra-Feld"
 
 #: kallithea/templates/admin/repos/repo_edit.html:37
-msgid "Caches"
-msgstr "Zwischenspeicher"
+msgid "Remote"
+msgstr "Entfernt"
 
 #: kallithea/templates/admin/repos/repo_edit.html:40
-msgid "Remote"
-msgstr "Entfernt"
-
-#: kallithea/templates/admin/repos/repo_edit.html:43
 #: kallithea/templates/summary/statistics.html:8
 #: kallithea/templates/summary/summary.html:169
 #: kallithea/templates/summary/summary.html:170
@@ -3309,7 +3305,7 @@
 "öffentlichen Logbuch für jeden einsehbar."
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:46
-#: kallithea/templates/data_table/_dt_elements.html:68
+#: kallithea/templates/data_table/_dt_elements.html:74
 #, python-format
 msgid "Confirm to delete this repository: %s"
 msgstr "Löschen des Repositorys bestätigen: %s"
@@ -3343,45 +3339,14 @@
 "Administrierender es verfallen lässt. Der Administrierende kann es sowohl "
 "permanent löschen oder wiederherstellen."
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:4
-msgid "Invalidate Repository Cache"
-msgstr "Ungültiger Repositorycache"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:6
-msgid ""
-"Manually invalidate cache for this repository. On first access, the "
-"repository will be cached again."
-msgstr ""
-"Manuell den Zwischenspeicher für dieses Repository verfallen lassen. Beim "
-"ersten Zugriff wird der Zwischenspeicher erneut aufgefüllt."
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:9
-msgid "List of Cached Values"
-msgstr "Liste der zwischengespeicherten Werte"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:12
-msgid "Prefix"
-msgstr "Präfix"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:13
+#: kallithea/templates/admin/repos/repo_edit_fields.html:6
+msgid "Label"
+msgstr "Bezeichnung"
+
 #: kallithea/templates/admin/repos/repo_edit_fields.html:7
 msgid "Key"
 msgstr "Schlüssel"
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:14
-#: kallithea/templates/admin/user_groups/user_group_add.html:40
-#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
-#: kallithea/templates/admin/user_groups/user_groups.html:41
-#: kallithea/templates/admin/users/user_add.html:69
-#: kallithea/templates/admin/users/user_edit_profile.html:74
-#: kallithea/templates/admin/users/users.html:42
-msgid "Active"
-msgstr "Aktiv"
-
-#: kallithea/templates/admin/repos/repo_edit_fields.html:6
-msgid "Label"
-msgstr "Bezeichnung"
-
 #: kallithea/templates/admin/repos/repo_edit_fields.html:20
 #, python-format
 msgid "Confirm to delete this field: %s"
@@ -3996,6 +3961,15 @@
 msgid "Short, optional description for this user group."
 msgstr "Kurze, optionale Beschreibung für diese Benutzergruppe."
 
+#: kallithea/templates/admin/user_groups/user_group_add.html:40
+#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
+#: kallithea/templates/admin/user_groups/user_groups.html:41
+#: kallithea/templates/admin/users/user_add.html:69
+#: kallithea/templates/admin/users/user_edit_profile.html:74
+#: kallithea/templates/admin/users/users.html:42
+msgid "Active"
+msgstr "Aktiv"
+
 #: kallithea/templates/admin/user_groups/user_group_edit.html:5
 #, python-format
 msgid "%s user group settings"
@@ -4018,7 +3992,7 @@
 msgstr "Mitglieder"
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:19
-#: kallithea/templates/data_table/_dt_elements.html:102
+#: kallithea/templates/data_table/_dt_elements.html:108
 #, python-format
 msgid "Confirm to delete this user group: %s"
 msgstr "Bestätigen, um diese Benutzergruppe zu löschen: %s"
@@ -4092,7 +4066,7 @@
 msgstr "Mitglieder der Benutzergruppe"
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:90
+#: kallithea/templates/data_table/_dt_elements.html:96
 #, python-format
 msgid "Confirm to delete this user: %s"
 msgstr ""
@@ -4192,10 +4166,12 @@
 msgstr ""
 
 #: kallithea/templates/base/base.html:167
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Follow"
 msgstr ""
 
 #: kallithea/templates/base/base.html:168
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Unfollow"
 msgstr ""
 
@@ -4610,25 +4586,25 @@
 msgid "Merge"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:96
+#: kallithea/templates/changeset/changeset.html:95
 msgid "Grafted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:102
+#: kallithea/templates/changeset/changeset.html:100
 msgid "Transplanted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:108
+#: kallithea/templates/changeset/changeset.html:106
 #, fuzzy
 msgid "Replaced by:"
 msgstr "Erstellt von"
 
-#: kallithea/templates/changeset/changeset.html:122
+#: kallithea/templates/changeset/changeset.html:120
 #, fuzzy
 msgid "Preceded by:"
 msgstr "Erstellt von"
 
-#: kallithea/templates/changeset/changeset.html:139
+#: kallithea/templates/changeset/changeset.html:137
 #: kallithea/templates/compare/compare_diff.html:59
 #: kallithea/templates/pullrequests/pullrequest_show.html:290
 #, python-format
@@ -4637,7 +4613,7 @@
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/templates/changeset/changeset.html:141
+#: kallithea/templates/changeset/changeset.html:139
 #: kallithea/templates/compare/compare_diff.html:61
 #: kallithea/templates/pullrequests/pullrequest_show.html:292
 #, python-format
@@ -4646,8 +4622,8 @@
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
 #: kallithea/templates/pullrequests/pullrequest_show.html:333
@@ -4891,23 +4867,23 @@
 msgid "Repository creation in progress..."
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:42
+#: kallithea/templates/data_table/_dt_elements.html:48
 msgid "No changesets yet"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:48
-#: kallithea/templates/data_table/_dt_elements.html:50
+#: kallithea/templates/data_table/_dt_elements.html:54
+#: kallithea/templates/data_table/_dt_elements.html:56
 #, python-format
 msgid "Subscribe to %s rss feed"
 msgstr "Abonniere den %s RSS Feed"
 
-#: kallithea/templates/data_table/_dt_elements.html:56
-#: kallithea/templates/data_table/_dt_elements.html:58
+#: kallithea/templates/data_table/_dt_elements.html:62
+#: kallithea/templates/data_table/_dt_elements.html:64
 #, python-format
 msgid "Subscribe to %s atom feed"
 msgstr "Abonniere den %s ATOM Feed"
 
-#: kallithea/templates/data_table/_dt_elements.html:76
+#: kallithea/templates/data_table/_dt_elements.html:82
 msgid "Creating"
 msgstr ""
 
@@ -4945,6 +4921,13 @@
 msgid "by"
 msgstr ""
 
+#: kallithea/templates/email_templates/changeset_comment.html:36
+#: kallithea/templates/email_templates/pull_request_comment.html:43
+#, fuzzy
+#| msgid "Comment"
+msgid "View Comment"
+msgstr "Kommentar"
+
 #: kallithea/templates/email_templates/comment.html:27
 #, fuzzy
 #| msgid "Status change"
@@ -4959,35 +4942,45 @@
 "Dieser Pull Request wurde geschlossen und kann daher nicht aktualisiert "
 "werden."
 
-#: kallithea/templates/email_templates/password_reset.html:9
+#: kallithea/templates/email_templates/default.html:4
+msgid "Message"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:4
+#, fuzzy
+#| msgid "Password Reset"
+msgid "Password Reset Request"
+msgstr "Passwort zurücksetzen"
+
+#: kallithea/templates/email_templates/password_reset.html:10
 #, python-format
 msgid "Hello %s"
 msgstr "Hallo %s"
 
-#: kallithea/templates/email_templates/password_reset.html:16
+#: kallithea/templates/email_templates/password_reset.html:17
 #, fuzzy
 msgid "We have received a request to reset the password for your account."
 msgstr ""
 "Wir haben eine Anforderung erhalten, für deinen Account ein neues "
 "Passwort zu erstellen."
 
-#: kallithea/templates/email_templates/password_reset.html:25
+#: kallithea/templates/email_templates/password_reset.html:26
 msgid ""
 "This account is however managed outside this system and the password "
 "cannot be changed here."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:28
+#: kallithea/templates/email_templates/password_reset.html:29
 msgid "To set a new password, click the following link"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:33
+#: kallithea/templates/email_templates/password_reset.html:34
 msgid ""
 "Should you not be able to use the link above, please type the following "
 "code into the password reset form"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:44
+#: kallithea/templates/email_templates/password_reset.html:45
 msgid ""
 "If it weren't you who requested the password reset, just disregard this "
 "message."
@@ -5021,6 +5014,12 @@
 msgid "to"
 msgstr ""
 
+#: kallithea/templates/email_templates/pull_request.html:85
+#, fuzzy
+#| msgid "New Pull Request"
+msgid "View Pull Request"
+msgstr "Neuer Pull Request"
+
 #: kallithea/templates/email_templates/pull_request_comment.html:4
 #, fuzzy, python-format
 #| msgid "%(user)s commented on pull request %(age)s"
@@ -5039,12 +5038,22 @@
 msgid "Comment on Pull Request %s \"%s\""
 msgstr "Pull Request [kommentiert] für"
 
-#: kallithea/templates/email_templates/registration.html:22
+#: kallithea/templates/email_templates/registration.html:5
+#, fuzzy
+#| msgid "New user registration"
+msgid "New User Registration"
+msgstr "Neue Benutzerregistrierung"
+
+#: kallithea/templates/email_templates/registration.html:23
 #, fuzzy
 #| msgid "Group name"
 msgid "Full Name"
 msgstr "Gruppen name"
 
+#: kallithea/templates/email_templates/registration.html:42
+msgid "View User Profile"
+msgstr ""
+
 #: kallithea/templates/files/diff_2way.html:15
 #, python-format
 msgid "%s File side-by-side diff"
@@ -5655,45 +5664,45 @@
 msgid "Stats gathered: "
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:87
-#: kallithea/templates/summary/summary.html:371
+#: kallithea/templates/summary/statistics.html:85
+#: kallithea/templates/summary/summary.html:369
 msgid "files"
 msgstr "Dateien"
 
-#: kallithea/templates/summary/statistics.html:111
-#: kallithea/templates/summary/summary.html:401
+#: kallithea/templates/summary/statistics.html:109
+#: kallithea/templates/summary/summary.html:399
 msgid "Show more"
 msgstr "Mehr anzeigen"
 
-#: kallithea/templates/summary/statistics.html:405
+#: kallithea/templates/summary/statistics.html:395
 msgid "commits"
 msgstr "Commits"
 
-#: kallithea/templates/summary/statistics.html:406
+#: kallithea/templates/summary/statistics.html:396
 msgid "files added"
 msgstr "Dateien hinzugefügt"
 
-#: kallithea/templates/summary/statistics.html:407
+#: kallithea/templates/summary/statistics.html:397
 msgid "files changed"
 msgstr "Dateien geändert"
 
-#: kallithea/templates/summary/statistics.html:408
+#: kallithea/templates/summary/statistics.html:398
 msgid "files removed"
 msgstr "Dateien entfernt"
 
-#: kallithea/templates/summary/statistics.html:410
+#: kallithea/templates/summary/statistics.html:400
 msgid "commit"
 msgstr "Commit"
 
-#: kallithea/templates/summary/statistics.html:411
+#: kallithea/templates/summary/statistics.html:401
 msgid "file added"
 msgstr "Datei hinzugefügt"
 
-#: kallithea/templates/summary/statistics.html:412
+#: kallithea/templates/summary/statistics.html:402
 msgid "file changed"
 msgstr "Datei geändert"
 
-#: kallithea/templates/summary/statistics.html:413
+#: kallithea/templates/summary/statistics.html:403
 msgid "file removed"
 msgstr "Datei entfernt"
 
@@ -5797,6 +5806,31 @@
 msgid "Download %s as %s"
 msgstr "%s als %s herunterladen"
 
+#~ msgid "Cache invalidation successful"
+#~ msgstr "Cache Entfernung war erfolgreich"
+
+#~ msgid "An error occurred during cache invalidation"
+#~ msgstr "Währen der Cache Invalidierung trat ein Fehler auf"
+
+#~ msgid "Caches"
+#~ msgstr "Zwischenspeicher"
+
+#~ msgid "Invalidate Repository Cache"
+#~ msgstr "Ungültiger Repositorycache"
+
+#~ msgid ""
+#~ "Manually invalidate cache for this repository. On first access, the "
+#~ "repository will be cached again."
+#~ msgstr ""
+#~ "Manuell den Zwischenspeicher für dieses Repository verfallen lassen. "
+#~ "Beim ersten Zugriff wird der Zwischenspeicher erneut aufgefüllt."
+
+#~ msgid "List of Cached Values"
+#~ msgstr "Liste der zwischengespeicherten Werte"
+
+#~ msgid "Prefix"
+#~ msgstr "Präfix"
+
 #~ msgid "This repository has been locked by %s on %s"
 #~ msgstr "Dieses Repository ist von %s am %s gesperrt worden"
 
--- a/kallithea/i18n/el/LC_MESSAGES/kallithea.po	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/i18n/el/LC_MESSAGES/kallithea.po	Mon May 04 19:24:04 2020 +0200
@@ -4,9 +4,9 @@
 msgstr ""
 "Project-Id-Version: Kallithea 0.3\n"
 "Report-Msgid-Bugs-To: translations@kallithea-scm.org\n"
-"POT-Creation-Date: 2020-01-05 04:10+0100\n"
-"PO-Revision-Date: 2019-06-26 19:00+0000\n"
-"Last-Translator: THANOS SIOURDAKIS <siourdakisthanos@gmail.com>\n"
+"POT-Creation-Date: 2020-05-04 19:12+0200\n"
+"PO-Revision-Date: 2020-04-22 16:11+0000\n"
+"Last-Translator: Asterios Dimitriou <steve@pci.gr>\n"
 "Language-Team: Greek <https://hosted.weblate.org/projects/kallithea/"
 "kallithea/el/>\n"
 "Language: el\n"
@@ -14,17 +14,17 @@
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=n != 1;\n"
-"X-Generator: Weblate 3.7.1-dev\n"
+"X-Generator: Weblate 4.0.2-dev\n"
 
 #: kallithea/controllers/changelog.py:67
-#: kallithea/controllers/pullrequests.py:250 kallithea/lib/base.py:602
+#: kallithea/controllers/pullrequests.py:247 kallithea/lib/base.py:602
 msgid "There are no changesets yet"
 msgstr "Δεν υπάρχουν σετ αλλαγών ακόμα"
 
 #: kallithea/controllers/admin/permissions.py:64
 #: kallithea/controllers/admin/permissions.py:68
 #: kallithea/controllers/admin/permissions.py:72
-#: kallithea/controllers/changelog.py:137
+#: kallithea/controllers/changelog.py:136
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:7
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:88
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:7
@@ -33,101 +33,94 @@
 msgid "None"
 msgstr "Χωρίς"
 
-#: kallithea/controllers/changelog.py:140 kallithea/controllers/files.py:189
+#: kallithea/controllers/changelog.py:139 kallithea/controllers/files.py:189
 msgid "(closed)"
 msgstr "(κλειστό)"
 
-#: kallithea/controllers/changeset.py:81
+#: kallithea/controllers/changeset.py:82
 msgid "Show whitespace"
 msgstr "Εμφάνιση κενού"
 
-#: kallithea/controllers/changeset.py:88
-#: kallithea/controllers/changeset.py:95
+#: kallithea/controllers/changeset.py:89
+#: kallithea/controllers/changeset.py:96
 #: kallithea/templates/files/diff_2way.html:55
 msgid "Ignore whitespace"
 msgstr "Αγνόηση κενού"
 
-#: kallithea/controllers/changeset.py:161
+#: kallithea/controllers/changeset.py:162
 #, python-format
 msgid "Increase diff context to %(num)s lines"
 msgstr "Αύξηση του diff πλαισίου σε %(num)s γραμμές"
 
-#: kallithea/controllers/changeset.py:201
-#, fuzzy
-#| msgid "No permission to change pull request status"
+#: kallithea/controllers/changeset.py:202
 msgid "No permission to change status"
-msgstr "Χωρίς δικαιώματα αλλαγής της κατάστασης του αιτήματος έλξης"
-
-#: kallithea/controllers/changeset.py:212
+msgstr "Χωρίς δικαιώματα αλλαγής της κατάστασης"
+
+#: kallithea/controllers/changeset.py:213
 #, python-format
 msgid "Successfully deleted pull request %s"
 msgstr "Επιτυχής διαγραφή αιτήματος έλξης %s"
 
 #: kallithea/controllers/changeset.py:319 kallithea/controllers/files.py:89
-#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:700
+#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:697
 msgid "Such revision does not exist for this repository"
 msgstr "Δεν υπάρχει τέτοια αναθεώρηση για αυτό το αποθετήριο"
 
 #: kallithea/controllers/compare.py:68
-#, fuzzy, python-format
-#| msgid "Error creating repository %s"
+#, python-format
 msgid "Could not find other repository %s"
-msgstr "Βλάβη κατά τη δημιουργία του αποθετηρίου %s"
+msgstr "Δεν βρέθηκε το αποθετήριο %s"
 
 #: kallithea/controllers/compare.py:74
-#, fuzzy
-#| msgid "Cannot compare repositories without using common ancestor"
 msgid "Cannot compare repositories of different types"
-msgstr ""
-"Δεν μπορεί να γίνει σύγκριση αποθετηρίων χωρίς να χρησιμοποιηθεί κοινός "
-"πρόγονος"
-
-#: kallithea/controllers/compare.py:246
+msgstr "Δεν μπορεί να γίνει σύγκριση αποθετηρίων διαφορετικού τύπου"
+
+#: kallithea/controllers/compare.py:247
 msgid "Cannot show empty diff"
-msgstr ""
-
-#: kallithea/controllers/compare.py:248
+msgstr "Δεν μπορεί να γίνει η εμφάνιση άδειου diff"
+
+#: kallithea/controllers/compare.py:249
 msgid "No ancestor found for merge diff"
-msgstr ""
-
-#: kallithea/controllers/compare.py:252
+msgstr "Δεν βρέθηκε πρόγονος για να συγχωνευθούν οι διαφορές"
+
+#: kallithea/controllers/compare.py:253
 msgid "Multiple merge ancestors found for merge compare"
-msgstr ""
-
-#: kallithea/controllers/compare.py:268
+msgstr "Βρέθηκαν πολλαπλοί πρόγονοι για σύγκριση της συγχώνευσης"
+
+#: kallithea/controllers/compare.py:269
 msgid "Cannot compare repositories without using common ancestor"
 msgstr ""
 "Δεν μπορεί να γίνει σύγκριση αποθετηρίων χωρίς να χρησιμοποιηθεί κοινός "
 "πρόγονος"
 
-#: kallithea/controllers/error.py:71
+#: kallithea/controllers/error.py:67
 msgid "No response"
 msgstr "Χωρίς απόκριση"
 
-#: kallithea/controllers/error.py:72
+#: kallithea/controllers/error.py:68
 msgid "Unknown error"
-msgstr "Άγνωστο λάθος"
-
-#: kallithea/controllers/error.py:85
+msgstr "Άγνωστο σφάλμα"
+
+#: kallithea/controllers/error.py:81
 msgid ""
 "The request could not be understood by the server due to malformed syntax."
 msgstr ""
 "Η αίτηση δεν  μπόρεσε να ερμηνευτεί από τον εξυπηρετητή λόγω κακής "
 "διατύπωσης."
 
-#: kallithea/controllers/error.py:88
+#: kallithea/controllers/error.py:84
 msgid "Unauthorized access to resource"
 msgstr "Ανεξουσιοδοτημένη πρόσβαση στον πόρο"
 
-#: kallithea/controllers/error.py:90
+#: kallithea/controllers/error.py:86
 msgid "You don't have permission to view this page"
 msgstr "Δεν έχετε άδεια για να εμφανίσετε αυτή τη σελίδα"
 
-#: kallithea/controllers/error.py:92
+#: kallithea/controllers/error.py:88
 msgid "The resource could not be found"
 msgstr "Ο πόρος δεν μπορεί να βρεθεί"
 
-#: kallithea/controllers/error.py:94
+#: kallithea/controllers/error.py:90
 msgid ""
 "The server encountered an unexpected condition which prevented it from "
 "fulfilling the request."
@@ -135,14 +128,14 @@
 "Ο εξυπηρετητής συνάντησε μια απρόσμενη κατάσταση που τον απέτρεψαν να "
 "πραγματοποιήσει την αίτηση."
 
-#: kallithea/controllers/feed.py:63
+#: kallithea/controllers/feed.py:59
 #, python-format
 msgid "%s committed on %s"
 msgstr "%s συνέβαλε στο %s"
 
-#: kallithea/controllers/feed.py:88
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/controllers/feed.py:84
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/compare/compare_diff.html:95
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
@@ -150,12 +143,12 @@
 msgid "Changeset was too big and was cut off..."
 msgstr "Το σετ αλλαγών ήταν πολύ μεγάλο και περικόπηκε..."
 
-#: kallithea/controllers/feed.py:111 kallithea/controllers/feed.py:140
+#: kallithea/controllers/feed.py:107
 #, python-format
 msgid "%s %s feed"
 msgstr "%s %s τροφοδοσία"
 
-#: kallithea/controllers/feed.py:113 kallithea/controllers/feed.py:142
+#: kallithea/controllers/feed.py:109
 #, python-format
 msgid "Changes on %s repository"
 msgstr "Αλλαγές στο αποθετήριο %s"
@@ -165,120 +158,117 @@
 msgstr "Κλικ εδώ για προθήκη νέου αρχείου"
 
 #: kallithea/controllers/files.py:86
-#, fuzzy
-#| msgid "There are no files yet. %s"
 msgid "There are no files yet."
-msgstr "Δεν υπάρχουν αρχεία ακόμα. %s"
+msgstr "Δεν υπάρχουν αρχεία ακόμα."
 
 #: kallithea/controllers/files.py:186
 #, python-format
 msgid "%s at %s"
 msgstr "%s την %s"
 
-#: kallithea/controllers/files.py:296
-#, fuzzy
+#: kallithea/controllers/files.py:295
 msgid "You can only delete files with revision being a valid branch"
 msgstr ""
 "Μπορείτε να διαγράψετε μόνο αρχεία με αναθεώρηση που βρίσκονται σε έγκυρη "
 "διακλάδωση"
 
-#: kallithea/controllers/files.py:307
+#: kallithea/controllers/files.py:306
 #, python-format
 msgid "Deleted file %s via Kallithea"
-msgstr "Διαγραφή αρχείου %s μέσω του Kallithea"
-
-#: kallithea/controllers/files.py:331
+msgstr "Διαγραφή αρχείου %s μέσω της Καλλιθέας"
+
+#: kallithea/controllers/files.py:330
 #, python-format
 msgid "Successfully deleted file %s"
 msgstr "Επιτυχής διαγραφή αρχείου %s"
 
-#: kallithea/controllers/files.py:335 kallithea/controllers/files.py:394
-#: kallithea/controllers/files.py:469
+#: kallithea/controllers/files.py:334 kallithea/controllers/files.py:392
+#: kallithea/controllers/files.py:467
 msgid "Error occurred during commit"
-msgstr "Συνέβη λάθος κατά το commit"
-
-#: kallithea/controllers/files.py:350
+msgstr "Παρουσιάστηκε σφάλμα κατά το commit"
+
+#: kallithea/controllers/files.py:349
 msgid "You can only edit files with revision being a valid branch"
 msgstr ""
 "Μπορείτε να επεξεργαστείτε μόνο αρχεία σε αναθεώρηση που βρίσκονται σε "
 "έγκυρη διακλάδωση"
 
-#: kallithea/controllers/files.py:364
+#: kallithea/controllers/files.py:363
 #, python-format
 msgid "Edited file %s via Kallithea"
-msgstr "Επεξεργασία αρχείου %s μέσω του Kallithea"
-
-#: kallithea/controllers/files.py:380
+msgstr "Επεξεργασία αρχείου %s μέσω της Καλλιθέας"
+
+#: kallithea/controllers/files.py:378
 msgid "No changes"
 msgstr "Καμία αλλαγή"
 
-#: kallithea/controllers/files.py:390 kallithea/controllers/files.py:458
+#: kallithea/controllers/files.py:388 kallithea/controllers/files.py:456
 #, python-format
 msgid "Successfully committed to %s"
 msgstr "Επιτυχής παράδοση σε %s"
 
-#: kallithea/controllers/files.py:409
+#: kallithea/controllers/files.py:407
 msgid "Added file via Kallithea"
-msgstr "Προσθήκη αρχείου μέσω Kallithea"
-
-#: kallithea/controllers/files.py:430
+msgstr "Προσθήκη αρχείου μέσω της Καλλιθέας"
+
+#: kallithea/controllers/files.py:428
 msgid "No content"
 msgstr "Χωρίς περιεχόμενο"
 
-#: kallithea/controllers/files.py:434
+#: kallithea/controllers/files.py:432
 msgid "No filename"
 msgstr "Χωρίς όνομα αρχείου"
 
-#: kallithea/controllers/files.py:461
+#: kallithea/controllers/files.py:459
 msgid "Location must be relative path and must not contain .. in path"
 msgstr ""
 "Η τοποθεσία πρέπει να είναι σχετική διαδρομή και να μην περιέχει .. μέσα "
 "της"
 
-#: kallithea/controllers/files.py:493
+#: kallithea/controllers/files.py:491
 msgid "Downloads disabled"
 msgstr "Οι μεταφορτώσεις απενεργοποιήθηκαν"
 
-#: kallithea/controllers/files.py:504
+#: kallithea/controllers/files.py:502
 #, python-format
 msgid "Unknown revision %s"
 msgstr "Άγνωστη αναθεώρηση %s"
 
-#: kallithea/controllers/files.py:506
+#: kallithea/controllers/files.py:504
 msgid "Empty repository"
 msgstr "Άδειο αποθετήριο"
 
-#: kallithea/controllers/files.py:508
+#: kallithea/controllers/files.py:506
 msgid "Unknown archive type"
 msgstr "Άγνωστος τύπος αρχειοθέτησης"
 
-#: kallithea/controllers/files.py:729
+#: kallithea/controllers/files.py:726
 #: kallithea/templates/changeset/changeset_range.html:9
 #: kallithea/templates/email_templates/pull_request.html:64
 #: kallithea/templates/pullrequests/pullrequest.html:84
 msgid "Changesets"
 msgstr "Σετ αλλαγών"
 
-#: kallithea/controllers/files.py:730
-#: kallithea/controllers/pullrequests.py:182 kallithea/model/scm.py:676
+#: kallithea/controllers/files.py:727
+#: kallithea/controllers/pullrequests.py:174 kallithea/model/scm.py:663
 msgid "Branches"
 msgstr "Κλάδοι"
 
-#: kallithea/controllers/files.py:731
-#: kallithea/controllers/pullrequests.py:183 kallithea/model/scm.py:687
+#: kallithea/controllers/files.py:728
+#: kallithea/controllers/pullrequests.py:175 kallithea/model/scm.py:674
 msgid "Tags"
 msgstr "Ετικέτες"
 
-#: kallithea/controllers/forks.py:174
+#: kallithea/controllers/forks.py:175
 #, python-format
 msgid "An error occurred during repository forking %s"
-msgstr "Συνέβει ένα λάθος κατά την διακλάδωση του αποθετηρίου %s"
-
-#: kallithea/controllers/home.py:79
+msgstr "Παρουσιάστηκε σφάλμα κατά την διακλάδωση του αποθετηρίου %s"
+
+#: kallithea/controllers/home.py:77
 msgid "Groups"
 msgstr "Ομάδες"
 
-#: kallithea/controllers/home.py:89
+#: kallithea/controllers/home.py:87
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:90
 #: kallithea/templates/admin/repos/repo_add.html:12
 #: kallithea/templates/admin/repos/repo_add.html:16
@@ -290,48 +280,54 @@
 msgid "Repositories"
 msgstr "Αποθετήρια"
 
-#: kallithea/controllers/home.py:122
+#: kallithea/controllers/home.py:119
 #: kallithea/templates/files/files_add.html:32
 #: kallithea/templates/files/files_delete.html:23
 #: kallithea/templates/files/files_edit.html:32
 msgid "Branch"
 msgstr "Κλάδος"
 
-#: kallithea/controllers/home.py:128
+#: kallithea/controllers/home.py:125
 msgid "Closed Branches"
 msgstr "Κλειστοί Κλάδοι"
 
-#: kallithea/controllers/home.py:134
+#: kallithea/controllers/home.py:131
 msgid "Tag"
 msgstr "Ετικέτα"
 
-#: kallithea/controllers/home.py:140
+#: kallithea/controllers/home.py:137
 msgid "Bookmark"
 msgstr "Σελιδοδείκτης"
 
-#: kallithea/controllers/journal.py:112 kallithea/controllers/journal.py:154
+#: kallithea/controllers/journal.py:146 kallithea/controllers/journal.py:157
 #: kallithea/templates/journal/public_journal.html:4
 #: kallithea/templates/journal/public_journal.html:18
 msgid "Public Journal"
 msgstr "Δημόσιο Ημερολόγιο"
 
-#: kallithea/controllers/journal.py:116 kallithea/controllers/journal.py:158
+#: kallithea/controllers/journal.py:150 kallithea/controllers/journal.py:161
 #: kallithea/templates/base/base.html:290
 #: kallithea/templates/journal/journal.html:5
 #: kallithea/templates/journal/journal.html:13
 msgid "Journal"
 msgstr "Ημερολόγιο"
 
-#: kallithea/controllers/login.py:140 kallithea/controllers/login.py:185
+#: kallithea/controllers/login.py:109
+#, fuzzy
+#| msgid "Authentication"
+msgid "Authentication failed."
+msgstr "Έλεγχος ταυτότητας"
+
+#: kallithea/controllers/login.py:142 kallithea/controllers/login.py:187
 msgid "Bad captcha"
 msgstr "Λάθος captcha"
 
-#: kallithea/controllers/login.py:146
+#: kallithea/controllers/login.py:148
 #, python-format
 msgid "You have successfully registered with %s"
 msgstr "Εγγραφήκατε επιτυχώς στο %s"
 
-#: kallithea/controllers/login.py:190
+#: kallithea/controllers/login.py:192
 msgid "A password reset confirmation code has been sent"
 msgstr "Στάλθηκε ένας κωδικός επιβεβαίωσης επαναφοράς του συνθηματικού"
 
@@ -344,235 +340,234 @@
 msgid "Successfully updated password"
 msgstr "Το συνθηματικό ενημερώθηκε επιτυχώς"
 
-#: kallithea/controllers/pullrequests.py:68
+#: kallithea/controllers/pullrequests.py:67
 #, python-format
 msgid "Invalid reviewer \"%s\" specified"
 msgstr "Καθορίστηκε άκυρος σχολιαστής \"%s\""
 
-#: kallithea/controllers/pullrequests.py:131
+#: kallithea/controllers/pullrequests.py:123
 #, python-format
 msgid "%s (closed)"
 msgstr "%s (κλειστό)"
 
-#: kallithea/controllers/pullrequests.py:158
+#: kallithea/controllers/pullrequests.py:150
 #: kallithea/templates/changeset/changeset.html:12
 msgid "Changeset"
 msgstr "Σετ αλλαγών"
 
-#: kallithea/controllers/pullrequests.py:179
+#: kallithea/controllers/pullrequests.py:171
 msgid "Special"
 msgstr "Ειδικός"
 
-#: kallithea/controllers/pullrequests.py:180
+#: kallithea/controllers/pullrequests.py:172
 msgid "Peer branches"
 msgstr "Ομότιμοι κλάδοι"
 
-#: kallithea/controllers/pullrequests.py:181 kallithea/model/scm.py:682
+#: kallithea/controllers/pullrequests.py:173 kallithea/model/scm.py:669
 msgid "Bookmarks"
 msgstr "Σελιδοδείκτες"
 
-#: kallithea/controllers/pullrequests.py:318
+#: kallithea/controllers/pullrequests.py:315
 #, python-format
 msgid "Error creating pull request: %s"
-msgstr "Λάθος στη δημιουργία αιτήματος έλξης - pull request: %s"
-
-#: kallithea/controllers/pullrequests.py:345
-#: kallithea/controllers/pullrequests.py:368
+msgstr "Σφάλμα στη δημιουργία αιτήματος έλξης - pull request: %s"
+
+#: kallithea/controllers/pullrequests.py:342
+#: kallithea/controllers/pullrequests.py:365
 msgid "Error occurred while creating pull request"
-msgstr "Λάθος κατά τη δημιουργία αιτήματος έλξης (pull request)"
-
-#: kallithea/controllers/pullrequests.py:350
+msgstr "Παρουσιάστηκε σφάλμα κατά τη δημιουργία αίτησης έλξης"
+
+#: kallithea/controllers/pullrequests.py:347
 msgid "Successfully opened new pull request"
 msgstr "Ένα νέο αίτημα έλξης (pull request) δημιουργήθηκε επιτυχώς"
 
-#: kallithea/controllers/pullrequests.py:373
-#, fuzzy
-#| msgid "Pull request update created"
+#: kallithea/controllers/pullrequests.py:370
 msgid "New pull request iteration created"
-msgstr "Δημιουργήθηκε ενημέρωση αιτήματος έλξης"
-
-#: kallithea/controllers/pullrequests.py:401
+msgstr "Δημιουργήθηκε νέο αίτημα έλξης"
+
+#: kallithea/controllers/pullrequests.py:398
 #, python-format
 msgid "Meanwhile, the following reviewers have been added: %s"
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:405
+msgstr "Εντωμεταξύ, οι ακόλουθοι κριτικοί προστέθηκαν: %s"
+
+#: kallithea/controllers/pullrequests.py:402
 #, python-format
 msgid "Meanwhile, the following reviewers have been removed: %s"
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:421
-#: kallithea/model/pull_request.py:232
+msgstr "Εντωμεταξύ, οι ακόλουθοι κριτικοί αφαιρέθηκαν: %s"
+
+#: kallithea/controllers/pullrequests.py:418
+#: kallithea/model/pull_request.py:230
 msgid "No description"
 msgstr "Χωρίς περιγραφή"
 
-#: kallithea/controllers/pullrequests.py:430
+#: kallithea/controllers/pullrequests.py:427
 msgid "Pull request updated"
 msgstr "Ενημερώθηκε η αίτηση έλξης"
 
-#: kallithea/controllers/pullrequests.py:443
+#: kallithea/controllers/pullrequests.py:440
 msgid "Successfully deleted pull request"
 msgstr "Επιτυχής διαγραφή αιτήματος έλξης"
 
-#: kallithea/controllers/pullrequests.py:479
+#: kallithea/controllers/pullrequests.py:476
 #, python-format
 msgid "Revision %s not found in %s"
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:506
-#, fuzzy, python-format
-#| msgid "No changesets found for updating this pull request."
+msgstr "Η αναθεώρηση %s δεν βρέθηκε στο %s"
+
+#: kallithea/controllers/pullrequests.py:504
+#, python-format
 msgid "Error: changesets not found when displaying pull request from %s."
-msgstr "Δεν βρέθηκαν σετ αλλαγών για ενημέρωση αυτού του αιτήματος έλξης."
-
-#: kallithea/controllers/pullrequests.py:520
+msgstr ""
+"Σφάλμα: τα σετ αλλαγών δεν βρέθηκαν όταν εμφανίζεται το αίτημα έλξης από "
+"το %s."
+
+#: kallithea/controllers/pullrequests.py:518
 #, python-format
 msgid "This pull request has already been merged to %s."
 msgstr "Το αίτημα έλξης έχει ήδη συγχωνευτεί με το %s."
 
-#: kallithea/controllers/pullrequests.py:522
+#: kallithea/controllers/pullrequests.py:520
 msgid "This pull request has been closed and can not be updated."
 msgstr "Αυτό το αίτημα έλξης έχει κλείσει και δεν μπορεί να ενημερωθεί."
 
-#: kallithea/controllers/pullrequests.py:546
-#, fuzzy, python-format
-#| msgid "The following changes are available on %s:"
+#: kallithea/controllers/pullrequests.py:539
+#, python-format
 msgid "The following additional changes are available on %s:"
-msgstr "Οι ακόλουθες αλλαγές είναι διαθέσιμες στο %s:"
-
-#: kallithea/controllers/pullrequests.py:548
-#: kallithea/controllers/pullrequests.py:552
-#, fuzzy
-#| msgid "No changesets found for updating this pull request."
+msgstr "Οι επιπλέον ακόλουθες αλλαγές είναι διαθέσιμες στο %s:"
+
+#: kallithea/controllers/pullrequests.py:541
+#: kallithea/controllers/pullrequests.py:545
 msgid "No additional changesets found for iterating on this pull request."
-msgstr "Δεν βρέθηκαν σετ αλλαγών για ενημέρωση αυτού του αιτήματος έλξης."
-
-#: kallithea/controllers/pullrequests.py:560
+msgstr ""
+"Δεν βρέθηκαν επιπλέον σετ αλλαγών στην προσέγγιση αυτού του αιτήματος "
+"έλξης."
+
+#: kallithea/controllers/pullrequests.py:553
 #, python-format
 msgid "Note: Branch %s has another head: %s."
 msgstr "Σημείωση: Ο κλάδος %s έχει άλλη κεφαλή (head): %s."
 
-#: kallithea/controllers/pullrequests.py:567
-#, fuzzy
-#| msgid "Git pull requests don't support updates yet."
+#: kallithea/controllers/pullrequests.py:560
 msgid "Git pull requests don't support iterating yet."
 msgstr "Αιτήματα έλξης του git δεν υποστηρίζουν ακόμα ενημερώσεις."
 
-#: kallithea/controllers/pullrequests.py:569
-#, fuzzy, python-format
-#| msgid "No changesets found for updating this pull request."
+#: kallithea/controllers/pullrequests.py:562
+#, python-format
 msgid ""
 "Error: some changesets not found when displaying pull request from %s."
-msgstr "Δεν βρέθηκαν σετ αλλαγών για ενημέρωση αυτού του αιτήματος έλξης."
-
-#: kallithea/controllers/pullrequests.py:593
+msgstr ""
+"Σφάλμα: κάποια σετ αλλαγών δεν βρέθηκαν όταν εμφανιζόταν αυτό το αίτημα "
+"έλξης από το %s."
+
+#: kallithea/controllers/pullrequests.py:586
 msgid "The diff can't be shown - the PR revisions could not be found."
 msgstr ""
+"Οι διαφορές δεν μπορούν να εμφανιστούν - οι αναθεωρήσεις δεν βρέθηκαν."
+
+#: kallithea/controllers/search.py:132
+msgid "Invalid search query. Try quoting it."
+msgstr "Άκυρο αίτημα αναζήτησης. Δοκιμάστε με εισαγωγικά."
 
 #: kallithea/controllers/search.py:136
-msgid "Invalid search query. Try quoting it."
-msgstr "Άκυρο αίτημα αναζήτησης. Δοκιμάστε με εισαγωγικά."
-
-#: kallithea/controllers/search.py:140
 msgid "The server has no search index."
-msgstr ""
-
-#: kallithea/controllers/search.py:143
+msgstr "Ο διακομιστής δεν έχει ευρετήριο αναζήτησης."
+
+#: kallithea/controllers/search.py:139
 msgid "An error occurred during search operation."
-msgstr "Ένα λάθος συνέβη κατά την διαδικασία αναζήτησης."
-
-#: kallithea/controllers/summary.py:168
-#: kallithea/templates/summary/summary.html:412
+msgstr "Παρουσιάστηκε σφάλμα κατά τη λειτουργία αναζήτησης."
+
+#: kallithea/controllers/summary.py:169
+#: kallithea/templates/summary/summary.html:410
 msgid "No data ready yet"
 msgstr "Δεν υπάρχουν ακόμα έτοιμα δεδομένα"
 
-#: kallithea/controllers/summary.py:171
+#: kallithea/controllers/summary.py:172
 #: kallithea/templates/summary/summary.html:97
 msgid "Statistics are disabled for this repository"
 msgstr "Τα στατιστικά είναι απενεργοποιημένα για αυτό το αποθετήριο"
 
-#: kallithea/controllers/admin/auth_settings.py:137
+#: kallithea/controllers/admin/auth_settings.py:136
 msgid "Auth settings updated successfully"
 msgstr "Οι ρυθμίσεις εξουσιοδότησης ενημερώθηκαν επιτυχώς"
 
-#: kallithea/controllers/admin/auth_settings.py:148
+#: kallithea/controllers/admin/auth_settings.py:147
 msgid "error occurred during update of auth settings"
-msgstr "παρουσιάστηκε βλάβη κατά την ενημέρωση των ρυθμίσεων εξουσιοδότησης"
-
-#: kallithea/controllers/admin/defaults.py:75
+msgstr ""
+"παρουσιάστηκε σφάλμα κατά την ενημέρωση των ρυθμίσεων εξουσιοδότησης"
+
+#: kallithea/controllers/admin/defaults.py:74
 msgid "Default settings updated successfully"
 msgstr "Οι προεπιλεγμένες ρυθμίσεις ενημερώθηκαν επιτυχώς"
 
-#: kallithea/controllers/admin/defaults.py:90
+#: kallithea/controllers/admin/defaults.py:89
 msgid "Error occurred during update of defaults"
-msgstr "Συνέβη μία βλάβη κατά την ενημέρωση των προεπιλογών"
+msgstr "Παρουσιάστηκε σφάλμα κατά την ενημέρωση των προεπιλογών"
 
 #: kallithea/controllers/admin/gists.py:59
 #: kallithea/controllers/admin/my_account.py:232
-#: kallithea/controllers/admin/users.py:248
+#: kallithea/controllers/admin/users.py:246
 msgid "Forever"
 msgstr "Πάντα"
 
 #: kallithea/controllers/admin/gists.py:60
 #: kallithea/controllers/admin/my_account.py:233
-#: kallithea/controllers/admin/users.py:249
+#: kallithea/controllers/admin/users.py:247
 msgid "5 minutes"
 msgstr "5 λεπτά"
 
 #: kallithea/controllers/admin/gists.py:61
 #: kallithea/controllers/admin/my_account.py:234
-#: kallithea/controllers/admin/users.py:250
+#: kallithea/controllers/admin/users.py:248
 msgid "1 hour"
 msgstr "1 ώρα"
 
 #: kallithea/controllers/admin/gists.py:62
 #: kallithea/controllers/admin/my_account.py:235
-#: kallithea/controllers/admin/users.py:251
+#: kallithea/controllers/admin/users.py:249
 msgid "1 day"
 msgstr "1 ημέρα"
 
 #: kallithea/controllers/admin/gists.py:63
 #: kallithea/controllers/admin/my_account.py:236
-#: kallithea/controllers/admin/users.py:252
+#: kallithea/controllers/admin/users.py:250
 msgid "1 month"
 msgstr "1 μήνας"
 
 #: kallithea/controllers/admin/gists.py:67
 #: kallithea/controllers/admin/my_account.py:238
-#: kallithea/controllers/admin/users.py:254
+#: kallithea/controllers/admin/users.py:252
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:65
 #: kallithea/templates/admin/users/user_edit_api_keys.html:65
 msgid "Lifetime"
 msgstr "Διάρκεια ζωής"
 
-#: kallithea/controllers/admin/gists.py:142
+#: kallithea/controllers/admin/gists.py:148
 msgid "Error occurred during gist creation"
-msgstr "Συνέβη μία βλάβη κατά τη δημιουργία του gist"
-
-#: kallithea/controllers/admin/gists.py:158
+msgstr "Παρουσιάστηκε σφάλμα κατά τη δημιουργία του gist"
+
+#: kallithea/controllers/admin/gists.py:164
 #, python-format
 msgid "Deleted gist %s"
 msgstr "Διαγράφηκε το gist %s"
 
-#: kallithea/controllers/admin/gists.py:198
+#: kallithea/controllers/admin/gists.py:207
 msgid "Unmodified"
 msgstr "Mη τροποποιημένo"
 
-#: kallithea/controllers/admin/gists.py:228
+#: kallithea/controllers/admin/gists.py:237
 msgid "Successfully updated gist content"
 msgstr "Το περιεχόμενο του gist ενημερώθηκε επιτυχώς"
 
-#: kallithea/controllers/admin/gists.py:233
+#: kallithea/controllers/admin/gists.py:242
 msgid "Successfully updated gist data"
 msgstr "Τα δεδομένα του gist ενημερώθηκαν επιτυχώς"
 
-#: kallithea/controllers/admin/gists.py:236
+#: kallithea/controllers/admin/gists.py:245
 #, python-format
 msgid "Error occurred during update of gist %s"
 msgstr "Σφάλμα συνέβη κατά την ενημέρωση του gist %s"
 
-#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:211
-#: kallithea/model/user.py:232
+#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:205
+#: kallithea/model/user.py:226
 msgid "You can't edit this user since it's crucial for entire application"
 msgstr ""
 "Δεν μπορείτε να επεξεργαστείτε αυτόν το χρήστη καθώς είναι κρίσιμος για "
@@ -583,7 +578,7 @@
 msgstr "Ο λογαριασμός σας ενημερώθηκε επιτυχώς"
 
 #: kallithea/controllers/admin/my_account.py:134
-#: kallithea/controllers/admin/users.py:181
+#: kallithea/controllers/admin/users.py:179
 #, python-format
 msgid "Error occurred during update of user %s"
 msgstr "Συνέβη ένα σφάλμα κατά την ενημέρωση του χρήστη %s"
@@ -593,49 +588,46 @@
 msgstr "Συνέβη ένα σφάλμα κατά την ενημέρωση του κωδικού του χρήστη"
 
 #: kallithea/controllers/admin/my_account.py:209
-#: kallithea/controllers/admin/users.py:367
+#: kallithea/controllers/admin/users.py:365
 #, python-format
 msgid "Added email %s to user"
 msgstr "Προστέθηκε το email %s στον χρήστη"
 
 #: kallithea/controllers/admin/my_account.py:215
-#: kallithea/controllers/admin/users.py:373
+#: kallithea/controllers/admin/users.py:371
 msgid "An error occurred during email saving"
 msgstr "Συνέβη ένα σφάλμα κατά την αποθήκευση του email"
 
 #: kallithea/controllers/admin/my_account.py:224
-#: kallithea/controllers/admin/users.py:383
+#: kallithea/controllers/admin/users.py:381
 msgid "Removed email from user"
 msgstr "Αφαιρέθηκε το email από τον χρήστη"
 
 #: kallithea/controllers/admin/my_account.py:248
-#: kallithea/controllers/admin/users.py:271
+#: kallithea/controllers/admin/users.py:269
 msgid "API key successfully created"
 msgstr "Το API κλειδί δημιουργήθηκε επιτυχώς"
 
 #: kallithea/controllers/admin/my_account.py:257
-#: kallithea/controllers/admin/users.py:281
+#: kallithea/controllers/admin/users.py:279
 msgid "API key successfully reset"
 msgstr "Το API κλειδί επαναφέρθηκε επιτυχώς"
 
 #: kallithea/controllers/admin/my_account.py:261
-#: kallithea/controllers/admin/users.py:285
+#: kallithea/controllers/admin/users.py:283
 msgid "API key successfully deleted"
 msgstr "Το API κλειδί διαγράφηκε επιτυχώς"
 
 #: kallithea/controllers/admin/my_account.py:281
-#: kallithea/controllers/admin/users.py:456
-#, fuzzy, python-format
-#| msgid "API key successfully created"
+#: kallithea/controllers/admin/users.py:454
+#, python-format
 msgid "SSH key %s successfully added"
-msgstr "Το API κλειδί δημιουργήθηκε επιτυχώς"
+msgstr "Το SSH κλειδί %s δημιουργήθηκε επιτυχώς"
 
 #: kallithea/controllers/admin/my_account.py:293
-#: kallithea/controllers/admin/users.py:470
-#, fuzzy
-#| msgid "API key successfully deleted"
+#: kallithea/controllers/admin/users.py:468
 msgid "SSH key successfully deleted"
-msgstr "Το API κλειδί διαγράφηκε επιτυχώς"
+msgstr "Το SSH κλειδί διαγράφηκε επιτυχώς"
 
 #: kallithea/controllers/admin/permissions.py:65
 #: kallithea/controllers/admin/permissions.py:69
@@ -709,11 +701,11 @@
 msgid "Allowed with automatic account activation"
 msgstr "Επιτρέπεται με αυτόματη ενεργοποίηση του λογαριασμού"
 
-#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1673
+#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1578
 msgid "Manual activation of external account"
 msgstr "Χειροποίητη ενεργοποίηση εξωτερικού λογαριασμού"
 
-#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1674
+#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1579
 msgid "Automatic activation of external account"
 msgstr "Αυτόματη ενεργοποίηση εξωτερικού λογαριασμού"
 
@@ -733,784 +725,809 @@
 
 #: kallithea/controllers/admin/permissions.py:142
 msgid "Error occurred during update of permissions"
-msgstr "Συνέβει μια βλάβη κατά την ενημέρωση των δικαιωμάτων"
+msgstr "Παρουσιάστηκε σφάλμα κατά την ενημέρωση δικαιωμάτων"
+
+#: kallithea/controllers/admin/repo_groups.py:165
+#, python-format
+msgid "Error occurred during creation of repository group %s"
+msgstr "Συνέβηκε κάποιο λάθος κατά την δημιουργία της ομάδας αποθετηρίου %s"
 
 #: kallithea/controllers/admin/repo_groups.py:172
 #, python-format
-msgid "Error occurred during creation of repository group %s"
-msgstr "Συνέβει μια βλάβη κατά την δημιουργία της ομάδας αποθετηρίου %s"
-
-#: kallithea/controllers/admin/repo_groups.py:177
-#, python-format
 msgid "Created repository group %s"
 msgstr "Δημιουργήθηκε η ομάδα αποθετηρίου %s"
 
-#: kallithea/controllers/admin/repo_groups.py:224
+#: kallithea/controllers/admin/repo_groups.py:219
 #, python-format
 msgid "Updated repository group %s"
 msgstr "Ενημερώθηκε η ομάδα αποθετηρίου %s"
 
-#: kallithea/controllers/admin/repo_groups.py:240
+#: kallithea/controllers/admin/repo_groups.py:235
 #, python-format
 msgid "Error occurred during update of repository group %s"
-msgstr "Συνέβει μια βλάβη κατά την ενημέρωση της ομάδας αποθετηρίου %s"
-
-#: kallithea/controllers/admin/repo_groups.py:250
+msgstr "Παρουσιάστηκε σφάλμα κατά την ενημέρωση της ομάδας αποθετηρίων %s"
+
+#: kallithea/controllers/admin/repo_groups.py:245
 #, python-format
 msgid "This group contains %s repositories and cannot be deleted"
 msgstr "Αυτή η ομάδα περιέχει %s αποθετήρια και δε μπορεί να διαγραφεί"
 
-#: kallithea/controllers/admin/repo_groups.py:257
+#: kallithea/controllers/admin/repo_groups.py:252
 #, python-format
 msgid "This group contains %s subgroups and cannot be deleted"
 msgstr "Αυτή η ομάδα περιέχει %s υποομάδες και δε μπορεί να διαγραφεί"
 
-#: kallithea/controllers/admin/repo_groups.py:263
+#: kallithea/controllers/admin/repo_groups.py:258
 #, python-format
 msgid "Removed repository group %s"
 msgstr "Αφαιρέθηκε η ομάδα αποθετηρίου %s"
 
-#: kallithea/controllers/admin/repo_groups.py:268
+#: kallithea/controllers/admin/repo_groups.py:263
 #, python-format
 msgid "Error occurred during deletion of repository group %s"
-msgstr "Συνέβει μια βλάβη κατά την διαγραφή της ομάδας αποθετηρίου %s"
-
-#: kallithea/controllers/admin/repo_groups.py:352
-#: kallithea/controllers/admin/repo_groups.py:382
-#: kallithea/controllers/admin/user_groups.py:294
+msgstr "Παρουσιάστηκε σφάλμα κατά τη διαγραφή της ομάδας αποθετηρίων %s"
+
+#: kallithea/controllers/admin/repo_groups.py:347
+#: kallithea/controllers/admin/repo_groups.py:377
+#: kallithea/controllers/admin/user_groups.py:290
 msgid "Cannot revoke permission for yourself as admin"
 msgstr "Δεν μπορείτε να ανακαλέσετε την άδεια σας ως διαχειριστής"
 
-#: kallithea/controllers/admin/repo_groups.py:367
+#: kallithea/controllers/admin/repo_groups.py:362
 msgid "Repository group permissions updated"
 msgstr "Τα δικαιώματα της ομάδας αποθετηρίου ενημερώθηκαν"
 
-#: kallithea/controllers/admin/repo_groups.py:399
-#: kallithea/controllers/admin/repos.py:358
-#: kallithea/controllers/admin/user_groups.py:306
+#: kallithea/controllers/admin/repo_groups.py:394
+#: kallithea/controllers/admin/repos.py:357
+#: kallithea/controllers/admin/user_groups.py:302
 msgid "An error occurred during revoking of permission"
-msgstr "Συνέβει μια βλάβη κατά την ανάκληση της άδειας"
-
-#: kallithea/controllers/admin/repos.py:136
+msgstr "Παρουσιάστηκε σφάλμα κατά την ανάκληση του δικαιώματος"
+
+#: kallithea/controllers/admin/repos.py:137
 #, python-format
 msgid "Error creating repository %s"
-msgstr "Βλάβη κατά τη δημιουργία του αποθετηρίου %s"
-
-#: kallithea/controllers/admin/repos.py:194
+msgstr "Σφάλμα κατά τη δημιουργία αποθετηρίου %s"
+
+#: kallithea/controllers/admin/repos.py:193
 #, python-format
 msgid "Created repository %s from %s"
 msgstr "Δημιουργήθηκε το αποθετήριο %s από το %s"
 
-#: kallithea/controllers/admin/repos.py:203
+#: kallithea/controllers/admin/repos.py:202
 #, python-format
 msgid "Forked repository %s as %s"
 msgstr "Κλωνοποιήθηκε το αποθετηρίο %s ως %s"
 
-#: kallithea/controllers/admin/repos.py:206
+#: kallithea/controllers/admin/repos.py:205
 #, python-format
 msgid "Created repository %s"
 msgstr "Δημιουργήθηκε το αποθετήριο %s"
 
-#: kallithea/controllers/admin/repos.py:235
+#: kallithea/controllers/admin/repos.py:234
 #, python-format
 msgid "Repository %s updated successfully"
 msgstr "Το αποθετήριο %s ενημερώθηκε επιτυχώς"
 
-#: kallithea/controllers/admin/repos.py:255
+#: kallithea/controllers/admin/repos.py:254
 #, python-format
 msgid "Error occurred during update of repository %s"
-msgstr "Συνέβει μια βλάβη κατά την ενημέρωση του αποθετηρίου %s"
-
-#: kallithea/controllers/admin/repos.py:273
+msgstr "Παρουσιάστηκε σφάλμα κατά την ενημέρωση του αποθετηρίου %s"
+
+#: kallithea/controllers/admin/repos.py:272
 #, python-format
 msgid "Detached %s forks"
 msgstr "Αποσυνδέθηκαν %s κλώνοι"
 
-#: kallithea/controllers/admin/repos.py:276
+#: kallithea/controllers/admin/repos.py:275
 #, python-format
 msgid "Deleted %s forks"
 msgstr "Διαγράφηκαν %s κλώνοι"
 
-#: kallithea/controllers/admin/repos.py:281
+#: kallithea/controllers/admin/repos.py:280
 #, python-format
 msgid "Deleted repository %s"
 msgstr "Διαγράφηκε το αποθετήριο %s"
 
-#: kallithea/controllers/admin/repos.py:284
+#: kallithea/controllers/admin/repos.py:283
 #, python-format
 msgid "Cannot delete repository %s which still has forks"
 msgstr "Δε μπορεί να διαγραφεί το αποθετήριο %s που ακόμα έχει κλώνους"
 
-#: kallithea/controllers/admin/repos.py:289
+#: kallithea/controllers/admin/repos.py:288
 #, python-format
 msgid "An error occurred during deletion of %s"
-msgstr "Συνέβει μια βλάβη κατά την διαγραφή του %s"
-
-#: kallithea/controllers/admin/repos.py:329
+msgstr "Παρουσιάστηκε σφάλμα κατά την διαγραφή του %s"
+
+#: kallithea/controllers/admin/repos.py:328
 msgid "Repository permissions updated"
 msgstr "Τα δικαιώματα του αποθετηρίου ενημερώθηκαν"
 
-#: kallithea/controllers/admin/repos.py:388
+#: kallithea/controllers/admin/repos.py:387
 #, python-format
 msgid "Field validation error: %s"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:391
-#, fuzzy, python-format
-#| msgid "An error occurred during creation of field"
+msgstr "Σφάλμα στην επιβεβαίωση του πεδίου: %s"
+
+#: kallithea/controllers/admin/repos.py:390
+#, python-format
 msgid "An error occurred during creation of field: %r"
-msgstr "Συνέβει μια βλάβη κατά τη δημιουργία του πεδίου"
-
-#: kallithea/controllers/admin/repos.py:402
+msgstr "Παρουσιάστηκε σφάλμα κατά τη δημιουργία πεδίου: %r"
+
+#: kallithea/controllers/admin/repos.py:401
 msgid "An error occurred during removal of field"
-msgstr "Συνέβει μια βλάβη κατά την απομάκρυνση του πεδίου"
-
-#: kallithea/controllers/admin/repos.py:416
+msgstr "Παρουσιάστηκε σφάλμα κατά την απομάκρυνση του πεδίου"
+
+#: kallithea/controllers/admin/repos.py:415
 msgid "-- Not a fork --"
 msgstr "-- Όχι κλώνος --"
 
-#: kallithea/controllers/admin/repos.py:448
+#: kallithea/controllers/admin/repos.py:447
 msgid "Updated repository visibility in public journal"
 msgstr "Ενημερώθηκε η ορατότητα του αποθετηρίου στο δημόσιο ημερολόγιο"
 
-#: kallithea/controllers/admin/repos.py:452
+#: kallithea/controllers/admin/repos.py:451
 msgid "An error occurred during setting this repository in public journal"
 msgstr ""
-"Συνέβει μια βλάβη κατά την τοποθέτηση αυτού το αποθετηρίου στο δημόσιο "
+"Παρουσιάστηκε σφάλμα κατά την τοποθέτηση αυτού το αποθετηρίου στο δημόσιο "
 "ημερολόγιο"
 
-#: kallithea/controllers/admin/repos.py:468
+#: kallithea/controllers/admin/repos.py:467
 msgid "Nothing"
 msgstr "Χωρίς"
 
-#: kallithea/controllers/admin/repos.py:470
+#: kallithea/controllers/admin/repos.py:469
 #, python-format
 msgid "Marked repository %s as fork of %s"
 msgstr "Σημειώθηκε το αποθετήριο %s σαν κλώνος του %s"
 
-#: kallithea/controllers/admin/repos.py:477
+#: kallithea/controllers/admin/repos.py:476
 msgid "An error occurred during this operation"
 msgstr "Παρουσιάστηκε ένα σφάλμα κατά τη διάρκεια αυτής της λειτουργίας"
 
-#: kallithea/controllers/admin/repos.py:490
-msgid "Cache invalidation successful"
-msgstr "Η ακύρωση της cache ήταν επιτυχής"
-
-#: kallithea/controllers/admin/repos.py:494
-msgid "An error occurred during cache invalidation"
-msgstr "Παρουσιάστηκε ένα σφάλμα κατά τη διάρκεια ακύρωσης της cache"
-
-#: kallithea/controllers/admin/repos.py:507
+#: kallithea/controllers/admin/repos.py:488
 msgid "Pulled from remote location"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:510
+msgstr "Ελκύσθηκε από απομακρυσμένη τοποθεσία"
+
+#: kallithea/controllers/admin/repos.py:491
 msgid "An error occurred during pull from remote location"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:541
+msgstr "Παρουσιάστηκε σφάλμα κατά την έλξη από την απομακρυσμένη τοποθεσία"
+
+#: kallithea/controllers/admin/repos.py:522
 msgid "An error occurred during deletion of repository stats"
 msgstr ""
-
-#: kallithea/controllers/admin/settings.py:131
+"Παρουσιάστηκε σφάλμα κατά τη διαγραφή των στατιστικών του αποθετηρίου"
+
+#: kallithea/controllers/admin/settings.py:132
 msgid "Updated VCS settings"
-msgstr ""
-
-#: kallithea/controllers/admin/settings.py:135 kallithea/lib/utils.py:237
+msgstr "Ενημερωμένες ρυθμίσεις VCS"
+
+#: kallithea/controllers/admin/settings.py:136
 msgid ""
 "Unable to activate hgsubversion support. The \"hgsubversion\" library is "
 "missing"
 msgstr ""
-
-#: kallithea/controllers/admin/settings.py:141
-#: kallithea/controllers/admin/settings.py:233
+"Δεν γίνεται να ενεργοποιηθεί υποστήριξη για το hgsubversion. Λείπει η "
+"βιβλιοθήκη \"hgsubversion\""
+
+#: kallithea/controllers/admin/settings.py:142
+#: kallithea/controllers/admin/settings.py:234
 msgid "Error occurred while updating application settings"
-msgstr ""
-
-#: kallithea/controllers/admin/settings.py:176
+msgstr "Παρουσιάστηκε σφάλμα κατά την ενημέρωση των ρυθμίσεων της εφαρμογής"
+
+#: kallithea/controllers/admin/settings.py:177
 #, python-format
 msgid "Repositories successfully rescanned. Added: %s. Removed: %s."
 msgstr ""
-
-#: kallithea/controllers/admin/settings.py:188
-#, fuzzy, python-format
-#| msgid "Deleted repository %s"
+"Τα αποθετήρια ξανασαρώθηκαν επιτυχώς. Προστέθηκαν: %s. Αφαιρέθηκαν %s."
+
+#: kallithea/controllers/admin/settings.py:189
+#, python-format
 msgid "Invalidated %s repositories"
-msgstr "Διαγράφηκε το αποθετήριο %s"
-
-#: kallithea/controllers/admin/settings.py:229
+msgstr "Ακυρώθηκαν %s αποθετήρια"
+
+#: kallithea/controllers/admin/settings.py:230
 msgid "Updated application settings"
-msgstr ""
-
-#: kallithea/controllers/admin/settings.py:283
+msgstr "Ενημερώθηκαν οι ρυθμίσεις της εφαρμογής"
+
+#: kallithea/controllers/admin/settings.py:284
 msgid "Updated visualisation settings"
-msgstr ""
-
-#: kallithea/controllers/admin/settings.py:288
+msgstr "Ενημερώθηκαν οι ρυθμίσεις της απεικόνισης"
+
+#: kallithea/controllers/admin/settings.py:289
 msgid "Error occurred during updating visualisation settings"
-msgstr ""
-
-#: kallithea/controllers/admin/settings.py:312
+msgstr "Παρουσιάστηκε σφάλμα κατά την ενημέρωση των ρυθμίσεων απεικόνισης"
+
+#: kallithea/controllers/admin/settings.py:313
 msgid "Please enter email address"
-msgstr ""
-
-#: kallithea/controllers/admin/settings.py:327
+msgstr "Παρακαλώ εισάγετε την διεύθυνση ηλεκτρονικού ταχυδρομείου"
+
+#: kallithea/controllers/admin/settings.py:328
 msgid "Send email task created"
-msgstr ""
-
-#: kallithea/controllers/admin/settings.py:355
-#, fuzzy
-#| msgid "No data ready yet"
+msgstr "Δημιουργήθηκε η εργασία της αποστολής ηλεκτρονικού ταχυδρομείου"
+
+#: kallithea/controllers/admin/settings.py:356
 msgid "Hook already exists"
-msgstr "Δεν υπάρχουν ακόμα έτοιμα δεδομένα"
-
-#: kallithea/controllers/admin/settings.py:357
+msgstr "Το άγκιστρο υπάρχει ήδη"
+
+#: kallithea/controllers/admin/settings.py:358
 msgid "Builtin hooks are read-only. Please use another hook name."
 msgstr ""
-
-#: kallithea/controllers/admin/settings.py:360
+"Τα ενσωματωμένα άγκιστρα είναι μόνο για ανάγνωση. Παρακαλώ δώστε άλλο "
+"όνομα στο άγκιστρο."
+
+#: kallithea/controllers/admin/settings.py:361
 msgid "Added new hook"
-msgstr ""
-
-#: kallithea/controllers/admin/settings.py:376
+msgstr "Προσθήκη νέου άγκιστρου"
+
+#: kallithea/controllers/admin/settings.py:377
 msgid "Updated hooks"
-msgstr ""
-
-#: kallithea/controllers/admin/settings.py:380
+msgstr "Τα άγκιστρα ενημερώθηκαν"
+
+#: kallithea/controllers/admin/settings.py:381
 msgid "Error occurred during hook creation"
-msgstr ""
-
-#: kallithea/controllers/admin/settings.py:404
+msgstr "Παρουσιάστηκε σφάλμα κατά την δημιουργία του άγκιστρου"
+
+#: kallithea/controllers/admin/settings.py:405
 msgid "Whoosh reindex task scheduled"
-msgstr ""
-
-#: kallithea/controllers/admin/user_groups.py:138
+msgstr "Προγραμματίστηκε η αναδημιουργία ευρετηρίου για το Whoosh"
+
+#: kallithea/controllers/admin/user_groups.py:134
 #, python-format
 msgid "Created user group %s"
-msgstr ""
-
-#: kallithea/controllers/admin/user_groups.py:151
+msgstr "Δημιουργήθηκε η ομάδα χρηστών %s"
+
+#: kallithea/controllers/admin/user_groups.py:147
 #, python-format
 msgid "Error occurred during creation of user group %s"
-msgstr ""
-
-#: kallithea/controllers/admin/user_groups.py:179
+msgstr "Παρουσιάστηκε σφάλμα κατά τη δημιουργία της ομάδας χρηστών %s"
+
+#: kallithea/controllers/admin/user_groups.py:175
 #, python-format
 msgid "Updated user group %s"
-msgstr ""
-
-#: kallithea/controllers/admin/user_groups.py:201
+msgstr "Ενημερώθηκε η ομάδα χρηστών %s"
+
+#: kallithea/controllers/admin/user_groups.py:197
 #, python-format
 msgid "Error occurred during update of user group %s"
-msgstr ""
-
-#: kallithea/controllers/admin/user_groups.py:212
+msgstr "Παρουσιάστηκε σφάλμα κατά την ενημέρωση της ομάδας χρηστών %s"
+
+#: kallithea/controllers/admin/user_groups.py:208
 msgid "Successfully deleted user group"
-msgstr ""
-
-#: kallithea/controllers/admin/user_groups.py:217
+msgstr "Η ομάδα χρηστών διαγράφηκε επιτυχώς"
+
+#: kallithea/controllers/admin/user_groups.py:213
 msgid "An error occurred during deletion of user group"
-msgstr ""
-
-#: kallithea/controllers/admin/user_groups.py:273
+msgstr "Παρουσιάστηκε σφάλμα κατά την διαγραφή της ομάδας χρηστών"
+
+#: kallithea/controllers/admin/user_groups.py:269
 msgid "Target group cannot be the same"
-msgstr ""
-
-#: kallithea/controllers/admin/user_groups.py:279
+msgstr "Η ομάδα προορισμός δεν μπορεί να είναι η ίδια"
+
+#: kallithea/controllers/admin/user_groups.py:275
 msgid "User group permissions updated"
-msgstr ""
+msgstr "Τα δικαιώματα της ομάδας χρηστών ενημερώθηκαν"
+
+#: kallithea/controllers/admin/user_groups.py:384
+#: kallithea/controllers/admin/users.py:336
+msgid "Updated permissions"
+msgstr "Τα δικαιώματα ενημερώθηκαν"
 
 #: kallithea/controllers/admin/user_groups.py:388
-#: kallithea/controllers/admin/users.py:338
-msgid "Updated permissions"
-msgstr ""
-
-#: kallithea/controllers/admin/user_groups.py:392
-#: kallithea/controllers/admin/users.py:342
+#: kallithea/controllers/admin/users.py:340
 msgid "An error occurred during permissions saving"
-msgstr ""
-
-#: kallithea/controllers/admin/users.py:123
+msgstr "Παρουσιάστηκε σφάλμα κατά την αποθήκευση των δικαιωμάτων"
+
+#: kallithea/controllers/admin/users.py:121
 #, python-format
 msgid "Created user %s"
-msgstr ""
-
-#: kallithea/controllers/admin/users.py:138
+msgstr "Δημιουργήθηκε ο χρήστης %s"
+
+#: kallithea/controllers/admin/users.py:136
 #, python-format
 msgid "Error occurred during creation of user %s"
-msgstr ""
-
-#: kallithea/controllers/admin/users.py:162
+msgstr "Παρουσιάστηκε σφάλμα κατά την δημιουργία του χρήστη %s"
+
+#: kallithea/controllers/admin/users.py:160
 msgid "User updated successfully"
-msgstr ""
-
-#: kallithea/controllers/admin/users.py:190
+msgstr "Ο χρήστης ενημερώθηκε επιτυχώς"
+
+#: kallithea/controllers/admin/users.py:188
 msgid "Successfully deleted user"
-msgstr ""
-
-#: kallithea/controllers/admin/users.py:195
+msgstr "Ο χρήστης διαγράφηκε επιτυχώς"
+
+#: kallithea/controllers/admin/users.py:193
 msgid "An error occurred during deletion of user"
-msgstr ""
-
-#: kallithea/controllers/admin/users.py:203
+msgstr "Παρουσιάστηκε σφάλμα κατά τη διαγραφή του χρήστη"
+
+#: kallithea/controllers/admin/users.py:201
 msgid "The default user cannot be edited"
-msgstr ""
-
-#: kallithea/controllers/admin/users.py:409
+msgstr "Δεν μπορεί να γίνει επεξεργασία στον προεπιλεγμένο χρήστη"
+
+#: kallithea/controllers/admin/users.py:407
 #, python-format
 msgid "Added IP address %s to user whitelist"
-msgstr ""
-
-#: kallithea/controllers/admin/users.py:415
+msgstr "Η IP διεύθυνση %s προστέθηκε στην λίστα επιτρεπόμενων του χρήστη"
+
+#: kallithea/controllers/admin/users.py:413
 msgid "An error occurred while adding IP address"
-msgstr ""
-
-#: kallithea/controllers/admin/users.py:427
+msgstr "Παρουσιάστηκε σφάλμα κατά την προσθήκη της IP διεύθυνσης"
+
+#: kallithea/controllers/admin/users.py:425
 msgid "Removed IP address from user whitelist"
-msgstr ""
-
-#: kallithea/lib/auth.py:684
+msgstr "Η IP διεύθυνση αφαιρέθηκε από τη λίστα επιτρεπόμενων του χρήστη"
+
+#: kallithea/lib/auth.py:634
 msgid "You need to be a registered user to perform this action"
 msgstr ""
-
-#: kallithea/lib/auth.py:712
+"Πρέπει να είστε εγγεγραμμένος χρήστης για να εκτελέσετε αυτή την ενέργεια"
+
+#: kallithea/lib/auth.py:662
 msgid "You need to be signed in to view this page"
-msgstr ""
+msgstr "Πρέπει να είστε συνδεμένος για να δείτε αυτήν τη σελίδα"
 
 #: kallithea/lib/base.py:483
 msgid ""
 "CSRF token leak has been detected - all form tokens have been expired"
 msgstr ""
+"Εντοπίστηκε διαρροή ενός διακριτικού CSRF - όλα τα διακριτικά της φόρμας "
+"έχουν λήξει"
 
 #: kallithea/lib/base.py:580
 msgid "Repository not found in the filesystem"
-msgstr ""
+msgstr "Το αποθετήριο δε βρέθηκε στο σύστημα αρχείων"
 
 #: kallithea/lib/base.py:605
 #, python-format
 msgid "Changeset for %s %s not found in %s"
-msgstr ""
+msgstr "Το σετ αλλαγών για %s %sδεν βρέθηκε στο %s"
 
 #: kallithea/lib/base.py:647
 msgid "SSH access is disabled."
-msgstr ""
+msgstr "Η πρόσβαση μέσω SSH είναι απενεργοποιημένη."
 
 #: kallithea/lib/diffs.py:194
 msgid "Binary file"
-msgstr ""
+msgstr "Δυαδικό αρχείο"
 
 #: kallithea/lib/diffs.py:214
 msgid ""
 "Changeset was too big and was cut off, use diff menu to display this diff"
 msgstr ""
-
-#: kallithea/lib/diffs.py:224
+"Το σετ αλλαγών ήταν πολύ μεγάλο και αποκόπηκε, χρησιμοποιήστε το μενού "
+"διαφορών για να εμφανίσετε τις διαφορές"
+
+#: kallithea/lib/diffs.py:223
 msgid "No changes detected"
-msgstr ""
-
-#: kallithea/lib/helpers.py:653
+msgstr "Δεν εντοπίστηκαν αλλαγές"
+
+#: kallithea/lib/helpers.py:670
 #, python-format
 msgid "Deleted branch: %s"
-msgstr ""
-
-#: kallithea/lib/helpers.py:655
+msgstr "Διαγραφή κλάδου: %s"
+
+#: kallithea/lib/helpers.py:672
 #, python-format
 msgid "Created tag: %s"
-msgstr ""
-
-#: kallithea/lib/helpers.py:666
+msgstr "Δημιουργηθείσα ετικέτα: %s"
+
+#: kallithea/lib/helpers.py:683
 #, python-format
 msgid "Changeset %s not found"
-msgstr ""
-
-#: kallithea/lib/helpers.py:715
+msgstr "Δεν βρέθηκε το σετ αλλαγών %s"
+
+#: kallithea/lib/helpers.py:732
 #, python-format
 msgid "Show all combined changesets %s->%s"
-msgstr ""
-
-#: kallithea/lib/helpers.py:721
+msgstr "Εμφάνιση όλων των συνδυασμένων σετ αλλαγών %s->%s"
+
+#: kallithea/lib/helpers.py:738
 msgid "Compare view"
-msgstr ""
-
-#: kallithea/lib/helpers.py:740
+msgstr "Σύγκριση εμφάνισης"
+
+#: kallithea/lib/helpers.py:757
 msgid "and"
-msgstr ""
-
-#: kallithea/lib/helpers.py:741
+msgstr "και"
+
+#: kallithea/lib/helpers.py:758
 #, python-format
 msgid "%s more"
-msgstr ""
-
-#: kallithea/lib/helpers.py:742
+msgstr "%s επιπλέον"
+
+#: kallithea/lib/helpers.py:759
 #: kallithea/templates/changelog/changelog.html:43
 msgid "revisions"
-msgstr ""
-
-#: kallithea/lib/helpers.py:766
+msgstr "αναθεωρήσεις"
+
+#: kallithea/lib/helpers.py:783
 #, python-format
 msgid "Fork name %s"
-msgstr ""
-
-#: kallithea/lib/helpers.py:787
+msgstr "Όνομα κλώνου %s"
+
+#: kallithea/lib/helpers.py:804
 #, python-format
 msgid "Pull request %s"
-msgstr ""
-
-#: kallithea/lib/helpers.py:797
+msgstr "Αίτημα έλξης %s"
+
+#: kallithea/lib/helpers.py:814
 msgid "[deleted] repository"
-msgstr ""
-
-#: kallithea/lib/helpers.py:799 kallithea/lib/helpers.py:811
+msgstr "[διαγραμμένο] αποθετήριο"
+
+#: kallithea/lib/helpers.py:816 kallithea/lib/helpers.py:828
 msgid "[created] repository"
-msgstr ""
-
-#: kallithea/lib/helpers.py:801
+msgstr "[δημιουργημένο] αποθετήριο"
+
+#: kallithea/lib/helpers.py:818
 msgid "[created] repository as fork"
 msgstr ""
 
-#: kallithea/lib/helpers.py:803 kallithea/lib/helpers.py:813
+#: kallithea/lib/helpers.py:820 kallithea/lib/helpers.py:830
 msgid "[forked] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:805 kallithea/lib/helpers.py:815
+#: kallithea/lib/helpers.py:822 kallithea/lib/helpers.py:832
 msgid "[updated] repository"
-msgstr ""
-
-#: kallithea/lib/helpers.py:807
+msgstr "[ενημερωμένο] αποθετήριο"
+
+#: kallithea/lib/helpers.py:824
 msgid "[downloaded] archive from repository"
-msgstr ""
-
-#: kallithea/lib/helpers.py:809
+msgstr "[λήψη] αρχείο από το αποθετήριο"
+
+#: kallithea/lib/helpers.py:826
 msgid "[delete] repository"
-msgstr ""
-
-#: kallithea/lib/helpers.py:817
+msgstr "[διαγραμμένο] αποθετήριο"
+
+#: kallithea/lib/helpers.py:834
 msgid "[created] user"
 msgstr ""
 
-#: kallithea/lib/helpers.py:819
+#: kallithea/lib/helpers.py:836
 msgid "[updated] user"
 msgstr ""
 
-#: kallithea/lib/helpers.py:821
+#: kallithea/lib/helpers.py:838
 msgid "[created] user group"
 msgstr ""
 
-#: kallithea/lib/helpers.py:823
+#: kallithea/lib/helpers.py:840
 msgid "[updated] user group"
 msgstr ""
 
-#: kallithea/lib/helpers.py:825
+#: kallithea/lib/helpers.py:842
 msgid "[commented] on revision in repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:827
+#: kallithea/lib/helpers.py:844
 msgid "[commented] on pull request for"
 msgstr ""
 
-#: kallithea/lib/helpers.py:829
+#: kallithea/lib/helpers.py:846
 msgid "[closed] pull request for"
 msgstr ""
 
-#: kallithea/lib/helpers.py:831
+#: kallithea/lib/helpers.py:848
 msgid "[pushed] into"
 msgstr ""
 
-#: kallithea/lib/helpers.py:833
+#: kallithea/lib/helpers.py:850
 msgid "[committed via Kallithea] into repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:835
+#: kallithea/lib/helpers.py:852
 msgid "[pulled from remote] into repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:837
+#: kallithea/lib/helpers.py:854
 msgid "[pulled] from"
 msgstr ""
 
-#: kallithea/lib/helpers.py:839
+#: kallithea/lib/helpers.py:856
 msgid "[started following] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:841
+#: kallithea/lib/helpers.py:858
 msgid "[stopped following] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:961
+#: kallithea/lib/helpers.py:975
 #, python-format
 msgid " and %s more"
-msgstr ""
-
-#: kallithea/lib/helpers.py:965
+msgstr " και %s περισσότερα"
+
+#: kallithea/lib/helpers.py:979
 #: kallithea/templates/compare/compare_diff.html:69
 #: kallithea/templates/pullrequests/pullrequest_show.html:297
 msgid "No files"
-msgstr ""
-
-#: kallithea/lib/helpers.py:990
-msgid "new file"
-msgstr ""
-
-#: kallithea/lib/helpers.py:993
-msgid "mod"
-msgstr ""
-
-#: kallithea/lib/helpers.py:996
-msgid "del"
-msgstr ""
-
-#: kallithea/lib/helpers.py:999
-msgid "rename"
-msgstr ""
+msgstr "Δεν υπάρχουν αρχεία"
 
 #: kallithea/lib/helpers.py:1004
+msgid "new file"
+msgstr "νέο αρχείο"
+
+#: kallithea/lib/helpers.py:1007
+msgid "mod"
+msgstr "τροποποιημένο"
+
+#: kallithea/lib/helpers.py:1010
+msgid "del"
+msgstr "διαγραμμένο"
+
+#: kallithea/lib/helpers.py:1013
+msgid "rename"
+msgstr "μετονομασμένο"
+
+#: kallithea/lib/helpers.py:1018
 msgid "chmod"
-msgstr ""
-
-#: kallithea/lib/helpers.py:1297
+msgstr "αλλ δικαιωμ"
+
+#: kallithea/lib/helpers.py:1323
 #, python-format
 msgid ""
 "%s repository is not mapped to db perhaps it was created or renamed from "
 "the filesystem please run the application again in order to rescan "
 "repositories"
 msgstr ""
-
-#: kallithea/lib/ssh.py:73
+"Το αποθετήριο δεδομένων %s δεν έχει αντιστοιχιστεί στη βάση δεδομένων. "
+"Ίσως δημιουργήθηκε ή μετονομάστηκε από το σύστημα αρχείων. Εκτελέστε ξανά "
+"την εφαρμογή για να σαρώσετε ξανά τα αποθετήρια δεδομένων"
+
+#: kallithea/lib/ssh.py:75
 msgid "SSH key is missing"
-msgstr ""
-
-#: kallithea/lib/ssh.py:77
-msgid "Incorrect SSH key - it must have both a key type and a base64 part"
-msgstr ""
-
-#: kallithea/lib/ssh.py:81
+msgstr "Το κλειδί SSH λείπει"
+
+#: kallithea/lib/ssh.py:79
+msgid ""
+"Incorrect SSH key - it must have both a key type and a base64 part, like "
+"'ssh-rsa ASRNeaZu4FA...xlJp='"
+msgstr ""
+"Λανθασμένο κλειδί SSH - πρέπει να έχει έναν τύπο κλειδιού καθώς και ένα "
+"τμήμα base64, όπως \"ssh-rsa ASRNeaZu4FA ... xlJp =\""
+
+#: kallithea/lib/ssh.py:83
 msgid "Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'"
-msgstr ""
-
-#: kallithea/lib/ssh.py:84
+msgstr "Εσφαλμένο κλειδί SSH - πρέπει να ξεκινά με 'ssh-(rsa|dss|ed25519)'"
+
+#: kallithea/lib/ssh.py:86
 #, python-format
 msgid "Incorrect SSH key - unexpected characters in base64 part %r"
 msgstr ""
-
-#: kallithea/lib/ssh.py:89
+"Εσφαλμένο κλειδί SSH - μη αναμενόμενοι χαρακτήρες στο τμήμα base64 %r"
+
+#: kallithea/lib/ssh.py:91
 #, python-format
 msgid "Incorrect SSH key - failed to decode base64 part %r"
 msgstr ""
-
-#: kallithea/lib/ssh.py:92
+"Εσφαλμένο κλειδί SSH - απέτυχε η αποκωδικοποίηση του τμήματος base64 %r"
+
+#: kallithea/lib/ssh.py:94
 #, python-format
 msgid "Incorrect SSH key - base64 part is not %r as claimed but %r"
 msgstr ""
-
-#: kallithea/lib/utils2.py:334
+"Εσφαλμένο κλειδί SSH - το base64 μέρος δεν είναι %r όπως ζητήθηκε, αλλά %r"
+
+#: kallithea/lib/utils2.py:253
 #, python-format
 msgid "%d year"
 msgid_plural "%d years"
-msgstr[0] ""
-msgstr[1] ""
-
-#: kallithea/lib/utils2.py:335
+msgstr[0] "%d έτος"
+msgstr[1] "%d έτη"
+
+#: kallithea/lib/utils2.py:254
 #, python-format
 msgid "%d month"
 msgid_plural "%d months"
-msgstr[0] ""
-msgstr[1] ""
-
-#: kallithea/lib/utils2.py:336
+msgstr[0] "%d μήνας"
+msgstr[1] "%d μήνες"
+
+#: kallithea/lib/utils2.py:255
 #, python-format
 msgid "%d day"
 msgid_plural "%d days"
-msgstr[0] ""
-msgstr[1] ""
-
-#: kallithea/lib/utils2.py:337
+msgstr[0] "%d ημέρα"
+msgstr[1] "%d ημέρες"
+
+#: kallithea/lib/utils2.py:256
 #, python-format
 msgid "%d hour"
 msgid_plural "%d hours"
-msgstr[0] ""
-msgstr[1] ""
-
-#: kallithea/lib/utils2.py:338
+msgstr[0] "%d ώρα"
+msgstr[1] "%d ώρες"
+
+#: kallithea/lib/utils2.py:257
 #, python-format
 msgid "%d minute"
 msgid_plural "%d minutes"
-msgstr[0] ""
-msgstr[1] ""
-
-#: kallithea/lib/utils2.py:339
+msgstr[0] "%d λεπτό"
+msgstr[1] "%d λεπτά"
+
+#: kallithea/lib/utils2.py:258
 #, python-format
 msgid "%d second"
 msgid_plural "%d seconds"
-msgstr[0] ""
-msgstr[1] ""
-
-#: kallithea/lib/utils2.py:355
+msgstr[0] "%d δευτερόλεπτο"
+msgstr[1] "%d δευτερόλεπτα"
+
+#: kallithea/lib/utils2.py:274
 #, python-format
 msgid "in %s"
-msgstr ""
-
-#: kallithea/lib/utils2.py:357
+msgstr "σε %s"
+
+#: kallithea/lib/utils2.py:276
 #, python-format
 msgid "%s ago"
-msgstr ""
-
-#: kallithea/lib/utils2.py:359
+msgstr "%s πριν"
+
+#: kallithea/lib/utils2.py:278
 #, python-format
 msgid "in %s and %s"
-msgstr ""
-
-#: kallithea/lib/utils2.py:362
+msgstr "σε %s και %s"
+
+#: kallithea/lib/utils2.py:281
 #, python-format
 msgid "%s and %s ago"
-msgstr ""
-
-#: kallithea/lib/utils2.py:365
+msgstr "%s και %s πριν"
+
+#: kallithea/lib/utils2.py:284
 msgid "just now"
-msgstr ""
+msgstr "μόλις τώρα"
 
 #: kallithea/model/comment.py:68
 #, python-format
 msgid "on line %s"
-msgstr ""
-
-#: kallithea/model/comment.py:221 kallithea/model/pull_request.py:114
+msgstr "στη γραμμή %s"
+
+#: kallithea/model/comment.py:219 kallithea/model/pull_request.py:112
 msgid "[Mention]"
-msgstr ""
-
-#: kallithea/model/db.py:1496
+msgstr "[Αναφορά]"
+
+#: kallithea/model/db.py:1411
 msgid "top level"
-msgstr ""
-
-#: kallithea/model/db.py:1637
+msgstr "ανώτερο επίπεδο"
+
+#: kallithea/model/db.py:1542
 msgid "Kallithea Administrator"
-msgstr ""
-
-#: kallithea/model/db.py:1639
+msgstr "Διαχειριστής Καλλιθέας"
+
+#: kallithea/model/db.py:1544
 msgid "Default user has no access to new repositories"
-msgstr ""
-
-#: kallithea/model/db.py:1640
+msgstr "Ο προεπιλεγμένος χρήστης δεν έχει πρόσβαση σε νέα αποθετήρια"
+
+#: kallithea/model/db.py:1545
 msgid "Default user has read access to new repositories"
-msgstr ""
-
-#: kallithea/model/db.py:1641
+msgstr "Ο προεπιλεγμένος χρήστης έχει πρόσβαση ανάγνωσης σε νέα αποθετήρια"
+
+#: kallithea/model/db.py:1546
 msgid "Default user has write access to new repositories"
-msgstr ""
-
-#: kallithea/model/db.py:1642
+msgstr "Ο προεπιλεγμένος χρήστης έχει πρόσβαση εγγραφής σε νέα αποθετήρια"
+
+#: kallithea/model/db.py:1547
 msgid "Default user has admin access to new repositories"
 msgstr ""
-
-#: kallithea/model/db.py:1644
+"Ο προεπιλεγμένος χρήστης έχει πρόσβαση διαχειριστή σε νέα αποθετήρια"
+
+#: kallithea/model/db.py:1549
 msgid "Default user has no access to new repository groups"
 msgstr ""
-
-#: kallithea/model/db.py:1645
+"Ο προεπιλεγμένος χρήστης δεν έχει πρόσβαση σε νέες ομάδες αποθετηρίων"
+
+#: kallithea/model/db.py:1550
 msgid "Default user has read access to new repository groups"
 msgstr ""
-
-#: kallithea/model/db.py:1646
+"Ο προεπιλεγμένος χρήστης έχει πρόσβαση ανάγνωσης σε νέες ομάδες "
+"αποθετηρίων"
+
+#: kallithea/model/db.py:1551
 msgid "Default user has write access to new repository groups"
 msgstr ""
-
-#: kallithea/model/db.py:1647
+"Ο προεπιλεγμένος χρήστης έχει πρόσβαση εγγραφής σε νέες ομάδες αποθετηρίων"
+
+#: kallithea/model/db.py:1552
 msgid "Default user has admin access to new repository groups"
 msgstr ""
-
-#: kallithea/model/db.py:1649
+"Ο προεπιλεγμένος χρήστης έχει πρόσβαση διαχειριστή σε νέες ομάδες "
+"αποθετηρίων"
+
+#: kallithea/model/db.py:1554
 msgid "Default user has no access to new user groups"
-msgstr ""
-
-#: kallithea/model/db.py:1650
+msgstr "Ο προεπιλεγμένος χρήστης δεν έχει πρόσβαση σε νέες ομάδες χρηστών"
+
+#: kallithea/model/db.py:1555
 msgid "Default user has read access to new user groups"
 msgstr ""
-
-#: kallithea/model/db.py:1651
+"Ο προεπιλεγμένος χρήστης έχει πρόσβαση ανάγνωσης σε νέες ομάδες χρηστών"
+
+#: kallithea/model/db.py:1556
 msgid "Default user has write access to new user groups"
 msgstr ""
-
-#: kallithea/model/db.py:1652
+"Ο προεπιλεγμένος χρήστης έχει πρόσβαση εγγραφής σε νέες ομάδες χρηστών"
+
+#: kallithea/model/db.py:1557
 msgid "Default user has admin access to new user groups"
 msgstr ""
-
-#: kallithea/model/db.py:1654
+"Ο προεπιλεγμένος χρήστης έχει πρόσβαση διαχειριστή σε νέες ομάδες χρηστών"
+
+#: kallithea/model/db.py:1559
 msgid "Only admins can create repository groups"
-msgstr ""
-
-#: kallithea/model/db.py:1655
+msgstr "Μόνο οι διαχειριστές μπορούν να δημιουργήσουν ομάδες αποθετηρίων"
+
+#: kallithea/model/db.py:1560
 msgid "Non-admins can create repository groups"
-msgstr ""
-
-#: kallithea/model/db.py:1657
+msgstr "Οι μη διαχειριστές μπορούν να δημιουργήσουν ομάδες αποθετηρίων"
+
+#: kallithea/model/db.py:1562
 msgid "Only admins can create user groups"
-msgstr ""
-
-#: kallithea/model/db.py:1658
+msgstr "Μόνο οι διαχειριστές μπορούν να δημιουργήσουν ομάδες χρηστών"
+
+#: kallithea/model/db.py:1563
 msgid "Non-admins can create user groups"
-msgstr ""
-
-#: kallithea/model/db.py:1660
+msgstr "Οι μη διαχειριστές μπορούν να δημιουργήσουν ομάδες χρηστών"
+
+#: kallithea/model/db.py:1565
 msgid "Only admins can create top level repositories"
 msgstr ""
-
-#: kallithea/model/db.py:1661
+"Μόνο οι διαχειριστές μπορούν να δημιουργήσουν αποθετήρια ανώτατου επιπέδου"
+
+#: kallithea/model/db.py:1566
 msgid "Non-admins can create top level repositories"
 msgstr ""
-
-#: kallithea/model/db.py:1663
+"Οι μη διαχειριστές μπορούν να δημιουργήσουν αποθετήρια ανώτατου επιπέδου"
+
+#: kallithea/model/db.py:1568
 msgid ""
 "Repository creation enabled with write permission to a repository group"
 msgstr ""
-
-#: kallithea/model/db.py:1664
+"Η δημιουργία αποθετηρίου είναι ενεργοποιημένη με δικαιώματα εγγραφής σε "
+"μια ομάδα αποθετηρίων"
+
+#: kallithea/model/db.py:1569
 msgid ""
 "Repository creation disabled with write permission to a repository group"
 msgstr ""
-
-#: kallithea/model/db.py:1666
+"Η δημιουργία αποθετηρίου απενεργοποιήθηκε με δικαιώματα εγγραφής σε μια "
+"ομάδα αποθετηρίων"
+
+#: kallithea/model/db.py:1571
 msgid "Only admins can fork repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1667
+#: kallithea/model/db.py:1572
 msgid "Non-admins can fork repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1669
+#: kallithea/model/db.py:1574
 msgid "Registration disabled"
-msgstr ""
-
-#: kallithea/model/db.py:1670
+msgstr "Η εγγραφή απενεργοποιήθηκε"
+
+#: kallithea/model/db.py:1575
 msgid "User registration with manual account activation"
-msgstr ""
-
-#: kallithea/model/db.py:1671
+msgstr "Εγγραφή χρήστη με χειροκίνητη ενεργοποίηση λογαριασμού"
+
+#: kallithea/model/db.py:1576
 msgid "User registration with automatic account activation"
-msgstr ""
-
-#: kallithea/model/db.py:2206
+msgstr "Εγγραφή χρήστη με αυτόματη ενεργοποίηση λογαριασμού"
+
+#: kallithea/model/db.py:1992
 msgid "Not reviewed"
-msgstr ""
-
-#: kallithea/model/db.py:2207
+msgstr "Δεν έχει ελεγχθεί"
+
+#: kallithea/model/db.py:1993
 msgid "Under review"
-msgstr ""
-
-#: kallithea/model/db.py:2208
+msgstr "Υπό εξέταση"
+
+#: kallithea/model/db.py:1994
 msgid "Not approved"
-msgstr ""
-
-#: kallithea/model/db.py:2209
+msgstr "Δεν έχει εγκριθεί"
+
+#: kallithea/model/db.py:1995
 msgid "Approved"
-msgstr ""
+msgstr "Εγκρίθηκε"
 
 #: kallithea/model/forms.py:58
 msgid "Please enter a login"
-msgstr ""
+msgstr "Παρακαλώ εισάγετε ένα όνομα χρήστη"
 
 #: kallithea/model/forms.py:59
 #, python-format
 msgid "Enter a value %(min)i characters long or more"
-msgstr ""
+msgstr "Εισαγάγετε μια τιμή με μήκος %(min)i χαρακτήρες ή περισσότερους"
 
 #: kallithea/model/forms.py:67
 msgid "Please enter a password"
@@ -1519,340 +1536,360 @@
 #: kallithea/model/forms.py:68
 #, python-format
 msgid "Enter %(min)i characters or more"
-msgstr ""
+msgstr "Εισαγάγετε %(min)i χαρακτήρες ή περισσότερους"
 
 #: kallithea/model/forms.py:170
 msgid "Name must not contain only digits"
-msgstr ""
-
-#: kallithea/model/notification.py:164
+msgstr "Το όνομα δεν πρέπει να περιέχει μόνο ψηφία"
+
+#: kallithea/model/notification.py:162
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s changeset %(short_id)s \"%(message_short)s\" on "
 "%(branch)s"
 msgstr ""
 
-#: kallithea/model/notification.py:167
+#: kallithea/model/notification.py:165
 #, python-format
 msgid "New user %(new_username)s registered"
-msgstr ""
-
-#: kallithea/model/notification.py:169
+msgstr "Καταχωρήθηκε νέος χρήστης %(new_username)s"
+
+#: kallithea/model/notification.py:167
 #, python-format
 msgid ""
 "[Review] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:170
+#: kallithea/model/notification.py:168
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:183
+#: kallithea/model/notification.py:188
 msgid "Closing"
-msgstr ""
-
-#: kallithea/model/pull_request.py:73
+msgstr "Κλείσιμο"
+
+#: kallithea/model/pull_request.py:72
 #, python-format
 msgid ""
 "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s"
 msgstr ""
-
-#: kallithea/model/pull_request.py:209
-#, fuzzy
-#| msgid "Error creating pull request: %s"
+"Ο χρήστης %(user)s θέλει να αναθεωρήσετε την αίτηση έλξης %(pr_nice_id)s: "
+"%(pr_title)s"
+
+#: kallithea/model/pull_request.py:207
 msgid "Cannot create empty pull request"
-msgstr "Λάθος στη δημιουργία αιτήματος έλξης - pull request: %s"
-
-#: kallithea/model/pull_request.py:217
+msgstr "Δεν είναι δυνατή η δημιουργία κενής αίτησης έλξης"
+
+#: kallithea/model/pull_request.py:215
 #, python-format
 msgid ""
 "Cannot create pull request - criss cross merge detected, please merge a "
 "later %s revision to %s"
 msgstr ""
-
-#: kallithea/model/pull_request.py:245 kallithea/model/pull_request.py:380
+"Δεν είναι δυνατή η δημιουργία αίτησης έλξης - εντοπίστηκε διασταυρούμενη "
+"συγχώνευση, παρακαλώ συγχωνεύστε μια μεταγενέστερη αναθεώρηση %s στο %s"
+
+#: kallithea/model/pull_request.py:243 kallithea/model/pull_request.py:378
 msgid "You are not authorized to create the pull request"
-msgstr ""
-
-#: kallithea/model/pull_request.py:339
-#, fuzzy
-#| msgid "Missing changesets since the previous pull request:"
+msgstr "Δεν έχετε εξουσιοδότηση για τη δημιουργία του αιτήματος έλξης"
+
+#: kallithea/model/pull_request.py:337
 msgid "Missing changesets since the previous iteration:"
-msgstr "Ελλιπή σετ αλλαγών από την προηγούμενη αίτηση έλξης:"
-
-#: kallithea/model/pull_request.py:346
-#, fuzzy, python-format
-#| msgid "New changesets on %s %s since the previous pull request:"
+msgstr "Λείπουν σετ αλλαγών από την προηγούμενη επανάληψη:"
+
+#: kallithea/model/pull_request.py:344
+#, python-format
 msgid "New changesets on %s %s since the previous iteration:"
-msgstr "Καινούρια σετ αλλαγών στα %s %s από την προηγούμενη αίτηση έλξης:"
-
-#: kallithea/model/pull_request.py:353
-#, fuzzy
-#| msgid "Ancestor didn't change - show diff since previous version:"
+msgstr "Νέα σετ αλλαγών στο %s %s από την προηγούμενη επανάληψη:"
+
+#: kallithea/model/pull_request.py:351
 msgid "Ancestor didn't change - diff since previous iteration:"
-msgstr ""
-"Το γονικό δεν άλλαξε - εμφάνισε τις διαφορές από την προηγούμενη έκδοση:"
-
-#: kallithea/model/pull_request.py:360
-#, fuzzy, python-format
-#| msgid ""
-#| "This pull request is based on another %s revision and there is no "
-#| "simple diff."
+msgstr "Ο πρόγονος δεν άλλαξε - διαφορά από την προηγούμενη επανάληψη:"
+
+#: kallithea/model/pull_request.py:358
+#, python-format
 msgid ""
 "This iteration is based on another %s revision and there is no simple "
 "diff."
 msgstr ""
-"Αυτή η αίτηση έλξης είναι βασισμένη σε μία άλλη %s αναθεώρηση και δεν "
-"υπάρχει ένα απλό diff."
-
-#: kallithea/model/pull_request.py:362
-#, fuzzy, python-format
-#| msgid "No changes found on %s %s since previous version."
+"Αυτή η επανάληψη βασίζεται σε μια άλλη αναθεώρηση %s και δεν υπάρχει απλή "
+"διαφορά."
+
+#: kallithea/model/pull_request.py:360
+#, python-format
 msgid "No changes found on %s %s since previous iteration."
 msgstr "Δεν βρέθηκαν αλλαγές στο %s %s από την προηγούμενη έκδοση."
 
-#: kallithea/model/pull_request.py:388
+#: kallithea/model/pull_request.py:386
 #, python-format
 msgid "Closed, next iteration: %s ."
-msgstr ""
-
-#: kallithea/model/scm.py:668
+msgstr "Κλειστή, επόμενη επανάληψη: %s ."
+
+#: kallithea/model/scm.py:655
 msgid "latest tip"
-msgstr ""
-
-#: kallithea/model/ssh_key.py:56
+msgstr "τελευταία κεφαλή"
+
+#: kallithea/model/ssh_key.py:57
 #, python-format
 msgid "SSH key %r is invalid: %s"
-msgstr ""
-
-#: kallithea/model/ssh_key.py:68
+msgstr "Το κλειδί SSH %r δεν είναι έγκυρο: %s"
+
+#: kallithea/model/ssh_key.py:69
 #, python-format
 msgid "SSH key %s is already used by %s"
-msgstr ""
-
-#: kallithea/model/ssh_key.py:89
-#, python-format
-msgid "SSH key %r not found"
-msgstr ""
-
-#: kallithea/model/user.py:186
+msgstr "Το κλειδί SSH %s χρησιμοποιείται ήδη από το χρήστη %s"
+
+#: kallithea/model/ssh_key.py:88
+#, python-format
+msgid "SSH key with fingerprint %r found"
+msgstr "Βρέθηκε κλειδί SSH με δακτυλικό αποτύπωμα %r"
+
+#: kallithea/model/user.py:180
 msgid "New user registration"
-msgstr ""
-
-#: kallithea/model/user.py:250
+msgstr "Εγγραφή νέου χρήστη"
+
+#: kallithea/model/user.py:244
 msgid ""
 "You can't remove this user since it is crucial for the entire application"
 msgstr ""
-
-#: kallithea/model/user.py:255
+"Δεν μπορείτε να καταργήσετε αυτόν το χρήστη, καθώς είναι ζωτικής σημασίας "
+"για ολόκληρη την εφαρμογή"
+
+#: kallithea/model/user.py:249
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repositories and cannot be removed. Switch "
 "owners or remove those repositories: %s"
 msgstr ""
-
-#: kallithea/model/user.py:260
+"Ο χρήστης \"%s\" εξακολουθεί να κατέχει %s αποθετήρια και δεν είναι "
+"δυνατόν να αφαιρεθεί. Αλλάξτε κάτοχο ή καταργείστε αυτά τα αποθετήρια: %s"
+
+#: kallithea/model/user.py:254
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repository groups and cannot be removed. Switch "
 "owners or remove those repository groups: %s"
 msgstr ""
-
-#: kallithea/model/user.py:267
+"Ο χρήστης \"%s\" εξακολουθεί να κατέχει %s ομάδες αποθετηρίων και δεν "
+"είναι δυνατόν να αφαιρεθεί. Αλλάξτε κάτοχο ή καταργείστε αυτές τις "
+"ομάδες: %s"
+
+#: kallithea/model/user.py:261
 #, python-format
 msgid ""
 "User \"%s\" still owns %s user groups and cannot be removed. Switch "
 "owners or remove those user groups: %s"
 msgstr ""
-
-#: kallithea/model/user.py:361
+"Ο χρήστης \"%s\" εξακολουθεί να κατέχει %s ομάδες χρηστών και δεν είναι "
+"δυνατόν να αφαιρεθεί. Αλλάξτε κάτοχο ή αφαιρέστε αυτές τις ομάδες "
+"χρηστών: %s"
+
+#: kallithea/model/user.py:355
 msgid "Password reset link"
-msgstr ""
-
-#: kallithea/model/user.py:408
+msgstr "Σύνδεσμος επαναφοράς κωδικού πρόσβασης"
+
+#: kallithea/model/user.py:402
 msgid "Password reset notification"
-msgstr ""
-
-#: kallithea/model/user.py:409
+msgstr "Ειδοποίηση επαναφοράς κωδικού πρόσβασης"
+
+#: kallithea/model/user.py:403
 #, python-format
 msgid ""
 "The password to your account %s has been changed using password reset "
 "form."
 msgstr ""
-
-#: kallithea/model/validators.py:52 kallithea/model/validators.py:53
+"Ο κωδικός πρόσβασης στο λογαριασμό σας %s έχει αλλάξει χρησιμοποιώντας τη "
+"φόρμα επαναφοράς κωδικού πρόσβασης."
+
+#: kallithea/model/validators.py:53 kallithea/model/validators.py:54
 msgid "Value cannot be an empty list"
-msgstr ""
-
-#: kallithea/model/validators.py:72
+msgstr "Η τιμή δεν μπορεί να είναι μια κενή λίστα"
+
+#: kallithea/model/validators.py:73
 #, python-format
 msgid "Username \"%(username)s\" already exists"
-msgstr ""
-
-#: kallithea/model/validators.py:74
+msgstr "Το όνομα χρήστη \"%(username)s\" υπάρχει ήδη"
+
+#: kallithea/model/validators.py:75
 #, python-format
 msgid "Username \"%(username)s\" cannot be used"
-msgstr ""
-
-#: kallithea/model/validators.py:76
+msgstr "Δεν είναι δυνατή η χρήση του ονόματος χρήστη \"%(username)s\""
+
+#: kallithea/model/validators.py:77
 msgid ""
 "Username may only contain alphanumeric characters underscores, periods or "
 "dashes and must begin with an alphanumeric character or underscore"
 msgstr ""
-
-#: kallithea/model/validators.py:103
+"Το όνομα χρήστη μπορεί να περιέχει μόνο αλφαριθμητικούς χαρακτήρες, κάτω "
+"παύλες, τελείες ή παύλες και πρέπει να ξεκινά με αλφαριθμητικό χαρακτήρα "
+"ή κάτω παύλα"
+
+#: kallithea/model/validators.py:104
 msgid "The input is not valid"
-msgstr ""
-
-#: kallithea/model/validators.py:110
+msgstr "Η είσοδος δεν είναι έγκυρη"
+
+#: kallithea/model/validators.py:111
 #, python-format
 msgid "Username %(username)s is not valid"
-msgstr ""
-
-#: kallithea/model/validators.py:131
-msgid "Invalid user group name"
-msgstr ""
+msgstr "Το όνομα χρήστη %(username)s δεν είναι έγκυρο"
 
 #: kallithea/model/validators.py:132
+msgid "Invalid user group name"
+msgstr "Μη έγκυρο όνομα ομάδας χρηστών"
+
+#: kallithea/model/validators.py:133
 #, python-format
 msgid "User group \"%(usergroup)s\" already exists"
-msgstr ""
-
-#: kallithea/model/validators.py:134
+msgstr "Η ομάδα χρηστών \"%(usergroup)s\" υπάρχει ήδη"
+
+#: kallithea/model/validators.py:135
 msgid ""
 "user group name may only contain alphanumeric characters underscores, "
 "periods or dashes and must begin with alphanumeric character"
 msgstr ""
-
-#: kallithea/model/validators.py:174
-msgid "Cannot assign this group as parent"
-msgstr ""
+"Το όνομα της ομάδας χρηστών μπορεί να περιέχει μόνο αλφαριθμητικούς "
+"χαρακτήρες, κάτω παύλες, τελείες ή παύλες και πρέπει να ξεκινά με "
+"αλφαριθμητικό χαρακτήρα"
 
 #: kallithea/model/validators.py:175
+msgid "Cannot assign this group as parent"
+msgstr "Δεν είναι δυνατή η εκχώρηση αυτής της ομάδας ως γονικής"
+
+#: kallithea/model/validators.py:176
 #, python-format
 msgid "Group \"%(group_name)s\" already exists"
-msgstr ""
-
-#: kallithea/model/validators.py:177
+msgstr "Η ομάδα \"%(group_name)s\" υπάρχει ήδη"
+
+#: kallithea/model/validators.py:178
 #, python-format
 msgid "Repository with name \"%(group_name)s\" already exists"
-msgstr ""
-
-#: kallithea/model/validators.py:233
+msgstr "Το αποθετήριο με όνομα \"%(group_name)s\" υπάρχει ήδη"
+
+#: kallithea/model/validators.py:230
 msgid "Invalid characters (non-ascii) in password"
-msgstr ""
-
-#: kallithea/model/validators.py:248
+msgstr "Μη έγκυροι χαρακτήρες (μη ascii) στον κωδικό πρόσβασης"
+
+#: kallithea/model/validators.py:245
 msgid "Invalid old password"
-msgstr ""
-
-#: kallithea/model/validators.py:264
+msgstr "Ο παλιός κωδικός πρόσβασης δεν είναι έγκυρος"
+
+#: kallithea/model/validators.py:261
 msgid "Passwords do not match"
-msgstr ""
-
-#: kallithea/model/validators.py:279
+msgstr "Οι κωδικοί πρόσβασης δεν ταιριάζουν"
+
+#: kallithea/model/validators.py:276
 msgid "Invalid username or password"
-msgstr ""
-
-#: kallithea/model/validators.py:313
+msgstr "Το όνομα χρήστη ή ο κωδικός πρόσβασης δεν είναι έγκυρος"
+
+#: kallithea/model/validators.py:310
 #, python-format
 msgid "Repository name %(repo)s is not allowed"
-msgstr ""
-
-#: kallithea/model/validators.py:315
+msgstr "Δεν επιτρέπεται το %(repo)s ως όνομα του αποθετηρίου"
+
+#: kallithea/model/validators.py:312
 #, python-format
 msgid "Repository named %(repo)s already exists"
-msgstr ""
-
-#: kallithea/model/validators.py:316
+msgstr "Το αποθετήριο με το όνομα %(repo)s υπάρχει ήδη"
+
+#: kallithea/model/validators.py:313
 #, python-format
 msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
-msgstr ""
-
-#: kallithea/model/validators.py:318
+msgstr "Το αποθετήριο \"%(repo)s\" υπάρχει ήδη στην ομάδα \"%(group)s\""
+
+#: kallithea/model/validators.py:315
 #, python-format
 msgid "Repository group with name \"%(repo)s\" already exists"
-msgstr ""
-
-#: kallithea/model/validators.py:404
+msgstr "Η ομάδα αποθετηρίου με το όνομα \"%(repo)s\" υπάρχει ήδη"
+
+#: kallithea/model/validators.py:401
 msgid "Invalid repository URL"
-msgstr ""
-
-#: kallithea/model/validators.py:405
+msgstr "Μη έγκυρη διεύθυνση URL αποθετηρίου"
+
+#: kallithea/model/validators.py:402
 msgid ""
 "Invalid repository URL. It must be a valid http, https, ssh, svn+http or "
 "svn+https URL"
 msgstr ""
-
-#: kallithea/model/validators.py:430
+"Μη έγκυρη διεύθυνση URL του αποθετηρίου. Πρέπει να είναι μια έγκυρη http, "
+"https, ssh, svn+http ή svn+https διεύθυνση URL"
+
+#: kallithea/model/validators.py:427
 msgid "Fork has to be the same type as parent"
 msgstr ""
 
-#: kallithea/model/validators.py:445
+#: kallithea/model/validators.py:442
 msgid "You don't have permissions to create repository in this group"
-msgstr ""
-
-#: kallithea/model/validators.py:447
+msgstr "Δεν έχετε δικαιώματα δημιουργίας αποθετηρίου σε αυτήν την ομάδα"
+
+#: kallithea/model/validators.py:444
 msgid "no permission to create repository in root location"
-msgstr ""
-
-#: kallithea/model/validators.py:497
+msgstr "Δεν υπάρχει δικαίωμα δημιουργίας αποθετηρίου στη ριζική τοποθεσία"
+
+#: kallithea/model/validators.py:494
 msgid "You don't have permissions to create a group in this location"
-msgstr ""
-
-#: kallithea/model/validators.py:537
+msgstr "Δεν έχετε δικαιώματα δημιουργίας ομάδας σε αυτήν την τοποθεσία"
+
+#: kallithea/model/validators.py:534
 msgid "This username or user group name is not valid"
-msgstr ""
-
-#: kallithea/model/validators.py:630
+msgstr "Αυτό το όνομα χρήστη ή το όνομα ομάδας χρηστών δεν είναι έγκυρο"
+
+#: kallithea/model/validators.py:627
 msgid "This is not a valid path"
-msgstr ""
-
-#: kallithea/model/validators.py:647
+msgstr "Αυτή η διαδρομή δεν είναι έγκυρη"
+
+#: kallithea/model/validators.py:644
 msgid "This email address is already in use"
-msgstr ""
-
-#: kallithea/model/validators.py:667
+msgstr "Αυτή η διεύθυνση ηλεκτρονικού ταχυδρομείου χρησιμοποιείται ήδη"
+
+#: kallithea/model/validators.py:664
 #, python-format
 msgid "Email address \"%(email)s\" not found"
-msgstr ""
-
-#: kallithea/model/validators.py:704
+msgstr "Η διεύθυνση ηλεκτρονικού ταχυδρομείου \"%(email)s\" δεν βρέθηκε"
+
+#: kallithea/model/validators.py:701
 msgid ""
 "The LDAP Login attribute of the CN must be specified - this is the name "
 "of the attribute that is equivalent to \"username\""
 msgstr ""
-
-#: kallithea/model/validators.py:716
+"Πρέπει να προσδιοριστεί το χαρακτηριστικό LDAP Login του CN - αυτό είναι "
+"το όνομα του χαρακτηριστικού που είναι ισοδύναμο με το \"όνομα χρήστη\""
+
+#: kallithea/model/validators.py:713
 msgid "Please enter a valid IPv4 or IPv6 address"
-msgstr ""
-
-#: kallithea/model/validators.py:717
+msgstr "Παρακαλώ εισαγάγετε μια έγκυρη διεύθυνση IPv4 ή IPv6"
+
+#: kallithea/model/validators.py:714
 #, python-format
 msgid ""
 "The network size (bits) must be within the range of 0-32 (not %(bits)r)"
 msgstr ""
-
-#: kallithea/model/validators.py:750
+"Το μέγεθος δικτύου (bits) πρέπει να βρίσκεται εντός της περιοχής 0-32 "
+"(όχι %(bits)r)"
+
+#: kallithea/model/validators.py:747
 msgid "Key name can only consist of letters, underscore, dash or numbers"
 msgstr ""
-
-#: kallithea/model/validators.py:764
+"Το όνομα κλειδιού μπορεί να αποτελείται μόνο από γράμματα, κάτω παύλα, "
+"παύλα ή αριθμούς"
+
+#: kallithea/model/validators.py:761
 msgid "Filename cannot be inside a directory"
-msgstr ""
-
-#: kallithea/model/validators.py:780
+msgstr "Το όνομα αρχείου δεν μπορεί να βρίσκεται μέσα σε έναν κατάλογο"
+
+#: kallithea/model/validators.py:777
 #, python-format
 msgid "Plugins %(loaded)s and %(next_to_load)s both export the same name"
 msgstr ""
+"Τα πρόσθετα %(loaded)s και %(next_to_load)s εξάγουν και τα δύο το ίδιο "
+"όνομα"
 
 #: kallithea/templates/about.html:4 kallithea/templates/about.html:13
 msgid "About"
-msgstr ""
+msgstr "Σχετικά"
 
 #: kallithea/templates/admin/repos/repo_add.html:5
 #: kallithea/templates/admin/repos/repo_add.html:19
@@ -1860,7 +1897,7 @@
 #: kallithea/templates/index_base.html:25
 #: kallithea/templates/index_base.html:30
 msgid "Add Repository"
-msgstr ""
+msgstr "Προσθήκη Αποθετηρίου"
 
 #: kallithea/templates/admin/repo_groups/repo_group_add.html:5
 #: kallithea/templates/admin/repo_groups/repo_group_add.html:13
@@ -1868,21 +1905,23 @@
 #: kallithea/templates/index_base.html:27
 #: kallithea/templates/index_base.html:32
 msgid "Add Repository Group"
-msgstr ""
+msgstr "Προσθήκη Ομάδας Αποθετηρίων"
 
 #: kallithea/templates/index_base.html:37
 msgid "You have admin right to this group, and can edit it"
 msgstr ""
+"Έχετε δικαίωμα διαχειριστή σε αυτήν την ομάδα και μπορείτε να την "
+"επεξεργαστείτε"
 
 #: kallithea/templates/index_base.html:37
 msgid "Edit Repository Group"
-msgstr ""
+msgstr "Επεξεργασία Ομάδας Αποθετηρίων"
 
 #: kallithea/templates/admin/admin_log.html:7
 #: kallithea/templates/admin/permissions/permissions_globals.html:14
 #: kallithea/templates/index_base.html:53
 msgid "Repository"
-msgstr ""
+msgstr "Αποθετήριο"
 
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:59
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:61
@@ -1905,24 +1944,24 @@
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:60
 #: kallithea/templates/email_templates/pull_request.html:37
 #: kallithea/templates/forks/fork.html:34
-#: kallithea/templates/index_base.html:58
+#: kallithea/templates/index_base.html:59
 #: kallithea/templates/pullrequests/pullrequest.html:33
 #: kallithea/templates/pullrequests/pullrequest_show.html:38
 #: kallithea/templates/pullrequests/pullrequest_show.html:59
 #: kallithea/templates/summary/summary.html:87
 msgid "Description"
-msgstr ""
-
-#: kallithea/templates/index_base.html:60
+msgstr "Περιγραφή"
+
+#: kallithea/templates/index_base.html:61
 msgid "Last Change"
-msgstr ""
+msgstr "Τελευταία Αλλαγή"
 
 #: kallithea/templates/admin/my_account/my_account_repos.html:15
 #: kallithea/templates/admin/my_account/my_account_watched.html:15
 #: kallithea/templates/admin/repos/repos.html:41
-#: kallithea/templates/index_base.html:62
+#: kallithea/templates/index_base.html:63
 msgid "Tip"
-msgstr ""
+msgstr "Κεφαλή"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:10
 #: kallithea/templates/admin/repo_groups/repo_groups.html:42
@@ -1930,23 +1969,23 @@
 #: kallithea/templates/admin/repos/repos.html:42
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:8
 #: kallithea/templates/admin/user_groups/user_groups.html:42
-#: kallithea/templates/index_base.html:63
+#: kallithea/templates/index_base.html:64
 #: kallithea/templates/pullrequests/pullrequest_data.html:16
 #: kallithea/templates/pullrequests/pullrequest_show.html:124
 #: kallithea/templates/pullrequests/pullrequest_show.html:219
 #: kallithea/templates/summary/summary.html:132
 msgid "Owner"
-msgstr ""
+msgstr "Κάτοχος"
 
 #: kallithea/templates/base/base.html:380 kallithea/templates/login.html:5
 #: kallithea/templates/login.html:19
 msgid "Log In"
-msgstr ""
+msgstr "Σύνδεση"
 
 #: kallithea/templates/login.html:17
 #, python-format
 msgid "Log In to %s"
-msgstr ""
+msgstr "Συνδεθείτε στο %s"
 
 #: kallithea/templates/admin/admin_log.html:5
 #: kallithea/templates/admin/my_account/my_account_profile.html:18
@@ -1954,109 +1993,120 @@
 #: kallithea/templates/admin/users/user_edit_profile.html:18
 #: kallithea/templates/admin/users/users.html:37
 #: kallithea/templates/base/base.html:364
-#: kallithea/templates/email_templates/registration.html:11
+#: kallithea/templates/email_templates/registration.html:12
 #: kallithea/templates/login.html:28 kallithea/templates/register.html:31
 msgid "Username"
-msgstr ""
+msgstr "Όνομα χρήστη"
 
 #: kallithea/templates/admin/my_account/my_account.html:27
 #: kallithea/templates/admin/users/user_add.html:34
 #: kallithea/templates/base/base.html:368 kallithea/templates/login.html:34
 #: kallithea/templates/register.html:38
 msgid "Password"
-msgstr ""
+msgstr "Κωδικό πρόσβασης"
 
 #: kallithea/templates/login.html:44
 msgid "Stay logged in after browser restart"
 msgstr ""
+"Μείνετε συνδεδεμένοι μετά την επανεκκίνηση του προγράμματος περιήγησης"
 
 #: kallithea/templates/login.html:52
 msgid "Forgot your password ?"
-msgstr ""
+msgstr "Ξεχάσατε τον κωδικό σας;"
 
 #: kallithea/templates/login.html:55
 msgid "Don't have an account ?"
-msgstr ""
+msgstr "Δεν έχετε λογαριασμό;"
 
 #: kallithea/templates/login.html:62
 msgid "Sign In"
-msgstr ""
+msgstr "Είσοδος"
 
 #: kallithea/templates/password_reset.html:5
 msgid "Password Reset"
-msgstr ""
+msgstr "Επαναφορά κωδικού"
 
 #: kallithea/templates/password_reset.html:21
 #: kallithea/templates/password_reset_confirmation.html:16
 #, python-format
 msgid "Reset Your Password to %s"
-msgstr ""
+msgstr "Επαναφορά του κωδικού πρόσβασής σας στο %s"
 
 #: kallithea/templates/password_reset.html:23
 #: kallithea/templates/password_reset_confirmation.html:5
 #: kallithea/templates/password_reset_confirmation.html:18
 msgid "Reset Your Password"
-msgstr ""
+msgstr "Επαναφορά του κωδικού πρόσβασής σας"
 
 #: kallithea/templates/password_reset.html:30
 msgid "Email Address"
-msgstr ""
+msgstr "Διεύθυνση ηλεκτρονικού ταχυδρομείου"
 
 #: kallithea/templates/password_reset.html:38
 #: kallithea/templates/register.html:74
 msgid "Captcha"
-msgstr ""
+msgstr "Captcha"
 
 #: kallithea/templates/password_reset.html:47
 msgid "Send Password Reset Email"
 msgstr ""
+"Αποστολή μηνύματος ηλεκτρονικού ταχυδρομείου για την επαναφορά του "
+"κωδικού πρόσβασης"
 
 #: kallithea/templates/password_reset.html:52
 msgid ""
 "A password reset link will be sent to the specified email address if it "
 "is registered in the system."
 msgstr ""
+"Ένας σύνδεσμος για την επαναφορά του κωδικού πρόσβασης θα σταλεί στην "
+"καθορισμένη διεύθυνση ηλεκτρονικού ταχυδρομείου, εάν έχει καταχωρηθεί στο "
+"σύστημα."
 
 #: kallithea/templates/password_reset_confirmation.html:23
 #, python-format
 msgid "You are about to set a new password for the email address %s."
 msgstr ""
+"Πρόκειται να ορίσετε έναν νέο κωδικό πρόσβασης για τη διεύθυνση "
+"ηλεκτρονικού ταχυδρομείου %s."
 
 #: kallithea/templates/password_reset_confirmation.html:24
 msgid ""
 "Note that you must use the same browser session for this as the one used "
 "to request the password reset."
 msgstr ""
+"Λάβετε υπόψη ότι πρέπει να χρησιμοποιήσετε την ίδια περίοδο λειτουργίας "
+"του προγράμματος περιήγησης με αυτήν που χρησιμοποιήθηκε για να ζητήσετε "
+"την επαναφορά του κωδικού πρόσβασης."
 
 #: kallithea/templates/password_reset_confirmation.html:29
 msgid "Code you received in the email"
-msgstr ""
+msgstr "Κωδικός που λάβατε στο μήνυμα ηλεκτρονικού ταχυδρομείου"
 
 #: kallithea/templates/password_reset_confirmation.html:36
 msgid "New Password"
-msgstr ""
+msgstr "Νέος Κωδικός"
 
 #: kallithea/templates/password_reset_confirmation.html:43
 msgid "Confirm New Password"
-msgstr ""
+msgstr "Επιβεβαίωση Νέου Κωδικού Πρόσβασης"
 
 #: kallithea/templates/password_reset_confirmation.html:51
 msgid "Confirm"
-msgstr ""
+msgstr "Επιβεβαίωση"
 
 #: kallithea/templates/register.html:5 kallithea/templates/register.html:24
 #: kallithea/templates/register.html:83
 msgid "Sign Up"
-msgstr ""
+msgstr "Εγγραφή"
 
 #: kallithea/templates/register.html:22
 #, python-format
 msgid "Sign Up to %s"
-msgstr ""
+msgstr "Εγγραφείτε στο %s"
 
 #: kallithea/templates/register.html:45
 msgid "Re-enter password"
-msgstr ""
+msgstr "Εισαγάγετε ξανά τον κωδικό πρόσβασης"
 
 #: kallithea/templates/admin/my_account/my_account_profile.html:25
 #: kallithea/templates/admin/users/user_add.html:48
@@ -2064,7 +2114,7 @@
 #: kallithea/templates/admin/users/users.html:38
 #: kallithea/templates/register.html:52
 msgid "First Name"
-msgstr ""
+msgstr "Όνομα"
 
 #: kallithea/templates/admin/my_account/my_account_profile.html:32
 #: kallithea/templates/admin/users/user_add.html:55
@@ -2072,48 +2122,51 @@
 #: kallithea/templates/admin/users/users.html:39
 #: kallithea/templates/register.html:59
 msgid "Last Name"
-msgstr ""
+msgstr "Επώνυμο"
 
 #: kallithea/templates/admin/my_account/my_account_profile.html:39
 #: kallithea/templates/admin/settings/settings.html:31
 #: kallithea/templates/admin/users/user_add.html:62
 #: kallithea/templates/admin/users/user_edit_profile.html:25
-#: kallithea/templates/email_templates/registration.html:33
+#: kallithea/templates/email_templates/registration.html:34
 #: kallithea/templates/register.html:66
 msgid "Email"
-msgstr ""
+msgstr "Διεύθυνση Ηλεκτρονικού Ταχυδρομείου"
 
 #: kallithea/templates/register.html:85
 msgid "Registered accounts are ready to use and need no further action."
 msgstr ""
+"Οι εγγεγραμμένοι λογαριασμοί είναι έτοιμοι για χρήση και δεν χρειάζονται "
+"περαιτέρω ενέργειες."
 
 #: kallithea/templates/register.html:87
 msgid "Please wait for an administrator to activate your account."
 msgstr ""
+"Περιμένετε έως ότου ένας διαχειριστής ενεργοποιήσει τον λογαριασμό σας."
 
 #: kallithea/templates/admin/admin.html:5
 #: kallithea/templates/admin/admin.html:13
 #: kallithea/templates/base/base.html:55
 msgid "Admin Journal"
-msgstr ""
+msgstr "Ημερολόγιο Διαχειριστή"
 
 #: kallithea/templates/admin/admin.html:10
 #: kallithea/templates/journal/journal.html:10
 msgid "journal filter..."
-msgstr ""
+msgstr "φίλτρο εγγραφών..."
 
 #: kallithea/templates/admin/admin.html:12
 #: kallithea/templates/journal/journal.html:12
 msgid "Filter"
-msgstr ""
+msgstr "Φίλτρο"
 
 #: kallithea/templates/admin/admin.html:13
 #: kallithea/templates/journal/journal.html:13
 #, python-format
 msgid "%s Entry"
 msgid_plural "%s Entries"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%s Καταχώρηση"
+msgstr[1] "%s Καταχωρήσεις"
 
 #: kallithea/templates/admin/admin_log.html:6
 #: kallithea/templates/admin/my_account/my_account_repos.html:16
@@ -2125,50 +2178,52 @@
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:7
 #: kallithea/templates/admin/users/users.html:45
 msgid "Action"
-msgstr ""
+msgstr "Ενέργεια"
 
 #: kallithea/templates/admin/admin_log.html:8
 msgid "Date"
-msgstr ""
+msgstr "Ημερομηνία"
 
 #: kallithea/templates/admin/admin_log.html:9
 msgid "From IP"
-msgstr ""
+msgstr "Από IP"
 
 #: kallithea/templates/admin/admin_log.html:61
 msgid "No actions yet"
-msgstr ""
+msgstr "Καμία ενέργεια ακόμα"
 
 #: kallithea/templates/admin/auth/auth_settings.html:5
 msgid "Authentication Settings"
-msgstr ""
+msgstr "Ρυθμίσεις ελέγχου ταυτότητας"
 
 #: kallithea/templates/admin/auth/auth_settings.html:11
 #: kallithea/templates/base/base.html:61
 msgid "Authentication"
-msgstr ""
+msgstr "Έλεγχος ταυτότητας"
 
 #: kallithea/templates/admin/auth/auth_settings.html:27
 msgid "Authentication Plugins"
-msgstr ""
+msgstr "Πρόσθετα ελέγχου ταυτότητας"
 
 #: kallithea/templates/admin/auth/auth_settings.html:29
 msgid "Enabled Plugins"
-msgstr ""
+msgstr "Ενεργοποιημένα Πρόσθετα"
 
 #: kallithea/templates/admin/auth/auth_settings.html:32
 msgid ""
 "Comma-separated list of plugins; Kallithea will try user authentication "
 "in plugin order"
 msgstr ""
+"Λίστα πρόσθετων διαχωρισμένη με κόμματα. Η Καλλιθέα θα προσπαθήσει να "
+"ελέγξει την ταυτότητα του χρήστη με τη σειρά του πρόσθετου"
 
 #: kallithea/templates/admin/auth/auth_settings.html:36
 msgid "Available built-in plugins"
-msgstr ""
+msgstr "Διαθέσιμα ενσωματωμένα πρόσθετα"
 
 #: kallithea/templates/admin/auth/auth_settings.html:53
 msgid "Plugin"
-msgstr ""
+msgstr "Πρόσθετο"
 
 #: kallithea/templates/admin/auth/auth_settings.html:101
 #: kallithea/templates/admin/defaults/defaults.html:59
@@ -2188,26 +2243,26 @@
 #: kallithea/templates/admin/users/user_edit_profile.html:89
 #: kallithea/templates/base/default_perms_box.html:42
 msgid "Save"
-msgstr ""
+msgstr "Αποθήκευση"
 
 #: kallithea/templates/admin/defaults/defaults.html:5
 #: kallithea/templates/admin/defaults/defaults.html:11
 #: kallithea/templates/base/base.html:62
 msgid "Repository Defaults"
-msgstr ""
+msgstr "Προεπιλογές Αποθετηρίου"
 
 #: kallithea/templates/admin/defaults/defaults.html:27
 #: kallithea/templates/admin/repos/repo_add_base.html:42
 #: kallithea/templates/admin/repos/repo_edit_fields.html:8
 msgid "Type"
-msgstr ""
+msgstr "Τύπος"
 
 #: kallithea/templates/admin/defaults/defaults.html:34
 #: kallithea/templates/admin/repos/repo_add_base.html:56
 #: kallithea/templates/admin/repos/repo_edit_settings.html:62
 #: kallithea/templates/data_table/_dt_elements.html:21
 msgid "Private repository"
-msgstr ""
+msgstr "Ιδιωτικό αποθετήριο"
 
 #: kallithea/templates/admin/defaults/defaults.html:37
 #: kallithea/templates/admin/repos/repo_add_base.html:59
@@ -2217,31 +2272,33 @@
 "Private repositories are only visible to people explicitly added as "
 "collaborators."
 msgstr ""
+"Τα ιδιωτικά αποθετήρια είναι ορατά μόνο σε άτομα που προστίθενται ρητά ως "
+"συνεργάτες."
 
 #: kallithea/templates/admin/defaults/defaults.html:42
 #: kallithea/templates/admin/repos/repo_edit_settings.html:69
 msgid "Enable statistics"
-msgstr ""
+msgstr "Ενεργοποίηση στατιστικών"
 
 #: kallithea/templates/admin/defaults/defaults.html:45
 #: kallithea/templates/admin/repos/repo_edit_settings.html:72
 msgid "Enable statistics window on summary page."
-msgstr ""
+msgstr "Ενεργοποίηση παραθύρου στατιστικών στοιχείων στη σελίδα περίληψης."
 
 #: kallithea/templates/admin/defaults/defaults.html:50
 #: kallithea/templates/admin/repos/repo_edit_settings.html:76
 msgid "Enable downloads"
-msgstr ""
+msgstr "Ενεργοποίηση λήψεων"
 
 #: kallithea/templates/admin/defaults/defaults.html:53
 #: kallithea/templates/admin/repos/repo_edit_settings.html:79
 msgid "Enable download menu on summary page."
-msgstr ""
+msgstr "Ενεργοποίηση μενού λήψης στη σελίδα περίληψης."
 
 #: kallithea/templates/admin/gists/edit.html:5
 #: kallithea/templates/admin/gists/edit.html:18
 msgid "Edit Gist"
-msgstr ""
+msgstr "Επεξεργασία Gist"
 
 #: kallithea/templates/admin/gists/edit.html:35
 #, python-format
@@ -2249,20 +2306,23 @@
 "Gist was updated since you started editing. Copy your changes and click "
 "%(here)s to reload new version."
 msgstr ""
+"Το gist ενημερώθηκε από τότε που αρχίσατε την επεξεργασία. Αντιγράψτε τις "
+"αλλαγές σας και κάντε κλικ στο κουμπί %(here)s για να φορτώσετε τη νέα "
+"έκδοση."
 
 #: kallithea/templates/admin/gists/edit.html:36
 msgid "here"
-msgstr ""
+msgstr "εδώ"
 
 #: kallithea/templates/admin/gists/edit.html:51
 #: kallithea/templates/admin/gists/new.html:35
 msgid "Gist description ..."
-msgstr ""
+msgstr "Περιγραφή gist..."
 
 #: kallithea/templates/admin/gists/edit.html:54
 #: kallithea/templates/admin/gists/new.html:38
 msgid "Gist lifetime"
-msgstr ""
+msgstr "Διάρκεια ζωής του gist"
 
 #: kallithea/templates/admin/gists/edit.html:59
 #: kallithea/templates/admin/gists/edit.html:61
@@ -2277,7 +2337,7 @@
 #: kallithea/templates/admin/users/user_edit_api_keys.html:26
 #: kallithea/templates/admin/users/user_edit_api_keys.html:31
 msgid "Expires"
-msgstr ""
+msgstr "Λήγει"
 
 #: kallithea/templates/admin/gists/edit.html:59
 #: kallithea/templates/admin/gists/index.html:54
@@ -2289,66 +2349,66 @@
 #: kallithea/templates/admin/users/user_edit_api_keys.html:26
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:21
 msgid "Never"
-msgstr ""
+msgstr "Ποτέ"
 
 #: kallithea/templates/admin/gists/edit.html:145
 msgid "Update Gist"
-msgstr ""
+msgstr "Ενημέρωση του gist"
 
 #: kallithea/templates/admin/gists/edit.html:146
 #: kallithea/templates/base/root.html:27
 #: kallithea/templates/changeset/changeset_file_comment.html:130
 msgid "Cancel"
-msgstr ""
+msgstr "Ακύρωση"
 
 #: kallithea/templates/admin/gists/index.html:6
 #: kallithea/templates/admin/gists/index.html:16
 #, python-format
 msgid "Private Gists for User %s"
-msgstr ""
+msgstr "Ιδιωτικά gists για το χρήστη %s"
 
 #: kallithea/templates/admin/gists/index.html:8
 #: kallithea/templates/admin/gists/index.html:18
 #, python-format
 msgid "Public Gists for User %s"
-msgstr ""
+msgstr "Δημόσια gists για το χρήστη %s"
 
 #: kallithea/templates/admin/gists/index.html:10
 #: kallithea/templates/admin/gists/index.html:20
 msgid "Public Gists"
-msgstr ""
+msgstr "Δημόσια Gists"
 
 #: kallithea/templates/admin/gists/index.html:37
 #: kallithea/templates/admin/gists/show.html:25
 #: kallithea/templates/base/base.html:305
 msgid "Create New Gist"
-msgstr ""
+msgstr "Δημιουργία Νέου Gist"
 
 #: kallithea/templates/admin/gists/index.html:51
-#: kallithea/templates/data_table/_dt_elements.html:78
+#: kallithea/templates/data_table/_dt_elements.html:84
 msgid "Created"
-msgstr ""
+msgstr "Δημιουργήθηκε"
 
 #: kallithea/templates/admin/gists/index.html:66
 msgid "There are no gists yet"
-msgstr ""
+msgstr "Δεν υπάρχουν ακόμη gists"
 
 #: kallithea/templates/admin/gists/new.html:5
 #: kallithea/templates/admin/gists/new.html:18
 msgid "New Gist"
-msgstr ""
+msgstr "Νέο Gist"
 
 #: kallithea/templates/admin/gists/new.html:45
 msgid "Name this gist ..."
-msgstr ""
+msgstr "Ονομάστε αυτό το gist..."
 
 #: kallithea/templates/admin/gists/new.html:53
 msgid "Create Private Gist"
-msgstr ""
+msgstr "Δημιουργία Ιδιωτικού Gist"
 
 #: kallithea/templates/admin/gists/new.html:54
 msgid "Create Public Gist"
-msgstr ""
+msgstr "Δημιουργία Δημόσιου Gist"
 
 #: kallithea/templates/admin/gists/new.html:55
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:14
@@ -2380,24 +2440,24 @@
 #: kallithea/templates/files/files_edit.html:72
 #: kallithea/templates/pullrequests/pullrequest.html:78
 msgid "Reset"
-msgstr ""
+msgstr "Επαναφορά"
 
 #: kallithea/templates/admin/gists/show.html:5
 #: kallithea/templates/admin/gists/show.html:9
 msgid "Gist"
-msgstr ""
+msgstr "Gist"
 
 #: kallithea/templates/admin/gists/show.html:10
 msgid "URL"
-msgstr ""
+msgstr "URL"
 
 #: kallithea/templates/admin/gists/show.html:35
 msgid "Public Gist"
-msgstr ""
+msgstr "Δημόσιο Gist"
 
 #: kallithea/templates/admin/gists/show.html:37
 msgid "Private Gist"
-msgstr ""
+msgstr "Ιδιωτικό Gist"
 
 #: kallithea/templates/admin/gists/show.html:54
 #: kallithea/templates/admin/my_account/my_account_emails.html:23
@@ -2409,136 +2469,136 @@
 #: kallithea/templates/admin/users/user_edit_ips.html:21
 #: kallithea/templates/changeset/changeset_file_comment.html:30
 #: kallithea/templates/changeset/changeset_file_comment.html:121
-#: kallithea/templates/data_table/_dt_elements.html:69
-#: kallithea/templates/data_table/_dt_elements.html:89
-#: kallithea/templates/data_table/_dt_elements.html:91
-#: kallithea/templates/data_table/_dt_elements.html:101
-#: kallithea/templates/data_table/_dt_elements.html:103
-#: kallithea/templates/data_table/_dt_elements.html:120
-#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:75
+#: kallithea/templates/data_table/_dt_elements.html:95
+#: kallithea/templates/data_table/_dt_elements.html:97
+#: kallithea/templates/data_table/_dt_elements.html:107
+#: kallithea/templates/data_table/_dt_elements.html:109
+#: kallithea/templates/data_table/_dt_elements.html:126
+#: kallithea/templates/data_table/_dt_elements.html:128
 #: kallithea/templates/files/files_source.html:35
 #: kallithea/templates/files/files_source.html:38
 #: kallithea/templates/files/files_source.html:41
 #: kallithea/templates/pullrequests/pullrequest_data.html:20
 msgid "Delete"
-msgstr ""
+msgstr "Διαγραφή"
 
 #: kallithea/templates/admin/gists/show.html:54
 msgid "Confirm to delete this Gist"
-msgstr ""
+msgstr "Επιβεβαίωση για διαγραφή αυτού του Gist"
 
 #: kallithea/templates/admin/gists/show.html:61
 #: kallithea/templates/base/perms_summary.html:44
 #: kallithea/templates/base/perms_summary.html:81
 #: kallithea/templates/base/perms_summary.html:83
-#: kallithea/templates/data_table/_dt_elements.html:63
-#: kallithea/templates/data_table/_dt_elements.html:64
-#: kallithea/templates/data_table/_dt_elements.html:85
-#: kallithea/templates/data_table/_dt_elements.html:86
-#: kallithea/templates/data_table/_dt_elements.html:97
-#: kallithea/templates/data_table/_dt_elements.html:98
-#: kallithea/templates/data_table/_dt_elements.html:116
-#: kallithea/templates/data_table/_dt_elements.html:117
+#: kallithea/templates/data_table/_dt_elements.html:69
+#: kallithea/templates/data_table/_dt_elements.html:70
+#: kallithea/templates/data_table/_dt_elements.html:91
+#: kallithea/templates/data_table/_dt_elements.html:92
+#: kallithea/templates/data_table/_dt_elements.html:103
+#: kallithea/templates/data_table/_dt_elements.html:104
+#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:123
 #: kallithea/templates/files/diff_2way.html:56
 #: kallithea/templates/files/files_source.html:37
 #: kallithea/templates/files/files_source.html:40
 #: kallithea/templates/pullrequests/pullrequest_show.html:41
 msgid "Edit"
-msgstr ""
+msgstr "Επεξεργασία"
 
 #: kallithea/templates/admin/gists/show.html:63
 #: kallithea/templates/files/files_edit.html:52
 #: kallithea/templates/files/files_source.html:30
 msgid "Show as Raw"
-msgstr ""
+msgstr "Ακατέργαστη Εμφάνιση"
 
 #: kallithea/templates/admin/gists/show.html:69
 msgid "created"
-msgstr ""
+msgstr "δημιουργήθηκε"
 
 #: kallithea/templates/admin/gists/show.html:82
 msgid "Show as raw"
-msgstr ""
+msgstr "Ακατέργαστη εμφάνιση"
 
 #: kallithea/templates/admin/my_account/my_account.html:5
 #: kallithea/templates/admin/my_account/my_account.html:9
 #: kallithea/templates/base/base.html:390
 msgid "My Account"
-msgstr ""
+msgstr "Ο Λογαριασμός Μου"
 
 #: kallithea/templates/admin/my_account/my_account.html:25
 #: kallithea/templates/admin/users/user_edit.html:29
 msgid "Profile"
-msgstr ""
+msgstr "Προφίλ"
 
 #: kallithea/templates/admin/my_account/my_account.html:26
 msgid "Email Addresses"
-msgstr ""
+msgstr "Διευθύνσεις ηλεκτρονικού ταχυδρομείου"
 
 #: kallithea/templates/admin/my_account/my_account.html:29
 #: kallithea/templates/admin/users/user_edit.html:32
 msgid "SSH Keys"
-msgstr ""
+msgstr "Κλειδιά SSH"
 
 #: kallithea/templates/admin/my_account/my_account.html:31
 #: kallithea/templates/admin/users/user_edit.html:34
 msgid "API Keys"
-msgstr ""
+msgstr "Κλειδιά API"
 
 #: kallithea/templates/admin/my_account/my_account.html:32
 msgid "Owned Repositories"
-msgstr ""
+msgstr "Αποθετήρια που μου Ανήκουν"
 
 #: kallithea/templates/admin/my_account/my_account.html:33
 #: kallithea/templates/journal/journal.html:33
 msgid "Watched Repositories"
-msgstr ""
+msgstr "Αποθετήρια που Παρακολουθώ"
 
 #: kallithea/templates/admin/my_account/my_account.html:34
 #: kallithea/templates/admin/permissions/permissions.html:30
 #: kallithea/templates/admin/user_groups/user_group_edit.html:32
 #: kallithea/templates/admin/users/user_edit.html:37
 msgid "Show Permissions"
-msgstr ""
+msgstr "Εμφάνιση Δικαιωμάτων"
 
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:5
 #: kallithea/templates/admin/users/user_edit_api_keys.html:5
 msgid "Built-in"
-msgstr ""
+msgstr "Ενσωματωμένο"
 
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:13
 #: kallithea/templates/admin/users/user_edit_api_keys.html:13
 #, python-format
 msgid "Confirm to reset this API key: %s"
-msgstr ""
+msgstr "Επιβεβαίωση για επαναφορά αυτού του κλειδιού API: %s"
 
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:29
 #: kallithea/templates/admin/users/user_edit_api_keys.html:29
 msgid "Expired"
-msgstr ""
+msgstr "Έληξε"
 
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:39
 #: kallithea/templates/admin/users/user_edit_api_keys.html:39
 #, python-format
 msgid "Confirm to remove this API key: %s"
-msgstr ""
+msgstr "Επιβεβαίωση κατάργησης αυτού του κλειδιού API: %s"
 
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:41
 #: kallithea/templates/admin/my_account/my_account_ssh_keys.html:30
 #: kallithea/templates/admin/users/user_edit_api_keys.html:41
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:30
 msgid "Remove"
-msgstr ""
+msgstr "Κατάργηση"
 
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:48
 #: kallithea/templates/admin/users/user_edit_api_keys.html:48
 msgid "No additional API keys specified"
-msgstr ""
+msgstr "Δεν έχουν καθοριστεί πρόσθετα κλειδιά API"
 
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:56
 #: kallithea/templates/admin/users/user_edit_api_keys.html:56
 msgid "New API key"
-msgstr ""
+msgstr "Νέο κλειδί API"
 
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:72
 #: kallithea/templates/admin/my_account/my_account_emails.html:46
@@ -2551,7 +2611,7 @@
 #: kallithea/templates/admin/users/user_edit_ips.html:44
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:65
 msgid "Add"
-msgstr ""
+msgstr "Προσθήκη"
 
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:81
 #, python-format
@@ -2561,6 +2621,11 @@
 "account, as if you had provided the script or service with your actual\n"
 "password.\n"
 msgstr ""
+"\n"
+"Τα κλειδιά API χρησιμοποιούνται για να επιτρέπουν προγράμματα ή υπηρεσίες "
+"να έχουν πρόσβαση στο %s χρησιμοποιώντας το λογαριασμό σας, σαν να "
+"παρείχατε στο πρόγραμμα ή την υπηρεσία, τον πραγματικό σας κωδικό "
+"πρόσβασης.\n"
 
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:86
 msgid ""
@@ -2569,74 +2634,84 @@
 "nor passed to untrusted scripts or services. If such sharing should\n"
 "happen anyway, reset the API key on this page to prevent further use.\n"
 msgstr ""
+"\n"
+"Όπως οι κωδικοί πρόσβασης, τα κλειδιά API δεν πρέπει επομένως να "
+"κοινοποιούνται ποτέ σε άλλους,\n"
+"ούτε να μεταβιβάζονται σε μη αξιόπιστα προγράμματα ή υπηρεσίες. Εάν μια "
+"τέτοια κοινοποίηση πρέπει\n"
+"γίνει, επαναφέρετε το κλειδί API σε αυτήν τη σελίδα για να αποφύγετε "
+"περαιτέρω χρήση.\n"
 
 #: kallithea/templates/admin/my_account/my_account_emails.html:9
 #: kallithea/templates/admin/users/user_edit_emails.html:9
 msgid "Primary"
-msgstr ""
+msgstr "Πρωτεύων"
 
 #: kallithea/templates/admin/my_account/my_account_emails.html:24
 #: kallithea/templates/admin/users/user_edit_emails.html:24
 #, python-format
 msgid "Confirm to delete this email: %s"
 msgstr ""
+"Επιβεβαίωση διαγραφής αυτού του μηνύματος ηλεκτρονικού ταχυδρομείου: %s"
 
 #: kallithea/templates/admin/my_account/my_account_emails.html:30
 #: kallithea/templates/admin/users/user_edit_emails.html:30
 msgid "No additional emails specified."
-msgstr ""
+msgstr "Δεν έχουν καθοριστεί πρόσθετα μηνύματα ηλεκτρονικού ταχυδρομείου."
 
 #: kallithea/templates/admin/my_account/my_account_emails.html:39
 #: kallithea/templates/admin/users/user_edit_emails.html:39
 msgid "New email address"
-msgstr ""
+msgstr "Νέα διεύθυνση ηλεκτρονικού ταχυδρομείου"
 
 #: kallithea/templates/admin/my_account/my_account_password.html:1
 msgid "Change Your Account Password"
-msgstr ""
+msgstr "Αλλαγή του Κωδικού Πρόσβασης του Λογαριασμού σας"
 
 #: kallithea/templates/admin/my_account/my_account_password.html:8
 msgid "Current password"
-msgstr ""
+msgstr "Τρέχων κωδικός πρόσβασης"
 
 #: kallithea/templates/admin/my_account/my_account_password.html:15
 #: kallithea/templates/admin/users/user_edit_profile.html:46
 msgid "New password"
-msgstr ""
+msgstr "Νέος κωδικός πρόσβασης"
 
 #: kallithea/templates/admin/my_account/my_account_password.html:22
 msgid "Confirm new password"
-msgstr ""
+msgstr "Επιβεβαίωση νέου κωδικού πρόσβασης"
 
 #: kallithea/templates/admin/my_account/my_account_password.html:39
 #, python-format
 msgid ""
 "This account is managed with %s and the password cannot be changed here"
 msgstr ""
+"Η διαχείριση αυτού του λογαριασμού γίνεται με %s και ο κωδικός πρόσβασης "
+"δεν μπορεί να αλλάξει εδώ"
 
 #: kallithea/templates/admin/my_account/my_account_perms.html:3
 msgid "Current IP"
-msgstr ""
+msgstr "Τρέχουσα IP"
 
 #: kallithea/templates/admin/my_account/my_account_profile.html:4
 #: kallithea/templates/admin/users/user_edit_profile.html:4
 msgid "Gravatar"
-msgstr ""
+msgstr "Gravatar"
 
 #: kallithea/templates/admin/my_account/my_account_profile.html:10
 #: kallithea/templates/admin/users/user_edit_profile.html:10
 #, python-format
 msgid "Change %s avatar at"
-msgstr ""
+msgstr "Αλλαγή avatar %s στο"
 
 #: kallithea/templates/admin/my_account/my_account_profile.html:12
 #: kallithea/templates/admin/users/user_edit_profile.html:12
 msgid "Avatars are disabled"
-msgstr ""
+msgstr "Τα Avatars είναι απενεργοποιημένα"
 
 #: kallithea/templates/admin/my_account/my_account_repos.html:1
 msgid "Repositories You Own"
-msgstr ""
+msgstr "Αποθετήρια που σας ανήκουν"
 
 #: kallithea/templates/admin/my_account/my_account_repos.html:13
 #: kallithea/templates/admin/my_account/my_account_watched.html:13
@@ -2648,71 +2723,71 @@
 #: kallithea/templates/base/perms_summary.html:54
 #: kallithea/templates/files/files_browser.html:54
 msgid "Name"
-msgstr ""
+msgstr "Όνομα"
 
 #: kallithea/templates/admin/my_account/my_account_ssh_keys.html:4
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:4
 msgid "Fingerprint"
-msgstr ""
+msgstr "Αποτύπωμα"
 
 #: kallithea/templates/admin/my_account/my_account_ssh_keys.html:6
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:6
 msgid "Last Used"
-msgstr ""
+msgstr "Τελευταία χρησιμοποιήθηκε"
 
 #: kallithea/templates/admin/my_account/my_account_ssh_keys.html:28
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:28
 #, python-format
 msgid "Confirm to remove this SSH key: %s"
-msgstr ""
+msgstr "Επιβεβαίωση κατάργησης αυτού του κλειδιού SSH: %s"
 
 #: kallithea/templates/admin/my_account/my_account_ssh_keys.html:39
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:39
 msgid "No SSH keys have been added"
-msgstr ""
+msgstr "Δεν έχουν προστεθεί κλειδιά SSH"
 
 #: kallithea/templates/admin/my_account/my_account_ssh_keys.html:49
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:49
 msgid "New SSH key"
-msgstr ""
+msgstr "Νέο κλειδί SSH"
 
 #: kallithea/templates/admin/my_account/my_account_ssh_keys.html:52
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:52
 msgid "Public key"
-msgstr ""
+msgstr "Δημόσιο κλειδί"
 
 #: kallithea/templates/admin/my_account/my_account_ssh_keys.html:54
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:54
 msgid "Public key (contents of e.g. ~/.ssh/id_rsa.pub)"
-msgstr ""
+msgstr "Δημόσιο κλειδί (περιεχόμενο π.χ. ~/.ssh/id_rsa.pub)"
 
 #: kallithea/templates/admin/my_account/my_account_watched.html:1
 msgid "Repositories You are Watching"
-msgstr ""
+msgstr "Αποθετήρια που παρακολουθείτε"
 
 #: kallithea/templates/admin/permissions/permissions.html:5
 #: kallithea/templates/admin/permissions/permissions.html:11
 #: kallithea/templates/base/base.html:60
 msgid "Default Permissions"
-msgstr ""
+msgstr "Προεπιλεγμένα Δικαιώματα"
 
 #: kallithea/templates/admin/permissions/permissions.html:28
 #: kallithea/templates/admin/settings/settings.html:29
 msgid "Global"
-msgstr ""
+msgstr "Γενικά"
 
 #: kallithea/templates/admin/permissions/permissions.html:29
 #: kallithea/templates/admin/users/user_edit.html:35
 msgid "IP Whitelist"
-msgstr ""
+msgstr "Λίστα επιτρεπόμενων IP"
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:4
 msgid "Anonymous access"
-msgstr ""
+msgstr "Ανώνυμη πρόσβαση"
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:8
 msgid "Allow anonymous access"
-msgstr ""
+msgstr "Να επιτρέπεται η ανώνυμη πρόσβαση"
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:10
 #, python-format
@@ -2720,6 +2795,8 @@
 "Allow access to Kallithea without needing to log in. Anonymous users use "
 "%s user permissions."
 msgstr ""
+"Να επιτρέπεται η πρόσβαση στην Καλλιθέα χωρίς να χρειάζεται να "
+"συνδεθείτε. Οι ανώνυμοι χρήστες χρησιμοποιούν δικαιώματα χρήστη %s."
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:19
 msgid ""
@@ -2727,22 +2804,25 @@
 "permission, note that all custom default permission on repositories will "
 "be lost"
 msgstr ""
+"Όλα τα προεπιλεγμένα δικαιώματα σε κάθε αποθετήριο θα επαναφερθούν στα "
+"επιλεγμένα δικαιώματα. Σημειώστε ότι όλα τα προσαρμοσμένα προεπιλεγμένα "
+"δικαιώματα στα αποθετήρια θα χαθούν"
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:20
 msgid "Apply to all existing repositories"
-msgstr ""
+msgstr "Εφαρμογή σε όλα τα υπάρχοντα αποθετήρια"
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:23
 msgid "Permissions for the Default user on new repositories."
-msgstr ""
+msgstr "Δικαιώματα για τον προεπιλεγμένο χρήστη σε νέα αποθετήρια."
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:27
 #: kallithea/templates/admin/repos/repo_add_base.html:28
 #: kallithea/templates/admin/repos/repo_edit_settings.html:33
-#: kallithea/templates/data_table/_dt_elements.html:134
+#: kallithea/templates/data_table/_dt_elements.html:140
 #: kallithea/templates/forks/fork.html:42
 msgid "Repository group"
-msgstr ""
+msgstr "Ομάδα αποθετηρίου"
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:32
 msgid ""
@@ -2750,19 +2830,22 @@
 "permission, note that all custom default permission on repository groups "
 "will be lost"
 msgstr ""
+"Όλα τα προεπιλεγμένα δικαιώματα σε κάθε ομάδα αποθετηρίων θα επαναφερθούν "
+"στα επιλεγμένα δικαιώματα. Σημειώστε ότι όλα τα προσαρμοσμένα "
+"προεπιλεγμένα δικαιώματα στις ομάδες αποθετηρίων θα χαθούν"
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:33
 msgid "Apply to all existing repository groups"
-msgstr ""
+msgstr "Εφαρμογή σε όλες τις υπάρχουσες ομάδες αποθετηρίων"
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:36
 msgid "Permissions for the Default user on new repository groups."
-msgstr ""
+msgstr "Δικαιώματα για τον προεπιλεγμένο χρήστη σε νέες ομάδες αποθετηρίων."
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:40
-#: kallithea/templates/data_table/_dt_elements.html:141
+#: kallithea/templates/data_table/_dt_elements.html:147
 msgid "User group"
-msgstr ""
+msgstr "Ομάδα χρηστών"
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:45
 msgid ""
@@ -2770,33 +2853,41 @@
 "permission, note that all custom default permission on user groups will "
 "be lost"
 msgstr ""
+"Όλα τα προεπιλεγμένα δικαιώματα σε κάθε ομάδα χρηστών θα επαναφερθούν στα "
+"επιλεγμένα δικαιώματα. Σημειώστε ότι όλα τα προσαρμοσμένα προεπιλεγμένα "
+"δικαιώματα στις ομάδες χρηστών θα χαθούν"
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:46
 msgid "Apply to all existing user groups"
-msgstr ""
+msgstr "Εφαρμογή σε όλες τις υπάρχουσες ομάδες χρηστών"
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:49
 msgid "Permissions for the Default user on new user groups."
-msgstr ""
+msgstr "Δικαιώματα για τον προεπιλεγμένο χρήστη σε νέες ομάδες χρηστών."
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:53
 msgid "Top level repository creation"
-msgstr ""
+msgstr "Δημιουργία αποθετηρίου ανώτατου επιπέδου"
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:56
 msgid ""
 "Enable this to allow non-admins to create repositories at the top level."
 msgstr ""
+"Ενεργοποιήστε αυτήν την επιλογή ώστε να επιτρέπεται σε μη διαχειριστές να "
+"δημιουργούν αποθετήρια στο ανώτερο επίπεδο."
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:57
 msgid ""
 "Note: This will also give all users API access to create repositories "
 "everywhere. That might change in future versions."
 msgstr ""
+"Σημείωση: Αυτό θα δώσει επίσης σε όλους τους χρήστες πρόσβαση API για τη "
+"δημιουργία αποθετηρίων παντού. Αυτό μπορεί να αλλάξει σε μελλοντικές "
+"εκδόσεις."
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:61
 msgid "Repository creation with group write access"
-msgstr ""
+msgstr "Δημιουργία αποθετηρίου με πρόσβαση εγγραφής ομάδας"
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:64
 msgid ""
@@ -2804,14 +2895,19 @@
 "repositories inside that group. Without this, group write permissions "
 "mean nothing."
 msgstr ""
+"Με αυτό, η άδεια εγγραφής σε μια ομάδα αποθετηρίων επιτρέπει τη "
+"δημιουργία αποθετηρίων εντός αυτής της ομάδας. Χωρίς αυτό, τα δικαιώματα "
+"ομαδικής εγγραφής δεν σημαίνουν τίποτα."
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:68
 msgid "User group creation"
-msgstr ""
+msgstr "Δημιουργία ομάδας χρηστών"
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:71
 msgid "Enable this to allow non-admins to create user groups."
 msgstr ""
+"Ενεργοποιήστε αυτήν την επιλογή για να επιτρέψετε σε μη διαχειριστές να "
+"δημιουργούν ομάδες χρηστών."
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:75
 msgid "Repository forking"
@@ -2823,27 +2919,27 @@
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:82
 msgid "Registration"
-msgstr ""
+msgstr "Εγγραφή"
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:88
 msgid "External auth account activation"
-msgstr ""
+msgstr "Ενεργοποίηση λογαριασμού εξωτερικού ελέγχου"
 
 #: kallithea/templates/admin/permissions/permissions_ips.html:12
 #: kallithea/templates/admin/users/user_edit_ips.html:22
 #, python-format
 msgid "Confirm to delete this IP address: %s"
-msgstr ""
+msgstr "Επιβεβαίωση για διαγραφή αυτής της διεύθυνσης IP: %s"
 
 #: kallithea/templates/admin/permissions/permissions_ips.html:18
 #: kallithea/templates/admin/users/user_edit_ips.html:29
 msgid "All IP addresses are allowed."
-msgstr ""
+msgstr "Επιτρέπονται όλες οι διευθύνσεις IP."
 
 #: kallithea/templates/admin/permissions/permissions_ips.html:25
 #: kallithea/templates/admin/users/user_edit_ips.html:37
 msgid "New IP address"
-msgstr ""
+msgstr "Νέα διεύθυνση IP"
 
 #: kallithea/templates/admin/repo_groups/repo_group_add.html:11
 #: kallithea/templates/admin/repo_groups/repo_group_edit.html:11
@@ -2852,38 +2948,38 @@
 #: kallithea/templates/base/base.html:57
 #: kallithea/templates/base/base.html:76
 msgid "Repository Groups"
-msgstr ""
+msgstr "Ομάδες Αποθετηρίου"
 
 #: kallithea/templates/admin/repo_groups/repo_group_add.html:28
 #: kallithea/templates/admin/repo_groups/repo_group_edit_settings.html:5
 #: kallithea/templates/admin/user_groups/user_group_add.html:27
 #: kallithea/templates/admin/user_groups/user_group_edit_settings.html:4
 msgid "Group name"
-msgstr ""
+msgstr "Όνομα ομάδας"
 
 #: kallithea/templates/admin/repo_groups/repo_group_add.html:42
 #: kallithea/templates/admin/repo_groups/repo_group_edit_settings.html:19
 msgid "Group parent"
-msgstr ""
+msgstr "Γονική ομάδα"
 
 #: kallithea/templates/admin/repo_groups/repo_group_add.html:49
 #: kallithea/templates/admin/repos/repo_add_base.html:35
 msgid "Copy parent group permissions"
-msgstr ""
+msgstr "Αντιγραφή δικαιωμάτων γονικής ομάδας"
 
 #: kallithea/templates/admin/repo_groups/repo_group_add.html:52
 #: kallithea/templates/admin/repos/repo_add_base.html:38
 msgid "Copy permission set from parent repository group."
-msgstr ""
+msgstr "Αντιγραφή συνόλου δικαιωμάτων από γονική ομάδα αποθετηρίου."
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit.html:5
 #, python-format
 msgid "%s Repository Group Settings"
-msgstr ""
+msgstr "Ρυθμίσεις ομάδας αποθετηρίου %s"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit.html:29
 msgid "Add Child Group"
-msgstr ""
+msgstr "Προσθήκη Θυγατρικής Ομάδας"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit.html:36
 #: kallithea/templates/admin/repos/repo_edit.html:12
@@ -2893,82 +2989,80 @@
 #: kallithea/templates/base/base.html:63
 #: kallithea/templates/base/base.html:152
 msgid "Settings"
-msgstr ""
+msgstr "Ρυθμίσεις"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit.html:37
 #: kallithea/templates/admin/repos/repo_edit.html:31
 #: kallithea/templates/admin/user_groups/user_group_edit.html:30
 #: kallithea/templates/admin/users/user_edit.html:36
 msgid "Advanced"
-msgstr ""
+msgstr "Προχωρημένες Ρυθμίσεις"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit.html:38
 #: kallithea/templates/admin/repos/repo_edit.html:28
 #: kallithea/templates/admin/user_groups/user_group_edit.html:31
 msgid "Permissions"
-msgstr ""
+msgstr "Δικαιώματα"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:1
 #, python-format
 msgid "Repository Group: %s"
-msgstr ""
+msgstr "Ομάδα αποθετηρίου: %s"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:6
 msgid "Top level repositories"
-msgstr ""
+msgstr "Αποθετήρια ανώτατου επιπέδου"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:7
 msgid "Total repositories"
-msgstr ""
+msgstr "Σύνολο αποθετηρίων"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:8
 msgid "Children groups"
-msgstr ""
+msgstr "Θυγατρικές ομάδες"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:9
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:7
 #: kallithea/templates/admin/users/user_edit_advanced.html:8
 #: kallithea/templates/pullrequests/pullrequest_show.html:118
 msgid "Created on"
-msgstr ""
+msgstr "Δημιουργήθηκε στις"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:121
+#: kallithea/templates/data_table/_dt_elements.html:127
 #, python-format
 msgid "Confirm to delete this group: %s with %s repository"
 msgid_plural "Confirm to delete this group: %s with %s repositories"
 msgstr[0] ""
+"Επιβεβαίωση διαγραφής αυτής της ομάδας: %s με αποθετήριο δεδομένων %s"
 msgstr[1] ""
+"Επιβεβαίωση διαγραφής αυτής της ομάδας: %s με αποθετήρια δεδομένων %s"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:25
 msgid "Delete this repository group"
-msgstr ""
+msgstr "Διαγραφή αυτής της ομάδας αποθετηρίων"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:7
 msgid "Not visible"
-msgstr ""
+msgstr "Μη ορατό"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:8
-#, fuzzy
-#| msgid "Disabled"
 msgid "Visible"
-msgstr "Απενεργοποιημένο"
+msgstr "Ορατό"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:9
-#, fuzzy
-#| msgid "No response"
 msgid "Add repos"
-msgstr "Χωρίς απόκριση"
+msgstr "Προσθήκη αποθετηρίων"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:10
 msgid "Add/Edit groups"
-msgstr ""
+msgstr "Προσθήκη/Επεξεργασία ομάδων"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:11
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:11
 #: kallithea/templates/admin/user_groups/user_group_edit_perms.html:11
 msgid "User/User Group"
-msgstr ""
+msgstr "Χρήστης / Ομάδα χρηστών"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:28
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:45
@@ -2977,7 +3071,7 @@
 #: kallithea/templates/admin/user_groups/user_group_edit_perms.html:28
 #: kallithea/templates/admin/user_groups/user_group_edit_perms.html:45
 msgid "Default"
-msgstr ""
+msgstr "Προεπιλογή"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:34
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:71
@@ -2986,59 +3080,63 @@
 #: kallithea/templates/admin/user_groups/user_group_edit_perms.html:34
 #: kallithea/templates/admin/user_groups/user_group_edit_perms.html:71
 msgid "Revoke"
-msgstr ""
+msgstr "Ανακάλεσε"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:81
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:77
 #: kallithea/templates/admin/user_groups/user_group_edit_perms.html:81
 msgid "Add new"
-msgstr ""
+msgstr "Προσθήκη νέου"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:87
 msgid "Apply to children"
-msgstr ""
+msgstr "Εφαρμογή στα θυγατρικά"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:91
 msgid "Both"
-msgstr ""
+msgstr "Και τα δυο"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:92
 msgid ""
 "Set or revoke permission to all children of that group, including non-"
 "private repositories and other groups if selected."
 msgstr ""
+"Ορίστε ή ανακαλέστε τα θυγατρικά δικαιώματα αυτής της ομάδας, "
+"συμπεριλαμβανομένων των μη ιδιωτικών αποθετηρίων και άλλων ομάδων, εάν "
+"επιλεγεί."
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_settings.html:38
 msgid "Remove this group"
-msgstr ""
+msgstr "Κατάργηση αυτής της ομάδας"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_settings.html:38
 msgid "Confirm to delete this group"
-msgstr ""
+msgstr "Επιβεβαιώστε για να διαγράψετε αυτή την ομάδα"
 
 #: kallithea/templates/admin/repo_groups/repo_group_show.html:4
-#, fuzzy, python-format
-#| msgid "Updated repository group %s"
+#, python-format
 msgid "Repository group %s"
-msgstr "Ενημερώθηκε η ομάδα αποθετηρίου %s"
+msgstr "Ομάδα αποθετηρίων %s"
 
 #: kallithea/templates/admin/repo_groups/repo_groups.html:5
 msgid "Repository Groups Administration"
-msgstr ""
+msgstr "Διαχείριση Ομάδων Αποθετηρίου"
 
 #: kallithea/templates/admin/repo_groups/repo_groups.html:41
 msgid "Number of Top-level Repositories"
-msgstr ""
+msgstr "Αριθμός αποθετηρίων ανώτατου επιπέδου"
 
 #: kallithea/templates/admin/repos/repo_add_base.html:12
 msgid "Clone remote repository"
-msgstr ""
+msgstr "Κλωνοποίηση απομακρυσμένου αποθετηρίου"
 
 #: kallithea/templates/admin/repos/repo_add_base.html:16
 msgid ""
 "Optional: URL of a remote repository. If set, the repository will be "
 "created as a clone from this URL."
 msgstr ""
+"Προαιρετικό: Διεύθυνση URL ενός απομακρυσμένου αποθετηρίου. Εάν οριστεί, "
+"το αποθετήριο θα δημιουργηθεί ως κλώνος από αυτήν τη διεύθυνση URL."
 
 #: kallithea/templates/admin/repos/repo_add_base.html:24
 #: kallithea/templates/admin/repos/repo_edit_settings.html:57
@@ -3046,16 +3144,19 @@
 msgid ""
 "Keep it short and to the point. Use a README file for longer descriptions."
 msgstr ""
+"Κρατήστε τη σύντομη και περιεκτική. Χρησιμοποιήστε ένα αρχείο README για "
+"μεγαλύτερες περιγραφές."
 
 #: kallithea/templates/admin/repos/repo_add_base.html:31
 #: kallithea/templates/admin/repos/repo_edit_settings.html:36
 #: kallithea/templates/forks/fork.html:45
 msgid "Optionally select a group to put this repository into."
 msgstr ""
+"Προαιρετικά, επιλέξτε μια ομάδα για να τοποθετήσετε αυτό το αποθετήριο."
 
 #: kallithea/templates/admin/repos/repo_add_base.html:45
 msgid "Type of repository to create."
-msgstr ""
+msgstr "Τύπος αποθετηρίου προς δημιουργία."
 
 #: kallithea/templates/admin/repos/repo_add_base.html:49
 #: kallithea/templates/admin/repos/repo_edit_settings.html:40
@@ -3068,15 +3169,17 @@
 "Default revision for files page, downloads, full text search index and "
 "readme generation"
 msgstr ""
+"Προεπιλεγμένη αναθεώρηση για τη σελίδα αρχείων, λήψεων, ευρετήριο "
+"αναζήτησης πλήρους κειμένου και δημιουργία readme"
 
 #: kallithea/templates/admin/repos/repo_creating.html:9
 #, python-format
 msgid "%s Creating Repository"
-msgstr ""
+msgstr "%s Δημιουργία Αποθετηρίου"
 
 #: kallithea/templates/admin/repos/repo_creating.html:13
 msgid "Creating repository"
-msgstr ""
+msgstr "Δημιουργία αποθετηρίου"
 
 #: kallithea/templates/admin/repos/repo_creating.html:27
 #, python-format
@@ -3084,44 +3187,45 @@
 "Repository \"%(repo_name)s\" is being created, you will be redirected "
 "when this process is finished.repo_name"
 msgstr ""
+"Δημιουργείται το αποθετήριο \"%(repo_name)s\", θα ανακατευθυνθείτε όταν "
+"ολοκληρωθεί αυτή η διαδικασία."
 
 #: kallithea/templates/admin/repos/repo_creating.html:39
 msgid ""
 "We're sorry but error occurred during this operation. Please check your "
 "Kallithea server logs, or contact administrator."
 msgstr ""
+"Λυπούμαστε, αλλά παρουσιάστηκε σφάλμα κατά τη διάρκεια αυτής της "
+"λειτουργίας. Ελέγξτε τα αρχεία καταγραφής του διακομιστή Καλλιθέας ή "
+"επικοινωνήστε με το διαχειριστή."
 
 #: kallithea/templates/admin/repos/repo_edit.html:8
 #, python-format
 msgid "%s Repository Settings"
-msgstr ""
+msgstr "Ρυθμίσεις Αποθετηρίου %s"
 
 #: kallithea/templates/admin/repos/repo_edit.html:34
 msgid "Extra Fields"
-msgstr ""
+msgstr "Επιπλέον Πεδία"
 
 #: kallithea/templates/admin/repos/repo_edit.html:37
-msgid "Caches"
-msgstr ""
+msgid "Remote"
+msgstr "Απομακρυσμένο"
 
 #: kallithea/templates/admin/repos/repo_edit.html:40
-msgid "Remote"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit.html:43
 #: kallithea/templates/summary/statistics.html:8
 #: kallithea/templates/summary/summary.html:169
 #: kallithea/templates/summary/summary.html:170
 msgid "Statistics"
-msgstr ""
+msgstr "Στατιστικά"
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:1
 msgid "Parent"
-msgstr ""
+msgstr "Γονικό"
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:5
 msgid "Set"
-msgstr ""
+msgstr "Ορισμός"
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:7
 msgid "Manually set this repository as a fork of another from the list."
@@ -3129,38 +3233,40 @@
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:20
 msgid "Public Journal Visibility"
-msgstr ""
+msgstr "Ορατότητα δημόσιων εγγραφών"
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:27
 msgid "Remove from public journal"
-msgstr ""
+msgstr "Κατάργηση από τις δημόσιες εγγραφές"
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:32
 msgid "Add to Public Journal"
-msgstr ""
+msgstr "Προσθήκη στις Δημόσια Εγγραφές"
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:37
 msgid ""
 "All actions done in this repository will be visible to everyone in the "
 "public journal."
 msgstr ""
+"Όλες οι ενέργειες που γίνονται σε αυτό το αποθετήριο θα είναι ορατές σε "
+"όλους στις δημόσιες εγγραφές."
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:46
-#: kallithea/templates/data_table/_dt_elements.html:68
+#: kallithea/templates/data_table/_dt_elements.html:74
 #, python-format
 msgid "Confirm to delete this repository: %s"
-msgstr ""
+msgstr "Επιβεβαίωση διαγραφής αυτού του αποθετηρίου: %s"
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:48
 msgid "Delete this Repository"
-msgstr ""
+msgstr "Διαγραφή αυτού του Αποθετηρίου"
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:51
 #, python-format
 msgid "This repository has %s fork"
 msgid_plural "This repository has %s forks"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "Αυτό το αποθετήριο έχει %s παράγωγο"
+msgstr[1] "Αυτό το αποθετήριο έχει %s παράγωγα"
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:54
 msgid "Detach forks"
@@ -3176,102 +3282,77 @@
 "administrator expires it. The administrator can both permanently delete "
 "it or restore it."
 msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:4
-msgid "Invalidate Repository Cache"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:6
-msgid ""
-"Manually invalidate cache for this repository. On first access, the "
-"repository will be cached again."
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:9
-msgid "List of Cached Values"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:12
-msgid "Prefix"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:13
-#: kallithea/templates/admin/repos/repo_edit_fields.html:7
-msgid "Key"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:14
-#: kallithea/templates/admin/user_groups/user_group_add.html:40
-#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
-#: kallithea/templates/admin/user_groups/user_groups.html:41
-#: kallithea/templates/admin/users/user_add.html:69
-#: kallithea/templates/admin/users/user_edit_profile.html:74
-#: kallithea/templates/admin/users/users.html:42
-msgid "Active"
-msgstr ""
+"Το διαγραμμένο αποθετήριο θα απομακρυνθεί και θα κρυφτεί έως ότου το "
+"λήξει ο διαχειριστής. Ο διαχειριστής μπορεί να το διαγράψει οριστικά ή να "
+"το επαναφέρει."
 
 #: kallithea/templates/admin/repos/repo_edit_fields.html:6
 msgid "Label"
-msgstr ""
+msgstr "Ετικέτα"
+
+#: kallithea/templates/admin/repos/repo_edit_fields.html:7
+msgid "Key"
+msgstr "Κλειδί"
 
 #: kallithea/templates/admin/repos/repo_edit_fields.html:20
 #, python-format
 msgid "Confirm to delete this field: %s"
-msgstr ""
+msgstr "Επιβεβαίωση διαγραφής αυτού του πεδίου: %s"
 
 #: kallithea/templates/admin/repos/repo_edit_fields.html:31
 msgid "New field key"
-msgstr ""
+msgstr "Νέο κλειδί πεδίου"
 
 #: kallithea/templates/admin/repos/repo_edit_fields.html:38
 msgid "New field label"
-msgstr ""
+msgstr "Νέα ετικέτα πεδίου"
 
 #: kallithea/templates/admin/repos/repo_edit_fields.html:40
 msgid "Enter short label"
-msgstr ""
+msgstr "Εισαγωγή σύντομης ετικέτας"
 
 #: kallithea/templates/admin/repos/repo_edit_fields.html:45
 msgid "New field description"
-msgstr ""
+msgstr "Νέα περιγραφή πεδίου"
 
 #: kallithea/templates/admin/repos/repo_edit_fields.html:47
 msgid "Enter description of a field"
-msgstr ""
+msgstr "Εισαγωγή περιγραφής ενός πεδίου"
 
 #: kallithea/templates/admin/repos/repo_edit_fields.html:61
 msgid "Extra fields are disabled."
-msgstr ""
+msgstr "Τα επιπλέον πεδία είναι απενεργοποιημένα."
 
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:20
 msgid "Private Repository"
-msgstr ""
+msgstr "Ιδιωτικό Αποθετήριο"
 
 #: kallithea/templates/admin/repos/repo_edit_remote.html:4
-#, fuzzy
-#| msgid "Empty repository"
 msgid "Fork of repository"
-msgstr "Άδειο αποθετήριο"
+msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit_remote.html:7
 msgid "Remote repository URL"
-msgstr ""
+msgstr "Διεύθυνση URL απομακρυσμένου αποθετηρίου"
 
 #: kallithea/templates/admin/repos/repo_edit_remote.html:15
 msgid "Pull Changes from Remote Repository"
-msgstr ""
+msgstr "Τραβήξτε τις αλλαγές από το απομακρυσμένο αποθετήριο"
 
 #: kallithea/templates/admin/repos/repo_edit_remote.html:17
 msgid "Confirm to pull changes from remote repository."
 msgstr ""
+"Επιβεβαιώστε ότι θα τραβήξετε αλλαγές από το απομακρυσμένο αποθετήριο "
+"δεδομένων."
 
 #: kallithea/templates/admin/repos/repo_edit_remote.html:23
 msgid "This repository does not have a remote repository URL."
 msgstr ""
+"Αυτό το αποθετήριο δεν έχει διεύθυνση URL απομακρυσμένου αποθετηρίου."
 
 #: kallithea/templates/admin/repos/repo_edit_settings.html:10
 msgid "Permanent URL"
-msgstr ""
+msgstr "Μόνιμη διεύθυνση URL"
 
 #: kallithea/templates/admin/repos/repo_edit_settings.html:14
 msgid ""
@@ -3282,73 +3363,84 @@
 "                               This is useful for CI systems, or any "
 "other cases that you need to hardcode the URL into a 3rd party service."
 msgstr ""
+"Η διεύθυνση URL του αποθετηρίου αλλάζει όταν αυτό μετονομαστεί ή "
+"μετακινηθεί σε άλλη ομάδα.\n"
+"                               Η χρήση της παραπάνω μόνιμης διεύθυνσης "
+"URL εγγυάται ότι αυτό το αποθετήριο θα είναι πάντα προσβάσιμο σε αυτήν τη "
+"διεύθυνση URL.\n"
+"                               Αυτό είναι χρήσιμο για συστήματα CI ή για "
+"άλλες περιπτώσεις που χρειάζεστε να κωδικοποιήσετε τη διεύθυνση URL σε "
+"κάποια υπηρεσία τρίτου μέρους."
 
 #: kallithea/templates/admin/repos/repo_edit_settings.html:21
 msgid "Remote repository"
-msgstr ""
+msgstr "Απομακρυσμένο αποθετήριο"
 
 #: kallithea/templates/admin/repos/repo_edit_settings.html:24
 msgid "Repository URL"
-msgstr ""
+msgstr "URL Αποθετηρίου"
 
 #: kallithea/templates/admin/repos/repo_edit_settings.html:28
 msgid ""
 "Optional: URL of a remote repository. If set, the repository can be "
 "pulled from this URL."
 msgstr ""
+"Προαιρετικό: Διεύθυνση URL ενός απομακρυσμένου αποθετηρίου. Εάν οριστεί, "
+"το αποθετήριο μπορεί να τραβηχτεί από αυτήν τη διεύθυνση URL."
 
 #: kallithea/templates/admin/repos/repo_edit_settings.html:43
 msgid "Default revision for files page, downloads, whoosh and readme"
 msgstr ""
+"Προεπιλεγμένη αναθεώρηση για τη σελίδα αρχείων, λήψεων, Whoosh και readme"
 
 #: kallithea/templates/admin/repos/repo_edit_settings.html:49
 #: kallithea/templates/pullrequests/pullrequest_show.html:131
 msgid "Type name of user"
-msgstr ""
+msgstr "Πληκτρολογήστε το όνομα του χρήστη"
 
 #: kallithea/templates/admin/repos/repo_edit_settings.html:50
 msgid "Change owner of this repository."
-msgstr ""
+msgstr "Αλλάξτε τον κάτοχο αυτού του αποθετηρίου."
 
 #: kallithea/templates/admin/repos/repo_edit_statistics.html:5
 msgid "Processed commits"
-msgstr ""
+msgstr "Επεξεργασμένα commits"
 
 #: kallithea/templates/admin/repos/repo_edit_statistics.html:6
 msgid "Processed progress"
-msgstr ""
+msgstr "Επεξεργασμένη πρόοδος"
 
 #: kallithea/templates/admin/repos/repo_edit_statistics.html:10
 msgid "Reset Statistics"
-msgstr ""
+msgstr "Επαναφορά Στατιστικών"
 
 #: kallithea/templates/admin/repos/repo_edit_statistics.html:10
 msgid "Confirm to remove current statistics."
-msgstr ""
+msgstr "Επιβεβαιώστε την κατάργηση των τρεχόντων στατιστικών στοιχείων."
 
 #: kallithea/templates/admin/repos/repos.html:5
 msgid "Repositories Administration"
-msgstr ""
+msgstr "Διαχείριση Αποθετηρίων"
 
 #: kallithea/templates/admin/repos/repos.html:43
 msgid "State"
-msgstr ""
+msgstr "Κατάσταση"
 
 #: kallithea/templates/admin/settings/settings.html:5
 msgid "Settings Administration"
-msgstr ""
+msgstr "Διαχείριση Ρυθμίσεων"
 
 #: kallithea/templates/admin/settings/settings.html:27
 msgid "VCS"
-msgstr ""
+msgstr "VCS"
 
 #: kallithea/templates/admin/settings/settings.html:28
 msgid "Remap and Rescan"
-msgstr ""
+msgstr "Επανάληψη αντιστοίχισης και επανασάρωση"
 
 #: kallithea/templates/admin/settings/settings.html:30
 msgid "Visual"
-msgstr ""
+msgstr "Εμφάνιση"
 
 #: kallithea/templates/admin/settings/settings.html:32
 #: kallithea/templates/admin/settings/settings_vcs.html:4
@@ -3357,27 +3449,27 @@
 
 #: kallithea/templates/admin/settings/settings.html:33
 msgid "Full Text Search"
-msgstr ""
+msgstr "Αναζήτηση Πλήρους Κειμένου"
 
 #: kallithea/templates/admin/settings/settings.html:34
 msgid "System Info"
-msgstr ""
+msgstr "Πληροφορίες Συστήματος"
 
 #: kallithea/templates/admin/settings/settings_email.html:4
 msgid "Send test email to"
-msgstr ""
+msgstr "Αποστολή δοκιμαστικού μηνύματος ηλεκτρονικού ταχυδρομείου σε"
 
 #: kallithea/templates/admin/settings/settings_email.html:12
 msgid "Send"
-msgstr ""
+msgstr "Αποστολή"
 
 #: kallithea/templates/admin/settings/settings_global.html:4
 msgid "Site branding"
-msgstr ""
+msgstr "Επωνυμία ιστότοπου"
 
 #: kallithea/templates/admin/settings/settings_global.html:7
 msgid "Set a custom title for your Kallithea Service."
-msgstr ""
+msgstr "Ορίστε έναν προσαρμοσμένο τίτλο για την υπηρεσία της Καλλιθέα σας."
 
 #: kallithea/templates/admin/settings/settings_global.html:12
 msgid "HTTP authentication realm"
@@ -3385,7 +3477,7 @@
 
 #: kallithea/templates/admin/settings/settings_global.html:19
 msgid "HTML/JavaScript/CSS customization block"
-msgstr ""
+msgstr "Μπλοκ προσαρμογής HTML / JavaScript / CSS"
 
 #: kallithea/templates/admin/settings/settings_global.html:22
 msgid ""
@@ -3395,34 +3487,40 @@
 "to                         perform instance-specific customizations like "
 "adding a                         project banner at the top of every page."
 msgstr ""
+"HTML (ενδεχομένως με JavaScript ή / και CSS) που θα προστεθούν στο κάτω "
+"μέρος της κάθε σελίδας. Αυτό μπορεί να χρησιμοποιηθεί για web analytics, "
+"αλλά και για την προσαρμογή της εμφάνισης, όπως η προσθήκη ενός banner "
+"στο επάνω μέρος κάθε σελίδας."
 
 #: kallithea/templates/admin/settings/settings_global.html:32
 msgid "ReCaptcha public key"
-msgstr ""
+msgstr "Δημόσιο κλειδί ReCaptcha"
 
 #: kallithea/templates/admin/settings/settings_global.html:35
 msgid "Public key for reCaptcha system."
-msgstr ""
+msgstr "Δημόσιο κλειδί για το σύστημα reCaptcha."
 
 #: kallithea/templates/admin/settings/settings_global.html:40
 msgid "ReCaptcha private key"
-msgstr ""
+msgstr "Ιδιωτικό κλειδί ReCaptcha"
 
 #: kallithea/templates/admin/settings/settings_global.html:43
 msgid ""
 "Private key for reCaptcha system. Setting this value will enable captcha "
 "on registration."
 msgstr ""
+"Ιδιωτικό κλειδί για το σύστημα reCaptcha. Ο καθορισμός αυτής της τιμής θα "
+"ενεργοποιήσει το captcha κατά την εγγραφή."
 
 #: kallithea/templates/admin/settings/settings_global.html:49
 #: kallithea/templates/admin/settings/settings_vcs.html:65
 #: kallithea/templates/admin/settings/settings_visual.html:128
 msgid "Save Settings"
-msgstr ""
+msgstr "Αποθήκευση Ρυθμίσεων"
 
 #: kallithea/templates/admin/settings/settings_hooks.html:3
 msgid "Built-in Mercurial Hooks (Read-Only)"
-msgstr ""
+msgstr "Ενσωματωμένοι Mercurial Hooks (μόνο για ανάγνωση)"
 
 #: kallithea/templates/admin/settings/settings_hooks.html:17
 msgid "Custom Hooks"
@@ -3440,39 +3538,46 @@
 
 #: kallithea/templates/admin/settings/settings_mapping.html:4
 msgid "Rescan options"
-msgstr ""
+msgstr "Επιλογές Επανασάρωσης"
 
 #: kallithea/templates/admin/settings/settings_mapping.html:9
 msgid "Delete records of missing repositories"
-msgstr ""
+msgstr "Διαγραφή εγγραφών αποθετηρίων που λείπουν"
 
 #: kallithea/templates/admin/settings/settings_mapping.html:12
 msgid ""
 "Check this option to remove all comments, pull requests and other records "
 "related to repositories that no longer exist in the filesystem."
 msgstr ""
+"Επιλέξτε αυτήν την επιλογή για να καταργήσετε όλα τα σχόλια, να αιτήματα "
+"έλξης και άλλες εγγραφές που σχετίζονται με αποθετήρια που δεν υπάρχουν "
+"πλέον στο σύστημα αρχείων."
 
 #: kallithea/templates/admin/settings/settings_mapping.html:17
 msgid "Invalidate cache for all repositories"
-msgstr ""
+msgstr "Ακυρώνει την προσωρινή αποθήκευση για όλα τα αποθετήρια"
 
 #: kallithea/templates/admin/settings/settings_mapping.html:20
 msgid "Check this to reload data and clear cache keys for all repositories."
 msgstr ""
+"Επιλέξτε αυτό για να φορτώσετε ξανά τα δεδομένα και να καταργήστε την "
+"cache για όλα τα αποθετήρια."
 
 #: kallithea/templates/admin/settings/settings_mapping.html:25
 msgid "Install Git hooks"
-msgstr ""
+msgstr "Εγκατάσταση Git hooks"
 
 #: kallithea/templates/admin/settings/settings_mapping.html:28
 msgid ""
 "Verify if Kallithea's Git hooks are installed for each repository. "
 "Current hooks will be updated to the latest version."
 msgstr ""
+"Επαληθεύστε εάν τα Git hooks της Καλλιθέας είναι εγκατεστημένα για κάθε "
+"αποθετήριο. Τα τρέχοντα hooks θα ενημερωθούν στην τελευταία έκδοση."
 
 #: kallithea/templates/admin/settings/settings_mapping.html:32
 msgid "Overwrite existing Git hooks"
-msgstr ""
+msgstr "Αντικατάσταση υπαρχόντων Git hooks"
 
 #: kallithea/templates/admin/settings/settings_mapping.html:35
 msgid ""
@@ -3480,127 +3585,144 @@
 "not seem to come from Kallithea. WARNING: This operation will destroy any "
 "custom git hooks you may have deployed by hand!"
 msgstr ""
+"Εάν εγκαθιστάτε Git hooks, αντικαταστήστε τυχόν υπάρχοντα hooks, ακόμα κι "
+"αν δεν φαίνεται να προέρχονται από την Καλλιθέα. ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Αυτή η "
+"λειτουργία θα καταστρέψει τυχόν προσαρμοσμένα git hooks που μπορεί να "
+"έχετε αναπτύξει με το χέρι!"
 
 #: kallithea/templates/admin/settings/settings_mapping.html:41
 msgid "Rescan Repositories"
-msgstr ""
+msgstr "Επανασάρωση αποθετηρίων"
 
 #: kallithea/templates/admin/settings/settings_search.html:4
 msgid "Index build option"
-msgstr ""
+msgstr "Επιλογή δημιουργίας ευρετηρίου"
 
 #: kallithea/templates/admin/settings/settings_search.html:9
 msgid "Build from scratch"
-msgstr ""
+msgstr "Κατασκευή από το μηδέν"
 
 #: kallithea/templates/admin/settings/settings_search.html:12
 msgid ""
 "This option completely reindexeses all of the repositories for proper "
 "fulltext search capabilities."
 msgstr ""
+"Αυτή η επιλογή ξαναδημιουργεί πλήρως τα ευρετήρια σε όλα τα αποθετήρια "
+"για δυνατότητα αναζήτησης πλήρους κειμένου."
 
 #: kallithea/templates/admin/settings/settings_search.html:18
 msgid "Reindex"
-msgstr ""
+msgstr "Αναδημιουργία ευρετηρίου"
 
 #: kallithea/templates/admin/settings/settings_system.html:2
 msgid "Checking for updates..."
-msgstr ""
+msgstr "Έλεγχος για ενημερώσεις..."
 
 #: kallithea/templates/admin/settings/settings_system.html:7
 msgid "Kallithea version"
-msgstr ""
+msgstr "Έκδοση Καλλιθέας"
 
 #: kallithea/templates/admin/settings/settings_system.html:8
 msgid "Kallithea configuration file"
-msgstr ""
+msgstr "Αρχείο διαμόρφωσης Καλλιθέας"
 
 #: kallithea/templates/admin/settings/settings_system.html:9
 msgid "Python version"
-msgstr ""
+msgstr "Έκδοση Python"
 
 #: kallithea/templates/admin/settings/settings_system.html:10
 msgid "Platform"
-msgstr ""
+msgstr "Πλατφόρμα"
 
 #: kallithea/templates/admin/settings/settings_system.html:11
 msgid "Git version"
-msgstr ""
+msgstr "Έκδοση Git"
 
 #: kallithea/templates/admin/settings/settings_system.html:12
 msgid "Git path"
-msgstr ""
+msgstr "Διαδρομή Git"
 
 #: kallithea/templates/admin/settings/settings_system.html:22
 msgid "Python Packages"
-msgstr ""
+msgstr "Πακέτα Python"
 
 #: kallithea/templates/admin/settings/settings_vcs.html:9
 msgid "Show repository size after push"
-msgstr ""
+msgstr "Εμφάνιση μεγέθους αποθετηρίου μετά την ώθηση"
 
 #: kallithea/templates/admin/settings/settings_vcs.html:15
 msgid "Update repository after push (hg update)"
-msgstr ""
+msgstr "Ενημέρωση αποθετηρίου μετά την ώθηση (hg update)"
 
 #: kallithea/templates/admin/settings/settings_vcs.html:21
 msgid "Mercurial extensions"
-msgstr ""
+msgstr "Επεκτάσεις Mercurial"
 
 #: kallithea/templates/admin/settings/settings_vcs.html:26
 msgid "Enable largefiles extension"
-msgstr ""
+msgstr "Ενεργοποίηση επέκτασης μεγάλων αρχείων"
 
 #: kallithea/templates/admin/settings/settings_vcs.html:32
 msgid "Enable hgsubversion extension"
-msgstr ""
+msgstr "Ενεργοποίηση επέκτασης hgsubversion"
 
 #: kallithea/templates/admin/settings/settings_vcs.html:35
 msgid ""
 "Requires hgsubversion library to be installed. Enables cloning of remote "
 "Subversion repositories while converting them to Mercurial."
 msgstr ""
+"Απαιτεί την εγκατάσταση της βιβλιοθήκης hgsubversion. Ενεργοποιεί την "
+"κλωνοποίηση απομακρυσμένων Subversion αποθετηρίων και τη μετατροπή τους "
+"σε Mercurial."
 
 #: kallithea/templates/admin/settings/settings_vcs.html:47
 msgid "Location of repositories"
-msgstr ""
+msgstr "Τοποθεσία αποθετηρίων"
 
 #: kallithea/templates/admin/settings/settings_vcs.html:52
 msgid ""
 "Click to unlock. You must restart Kallithea in order to make this setting "
 "take effect."
 msgstr ""
+"Κάντε κλικ για να ξεκλειδώσετε. Πρέπει να επανεκκινήσετε την Καλλιθέα για "
+"να εφαρμοστεί αυτή η ρύθμιση."
 
 #: kallithea/templates/admin/settings/settings_vcs.html:56
 msgid ""
 "Filesystem location where repositories are stored. After changing this "
 "value, a restart and rescan of the repository folder are both required."
 msgstr ""
+"Θέση συστήματος αρχείων όπου αποθηκεύονται τα αποθετήρια. Μετά την αλλαγή "
+"αυτής της τιμής, απαιτείται επανεκκίνηση και σάρωση του φακέλου "
+"αποθετηρίου."
 
 #: kallithea/templates/admin/settings/settings_visual.html:4
 msgid "General"
-msgstr ""
+msgstr "Γενικά"
 
 #: kallithea/templates/admin/settings/settings_visual.html:9
 msgid "Use repository extra fields"
-msgstr ""
+msgstr "Χρήση πρόσθετων πεδίων αποθετηρίου"
 
 #: kallithea/templates/admin/settings/settings_visual.html:12
 msgid "Allows storing additional customized fields per repository."
 msgstr ""
+"Επιτρέπει την αποθήκευση πρόσθετων προσαρμοσμένων πεδίων ανά αποθετήριο."
 
 #: kallithea/templates/admin/settings/settings_visual.html:17
 msgid "Show Kallithea version"
-msgstr ""
+msgstr "Εμφάνιση της έκδοσης Καλλιθέας"
 
 #: kallithea/templates/admin/settings/settings_visual.html:20
 msgid ""
 "Shows or hides a version number of Kallithea displayed in the footer."
 msgstr ""
+"Εμφανίζει ή αποκρύπτει τον αριθμό έκδοσης της Καλλιθέας που εμφανίζεται "
+"στο υποσέλιδο."
 
 #: kallithea/templates/admin/settings/settings_visual.html:25
 msgid "Show user Gravatars"
-msgstr ""
+msgstr "Εμφάνιση Gravatars του χρήστη"
 
 #: kallithea/templates/admin/settings/settings_visual.html:29
 msgid ""
@@ -3618,10 +3740,24 @@
 "                                                        {netloc}    "
 "network location/server host of running Kallithea server"
 msgstr ""
+"Το Gravatar URL σας επιτρέπει να χρησιμοποιήσετε avatar από έναν άλλο "
+"διακομιστή.\n"
+"                                                        Οι ακόλουθες "
+"μεταβλητές της διεύθυνσης URL θα αντικατασταθούν ανάλογα.\n"
+"                                                        {scheme} 'http' ή "
+"'https' που αποστέλλεται από την εκτέλεση του διακομιστή της Καλλιθέας,\n"
+"                                                        {email} "
+"ηλεκτρονικό ταχυδρομείο,\n"
+"                                                        {md5email} md5 "
+"hash του email χρήστη (όπως στο gravatar.com),\n"
+"                                                        {size} μέγεθος "
+"της εικόνας που αναμένεται από το διακομιστή,\n"
+"                                                        {netloc} θέση "
+"δικτύου/διακομιστή που τρέχει την Καλλιθέα"
 
 #: kallithea/templates/admin/settings/settings_visual.html:40
 msgid "HTTP Clone URL"
-msgstr ""
+msgstr "HTTP Clone URL"
 
 #: kallithea/templates/admin/settings/settings_visual.html:43
 msgid ""
@@ -3645,64 +3781,90 @@
 "hostname\n"
 "                                                    "
 msgstr ""
+"Κατασκευή σχήματος του URL clone π.χ. '{scheme}}}{user}@{netloc}/"
+"{repo}'.\n"
+"                                                    Οι ακόλουθες "
+"μεταβλητές είναι διαθέσιμες:\n"
+"                                                    {scheme} 'http' ή "
+"'https' αποστέλλεται από την εκτέλεση του διακομιστή της Καλλιθέας,\n"
+"                                                    {user} τρέχον όνομα "
+"χρήστη,\n"
+"                                                    {netloc} θέση δικτύου/"
+"κεντρικός υπολογιστής διακομιστή που τρέχει το διακομιστή της Καλλιθέας,\n"
+"                                                    {repo} πλήρες όνομα "
+"αποθετηρίου,\n"
+"                                                    {repoid} ID του "
+"αποθετηρίου, μπορεί να χρησιμοποιηθεί για την κατασκευή clone-by-id,\n"
+"                                                    {system_user} όνομα "
+"του χρήστη του συστήματος Καλλιθέας,\n"
+"                                                    {hostname} όνομα του "
+"διακομιστή\n"
+"                                                    "
 
 #: kallithea/templates/admin/settings/settings_visual.html:56
 msgid "SSH Clone URL"
-msgstr ""
+msgstr "SSH Clone URL"
 
 #: kallithea/templates/admin/settings/settings_visual.html:59
 msgid ""
 "Schema for constructing SSH clone URL, eg. 'ssh://{system_user}"
 "@{hostname}/{repo}'."
 msgstr ""
+"Κατασκευή σχήματος SSH clone URL, πχ. 'ssh://{system_user}@{hostname}/"
+"{repo}'."
 
 #: kallithea/templates/admin/settings/settings_visual.html:67
-#, fuzzy
-#| msgid "Repositories"
 msgid "Repository page size"
-msgstr "Αποθετήρια"
+msgstr "Μέγεθος σελίδας αποθετηρίου"
 
 #: kallithea/templates/admin/settings/settings_visual.html:70
 msgid ""
 "Number of items displayed in the repository pages before pagination is "
 "shown."
 msgstr ""
+"Ο αριθμός των αντικειμένων που εμφανίζονται στις σελίδες αποθετηρίου πριν "
+"εφαρμοστεί η σελιδοποίηση."
 
 #: kallithea/templates/admin/settings/settings_visual.html:75
 msgid "Admin page size"
-msgstr ""
+msgstr "Μέγεθος σελίδας διαχειριστή"
 
 #: kallithea/templates/admin/settings/settings_visual.html:78
 msgid ""
 "Number of items displayed in the admin pages grids before pagination is "
 "shown."
 msgstr ""
+"Ο αριθμός των στοιχείων που εμφανίζονται στα πλέγματα των σελίδων "
+"διαχειριστή πριν εφαρμοστεί η σελιδοποίηση."
 
 #: kallithea/templates/admin/settings/settings_visual.html:83
 msgid "Icons"
-msgstr ""
+msgstr "Εικονίδια"
 
 #: kallithea/templates/admin/settings/settings_visual.html:88
 msgid "Show public repository icon on repositories"
-msgstr ""
+msgstr "Εμφάνιση δημόσιου εικονιδίου αποθετηρίου στα αποθετήρια"
 
 #: kallithea/templates/admin/settings/settings_visual.html:94
 msgid "Show private repository icon on repositories"
-msgstr ""
+msgstr "Εμφάνιση εικονιδίου ιδιωτικού αποθετηρίου στα αποθετήρια"
 
 #: kallithea/templates/admin/settings/settings_visual.html:97
 msgid "Show public/private icons next to repository names."
 msgstr ""
+"Εμφάνιση δημόσιων/ιδιωτικών εικονιδίων δίπλα στα ονόματα αποθετηρίων."
 
 #: kallithea/templates/admin/settings/settings_visual.html:102
 msgid "Meta Tagging"
-msgstr ""
+msgstr "Μεταετικέτες"
 
 #: kallithea/templates/admin/settings/settings_visual.html:107
 msgid ""
 "Parses meta tags from the repository description field and turns them "
 "into colored tags."
 msgstr ""
+"Αναλύει τις μετα-ετικέτες από το πεδίο περιγραφής του αποθετηρίου και τις "
+"μετατρέπει σε έγχρωμες ετικέτες."
 
 #: kallithea/templates/admin/settings/settings_visual.html:111
 msgid "Stylify recognised meta tags:"
@@ -3710,7 +3872,7 @@
 
 #: kallithea/templates/admin/user_groups/user_group_add.html:5
 msgid "Add user group"
-msgstr ""
+msgstr "Προσθήκη ομάδας χρηστών"
 
 #: kallithea/templates/admin/user_groups/user_group_add.html:10
 #: kallithea/templates/admin/user_groups/user_group_edit.html:11
@@ -3718,126 +3880,135 @@
 #: kallithea/templates/base/base.html:59
 #: kallithea/templates/base/base.html:79
 msgid "User Groups"
-msgstr ""
+msgstr "Ομάδες Χρηστών"
 
 #: kallithea/templates/admin/user_groups/user_group_add.html:12
 #: kallithea/templates/admin/user_groups/user_groups.html:24
 msgid "Add User Group"
-msgstr ""
+msgstr "Προσθήκη Ομάδας Χρηστών"
 
 #: kallithea/templates/admin/user_groups/user_group_add.html:36
 #: kallithea/templates/admin/user_groups/user_group_edit_settings.html:13
 msgid "Short, optional description for this user group."
-msgstr ""
+msgstr "Σύντομη, προαιρετική περιγραφή για αυτήν την ομάδα χρηστών."
+
+#: kallithea/templates/admin/user_groups/user_group_add.html:40
+#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
+#: kallithea/templates/admin/user_groups/user_groups.html:41
+#: kallithea/templates/admin/users/user_add.html:69
+#: kallithea/templates/admin/users/user_edit_profile.html:74
+#: kallithea/templates/admin/users/users.html:42
+msgid "Active"
+msgstr "Ενεργό"
 
 #: kallithea/templates/admin/user_groups/user_group_edit.html:5
 #, python-format
 msgid "%s user group settings"
-msgstr ""
+msgstr "Ρυθμίσεις ομάδας χρηστών %s"
 
 #: kallithea/templates/admin/user_groups/user_group_edit.html:33
 msgid "Show Members"
-msgstr ""
+msgstr "Εμφάνιση Μελών"
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:1
 #, python-format
 msgid "User Group: %s"
-msgstr ""
+msgstr "Ομάδα Χρηστών: %s"
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:6
 #: kallithea/templates/admin/user_groups/user_group_edit_settings.html:23
 #: kallithea/templates/admin/user_groups/user_groups.html:40
 msgid "Members"
-msgstr ""
+msgstr "Μέλη"
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:19
-#: kallithea/templates/data_table/_dt_elements.html:102
+#: kallithea/templates/data_table/_dt_elements.html:108
 #, python-format
 msgid "Confirm to delete this user group: %s"
-msgstr ""
+msgstr "Επιβεβαίωση για διαγραφή αυτής της ομάδας χρηστών: %s"
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:21
 msgid "Delete this user group"
-msgstr ""
+msgstr "Διαγραφή αυτής της ομάδας χρηστών"
 
 #: kallithea/templates/admin/user_groups/user_group_edit_members.html:11
 msgid "No members yet"
-msgstr ""
+msgstr "Δεν υπάρχουν μέλη ακόμα"
 
 #: kallithea/templates/admin/user_groups/user_group_edit_settings.html:26
 msgid "Chosen group members"
-msgstr ""
+msgstr "Επιλεγμένα μέλη της ομάδας"
 
 #: kallithea/templates/admin/user_groups/user_group_edit_settings.html:39
 msgid "Available members"
-msgstr ""
+msgstr "Διαθέσιμα μέλη"
 
 #: kallithea/templates/admin/user_groups/user_groups.html:5
 msgid "User Groups Administration"
-msgstr ""
+msgstr "Διαχείριση Ομάδων Χρηστών"
 
 #: kallithea/templates/admin/users/user_add.html:5
 msgid "Add user"
-msgstr ""
+msgstr "Προσθήκη χρήστη"
 
 #: kallithea/templates/admin/users/user_add.html:10
 #: kallithea/templates/admin/users/user_edit.html:11
 #: kallithea/templates/admin/users/users.html:9
 #: kallithea/templates/base/base.html:58
 msgid "Users"
-msgstr ""
+msgstr "Χρήστες"
 
 #: kallithea/templates/admin/users/user_add.html:12
 #: kallithea/templates/admin/users/users.html:23
 msgid "Add User"
-msgstr ""
+msgstr "Προσθήκη χρήστη"
 
 #: kallithea/templates/admin/users/user_add.html:41
 msgid "Password confirmation"
-msgstr ""
+msgstr "Επιβεβαίωση κωδικού πρόσβασης"
 
 #: kallithea/templates/admin/users/user_edit.html:5
 #, python-format
 msgid "%s user settings"
-msgstr ""
+msgstr "Ρυθμίσεις χρήστη %s"
 
 #: kallithea/templates/admin/users/user_edit.html:30
 msgid "Emails"
-msgstr ""
+msgstr "Μηνύματα ηλεκτρονικού ταχυδρομείου"
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:1
 #, python-format
 msgid "User: %s"
-msgstr ""
+msgstr "Χρήστης: %s"
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:7
 #: kallithea/templates/admin/users/user_edit_profile.html:32
 msgid "Source of Record"
-msgstr ""
+msgstr "Προέλευση εγγραφής"
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:9
 #: kallithea/templates/admin/users/users.html:41
 msgid "Last Login"
-msgstr ""
+msgstr "Τελευταία Σύνδεση"
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:10
 msgid "Member of User Groups"
-msgstr ""
+msgstr "Μέλος των Ομάδων Χρηστών"
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:90
+#: kallithea/templates/data_table/_dt_elements.html:96
 #, python-format
 msgid "Confirm to delete this user: %s"
-msgstr ""
+msgstr "Επιβεβαίωση διαγραφής αυτού του χρήστη: %s"
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:23
 msgid "Delete this user"
-msgstr ""
+msgstr "Διαγραφή αυτού του χρήστη"
 
 #: kallithea/templates/admin/users/user_edit_ips.html:7
 #, python-format
 msgid "Inherited from %s"
-msgstr ""
+msgstr "Κληρονομήθηκε από %s"
 
 #: kallithea/templates/admin/users/user_edit_profile.html:39
 msgid "Name in Source of Record"
@@ -3845,34 +4016,34 @@
 
 #: kallithea/templates/admin/users/user_edit_profile.html:53
 msgid "New password confirmation"
-msgstr ""
+msgstr "Επιβεβαίωση νέου κωδικού πρόσβασης"
 
 #: kallithea/templates/admin/users/users.html:5
 msgid "Users Administration"
-msgstr ""
+msgstr "Διαχείριση Χρηστών"
 
 #: kallithea/templates/admin/users/users.html:44
 msgid "Auth Type"
-msgstr ""
+msgstr "Τύπος Πιστοποίησης"
 
 #: kallithea/templates/base/base.html:16
 #, python-format
 msgid "Server instance: %s"
-msgstr ""
+msgstr "Παρουσία διακομιστή: %s"
 
 #: kallithea/templates/base/base.html:28
 msgid "Support"
-msgstr ""
+msgstr "Υποστήριξη"
 
 #: kallithea/templates/base/base.html:86
 #: kallithea/templates/base/base.html:417
 msgid "Mercurial repository"
-msgstr ""
+msgstr "Αποθετήριο Mercurial"
 
 #: kallithea/templates/base/base.html:89
 #: kallithea/templates/base/base.html:420
 msgid "Git repository"
-msgstr ""
+msgstr "Αποθετήριο Git"
 
 #: kallithea/templates/base/base.html:115
 msgid "Create Fork"
@@ -3881,32 +4052,32 @@
 #: kallithea/templates/base/base.html:127
 #: kallithea/templates/summary/summary.html:9
 msgid "Summary"
-msgstr ""
+msgstr "Περίληψη"
 
 #: kallithea/templates/base/base.html:129
 #: kallithea/templates/base/base.html:131
 #: kallithea/templates/changelog/changelog.html:16
 msgid "Changelog"
-msgstr ""
+msgstr "Ιστορικό αλλαγών"
 
 #: kallithea/templates/base/base.html:133
 #: kallithea/templates/files/files.html:11
 msgid "Files"
-msgstr ""
+msgstr "Αρχεία"
 
 #: kallithea/templates/base/base.html:135
 #, python-format
 msgid "Show Pull Requests for %s"
-msgstr ""
+msgstr "Εμφάνιση Αιτήσεων Έλξης για %s"
 
 #: kallithea/templates/base/base.html:135
 msgid "Pull Requests"
-msgstr ""
+msgstr "Αιτήματα Έλξης"
 
 #: kallithea/templates/base/base.html:146
 #: kallithea/templates/base/base.html:148
 msgid "Options"
-msgstr ""
+msgstr "Επιλογές"
 
 #: kallithea/templates/base/base.html:156
 #: kallithea/templates/forks/forks_data.html:18
@@ -3915,22 +4086,24 @@
 
 #: kallithea/templates/base/base.html:158
 msgid "Compare"
-msgstr ""
+msgstr "Σύγκριση"
 
 #: kallithea/templates/base/base.html:160
 #: kallithea/templates/base/base.html:315
 #: kallithea/templates/search/search.html:14
 #: kallithea/templates/search/search.html:67
 msgid "Search"
-msgstr ""
+msgstr "Αναζήτηση"
 
 #: kallithea/templates/base/base.html:167
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Follow"
-msgstr ""
+msgstr "Παρακολούθηση"
 
 #: kallithea/templates/base/base.html:168
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Unfollow"
-msgstr ""
+msgstr "Κατάργηση παρακολούθησης"
 
 #: kallithea/templates/base/base.html:171
 #: kallithea/templates/forks/fork.html:9
@@ -3940,100 +4113,104 @@
 #: kallithea/templates/base/base.html:172
 #: kallithea/templates/pullrequests/pullrequest.html:77
 msgid "Create Pull Request"
-msgstr ""
+msgstr "Δημιουργία Αιτήματος Έλξης"
 
 #: kallithea/templates/base/base.html:184
 msgid "Switch To"
-msgstr ""
+msgstr "Αλλαγή Σε"
 
 #: kallithea/templates/base/base.html:196
 #: kallithea/templates/base/base.html:445
 msgid "No matches found"
-msgstr ""
+msgstr "Δεν βρέθηκαν αντιστοιχίσεις"
 
 #: kallithea/templates/base/base.html:289
 msgid "Show recent activity"
-msgstr ""
+msgstr "Εμφάνιση πρόσφατης δραστηριότητας"
 
 #: kallithea/templates/base/base.html:295
 #: kallithea/templates/base/base.html:296
 msgid "Public journal"
-msgstr ""
+msgstr "Δημόσιο Ημερολόγιο"
 
 #: kallithea/templates/base/base.html:301
 msgid "Show public gists"
-msgstr ""
+msgstr "Εμφάνιση δημόσιων gists"
 
 #: kallithea/templates/base/base.html:302
 msgid "Gists"
-msgstr ""
+msgstr "Gists"
 
 #: kallithea/templates/base/base.html:306
 msgid "All Public Gists"
-msgstr ""
+msgstr "Όλα τα Δημόσια Gists"
 
 #: kallithea/templates/base/base.html:308
 msgid "My Public Gists"
-msgstr ""
+msgstr "Τα Δημόσιά μου Gists"
 
 #: kallithea/templates/base/base.html:309
 msgid "My Private Gists"
-msgstr ""
+msgstr "Τα Ιδιωτικά μου Gists"
 
 #: kallithea/templates/base/base.html:314
 msgid "Search in repositories"
-msgstr ""
+msgstr "Αναζήτηση σε αποθετήρια"
 
 #: kallithea/templates/base/base.html:337
 #: kallithea/templates/base/base.html:338
 #: kallithea/templates/pullrequests/pullrequest_show_my.html:6
 #: kallithea/templates/pullrequests/pullrequest_show_my.html:10
 msgid "My Pull Requests"
-msgstr ""
+msgstr "Τα αιτήματά μου για έλξη"
 
 #: kallithea/templates/base/base.html:353
 msgid "Not Logged In"
-msgstr ""
+msgstr "Δεν έχετε συνδεθεί"
 
 #: kallithea/templates/base/base.html:362
 msgid "Login to Your Account"
-msgstr ""
+msgstr "Συνδεθείτε στο λογαριασμό σας"
 
 #: kallithea/templates/base/base.html:372
 msgid "Forgot password?"
-msgstr ""
+msgstr "Ξεχάσατε τον κωδικό πρόσβασης;"
 
 #: kallithea/templates/base/base.html:376
 msgid "Don't have an account?"
-msgstr ""
+msgstr "Δεν έχετε λογαριασμό;"
 
 #: kallithea/templates/base/base.html:393
 msgid "Log Out"
-msgstr ""
+msgstr "Αποσύνδεση"
 
 #: kallithea/templates/base/base.html:517
 msgid "Parent rev."
-msgstr ""
+msgstr "Γονική αναθ."
 
 #: kallithea/templates/base/base.html:526
 msgid "Child rev."
-msgstr ""
+msgstr "Θυγατρική αναθ."
 
 #: kallithea/templates/base/default_perms_box.html:11
 msgid "Create repositories"
-msgstr ""
+msgstr "Δημιουργία αποθετηρίων"
 
 #: kallithea/templates/base/default_perms_box.html:15
 msgid "Select this option to allow repository creation for this user"
 msgstr ""
+"Ενεργοποιήστε αυτήν την επιλογή για να επιτρέψετε τη δημιουργία "
+"αποθετηρίου για αυτόν το χρήστη"
 
 #: kallithea/templates/base/default_perms_box.html:21
 msgid "Create user groups"
-msgstr ""
+msgstr "Δημιουργία ομάδων χρηστών"
 
 #: kallithea/templates/base/default_perms_box.html:25
 msgid "Select this option to allow user group creation for this user"
 msgstr ""
+"Ενεργοποιήστε αυτήν την επιλογή για να επιτρέψετε τη δημιουργία ομάδας "
+"χρηστών για αυτόν το χρήστη"
 
 #: kallithea/templates/base/default_perms_box.html:31
 msgid "Fork repositories"
@@ -4046,111 +4223,113 @@
 #: kallithea/templates/base/perms_summary.html:13
 #: kallithea/templates/changelog/changelog.html:41
 msgid "Show"
-msgstr ""
+msgstr "Εμφάνιση"
 
 #: kallithea/templates/base/perms_summary.html:22
 msgid "No permissions defined yet"
-msgstr ""
+msgstr "Δεν έχουν οριστεί ακόμα δικαιώματα"
 
 #: kallithea/templates/base/perms_summary.html:30
 #: kallithea/templates/base/perms_summary.html:55
 msgid "Permission"
-msgstr ""
+msgstr "Δικαίωμα"
 
 #: kallithea/templates/base/perms_summary.html:32
 #: kallithea/templates/base/perms_summary.html:57
 msgid "Edit Permission"
-msgstr ""
+msgstr "Επεξεργασία Δικαιώματος"
 
 #: kallithea/templates/base/perms_summary.html:92
 msgid "No permission defined"
-msgstr ""
+msgstr "Δεν έχει οριστεί κανένα δικαίωμα"
 
 #: kallithea/templates/base/root.html:28
 msgid "Retry"
-msgstr ""
+msgstr "Επανάληψη"
 
 #: kallithea/templates/base/root.html:29
 #: kallithea/templates/changeset/changeset_file_comment.html:65
 msgid "Submitting ..."
-msgstr ""
+msgstr "Υποβολή..."
 
 #: kallithea/templates/base/root.html:30
 msgid "Unable to post"
-msgstr ""
+msgstr "Δεν είναι δυνατή η δημοσίευση"
 
 #: kallithea/templates/base/root.html:31
 msgid "Add Another Comment"
-msgstr ""
+msgstr "Προσθήκη και άλλου Σχολίου"
 
 #: kallithea/templates/base/root.html:32
 msgid "Stop following this repository"
-msgstr ""
+msgstr "Διακοπή παρακολούθησης αυτού του αποθετηρίου"
 
 #: kallithea/templates/base/root.html:33
 msgid "Start following this repository"
-msgstr ""
+msgstr "Έναρξη παρακολούθησης αυτού του αποθετηρίου"
 
 #: kallithea/templates/base/root.html:34
 msgid "Group"
-msgstr ""
+msgstr "Ομάδα"
 
 #: kallithea/templates/base/root.html:35
 msgid "Loading ..."
-msgstr ""
+msgstr "Φόρτωση..."
 
 #: kallithea/templates/base/root.html:36
 msgid "loading ..."
-msgstr ""
+msgstr "φόρτωση ..."
 
 #: kallithea/templates/base/root.html:37
 msgid "Search truncated"
-msgstr ""
+msgstr "Περικομμένη αναζήτηση"
 
 #: kallithea/templates/base/root.html:38
 msgid "No matching files"
-msgstr ""
+msgstr "Δεν υπάρχουν αρχεία που να ταιριάζουν"
 
 #: kallithea/templates/base/root.html:39
 msgid "Open New Pull Request from {0}"
-msgstr ""
+msgstr "Άνοιγμα νέας αίτησης έλξης από {0}"
 
 #: kallithea/templates/base/root.html:40
 msgid "Open New Pull Request for {0} &rarr; {1}"
-msgstr ""
+msgstr "Άνοιγμα νέου αιτήματος έλξης για {0} &rarr; {1}"
 
 #: kallithea/templates/base/root.html:41
 msgid "Show Selected Changesets {0} &rarr; {1}"
-msgstr ""
+msgstr "Εμφάνιση Επιλεγμένων Σετ Αλλαγών {0} &rarr; {1}"
 
 #: kallithea/templates/base/root.html:42
 msgid "Selection Link"
-msgstr ""
+msgstr "Σύνδεσμος Επιλογής"
 
 #: kallithea/templates/base/root.html:43
 #: kallithea/templates/changeset/diff_block.html:7
 msgid "Collapse Diff"
-msgstr ""
+msgstr "Σύμπτυξη Διαφοράς"
 
 #: kallithea/templates/base/root.html:44
 msgid "Expand Diff"
-msgstr ""
+msgstr "Ανάπτυξη Διαφοράς"
 
 #: kallithea/templates/base/root.html:45
 msgid "No revisions"
-msgstr ""
+msgstr "Χωρίς αναθεωρήσεις"
 
 #: kallithea/templates/base/root.html:46
 msgid "Type name of user or member to grant permission"
 msgstr ""
+"Πληκτρολογήστε το όνομα του χρήστη ή του μέλους για την εκχώρηση "
+"δικαιωμάτων"
 
 #: kallithea/templates/base/root.html:47
 msgid "Failed to revoke permission"
-msgstr ""
+msgstr "Απέτυχε η ανάκληση του δικαιωμάτος"
 
 #: kallithea/templates/base/root.html:48
 msgid "Confirm to revoke permission for {0}: {1} ?"
-msgstr ""
+msgstr "Επιβεβαιώστε την ανάκληση του δικαιώματος για {0}: {1};"
 
 #: kallithea/templates/base/root.html:51
 #: kallithea/templates/compare/compare_diff.html:108
@@ -4159,47 +4338,47 @@
 
 #: kallithea/templates/base/root.html:52
 msgid "Specify changeset"
-msgstr ""
+msgstr "Καθορισμός σετ αλλαγών"
 
 #: kallithea/templates/base/root.html:53
 msgid "Click to sort ascending"
-msgstr ""
+msgstr "Κάντε κλικ για αύξουσα ταξινόμηση"
 
 #: kallithea/templates/base/root.html:54
 msgid "Click to sort descending"
-msgstr ""
+msgstr "Κάντε κλικ για φθίνουσα ταξινόμηση"
 
 #: kallithea/templates/base/root.html:55
 msgid "No records found."
-msgstr ""
+msgstr "Δεν βρέθηκαν εγγραφές."
 
 #: kallithea/templates/base/root.html:56
 msgid "Data error."
-msgstr ""
+msgstr "Σφάλμα δεδομένων."
 
 #: kallithea/templates/base/root.html:57
 msgid "Loading..."
-msgstr ""
+msgstr "Φόρτωση..."
 
 #: kallithea/templates/changelog/changelog.html:8
 #, python-format
 msgid "%s Changelog"
-msgstr ""
+msgstr "%s Αρχείο καταγραφής αλλαγών"
 
 #: kallithea/templates/changelog/changelog.html:23
 #, python-format
 msgid "showing %d out of %d revision"
 msgid_plural "showing %d out of %d revisions"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "εμφάνιση %d από %d αναθεώρηση"
+msgstr[1] "εμφάνιση %d από %d αναθεώρησεις"
 
 #: kallithea/templates/changelog/changelog.html:47
 msgid "Clear selection"
-msgstr ""
+msgstr "Καθαρισμός επιλογής"
 
 #: kallithea/templates/changelog/changelog.html:54
 msgid "Go to tip of repository"
-msgstr ""
+msgstr "Μετάβαση στην κεφαλή του αποθετηρίου"
 
 #: kallithea/templates/changelog/changelog.html:59
 #: kallithea/templates/forks/forks_data.html:16
@@ -4215,27 +4394,27 @@
 #: kallithea/templates/changelog/changelog.html:65
 #: kallithea/templates/files/files.html:29
 msgid "Branch filter:"
-msgstr ""
+msgstr "Φίλτρο κλάδου:"
 
 #: kallithea/templates/changelog/changelog.html:221
 msgid "There are no changes yet"
-msgstr ""
+msgstr "Δεν υπάρχουν αλλαγές ακόμα"
 
 #: kallithea/templates/changelog/changelog_details.html:4
 #: kallithea/templates/changeset/changeset.html:77
 msgid "Removed"
-msgstr ""
+msgstr "Αφαιρέθηκε"
 
 #: kallithea/templates/changelog/changelog_details.html:5
 #: kallithea/templates/changeset/changeset.html:78
 msgid "Changed"
-msgstr ""
+msgstr "Αλλάχτηκε"
 
 #: kallithea/templates/changelog/changelog_details.html:6
 #: kallithea/templates/changeset/changeset.html:79
 #: kallithea/templates/changeset/diff_block.html:38
 msgid "Added"
-msgstr ""
+msgstr "Προστέθηκε"
 
 #: kallithea/templates/changelog/changelog_details.html:8
 #: kallithea/templates/changelog/changelog_details.html:9
@@ -4245,19 +4424,19 @@
 #: kallithea/templates/changeset/changeset.html:83
 #, python-format
 msgid "Affected %s files"
-msgstr ""
+msgstr "Επηρεάστηκαν %s αρχεία"
 
 #: kallithea/templates/changelog/changelog_table.html:20
 msgid "First (oldest) changeset in this list"
-msgstr ""
+msgstr "Πρώτο (παλαιότερο) σετ αλλαγών σε αυτήν τη λίστα"
 
 #: kallithea/templates/changelog/changelog_table.html:22
 msgid "Last (most recent) changeset in this list"
-msgstr ""
+msgstr "Τελευταίο (πιο πρόσφατο) σετ αλλαγών σε αυτήν τη λίστα"
 
 #: kallithea/templates/changelog/changelog_table.html:24
 msgid "Position in this list of changesets"
-msgstr ""
+msgstr "Θέση σε αυτήν τη λίστα των αλλαγών"
 
 #: kallithea/templates/changelog/changelog_table.html:35
 #, python-format
@@ -4265,28 +4444,29 @@
 "Changeset status: %s by %s\n"
 "Click to open associated pull request %s"
 msgstr ""
+"Κατάσταση συνόλου αλλαγών: %s από %s\n"
+"Κάντε κλικ για να ανοίξετε το συσχετισμένο αίτημα έλξης %s"
 
 #: kallithea/templates/changelog/changelog_table.html:41
 #, python-format
 msgid "Changeset status: %s by %s"
-msgstr ""
+msgstr "Κατάσταση σετ αλλαγών: %s από %s"
 
 #: kallithea/templates/changelog/changelog_table.html:60
 msgid "Expand commit message"
-msgstr ""
+msgstr "Ανάπτυξη μηνύματος commit"
 
 #: kallithea/templates/changelog/changelog_table.html:76
-#, fuzzy, python-format
-#| msgid "%s committed on %s"
+#, python-format
 msgid "%s comments"
-msgstr "%s συνέβαλε στο %s"
+msgstr "%s σχόλια"
 
 #: kallithea/templates/changelog/changelog_table.html:80
 #: kallithea/templates/changeset/changeset.html:63
 #: kallithea/templates/changeset/changeset_range.html:84
 #, python-format
 msgid "Bookmark %s"
-msgstr ""
+msgstr "Σελιδοδείκτης %s"
 
 #: kallithea/templates/changelog/changelog_table.html:83
 #: kallithea/templates/changeset/changeset.html:67
@@ -4294,677 +4474,711 @@
 #: kallithea/templates/pullrequests/pullrequest_show.html:165
 #, python-format
 msgid "Tag %s"
-msgstr ""
+msgstr "Ετικέτα %s"
 
 #: kallithea/templates/changelog/changelog_table.html:102
 #: kallithea/templates/changeset/changeset.html:71
 #: kallithea/templates/changeset/changeset_range.html:94
 #, python-format
 msgid "Branch %s"
-msgstr ""
+msgstr "Κλάδος %s"
 
 #: kallithea/templates/changeset/changeset.html:8
 #, python-format
 msgid "%s Changeset"
-msgstr ""
+msgstr "Σετ αλλαγών %s"
 
 #: kallithea/templates/changeset/changeset.html:34
 msgid "Changeset status"
-msgstr ""
+msgstr "Κατάσταση σετ αλλαγών"
 
 #: kallithea/templates/changeset/changeset.html:43
 #: kallithea/templates/changeset/diff_block.html:64
 #: kallithea/templates/files/diff_2way.html:51
 msgid "Raw diff"
-msgstr ""
+msgstr "Ακατέργαστη διαφορά"
 
 #: kallithea/templates/changeset/changeset.html:46
 msgid "Patch diff"
-msgstr ""
+msgstr "Διαφορά κώδικα"
 
 #: kallithea/templates/changeset/changeset.html:49
 #: kallithea/templates/changeset/diff_block.html:66
 #: kallithea/templates/files/diff_2way.html:54
 msgid "Download diff"
-msgstr ""
+msgstr "Λήψη διαφοράς"
 
 #: kallithea/templates/changeset/changeset.html:59
 #: kallithea/templates/changeset/changeset_range.html:80
 msgid "Merge"
-msgstr ""
-
-#: kallithea/templates/changeset/changeset.html:96
+msgstr "Συγχώνευση"
+
+#: kallithea/templates/changeset/changeset.html:95
 msgid "Grafted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:102
+#: kallithea/templates/changeset/changeset.html:100
 msgid "Transplanted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:108
+#: kallithea/templates/changeset/changeset.html:106
 msgid "Replaced by:"
-msgstr ""
-
-#: kallithea/templates/changeset/changeset.html:122
+msgstr "Αντικαταστάθηκε από:"
+
+#: kallithea/templates/changeset/changeset.html:120
 msgid "Preceded by:"
-msgstr ""
-
-#: kallithea/templates/changeset/changeset.html:139
+msgstr "Προηγείται από:"
+
+#: kallithea/templates/changeset/changeset.html:137
 #: kallithea/templates/compare/compare_diff.html:59
 #: kallithea/templates/pullrequests/pullrequest_show.html:290
 #, python-format
 msgid "%s file changed"
 msgid_plural "%s files changed"
-msgstr[0] ""
-msgstr[1] ""
-
-#: kallithea/templates/changeset/changeset.html:141
+msgstr[0] "Άλλαξε %s αρχείο"
+msgstr[1] "Άλλαξαν %s αρχεία"
+
+#: kallithea/templates/changeset/changeset.html:139
 #: kallithea/templates/compare/compare_diff.html:61
 #: kallithea/templates/pullrequests/pullrequest_show.html:292
 #, python-format
 msgid "%s file changed with %s insertions and %s deletions"
 msgid_plural "%s files changed with %s insertions and %s deletions"
-msgstr[0] ""
-msgstr[1] ""
-
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+msgstr[0] "Άλλαξε %s αρχείο με %s εισαγωγές και %s διαγραφές"
+msgstr[1] "Άλλαξαν %s αρχεία με %s εισαγωγές και %s διαγραφές"
+
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
 #: kallithea/templates/pullrequests/pullrequest_show.html:333
 msgid "Show full diff anyway"
-msgstr ""
+msgstr "Εμφάνιση πλήρους διαφοράς ούτως ή άλλως"
 
 #: kallithea/templates/changeset/changeset_file_comment.html:20
-#, fuzzy
-#| msgid "%s committed on %s"
 msgid "comment"
-msgstr "%s συνέβαλε στο %s"
+msgstr "σχόλιο"
 
 #: kallithea/templates/changeset/changeset_file_comment.html:21
 msgid "on pull request"
-msgstr ""
+msgstr "κατόπιν αιτήματος έλξης"
 
 #: kallithea/templates/changeset/changeset_file_comment.html:22
 msgid "No title"
-msgstr ""
+msgstr "Χωρίς τίτλο"
 
 #: kallithea/templates/changeset/changeset_file_comment.html:24
 msgid "on this changeset"
-msgstr ""
+msgstr "σε αυτό το σετ αλλαγών"
 
 #: kallithea/templates/changeset/changeset_file_comment.html:30
 msgid "Delete comment?"
-msgstr ""
+msgstr "Διαγραφή σχολίου;"
 
 #: kallithea/templates/changeset/changeset_file_comment.html:38
 #: kallithea/templates/changeset/changeset_file_comment.html:71
 msgid "Status change"
-msgstr ""
+msgstr "Αλλαγή κατάστασης"
 
 #: kallithea/templates/changeset/changeset_file_comment.html:87
 msgid "Comments are in plain text. Use @username to notify another user."
 msgstr ""
+"Τα σχόλια είναι σε απλό κείμενο. Χρησιμοποιήστε @username για να "
+"ειδοποιήσετε έναν άλλο χρήστη."
 
 #: kallithea/templates/changeset/changeset_file_comment.html:93
 msgid "Set changeset status"
-msgstr ""
+msgstr "Ορισμός κατάστασης σετ αλλαγών"
 
 #: kallithea/templates/changeset/changeset_file_comment.html:95
 msgid "Vote for pull request status"
-msgstr ""
+msgstr "Ψηφοφορία για την κατάσταση του αιτήματος έλξης"
 
 #: kallithea/templates/changeset/changeset_file_comment.html:101
 #: kallithea/templates/changeset/diff_block.html:46
 msgid "No change"
-msgstr ""
+msgstr "Καμία αλλαγή"
 
 #: kallithea/templates/changeset/changeset_file_comment.html:114
-#, fuzzy
 msgid "Finish pull request"
-msgstr "Λάθος στη δημιουργία αιτήματος έλξης - pull request: %s"
+msgstr "Τερματισμός αιτήματος έλξης"
 
 #: kallithea/templates/changeset/changeset_file_comment.html:117
 msgid "Close"
-msgstr ""
+msgstr "Κλείσιμο"
 
 #: kallithea/templates/changeset/changeset_file_comment.html:129
 msgid "Comment"
-msgstr ""
+msgstr "Σχολιασμός"
 
 #: kallithea/templates/changeset/changeset_file_comment.html:137
 msgid "You need to be logged in to comment."
-msgstr ""
+msgstr "Πρέπει να είστε συνδεδεμένος για να σχολιάσετε."
 
 #: kallithea/templates/changeset/changeset_file_comment.html:137
 msgid "Login now"
-msgstr ""
+msgstr "Συνδεθείτε τώρα"
 
 #: kallithea/templates/changeset/changeset_file_comment.html:141
 msgid "Hide"
-msgstr ""
+msgstr "Απόκρυψη"
 
 #: kallithea/templates/changeset/changeset_file_comment.html:153
 #, python-format
 msgid "%d comment"
 msgid_plural "%d comments"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%d σχόλιο"
+msgstr[1] "%d σχόλια"
 
 #: kallithea/templates/changeset/changeset_file_comment.html:154
 #, python-format
 msgid "%d inline"
 msgid_plural "%d inline"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%d ενσωματωμένο"
+msgstr[1] "%d ενσωματωμένα"
 
 #: kallithea/templates/changeset/changeset_file_comment.html:155
 #, python-format
 msgid "%d general"
 msgid_plural "%d general"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%d γενικά"
+msgstr[1] "%d γενικά"
 
 #: kallithea/templates/changeset/changeset_range.html:5
 #, python-format
 msgid "%s Changesets"
-msgstr ""
+msgstr "Σετ αλλαγών του %s"
 
 #: kallithea/templates/changeset/changeset_range.html:43
 #, python-format
 msgid "Changeset status: %s"
-msgstr ""
+msgstr "Κατάσταση σετ αλλαγών: %s"
 
 #: kallithea/templates/changeset/changeset_range.html:50
 msgid "Files affected"
-msgstr ""
+msgstr "Αρχεία που επηρεάστηκαν"
 
 #: kallithea/templates/changeset/diff_block.html:30
 msgid "No file before"
-msgstr ""
+msgstr "Δεν υπάρχει αρχείο πριν"
 
 #: kallithea/templates/changeset/diff_block.html:33
 msgid "File before"
-msgstr ""
+msgstr "Αρχείο πριν"
 
 #: kallithea/templates/changeset/diff_block.html:40
-#, fuzzy
-#| msgid "Unmodified"
 msgid "Modified"
-msgstr "Mη τροποποιημένo"
+msgstr "Τροποποιημένο"
 
 #: kallithea/templates/changeset/diff_block.html:42
 msgid "Deleted"
-msgstr ""
+msgstr "Διαγράφηκε"
 
 #: kallithea/templates/changeset/diff_block.html:44
 msgid "Renamed"
-msgstr ""
+msgstr "Μετονομάστηκε"
 
 #: kallithea/templates/changeset/diff_block.html:48
-#, fuzzy, python-format
-#| msgid "Unknown revision %s"
+#, python-format
 msgid "Unknown operation: %r"
-msgstr "Άγνωστη αναθεώρηση %s"
+msgstr "Άγνωστη λειτουργία: %r"
 
 #: kallithea/templates/changeset/diff_block.html:52
-#, fuzzy
-#| msgid "No filename"
 msgid "No file after"
-msgstr "Χωρίς όνομα αρχείου"
+msgstr "Δεν υπάρχει αρχείο μετά"
 
 #: kallithea/templates/changeset/diff_block.html:55
-#, fuzzy
-#| msgid "New file type"
 msgid "File after"
-msgstr "Άγνωστος τύπος αρχειοθέτησης"
+msgstr "Αρχείο μετά"
 
 #: kallithea/templates/changeset/diff_block.html:60
 #: kallithea/templates/files/diff_2way.html:43
 msgid "Show full diff for this file"
-msgstr ""
+msgstr "Εμφάνιση πλήρους διαφοράς για αυτό το αρχείο"
 
 #: kallithea/templates/changeset/diff_block.html:62
 #: kallithea/templates/files/diff_2way.html:47
 msgid "Show full side-by-side diff for this file"
-msgstr ""
+msgstr "Εμφάνιση πλήρους διαφοράς δίπλα-δίπλα για αυτό το αρχείο"
 
 #: kallithea/templates/changeset/diff_block.html:72
 msgid "Show inline comments"
-msgstr ""
+msgstr "Εμφάνιση ενσωματωμένων σχολίων"
 
 #: kallithea/templates/compare/compare_cs.html:5
 msgid "No changesets"
-msgstr ""
+msgstr "Χωρίς σετ αλλαγών"
 
 #: kallithea/templates/compare/compare_cs.html:12
 msgid "Criss cross merge situation with multiple merge ancestors detected!"
 msgstr ""
+"Εντοπίστηκε κατάσταση διασταυρούμενης συγχώνευσης με πολλούς προγόνους "
+"συγχώνευσης!"
 
 #: kallithea/templates/compare/compare_cs.html:15
 msgid ""
 "Please merge the target branch to your branch before creating a pull "
 "request."
 msgstr ""
+"Παρακαλώ συγχωνεύστε τον κλάδο-στόχο στον κλάδο σας πριν δημιουργήσετε "
+"ένα αίτημα έλξης."
 
 #: kallithea/templates/compare/compare_cs.html:19
 msgid "Merge Ancestor"
-msgstr ""
+msgstr "Πρόγονος Συγχώνευσης"
 
 #: kallithea/templates/compare/compare_cs.html:40
 msgid "Show merge diff"
-msgstr ""
+msgstr "Εμφάνιση διαφοράς συγχώνευσης"
 
 #: kallithea/templates/compare/compare_cs.html:54
 msgid "is"
-msgstr ""
+msgstr "είναι"
 
 #: kallithea/templates/compare/compare_cs.html:55
 #, python-format
 msgid "%s changesets"
-msgstr ""
+msgstr "%s σετ αλλαγών"
 
 #: kallithea/templates/compare/compare_cs.html:56
 msgid "behind"
-msgstr ""
+msgstr "πίσω"
 
 #: kallithea/templates/compare/compare_diff.html:6
 #: kallithea/templates/compare/compare_diff.html:8
 #, python-format
 msgid "%s Compare"
-msgstr ""
+msgstr "Σύγκριση %s"
 
 #: kallithea/templates/compare/compare_diff.html:13
 #: kallithea/templates/compare/compare_diff.html:41
 msgid "Compare Revisions"
-msgstr ""
+msgstr "Σύγκριση Αναθεωρήσεων"
 
 #: kallithea/templates/compare/compare_diff.html:39
 msgid "Swap"
-msgstr ""
+msgstr "Ανταλαγή"
 
 #: kallithea/templates/compare/compare_diff.html:48
 msgid "Compare revisions, branches, bookmarks, or tags."
-msgstr ""
+msgstr "Συγκρίνετε αναθεωρήσεις, κλάδους, σελιδοδείκτες ή ετικέτες."
 
 #: kallithea/templates/compare/compare_diff.html:53
 #: kallithea/templates/pullrequests/pullrequest_show.html:278
 #, python-format
 msgid "Showing %s commit"
 msgid_plural "Showing %s commits"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "Εμφάνιση %s commit"
+msgstr[1] "Εμφάνιση %s commits"
 
 #: kallithea/templates/compare/compare_diff.html:95
 msgid "Show full diff"
-msgstr ""
+msgstr "Εμφάνιση πλήρους διαφοράς"
 
 #: kallithea/templates/data_table/_dt_elements.html:23
 msgid "Public repository"
-msgstr ""
+msgstr "Δημόσιο αποθετήριο"
 
 #: kallithea/templates/data_table/_dt_elements.html:29
 msgid "Repository creation in progress..."
-msgstr ""
-
-#: kallithea/templates/data_table/_dt_elements.html:42
-msgid "No changesets yet"
-msgstr ""
+msgstr "Η δημιουργία αποθετηρίου βρίσκεται σε εξέλιξη..."
 
 #: kallithea/templates/data_table/_dt_elements.html:48
-#: kallithea/templates/data_table/_dt_elements.html:50
+msgid "No changesets yet"
+msgstr "Δεν υπάρχουν ακόμα σετ αλλαγών"
+
+#: kallithea/templates/data_table/_dt_elements.html:54
+#: kallithea/templates/data_table/_dt_elements.html:56
 #, python-format
 msgid "Subscribe to %s rss feed"
-msgstr ""
-
-#: kallithea/templates/data_table/_dt_elements.html:56
-#: kallithea/templates/data_table/_dt_elements.html:58
+msgstr "Εγγραφή στην τροφοδοσία rss του %s"
+
+#: kallithea/templates/data_table/_dt_elements.html:62
+#: kallithea/templates/data_table/_dt_elements.html:64
 #, python-format
 msgid "Subscribe to %s atom feed"
-msgstr ""
-
-#: kallithea/templates/data_table/_dt_elements.html:76
+msgstr "Εγγραφή στην τροφοδοσία του %s atom"
+
+#: kallithea/templates/data_table/_dt_elements.html:82
 msgid "Creating"
-msgstr ""
+msgstr "Δημιουργία σε εξέλιξη"
 
 #: kallithea/templates/email_templates/changeset_comment.html:4
 #, python-format
 msgid "Mention in Comment on Changeset \"%s\""
-msgstr ""
+msgstr "Αναφορά στο Σχόλιο για το σετ αλλαγών \"%s\""
 
 #: kallithea/templates/email_templates/changeset_comment.html:4
 #, python-format
 msgid "Comment on Changeset \"%s\""
-msgstr ""
+msgstr "Σχόλιο για το σετ αλλαγών \"%s\""
 
 #: kallithea/templates/email_templates/changeset_comment.html:20
-#, fuzzy
-#| msgid "Changeset"
 msgid "Changeset on"
-msgstr "Σετ αλλαγών"
+msgstr "Σετ αλλαγών σε"
 
 #: kallithea/templates/email_templates/changeset_comment.html:23
 #: kallithea/templates/email_templates/pull_request.html:22
 #: kallithea/templates/email_templates/pull_request.html:28
 #: kallithea/templates/email_templates/pull_request_comment.html:30
 #: kallithea/templates/email_templates/pull_request_comment.html:36
-#, fuzzy
-#| msgid "Branch"
 msgid "branch"
-msgstr "Κλάδος"
+msgstr "κλάδος"
 
 #: kallithea/templates/email_templates/changeset_comment.html:29
 #: kallithea/templates/email_templates/pull_request.html:15
 #: kallithea/templates/email_templates/pull_request_comment.html:23
 msgid "by"
-msgstr ""
+msgstr "από"
+
+#: kallithea/templates/email_templates/changeset_comment.html:36
+#: kallithea/templates/email_templates/pull_request_comment.html:43
+#, fuzzy
+#| msgid "Comment"
+msgid "View Comment"
+msgstr "Σχολιασμός"
 
 #: kallithea/templates/email_templates/comment.html:27
 msgid "Status change:"
-msgstr ""
+msgstr "Αλλαγή κατάστασης:"
 
 #: kallithea/templates/email_templates/comment.html:33
+msgid "The pull request has been closed."
+msgstr "Το αίτημα έλξης έχει κλείσει."
+
+#: kallithea/templates/email_templates/default.html:4
 #, fuzzy
-#| msgid "This pull request has been closed and can not be updated."
-msgid "The pull request has been closed."
-msgstr "Αυτό το αίτημα έλξης έχει κλείσει και δεν μπορεί να ενημερωθεί."
-
-#: kallithea/templates/email_templates/password_reset.html:9
+#| msgid "Commit Message"
+msgid "Message"
+msgstr "Μήνυμα Υποβολής"
+
+#: kallithea/templates/email_templates/password_reset.html:4
+#, fuzzy
+#| msgid "Password Reset"
+msgid "Password Reset Request"
+msgstr "Επαναφορά κωδικού"
+
+#: kallithea/templates/email_templates/password_reset.html:10
 #, python-format
 msgid "Hello %s"
-msgstr ""
-
-#: kallithea/templates/email_templates/password_reset.html:16
+msgstr "Γεια σας %s"
+
+#: kallithea/templates/email_templates/password_reset.html:17
 msgid "We have received a request to reset the password for your account."
 msgstr ""
-
-#: kallithea/templates/email_templates/password_reset.html:25
+"Λάβαμε ένα αίτημα για επαναφορά του κωδικού πρόσβασης για το λογαριασμό "
+"σας."
+
+#: kallithea/templates/email_templates/password_reset.html:26
 msgid ""
 "This account is however managed outside this system and the password "
 "cannot be changed here."
 msgstr ""
-
-#: kallithea/templates/email_templates/password_reset.html:28
+"Ωστόσο, η διαχείριση αυτού του λογαριασμού γίνεται εκτός αυτού του "
+"συστήματος και ο κωδικός πρόσβασης δεν μπορεί να αλλάξει εδώ."
+
+#: kallithea/templates/email_templates/password_reset.html:29
 msgid "To set a new password, click the following link"
 msgstr ""
-
-#: kallithea/templates/email_templates/password_reset.html:33
+"Για να ορίσετε έναν νέο κωδικό πρόσβασης, κάντε κλικ στον ακόλουθο "
+"σύνδεσμο"
+
+#: kallithea/templates/email_templates/password_reset.html:34
 msgid ""
 "Should you not be able to use the link above, please type the following "
 "code into the password reset form"
 msgstr ""
-
-#: kallithea/templates/email_templates/password_reset.html:44
+"Εάν δεν μπορείτε να χρησιμοποιήσετε τον παραπάνω σύνδεσμο, πληκτρολογήστε "
+"τον ακόλουθο κώδικα στη φόρμα επαναφοράς κωδικού πρόσβασης"
+
+#: kallithea/templates/email_templates/password_reset.html:45
 msgid ""
 "If it weren't you who requested the password reset, just disregard this "
 "message."
 msgstr ""
+"Αν δεν ήσασταν εσείς που ζητήσατε την επαναφορά κωδικού πρόσβασης, απλώς "
+"αγνοήστε αυτό το μήνυμα."
 
 #: kallithea/templates/email_templates/pull_request.html:4
 #, python-format
 msgid "Mention on Pull Request %s \"%s\" by %s"
-msgstr ""
+msgstr "Αναφορά στην αίτημα έλξης %s \"%s\" από %s"
 
 #: kallithea/templates/email_templates/pull_request.html:4
 #, python-format
 msgid "Added as Reviewer of Pull Request %s \"%s\" by %s"
-msgstr ""
+msgstr "Προστεθήκατε ως αναθεωρητής του αιτήματος έλξης %s \"%s\" από %s"
 
 #: kallithea/templates/email_templates/pull_request.html:12
 #: kallithea/templates/email_templates/pull_request_comment.html:20
-#, fuzzy
-#| msgid "Finish pull request"
 msgid "Pull request"
-msgstr "Λάθος στη δημιουργία αιτήματος έλξης - pull request: %s"
+msgstr "Αίτημα έλξης"
 
 #: kallithea/templates/email_templates/pull_request.html:19
 #: kallithea/templates/email_templates/pull_request_comment.html:27
 msgid "from"
-msgstr ""
+msgstr "από"
 
 #: kallithea/templates/email_templates/pull_request.html:25
 #: kallithea/templates/email_templates/pull_request_comment.html:33
 msgid "to"
-msgstr ""
+msgstr "προς"
+
+#: kallithea/templates/email_templates/pull_request.html:85
+#, fuzzy
+#| msgid "New Pull Request"
+msgid "View Pull Request"
+msgstr "Νέο Αίτημα Έλξης"
 
 #: kallithea/templates/email_templates/pull_request_comment.html:4
-#, fuzzy, python-format
-#| msgid "Error creating pull request: %s"
+#, python-format
 msgid "Mention in Comment on Pull Request %s \"%s\""
-msgstr "Λάθος στη δημιουργία αιτήματος έλξης - pull request: %s"
+msgstr "Αναφορά στο σχόλιο για το αίτημα έλξης %s \"%s\""
 
 #: kallithea/templates/email_templates/pull_request_comment.html:4
 #, python-format
 msgid "Pull Request %s \"%s\" Closed"
-msgstr ""
+msgstr "Το αίτημα έλξης %s \"%s\" είναι Κλειστό"
 
 #: kallithea/templates/email_templates/pull_request_comment.html:4
-#, fuzzy, python-format
-#| msgid "Error creating pull request: %s"
+#, python-format
 msgid "Comment on Pull Request %s \"%s\""
-msgstr "Λάθος στη δημιουργία αιτήματος έλξης - pull request: %s"
-
-#: kallithea/templates/email_templates/registration.html:22
+msgstr "Σχόλιο στην αίτηση έλξης %s \"%s\""
+
+#: kallithea/templates/email_templates/registration.html:5
+#, fuzzy
+#| msgid "New user registration"
+msgid "New User Registration"
+msgstr "Εγγραφή νέου χρήστη"
+
+#: kallithea/templates/email_templates/registration.html:23
 msgid "Full Name"
+msgstr "Ονοματεπώνυμο"
+
+#: kallithea/templates/email_templates/registration.html:42
+msgid "View User Profile"
 msgstr ""
 
 #: kallithea/templates/files/diff_2way.html:15
 #, python-format
 msgid "%s File side-by-side diff"
-msgstr ""
+msgstr "%s Αρχείο διαφοράς δίπλα-δίπλα"
 
 #: kallithea/templates/files/diff_2way.html:19
 #: kallithea/templates/files/file_diff.html:8
 msgid "File diff"
-msgstr ""
+msgstr "Αρχείο διαφοράς"
 
 #: kallithea/templates/files/file_diff.html:4
 #, python-format
 msgid "%s File Diff"
-msgstr ""
+msgstr "%s Αρχείο διαφοράς"
 
 #: kallithea/templates/files/files.html:4
 #: kallithea/templates/files/files.html:74
 #, python-format
 msgid "%s Files"
-msgstr ""
+msgstr "%s Αρχεία"
 
 #: kallithea/templates/files/files_add.html:4
 #, python-format
 msgid "%s Files Add"
-msgstr ""
+msgstr "%s Προσθήκη Αρχείων"
 
 #: kallithea/templates/files/files_add.html:21
 #: kallithea/templates/files/files_ypjax.html:9
 #: kallithea/templates/summary/summary.html:199
 msgid "Add New File"
-msgstr ""
+msgstr "Προσθήκη Νέου Αρχείου"
 
 #: kallithea/templates/files/files_add.html:39
 #: kallithea/templates/files/files_edit.html:39
 #: kallithea/templates/files/files_ypjax.html:3
 msgid "Location"
-msgstr ""
+msgstr "Τοποθεσία"
 
 #: kallithea/templates/files/files_add.html:41
 msgid "Enter filename..."
-msgstr ""
+msgstr "Εισαγωγή ονόματος αρχείου..."
 
 #: kallithea/templates/files/files_add.html:43
 #: kallithea/templates/files/files_add.html:47
 msgid "or"
-msgstr ""
+msgstr "ή"
 
 #: kallithea/templates/files/files_add.html:43
 msgid "Upload File"
-msgstr ""
+msgstr "Ανέβασμα Αρχείου"
 
 #: kallithea/templates/files/files_add.html:47
 msgid "Create New File"
-msgstr ""
+msgstr "Δημιουργία Νέου Αρχείου"
 
 #: kallithea/templates/files/files_add.html:53
-#, fuzzy
 msgid "New file type"
-msgstr "Άγνωστος τύπος αρχειοθέτησης"
+msgstr "Νέος τύπος αρχείου"
 
 #: kallithea/templates/files/files_add.html:64
 #: kallithea/templates/files/files_delete.html:34
 #: kallithea/templates/files/files_edit.html:67
 msgid "Commit Message"
-msgstr ""
+msgstr "Μήνυμα Υποβολής"
 
 #: kallithea/templates/files/files_add.html:68
 #: kallithea/templates/files/files_delete.html:40
 #: kallithea/templates/files/files_edit.html:71
 msgid "Commit Changes"
-msgstr ""
+msgstr "Υποβολή Των Αλλαγών"
 
 #: kallithea/templates/files/files_browser.html:40
 msgid "Search File List"
-msgstr ""
+msgstr "Αναζήτηση στη Λίστα Αρχείων"
 
 #: kallithea/templates/files/files_browser.html:45
 msgid "Loading file list..."
-msgstr ""
+msgstr "Φόρτωση λίστας αρχείων..."
 
 #: kallithea/templates/files/files_browser.html:55
 #: kallithea/templates/summary/summary.html:153
 msgid "Size"
-msgstr ""
+msgstr "Μέγεθος"
 
 #: kallithea/templates/files/files_browser.html:56
 msgid "Last Revision"
-msgstr ""
+msgstr "Τελευταία Αναθεώρηση"
 
 #: kallithea/templates/files/files_browser.html:57
 msgid "Last Modified"
-msgstr ""
+msgstr "Τελευταία Τροποποίηση"
 
 #: kallithea/templates/files/files_browser.html:58
 msgid "Last Committer"
-msgstr ""
+msgstr "Τελευταίος Υποβάλλων"
 
 #: kallithea/templates/files/files_delete.html:4
 #, python-format
 msgid "%s Files Delete"
-msgstr ""
+msgstr "%s Διαγραφή Αρχείων"
 
 #: kallithea/templates/files/files_delete.html:12
 #: kallithea/templates/files/files_delete.html:30
 msgid "Delete file"
-msgstr ""
+msgstr "Διαγραφή αρχείου"
 
 #: kallithea/templates/files/files_edit.html:4
 #, python-format
 msgid "%s File Edit"
-msgstr ""
+msgstr "%s Επεξεργασία Αρχείου"
 
 #: kallithea/templates/files/files_edit.html:21
 msgid "Edit file"
-msgstr ""
+msgstr "Επεξεργασία αρχείου"
 
 #: kallithea/templates/files/files_edit.html:51
 #: kallithea/templates/files/files_source.html:28
 msgid "Show Annotation"
-msgstr ""
+msgstr "Εμφάνιση Σχολιασμού"
 
 #: kallithea/templates/files/files_edit.html:53
 #: kallithea/templates/files/files_source.html:31
 msgid "Download as Raw"
-msgstr ""
+msgstr "Λήψη ως ακατέργαστο"
 
 #: kallithea/templates/files/files_edit.html:56
 msgid "Source"
-msgstr ""
+msgstr "Πηγή"
 
 #: kallithea/templates/files/files_history_box.html:2
 #, python-format
 msgid "%s author"
 msgid_plural "%s authors"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "%s συντάκτης"
+msgstr[1] "%s συντάκτες"
 
 #: kallithea/templates/files/files_source.html:6
 msgid "Diff to Revision"
-msgstr ""
+msgstr "Διαφορά σε Αναθεώρηση"
 
 #: kallithea/templates/files/files_source.html:7
 msgid "Show at Revision"
-msgstr ""
+msgstr "Εμφάνιση στην Αναθεώρηση"
 
 #: kallithea/templates/files/files_source.html:9
 msgid "Show Full History"
-msgstr ""
+msgstr "Εμφάνιση Πλήρους Ιστορικού"
 
 #: kallithea/templates/files/files_source.html:10
 msgid "Show Authors"
-msgstr ""
+msgstr "Εμφάνιση Συντακτών"
 
 #: kallithea/templates/files/files_source.html:26
 msgid "Show Source"
-msgstr ""
+msgstr "Εμφάνιση Πηγής"
 
 #: kallithea/templates/files/files_source.html:34
 #, python-format
 msgid "Edit on Branch: %s"
-msgstr ""
+msgstr "Επεξεργασία στον κλάδο: %s"
 
 #: kallithea/templates/files/files_source.html:37
 msgid "Editing binary files not allowed"
-msgstr ""
+msgstr "Η επεξεργασία δυαδικών αρχείων δεν επιτρέπεται"
 
 #: kallithea/templates/files/files_source.html:40
 msgid "Editing files allowed only when on branch head revision"
 msgstr ""
+"Η επεξεργασία αρχείων επιτρέπεται μόνο σε αναθεώρηση επί της κεφαλής του "
+"κλάδου"
 
 #: kallithea/templates/files/files_source.html:41
 msgid "Deleting files allowed only when on branch head revision"
 msgstr ""
+"Η διαγραφή αρχείων επιτρέπεται μόνο σε αναθεώρηση επί της κεφαλής του "
+"κλάδου"
 
 #: kallithea/templates/files/files_source.html:58
 #, python-format
 msgid "Binary file (%s)"
-msgstr ""
+msgstr "Δυαδικό αρχείο (%s)"
 
 #: kallithea/templates/files/files_source.html:69
 msgid "File is too big to display."
-msgstr ""
+msgstr "Το αρχείο είναι πολύ μεγάλο για προβολή."
 
 #: kallithea/templates/files/files_source.html:71
 msgid "Show full annotation anyway."
-msgstr ""
+msgstr "Εμφάνιση πλήρους σχολιασμού ούτως ή άλλως."
 
 #: kallithea/templates/files/files_source.html:73
 msgid "Show as raw."
-msgstr ""
+msgstr "Ακατέργαστη εμφάνιση."
 
 #: kallithea/templates/files/files_ypjax.html:5
 msgid "annotation"
-msgstr ""
+msgstr "σχολιασμός"
 
 #: kallithea/templates/files/files_ypjax.html:23
 msgid "Go Back"
-msgstr ""
+msgstr "Πήγαινε Πίσω"
 
 #: kallithea/templates/files/files_ypjax.html:24
 msgid "No files at given path"
-msgstr ""
+msgstr "Δεν υπάρχουν αρχεία στη δοσμένη διαδρομή"
 
 #: kallithea/templates/followers/followers.html:5
 #, python-format
 msgid "%s Followers"
-msgstr ""
+msgstr "%s Ακόλουθοι"
 
 #: kallithea/templates/followers/followers.html:9
 #: kallithea/templates/summary/summary.html:138
 #: kallithea/templates/summary/summary.html:139
 msgid "Followers"
-msgstr ""
+msgstr "Ακόλουθοι"
 
 #: kallithea/templates/followers/followers_data.html:9
 msgid "Started following -"
-msgstr ""
+msgstr "Ξεκίνησαν να ακολουθούν -"
 
 #: kallithea/templates/forks/fork.html:5
 #, python-format
@@ -4978,14 +5192,16 @@
 #: kallithea/templates/forks/fork.html:53
 msgid "Default revision for files page, downloads, whoosh, and readme."
 msgstr ""
+"Προεπιλεγμένη αναθεώρηση για τη σελίδα αρχείων, λήψεων, whoosh, και "
+"readme."
 
 #: kallithea/templates/forks/fork.html:58
 msgid "Private"
-msgstr ""
+msgstr "Ιδιωτικό"
 
 #: kallithea/templates/forks/fork.html:66
 msgid "Copy permissions"
-msgstr ""
+msgstr "Αντιγραφή δικαιωμάτων"
 
 #: kallithea/templates/forks/fork.html:69
 msgid "Copy permissions from forked repository"
@@ -4993,11 +5209,11 @@
 
 #: kallithea/templates/forks/fork.html:75
 msgid "Update after clone"
-msgstr ""
+msgstr "Ενημέρωση μετά την κλωνοποίηση"
 
 #: kallithea/templates/forks/fork.html:78
 msgid "Checkout source after making a clone"
-msgstr ""
+msgstr "Πηγαίνετε στον κώδικα μετά την κλωνοποίηση"
 
 #: kallithea/templates/forks/fork.html:85
 msgid "Fork this Repository"
@@ -5024,377 +5240,385 @@
 
 #: kallithea/templates/journal/journal.html:22
 msgid "ATOM journal feed"
-msgstr ""
+msgstr "Ημερολόγιο τροφοδοσίας ATOM"
 
 #: kallithea/templates/journal/journal.html:23
 msgid "RSS journal feed"
-msgstr ""
+msgstr "Ημερολόγιο τροφοδοσίας RSS"
 
 #: kallithea/templates/journal/journal.html:34
 msgid "My Repositories"
-msgstr ""
+msgstr "Τα αποθετήριά μου"
 
 #: kallithea/templates/journal/journal_data.html:42
 msgid "No entries yet"
-msgstr ""
+msgstr "Δεν υπάρχουν ακόμη καταχωρήσεις"
 
 #: kallithea/templates/journal/public_journal.html:10
 msgid "ATOM public journal feed"
-msgstr ""
+msgstr "Δημόσιο ημερολόγιο τροφοδοσίας ATOM"
 
 #: kallithea/templates/journal/public_journal.html:11
 msgid "RSS public journal feed"
-msgstr ""
+msgstr "Δημόσιο ημερολόγιο τροφοδοσίας RSS"
 
 #: kallithea/templates/pullrequests/pullrequest.html:4
 #: kallithea/templates/pullrequests/pullrequest.html:8
 msgid "New Pull Request"
-msgstr ""
+msgstr "Νέο Αίτημα Έλξης"
 
 #: kallithea/templates/pullrequests/pullrequest.html:26
 #: kallithea/templates/pullrequests/pullrequest_data.html:15
 #: kallithea/templates/pullrequests/pullrequest_show.html:29
 #: kallithea/templates/pullrequests/pullrequest_show.html:52
 msgid "Title"
-msgstr ""
+msgstr "Τίτλος"
 
 #: kallithea/templates/pullrequests/pullrequest.html:28
 msgid "Summarize the changes - or leave empty"
-msgstr ""
+msgstr "Συνοψίστε τις αλλαγές - ή αφήστε το κενό"
 
 #: kallithea/templates/pullrequests/pullrequest.html:35
 #: kallithea/templates/pullrequests/pullrequest_show.html:61
 msgid "Write a short description on this pull request"
-msgstr ""
+msgstr "Γράψτε μια σύντομη περιγραφή σχετικά με αυτό το αίτημα έλξης"
 
 #: kallithea/templates/pullrequests/pullrequest.html:40
 msgid "Changeset flow"
-msgstr ""
+msgstr "Ροή σετ αλλαγών"
 
 #: kallithea/templates/pullrequests/pullrequest.html:46
 msgid "Origin repository"
-msgstr ""
+msgstr "Αποθετήριο προέλευσης"
 
 #: kallithea/templates/pullrequests/pullrequest.html:52
 #: kallithea/templates/pullrequests/pullrequest.html:68
 msgid "Revision"
-msgstr ""
+msgstr "Αναθεώρηση"
 
 #: kallithea/templates/pullrequests/pullrequest.html:62
 msgid "Destination repository"
-msgstr ""
+msgstr "Αποθετήριο προορισμού"
 
 #: kallithea/templates/pullrequests/pullrequest_data.html:6
 msgid "No entries"
-msgstr ""
+msgstr "Χωρίς καταχωρήσεις"
 
 #: kallithea/templates/pullrequests/pullrequest_data.html:14
 msgid "Vote"
-msgstr ""
+msgstr "Ψήφος"
 
 #: kallithea/templates/pullrequests/pullrequest_data.html:17
 msgid "Age"
-msgstr ""
+msgstr "Ηλικία"
 
 #: kallithea/templates/pullrequests/pullrequest_data.html:18
 msgid "From"
-msgstr ""
+msgstr "Από"
 
 #: kallithea/templates/pullrequests/pullrequest_data.html:19
 msgid "To"
-msgstr ""
+msgstr "Προς"
 
 #: kallithea/templates/pullrequests/pullrequest_data.html:28
 #, python-format
 msgid "You voted: %s"
-msgstr ""
+msgstr "Ψηφίσατε: %s"
 
 #: kallithea/templates/pullrequests/pullrequest_data.html:30
 msgid "You didn't vote"
-msgstr ""
+msgstr "Δεν ψηφίσατε"
 
 #: kallithea/templates/pullrequests/pullrequest_data.html:35
 msgid "(no title)"
-msgstr ""
+msgstr "(χωρίς τίτλο)"
 
 #: kallithea/templates/pullrequests/pullrequest_data.html:37
 #: kallithea/templates/pullrequests/pullrequest_show.html:31
 #: kallithea/templates/pullrequests/pullrequest_show.html:73
 msgid "Closed"
-msgstr ""
+msgstr "Κλειστό"
 
 #: kallithea/templates/pullrequests/pullrequest_data.html:67
 msgid "Delete Pull Request"
-msgstr ""
+msgstr "Διαγραφή Αιτήματος Έλξης"
 
 #: kallithea/templates/pullrequests/pullrequest_data.html:68
 msgid "Confirm to delete this pull request"
-msgstr ""
+msgstr "Επιβεβαίωση διαγραφής αυτού του αιτήματος έλξης"
 
 #: kallithea/templates/pullrequests/pullrequest_data.html:70
 #, python-format
 msgid "Confirm again to delete this pull request with %s comments"
 msgstr ""
+"Επιβεβαίωση ξανά για τη διαγραφή αυτού του αιτήματος έλξης με %s σχόλια"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:6
 #, python-format
 msgid "%s Pull Request %s"
-msgstr ""
+msgstr "%s Αίτημα Έλξης %s"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:10
 #, python-format
 msgid "Pull request %s from %s#%s"
-msgstr ""
+msgstr "Αίτημα έλξης %s από %s#%s"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:54
 msgid "Summarize the changes"
-msgstr ""
+msgstr "Σύνοψη των αλλαγών"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:67
 msgid "Voting Result"
-msgstr ""
+msgstr "Αποτέλεσμα Ψηφοφορίας"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:70
 #: kallithea/templates/pullrequests/pullrequest_show.html:71
 msgid "Pull request status calculated from votes"
-msgstr ""
+msgstr "Η κατάσταση του αιτήματος έλξης υπολογισμένο από τις ψήφους"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:81
 msgid "Origin"
-msgstr ""
+msgstr "Προέλευση"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:86
 msgid "on"
-msgstr ""
+msgstr "επί"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:92
 msgid "Target"
-msgstr ""
+msgstr "Στόχος"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:95
 msgid ""
 "This is just a range of changesets and doesn't have a target or a real "
 "merge ancestor."
 msgstr ""
+"Αυτό είναι μόνο μια σειρά από σετ αλλαγών και δεν έχει προορισμό ή "
+"πραγματικό πρόγονο συγχώνευσης."
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:103
 msgid "Pull changes"
-msgstr ""
+msgstr "Τράβηγμα αλλαγών"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:136
 msgid "Next iteration"
-msgstr ""
+msgstr "Επόμενη επανάληψη"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:153
 msgid "Current revision - no change"
-msgstr ""
+msgstr "Τρέχουσα αναθεώρηση - καμία αλλαγή"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:177
 msgid ""
 "Pull request iterations do not change content once created. Select a "
 "revision to create a new iteration."
 msgstr ""
+"Οι επαναλήψεις αιτήσεων έλξης δεν αλλάζουν περιεχόμενο μετά τη δημιουργία "
+"τους. Επιλέξτε μια αναθεώρηση για να δημιουργήσετε μια νέα επανάληψη."
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:187
 msgid "Save Changes"
-msgstr ""
+msgstr "Αποθήκευση Αλλαγών"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:188
 msgid "Create New Iteration with Changes"
-msgstr ""
+msgstr "Δημιουργία Νέας Επανάληψης με τις Αλλαγές"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:189
 msgid "Cancel Changes"
-msgstr ""
+msgstr "Ακύρωση Αλλαγών"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:197
 msgid "Reviewers"
-msgstr ""
+msgstr "Επιθεωρητές"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:223
 msgid "Remove reviewer"
-msgstr ""
+msgstr "Κατάργηση επιθεωρητή"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:234
 msgid "Type name of reviewer to add"
-msgstr ""
+msgstr "Πληκτρολογήστε το όνομα του επιθεωρητή για προσθήκη"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:240
 msgid "Potential Reviewers"
-msgstr ""
+msgstr "Πιθανοί Επιθεωρητές"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:243
 msgid "Click to add the repository owner as reviewer:"
 msgstr ""
+"Κάντε κλικ για να προσθέσετε τον κάτοχο του αποθετηρίου ως επιθεωρητή:"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:268
 msgid "Pull Request Content"
-msgstr ""
+msgstr "Περιεχόμενο Αιτήματος Έλξης"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:283
 msgid "Common ancestor"
-msgstr ""
+msgstr "Κοινός πρόγονος"
 
 #: kallithea/templates/pullrequests/pullrequest_show_all.html:6
 #, python-format
 msgid "%s Pull Requests"
-msgstr ""
+msgstr "%s Αιτήματα Έλξης"
 
 #: kallithea/templates/pullrequests/pullrequest_show_all.html:11
 #, python-format
 msgid "Pull Requests from '%s'"
-msgstr ""
+msgstr "Αιτήματα Έλξης από '%s'"
 
 #: kallithea/templates/pullrequests/pullrequest_show_all.html:13
 #, python-format
 msgid "Pull Requests to '%s'"
-msgstr ""
+msgstr "Αιτήματα Έλξης προς '%s'"
 
 #: kallithea/templates/pullrequests/pullrequest_show_all.html:31
 msgid "Open New Pull Request"
-msgstr ""
+msgstr "Άνοιγμα Νέου Αιτήματος Έλξης"
 
 #: kallithea/templates/pullrequests/pullrequest_show_all.html:34
 #, python-format
 msgid "Show Pull Requests to %s"
-msgstr ""
+msgstr "Εμφάνιση Αιτημάτων Έλξης προς %s"
 
 #: kallithea/templates/pullrequests/pullrequest_show_all.html:36
 #, python-format
 msgid "Show Pull Requests from '%s'"
-msgstr ""
+msgstr "Εμφάνιση Αιτημάτων Έλξης από '%s'"
 
 #: kallithea/templates/pullrequests/pullrequest_show_all.html:44
 #: kallithea/templates/pullrequests/pullrequest_show_my.html:28
 msgid "Hide closed pull requests (only show open pull requests)"
 msgstr ""
+"Απόκρυψη κλειστών αιτημάτων έλξης (εμφάνιση μόνο ανοικτών αιτημάτων έλξης)"
 
 #: kallithea/templates/pullrequests/pullrequest_show_all.html:46
 #: kallithea/templates/pullrequests/pullrequest_show_my.html:30
 msgid "Show closed pull requests (in addition to open pull requests)"
 msgstr ""
+"Εμφάνιση κλειστών αιτημάτων έλξης (εκτός από τα ανοιχτά αιτήματα έλξης)"
 
 #: kallithea/templates/pullrequests/pullrequest_show_my.html:34
 msgid "Pull Requests Created by Me"
-msgstr ""
+msgstr "Αιτήματα Έλξης που Δημιουργήθηκαν από Εμένα"
 
 #: kallithea/templates/pullrequests/pullrequest_show_my.html:37
 msgid "Pull Requests Needing My Review"
-msgstr ""
+msgstr "Αιτήματα Έλξης που Χρειάζονται την Επιθεώρησή μου"
 
 #: kallithea/templates/pullrequests/pullrequest_show_my.html:40
 msgid "Pull Requests I Participate In"
-msgstr ""
+msgstr "Αιτήματα Έλξης που Συμμετέχω"
 
 #: kallithea/templates/search/search.html:6
 #, python-format
 msgid "%s Search"
-msgstr ""
+msgstr "%s Αναζήτηση"
 
 #: kallithea/templates/search/search.html:8
 #: kallithea/templates/search/search.html:16
 msgid "Search in All Repositories"
-msgstr ""
+msgstr "Αναζήτηση σε Όλα τα Αποθετήρια"
 
 #: kallithea/templates/search/search.html:47
 msgid "Search term"
-msgstr ""
+msgstr "Όρος αναζήτησης"
 
 #: kallithea/templates/search/search.html:54
 msgid "Search in"
-msgstr ""
+msgstr "Αναζήτηση σε"
 
 #: kallithea/templates/search/search.html:56
 msgid "File contents"
-msgstr ""
+msgstr "Περιεχόμενα αρχείου"
 
 #: kallithea/templates/search/search.html:57
 msgid "Commit messages"
-msgstr ""
+msgstr "Μηνύματα commit"
 
 #: kallithea/templates/search/search.html:58
 msgid "File names"
-msgstr ""
+msgstr "Ονόματα αρχείων"
 
 #: kallithea/templates/search/search_commit.html:30
 #: kallithea/templates/search/search_content.html:18
 #: kallithea/templates/search/search_path.html:15
 msgid "Permission denied"
-msgstr ""
+msgstr "Άρνηση δικαιώματος"
 
 #: kallithea/templates/summary/statistics.html:4
 #, python-format
 msgid "%s Statistics"
-msgstr ""
+msgstr "%s Στατιστικά"
 
 #: kallithea/templates/summary/statistics.html:16
 #: kallithea/templates/summary/summary.html:27
 #, python-format
 msgid "%s ATOM feed"
-msgstr ""
+msgstr "%s Τροφοδοσία ATOM"
 
 #: kallithea/templates/summary/statistics.html:17
 #: kallithea/templates/summary/summary.html:28
 #, python-format
 msgid "%s RSS feed"
-msgstr ""
+msgstr "%s Τροφοδοσία RSS"
 
 #: kallithea/templates/summary/statistics.html:35
 #: kallithea/templates/summary/summary.html:99
 #: kallithea/templates/summary/summary.html:113
 msgid "Enable"
-msgstr ""
+msgstr "Ενεργοποίηση"
 
 #: kallithea/templates/summary/statistics.html:38
 msgid "Stats gathered: "
-msgstr ""
-
-#: kallithea/templates/summary/statistics.html:87
-#: kallithea/templates/summary/summary.html:371
+msgstr "Στατιστικά που συγκεντρώθηκαν: "
+
+#: kallithea/templates/summary/statistics.html:85
+#: kallithea/templates/summary/summary.html:369
 msgid "files"
-msgstr ""
-
-#: kallithea/templates/summary/statistics.html:111
-#: kallithea/templates/summary/summary.html:401
+msgstr "αρχεία"
+
+#: kallithea/templates/summary/statistics.html:109
+#: kallithea/templates/summary/summary.html:399
 msgid "Show more"
-msgstr ""
-
-#: kallithea/templates/summary/statistics.html:405
+msgstr "Εμφάνιση περισσότερων"
+
+#: kallithea/templates/summary/statistics.html:395
 msgid "commits"
-msgstr ""
-
-#: kallithea/templates/summary/statistics.html:406
+msgstr "commits"
+
+#: kallithea/templates/summary/statistics.html:396
 msgid "files added"
-msgstr ""
-
-#: kallithea/templates/summary/statistics.html:407
+msgstr "αρχεία που προστέθηκαν"
+
+#: kallithea/templates/summary/statistics.html:397
 msgid "files changed"
-msgstr ""
-
-#: kallithea/templates/summary/statistics.html:408
+msgstr "αρχεία που άλλαξαν"
+
+#: kallithea/templates/summary/statistics.html:398
 msgid "files removed"
-msgstr ""
-
-#: kallithea/templates/summary/statistics.html:410
+msgstr "αρχεία που αφαιρέθηκαν"
+
+#: kallithea/templates/summary/statistics.html:400
 msgid "commit"
-msgstr ""
-
-#: kallithea/templates/summary/statistics.html:411
+msgstr "commit"
+
+#: kallithea/templates/summary/statistics.html:401
 msgid "file added"
-msgstr ""
-
-#: kallithea/templates/summary/statistics.html:412
+msgstr "αρχείο προστέθηκε"
+
+#: kallithea/templates/summary/statistics.html:402
 msgid "file changed"
-msgstr ""
-
-#: kallithea/templates/summary/statistics.html:413
+msgstr "αρχείο άλλαξε"
+
+#: kallithea/templates/summary/statistics.html:403
 msgid "file removed"
-msgstr ""
+msgstr "αρχείο αφαιρέθηκε"
 
 #: kallithea/templates/summary/summary.html:5
 #, python-format
 msgid "%s Summary"
-msgstr ""
+msgstr "%s Σύνοψη"
 
 #: kallithea/templates/summary/summary.html:13
 msgid "Fork of"
@@ -5402,91 +5626,103 @@
 
 #: kallithea/templates/summary/summary.html:18
 msgid "Clone from"
-msgstr ""
+msgstr "Κλώνος από"
 
 #: kallithea/templates/summary/summary.html:54
 msgid "Clone URL"
-msgstr ""
+msgstr "Clone URL"
 
 #: kallithea/templates/summary/summary.html:63
 msgid "Use ID"
-msgstr ""
+msgstr "Χρήση ID"
 
 #: kallithea/templates/summary/summary.html:65
 #: kallithea/templates/summary/summary.html:73
 msgid "Use SSH"
-msgstr ""
+msgstr "Χρήση SSH"
 
 #: kallithea/templates/summary/summary.html:71
 msgid "Use Name"
-msgstr ""
+msgstr "Χρήση Ονόματος"
 
 #: kallithea/templates/summary/summary.html:80
 msgid "Use HTTP"
-msgstr ""
+msgstr "Χρήση HTTP"
 
 #: kallithea/templates/summary/summary.html:92
 msgid "Trending files"
-msgstr ""
+msgstr "Δημοφιλή αρχεία"
 
 #: kallithea/templates/summary/summary.html:106
 msgid "Download"
-msgstr ""
+msgstr "Λήψη"
 
 #: kallithea/templates/summary/summary.html:109
 msgid "There are no downloads yet"
-msgstr ""
+msgstr "Δεν υπάρχουν λήψεις ακόμα"
 
 #: kallithea/templates/summary/summary.html:111
 msgid "Downloads are disabled for this repository"
-msgstr ""
+msgstr "Οι λήψεις είναι απενεργοποιημένες για αυτό το αποθετήριο"
 
 #: kallithea/templates/summary/summary.html:117
 msgid "Download as zip"
-msgstr ""
+msgstr "Λήψη ως zip"
 
 #: kallithea/templates/summary/summary.html:121
 msgid "Check this to download archive with subrepos"
-msgstr ""
+msgstr "Επιλέξτε αυτό για τη λήψη του αρχείου με τα υπο-αποθετήρια"
 
 #: kallithea/templates/summary/summary.html:123
 msgid "With subrepos"
-msgstr ""
+msgstr "Με υπο-αποθετήρια"
 
 #: kallithea/templates/summary/summary.html:161
 #: kallithea/templates/summary/summary.html:163
 msgid "Feed"
-msgstr ""
+msgstr "Ροή"
 
 #: kallithea/templates/summary/summary.html:183
 msgid "Latest Changes"
-msgstr ""
+msgstr "Τελευταίες Αλλαγές"
 
 #: kallithea/templates/summary/summary.html:185
 msgid "Quick Start"
-msgstr ""
+msgstr "Γρήγορη Εκκίνηση"
 
 #: kallithea/templates/summary/summary.html:196
 msgid "Add or upload files directly via Kallithea"
-msgstr ""
+msgstr "Προσθέστε ή ανεβάστε αρχεία απευθείας μέσω Καλλιθέας"
 
 #: kallithea/templates/summary/summary.html:204
 msgid "Push new repository"
-msgstr ""
+msgstr "Ώθηση νέου αποθετηρίου"
 
 #: kallithea/templates/summary/summary.html:212
 msgid "Existing repository?"
-msgstr ""
+msgstr "Υπάρχον αποθετήριο;"
 
 #: kallithea/templates/summary/summary.html:230
 #, python-format
 msgid "Readme file from revision %s:%s"
-msgstr ""
+msgstr "Αρχείο Readme από την αναθεώρηση %s:%s"
 
 #: kallithea/templates/summary/summary.html:315
 #, python-format
 msgid "Download %s as %s"
-msgstr ""
+msgstr "Λήψη %s ως %s"
+
+#~ msgid "Cache invalidation successful"
+#~ msgstr "Η ακύρωση της cache ήταν επιτυχής"
+
+#~ msgid "An error occurred during cache invalidation"
+#~ msgstr "Παρουσιάστηκε ένα σφάλμα κατά τη διάρκεια ακύρωσης της cache"
+
+#~ msgid "Caches"
+#~ msgstr "Προσωρινές Αποθήκες"
+
+#~ msgid "Prefix"
+#~ msgstr "Πρόθεμα"
 
 #~ msgid "This repository has been locked by %s on %s"
 #~ msgstr "Το αποθετήριο κλειδώθηκε από %s την %s"
Binary file kallithea/i18n/en/LC_MESSAGES/kallithea.mo has changed
--- a/kallithea/i18n/es/LC_MESSAGES/kallithea.po	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/i18n/es/LC_MESSAGES/kallithea.po	Mon May 04 19:24:04 2020 +0200
@@ -4,7 +4,7 @@
 msgstr ""
 "Project-Id-Version: Kallithea 0.3\n"
 "Report-Msgid-Bugs-To: translations@kallithea-scm.org\n"
-"POT-Creation-Date: 2020-01-05 04:10+0100\n"
+"POT-Creation-Date: 2020-05-04 19:12+0200\n"
 "PO-Revision-Date: 2018-04-18 11:43+0000\n"
 "Last-Translator: Jesús Sánchez <jsanchezfdz95@gmail.com>\n"
 "Language-Team: Spanish <https://hosted.weblate.org/projects/kallithea/"
@@ -17,14 +17,14 @@
 "X-Generator: Weblate 3.0-dev\n"
 
 #: kallithea/controllers/changelog.py:67
-#: kallithea/controllers/pullrequests.py:250 kallithea/lib/base.py:602
+#: kallithea/controllers/pullrequests.py:247 kallithea/lib/base.py:602
 msgid "There are no changesets yet"
 msgstr "Aún no hay cambios"
 
 #: kallithea/controllers/admin/permissions.py:64
 #: kallithea/controllers/admin/permissions.py:68
 #: kallithea/controllers/admin/permissions.py:72
-#: kallithea/controllers/changelog.py:137
+#: kallithea/controllers/changelog.py:136
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:7
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:88
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:7
@@ -33,38 +33,38 @@
 msgid "None"
 msgstr "Ninguno"
 
-#: kallithea/controllers/changelog.py:140 kallithea/controllers/files.py:189
+#: kallithea/controllers/changelog.py:139 kallithea/controllers/files.py:189
 msgid "(closed)"
 msgstr "(cerrado)"
 
-#: kallithea/controllers/changeset.py:81
+#: kallithea/controllers/changeset.py:82
 msgid "Show whitespace"
 msgstr "Mostrar espacios en blanco"
 
-#: kallithea/controllers/changeset.py:88
-#: kallithea/controllers/changeset.py:95
+#: kallithea/controllers/changeset.py:89
+#: kallithea/controllers/changeset.py:96
 #: kallithea/templates/files/diff_2way.html:55
 msgid "Ignore whitespace"
 msgstr "Ignorar espacios en blanco"
 
-#: kallithea/controllers/changeset.py:161
+#: kallithea/controllers/changeset.py:162
 #, python-format
 msgid "Increase diff context to %(num)s lines"
 msgstr "Aumentar el contexto del diff a %(num)s lineas"
 
-#: kallithea/controllers/changeset.py:201
+#: kallithea/controllers/changeset.py:202
 #, fuzzy
 #| msgid "No permission to change pull request status"
 msgid "No permission to change status"
 msgstr "No tene permiso para cambiar el estado de la petición pull"
 
-#: kallithea/controllers/changeset.py:212
+#: kallithea/controllers/changeset.py:213
 #, python-format
 msgid "Successfully deleted pull request %s"
 msgstr "Petición de pull %s eliminada correctamente"
 
 #: kallithea/controllers/changeset.py:319 kallithea/controllers/files.py:89
-#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:700
+#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:697
 msgid "Such revision does not exist for this repository"
 msgstr "La revisión no existe en este repositorio"
 
@@ -77,50 +77,50 @@
 msgid "Cannot compare repositories of different types"
 msgstr "No se pueden comparar repositorios de diferentes tipos"
 
-#: kallithea/controllers/compare.py:246
+#: kallithea/controllers/compare.py:247
 msgid "Cannot show empty diff"
 msgstr "No se puede mostrar diff vacio"
 
-#: kallithea/controllers/compare.py:248
+#: kallithea/controllers/compare.py:249
 msgid "No ancestor found for merge diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:252
+#: kallithea/controllers/compare.py:253
 msgid "Multiple merge ancestors found for merge compare"
 msgstr ""
 
-#: kallithea/controllers/compare.py:268
+#: kallithea/controllers/compare.py:269
 msgid "Cannot compare repositories without using common ancestor"
 msgstr "No se pueden comparar repositorios sin usar un ancestro común"
 
-#: kallithea/controllers/error.py:71
+#: kallithea/controllers/error.py:67
 msgid "No response"
 msgstr "No hay respuesta"
 
-#: kallithea/controllers/error.py:72
+#: kallithea/controllers/error.py:68
 msgid "Unknown error"
 msgstr "Error desconocido"
 
-#: kallithea/controllers/error.py:85
+#: kallithea/controllers/error.py:81
 msgid ""
 "The request could not be understood by the server due to malformed syntax."
 msgstr ""
 "La petición no ha podido ser atendida por el servidor debido un error de "
 "sintaxis."
 
-#: kallithea/controllers/error.py:88
+#: kallithea/controllers/error.py:84
 msgid "Unauthorized access to resource"
 msgstr "Acceso no autorizado al recurso"
 
-#: kallithea/controllers/error.py:90
+#: kallithea/controllers/error.py:86
 msgid "You don't have permission to view this page"
 msgstr "No tiene permiso para ver esta página"
 
-#: kallithea/controllers/error.py:92
+#: kallithea/controllers/error.py:88
 msgid "The resource could not be found"
 msgstr "No se ha encontrado el recurso"
 
-#: kallithea/controllers/error.py:94
+#: kallithea/controllers/error.py:90
 msgid ""
 "The server encountered an unexpected condition which prevented it from "
 "fulfilling the request."
@@ -128,14 +128,14 @@
 "La petición no se ha podido completar debido a que el servidor encontró "
 "un problema inesperado."
 
-#: kallithea/controllers/feed.py:63
+#: kallithea/controllers/feed.py:59
 #, python-format
 msgid "%s committed on %s"
 msgstr "%s anotó en %s"
 
-#: kallithea/controllers/feed.py:88
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/controllers/feed.py:84
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/compare/compare_diff.html:95
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
@@ -143,12 +143,12 @@
 msgid "Changeset was too big and was cut off..."
 msgstr "El cambio era demasiado grande y se redució..."
 
-#: kallithea/controllers/feed.py:111 kallithea/controllers/feed.py:140
+#: kallithea/controllers/feed.py:107
 #, python-format
 msgid "%s %s feed"
 msgstr "%s%s canal"
 
-#: kallithea/controllers/feed.py:113 kallithea/controllers/feed.py:142
+#: kallithea/controllers/feed.py:109
 #, fuzzy, python-format
 msgid "Changes on %s repository"
 msgstr "Cambios en %s repositorio"
@@ -168,106 +168,106 @@
 msgid "%s at %s"
 msgstr "%s en %s"
 
-#: kallithea/controllers/files.py:296
+#: kallithea/controllers/files.py:295
 msgid "You can only delete files with revision being a valid branch"
 msgstr ""
 "Sólo puede borrar archivos si la revisión pertenece a una rama válida"
 
-#: kallithea/controllers/files.py:307
+#: kallithea/controllers/files.py:306
 #, python-format
 msgid "Deleted file %s via Kallithea"
 msgstr "Archivo %s eliminado mediante Kallithea"
 
-#: kallithea/controllers/files.py:331
+#: kallithea/controllers/files.py:330
 #, python-format
 msgid "Successfully deleted file %s"
 msgstr "El archivo %s se eliminó correctamente"
 
-#: kallithea/controllers/files.py:335 kallithea/controllers/files.py:394
-#: kallithea/controllers/files.py:469
+#: kallithea/controllers/files.py:334 kallithea/controllers/files.py:392
+#: kallithea/controllers/files.py:467
 msgid "Error occurred during commit"
 msgstr "Ocurrió un error al anotar los cambios"
 
-#: kallithea/controllers/files.py:350
+#: kallithea/controllers/files.py:349
 msgid "You can only edit files with revision being a valid branch"
 msgstr ""
 "Sólo puede editar archivos si la revisión pertenece a una rama válida"
 
-#: kallithea/controllers/files.py:364
+#: kallithea/controllers/files.py:363
 #, python-format
 msgid "Edited file %s via Kallithea"
 msgstr "Archivo %s editado mediante Kallithea"
 
-#: kallithea/controllers/files.py:380
+#: kallithea/controllers/files.py:378
 msgid "No changes"
 msgstr "No hay cambios"
 
-#: kallithea/controllers/files.py:390 kallithea/controllers/files.py:458
+#: kallithea/controllers/files.py:388 kallithea/controllers/files.py:456
 #, python-format
 msgid "Successfully committed to %s"
 msgstr "Anotado correctamente a %s"
 
-#: kallithea/controllers/files.py:409
+#: kallithea/controllers/files.py:407
 msgid "Added file via Kallithea"
 msgstr "Archivo añadido mediante Kallithea"
 
-#: kallithea/controllers/files.py:430
+#: kallithea/controllers/files.py:428
 msgid "No content"
 msgstr "Sin contenido"
 
-#: kallithea/controllers/files.py:434
+#: kallithea/controllers/files.py:432
 msgid "No filename"
 msgstr "Sin nombre de archivo"
 
-#: kallithea/controllers/files.py:461
+#: kallithea/controllers/files.py:459
 #, fuzzy
 msgid "Location must be relative path and must not contain .. in path"
 msgstr "La ruta debe ser relativa y no debe contener .. en la ruta"
 
-#: kallithea/controllers/files.py:493
+#: kallithea/controllers/files.py:491
 msgid "Downloads disabled"
 msgstr "Descargas deshabilitadas"
 
-#: kallithea/controllers/files.py:504
+#: kallithea/controllers/files.py:502
 #, python-format
 msgid "Unknown revision %s"
 msgstr "Revisión desconocida %s"
 
-#: kallithea/controllers/files.py:506
+#: kallithea/controllers/files.py:504
 msgid "Empty repository"
 msgstr "Repositorio vacío"
 
-#: kallithea/controllers/files.py:508
+#: kallithea/controllers/files.py:506
 msgid "Unknown archive type"
 msgstr "Tipo de archivo desconocido"
 
-#: kallithea/controllers/files.py:729
+#: kallithea/controllers/files.py:726
 #: kallithea/templates/changeset/changeset_range.html:9
 #: kallithea/templates/email_templates/pull_request.html:64
 #: kallithea/templates/pullrequests/pullrequest.html:84
 msgid "Changesets"
 msgstr "Cambios"
 
-#: kallithea/controllers/files.py:730
-#: kallithea/controllers/pullrequests.py:182 kallithea/model/scm.py:676
+#: kallithea/controllers/files.py:727
+#: kallithea/controllers/pullrequests.py:174 kallithea/model/scm.py:663
 msgid "Branches"
 msgstr "Ramas"
 
-#: kallithea/controllers/files.py:731
-#: kallithea/controllers/pullrequests.py:183 kallithea/model/scm.py:687
+#: kallithea/controllers/files.py:728
+#: kallithea/controllers/pullrequests.py:175 kallithea/model/scm.py:674
 msgid "Tags"
 msgstr "Etiquetas"
 
-#: kallithea/controllers/forks.py:174
+#: kallithea/controllers/forks.py:175
 #, python-format
 msgid "An error occurred during repository forking %s"
 msgstr "Ocurrió un error mientras se bifurcaba el repositorio %s"
 
-#: kallithea/controllers/home.py:79
+#: kallithea/controllers/home.py:77
 msgid "Groups"
 msgstr "Grupos"
 
-#: kallithea/controllers/home.py:89
+#: kallithea/controllers/home.py:87
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:90
 #: kallithea/templates/admin/repos/repo_add.html:12
 #: kallithea/templates/admin/repos/repo_add.html:16
@@ -279,48 +279,52 @@
 msgid "Repositories"
 msgstr "Repositorios"
 
-#: kallithea/controllers/home.py:122
+#: kallithea/controllers/home.py:119
 #: kallithea/templates/files/files_add.html:32
 #: kallithea/templates/files/files_delete.html:23
 #: kallithea/templates/files/files_edit.html:32
 msgid "Branch"
 msgstr "Rama"
 
-#: kallithea/controllers/home.py:128
+#: kallithea/controllers/home.py:125
 msgid "Closed Branches"
 msgstr "Ramas cerradas"
 
-#: kallithea/controllers/home.py:134
+#: kallithea/controllers/home.py:131
 msgid "Tag"
 msgstr "Etiqueta"
 
-#: kallithea/controllers/home.py:140
+#: kallithea/controllers/home.py:137
 msgid "Bookmark"
 msgstr "Marcador"
 
-#: kallithea/controllers/journal.py:112 kallithea/controllers/journal.py:154
+#: kallithea/controllers/journal.py:146 kallithea/controllers/journal.py:157
 #: kallithea/templates/journal/public_journal.html:4
 #: kallithea/templates/journal/public_journal.html:18
 msgid "Public Journal"
 msgstr "Registro público"
 
-#: kallithea/controllers/journal.py:116 kallithea/controllers/journal.py:158
+#: kallithea/controllers/journal.py:150 kallithea/controllers/journal.py:161
 #: kallithea/templates/base/base.html:290
 #: kallithea/templates/journal/journal.html:5
 #: kallithea/templates/journal/journal.html:13
 msgid "Journal"
 msgstr "Registro"
 
-#: kallithea/controllers/login.py:140 kallithea/controllers/login.py:185
+#: kallithea/controllers/login.py:109
+msgid "Authentication failed."
+msgstr ""
+
+#: kallithea/controllers/login.py:142 kallithea/controllers/login.py:187
 msgid "Bad captcha"
 msgstr "CAPTCHA erróneo"
 
-#: kallithea/controllers/login.py:146
+#: kallithea/controllers/login.py:148
 #, python-format
 msgid "You have successfully registered with %s"
 msgstr "El registro en %s se ha efectuado correctamente"
 
-#: kallithea/controllers/login.py:190
+#: kallithea/controllers/login.py:192
 msgid "A password reset confirmation code has been sent"
 msgstr "Se ha enviado una confirmación de restauración de contraseña"
 
@@ -333,236 +337,236 @@
 msgid "Successfully updated password"
 msgstr "Contraseña actualizada correctamente"
 
-#: kallithea/controllers/pullrequests.py:68
+#: kallithea/controllers/pullrequests.py:67
 #, python-format
 msgid "Invalid reviewer \"%s\" specified"
 msgstr "El validador \"%s\" no es correcto"
 
-#: kallithea/controllers/pullrequests.py:131
+#: kallithea/controllers/pullrequests.py:123
 #, python-format
 msgid "%s (closed)"
 msgstr "%s (cerrado)"
 
-#: kallithea/controllers/pullrequests.py:158
+#: kallithea/controllers/pullrequests.py:150
 #: kallithea/templates/changeset/changeset.html:12
 msgid "Changeset"
 msgstr "Cambio"
 
-#: kallithea/controllers/pullrequests.py:179
+#: kallithea/controllers/pullrequests.py:171
 msgid "Special"
 msgstr "Especial"
 
-#: kallithea/controllers/pullrequests.py:180
+#: kallithea/controllers/pullrequests.py:172
 #, fuzzy
 msgid "Peer branches"
 msgstr "Ramas de los pares"
 
-#: kallithea/controllers/pullrequests.py:181 kallithea/model/scm.py:682
+#: kallithea/controllers/pullrequests.py:173 kallithea/model/scm.py:669
 msgid "Bookmarks"
 msgstr "Marcadores"
 
-#: kallithea/controllers/pullrequests.py:318
+#: kallithea/controllers/pullrequests.py:315
 #, python-format
 msgid "Error creating pull request: %s"
 msgstr "Error al crear la petición de pull: %s"
 
-#: kallithea/controllers/pullrequests.py:345
-#: kallithea/controllers/pullrequests.py:368
+#: kallithea/controllers/pullrequests.py:342
+#: kallithea/controllers/pullrequests.py:365
 msgid "Error occurred while creating pull request"
 msgstr "Ocurrió un error al crear la petición de pull"
 
-#: kallithea/controllers/pullrequests.py:350
+#: kallithea/controllers/pullrequests.py:347
 msgid "Successfully opened new pull request"
 msgstr "La petición de pull se ha creado correctamente"
 
-#: kallithea/controllers/pullrequests.py:373
+#: kallithea/controllers/pullrequests.py:370
 #, fuzzy
 #| msgid "Pull request update created"
 msgid "New pull request iteration created"
 msgstr "Actualización de la petición pull creada"
 
-#: kallithea/controllers/pullrequests.py:401
+#: kallithea/controllers/pullrequests.py:398
 #, python-format
 msgid "Meanwhile, the following reviewers have been added: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:405
+#: kallithea/controllers/pullrequests.py:402
 #, python-format
 msgid "Meanwhile, the following reviewers have been removed: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:421
-#: kallithea/model/pull_request.py:232
+#: kallithea/controllers/pullrequests.py:418
+#: kallithea/model/pull_request.py:230
 msgid "No description"
 msgstr "No hay descripción"
 
-#: kallithea/controllers/pullrequests.py:430
+#: kallithea/controllers/pullrequests.py:427
 msgid "Pull request updated"
 msgstr "Petición pull actualizada"
 
-#: kallithea/controllers/pullrequests.py:443
+#: kallithea/controllers/pullrequests.py:440
 msgid "Successfully deleted pull request"
 msgstr "Petición pull eliminada correctamente"
 
-#: kallithea/controllers/pullrequests.py:479
+#: kallithea/controllers/pullrequests.py:476
 #, python-format
 msgid "Revision %s not found in %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:506
+#: kallithea/controllers/pullrequests.py:504
 #, fuzzy, python-format
 #| msgid "No changesets found for updating this pull request."
 msgid "Error: changesets not found when displaying pull request from %s."
 msgstr "No se encontraron cambios para actualizar la petición pull."
 
-#: kallithea/controllers/pullrequests.py:520
+#: kallithea/controllers/pullrequests.py:518
 #, python-format
 msgid "This pull request has already been merged to %s."
 msgstr "La petición pull ya ha sido incluida a %s."
 
-#: kallithea/controllers/pullrequests.py:522
+#: kallithea/controllers/pullrequests.py:520
 msgid "This pull request has been closed and can not be updated."
 msgstr "La petición pull esta cerrada y no se puede actualizar."
 
-#: kallithea/controllers/pullrequests.py:546
+#: kallithea/controllers/pullrequests.py:539
 #, fuzzy, python-format
 #| msgid "The following changes are available on %s:"
 msgid "The following additional changes are available on %s:"
 msgstr "Los siguientes cambios están disponibles en %s:"
 
-#: kallithea/controllers/pullrequests.py:548
-#: kallithea/controllers/pullrequests.py:552
+#: kallithea/controllers/pullrequests.py:541
+#: kallithea/controllers/pullrequests.py:545
 #, fuzzy
 #| msgid "No changesets found for updating this pull request."
 msgid "No additional changesets found for iterating on this pull request."
 msgstr "No se encontraron cambios para actualizar la petición pull."
 
-#: kallithea/controllers/pullrequests.py:560
+#: kallithea/controllers/pullrequests.py:553
 #, fuzzy, python-format
 msgid "Note: Branch %s has another head: %s."
 msgstr "Nota: la rama %s tiene otro head: %s."
 
-#: kallithea/controllers/pullrequests.py:567
+#: kallithea/controllers/pullrequests.py:560
 #, fuzzy
 #| msgid "Git pull requests don't support updates yet."
 msgid "Git pull requests don't support iterating yet."
 msgstr "La peticiones pull de Git aún no soportan actualizaciones."
 
-#: kallithea/controllers/pullrequests.py:569
+#: kallithea/controllers/pullrequests.py:562
 #, fuzzy, python-format
 #| msgid "No changesets found for updating this pull request."
 msgid ""
 "Error: some changesets not found when displaying pull request from %s."
 msgstr "No se encontraron cambios para actualizar la petición pull."
 
-#: kallithea/controllers/pullrequests.py:593
+#: kallithea/controllers/pullrequests.py:586
 msgid "The diff can't be shown - the PR revisions could not be found."
 msgstr ""
 
-#: kallithea/controllers/search.py:136
+#: kallithea/controllers/search.py:132
 msgid "Invalid search query. Try quoting it."
 msgstr "Consulta de búsqueda inválida. Inténtelo entre comillas."
 
-#: kallithea/controllers/search.py:140
+#: kallithea/controllers/search.py:136
 msgid "The server has no search index."
 msgstr ""
 
-#: kallithea/controllers/search.py:143
+#: kallithea/controllers/search.py:139
 msgid "An error occurred during search operation."
 msgstr "Ocurrió un error mientras se ejecutaba la búsqueda."
 
-#: kallithea/controllers/summary.py:168
-#: kallithea/templates/summary/summary.html:412
+#: kallithea/controllers/summary.py:169
+#: kallithea/templates/summary/summary.html:410
 msgid "No data ready yet"
 msgstr "Todavía no hay datos disponibles"
 
-#: kallithea/controllers/summary.py:171
+#: kallithea/controllers/summary.py:172
 #: kallithea/templates/summary/summary.html:97
 msgid "Statistics are disabled for this repository"
 msgstr "Las estadísticas están deshabilitadas en este repositorio"
 
-#: kallithea/controllers/admin/auth_settings.py:137
+#: kallithea/controllers/admin/auth_settings.py:136
 msgid "Auth settings updated successfully"
 msgstr "Los ajustes de autentificación se han actualizado correctamente"
 
-#: kallithea/controllers/admin/auth_settings.py:148
+#: kallithea/controllers/admin/auth_settings.py:147
 msgid "error occurred during update of auth settings"
 msgstr "ocurrió un error al actualizar los ajustes de autentificación"
 
-#: kallithea/controllers/admin/defaults.py:75
+#: kallithea/controllers/admin/defaults.py:74
 msgid "Default settings updated successfully"
 msgstr "Los ajustes predeterminados se han actualizado correctamente"
 
-#: kallithea/controllers/admin/defaults.py:90
+#: kallithea/controllers/admin/defaults.py:89
 msgid "Error occurred during update of defaults"
 msgstr "Ocurrió un error al actualizar los ajustes predeterminados"
 
 #: kallithea/controllers/admin/gists.py:59
 #: kallithea/controllers/admin/my_account.py:232
-#: kallithea/controllers/admin/users.py:248
+#: kallithea/controllers/admin/users.py:246
 msgid "Forever"
 msgstr "Para siempre"
 
 #: kallithea/controllers/admin/gists.py:60
 #: kallithea/controllers/admin/my_account.py:233
-#: kallithea/controllers/admin/users.py:249
+#: kallithea/controllers/admin/users.py:247
 msgid "5 minutes"
 msgstr "5 minutos"
 
 #: kallithea/controllers/admin/gists.py:61
 #: kallithea/controllers/admin/my_account.py:234
-#: kallithea/controllers/admin/users.py:250
+#: kallithea/controllers/admin/users.py:248
 msgid "1 hour"
 msgstr "1 hora"
 
 #: kallithea/controllers/admin/gists.py:62
 #: kallithea/controllers/admin/my_account.py:235
-#: kallithea/controllers/admin/users.py:251
+#: kallithea/controllers/admin/users.py:249
 msgid "1 day"
 msgstr "1 día"
 
 #: kallithea/controllers/admin/gists.py:63
 #: kallithea/controllers/admin/my_account.py:236
-#: kallithea/controllers/admin/users.py:252
+#: kallithea/controllers/admin/users.py:250
 msgid "1 month"
 msgstr "1 mes"
 
 #: kallithea/controllers/admin/gists.py:67
 #: kallithea/controllers/admin/my_account.py:238
-#: kallithea/controllers/admin/users.py:254
+#: kallithea/controllers/admin/users.py:252
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:65
 #: kallithea/templates/admin/users/user_edit_api_keys.html:65
 msgid "Lifetime"
 msgstr "Tiempo de vida"
 
-#: kallithea/controllers/admin/gists.py:142
+#: kallithea/controllers/admin/gists.py:148
 msgid "Error occurred during gist creation"
 msgstr "Ocurrió un error mientras se creaba el gist"
 
-#: kallithea/controllers/admin/gists.py:158
+#: kallithea/controllers/admin/gists.py:164
 #, python-format
 msgid "Deleted gist %s"
 msgstr "Gist %s eliminado"
 
-#: kallithea/controllers/admin/gists.py:198
+#: kallithea/controllers/admin/gists.py:207
 msgid "Unmodified"
 msgstr "Sin modificar"
 
-#: kallithea/controllers/admin/gists.py:228
+#: kallithea/controllers/admin/gists.py:237
 msgid "Successfully updated gist content"
 msgstr "Gist actualizado correctamente"
 
-#: kallithea/controllers/admin/gists.py:233
+#: kallithea/controllers/admin/gists.py:242
 msgid "Successfully updated gist data"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:236
+#: kallithea/controllers/admin/gists.py:245
 #, python-format
 msgid "Error occurred during update of gist %s"
 msgstr ""
 
-#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:211
-#: kallithea/model/user.py:232
+#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:205
+#: kallithea/model/user.py:226
 msgid "You can't edit this user since it's crucial for entire application"
 msgstr ""
 
@@ -571,7 +575,7 @@
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:134
-#: kallithea/controllers/admin/users.py:181
+#: kallithea/controllers/admin/users.py:179
 #, python-format
 msgid "Error occurred during update of user %s"
 msgstr ""
@@ -581,44 +585,44 @@
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:209
-#: kallithea/controllers/admin/users.py:367
+#: kallithea/controllers/admin/users.py:365
 #, python-format
 msgid "Added email %s to user"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:215
-#: kallithea/controllers/admin/users.py:373
+#: kallithea/controllers/admin/users.py:371
 msgid "An error occurred during email saving"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:224
-#: kallithea/controllers/admin/users.py:383
+#: kallithea/controllers/admin/users.py:381
 msgid "Removed email from user"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:248
-#: kallithea/controllers/admin/users.py:271
+#: kallithea/controllers/admin/users.py:269
 msgid "API key successfully created"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:257
-#: kallithea/controllers/admin/users.py:281
+#: kallithea/controllers/admin/users.py:279
 msgid "API key successfully reset"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:261
-#: kallithea/controllers/admin/users.py:285
+#: kallithea/controllers/admin/users.py:283
 msgid "API key successfully deleted"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:281
-#: kallithea/controllers/admin/users.py:456
+#: kallithea/controllers/admin/users.py:454
 #, python-format
 msgid "SSH key %s successfully added"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:293
-#: kallithea/controllers/admin/users.py:470
+#: kallithea/controllers/admin/users.py:468
 #, fuzzy
 #| msgid "Successfully deleted file %s"
 msgid "SSH key successfully deleted"
@@ -696,11 +700,11 @@
 msgid "Allowed with automatic account activation"
 msgstr ""
 
-#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1673
+#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1578
 msgid "Manual activation of external account"
 msgstr ""
 
-#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1674
+#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1579
 msgid "Automatic activation of external account"
 msgstr ""
 
@@ -722,342 +726,334 @@
 msgid "Error occurred during update of permissions"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:172
+#: kallithea/controllers/admin/repo_groups.py:165
 #, python-format
 msgid "Error occurred during creation of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:177
+#: kallithea/controllers/admin/repo_groups.py:172
 #, python-format
 msgid "Created repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:224
+#: kallithea/controllers/admin/repo_groups.py:219
 #, python-format
 msgid "Updated repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:240
+#: kallithea/controllers/admin/repo_groups.py:235
 #, python-format
 msgid "Error occurred during update of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:250
+#: kallithea/controllers/admin/repo_groups.py:245
 #, python-format
 msgid "This group contains %s repositories and cannot be deleted"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:257
+#: kallithea/controllers/admin/repo_groups.py:252
 #, python-format
 msgid "This group contains %s subgroups and cannot be deleted"
 msgstr ""
 
+#: kallithea/controllers/admin/repo_groups.py:258
+#, python-format
+msgid "Removed repository group %s"
+msgstr ""
+
 #: kallithea/controllers/admin/repo_groups.py:263
 #, python-format
-msgid "Removed repository group %s"
-msgstr ""
-
-#: kallithea/controllers/admin/repo_groups.py:268
-#, python-format
 msgid "Error occurred during deletion of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:352
-#: kallithea/controllers/admin/repo_groups.py:382
-#: kallithea/controllers/admin/user_groups.py:294
+#: kallithea/controllers/admin/repo_groups.py:347
+#: kallithea/controllers/admin/repo_groups.py:377
+#: kallithea/controllers/admin/user_groups.py:290
 msgid "Cannot revoke permission for yourself as admin"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:367
+#: kallithea/controllers/admin/repo_groups.py:362
 msgid "Repository group permissions updated"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:399
-#: kallithea/controllers/admin/repos.py:358
-#: kallithea/controllers/admin/user_groups.py:306
+#: kallithea/controllers/admin/repo_groups.py:394
+#: kallithea/controllers/admin/repos.py:357
+#: kallithea/controllers/admin/user_groups.py:302
 msgid "An error occurred during revoking of permission"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:136
+#: kallithea/controllers/admin/repos.py:137
 #, python-format
 msgid "Error creating repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:194
+#: kallithea/controllers/admin/repos.py:193
 #, python-format
 msgid "Created repository %s from %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:203
+#: kallithea/controllers/admin/repos.py:202
 #, python-format
 msgid "Forked repository %s as %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:206
+#: kallithea/controllers/admin/repos.py:205
 #, python-format
 msgid "Created repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:235
+#: kallithea/controllers/admin/repos.py:234
 #, python-format
 msgid "Repository %s updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:255
+#: kallithea/controllers/admin/repos.py:254
 #, python-format
 msgid "Error occurred during update of repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:273
+#: kallithea/controllers/admin/repos.py:272
 #, python-format
 msgid "Detached %s forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:276
+#: kallithea/controllers/admin/repos.py:275
 #, python-format
 msgid "Deleted %s forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:281
+#: kallithea/controllers/admin/repos.py:280
 #, python-format
 msgid "Deleted repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:284
+#: kallithea/controllers/admin/repos.py:283
 #, python-format
 msgid "Cannot delete repository %s which still has forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:289
+#: kallithea/controllers/admin/repos.py:288
 #, python-format
 msgid "An error occurred during deletion of %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:329
+#: kallithea/controllers/admin/repos.py:328
 msgid "Repository permissions updated"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:388
+#: kallithea/controllers/admin/repos.py:387
 #, python-format
 msgid "Field validation error: %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:391
+#: kallithea/controllers/admin/repos.py:390
 #, fuzzy, python-format
 #| msgid "An error occurred during repository forking %s"
 msgid "An error occurred during creation of field: %r"
 msgstr "Ocurrió un error mientras se bifurcaba el repositorio %s"
 
-#: kallithea/controllers/admin/repos.py:402
+#: kallithea/controllers/admin/repos.py:401
 msgid "An error occurred during removal of field"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:416
+#: kallithea/controllers/admin/repos.py:415
 msgid "-- Not a fork --"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:448
+#: kallithea/controllers/admin/repos.py:447
 msgid "Updated repository visibility in public journal"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:452
+#: kallithea/controllers/admin/repos.py:451
 msgid "An error occurred during setting this repository in public journal"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:468
+#: kallithea/controllers/admin/repos.py:467
 msgid "Nothing"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:470
+#: kallithea/controllers/admin/repos.py:469
 #, python-format
 msgid "Marked repository %s as fork of %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:477
+#: kallithea/controllers/admin/repos.py:476
 msgid "An error occurred during this operation"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:490
-msgid "Cache invalidation successful"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:494
-msgid "An error occurred during cache invalidation"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:507
+#: kallithea/controllers/admin/repos.py:488
 msgid "Pulled from remote location"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:510
+#: kallithea/controllers/admin/repos.py:491
 msgid "An error occurred during pull from remote location"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:541
+#: kallithea/controllers/admin/repos.py:522
 msgid "An error occurred during deletion of repository stats"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:131
+#: kallithea/controllers/admin/settings.py:132
 msgid "Updated VCS settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:135 kallithea/lib/utils.py:237
+#: kallithea/controllers/admin/settings.py:136
 msgid ""
 "Unable to activate hgsubversion support. The \"hgsubversion\" library is "
 "missing"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:141
-#: kallithea/controllers/admin/settings.py:233
+#: kallithea/controllers/admin/settings.py:142
+#: kallithea/controllers/admin/settings.py:234
 msgid "Error occurred while updating application settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:176
+#: kallithea/controllers/admin/settings.py:177
 #, python-format
 msgid "Repositories successfully rescanned. Added: %s. Removed: %s."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:188
+#: kallithea/controllers/admin/settings.py:189
 #, python-format
 msgid "Invalidated %s repositories"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:229
+#: kallithea/controllers/admin/settings.py:230
 msgid "Updated application settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:283
+#: kallithea/controllers/admin/settings.py:284
 msgid "Updated visualisation settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:288
+#: kallithea/controllers/admin/settings.py:289
 msgid "Error occurred during updating visualisation settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:312
+#: kallithea/controllers/admin/settings.py:313
 msgid "Please enter email address"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:327
+#: kallithea/controllers/admin/settings.py:328
 msgid "Send email task created"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:355
+#: kallithea/controllers/admin/settings.py:356
 #, fuzzy
 #| msgid "No data ready yet"
 msgid "Hook already exists"
 msgstr "Todavía no hay datos disponibles"
 
-#: kallithea/controllers/admin/settings.py:357
+#: kallithea/controllers/admin/settings.py:358
 msgid "Builtin hooks are read-only. Please use another hook name."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:360
+#: kallithea/controllers/admin/settings.py:361
 msgid "Added new hook"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:376
+#: kallithea/controllers/admin/settings.py:377
 msgid "Updated hooks"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:380
+#: kallithea/controllers/admin/settings.py:381
 msgid "Error occurred during hook creation"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:404
+#: kallithea/controllers/admin/settings.py:405
 msgid "Whoosh reindex task scheduled"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:138
+#: kallithea/controllers/admin/user_groups.py:134
 #, python-format
 msgid "Created user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:151
+#: kallithea/controllers/admin/user_groups.py:147
 #, python-format
 msgid "Error occurred during creation of user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:179
+#: kallithea/controllers/admin/user_groups.py:175
 #, python-format
 msgid "Updated user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:201
+#: kallithea/controllers/admin/user_groups.py:197
 #, python-format
 msgid "Error occurred during update of user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:212
+#: kallithea/controllers/admin/user_groups.py:208
 msgid "Successfully deleted user group"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:217
+#: kallithea/controllers/admin/user_groups.py:213
 msgid "An error occurred during deletion of user group"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:273
+#: kallithea/controllers/admin/user_groups.py:269
 msgid "Target group cannot be the same"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:279
+#: kallithea/controllers/admin/user_groups.py:275
 msgid "User group permissions updated"
 msgstr ""
 
+#: kallithea/controllers/admin/user_groups.py:384
+#: kallithea/controllers/admin/users.py:336
+msgid "Updated permissions"
+msgstr ""
+
 #: kallithea/controllers/admin/user_groups.py:388
-#: kallithea/controllers/admin/users.py:338
-msgid "Updated permissions"
-msgstr ""
-
-#: kallithea/controllers/admin/user_groups.py:392
-#: kallithea/controllers/admin/users.py:342
+#: kallithea/controllers/admin/users.py:340
 msgid "An error occurred during permissions saving"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:123
+#: kallithea/controllers/admin/users.py:121
 #, python-format
 msgid "Created user %s"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:138
+#: kallithea/controllers/admin/users.py:136
 #, python-format
 msgid "Error occurred during creation of user %s"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:162
+#: kallithea/controllers/admin/users.py:160
 msgid "User updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:190
+#: kallithea/controllers/admin/users.py:188
 msgid "Successfully deleted user"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:195
+#: kallithea/controllers/admin/users.py:193
 msgid "An error occurred during deletion of user"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:203
+#: kallithea/controllers/admin/users.py:201
 msgid "The default user cannot be edited"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:409
+#: kallithea/controllers/admin/users.py:407
 #, python-format
 msgid "Added IP address %s to user whitelist"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:415
+#: kallithea/controllers/admin/users.py:413
 msgid "An error occurred while adding IP address"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:427
+#: kallithea/controllers/admin/users.py:425
 msgid "Removed IP address from user whitelist"
 msgstr ""
 
-#: kallithea/lib/auth.py:684
+#: kallithea/lib/auth.py:634
 msgid "You need to be a registered user to perform this action"
 msgstr ""
 
-#: kallithea/lib/auth.py:712
+#: kallithea/lib/auth.py:662
 msgid "You need to be signed in to view this page"
 msgstr ""
 
@@ -1088,170 +1084,170 @@
 "Changeset was too big and was cut off, use diff menu to display this diff"
 msgstr ""
 
-#: kallithea/lib/diffs.py:224
+#: kallithea/lib/diffs.py:223
 msgid "No changes detected"
 msgstr ""
 
-#: kallithea/lib/helpers.py:653
+#: kallithea/lib/helpers.py:670
 #, python-format
 msgid "Deleted branch: %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:655
+#: kallithea/lib/helpers.py:672
 #, python-format
 msgid "Created tag: %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:666
+#: kallithea/lib/helpers.py:683
 #, python-format
 msgid "Changeset %s not found"
 msgstr ""
 
-#: kallithea/lib/helpers.py:715
+#: kallithea/lib/helpers.py:732
 #, python-format
 msgid "Show all combined changesets %s->%s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:721
+#: kallithea/lib/helpers.py:738
 msgid "Compare view"
 msgstr ""
 
-#: kallithea/lib/helpers.py:740
+#: kallithea/lib/helpers.py:757
 msgid "and"
 msgstr ""
 
-#: kallithea/lib/helpers.py:741
+#: kallithea/lib/helpers.py:758
 #, python-format
 msgid "%s more"
 msgstr ""
 
-#: kallithea/lib/helpers.py:742
+#: kallithea/lib/helpers.py:759
 #: kallithea/templates/changelog/changelog.html:43
 msgid "revisions"
 msgstr ""
 
-#: kallithea/lib/helpers.py:766
+#: kallithea/lib/helpers.py:783
 #, python-format
 msgid "Fork name %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:787
+#: kallithea/lib/helpers.py:804
 #, python-format
 msgid "Pull request %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:797
+#: kallithea/lib/helpers.py:814
 msgid "[deleted] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:799 kallithea/lib/helpers.py:811
+#: kallithea/lib/helpers.py:816 kallithea/lib/helpers.py:828
 msgid "[created] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:801
+#: kallithea/lib/helpers.py:818
 msgid "[created] repository as fork"
 msgstr ""
 
-#: kallithea/lib/helpers.py:803 kallithea/lib/helpers.py:813
+#: kallithea/lib/helpers.py:820 kallithea/lib/helpers.py:830
 msgid "[forked] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:805 kallithea/lib/helpers.py:815
+#: kallithea/lib/helpers.py:822 kallithea/lib/helpers.py:832
 msgid "[updated] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:807
+#: kallithea/lib/helpers.py:824
 msgid "[downloaded] archive from repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:809
+#: kallithea/lib/helpers.py:826
 msgid "[delete] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:817
+#: kallithea/lib/helpers.py:834
 msgid "[created] user"
 msgstr ""
 
-#: kallithea/lib/helpers.py:819
+#: kallithea/lib/helpers.py:836
 msgid "[updated] user"
 msgstr ""
 
-#: kallithea/lib/helpers.py:821
+#: kallithea/lib/helpers.py:838
 msgid "[created] user group"
 msgstr ""
 
-#: kallithea/lib/helpers.py:823
+#: kallithea/lib/helpers.py:840
 msgid "[updated] user group"
 msgstr ""
 
-#: kallithea/lib/helpers.py:825
+#: kallithea/lib/helpers.py:842
 msgid "[commented] on revision in repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:827
+#: kallithea/lib/helpers.py:844
 msgid "[commented] on pull request for"
 msgstr ""
 
-#: kallithea/lib/helpers.py:829
+#: kallithea/lib/helpers.py:846
 msgid "[closed] pull request for"
 msgstr ""
 
-#: kallithea/lib/helpers.py:831
+#: kallithea/lib/helpers.py:848
 msgid "[pushed] into"
 msgstr ""
 
-#: kallithea/lib/helpers.py:833
+#: kallithea/lib/helpers.py:850
 msgid "[committed via Kallithea] into repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:835
+#: kallithea/lib/helpers.py:852
 msgid "[pulled from remote] into repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:837
+#: kallithea/lib/helpers.py:854
 msgid "[pulled] from"
 msgstr ""
 
-#: kallithea/lib/helpers.py:839
+#: kallithea/lib/helpers.py:856
 msgid "[started following] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:841
+#: kallithea/lib/helpers.py:858
 msgid "[stopped following] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:961
+#: kallithea/lib/helpers.py:975
 #, python-format
 msgid " and %s more"
 msgstr ""
 
-#: kallithea/lib/helpers.py:965
+#: kallithea/lib/helpers.py:979
 #: kallithea/templates/compare/compare_diff.html:69
 #: kallithea/templates/pullrequests/pullrequest_show.html:297
 msgid "No files"
 msgstr ""
 
-#: kallithea/lib/helpers.py:990
+#: kallithea/lib/helpers.py:1004
 msgid "new file"
 msgstr ""
 
-#: kallithea/lib/helpers.py:993
+#: kallithea/lib/helpers.py:1007
 msgid "mod"
 msgstr ""
 
-#: kallithea/lib/helpers.py:996
+#: kallithea/lib/helpers.py:1010
 msgid "del"
 msgstr ""
 
-#: kallithea/lib/helpers.py:999
+#: kallithea/lib/helpers.py:1013
 msgid "rename"
 msgstr ""
 
-#: kallithea/lib/helpers.py:1004
+#: kallithea/lib/helpers.py:1018
 msgid "chmod"
 msgstr ""
 
-#: kallithea/lib/helpers.py:1297
+#: kallithea/lib/helpers.py:1323
 #, python-format
 msgid ""
 "%s repository is not mapped to db perhaps it was created or renamed from "
@@ -1259,96 +1255,98 @@
 "repositories"
 msgstr ""
 
-#: kallithea/lib/ssh.py:73
+#: kallithea/lib/ssh.py:75
 msgid "SSH key is missing"
 msgstr ""
 
-#: kallithea/lib/ssh.py:77
-msgid "Incorrect SSH key - it must have both a key type and a base64 part"
-msgstr ""
-
-#: kallithea/lib/ssh.py:81
+#: kallithea/lib/ssh.py:79
+msgid ""
+"Incorrect SSH key - it must have both a key type and a base64 part, like "
+"'ssh-rsa ASRNeaZu4FA...xlJp='"
+msgstr ""
+
+#: kallithea/lib/ssh.py:83
 msgid "Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'"
 msgstr ""
 
-#: kallithea/lib/ssh.py:84
+#: kallithea/lib/ssh.py:86
 #, python-format
 msgid "Incorrect SSH key - unexpected characters in base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:89
+#: kallithea/lib/ssh.py:91
 #, python-format
 msgid "Incorrect SSH key - failed to decode base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:92
+#: kallithea/lib/ssh.py:94
 #, python-format
 msgid "Incorrect SSH key - base64 part is not %r as claimed but %r"
 msgstr ""
 
-#: kallithea/lib/utils2.py:334
+#: kallithea/lib/utils2.py:253
 #, python-format
 msgid "%d year"
 msgid_plural "%d years"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:335
+#: kallithea/lib/utils2.py:254
 #, python-format
 msgid "%d month"
 msgid_plural "%d months"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:336
+#: kallithea/lib/utils2.py:255
 #, python-format
 msgid "%d day"
 msgid_plural "%d days"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:337
+#: kallithea/lib/utils2.py:256
 #, python-format
 msgid "%d hour"
 msgid_plural "%d hours"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:338
+#: kallithea/lib/utils2.py:257
 #, python-format
 msgid "%d minute"
 msgid_plural "%d minutes"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:339
+#: kallithea/lib/utils2.py:258
 #, python-format
 msgid "%d second"
 msgid_plural "%d seconds"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:355
+#: kallithea/lib/utils2.py:274
 #, python-format
 msgid "in %s"
 msgstr ""
 
-#: kallithea/lib/utils2.py:357
+#: kallithea/lib/utils2.py:276
 #, python-format
 msgid "%s ago"
 msgstr ""
 
-#: kallithea/lib/utils2.py:359
+#: kallithea/lib/utils2.py:278
 #, python-format
 msgid "in %s and %s"
 msgstr ""
 
-#: kallithea/lib/utils2.py:362
+#: kallithea/lib/utils2.py:281
 #, python-format
 msgid "%s and %s ago"
 msgstr ""
 
-#: kallithea/lib/utils2.py:365
+#: kallithea/lib/utils2.py:284
 msgid "just now"
 msgstr ""
 
@@ -1357,133 +1355,133 @@
 msgid "on line %s"
 msgstr ""
 
-#: kallithea/model/comment.py:221 kallithea/model/pull_request.py:114
+#: kallithea/model/comment.py:219 kallithea/model/pull_request.py:112
 msgid "[Mention]"
 msgstr ""
 
-#: kallithea/model/db.py:1496
+#: kallithea/model/db.py:1411
 msgid "top level"
 msgstr ""
 
-#: kallithea/model/db.py:1637
+#: kallithea/model/db.py:1542
 msgid "Kallithea Administrator"
 msgstr ""
 
-#: kallithea/model/db.py:1639
+#: kallithea/model/db.py:1544
 msgid "Default user has no access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1640
+#: kallithea/model/db.py:1545
 msgid "Default user has read access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1641
+#: kallithea/model/db.py:1546
 msgid "Default user has write access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1642
+#: kallithea/model/db.py:1547
 msgid "Default user has admin access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1644
+#: kallithea/model/db.py:1549
 msgid "Default user has no access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1645
+#: kallithea/model/db.py:1550
 msgid "Default user has read access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1646
+#: kallithea/model/db.py:1551
 msgid "Default user has write access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1647
+#: kallithea/model/db.py:1552
 msgid "Default user has admin access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1649
+#: kallithea/model/db.py:1554
 msgid "Default user has no access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1650
+#: kallithea/model/db.py:1555
 msgid "Default user has read access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1651
+#: kallithea/model/db.py:1556
 msgid "Default user has write access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1652
+#: kallithea/model/db.py:1557
 msgid "Default user has admin access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1654
+#: kallithea/model/db.py:1559
 msgid "Only admins can create repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1655
+#: kallithea/model/db.py:1560
 msgid "Non-admins can create repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1657
+#: kallithea/model/db.py:1562
 msgid "Only admins can create user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1658
+#: kallithea/model/db.py:1563
 msgid "Non-admins can create user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1660
+#: kallithea/model/db.py:1565
 msgid "Only admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1661
+#: kallithea/model/db.py:1566
 msgid "Non-admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1663
+#: kallithea/model/db.py:1568
 msgid ""
 "Repository creation enabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1664
+#: kallithea/model/db.py:1569
 msgid ""
 "Repository creation disabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1666
+#: kallithea/model/db.py:1571
 msgid "Only admins can fork repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1667
+#: kallithea/model/db.py:1572
 msgid "Non-admins can fork repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1669
+#: kallithea/model/db.py:1574
 msgid "Registration disabled"
 msgstr ""
 
-#: kallithea/model/db.py:1670
+#: kallithea/model/db.py:1575
 msgid "User registration with manual account activation"
 msgstr ""
 
-#: kallithea/model/db.py:1671
+#: kallithea/model/db.py:1576
 msgid "User registration with automatic account activation"
 msgstr ""
 
-#: kallithea/model/db.py:2206
+#: kallithea/model/db.py:1992
 msgid "Not reviewed"
 msgstr ""
 
-#: kallithea/model/db.py:2207
+#: kallithea/model/db.py:1993
 msgid "Under review"
 msgstr ""
 
-#: kallithea/model/db.py:2208
+#: kallithea/model/db.py:1994
 msgid "Not approved"
 msgstr ""
 
-#: kallithea/model/db.py:2209
+#: kallithea/model/db.py:1995
 msgid "Approved"
 msgstr ""
 
@@ -1509,79 +1507,79 @@
 msgid "Name must not contain only digits"
 msgstr ""
 
-#: kallithea/model/notification.py:164
+#: kallithea/model/notification.py:162
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s changeset %(short_id)s \"%(message_short)s\" on "
 "%(branch)s"
 msgstr ""
 
-#: kallithea/model/notification.py:167
+#: kallithea/model/notification.py:165
 #, python-format
 msgid "New user %(new_username)s registered"
 msgstr ""
 
-#: kallithea/model/notification.py:169
+#: kallithea/model/notification.py:167
 #, python-format
 msgid ""
 "[Review] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:170
+#: kallithea/model/notification.py:168
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:183
+#: kallithea/model/notification.py:188
 msgid "Closing"
 msgstr ""
 
-#: kallithea/model/pull_request.py:73
+#: kallithea/model/pull_request.py:72
 #, python-format
 msgid ""
 "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:209
+#: kallithea/model/pull_request.py:207
 #, fuzzy
 #| msgid "Error creating pull request: %s"
 msgid "Cannot create empty pull request"
 msgstr "Error al crear la petición de pull: %s"
 
-#: kallithea/model/pull_request.py:217
+#: kallithea/model/pull_request.py:215
 #, python-format
 msgid ""
 "Cannot create pull request - criss cross merge detected, please merge a "
 "later %s revision to %s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:245 kallithea/model/pull_request.py:380
+#: kallithea/model/pull_request.py:243 kallithea/model/pull_request.py:378
 msgid "You are not authorized to create the pull request"
 msgstr ""
 
-#: kallithea/model/pull_request.py:339
+#: kallithea/model/pull_request.py:337
 #, fuzzy
 #| msgid "Missing changesets since the previous pull request:"
 msgid "Missing changesets since the previous iteration:"
 msgstr "Cambios que faltan desde la ultima petición de pull:"
 
-#: kallithea/model/pull_request.py:346
+#: kallithea/model/pull_request.py:344
 #, fuzzy, python-format
 #| msgid "New changesets on %s %s since the previous pull request:"
 msgid "New changesets on %s %s since the previous iteration:"
 msgstr "Cambios nuevos en %s %s desde la ultima petición pull:"
 
-#: kallithea/model/pull_request.py:353
+#: kallithea/model/pull_request.py:351
 #, fuzzy
 #| msgid "Ancestor didn't change - show diff since previous version:"
 msgid "Ancestor didn't change - diff since previous iteration:"
 msgstr ""
 "El ascendente no ha cambiado - ver diferencias desde la versión anterior:"
 
-#: kallithea/model/pull_request.py:360
+#: kallithea/model/pull_request.py:358
 #, fuzzy, python-format
 msgid ""
 "This iteration is based on another %s revision and there is no simple "
@@ -1590,243 +1588,243 @@
 "La petición de pull está basada en otra %s revisión y no hay un diff "
 "simple."
 
-#: kallithea/model/pull_request.py:362
+#: kallithea/model/pull_request.py:360
 #, fuzzy, python-format
 #| msgid "No changes found on %s %s since previous version."
 msgid "No changes found on %s %s since previous iteration."
 msgstr "No se encontró ningún cambio en %s %s desde la versión anterior."
 
-#: kallithea/model/pull_request.py:388
+#: kallithea/model/pull_request.py:386
 #, python-format
 msgid "Closed, next iteration: %s ."
 msgstr ""
 
-#: kallithea/model/scm.py:668
+#: kallithea/model/scm.py:655
 msgid "latest tip"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:56
+#: kallithea/model/ssh_key.py:57
 #, python-format
 msgid "SSH key %r is invalid: %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:68
+#: kallithea/model/ssh_key.py:69
 #, python-format
 msgid "SSH key %s is already used by %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:89
-#, python-format
-msgid "SSH key %r not found"
-msgstr ""
-
-#: kallithea/model/user.py:186
+#: kallithea/model/ssh_key.py:88
+#, python-format
+msgid "SSH key with fingerprint %r found"
+msgstr ""
+
+#: kallithea/model/user.py:180
 msgid "New user registration"
 msgstr ""
 
-#: kallithea/model/user.py:250
+#: kallithea/model/user.py:244
 msgid ""
 "You can't remove this user since it is crucial for the entire application"
 msgstr ""
 
-#: kallithea/model/user.py:255
+#: kallithea/model/user.py:249
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repositories and cannot be removed. Switch "
 "owners or remove those repositories: %s"
 msgstr ""
 
-#: kallithea/model/user.py:260
+#: kallithea/model/user.py:254
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repository groups and cannot be removed. Switch "
 "owners or remove those repository groups: %s"
 msgstr ""
 
-#: kallithea/model/user.py:267
+#: kallithea/model/user.py:261
 #, python-format
 msgid ""
 "User \"%s\" still owns %s user groups and cannot be removed. Switch "
 "owners or remove those user groups: %s"
 msgstr ""
 
-#: kallithea/model/user.py:361
+#: kallithea/model/user.py:355
 msgid "Password reset link"
 msgstr ""
 
-#: kallithea/model/user.py:408
+#: kallithea/model/user.py:402
 msgid "Password reset notification"
 msgstr ""
 
-#: kallithea/model/user.py:409
+#: kallithea/model/user.py:403
 #, python-format
 msgid ""
 "The password to your account %s has been changed using password reset "
 "form."
 msgstr ""
 
-#: kallithea/model/validators.py:52 kallithea/model/validators.py:53
+#: kallithea/model/validators.py:53 kallithea/model/validators.py:54
 msgid "Value cannot be an empty list"
 msgstr ""
 
-#: kallithea/model/validators.py:72
+#: kallithea/model/validators.py:73
 #, python-format
 msgid "Username \"%(username)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:74
+#: kallithea/model/validators.py:75
 #, python-format
 msgid "Username \"%(username)s\" cannot be used"
 msgstr ""
 
-#: kallithea/model/validators.py:76
+#: kallithea/model/validators.py:77
 msgid ""
 "Username may only contain alphanumeric characters underscores, periods or "
 "dashes and must begin with an alphanumeric character or underscore"
 msgstr ""
 
-#: kallithea/model/validators.py:103
+#: kallithea/model/validators.py:104
 msgid "The input is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:110
+#: kallithea/model/validators.py:111
 #, python-format
 msgid "Username %(username)s is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:131
-msgid "Invalid user group name"
-msgstr ""
-
 #: kallithea/model/validators.py:132
+msgid "Invalid user group name"
+msgstr ""
+
+#: kallithea/model/validators.py:133
 #, python-format
 msgid "User group \"%(usergroup)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:134
+#: kallithea/model/validators.py:135
 msgid ""
 "user group name may only contain alphanumeric characters underscores, "
 "periods or dashes and must begin with alphanumeric character"
 msgstr ""
 
-#: kallithea/model/validators.py:174
-msgid "Cannot assign this group as parent"
-msgstr ""
-
 #: kallithea/model/validators.py:175
+msgid "Cannot assign this group as parent"
+msgstr ""
+
+#: kallithea/model/validators.py:176
 #, python-format
 msgid "Group \"%(group_name)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:177
+#: kallithea/model/validators.py:178
 #, python-format
 msgid "Repository with name \"%(group_name)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:233
+#: kallithea/model/validators.py:230
 msgid "Invalid characters (non-ascii) in password"
 msgstr ""
 
-#: kallithea/model/validators.py:248
+#: kallithea/model/validators.py:245
 msgid "Invalid old password"
 msgstr ""
 
-#: kallithea/model/validators.py:264
+#: kallithea/model/validators.py:261
 msgid "Passwords do not match"
 msgstr ""
 
-#: kallithea/model/validators.py:279
+#: kallithea/model/validators.py:276
 msgid "Invalid username or password"
 msgstr ""
 
+#: kallithea/model/validators.py:310
+#, python-format
+msgid "Repository name %(repo)s is not allowed"
+msgstr ""
+
+#: kallithea/model/validators.py:312
+#, python-format
+msgid "Repository named %(repo)s already exists"
+msgstr ""
+
 #: kallithea/model/validators.py:313
 #, python-format
-msgid "Repository name %(repo)s is not allowed"
+msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
 msgstr ""
 
 #: kallithea/model/validators.py:315
 #, python-format
-msgid "Repository named %(repo)s already exists"
-msgstr ""
-
-#: kallithea/model/validators.py:316
-#, python-format
-msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
-msgstr ""
-
-#: kallithea/model/validators.py:318
-#, python-format
 msgid "Repository group with name \"%(repo)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:404
+#: kallithea/model/validators.py:401
 msgid "Invalid repository URL"
 msgstr ""
 
-#: kallithea/model/validators.py:405
+#: kallithea/model/validators.py:402
 msgid ""
 "Invalid repository URL. It must be a valid http, https, ssh, svn+http or "
 "svn+https URL"
 msgstr ""
 
-#: kallithea/model/validators.py:430
+#: kallithea/model/validators.py:427
 msgid "Fork has to be the same type as parent"
 msgstr ""
 
-#: kallithea/model/validators.py:445
+#: kallithea/model/validators.py:442
 msgid "You don't have permissions to create repository in this group"
 msgstr ""
 
-#: kallithea/model/validators.py:447
+#: kallithea/model/validators.py:444
 msgid "no permission to create repository in root location"
 msgstr ""
 
-#: kallithea/model/validators.py:497
+#: kallithea/model/validators.py:494
 msgid "You don't have permissions to create a group in this location"
 msgstr ""
 
-#: kallithea/model/validators.py:537
+#: kallithea/model/validators.py:534
 msgid "This username or user group name is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:630
+#: kallithea/model/validators.py:627
 msgid "This is not a valid path"
 msgstr ""
 
-#: kallithea/model/validators.py:647
+#: kallithea/model/validators.py:644
 msgid "This email address is already in use"
 msgstr ""
 
-#: kallithea/model/validators.py:667
+#: kallithea/model/validators.py:664
 #, python-format
 msgid "Email address \"%(email)s\" not found"
 msgstr ""
 
-#: kallithea/model/validators.py:704
+#: kallithea/model/validators.py:701
 msgid ""
 "The LDAP Login attribute of the CN must be specified - this is the name "
 "of the attribute that is equivalent to \"username\""
 msgstr ""
 
-#: kallithea/model/validators.py:716
+#: kallithea/model/validators.py:713
 msgid "Please enter a valid IPv4 or IPv6 address"
 msgstr ""
 
-#: kallithea/model/validators.py:717
+#: kallithea/model/validators.py:714
 #, python-format
 msgid ""
 "The network size (bits) must be within the range of 0-32 (not %(bits)r)"
 msgstr ""
 
-#: kallithea/model/validators.py:750
+#: kallithea/model/validators.py:747
 msgid "Key name can only consist of letters, underscore, dash or numbers"
 msgstr ""
 
-#: kallithea/model/validators.py:764
+#: kallithea/model/validators.py:761
 msgid "Filename cannot be inside a directory"
 msgstr ""
 
-#: kallithea/model/validators.py:780
+#: kallithea/model/validators.py:777
 #, python-format
 msgid "Plugins %(loaded)s and %(next_to_load)s both export the same name"
 msgstr ""
@@ -1886,7 +1884,7 @@
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:60
 #: kallithea/templates/email_templates/pull_request.html:37
 #: kallithea/templates/forks/fork.html:34
-#: kallithea/templates/index_base.html:58
+#: kallithea/templates/index_base.html:59
 #: kallithea/templates/pullrequests/pullrequest.html:33
 #: kallithea/templates/pullrequests/pullrequest_show.html:38
 #: kallithea/templates/pullrequests/pullrequest_show.html:59
@@ -1894,14 +1892,14 @@
 msgid "Description"
 msgstr ""
 
-#: kallithea/templates/index_base.html:60
+#: kallithea/templates/index_base.html:61
 msgid "Last Change"
 msgstr ""
 
 #: kallithea/templates/admin/my_account/my_account_repos.html:15
 #: kallithea/templates/admin/my_account/my_account_watched.html:15
 #: kallithea/templates/admin/repos/repos.html:41
-#: kallithea/templates/index_base.html:62
+#: kallithea/templates/index_base.html:63
 msgid "Tip"
 msgstr ""
 
@@ -1911,7 +1909,7 @@
 #: kallithea/templates/admin/repos/repos.html:42
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:8
 #: kallithea/templates/admin/user_groups/user_groups.html:42
-#: kallithea/templates/index_base.html:63
+#: kallithea/templates/index_base.html:64
 #: kallithea/templates/pullrequests/pullrequest_data.html:16
 #: kallithea/templates/pullrequests/pullrequest_show.html:124
 #: kallithea/templates/pullrequests/pullrequest_show.html:219
@@ -1935,7 +1933,7 @@
 #: kallithea/templates/admin/users/user_edit_profile.html:18
 #: kallithea/templates/admin/users/users.html:37
 #: kallithea/templates/base/base.html:364
-#: kallithea/templates/email_templates/registration.html:11
+#: kallithea/templates/email_templates/registration.html:12
 #: kallithea/templates/login.html:28 kallithea/templates/register.html:31
 msgid "Username"
 msgstr ""
@@ -2059,7 +2057,7 @@
 #: kallithea/templates/admin/settings/settings.html:31
 #: kallithea/templates/admin/users/user_add.html:62
 #: kallithea/templates/admin/users/user_edit_profile.html:25
-#: kallithea/templates/email_templates/registration.html:33
+#: kallithea/templates/email_templates/registration.html:34
 #: kallithea/templates/register.html:66
 msgid "Email"
 msgstr ""
@@ -2306,7 +2304,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/gists/index.html:51
-#: kallithea/templates/data_table/_dt_elements.html:78
+#: kallithea/templates/data_table/_dt_elements.html:84
 msgid "Created"
 msgstr ""
 
@@ -2390,13 +2388,13 @@
 #: kallithea/templates/admin/users/user_edit_ips.html:21
 #: kallithea/templates/changeset/changeset_file_comment.html:30
 #: kallithea/templates/changeset/changeset_file_comment.html:121
-#: kallithea/templates/data_table/_dt_elements.html:69
-#: kallithea/templates/data_table/_dt_elements.html:89
-#: kallithea/templates/data_table/_dt_elements.html:91
-#: kallithea/templates/data_table/_dt_elements.html:101
-#: kallithea/templates/data_table/_dt_elements.html:103
-#: kallithea/templates/data_table/_dt_elements.html:120
-#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:75
+#: kallithea/templates/data_table/_dt_elements.html:95
+#: kallithea/templates/data_table/_dt_elements.html:97
+#: kallithea/templates/data_table/_dt_elements.html:107
+#: kallithea/templates/data_table/_dt_elements.html:109
+#: kallithea/templates/data_table/_dt_elements.html:126
+#: kallithea/templates/data_table/_dt_elements.html:128
 #: kallithea/templates/files/files_source.html:35
 #: kallithea/templates/files/files_source.html:38
 #: kallithea/templates/files/files_source.html:41
@@ -2412,14 +2410,14 @@
 #: kallithea/templates/base/perms_summary.html:44
 #: kallithea/templates/base/perms_summary.html:81
 #: kallithea/templates/base/perms_summary.html:83
-#: kallithea/templates/data_table/_dt_elements.html:63
-#: kallithea/templates/data_table/_dt_elements.html:64
-#: kallithea/templates/data_table/_dt_elements.html:85
-#: kallithea/templates/data_table/_dt_elements.html:86
-#: kallithea/templates/data_table/_dt_elements.html:97
-#: kallithea/templates/data_table/_dt_elements.html:98
-#: kallithea/templates/data_table/_dt_elements.html:116
-#: kallithea/templates/data_table/_dt_elements.html:117
+#: kallithea/templates/data_table/_dt_elements.html:69
+#: kallithea/templates/data_table/_dt_elements.html:70
+#: kallithea/templates/data_table/_dt_elements.html:91
+#: kallithea/templates/data_table/_dt_elements.html:92
+#: kallithea/templates/data_table/_dt_elements.html:103
+#: kallithea/templates/data_table/_dt_elements.html:104
+#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:123
 #: kallithea/templates/files/diff_2way.html:56
 #: kallithea/templates/files/files_source.html:37
 #: kallithea/templates/files/files_source.html:40
@@ -2720,7 +2718,7 @@
 #: kallithea/templates/admin/permissions/permissions_globals.html:27
 #: kallithea/templates/admin/repos/repo_add_base.html:28
 #: kallithea/templates/admin/repos/repo_edit_settings.html:33
-#: kallithea/templates/data_table/_dt_elements.html:134
+#: kallithea/templates/data_table/_dt_elements.html:140
 #: kallithea/templates/forks/fork.html:42
 msgid "Repository group"
 msgstr ""
@@ -2741,7 +2739,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:40
-#: kallithea/templates/data_table/_dt_elements.html:141
+#: kallithea/templates/data_table/_dt_elements.html:147
 msgid "User group"
 msgstr ""
 
@@ -2914,7 +2912,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:121
+#: kallithea/templates/data_table/_dt_elements.html:127
 #, python-format
 msgid "Confirm to delete this group: %s with %s repository"
 msgid_plural "Confirm to delete this group: %s with %s repositories"
@@ -3079,14 +3077,10 @@
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit.html:37
-msgid "Caches"
+msgid "Remote"
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit.html:40
-msgid "Remote"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit.html:43
 #: kallithea/templates/summary/statistics.html:8
 #: kallithea/templates/summary/summary.html:169
 #: kallithea/templates/summary/summary.html:170
@@ -3124,7 +3118,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:46
-#: kallithea/templates/data_table/_dt_elements.html:68
+#: kallithea/templates/data_table/_dt_elements.html:74
 #, python-format
 msgid "Confirm to delete this repository: %s"
 msgstr ""
@@ -3155,43 +3149,14 @@
 "it or restore it."
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:4
-msgid "Invalidate Repository Cache"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:6
-msgid ""
-"Manually invalidate cache for this repository. On first access, the "
-"repository will be cached again."
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:9
-msgid "List of Cached Values"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:12
-msgid "Prefix"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:13
+#: kallithea/templates/admin/repos/repo_edit_fields.html:6
+msgid "Label"
+msgstr ""
+
 #: kallithea/templates/admin/repos/repo_edit_fields.html:7
 msgid "Key"
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:14
-#: kallithea/templates/admin/user_groups/user_group_add.html:40
-#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
-#: kallithea/templates/admin/user_groups/user_groups.html:41
-#: kallithea/templates/admin/users/user_add.html:69
-#: kallithea/templates/admin/users/user_edit_profile.html:74
-#: kallithea/templates/admin/users/users.html:42
-msgid "Active"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_fields.html:6
-msgid "Label"
-msgstr ""
-
 #: kallithea/templates/admin/repos/repo_edit_fields.html:20
 #, python-format
 msgid "Confirm to delete this field: %s"
@@ -3708,6 +3673,15 @@
 msgid "Short, optional description for this user group."
 msgstr ""
 
+#: kallithea/templates/admin/user_groups/user_group_add.html:40
+#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
+#: kallithea/templates/admin/user_groups/user_groups.html:41
+#: kallithea/templates/admin/users/user_add.html:69
+#: kallithea/templates/admin/users/user_edit_profile.html:74
+#: kallithea/templates/admin/users/users.html:42
+msgid "Active"
+msgstr ""
+
 #: kallithea/templates/admin/user_groups/user_group_edit.html:5
 #, python-format
 msgid "%s user group settings"
@@ -3729,7 +3703,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:19
-#: kallithea/templates/data_table/_dt_elements.html:102
+#: kallithea/templates/data_table/_dt_elements.html:108
 #, python-format
 msgid "Confirm to delete this user group: %s"
 msgstr ""
@@ -3803,7 +3777,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:90
+#: kallithea/templates/data_table/_dt_elements.html:96
 #, python-format
 msgid "Confirm to delete this user: %s"
 msgstr ""
@@ -3903,10 +3877,12 @@
 msgstr ""
 
 #: kallithea/templates/base/base.html:167
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Follow"
 msgstr ""
 
 #: kallithea/templates/base/base.html:168
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Unfollow"
 msgstr ""
 
@@ -4311,23 +4287,23 @@
 msgid "Merge"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:96
+#: kallithea/templates/changeset/changeset.html:95
 msgid "Grafted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:102
+#: kallithea/templates/changeset/changeset.html:100
 msgid "Transplanted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:108
+#: kallithea/templates/changeset/changeset.html:106
 msgid "Replaced by:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:122
+#: kallithea/templates/changeset/changeset.html:120
 msgid "Preceded by:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:139
+#: kallithea/templates/changeset/changeset.html:137
 #: kallithea/templates/compare/compare_diff.html:59
 #: kallithea/templates/pullrequests/pullrequest_show.html:290
 #, python-format
@@ -4336,7 +4312,7 @@
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/templates/changeset/changeset.html:141
+#: kallithea/templates/changeset/changeset.html:139
 #: kallithea/templates/compare/compare_diff.html:61
 #: kallithea/templates/pullrequests/pullrequest_show.html:292
 #, python-format
@@ -4345,8 +4321,8 @@
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
 #: kallithea/templates/pullrequests/pullrequest_show.html:333
@@ -4582,23 +4558,23 @@
 msgid "Repository creation in progress..."
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:42
+#: kallithea/templates/data_table/_dt_elements.html:48
 msgid "No changesets yet"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:48
-#: kallithea/templates/data_table/_dt_elements.html:50
+#: kallithea/templates/data_table/_dt_elements.html:54
+#: kallithea/templates/data_table/_dt_elements.html:56
 #, python-format
 msgid "Subscribe to %s rss feed"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:56
-#: kallithea/templates/data_table/_dt_elements.html:58
+#: kallithea/templates/data_table/_dt_elements.html:62
+#: kallithea/templates/data_table/_dt_elements.html:64
 #, python-format
 msgid "Subscribe to %s atom feed"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:76
+#: kallithea/templates/data_table/_dt_elements.html:82
 msgid "Creating"
 msgstr ""
 
@@ -4634,6 +4610,13 @@
 msgid "by"
 msgstr ""
 
+#: kallithea/templates/email_templates/changeset_comment.html:36
+#: kallithea/templates/email_templates/pull_request_comment.html:43
+#, fuzzy
+#| msgid "%s committed on %s"
+msgid "View Comment"
+msgstr "%s anotó en %s"
+
 #: kallithea/templates/email_templates/comment.html:27
 msgid "Status change:"
 msgstr ""
@@ -4644,32 +4627,40 @@
 msgid "The pull request has been closed."
 msgstr "La petición pull esta cerrada y no se puede actualizar."
 
-#: kallithea/templates/email_templates/password_reset.html:9
+#: kallithea/templates/email_templates/default.html:4
+msgid "Message"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:4
+msgid "Password Reset Request"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:10
 #, python-format
 msgid "Hello %s"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:16
+#: kallithea/templates/email_templates/password_reset.html:17
 msgid "We have received a request to reset the password for your account."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:25
+#: kallithea/templates/email_templates/password_reset.html:26
 msgid ""
 "This account is however managed outside this system and the password "
 "cannot be changed here."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:28
+#: kallithea/templates/email_templates/password_reset.html:29
 msgid "To set a new password, click the following link"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:33
+#: kallithea/templates/email_templates/password_reset.html:34
 msgid ""
 "Should you not be able to use the link above, please type the following "
 "code into the password reset form"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:44
+#: kallithea/templates/email_templates/password_reset.html:45
 msgid ""
 "If it weren't you who requested the password reset, just disregard this "
 "message."
@@ -4702,6 +4693,12 @@
 msgid "to"
 msgstr ""
 
+#: kallithea/templates/email_templates/pull_request.html:85
+#, fuzzy
+#| msgid "Pull request updated"
+msgid "View Pull Request"
+msgstr "Petición pull actualizada"
+
 #: kallithea/templates/email_templates/pull_request_comment.html:4
 #, fuzzy, python-format
 #| msgid "Error creating pull request: %s"
@@ -4719,10 +4716,18 @@
 msgid "Comment on Pull Request %s \"%s\""
 msgstr "Error al crear la petición de pull: %s"
 
-#: kallithea/templates/email_templates/registration.html:22
+#: kallithea/templates/email_templates/registration.html:5
+msgid "New User Registration"
+msgstr ""
+
+#: kallithea/templates/email_templates/registration.html:23
 msgid "Full Name"
 msgstr ""
 
+#: kallithea/templates/email_templates/registration.html:42
+msgid "View User Profile"
+msgstr ""
+
 #: kallithea/templates/files/diff_2way.html:15
 #, python-format
 msgid "%s File side-by-side diff"
@@ -5323,45 +5328,45 @@
 msgid "Stats gathered: "
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:87
-#: kallithea/templates/summary/summary.html:371
+#: kallithea/templates/summary/statistics.html:85
+#: kallithea/templates/summary/summary.html:369
 msgid "files"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:111
-#: kallithea/templates/summary/summary.html:401
+#: kallithea/templates/summary/statistics.html:109
+#: kallithea/templates/summary/summary.html:399
 msgid "Show more"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:405
+#: kallithea/templates/summary/statistics.html:395
 msgid "commits"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:406
+#: kallithea/templates/summary/statistics.html:396
 msgid "files added"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:407
+#: kallithea/templates/summary/statistics.html:397
 msgid "files changed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:408
+#: kallithea/templates/summary/statistics.html:398
 msgid "files removed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:410
+#: kallithea/templates/summary/statistics.html:400
 msgid "commit"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:411
+#: kallithea/templates/summary/statistics.html:401
 msgid "file added"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:412
+#: kallithea/templates/summary/statistics.html:402
 msgid "file changed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:413
+#: kallithea/templates/summary/statistics.html:403
 msgid "file removed"
 msgstr ""
 
--- a/kallithea/i18n/fr/LC_MESSAGES/kallithea.po	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/i18n/fr/LC_MESSAGES/kallithea.po	Mon May 04 19:24:04 2020 +0200
@@ -4,9 +4,9 @@
 msgstr ""
 "Project-Id-Version: Kallithea 0.3\n"
 "Report-Msgid-Bugs-To: translations@kallithea-scm.org\n"
-"POT-Creation-Date: 2020-01-05 04:10+0100\n"
-"PO-Revision-Date: 2020-04-10 03:09+0000\n"
-"Last-Translator: Jeannette L <j.lavoie@net-c.ca>\n"
+"POT-Creation-Date: 2020-05-04 19:12+0200\n"
+"PO-Revision-Date: 2020-04-15 07:11+0000\n"
+"Last-Translator: Étienne Gilli <etienne@gilli.io>\n"
 "Language-Team: French <https://hosted.weblate.org/projects/kallithea/"
 "kallithea/fr/>\n"
 "Language: fr\n"
@@ -17,14 +17,14 @@
 "X-Generator: Weblate 4.0-dev\n"
 
 #: kallithea/controllers/changelog.py:67
-#: kallithea/controllers/pullrequests.py:250 kallithea/lib/base.py:602
+#: kallithea/controllers/pullrequests.py:247 kallithea/lib/base.py:602
 msgid "There are no changesets yet"
 msgstr "Il n’y a aucun changement pour le moment"
 
 #: kallithea/controllers/admin/permissions.py:64
 #: kallithea/controllers/admin/permissions.py:68
 #: kallithea/controllers/admin/permissions.py:72
-#: kallithea/controllers/changelog.py:137
+#: kallithea/controllers/changelog.py:136
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:7
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:88
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:7
@@ -33,36 +33,36 @@
 msgid "None"
 msgstr "Aucun"
 
-#: kallithea/controllers/changelog.py:140 kallithea/controllers/files.py:189
+#: kallithea/controllers/changelog.py:139 kallithea/controllers/files.py:189
 msgid "(closed)"
 msgstr "(fermé)"
 
-#: kallithea/controllers/changeset.py:81
+#: kallithea/controllers/changeset.py:82
 msgid "Show whitespace"
 msgstr "Afficher les espaces et tabulations"
 
-#: kallithea/controllers/changeset.py:88
-#: kallithea/controllers/changeset.py:95
+#: kallithea/controllers/changeset.py:89
+#: kallithea/controllers/changeset.py:96
 #: kallithea/templates/files/diff_2way.html:55
 msgid "Ignore whitespace"
 msgstr "Ignorer les espaces et tabulations"
 
-#: kallithea/controllers/changeset.py:161
+#: kallithea/controllers/changeset.py:162
 #, python-format
 msgid "Increase diff context to %(num)s lines"
 msgstr "Augmenter le contexte du diff à %(num)s lignes"
 
-#: kallithea/controllers/changeset.py:201
+#: kallithea/controllers/changeset.py:202
 msgid "No permission to change status"
 msgstr "Permission manquante pour changer le statut"
 
-#: kallithea/controllers/changeset.py:212
+#: kallithea/controllers/changeset.py:213
 #, python-format
 msgid "Successfully deleted pull request %s"
 msgstr "La requête de pull %s a été supprimée avec succès"
 
 #: kallithea/controllers/changeset.py:319 kallithea/controllers/files.py:89
-#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:700
+#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:697
 msgid "Such revision does not exist for this repository"
 msgstr "Une telle révision n'existe pas pour ce dépôt"
 
@@ -75,50 +75,50 @@
 msgid "Cannot compare repositories of different types"
 msgstr "Impossible de comparer des dépôts de types différents"
 
-#: kallithea/controllers/compare.py:246
+#: kallithea/controllers/compare.py:247
 msgid "Cannot show empty diff"
 msgstr "Impossible d'afficher un diff vide"
 
-#: kallithea/controllers/compare.py:248
+#: kallithea/controllers/compare.py:249
 msgid "No ancestor found for merge diff"
 msgstr "Aucun ancêtre trouvé pour le diff de fusion"
 
-#: kallithea/controllers/compare.py:252
+#: kallithea/controllers/compare.py:253
 msgid "Multiple merge ancestors found for merge compare"
 msgstr "Plusieurs ancêtres de fusion trouvés pour la comparaison de fusion"
 
-#: kallithea/controllers/compare.py:268
+#: kallithea/controllers/compare.py:269
 msgid "Cannot compare repositories without using common ancestor"
 msgstr "Impossible de comparer des dépôts sans utiliser un ancêtre commun"
 
-#: kallithea/controllers/error.py:71
+#: kallithea/controllers/error.py:67
 msgid "No response"
 msgstr "Pas de réponse"
 
-#: kallithea/controllers/error.py:72
+#: kallithea/controllers/error.py:68
 msgid "Unknown error"
 msgstr "Erreur inconnue"
 
-#: kallithea/controllers/error.py:85
+#: kallithea/controllers/error.py:81
 msgid ""
 "The request could not be understood by the server due to malformed syntax."
 msgstr ""
 "Le serveur n’a pas pu interpréter la requête à cause d’une erreur de "
 "syntaxe."
 
-#: kallithea/controllers/error.py:88
+#: kallithea/controllers/error.py:84
 msgid "Unauthorized access to resource"
 msgstr "Accès interdit à cette ressource"
 
-#: kallithea/controllers/error.py:90
+#: kallithea/controllers/error.py:86
 msgid "You don't have permission to view this page"
 msgstr "Vous n’avez pas la permission de voir cette page"
 
-#: kallithea/controllers/error.py:92
+#: kallithea/controllers/error.py:88
 msgid "The resource could not be found"
 msgstr "Ressource introuvable"
 
-#: kallithea/controllers/error.py:94
+#: kallithea/controllers/error.py:90
 msgid ""
 "The server encountered an unexpected condition which prevented it from "
 "fulfilling the request."
@@ -126,14 +126,14 @@
 "La requête n’a pu être traitée en raison d’une erreur survenue sur le "
 "serveur."
 
-#: kallithea/controllers/feed.py:63
+#: kallithea/controllers/feed.py:59
 #, python-format
 msgid "%s committed on %s"
 msgstr "%s a commité, le %s"
 
-#: kallithea/controllers/feed.py:88
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/controllers/feed.py:84
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/compare/compare_diff.html:95
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
@@ -141,12 +141,12 @@
 msgid "Changeset was too big and was cut off..."
 msgstr "Cet ensemble de changements était trop important et a été découpé…"
 
-#: kallithea/controllers/feed.py:111 kallithea/controllers/feed.py:140
+#: kallithea/controllers/feed.py:107
 #, python-format
 msgid "%s %s feed"
 msgstr "Flux %s de %s"
 
-#: kallithea/controllers/feed.py:113 kallithea/controllers/feed.py:142
+#: kallithea/controllers/feed.py:109
 #, python-format
 msgid "Changes on %s repository"
 msgstr "Changements sur le dépôt %s"
@@ -164,109 +164,109 @@
 msgid "%s at %s"
 msgstr "%s à %s"
 
-#: kallithea/controllers/files.py:296
+#: kallithea/controllers/files.py:295
 msgid "You can only delete files with revision being a valid branch"
 msgstr ""
 "Vous ne pouvez supprimer les fichiers que si la révision est une branche "
 "valide"
 
-#: kallithea/controllers/files.py:307
+#: kallithea/controllers/files.py:306
 #, python-format
 msgid "Deleted file %s via Kallithea"
 msgstr "Le fichier %s a été supprimé via Kallithea"
 
-#: kallithea/controllers/files.py:331
+#: kallithea/controllers/files.py:330
 #, python-format
 msgid "Successfully deleted file %s"
 msgstr "Suppression du fichier %s effectuée avec succès"
 
-#: kallithea/controllers/files.py:335 kallithea/controllers/files.py:394
-#: kallithea/controllers/files.py:469
+#: kallithea/controllers/files.py:334 kallithea/controllers/files.py:392
+#: kallithea/controllers/files.py:467
 msgid "Error occurred during commit"
 msgstr "Une erreur est survenue durant le commit"
 
-#: kallithea/controllers/files.py:350
+#: kallithea/controllers/files.py:349
 msgid "You can only edit files with revision being a valid branch"
 msgstr ""
 "Vous ne pouvez modifier les fichiers que si la révision est une branche "
 "valide"
 
-#: kallithea/controllers/files.py:364
+#: kallithea/controllers/files.py:363
 #, python-format
 msgid "Edited file %s via Kallithea"
 msgstr "%s édité via Kallithea"
 
-#: kallithea/controllers/files.py:380
+#: kallithea/controllers/files.py:378
 msgid "No changes"
 msgstr "Aucun changement"
 
-#: kallithea/controllers/files.py:390 kallithea/controllers/files.py:458
+#: kallithea/controllers/files.py:388 kallithea/controllers/files.py:456
 #, python-format
 msgid "Successfully committed to %s"
 msgstr "Commit réalisé avec succès sur %s"
 
-#: kallithea/controllers/files.py:409
+#: kallithea/controllers/files.py:407
 msgid "Added file via Kallithea"
 msgstr "%s ajouté par Kallithea"
 
-#: kallithea/controllers/files.py:430
+#: kallithea/controllers/files.py:428
 msgid "No content"
 msgstr "Aucun contenu"
 
-#: kallithea/controllers/files.py:434
+#: kallithea/controllers/files.py:432
 msgid "No filename"
 msgstr "Aucun nom de fichier"
 
-#: kallithea/controllers/files.py:461
+#: kallithea/controllers/files.py:459
 msgid "Location must be relative path and must not contain .. in path"
 msgstr ""
 "Le chemin doit être un chemin relatif et ne doit pas contenir .. dans le "
 "chemin"
 
-#: kallithea/controllers/files.py:493
+#: kallithea/controllers/files.py:491
 msgid "Downloads disabled"
 msgstr "Les téléchargements sont désactivés"
 
-#: kallithea/controllers/files.py:504
+#: kallithea/controllers/files.py:502
 #, python-format
 msgid "Unknown revision %s"
 msgstr "Révision %s inconnue"
 
-#: kallithea/controllers/files.py:506
+#: kallithea/controllers/files.py:504
 msgid "Empty repository"
 msgstr "Dépôt vide"
 
-#: kallithea/controllers/files.py:508
+#: kallithea/controllers/files.py:506
 msgid "Unknown archive type"
 msgstr "Type d’archive inconnu"
 
-#: kallithea/controllers/files.py:729
+#: kallithea/controllers/files.py:726
 #: kallithea/templates/changeset/changeset_range.html:9
 #: kallithea/templates/email_templates/pull_request.html:64
 #: kallithea/templates/pullrequests/pullrequest.html:84
 msgid "Changesets"
 msgstr "Changesets"
 
-#: kallithea/controllers/files.py:730
-#: kallithea/controllers/pullrequests.py:182 kallithea/model/scm.py:676
+#: kallithea/controllers/files.py:727
+#: kallithea/controllers/pullrequests.py:174 kallithea/model/scm.py:663
 msgid "Branches"
 msgstr "Branches"
 
-#: kallithea/controllers/files.py:731
-#: kallithea/controllers/pullrequests.py:183 kallithea/model/scm.py:687
+#: kallithea/controllers/files.py:728
+#: kallithea/controllers/pullrequests.py:175 kallithea/model/scm.py:674
 msgid "Tags"
 msgstr "Tags"
 
-#: kallithea/controllers/forks.py:174
+#: kallithea/controllers/forks.py:175
 #, python-format
 msgid "An error occurred during repository forking %s"
 msgstr "Une erreur est survenue durant le fork du dépôt %s"
 
-#: kallithea/controllers/home.py:79
+#: kallithea/controllers/home.py:77
 msgid "Groups"
 msgstr "Groupes"
 
-#: kallithea/controllers/home.py:89
+#: kallithea/controllers/home.py:87
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:90
 #: kallithea/templates/admin/repos/repo_add.html:12
 #: kallithea/templates/admin/repos/repo_add.html:16
@@ -278,48 +278,54 @@
 msgid "Repositories"
 msgstr "Dépôts"
 
-#: kallithea/controllers/home.py:122
+#: kallithea/controllers/home.py:119
 #: kallithea/templates/files/files_add.html:32
 #: kallithea/templates/files/files_delete.html:23
 #: kallithea/templates/files/files_edit.html:32
 msgid "Branch"
 msgstr "Branche"
 
-#: kallithea/controllers/home.py:128
+#: kallithea/controllers/home.py:125
 msgid "Closed Branches"
 msgstr "Branches fermées"
 
-#: kallithea/controllers/home.py:134
+#: kallithea/controllers/home.py:131
 msgid "Tag"
 msgstr "Étiquette"
 
-#: kallithea/controllers/home.py:140
+#: kallithea/controllers/home.py:137
 msgid "Bookmark"
 msgstr "Signet"
 
-#: kallithea/controllers/journal.py:112 kallithea/controllers/journal.py:154
+#: kallithea/controllers/journal.py:146 kallithea/controllers/journal.py:157
 #: kallithea/templates/journal/public_journal.html:4
 #: kallithea/templates/journal/public_journal.html:18
 msgid "Public Journal"
 msgstr "Journal public"
 
-#: kallithea/controllers/journal.py:116 kallithea/controllers/journal.py:158
+#: kallithea/controllers/journal.py:150 kallithea/controllers/journal.py:161
 #: kallithea/templates/base/base.html:290
 #: kallithea/templates/journal/journal.html:5
 #: kallithea/templates/journal/journal.html:13
 msgid "Journal"
 msgstr "Historique"
 
-#: kallithea/controllers/login.py:140 kallithea/controllers/login.py:185
+#: kallithea/controllers/login.py:109
+#, fuzzy
+#| msgid "Authentication"
+msgid "Authentication failed."
+msgstr "Authentification"
+
+#: kallithea/controllers/login.py:142 kallithea/controllers/login.py:187
 msgid "Bad captcha"
 msgstr "Mauvais captcha"
 
-#: kallithea/controllers/login.py:146
+#: kallithea/controllers/login.py:148
 #, python-format
 msgid "You have successfully registered with %s"
 msgstr "Vous vous êtes inscrit avec succès avec %s"
 
-#: kallithea/controllers/login.py:190
+#: kallithea/controllers/login.py:192
 msgid "A password reset confirmation code has been sent"
 msgstr ""
 "Un lien de confirmation de réinitialisation de mot de passe a été envoyé"
@@ -333,117 +339,117 @@
 msgid "Successfully updated password"
 msgstr "Mot de passe mis à jour avec succès"
 
-#: kallithea/controllers/pullrequests.py:68
+#: kallithea/controllers/pullrequests.py:67
 #, python-format
 msgid "Invalid reviewer \"%s\" specified"
 msgstr "Reviewer spécifié \"%s\" non valide"
 
-#: kallithea/controllers/pullrequests.py:131
+#: kallithea/controllers/pullrequests.py:123
 #, python-format
 msgid "%s (closed)"
 msgstr "%s (fermé)"
 
-#: kallithea/controllers/pullrequests.py:158
+#: kallithea/controllers/pullrequests.py:150
 #: kallithea/templates/changeset/changeset.html:12
 msgid "Changeset"
 msgstr "Changements"
 
-#: kallithea/controllers/pullrequests.py:179
+#: kallithea/controllers/pullrequests.py:171
 msgid "Special"
 msgstr "Spécial"
 
-#: kallithea/controllers/pullrequests.py:180
+#: kallithea/controllers/pullrequests.py:172
 msgid "Peer branches"
 msgstr "Branches appairées"
 
-#: kallithea/controllers/pullrequests.py:181 kallithea/model/scm.py:682
+#: kallithea/controllers/pullrequests.py:173 kallithea/model/scm.py:669
 msgid "Bookmarks"
 msgstr "Signets"
 
-#: kallithea/controllers/pullrequests.py:318
+#: kallithea/controllers/pullrequests.py:315
 #, python-format
 msgid "Error creating pull request: %s"
 msgstr "Erreur de création de la demande de pull : %s"
 
-#: kallithea/controllers/pullrequests.py:345
-#: kallithea/controllers/pullrequests.py:368
+#: kallithea/controllers/pullrequests.py:342
+#: kallithea/controllers/pullrequests.py:365
 msgid "Error occurred while creating pull request"
 msgstr "Une erreur est survenue durant la création de la pull request"
 
-#: kallithea/controllers/pullrequests.py:350
+#: kallithea/controllers/pullrequests.py:347
 msgid "Successfully opened new pull request"
 msgstr "La requête de pull a été ouverte avec succès"
 
-#: kallithea/controllers/pullrequests.py:373
+#: kallithea/controllers/pullrequests.py:370
 msgid "New pull request iteration created"
 msgstr "Nouvelle itération de requête de pull créée"
 
-#: kallithea/controllers/pullrequests.py:401
+#: kallithea/controllers/pullrequests.py:398
 #, python-format
 msgid "Meanwhile, the following reviewers have been added: %s"
 msgstr "Entretemps, les relecteurs suivants on été ajoutés : %s"
 
-#: kallithea/controllers/pullrequests.py:405
+#: kallithea/controllers/pullrequests.py:402
 #, python-format
 msgid "Meanwhile, the following reviewers have been removed: %s"
 msgstr "Entretemps, les relecteurs suivants ont été supprimés : %s"
 
-#: kallithea/controllers/pullrequests.py:421
-#: kallithea/model/pull_request.py:232
+#: kallithea/controllers/pullrequests.py:418
+#: kallithea/model/pull_request.py:230
 msgid "No description"
 msgstr "Aucune description"
 
-#: kallithea/controllers/pullrequests.py:430
+#: kallithea/controllers/pullrequests.py:427
 msgid "Pull request updated"
 msgstr "Pull request mise à jour"
 
-#: kallithea/controllers/pullrequests.py:443
+#: kallithea/controllers/pullrequests.py:440
 msgid "Successfully deleted pull request"
 msgstr "La requête de pull a été supprimée avec succès"
 
-#: kallithea/controllers/pullrequests.py:479
+#: kallithea/controllers/pullrequests.py:476
 #, python-format
 msgid "Revision %s not found in %s"
 msgstr "Révision %s non trouvée dans %s"
 
-#: kallithea/controllers/pullrequests.py:506
+#: kallithea/controllers/pullrequests.py:504
 #, python-format
 msgid "Error: changesets not found when displaying pull request from %s."
 msgstr ""
 "Erreur : Pas de changeset trouvé lors de l'affichage la requête de pull "
 "de %s."
 
-#: kallithea/controllers/pullrequests.py:520
+#: kallithea/controllers/pullrequests.py:518
 #, python-format
 msgid "This pull request has already been merged to %s."
 msgstr "Cette pull request a déjà été fusionnée à %s."
 
-#: kallithea/controllers/pullrequests.py:522
+#: kallithea/controllers/pullrequests.py:520
 msgid "This pull request has been closed and can not be updated."
 msgstr "Cette pull request a été fermée et ne peut pas être mise à jour."
 
-#: kallithea/controllers/pullrequests.py:546
+#: kallithea/controllers/pullrequests.py:539
 #, python-format
 msgid "The following additional changes are available on %s:"
 msgstr ""
 "Les modifications additionnelles suivantes sont disponibles sur %s :"
 
-#: kallithea/controllers/pullrequests.py:548
-#: kallithea/controllers/pullrequests.py:552
+#: kallithea/controllers/pullrequests.py:541
+#: kallithea/controllers/pullrequests.py:545
 msgid "No additional changesets found for iterating on this pull request."
 msgstr "Pas de changeset additionnel trouvé pour cette requête de pull."
 
-#: kallithea/controllers/pullrequests.py:560
+#: kallithea/controllers/pullrequests.py:553
 #, python-format
 msgid "Note: Branch %s has another head: %s."
 msgstr "Note : La branche %s a une autre tête : %s."
 
-#: kallithea/controllers/pullrequests.py:567
+#: kallithea/controllers/pullrequests.py:560
 msgid "Git pull requests don't support iterating yet."
 msgstr ""
 "Les itérations des requêtes de pull Git ne sont pas encore supportées."
 
-#: kallithea/controllers/pullrequests.py:569
+#: kallithea/controllers/pullrequests.py:562
 #, python-format
 msgid ""
 "Error: some changesets not found when displaying pull request from %s."
@@ -451,119 +457,119 @@
 "Erreur : certains changesets n'ont pas été trouvés lors de l'affichage la "
 "requête de pull depuis %s."
 
-#: kallithea/controllers/pullrequests.py:593
+#: kallithea/controllers/pullrequests.py:586
 msgid "The diff can't be shown - the PR revisions could not be found."
 msgstr ""
 "Le diff ne peut pas être affiché : révisions des requêtes de pull "
 "introuvables."
 
-#: kallithea/controllers/search.py:136
+#: kallithea/controllers/search.py:132
 msgid "Invalid search query. Try quoting it."
 msgstr "Requête invalide. Essayez de la mettre entre guillemets."
 
-#: kallithea/controllers/search.py:140
+#: kallithea/controllers/search.py:136
 msgid "The server has no search index."
 msgstr "Le serveur n'a pas d'index de recherche."
 
-#: kallithea/controllers/search.py:143
+#: kallithea/controllers/search.py:139
 msgid "An error occurred during search operation."
 msgstr "Une erreur est survenue pendant la recherche."
 
-#: kallithea/controllers/summary.py:168
-#: kallithea/templates/summary/summary.html:412
+#: kallithea/controllers/summary.py:169
+#: kallithea/templates/summary/summary.html:410
 msgid "No data ready yet"
 msgstr "Aucune donnée actuellement disponible"
 
-#: kallithea/controllers/summary.py:171
+#: kallithea/controllers/summary.py:172
 #: kallithea/templates/summary/summary.html:97
 msgid "Statistics are disabled for this repository"
 msgstr "La mise à jour des statistiques est désactivée pour ce dépôt"
 
-#: kallithea/controllers/admin/auth_settings.py:137
+#: kallithea/controllers/admin/auth_settings.py:136
 msgid "Auth settings updated successfully"
 msgstr "Mise à jour des paramètres d'authentification effectuée avec succès"
 
-#: kallithea/controllers/admin/auth_settings.py:148
+#: kallithea/controllers/admin/auth_settings.py:147
 msgid "error occurred during update of auth settings"
 msgstr ""
 "une erreur est survenue pendant la mise à jour des réglages "
 "d'authentification"
 
-#: kallithea/controllers/admin/defaults.py:75
+#: kallithea/controllers/admin/defaults.py:74
 msgid "Default settings updated successfully"
 msgstr "Mise à jour des réglages par défaut effectuée avec succès"
 
-#: kallithea/controllers/admin/defaults.py:90
+#: kallithea/controllers/admin/defaults.py:89
 msgid "Error occurred during update of defaults"
 msgstr ""
 "Une erreur est survenue durant la mise à jour des réglages par défaut"
 
 #: kallithea/controllers/admin/gists.py:59
 #: kallithea/controllers/admin/my_account.py:232
-#: kallithea/controllers/admin/users.py:248
+#: kallithea/controllers/admin/users.py:246
 msgid "Forever"
 msgstr "Pour toujours"
 
 #: kallithea/controllers/admin/gists.py:60
 #: kallithea/controllers/admin/my_account.py:233
-#: kallithea/controllers/admin/users.py:249
+#: kallithea/controllers/admin/users.py:247
 msgid "5 minutes"
 msgstr "5 minute"
 
 #: kallithea/controllers/admin/gists.py:61
 #: kallithea/controllers/admin/my_account.py:234
-#: kallithea/controllers/admin/users.py:250
+#: kallithea/controllers/admin/users.py:248
 msgid "1 hour"
 msgstr "1 heure"
 
 #: kallithea/controllers/admin/gists.py:62
 #: kallithea/controllers/admin/my_account.py:235
-#: kallithea/controllers/admin/users.py:251
+#: kallithea/controllers/admin/users.py:249
 msgid "1 day"
 msgstr "1 jour"
 
 #: kallithea/controllers/admin/gists.py:63
 #: kallithea/controllers/admin/my_account.py:236
-#: kallithea/controllers/admin/users.py:252
+#: kallithea/controllers/admin/users.py:250
 msgid "1 month"
 msgstr "1 mois"
 
 #: kallithea/controllers/admin/gists.py:67
 #: kallithea/controllers/admin/my_account.py:238
-#: kallithea/controllers/admin/users.py:254
+#: kallithea/controllers/admin/users.py:252
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:65
 #: kallithea/templates/admin/users/user_edit_api_keys.html:65
 msgid "Lifetime"
 msgstr "Toujours"
 
-#: kallithea/controllers/admin/gists.py:142
+#: kallithea/controllers/admin/gists.py:148
 msgid "Error occurred during gist creation"
 msgstr "Une erreur est survenue lors de la création du gist"
 
-#: kallithea/controllers/admin/gists.py:158
+#: kallithea/controllers/admin/gists.py:164
 #, python-format
 msgid "Deleted gist %s"
 msgstr "Gist %s supprimé"
 
-#: kallithea/controllers/admin/gists.py:198
+#: kallithea/controllers/admin/gists.py:207
 msgid "Unmodified"
 msgstr "Non modifié"
 
-#: kallithea/controllers/admin/gists.py:228
+#: kallithea/controllers/admin/gists.py:237
 msgid "Successfully updated gist content"
 msgstr "Le contenu du gist a été mis à jour avec succès"
 
-#: kallithea/controllers/admin/gists.py:233
+#: kallithea/controllers/admin/gists.py:242
 msgid "Successfully updated gist data"
 msgstr "Les données du gist on été mises à jour avec succès"
 
-#: kallithea/controllers/admin/gists.py:236
+#: kallithea/controllers/admin/gists.py:245
 #, python-format
 msgid "Error occurred during update of gist %s"
 msgstr "Une erreur est survenue durant la mise à jour du gist %s"
 
-#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:211
-#: kallithea/model/user.py:232
+#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:205
+#: kallithea/model/user.py:226
 msgid "You can't edit this user since it's crucial for entire application"
 msgstr ""
 "Vous ne pouvez pas éditer cet utilisateur ; il est nécessaire pour le bon "
@@ -574,7 +580,7 @@
 msgstr "Votre compte a été mis à jour avec succès"
 
 #: kallithea/controllers/admin/my_account.py:134
-#: kallithea/controllers/admin/users.py:181
+#: kallithea/controllers/admin/users.py:179
 #, python-format
 msgid "Error occurred during update of user %s"
 msgstr "Une erreur est survenue durant la mise à jour de l'utilisateur %s"
@@ -586,44 +592,44 @@
 "l'utilisateur"
 
 #: kallithea/controllers/admin/my_account.py:209
-#: kallithea/controllers/admin/users.py:367
+#: kallithea/controllers/admin/users.py:365
 #, python-format
 msgid "Added email %s to user"
 msgstr "L’e-mail « %s » a été ajouté à l’utilisateur"
 
 #: kallithea/controllers/admin/my_account.py:215
-#: kallithea/controllers/admin/users.py:373
+#: kallithea/controllers/admin/users.py:371
 msgid "An error occurred during email saving"
 msgstr "Une erreur est survenue durant l’enregistrement de l’e-mail"
 
 #: kallithea/controllers/admin/my_account.py:224
-#: kallithea/controllers/admin/users.py:383
+#: kallithea/controllers/admin/users.py:381
 msgid "Removed email from user"
 msgstr "L’e-mail a été enlevé de l’utilisateur"
 
 #: kallithea/controllers/admin/my_account.py:248
-#: kallithea/controllers/admin/users.py:271
+#: kallithea/controllers/admin/users.py:269
 msgid "API key successfully created"
 msgstr "Clé d'API créée avec succès"
 
 #: kallithea/controllers/admin/my_account.py:257
-#: kallithea/controllers/admin/users.py:281
+#: kallithea/controllers/admin/users.py:279
 msgid "API key successfully reset"
 msgstr "Clé d'API remise à zéro avec succès"
 
 #: kallithea/controllers/admin/my_account.py:261
-#: kallithea/controllers/admin/users.py:285
+#: kallithea/controllers/admin/users.py:283
 msgid "API key successfully deleted"
 msgstr "Clé d'API supprimée avec succès"
 
 #: kallithea/controllers/admin/my_account.py:281
-#: kallithea/controllers/admin/users.py:456
+#: kallithea/controllers/admin/users.py:454
 #, python-format
 msgid "SSH key %s successfully added"
 msgstr "Clé SSH %s ajoutée avec succès"
 
 #: kallithea/controllers/admin/my_account.py:293
-#: kallithea/controllers/admin/users.py:470
+#: kallithea/controllers/admin/users.py:468
 msgid "SSH key successfully deleted"
 msgstr "Clé SSH supprimée avec succès"
 
@@ -699,11 +705,11 @@
 msgid "Allowed with automatic account activation"
 msgstr "Autorisé avec activation de compte automatique"
 
-#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1673
+#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1578
 msgid "Manual activation of external account"
 msgstr "Activation manuelle du compte externe"
 
-#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1674
+#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1579
 msgid "Automatic activation of external account"
 msgstr "Activation automatique du compte externe"
 
@@ -725,190 +731,182 @@
 msgid "Error occurred during update of permissions"
 msgstr "Une erreur est survenue durant la mise à jour des permissions"
 
-#: kallithea/controllers/admin/repo_groups.py:172
+#: kallithea/controllers/admin/repo_groups.py:165
 #, python-format
 msgid "Error occurred during creation of repository group %s"
 msgstr "Une erreur est survenue durant la création du groupe de dépôts %s"
 
-#: kallithea/controllers/admin/repo_groups.py:177
+#: kallithea/controllers/admin/repo_groups.py:172
 #, python-format
 msgid "Created repository group %s"
 msgstr "Groupe de dépôts %s créé"
 
-#: kallithea/controllers/admin/repo_groups.py:224
+#: kallithea/controllers/admin/repo_groups.py:219
 #, python-format
 msgid "Updated repository group %s"
 msgstr "Groupe de dépôts %s mis à jour"
 
-#: kallithea/controllers/admin/repo_groups.py:240
+#: kallithea/controllers/admin/repo_groups.py:235
 #, python-format
 msgid "Error occurred during update of repository group %s"
 msgstr ""
 "Une erreur est survenue durant la mise à jour du groupe de dépôts %s"
 
-#: kallithea/controllers/admin/repo_groups.py:250
+#: kallithea/controllers/admin/repo_groups.py:245
 #, python-format
 msgid "This group contains %s repositories and cannot be deleted"
 msgstr "Ce groupe contient %s dépôts et ne peut être supprimé"
 
-#: kallithea/controllers/admin/repo_groups.py:257
+#: kallithea/controllers/admin/repo_groups.py:252
 #, python-format
 msgid "This group contains %s subgroups and cannot be deleted"
 msgstr "Ce groupe contient %s sous-groupes et ne peut pas être supprimé"
 
-#: kallithea/controllers/admin/repo_groups.py:263
+#: kallithea/controllers/admin/repo_groups.py:258
 #, python-format
 msgid "Removed repository group %s"
 msgstr "Groupe de dépôts %s supprimé"
 
-#: kallithea/controllers/admin/repo_groups.py:268
+#: kallithea/controllers/admin/repo_groups.py:263
 #, python-format
 msgid "Error occurred during deletion of repository group %s"
 msgstr ""
 "Une erreur est survenue durant la suppression du groupe de dépôts %s"
 
-#: kallithea/controllers/admin/repo_groups.py:352
-#: kallithea/controllers/admin/repo_groups.py:382
-#: kallithea/controllers/admin/user_groups.py:294
+#: kallithea/controllers/admin/repo_groups.py:347
+#: kallithea/controllers/admin/repo_groups.py:377
+#: kallithea/controllers/admin/user_groups.py:290
 msgid "Cannot revoke permission for yourself as admin"
 msgstr "Impossible de révoquer votre permission d'administrateur"
 
-#: kallithea/controllers/admin/repo_groups.py:367
+#: kallithea/controllers/admin/repo_groups.py:362
 msgid "Repository group permissions updated"
 msgstr "Permissions du groupe de dépôts mises à jour"
 
-#: kallithea/controllers/admin/repo_groups.py:399
-#: kallithea/controllers/admin/repos.py:358
-#: kallithea/controllers/admin/user_groups.py:306
+#: kallithea/controllers/admin/repo_groups.py:394
+#: kallithea/controllers/admin/repos.py:357
+#: kallithea/controllers/admin/user_groups.py:302
 msgid "An error occurred during revoking of permission"
 msgstr "Une erreur est survenue durant la révocation de la permission"
 
-#: kallithea/controllers/admin/repos.py:136
+#: kallithea/controllers/admin/repos.py:137
 #, python-format
 msgid "Error creating repository %s"
 msgstr "Erreur de création du dépôt %s"
 
-#: kallithea/controllers/admin/repos.py:194
+#: kallithea/controllers/admin/repos.py:193
 #, python-format
 msgid "Created repository %s from %s"
 msgstr "Dépôt %s créé depuis %s"
 
-#: kallithea/controllers/admin/repos.py:203
+#: kallithea/controllers/admin/repos.py:202
 #, python-format
 msgid "Forked repository %s as %s"
 msgstr "dépôt %s forké en tant que %s"
 
-#: kallithea/controllers/admin/repos.py:206
+#: kallithea/controllers/admin/repos.py:205
 #, python-format
 msgid "Created repository %s"
 msgstr "Dépôt %s créé"
 
-#: kallithea/controllers/admin/repos.py:235
+#: kallithea/controllers/admin/repos.py:234
 #, python-format
 msgid "Repository %s updated successfully"
 msgstr "Dépôt %s mis à jour avec succès"
 
-#: kallithea/controllers/admin/repos.py:255
+#: kallithea/controllers/admin/repos.py:254
 #, python-format
 msgid "Error occurred during update of repository %s"
 msgstr "Une erreur est survenue durant la mise à jour du dépôt %s"
 
-#: kallithea/controllers/admin/repos.py:273
+#: kallithea/controllers/admin/repos.py:272
 #, python-format
 msgid "Detached %s forks"
 msgstr "%s forks détachés"
 
-#: kallithea/controllers/admin/repos.py:276
+#: kallithea/controllers/admin/repos.py:275
 #, python-format
 msgid "Deleted %s forks"
 msgstr "%s forks supprimés"
 
-#: kallithea/controllers/admin/repos.py:281
+#: kallithea/controllers/admin/repos.py:280
 #, python-format
 msgid "Deleted repository %s"
 msgstr "Dépôt %s supprimé"
 
-#: kallithea/controllers/admin/repos.py:284
+#: kallithea/controllers/admin/repos.py:283
 #, python-format
 msgid "Cannot delete repository %s which still has forks"
 msgstr "Impossible de supprimer le dépôt %s : des forks y sont attachés"
 
-#: kallithea/controllers/admin/repos.py:289
+#: kallithea/controllers/admin/repos.py:288
 #, python-format
 msgid "An error occurred during deletion of %s"
 msgstr "Erreur pendant la suppression de %s"
 
-#: kallithea/controllers/admin/repos.py:329
+#: kallithea/controllers/admin/repos.py:328
 msgid "Repository permissions updated"
 msgstr "Permissions du dépôt mises à jour"
 
-#: kallithea/controllers/admin/repos.py:388
+#: kallithea/controllers/admin/repos.py:387
 #, python-format
 msgid "Field validation error: %s"
 msgstr "Erreur de validation du champ : %s"
 
-#: kallithea/controllers/admin/repos.py:391
+#: kallithea/controllers/admin/repos.py:390
 #, python-format
 msgid "An error occurred during creation of field: %r"
 msgstr "Une erreur est survenue durant la création du champ : %r"
 
-#: kallithea/controllers/admin/repos.py:402
+#: kallithea/controllers/admin/repos.py:401
 msgid "An error occurred during removal of field"
 msgstr "Une erreur est survenue durant la suppression du champ"
 
-#: kallithea/controllers/admin/repos.py:416
+#: kallithea/controllers/admin/repos.py:415
 msgid "-- Not a fork --"
 msgstr "-- Pas un fork --"
 
-#: kallithea/controllers/admin/repos.py:448
+#: kallithea/controllers/admin/repos.py:447
 msgid "Updated repository visibility in public journal"
 msgstr "La visibilité du dépôt dans le journal public a été mise à jour"
 
-#: kallithea/controllers/admin/repos.py:452
+#: kallithea/controllers/admin/repos.py:451
 msgid "An error occurred during setting this repository in public journal"
 msgstr ""
 "Une erreur est survenue durant la configuration du journal public pour ce "
 "dépôt"
 
-#: kallithea/controllers/admin/repos.py:468
+#: kallithea/controllers/admin/repos.py:467
 msgid "Nothing"
 msgstr "[Aucun dépôt]"
 
-#: kallithea/controllers/admin/repos.py:470
+#: kallithea/controllers/admin/repos.py:469
 #, python-format
 msgid "Marked repository %s as fork of %s"
 msgstr "Le dépôt %s a été marké comme fork de %s"
 
-#: kallithea/controllers/admin/repos.py:477
+#: kallithea/controllers/admin/repos.py:476
 msgid "An error occurred during this operation"
 msgstr "Une erreur est survenue durant cette opération"
 
-#: kallithea/controllers/admin/repos.py:490
-msgid "Cache invalidation successful"
-msgstr "Invalidation du cache réalisée avec succès"
-
-#: kallithea/controllers/admin/repos.py:494
-msgid "An error occurred during cache invalidation"
-msgstr "Une erreur est survenue durant l’invalidation du cache"
-
-#: kallithea/controllers/admin/repos.py:507
+#: kallithea/controllers/admin/repos.py:488
 msgid "Pulled from remote location"
 msgstr "Les changements distants ont été récupérés"
 
-#: kallithea/controllers/admin/repos.py:510
+#: kallithea/controllers/admin/repos.py:491
 msgid "An error occurred during pull from remote location"
 msgstr "Une erreur est survenue durant le pull depuis la source distante"
 
-#: kallithea/controllers/admin/repos.py:541
+#: kallithea/controllers/admin/repos.py:522
 msgid "An error occurred during deletion of repository stats"
 msgstr ""
 "Une erreur est survenue durant la suppression des statistiques du dépôt"
 
-#: kallithea/controllers/admin/settings.py:131
+#: kallithea/controllers/admin/settings.py:132
 msgid "Updated VCS settings"
 msgstr "Réglages des gestionnaires de versions mis à jour"
 
-#: kallithea/controllers/admin/settings.py:135 kallithea/lib/utils.py:237
+#: kallithea/controllers/admin/settings.py:136
 msgid ""
 "Unable to activate hgsubversion support. The \"hgsubversion\" library is "
 "missing"
@@ -916,165 +914,165 @@
 "Impossible d'activer la prise en charge de hgsubversion. La bibliothèque "
 "« hgsubversion » est manquante"
 
-#: kallithea/controllers/admin/settings.py:141
-#: kallithea/controllers/admin/settings.py:233
+#: kallithea/controllers/admin/settings.py:142
+#: kallithea/controllers/admin/settings.py:234
 msgid "Error occurred while updating application settings"
 msgstr ""
 "Une erreur est survenue durant la mise à jour des réglages de "
 "l'application"
 
-#: kallithea/controllers/admin/settings.py:176
+#: kallithea/controllers/admin/settings.py:177
 #, python-format
 msgid "Repositories successfully rescanned. Added: %s. Removed: %s."
 msgstr "Dépôts ré-analysés avec succès. Ajouté : %s. Supprimé : %s."
 
-#: kallithea/controllers/admin/settings.py:188
+#: kallithea/controllers/admin/settings.py:189
 #, python-format
 msgid "Invalidated %s repositories"
 msgstr "%s dépôts invalidés"
 
-#: kallithea/controllers/admin/settings.py:229
+#: kallithea/controllers/admin/settings.py:230
 msgid "Updated application settings"
 msgstr "Réglages mis à jour"
 
-#: kallithea/controllers/admin/settings.py:283
+#: kallithea/controllers/admin/settings.py:284
 msgid "Updated visualisation settings"
 msgstr "Réglages d’affichage mis à jour"
 
-#: kallithea/controllers/admin/settings.py:288
+#: kallithea/controllers/admin/settings.py:289
 msgid "Error occurred during updating visualisation settings"
 msgstr ""
 "Une erreur est survenue durant la mise à jour des réglages de "
 "visualisation"
 
-#: kallithea/controllers/admin/settings.py:312
+#: kallithea/controllers/admin/settings.py:313
 msgid "Please enter email address"
 msgstr "Veuillez entrer votre adresse e-mail"
 
-#: kallithea/controllers/admin/settings.py:327
+#: kallithea/controllers/admin/settings.py:328
 msgid "Send email task created"
 msgstr "Tâche d'envoi d'e-mail créée"
 
-#: kallithea/controllers/admin/settings.py:355
+#: kallithea/controllers/admin/settings.py:356
 msgid "Hook already exists"
 msgstr "Le hook existe déjà"
 
-#: kallithea/controllers/admin/settings.py:357
+#: kallithea/controllers/admin/settings.py:358
 msgid "Builtin hooks are read-only. Please use another hook name."
 msgstr ""
 "Les hooks intégrés sont en lecture seule. Merci de choisir un autre nom "
 "pour le hook."
 
-#: kallithea/controllers/admin/settings.py:360
+#: kallithea/controllers/admin/settings.py:361
 msgid "Added new hook"
 msgstr "Le nouveau hook a été ajouté"
 
-#: kallithea/controllers/admin/settings.py:376
+#: kallithea/controllers/admin/settings.py:377
 msgid "Updated hooks"
 msgstr "Hooks mis à jour"
 
-#: kallithea/controllers/admin/settings.py:380
+#: kallithea/controllers/admin/settings.py:381
 msgid "Error occurred during hook creation"
 msgstr "Une erreur est survenue durant la création du hook"
 
-#: kallithea/controllers/admin/settings.py:404
+#: kallithea/controllers/admin/settings.py:405
 msgid "Whoosh reindex task scheduled"
 msgstr "La tâche de réindexation Whoosh a été planifiée"
 
-#: kallithea/controllers/admin/user_groups.py:138
+#: kallithea/controllers/admin/user_groups.py:134
 #, python-format
 msgid "Created user group %s"
 msgstr "Groupe d'utilisateurs %s créé"
 
-#: kallithea/controllers/admin/user_groups.py:151
+#: kallithea/controllers/admin/user_groups.py:147
 #, python-format
 msgid "Error occurred during creation of user group %s"
 msgstr ""
 "Une erreur est survenue durant la création du groupe d'utilisateurs %s"
 
-#: kallithea/controllers/admin/user_groups.py:179
+#: kallithea/controllers/admin/user_groups.py:175
 #, python-format
 msgid "Updated user group %s"
 msgstr "Groupe d'utilisateurs %s mis à jour"
 
-#: kallithea/controllers/admin/user_groups.py:201
+#: kallithea/controllers/admin/user_groups.py:197
 #, python-format
 msgid "Error occurred during update of user group %s"
 msgstr ""
 "Une erreur est survenue durant la mise à jour du groupe d'utilisateurs %s"
 
-#: kallithea/controllers/admin/user_groups.py:212
+#: kallithea/controllers/admin/user_groups.py:208
 msgid "Successfully deleted user group"
 msgstr "Groupe d'utilisateurs supprimé avec succès"
 
-#: kallithea/controllers/admin/user_groups.py:217
+#: kallithea/controllers/admin/user_groups.py:213
 msgid "An error occurred during deletion of user group"
 msgstr ""
 "Une erreur est survenue durant la suppression du groupe d'utilisateurs"
 
-#: kallithea/controllers/admin/user_groups.py:273
+#: kallithea/controllers/admin/user_groups.py:269
 msgid "Target group cannot be the same"
 msgstr "Le groupe cible ne peut pas être le même"
 
-#: kallithea/controllers/admin/user_groups.py:279
+#: kallithea/controllers/admin/user_groups.py:275
 msgid "User group permissions updated"
 msgstr "Permissions du groupe d'utilisateurs mises à jour"
 
+#: kallithea/controllers/admin/user_groups.py:384
+#: kallithea/controllers/admin/users.py:336
+msgid "Updated permissions"
+msgstr "Permissions mises à jour"
+
 #: kallithea/controllers/admin/user_groups.py:388
-#: kallithea/controllers/admin/users.py:338
-msgid "Updated permissions"
-msgstr "Permissions mises à jour"
-
-#: kallithea/controllers/admin/user_groups.py:392
-#: kallithea/controllers/admin/users.py:342
+#: kallithea/controllers/admin/users.py:340
 msgid "An error occurred during permissions saving"
 msgstr "Une erreur est survenue durant l’enregistrement des permissions"
 
-#: kallithea/controllers/admin/users.py:123
+#: kallithea/controllers/admin/users.py:121
 #, python-format
 msgid "Created user %s"
 msgstr "Utilisateur %s créé"
 
-#: kallithea/controllers/admin/users.py:138
+#: kallithea/controllers/admin/users.py:136
 #, python-format
 msgid "Error occurred during creation of user %s"
 msgstr "Une erreur est survenue durant la création de l'utilisateur %s"
 
-#: kallithea/controllers/admin/users.py:162
+#: kallithea/controllers/admin/users.py:160
 msgid "User updated successfully"
 msgstr "L’utilisateur a été mis à jour avec succès"
 
-#: kallithea/controllers/admin/users.py:190
+#: kallithea/controllers/admin/users.py:188
 msgid "Successfully deleted user"
 msgstr "Utilisateur supprimé avec succès"
 
-#: kallithea/controllers/admin/users.py:195
+#: kallithea/controllers/admin/users.py:193
 msgid "An error occurred during deletion of user"
 msgstr "Une erreur est survenue durant la suppression de l’utilisateur"
 
-#: kallithea/controllers/admin/users.py:203
+#: kallithea/controllers/admin/users.py:201
 msgid "The default user cannot be edited"
 msgstr "L'utilisateur par défaut ne peut pas être modifié"
 
-#: kallithea/controllers/admin/users.py:409
+#: kallithea/controllers/admin/users.py:407
 #, python-format
 msgid "Added IP address %s to user whitelist"
 msgstr "L'adresse IP %s a été ajoutée à la liste blanche"
 
-#: kallithea/controllers/admin/users.py:415
+#: kallithea/controllers/admin/users.py:413
 msgid "An error occurred while adding IP address"
 msgstr "Une erreur est survenue durant la sauvegarde d'IP"
 
-#: kallithea/controllers/admin/users.py:427
+#: kallithea/controllers/admin/users.py:425
 msgid "Removed IP address from user whitelist"
 msgstr "L'adresse IP a été supprimée de la liste blanche"
 
-#: kallithea/lib/auth.py:684
+#: kallithea/lib/auth.py:634
 msgid "You need to be a registered user to perform this action"
 msgstr ""
 "Vous devez être un utilisateur enregistré pour effectuer cette action"
 
-#: kallithea/lib/auth.py:712
+#: kallithea/lib/auth.py:662
 msgid "You need to be signed in to view this page"
 msgstr "Vous devez être connecté pour visualiser cette page"
 
@@ -1109,170 +1107,170 @@
 "Cet ensemble de changements était trop gros pour être affiché et a été "
 "découpé, utilisez le menu « diff » pour afficher les différences"
 
-#: kallithea/lib/diffs.py:224
+#: kallithea/lib/diffs.py:223
 msgid "No changes detected"
 msgstr "Aucun changement détecté"
 
-#: kallithea/lib/helpers.py:653
+#: kallithea/lib/helpers.py:670
 #, python-format
 msgid "Deleted branch: %s"
 msgstr "Branche supprimée : %s"
 
-#: kallithea/lib/helpers.py:655
+#: kallithea/lib/helpers.py:672
 #, python-format
 msgid "Created tag: %s"
 msgstr "Étiquette créée : %s"
 
-#: kallithea/lib/helpers.py:666
+#: kallithea/lib/helpers.py:683
 #, python-format
 msgid "Changeset %s not found"
 msgstr "Ensemble de changements %s non trouvé"
 
-#: kallithea/lib/helpers.py:715
+#: kallithea/lib/helpers.py:732
 #, python-format
 msgid "Show all combined changesets %s->%s"
 msgstr "Afficher les changements combinés %s->%s"
 
-#: kallithea/lib/helpers.py:721
+#: kallithea/lib/helpers.py:738
 msgid "Compare view"
 msgstr "Vue de comparaison"
 
-#: kallithea/lib/helpers.py:740
+#: kallithea/lib/helpers.py:757
 msgid "and"
 msgstr "et"
 
-#: kallithea/lib/helpers.py:741
+#: kallithea/lib/helpers.py:758
 #, python-format
 msgid "%s more"
 msgstr "%s de plus"
 
-#: kallithea/lib/helpers.py:742
+#: kallithea/lib/helpers.py:759
 #: kallithea/templates/changelog/changelog.html:43
 msgid "revisions"
 msgstr "révisions"
 
-#: kallithea/lib/helpers.py:766
+#: kallithea/lib/helpers.py:783
 #, python-format
 msgid "Fork name %s"
 msgstr "Nom du fork %s"
 
-#: kallithea/lib/helpers.py:787
+#: kallithea/lib/helpers.py:804
 #, python-format
 msgid "Pull request %s"
 msgstr "Requête de pull %s"
 
-#: kallithea/lib/helpers.py:797
+#: kallithea/lib/helpers.py:814
 msgid "[deleted] repository"
 msgstr "[a supprimé] le dépôt"
 
-#: kallithea/lib/helpers.py:799 kallithea/lib/helpers.py:811
+#: kallithea/lib/helpers.py:816 kallithea/lib/helpers.py:828
 msgid "[created] repository"
 msgstr "[a créé] le dépôt"
 
-#: kallithea/lib/helpers.py:801
+#: kallithea/lib/helpers.py:818
 msgid "[created] repository as fork"
 msgstr "[a créé] le dépôt en tant que fork"
 
-#: kallithea/lib/helpers.py:803 kallithea/lib/helpers.py:813
+#: kallithea/lib/helpers.py:820 kallithea/lib/helpers.py:830
 msgid "[forked] repository"
 msgstr "[a forké] le dépôt"
 
-#: kallithea/lib/helpers.py:805 kallithea/lib/helpers.py:815
+#: kallithea/lib/helpers.py:822 kallithea/lib/helpers.py:832
 msgid "[updated] repository"
 msgstr "[a mis à jour] le dépôt"
 
-#: kallithea/lib/helpers.py:807
+#: kallithea/lib/helpers.py:824
 msgid "[downloaded] archive from repository"
 msgstr "[téléchargée] archive depuis le dépôt"
 
-#: kallithea/lib/helpers.py:809
+#: kallithea/lib/helpers.py:826
 msgid "[delete] repository"
 msgstr "[a supprimé] le dépôt"
 
-#: kallithea/lib/helpers.py:817
+#: kallithea/lib/helpers.py:834
 msgid "[created] user"
 msgstr "[a créé] l’utilisateur"
 
-#: kallithea/lib/helpers.py:819
+#: kallithea/lib/helpers.py:836
 msgid "[updated] user"
 msgstr "[a mis à jour] l’utilisateur"
 
-#: kallithea/lib/helpers.py:821
+#: kallithea/lib/helpers.py:838
 msgid "[created] user group"
 msgstr "[créé] groupe d'utilisateurs"
 
-#: kallithea/lib/helpers.py:823
+#: kallithea/lib/helpers.py:840
 msgid "[updated] user group"
 msgstr "[mis à jour] groupe d'utilisateurs"
 
-#: kallithea/lib/helpers.py:825
+#: kallithea/lib/helpers.py:842
 msgid "[commented] on revision in repository"
 msgstr "[a commenté] une révision du dépôt"
 
-#: kallithea/lib/helpers.py:827
+#: kallithea/lib/helpers.py:844
 msgid "[commented] on pull request for"
 msgstr "[a commenté] la requête de pull pour"
 
-#: kallithea/lib/helpers.py:829
+#: kallithea/lib/helpers.py:846
 msgid "[closed] pull request for"
 msgstr "[a fermé] la requête de pull de"
 
-#: kallithea/lib/helpers.py:831
+#: kallithea/lib/helpers.py:848
 msgid "[pushed] into"
 msgstr "[a pushé] dans"
 
-#: kallithea/lib/helpers.py:833
+#: kallithea/lib/helpers.py:850
 msgid "[committed via Kallithea] into repository"
 msgstr "[a commité via Kallithea] dans le dépôt"
 
-#: kallithea/lib/helpers.py:835
+#: kallithea/lib/helpers.py:852
 msgid "[pulled from remote] into repository"
 msgstr "[a pullé depuis un site distant] dans le dépôt"
 
-#: kallithea/lib/helpers.py:837
+#: kallithea/lib/helpers.py:854
 msgid "[pulled] from"
 msgstr "[a pullé] depuis"
 
-#: kallithea/lib/helpers.py:839
+#: kallithea/lib/helpers.py:856
 msgid "[started following] repository"
 msgstr "[suit maintenant] le dépôt"
 
-#: kallithea/lib/helpers.py:841
+#: kallithea/lib/helpers.py:858
 msgid "[stopped following] repository"
 msgstr "[ne suit plus] le dépôt"
 
-#: kallithea/lib/helpers.py:961
+#: kallithea/lib/helpers.py:975
 #, python-format
 msgid " and %s more"
 msgstr " et %s de plus"
 
-#: kallithea/lib/helpers.py:965
+#: kallithea/lib/helpers.py:979
 #: kallithea/templates/compare/compare_diff.html:69
 #: kallithea/templates/pullrequests/pullrequest_show.html:297
 msgid "No files"
 msgstr "Aucun fichier"
 
-#: kallithea/lib/helpers.py:990
+#: kallithea/lib/helpers.py:1004
 msgid "new file"
 msgstr "nouveau fichier"
 
-#: kallithea/lib/helpers.py:993
+#: kallithea/lib/helpers.py:1007
 msgid "mod"
 msgstr "mod"
 
-#: kallithea/lib/helpers.py:996
+#: kallithea/lib/helpers.py:1010
 msgid "del"
 msgstr "suppr."
 
-#: kallithea/lib/helpers.py:999
+#: kallithea/lib/helpers.py:1013
 msgid "rename"
 msgstr "renommer"
 
-#: kallithea/lib/helpers.py:1004
+#: kallithea/lib/helpers.py:1018
 msgid "chmod"
 msgstr "chmod"
 
-#: kallithea/lib/helpers.py:1297
+#: kallithea/lib/helpers.py:1323
 #, python-format
 msgid ""
 "%s repository is not mapped to db perhaps it was created or renamed from "
@@ -1283,100 +1281,104 @@
 "probablement été créé ou renommé manuellement. Veuillez relancer "
 "l’application pour rescanner les dépôts"
 
-#: kallithea/lib/ssh.py:73
+#: kallithea/lib/ssh.py:75
 msgid "SSH key is missing"
-msgstr "La clé SSH est manquante"
-
-#: kallithea/lib/ssh.py:77
-#, fuzzy
-msgid "Incorrect SSH key - it must have both a key type and a base64 part"
+msgstr "La clé SSH est absente"
+
+#: kallithea/lib/ssh.py:79
+msgid ""
+"Incorrect SSH key - it must have both a key type and a base64 part, like "
+"'ssh-rsa ASRNeaZu4FA...xlJp='"
 msgstr ""
-"Clé SSH incorrect – elle doit avoir à la fois le type de clé et une partie "
-"base64"
-
-#: kallithea/lib/ssh.py:81
+"Clé SSH incorrecte - elle doit comporter à la fois un type de clé et une "
+"partie en base64, comme 'ssh-rsa ASRNeaZu4FA...xlJp='"
+
+#: kallithea/lib/ssh.py:83
 msgid "Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'"
-msgstr "Clé SSH incorrecte – elle dpit commencer par « ssh-(rsa|dss|ed25519) »"
-
-#: kallithea/lib/ssh.py:84
+msgstr ""
+"Clé SSH incorrecte - elle doit commencer par 'ssh-(rsa|dss|ed25519)'"
+
+#: kallithea/lib/ssh.py:86
 #, python-format
 msgid "Incorrect SSH key - unexpected characters in base64 part %r"
-msgstr "Clé SSH incorrecte – caractères inattendus dans la partie base64 %r"
-
-#: kallithea/lib/ssh.py:89
+msgstr ""
+"Clé SSH incorrecte - caractères invalide dans la partie en base 64 %r"
+
+#: kallithea/lib/ssh.py:91
 #, python-format
 msgid "Incorrect SSH key - failed to decode base64 part %r"
-msgstr "Clé SSH incorrecte – échec du décodage de la partie base64 %r"
-
-#: kallithea/lib/ssh.py:92
+msgstr "Clé SSH incorrecte - échec au décodage de la partie en base64 %r"
+
+#: kallithea/lib/ssh.py:94
 #, python-format
 msgid "Incorrect SSH key - base64 part is not %r as claimed but %r"
 msgstr ""
-"Clé SSH incorrecte – la partie base64 n'est pas %r comme il est dit mais %r"
-
-#: kallithea/lib/utils2.py:334
+"Clé SSH incorrecte - la partie en base 64 n'est pas %r comme attendu mais "
+"%r"
+
+#: kallithea/lib/utils2.py:253
 #, python-format
 msgid "%d year"
 msgid_plural "%d years"
 msgstr[0] "%d an"
 msgstr[1] "%d ans"
 
-#: kallithea/lib/utils2.py:335
+#: kallithea/lib/utils2.py:254
 #, python-format
 msgid "%d month"
 msgid_plural "%d months"
 msgstr[0] "%d mois"
 msgstr[1] "%d mois"
 
-#: kallithea/lib/utils2.py:336
+#: kallithea/lib/utils2.py:255
 #, python-format
 msgid "%d day"
 msgid_plural "%d days"
 msgstr[0] "%d jour"
 msgstr[1] "%d jours"
 
-#: kallithea/lib/utils2.py:337
+#: kallithea/lib/utils2.py:256
 #, python-format
 msgid "%d hour"
 msgid_plural "%d hours"
 msgstr[0] "%d heure"
 msgstr[1] "%d heures"
 
-#: kallithea/lib/utils2.py:338
+#: kallithea/lib/utils2.py:257
 #, python-format
 msgid "%d minute"
 msgid_plural "%d minutes"
 msgstr[0] "%d minute"
 msgstr[1] "%d minutes"
 
-#: kallithea/lib/utils2.py:339
+#: kallithea/lib/utils2.py:258
 #, python-format
 msgid "%d second"
 msgid_plural "%d seconds"
 msgstr[0] "%d seconde"
 msgstr[1] "%d secondes"
 
-#: kallithea/lib/utils2.py:355
+#: kallithea/lib/utils2.py:274
 #, python-format
 msgid "in %s"
 msgstr "dans %s"
 
-#: kallithea/lib/utils2.py:357
+#: kallithea/lib/utils2.py:276
 #, python-format
 msgid "%s ago"
 msgstr "Il y a %s"
 
-#: kallithea/lib/utils2.py:359
+#: kallithea/lib/utils2.py:278
 #, python-format
 msgid "in %s and %s"
 msgstr "dans %s et %s"
 
-#: kallithea/lib/utils2.py:362
+#: kallithea/lib/utils2.py:281
 #, python-format
 msgid "%s and %s ago"
 msgstr "Il y a %s et %s"
 
-#: kallithea/lib/utils2.py:365
+#: kallithea/lib/utils2.py:284
 msgid "just now"
 msgstr "à l’instant"
 
@@ -1385,158 +1387,158 @@
 msgid "on line %s"
 msgstr "à la ligne %s"
 
-#: kallithea/model/comment.py:221 kallithea/model/pull_request.py:114
+#: kallithea/model/comment.py:219 kallithea/model/pull_request.py:112
 msgid "[Mention]"
 msgstr "[Mention]"
 
-#: kallithea/model/db.py:1496
+#: kallithea/model/db.py:1411
 msgid "top level"
 msgstr "niveau supérieur"
 
-#: kallithea/model/db.py:1637
+#: kallithea/model/db.py:1542
 msgid "Kallithea Administrator"
 msgstr "Administrateur Kallithea"
 
-#: kallithea/model/db.py:1639
+#: kallithea/model/db.py:1544
 msgid "Default user has no access to new repositories"
 msgstr "L'utilisateur par défaut n'a pas accès aux nouveaux dépôts"
 
-#: kallithea/model/db.py:1640
+#: kallithea/model/db.py:1545
 msgid "Default user has read access to new repositories"
 msgstr "L'utilisateur par défaut a un accès en lecture aux nouveaux dépôts"
 
-#: kallithea/model/db.py:1641
+#: kallithea/model/db.py:1546
 msgid "Default user has write access to new repositories"
 msgstr "L'utilisateur par défaut a un accès en écriture aux nouveaux dépôts"
 
-#: kallithea/model/db.py:1642
+#: kallithea/model/db.py:1547
 msgid "Default user has admin access to new repositories"
 msgstr ""
 "L'utilisateur par défaut a un accès administrateur aux nouveaux dépôts"
 
-#: kallithea/model/db.py:1644
+#: kallithea/model/db.py:1549
 msgid "Default user has no access to new repository groups"
 msgstr ""
 "L'utilisateur par défaut n'a pas accès aux nouveaux groupes de dépôts"
 
-#: kallithea/model/db.py:1645
+#: kallithea/model/db.py:1550
 msgid "Default user has read access to new repository groups"
 msgstr ""
 "L'utilisateur par défaut a accès en lecture seule aux nouveaux groupes de "
 "dépôts"
 
-#: kallithea/model/db.py:1646
+#: kallithea/model/db.py:1551
 msgid "Default user has write access to new repository groups"
 msgstr ""
 "L'utilisateur par défaut a accès en écriture aux nouveaux groupes de "
 "dépôts"
 
-#: kallithea/model/db.py:1647
+#: kallithea/model/db.py:1552
 msgid "Default user has admin access to new repository groups"
 msgstr ""
 "L'utilisateur par défaut a accès administrateur aux nouveaux groupes de "
 "dépôts"
 
-#: kallithea/model/db.py:1649
+#: kallithea/model/db.py:1554
 msgid "Default user has no access to new user groups"
 msgstr ""
 "L'utilisateur par défaut n'a pas accès aux nouveaux groupes d'utilisateurs"
 
-#: kallithea/model/db.py:1650
+#: kallithea/model/db.py:1555
 msgid "Default user has read access to new user groups"
 msgstr ""
 "L'utilisateur par défaut a accès en lecture seule aux nouveaux groupes "
 "d'utilisateurs"
 
-#: kallithea/model/db.py:1651
+#: kallithea/model/db.py:1556
 msgid "Default user has write access to new user groups"
 msgstr ""
 "L'utilisateur par défaut a accès en écriture aux nouveaux groupes "
 "d'utilisateurs"
 
-#: kallithea/model/db.py:1652
+#: kallithea/model/db.py:1557
 msgid "Default user has admin access to new user groups"
 msgstr ""
 "L'utilisateur par défaut a un accès administrateur aux nouveaux groupes "
 "d'utilisateurs"
 
-#: kallithea/model/db.py:1654
+#: kallithea/model/db.py:1559
 msgid "Only admins can create repository groups"
 msgstr "Seul un administrateur peut créer un groupe de dépôts"
 
-#: kallithea/model/db.py:1655
+#: kallithea/model/db.py:1560
 msgid "Non-admins can create repository groups"
 msgstr ""
 "Les utilisateurs non-administrateurs peuvent créer des groupes de dépôts"
 
-#: kallithea/model/db.py:1657
+#: kallithea/model/db.py:1562
 msgid "Only admins can create user groups"
 msgstr "Seul un administrateur peut créer des groupes d'utilisateurs"
 
-#: kallithea/model/db.py:1658
+#: kallithea/model/db.py:1563
 msgid "Non-admins can create user groups"
 msgstr ""
 "Les utilisateurs non-administrateurs peuvent créer des groupes "
 "d'utilisateurs"
 
-#: kallithea/model/db.py:1660
+#: kallithea/model/db.py:1565
 msgid "Only admins can create top level repositories"
 msgstr "Seul un administrateur peut créer des dépôts de niveau supérieur"
 
-#: kallithea/model/db.py:1661
+#: kallithea/model/db.py:1566
 msgid "Non-admins can create top level repositories"
 msgstr ""
 "Les utilisateurs non-administrateurs peuvent créer des dépôts de niveau "
 "supérieur"
 
-#: kallithea/model/db.py:1663
+#: kallithea/model/db.py:1568
 msgid ""
 "Repository creation enabled with write permission to a repository group"
 msgstr ""
 "Création de dépôts activée avec l'accès en écriture vers un groupe de "
 "dépôts"
 
-#: kallithea/model/db.py:1664
+#: kallithea/model/db.py:1569
 msgid ""
 "Repository creation disabled with write permission to a repository group"
 msgstr ""
 "Création de dépôts désactivée avec l'accès en écriture vers un groupe de "
 "dépôts"
 
-#: kallithea/model/db.py:1666
+#: kallithea/model/db.py:1571
 msgid "Only admins can fork repositories"
 msgstr "Seul un administrateur peut faire un fork de dépôt"
 
-#: kallithea/model/db.py:1667
+#: kallithea/model/db.py:1572
 msgid "Non-admins can fork repositories"
 msgstr "Les utilisateurs non-administrateurs peuvent faire un fork de dépôt"
 
-#: kallithea/model/db.py:1669
+#: kallithea/model/db.py:1574
 msgid "Registration disabled"
 msgstr "Enregistrement désactivé"
 
-#: kallithea/model/db.py:1670
+#: kallithea/model/db.py:1575
 msgid "User registration with manual account activation"
 msgstr "Enregistrement des utilisateurs avec activation de compte manuelle"
 
-#: kallithea/model/db.py:1671
+#: kallithea/model/db.py:1576
 msgid "User registration with automatic account activation"
 msgstr ""
 "Enregistrement des utilisateurs avec activation de compte automatique"
 
-#: kallithea/model/db.py:2206
+#: kallithea/model/db.py:1992
 msgid "Not reviewed"
 msgstr "Pas encore relue"
 
-#: kallithea/model/db.py:2207
+#: kallithea/model/db.py:1993
 msgid "Under review"
 msgstr "En cours de relecture"
 
-#: kallithea/model/db.py:2208
+#: kallithea/model/db.py:1994
 msgid "Not approved"
 msgstr "Non approuvée"
 
-#: kallithea/model/db.py:2209
+#: kallithea/model/db.py:1995
 msgid "Approved"
 msgstr "Approuvée"
 
@@ -1562,7 +1564,7 @@
 msgid "Name must not contain only digits"
 msgstr "Le nom ne doit pas contenir seulement des chiffres"
 
-#: kallithea/model/notification.py:164
+#: kallithea/model/notification.py:162
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s changeset %(short_id)s \"%(message_short)s\" on "
@@ -1571,12 +1573,12 @@
 "[Commentaire] Changeset %(short_id)s « %(message_short)s » de "
 "%(repo_name)s dans %(branch)s"
 
-#: kallithea/model/notification.py:167
+#: kallithea/model/notification.py:165
 #, python-format
 msgid "New user %(new_username)s registered"
 msgstr "Nouvel utilisateur %(new_username)s enregistré"
 
-#: kallithea/model/notification.py:169
+#: kallithea/model/notification.py:167
 #, python-format
 msgid ""
 "[Review] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
@@ -1585,7 +1587,7 @@
 "[Revue] %(repo_name)s PR %(pr_nice_id)s « %(pr_title_short)s » depuis "
 "%(pr_source_branch)s par %(pr_owner_username)s"
 
-#: kallithea/model/notification.py:170
+#: kallithea/model/notification.py:168
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
@@ -1594,11 +1596,11 @@
 "[Commentaire] %(repo_name)s PR %(pr_nice_id)s « %(pr_title_short)s » "
 "depuis %(pr_source_branch)s par %(pr_owner_username)s"
 
-#: kallithea/model/notification.py:183
+#: kallithea/model/notification.py:188
 msgid "Closing"
 msgstr "Fermeture"
 
-#: kallithea/model/pull_request.py:73
+#: kallithea/model/pull_request.py:72
 #, python-format
 msgid ""
 "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s"
@@ -1606,11 +1608,11 @@
 "%(user)s veut que vous regardiez la demande de pull %(pr_nice_id)s : "
 "%(pr_title)s"
 
-#: kallithea/model/pull_request.py:209
+#: kallithea/model/pull_request.py:207
 msgid "Cannot create empty pull request"
 msgstr "Impossible de créer une requête de pull vide"
 
-#: kallithea/model/pull_request.py:217
+#: kallithea/model/pull_request.py:215
 #, python-format
 msgid ""
 "Cannot create pull request - criss cross merge detected, please merge a "
@@ -1619,24 +1621,24 @@
 "Impossible de créer la requête de pull : fusion croisée détectée, merci "
 "de fusionner une révision plus vieille de %s vers %s"
 
-#: kallithea/model/pull_request.py:245 kallithea/model/pull_request.py:380
+#: kallithea/model/pull_request.py:243 kallithea/model/pull_request.py:378
 msgid "You are not authorized to create the pull request"
 msgstr "Vous n'êtes pas autorisé à créer cette requête de pull"
 
-#: kallithea/model/pull_request.py:339
+#: kallithea/model/pull_request.py:337
 msgid "Missing changesets since the previous iteration:"
 msgstr "Changeset manquant depuis la précédente itération :"
 
-#: kallithea/model/pull_request.py:346
+#: kallithea/model/pull_request.py:344
 #, python-format
 msgid "New changesets on %s %s since the previous iteration:"
 msgstr "Nouveau changeset sur %s %s depuis la précédente itération :"
 
-#: kallithea/model/pull_request.py:353
+#: kallithea/model/pull_request.py:351
 msgid "Ancestor didn't change - diff since previous iteration:"
 msgstr "L'ancêtre n'a pas changé - diff depuis l'itération précédente :"
 
-#: kallithea/model/pull_request.py:360
+#: kallithea/model/pull_request.py:358
 #, python-format
 msgid ""
 "This iteration is based on another %s revision and there is no simple "
@@ -1645,48 +1647,48 @@
 "Cette itération est basée sur une autre révision %s et il n'y a pas de "
 "diff simple."
 
-#: kallithea/model/pull_request.py:362
+#: kallithea/model/pull_request.py:360
 #, python-format
 msgid "No changes found on %s %s since previous iteration."
 msgstr "Aucun changement constaté sur %s %s depuis l'itération précédente."
 
-#: kallithea/model/pull_request.py:388
+#: kallithea/model/pull_request.py:386
 #, python-format
 msgid "Closed, next iteration: %s ."
 msgstr "Fermé, itération suivante : %s."
 
-#: kallithea/model/scm.py:668
+#: kallithea/model/scm.py:655
 msgid "latest tip"
 msgstr "Dernier sommet"
 
-#: kallithea/model/ssh_key.py:56
+#: kallithea/model/ssh_key.py:57
 #, fuzzy, python-format
 msgid "SSH key %r is invalid: %s"
 msgstr "La clé SSH %r est invalide : %s"
 
-#: kallithea/model/ssh_key.py:68
+#: kallithea/model/ssh_key.py:69
 #, python-format
 msgid "SSH key %s is already used by %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:89
+#: kallithea/model/ssh_key.py:88
 #, fuzzy, python-format
 #| msgid "Changeset %s not found"
-msgid "SSH key %r not found"
+msgid "SSH key with fingerprint %r found"
 msgstr "Ensemble de changements %s non trouvé"
 
-#: kallithea/model/user.py:186
+#: kallithea/model/user.py:180
 msgid "New user registration"
 msgstr "Nouveau enregistrement d'utilisateur"
 
-#: kallithea/model/user.py:250
+#: kallithea/model/user.py:244
 msgid ""
 "You can't remove this user since it is crucial for the entire application"
 msgstr ""
 "Vous ne pouvez pas supprimer cet utilisateur ; il est nécessaire pour le "
 "bon fonctionnement de l’application"
 
-#: kallithea/model/user.py:255
+#: kallithea/model/user.py:249
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repositories and cannot be removed. Switch "
@@ -1695,7 +1697,7 @@
 "L’utilisateur \"%s\" possède %s dépôts et ne peut être supprimé. Changez "
 "les propriétaires ou supprimez ces dépôts : %s"
 
-#: kallithea/model/user.py:260
+#: kallithea/model/user.py:254
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repository groups and cannot be removed. Switch "
@@ -1704,7 +1706,7 @@
 "L’utilisateur \"%s\" possède %s groupes de dépôt et ne peut être "
 "supprimé. Changez les propriétaires ou supprimez ces dépôts : %s"
 
-#: kallithea/model/user.py:267
+#: kallithea/model/user.py:261
 #, python-format
 msgid ""
 "User \"%s\" still owns %s user groups and cannot be removed. Switch "
@@ -1714,15 +1716,15 @@
 "être supprimé. Changez les propriétaires de ces groupes d'utilisateurs ou "
 "supprimez-les : %s"
 
-#: kallithea/model/user.py:361
+#: kallithea/model/user.py:355
 msgid "Password reset link"
 msgstr "Lien de remise à zéro du mot de passe"
 
-#: kallithea/model/user.py:408
+#: kallithea/model/user.py:402
 msgid "Password reset notification"
 msgstr "Notification de réinitialisation du mot de passe"
 
-#: kallithea/model/user.py:409
+#: kallithea/model/user.py:403
 #, python-format
 msgid ""
 "The password to your account %s has been changed using password reset "
@@ -1731,21 +1733,21 @@
 "Le mot de passe de votre compte %s a été changé via le formulaire de "
 "réinitialisation du mot de passe."
 
-#: kallithea/model/validators.py:52 kallithea/model/validators.py:53
+#: kallithea/model/validators.py:53 kallithea/model/validators.py:54
 msgid "Value cannot be an empty list"
 msgstr "Cette valeur ne peut être une liste vide"
 
-#: kallithea/model/validators.py:72
+#: kallithea/model/validators.py:73
 #, python-format
 msgid "Username \"%(username)s\" already exists"
 msgstr "Le nom d’utilisateur « %(username)s » existe déjà"
 
-#: kallithea/model/validators.py:74
+#: kallithea/model/validators.py:75
 #, python-format
 msgid "Username \"%(username)s\" cannot be used"
 msgstr "Le nom d’utilisateur « %(username)s » n’est pas valide"
 
-#: kallithea/model/validators.py:76
+#: kallithea/model/validators.py:77
 msgid ""
 "Username may only contain alphanumeric characters underscores, periods or "
 "dashes and must begin with an alphanumeric character or underscore"
@@ -1754,25 +1756,25 @@
 "des underscores (_), points, traits d'union et doit commencer avec un "
 "caractère alphanumérique ou un underscore"
 
-#: kallithea/model/validators.py:103
+#: kallithea/model/validators.py:104
 msgid "The input is not valid"
 msgstr "L'entrée n'est pas valide"
 
-#: kallithea/model/validators.py:110
+#: kallithea/model/validators.py:111
 #, python-format
 msgid "Username %(username)s is not valid"
 msgstr "Le nom d’utilisateur « %(username)s » n’est pas valide"
 
-#: kallithea/model/validators.py:131
+#: kallithea/model/validators.py:132
 msgid "Invalid user group name"
 msgstr "Nom de groupe d'utilisateurs invalide"
 
-#: kallithea/model/validators.py:132
+#: kallithea/model/validators.py:133
 #, python-format
 msgid "User group \"%(usergroup)s\" already exists"
 msgstr "Le groupe d'utilisateurs « %(usergroup)s » existe déjà"
 
-#: kallithea/model/validators.py:134
+#: kallithea/model/validators.py:135
 msgid ""
 "user group name may only contain alphanumeric characters underscores, "
 "periods or dashes and must begin with alphanumeric character"
@@ -1781,61 +1783,61 @@
 "alphanumériques, des tirets, des points, des traits d'union et doit "
 "commencer avec un caractère alphanumérique"
 
-#: kallithea/model/validators.py:174
+#: kallithea/model/validators.py:175
 msgid "Cannot assign this group as parent"
 msgstr "Impossible d’assigner ce groupe en tant que parent"
 
-#: kallithea/model/validators.py:175
+#: kallithea/model/validators.py:176
 #, python-format
 msgid "Group \"%(group_name)s\" already exists"
 msgstr "Le groupe « %(group_name)s » existe déjà"
 
-#: kallithea/model/validators.py:177
+#: kallithea/model/validators.py:178
 #, python-format
 msgid "Repository with name \"%(group_name)s\" already exists"
 msgstr "Un dépôt portant le nom « %(group_name)s » existe déjà"
 
-#: kallithea/model/validators.py:233
+#: kallithea/model/validators.py:230
 msgid "Invalid characters (non-ascii) in password"
 msgstr "Caractères incorrects (non-ASCII) dans le mot de passe"
 
-#: kallithea/model/validators.py:248
+#: kallithea/model/validators.py:245
 msgid "Invalid old password"
 msgstr "Ancien mot de passe invalide"
 
-#: kallithea/model/validators.py:264
+#: kallithea/model/validators.py:261
 msgid "Passwords do not match"
 msgstr "Les mots de passe ne correspondent pas"
 
-#: kallithea/model/validators.py:279
+#: kallithea/model/validators.py:276
 msgid "Invalid username or password"
 msgstr "Nom d'utilisateur ou mot de passe invalide"
 
-#: kallithea/model/validators.py:313
+#: kallithea/model/validators.py:310
 #, python-format
 msgid "Repository name %(repo)s is not allowed"
 msgstr "Le nom de dépôt « %(repo)s » n’est pas autorisé"
 
-#: kallithea/model/validators.py:315
+#: kallithea/model/validators.py:312
 #, python-format
 msgid "Repository named %(repo)s already exists"
 msgstr "Un dépôt portant le nom « %(repo)s » existe déjà"
 
-#: kallithea/model/validators.py:316
+#: kallithea/model/validators.py:313
 #, python-format
 msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
 msgstr "Le dépôt « %(repo)s » existe déjà dans le groupe « %(group)s »"
 
-#: kallithea/model/validators.py:318
+#: kallithea/model/validators.py:315
 #, python-format
 msgid "Repository group with name \"%(repo)s\" already exists"
 msgstr "Un groupe de dépôts avec le nom « %(repo)s » existe déjà"
 
-#: kallithea/model/validators.py:404
+#: kallithea/model/validators.py:401
 msgid "Invalid repository URL"
 msgstr "URL de dépôt invalide"
 
-#: kallithea/model/validators.py:405
+#: kallithea/model/validators.py:402
 msgid ""
 "Invalid repository URL. It must be a valid http, https, ssh, svn+http or "
 "svn+https URL"
@@ -1843,42 +1845,42 @@
 "URL de dépôt invalide. Ce doit être une URL valide de type http, https, "
 "ssh, svn+http ou svn+https"
 
-#: kallithea/model/validators.py:430
+#: kallithea/model/validators.py:427
 msgid "Fork has to be the same type as parent"
 msgstr "Le fork doit être du même type que le parent"
 
-#: kallithea/model/validators.py:445
+#: kallithea/model/validators.py:442
 msgid "You don't have permissions to create repository in this group"
 msgstr "Vous n’avez pas la permission de créer un dépôt dans ce"
 
-#: kallithea/model/validators.py:447
+#: kallithea/model/validators.py:444
 msgid "no permission to create repository in root location"
 msgstr "pas de permission de créer un dépôt dans la racine"
 
-#: kallithea/model/validators.py:497
+#: kallithea/model/validators.py:494
 msgid "You don't have permissions to create a group in this location"
 msgstr ""
 "Vous n'avez pas les permissions pour créer un groupe dans cet endroit"
 
-#: kallithea/model/validators.py:537
+#: kallithea/model/validators.py:534
 msgid "This username or user group name is not valid"
 msgstr ""
 "Ce nom d'utilisateur ou nom de groupe d'utilisateurs n'est pas valide"
 
-#: kallithea/model/validators.py:630
+#: kallithea/model/validators.py:627
 msgid "This is not a valid path"
 msgstr "Ceci n’est pas un chemin valide"
 
-#: kallithea/model/validators.py:647
+#: kallithea/model/validators.py:644
 msgid "This email address is already in use"
 msgstr "Cette adresse e-mail est déjà enregistrée"
 
-#: kallithea/model/validators.py:667
+#: kallithea/model/validators.py:664
 #, python-format
 msgid "Email address \"%(email)s\" not found"
 msgstr "L’adresse e-mail « %(email)s » n’existe pas"
 
-#: kallithea/model/validators.py:704
+#: kallithea/model/validators.py:701
 msgid ""
 "The LDAP Login attribute of the CN must be specified - this is the name "
 "of the attribute that is equivalent to \"username\""
@@ -1886,28 +1888,28 @@
 "L’attribut Login du CN doit être spécifié. Cet attribut correspond au nom "
 "d’utilisateur"
 
-#: kallithea/model/validators.py:716
+#: kallithea/model/validators.py:713
 msgid "Please enter a valid IPv4 or IPv6 address"
 msgstr "Veuillez entrer une adresse IPv4 ou IPv6 valide"
 
-#: kallithea/model/validators.py:717
+#: kallithea/model/validators.py:714
 #, python-format
 msgid ""
 "The network size (bits) must be within the range of 0-32 (not %(bits)r)"
 msgstr ""
 "La taille du réseau (bits) doit être entre 0 et 32 (et non %(bits)r)"
 
-#: kallithea/model/validators.py:750
+#: kallithea/model/validators.py:747
 msgid "Key name can only consist of letters, underscore, dash or numbers"
 msgstr ""
 "Le nom de la clé ne peut consister que de letters, de traits d'union, de "
 "tirets ou de nombres"
 
-#: kallithea/model/validators.py:764
+#: kallithea/model/validators.py:761
 msgid "Filename cannot be inside a directory"
 msgstr "Le nom du fichier ne peut être à l'intérieur d'un répertoire"
 
-#: kallithea/model/validators.py:780
+#: kallithea/model/validators.py:777
 #, python-format
 msgid "Plugins %(loaded)s and %(next_to_load)s both export the same name"
 msgstr ""
@@ -1970,7 +1972,7 @@
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:60
 #: kallithea/templates/email_templates/pull_request.html:37
 #: kallithea/templates/forks/fork.html:34
-#: kallithea/templates/index_base.html:58
+#: kallithea/templates/index_base.html:59
 #: kallithea/templates/pullrequests/pullrequest.html:33
 #: kallithea/templates/pullrequests/pullrequest_show.html:38
 #: kallithea/templates/pullrequests/pullrequest_show.html:59
@@ -1978,14 +1980,14 @@
 msgid "Description"
 msgstr "Description"
 
-#: kallithea/templates/index_base.html:60
+#: kallithea/templates/index_base.html:61
 msgid "Last Change"
 msgstr "Dernière modification"
 
 #: kallithea/templates/admin/my_account/my_account_repos.html:15
 #: kallithea/templates/admin/my_account/my_account_watched.html:15
 #: kallithea/templates/admin/repos/repos.html:41
-#: kallithea/templates/index_base.html:62
+#: kallithea/templates/index_base.html:63
 msgid "Tip"
 msgstr "Sommet"
 
@@ -1995,7 +1997,7 @@
 #: kallithea/templates/admin/repos/repos.html:42
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:8
 #: kallithea/templates/admin/user_groups/user_groups.html:42
-#: kallithea/templates/index_base.html:63
+#: kallithea/templates/index_base.html:64
 #: kallithea/templates/pullrequests/pullrequest_data.html:16
 #: kallithea/templates/pullrequests/pullrequest_show.html:124
 #: kallithea/templates/pullrequests/pullrequest_show.html:219
@@ -2019,7 +2021,7 @@
 #: kallithea/templates/admin/users/user_edit_profile.html:18
 #: kallithea/templates/admin/users/users.html:37
 #: kallithea/templates/base/base.html:364
-#: kallithea/templates/email_templates/registration.html:11
+#: kallithea/templates/email_templates/registration.html:12
 #: kallithea/templates/login.html:28 kallithea/templates/register.html:31
 msgid "Username"
 msgstr "Nom d’utilisateur"
@@ -2149,7 +2151,7 @@
 #: kallithea/templates/admin/settings/settings.html:31
 #: kallithea/templates/admin/users/user_add.html:62
 #: kallithea/templates/admin/users/user_edit_profile.html:25
-#: kallithea/templates/email_templates/registration.html:33
+#: kallithea/templates/email_templates/registration.html:34
 #: kallithea/templates/register.html:66
 msgid "Email"
 msgstr "E-mail"
@@ -2405,7 +2407,7 @@
 msgstr "Créer un nouveau gist"
 
 #: kallithea/templates/admin/gists/index.html:51
-#: kallithea/templates/data_table/_dt_elements.html:78
+#: kallithea/templates/data_table/_dt_elements.html:84
 msgid "Created"
 msgstr "Créé"
 
@@ -2489,13 +2491,13 @@
 #: kallithea/templates/admin/users/user_edit_ips.html:21
 #: kallithea/templates/changeset/changeset_file_comment.html:30
 #: kallithea/templates/changeset/changeset_file_comment.html:121
-#: kallithea/templates/data_table/_dt_elements.html:69
-#: kallithea/templates/data_table/_dt_elements.html:89
-#: kallithea/templates/data_table/_dt_elements.html:91
-#: kallithea/templates/data_table/_dt_elements.html:101
-#: kallithea/templates/data_table/_dt_elements.html:103
-#: kallithea/templates/data_table/_dt_elements.html:120
-#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:75
+#: kallithea/templates/data_table/_dt_elements.html:95
+#: kallithea/templates/data_table/_dt_elements.html:97
+#: kallithea/templates/data_table/_dt_elements.html:107
+#: kallithea/templates/data_table/_dt_elements.html:109
+#: kallithea/templates/data_table/_dt_elements.html:126
+#: kallithea/templates/data_table/_dt_elements.html:128
 #: kallithea/templates/files/files_source.html:35
 #: kallithea/templates/files/files_source.html:38
 #: kallithea/templates/files/files_source.html:41
@@ -2511,14 +2513,14 @@
 #: kallithea/templates/base/perms_summary.html:44
 #: kallithea/templates/base/perms_summary.html:81
 #: kallithea/templates/base/perms_summary.html:83
-#: kallithea/templates/data_table/_dt_elements.html:63
-#: kallithea/templates/data_table/_dt_elements.html:64
-#: kallithea/templates/data_table/_dt_elements.html:85
-#: kallithea/templates/data_table/_dt_elements.html:86
-#: kallithea/templates/data_table/_dt_elements.html:97
-#: kallithea/templates/data_table/_dt_elements.html:98
-#: kallithea/templates/data_table/_dt_elements.html:116
-#: kallithea/templates/data_table/_dt_elements.html:117
+#: kallithea/templates/data_table/_dt_elements.html:69
+#: kallithea/templates/data_table/_dt_elements.html:70
+#: kallithea/templates/data_table/_dt_elements.html:91
+#: kallithea/templates/data_table/_dt_elements.html:92
+#: kallithea/templates/data_table/_dt_elements.html:103
+#: kallithea/templates/data_table/_dt_elements.html:104
+#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:123
 #: kallithea/templates/files/diff_2way.html:56
 #: kallithea/templates/files/files_source.html:37
 #: kallithea/templates/files/files_source.html:40
@@ -2844,7 +2846,7 @@
 #: kallithea/templates/admin/permissions/permissions_globals.html:27
 #: kallithea/templates/admin/repos/repo_add_base.html:28
 #: kallithea/templates/admin/repos/repo_edit_settings.html:33
-#: kallithea/templates/data_table/_dt_elements.html:134
+#: kallithea/templates/data_table/_dt_elements.html:140
 #: kallithea/templates/forks/fork.html:42
 msgid "Repository group"
 msgstr "Groupe de dépôt"
@@ -2870,7 +2872,7 @@
 "dépôts."
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:40
-#: kallithea/templates/data_table/_dt_elements.html:141
+#: kallithea/templates/data_table/_dt_elements.html:147
 msgid "User group"
 msgstr "Groupe d'utilisateurs"
 
@@ -3060,7 +3062,7 @@
 msgstr "Créé le"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:121
+#: kallithea/templates/data_table/_dt_elements.html:127
 #, python-format
 msgid "Confirm to delete this group: %s with %s repository"
 msgid_plural "Confirm to delete this group: %s with %s repositories"
@@ -3236,14 +3238,10 @@
 msgstr "Champs supplémentaires"
 
 #: kallithea/templates/admin/repos/repo_edit.html:37
-msgid "Caches"
-msgstr "Caches"
+msgid "Remote"
+msgstr "Dépôt distant"
 
 #: kallithea/templates/admin/repos/repo_edit.html:40
-msgid "Remote"
-msgstr "Dépôt distant"
-
-#: kallithea/templates/admin/repos/repo_edit.html:43
 #: kallithea/templates/summary/statistics.html:8
 #: kallithea/templates/summary/summary.html:169
 #: kallithea/templates/summary/summary.html:170
@@ -3284,7 +3282,7 @@
 "journal public."
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:46
-#: kallithea/templates/data_table/_dt_elements.html:68
+#: kallithea/templates/data_table/_dt_elements.html:74
 #, python-format
 msgid "Confirm to delete this repository: %s"
 msgstr "Voulez-vous vraiment supprimer le dépôt %s ?"
@@ -3318,45 +3316,14 @@
 "l'administrateur le fasse expirer. L'administrateur peut soit le "
 "supprimer définitivement, soit le restaurer."
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:4
-msgid "Invalidate Repository Cache"
-msgstr "Invalider le cache du dépôt"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:6
-msgid ""
-"Manually invalidate cache for this repository. On first access, the "
-"repository will be cached again."
-msgstr ""
-"Invalider manuellement le cache de ce dépôt. Au prochain accès sur ce "
-"dépôt, il sera à nouveau mis en cache."
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:9
-msgid "List of Cached Values"
-msgstr "Liste des valeurs en cache"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:12
-msgid "Prefix"
-msgstr "Préfixe"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:13
+#: kallithea/templates/admin/repos/repo_edit_fields.html:6
+msgid "Label"
+msgstr "Libellé"
+
 #: kallithea/templates/admin/repos/repo_edit_fields.html:7
 msgid "Key"
 msgstr "Clé"
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:14
-#: kallithea/templates/admin/user_groups/user_group_add.html:40
-#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
-#: kallithea/templates/admin/user_groups/user_groups.html:41
-#: kallithea/templates/admin/users/user_add.html:69
-#: kallithea/templates/admin/users/user_edit_profile.html:74
-#: kallithea/templates/admin/users/users.html:42
-msgid "Active"
-msgstr "Actif"
-
-#: kallithea/templates/admin/repos/repo_edit_fields.html:6
-msgid "Label"
-msgstr "Libellé"
-
 #: kallithea/templates/admin/repos/repo_edit_fields.html:20
 #, python-format
 msgid "Confirm to delete this field: %s"
@@ -3969,6 +3936,15 @@
 msgid "Short, optional description for this user group."
 msgstr "Description courte pour ce groupe d'utilisateur (optionnel)."
 
+#: kallithea/templates/admin/user_groups/user_group_add.html:40
+#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
+#: kallithea/templates/admin/user_groups/user_groups.html:41
+#: kallithea/templates/admin/users/user_add.html:69
+#: kallithea/templates/admin/users/user_edit_profile.html:74
+#: kallithea/templates/admin/users/users.html:42
+msgid "Active"
+msgstr "Actif"
+
 #: kallithea/templates/admin/user_groups/user_group_edit.html:5
 #, python-format
 msgid "%s user group settings"
@@ -3990,7 +3966,7 @@
 msgstr "Membres"
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:19
-#: kallithea/templates/data_table/_dt_elements.html:102
+#: kallithea/templates/data_table/_dt_elements.html:108
 #, python-format
 msgid "Confirm to delete this user group: %s"
 msgstr "Voulez-vous vraiment supprimer ce groupe utilisateur : %s ?"
@@ -4064,7 +4040,7 @@
 msgstr "Membre des groupes d'utilisateurs"
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:90
+#: kallithea/templates/data_table/_dt_elements.html:96
 #, python-format
 msgid "Confirm to delete this user: %s"
 msgstr "Voulez-vous vraiment supprimer l’utilisateur « %s » ?"
@@ -4164,10 +4140,12 @@
 msgstr "Rechercher"
 
 #: kallithea/templates/base/base.html:167
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Follow"
 msgstr "Suivre"
 
 #: kallithea/templates/base/base.html:168
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Unfollow"
 msgstr "Arrêter de suivre"
 
@@ -4580,23 +4558,23 @@
 msgid "Merge"
 msgstr "Fusion"
 
-#: kallithea/templates/changeset/changeset.html:96
+#: kallithea/templates/changeset/changeset.html:95
 msgid "Grafted from:"
 msgstr "Grafté depuis :"
 
-#: kallithea/templates/changeset/changeset.html:102
+#: kallithea/templates/changeset/changeset.html:100
 msgid "Transplanted from:"
 msgstr "Transplanté depuis :"
 
-#: kallithea/templates/changeset/changeset.html:108
+#: kallithea/templates/changeset/changeset.html:106
 msgid "Replaced by:"
 msgstr "Remplacé par :"
 
-#: kallithea/templates/changeset/changeset.html:122
+#: kallithea/templates/changeset/changeset.html:120
 msgid "Preceded by:"
 msgstr "Précédé par :"
 
-#: kallithea/templates/changeset/changeset.html:139
+#: kallithea/templates/changeset/changeset.html:137
 #: kallithea/templates/compare/compare_diff.html:59
 #: kallithea/templates/pullrequests/pullrequest_show.html:290
 #, python-format
@@ -4605,7 +4583,7 @@
 msgstr[0] "%s fichier changé"
 msgstr[1] "%s fichiers changés"
 
-#: kallithea/templates/changeset/changeset.html:141
+#: kallithea/templates/changeset/changeset.html:139
 #: kallithea/templates/compare/compare_diff.html:61
 #: kallithea/templates/pullrequests/pullrequest_show.html:292
 #, python-format
@@ -4614,8 +4592,8 @@
 msgstr[0] "%s fichier changé avec %s insertions et %s suppressions"
 msgstr[1] "%s fichiers changés avec %s insertions et %s suppressions"
 
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
 #: kallithea/templates/pullrequests/pullrequest_show.html:333
@@ -4848,23 +4826,23 @@
 msgid "Repository creation in progress..."
 msgstr "Création du dépôt en cours..."
 
-#: kallithea/templates/data_table/_dt_elements.html:42
+#: kallithea/templates/data_table/_dt_elements.html:48
 msgid "No changesets yet"
 msgstr "Dépôt vide"
 
-#: kallithea/templates/data_table/_dt_elements.html:48
-#: kallithea/templates/data_table/_dt_elements.html:50
+#: kallithea/templates/data_table/_dt_elements.html:54
+#: kallithea/templates/data_table/_dt_elements.html:56
 #, python-format
 msgid "Subscribe to %s rss feed"
 msgstr "S’abonner au flux RSS de %s"
 
-#: kallithea/templates/data_table/_dt_elements.html:56
-#: kallithea/templates/data_table/_dt_elements.html:58
+#: kallithea/templates/data_table/_dt_elements.html:62
+#: kallithea/templates/data_table/_dt_elements.html:64
 #, python-format
 msgid "Subscribe to %s atom feed"
 msgstr "S’abonner au flux ATOM de %s"
 
-#: kallithea/templates/data_table/_dt_elements.html:76
+#: kallithea/templates/data_table/_dt_elements.html:82
 msgid "Creating"
 msgstr "En cours de création"
 
@@ -4896,6 +4874,13 @@
 msgid "by"
 msgstr "par"
 
+#: kallithea/templates/email_templates/changeset_comment.html:36
+#: kallithea/templates/email_templates/pull_request_comment.html:43
+#, fuzzy
+#| msgid "Comment"
+msgid "View Comment"
+msgstr "Commentaire"
+
 #: kallithea/templates/email_templates/comment.html:27
 msgid "Status change:"
 msgstr "Changement de statut :"
@@ -4904,18 +4889,30 @@
 msgid "The pull request has been closed."
 msgstr "La requête de pull a été fermée."
 
-#: kallithea/templates/email_templates/password_reset.html:9
+#: kallithea/templates/email_templates/default.html:4
+#, fuzzy
+#| msgid "Commit Message"
+msgid "Message"
+msgstr "Message de commit"
+
+#: kallithea/templates/email_templates/password_reset.html:4
+#, fuzzy
+#| msgid "Password Reset"
+msgid "Password Reset Request"
+msgstr "Remettre le mot de passe à zéro"
+
+#: kallithea/templates/email_templates/password_reset.html:10
 #, python-format
 msgid "Hello %s"
 msgstr "Bonjour %s"
 
-#: kallithea/templates/email_templates/password_reset.html:16
+#: kallithea/templates/email_templates/password_reset.html:17
 msgid "We have received a request to reset the password for your account."
 msgstr ""
 "Nous avons reçu une demande de réinitialisation du mot de passe de votre "
 "compte."
 
-#: kallithea/templates/email_templates/password_reset.html:25
+#: kallithea/templates/email_templates/password_reset.html:26
 msgid ""
 "This account is however managed outside this system and the password "
 "cannot be changed here."
@@ -4923,11 +4920,11 @@
 "Cependant, ce compte est géré hors de ce système et le mot de passe ne "
 "peut pas être changé ici."
 
-#: kallithea/templates/email_templates/password_reset.html:28
+#: kallithea/templates/email_templates/password_reset.html:29
 msgid "To set a new password, click the following link"
 msgstr "Pour choisir un nouveau mot de passe, cliquez sur le lien suivant"
 
-#: kallithea/templates/email_templates/password_reset.html:33
+#: kallithea/templates/email_templates/password_reset.html:34
 msgid ""
 "Should you not be able to use the link above, please type the following "
 "code into the password reset form"
@@ -4935,7 +4932,7 @@
 "Si vous ne pouvez pas utiliser le lien ci-dessus, merci de saisir le code "
 "suivant dans le formulaire de réinitialisation de mot de passe"
 
-#: kallithea/templates/email_templates/password_reset.html:44
+#: kallithea/templates/email_templates/password_reset.html:45
 msgid ""
 "If it weren't you who requested the password reset, just disregard this "
 "message."
@@ -4968,6 +4965,12 @@
 msgid "to"
 msgstr "vers"
 
+#: kallithea/templates/email_templates/pull_request.html:85
+#, fuzzy
+#| msgid "New Pull Request"
+msgid "View Pull Request"
+msgstr "Nouvelle requête de pull"
+
 #: kallithea/templates/email_templates/pull_request_comment.html:4
 #, python-format
 msgid "Mention in Comment on Pull Request %s \"%s\""
@@ -4983,10 +4986,22 @@
 msgid "Comment on Pull Request %s \"%s\""
 msgstr "Commentaire sur la requête de pull %s « %s »"
 
-#: kallithea/templates/email_templates/registration.html:22
+#: kallithea/templates/email_templates/registration.html:5
+#, fuzzy
+#| msgid "New user registration"
+msgid "New User Registration"
+msgstr "Nouveau enregistrement d'utilisateur"
+
+#: kallithea/templates/email_templates/registration.html:23
 msgid "Full Name"
 msgstr "Nom complet"
 
+#: kallithea/templates/email_templates/registration.html:42
+#, fuzzy
+#| msgid "View this user here"
+msgid "View User Profile"
+msgstr "Visualiser cet utilisateur ici"
+
 #: kallithea/templates/files/diff_2way.html:15
 #, python-format
 msgid "%s File side-by-side diff"
@@ -5604,45 +5619,45 @@
 msgid "Stats gathered: "
 msgstr "Statistiques obtenues : "
 
-#: kallithea/templates/summary/statistics.html:87
-#: kallithea/templates/summary/summary.html:371
+#: kallithea/templates/summary/statistics.html:85
+#: kallithea/templates/summary/summary.html:369
 msgid "files"
 msgstr "Fichiers"
 
-#: kallithea/templates/summary/statistics.html:111
-#: kallithea/templates/summary/summary.html:401
+#: kallithea/templates/summary/statistics.html:109
+#: kallithea/templates/summary/summary.html:399
 msgid "Show more"
 msgstr "Afficher plus"
 
-#: kallithea/templates/summary/statistics.html:405
+#: kallithea/templates/summary/statistics.html:395
 msgid "commits"
 msgstr "commits"
 
-#: kallithea/templates/summary/statistics.html:406
+#: kallithea/templates/summary/statistics.html:396
 msgid "files added"
 msgstr "fichiers ajoutés"
 
-#: kallithea/templates/summary/statistics.html:407
+#: kallithea/templates/summary/statistics.html:397
 msgid "files changed"
 msgstr "fichiers modifiés"
 
-#: kallithea/templates/summary/statistics.html:408
+#: kallithea/templates/summary/statistics.html:398
 msgid "files removed"
 msgstr "fichiers supprimés"
 
-#: kallithea/templates/summary/statistics.html:410
+#: kallithea/templates/summary/statistics.html:400
 msgid "commit"
 msgstr "commit"
 
-#: kallithea/templates/summary/statistics.html:411
+#: kallithea/templates/summary/statistics.html:401
 msgid "file added"
 msgstr "fichier ajouté"
 
-#: kallithea/templates/summary/statistics.html:412
+#: kallithea/templates/summary/statistics.html:402
 msgid "file changed"
 msgstr "fichié modifié"
 
-#: kallithea/templates/summary/statistics.html:413
+#: kallithea/templates/summary/statistics.html:403
 msgid "file removed"
 msgstr "fichier supprimé"
 
@@ -5746,6 +5761,31 @@
 msgid "Download %s as %s"
 msgstr "Télécharge %s comme %s"
 
+#~ msgid "Cache invalidation successful"
+#~ msgstr "Invalidation du cache réalisée avec succès"
+
+#~ msgid "An error occurred during cache invalidation"
+#~ msgstr "Une erreur est survenue durant l’invalidation du cache"
+
+#~ msgid "Caches"
+#~ msgstr "Caches"
+
+#~ msgid "Invalidate Repository Cache"
+#~ msgstr "Invalider le cache du dépôt"
+
+#~ msgid ""
+#~ "Manually invalidate cache for this repository. On first access, the "
+#~ "repository will be cached again."
+#~ msgstr ""
+#~ "Invalider manuellement le cache de ce dépôt. Au prochain accès sur ce "
+#~ "dépôt, il sera à nouveau mis en cache."
+
+#~ msgid "List of Cached Values"
+#~ msgstr "Liste des valeurs en cache"
+
+#~ msgid "Prefix"
+#~ msgstr "Préfixe"
+
 #~ msgid "This repository has been locked by %s on %s"
 #~ msgstr "Ce dépôt a été verrouillé par %s sur %s"
 
@@ -6218,9 +6258,6 @@
 #~ msgid "The comment was made with status"
 #~ msgstr "Le commentaire a été fait avec le statut"
 
-#~ msgid "View this user here"
-#~ msgstr "Visualiser cet utilisateur ici"
-
 #~ msgid "Previous revision"
 #~ msgstr "Révision précédente"
 
--- a/kallithea/i18n/how_to	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/i18n/how_to	Mon May 04 19:24:04 2020 +0200
@@ -55,9 +55,9 @@
 
 First update the translation strings::
 
-    python2 setup.py extract_messages
+    python3 setup.py extract_messages
 
-Then regenerate the translation files. This could either be done with `python2
+Then regenerate the translation files. This could either be done with `python3
 setup.py update_catalog` or with `msgmerge` from the `gettext` package. As
 Weblate is also touching these translation files, it is preferred to use the
 same tools (`msgmerge`) and settings as Weblate to minimize the diff::
@@ -73,11 +73,11 @@
 In the prepared development environment, run the following to ensure
 all translation strings are extracted and up-to-date::
 
-    python2 setup.py extract_messages
+    python3 setup.py extract_messages
 
 Create new language by executing following command::
 
-    python2 setup.py init_catalog -l <new_language_code>
+    python3 setup.py init_catalog -l <new_language_code>
 
 This creates a new translation under directory `kallithea/i18n/<new_language_code>`
 based on the translation template file, `kallithea/i18n/kallithea.pot`.
@@ -90,7 +90,7 @@
 
 Finally, compile the translations::
 
-    python2 setup.py compile_catalog -l <new_language_code>
+    python3 setup.py compile_catalog -l <new_language_code>
 
 
 Manually updating translations
@@ -98,11 +98,11 @@
 
 Extract the latest versions of strings for translation by running::
 
-    python2 setup.py extract_messages
+    python3 setup.py extract_messages
 
 Update the PO file by doing::
 
-    python2 setup.py update_catalog -l <new_language_code>
+    python3 setup.py update_catalog -l <new_language_code>
 
 Edit the newly updated translation file. Repeat all steps after the
 `init_catalog` step from the 'new translation' instructions above.
@@ -117,4 +117,139 @@
     py.test
 
 
+Managing translations with scripts/i18n tooling
+-----------------------------------------------
+
+The general idea with the ``scripts/i18n`` tooling is to keep changes in the
+main repository focussed on actual and reviewable changes with minimal noise.
+Noisy generated or redundant localization changes (that are useful when
+translations) are contained in the ``kallithea-i18n`` repo on the ``i18n``
+branch. The translation files in the main repository have no line numbers, no
+untranslated entries, no fuzzy entries, no unused entries, and no constantly
+changing records of "latest" this and that (name, date, version, etc).
+
+The branches in the main repo (``default`` and ``stable``) will thus only have
+stripped ``.pot`` and ``.po`` files: an (almost) empty
+``kallithea/i18n/kallithea.pot`` file, and minimal ``.po`` files. There are no
+binary ``.mo`` files in any repo - these are only generated when packaging for
+release (or locally if installing from source).
+
+Generally, ``kallithea/i18n/`` should not be changed on the ``default`` and
+``stable`` branches at all. The ``i18n`` branch should *only* change
+``kallithea/i18n/`` . If there are changesets with exceptions from that, these
+changesets should probably be grafted/redone in the "right" place.
+
+The basic flow is thus:
+
+0. All weblate translation is done on the ``i18n`` branch which generally is
+   based on the ``stable`` branch.
+1. Graft the essential part of all new changes on the ``i18n`` branch to
+   ``stable`` (while normalizing to current stripped state of stable).
+2. Merge from ``stable`` to ``i18n`` (while normalizing to the resulting
+   unstripped and fully ``msgmerge``'d state and ``.pot``-updating state).
+3. Verify that the content of the ``i18n`` branch will give exactly the content
+   of the ``stable`` branch after stripping. If there is a diff, something has
+   to be fixed in one way or the other ... and the whole process should
+   probably be redone.
+
+Translate
+^^^^^^^^^
+
+First land full translation changes in the ``kallithea-i18n`` repo on the
+``i18n`` branch. That can be done in pretty much any way you want. If changes
+for some reason have to be grafted or merged, there might be odd conflicts due
+to all the noise. Conflicts on the full ``i18n`` branch can perhaps be resolved
+more easily using non-stripping normalization before merging::
+
+  python3 setup.py extract_messages && cp kallithea/i18n/kallithea.pot full.pot && hg revert kallithea/i18n/kallithea.pot -r .
+  hg resolve kallithea/i18n/ --tool X --config merge-tools.X.executable=python3 --config merge-tools.X.args='scripts/i18n normalized-merge --merge-pot-file full.pot $local $base $other $output'
+
+Land in main repository - stripped
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When the full i18n changes have landed on the ``i18n`` branch, prepare to land
+them on ``stable``::
+
+  hg up -cr stable
+  python3 setup.py extract_messages && cp kallithea/i18n/kallithea.pot full.pot && hg revert kallithea/i18n/kallithea.pot
+
+Consider all new ``i18n`` changes since last merge from ``stable``::
+
+  hg log -G --style compact -r 'only("i18n", children(::stable))'
+
+Graft them one by one (or in collapsed chunks) while normalizing.
+
+If the graft has conflicts, use the ``scripts/i18n`` normalization tool to
+apply ``msgmerge`` and strip before doing 3-way merge and resolving conflicts::
+
+  hg resolve kallithea/i18n/ --tool X --config merge-tools.X.executable=python3 --config merge-tools.X.args='scripts/i18n normalized-merge --merge-pot-file full.pot --strip $local $base $other $output'
+
+When all conflicts have been resolved, continue the graft::
+
+  hg graft --continue
+
+Then make sure any non-conflicting files are normalized and stripped too::
+
+  scripts/i18n normalize-po-files --strip --merge-pot-file full.pot kallithea/i18n/*/LC_MESSAGES/kallithea.po
+  hg ci --amend --config ui.editor=true
+
+When things have been grafted to the ``stable`` branch, clean up history if
+necessary: clean up the author and commit message when necessary, and perhaps
+merge multiple changesets from same contributor.
+
+Merge back to ``i18n``
+^^^^^^^^^^^^^^^^^^^^^^
+
+For any i18n changes that for some reason have been done on the ``stable``
+branch, apply them manually on the ``i18n`` branch too - perhaps by grafting
+and editing manually. The merge done in this step will `not` take care of it.
+If the verification step done a bit later points out that something has been
+missed, strip and go back to this point.
+
+Then merge back to the ``i18n`` branch using normalization while keeping the
+full ``.po`` files, and updating the full ``.pot`` and ``.po`` to current
+state::
+
+  hg up -cr i18n
+  hg merge stable --tool internal:fail
+  hg revert kallithea/i18n/*/LC_MESSAGES/*.po -r .
+  hg resolve -m kallithea/i18n/*/LC_MESSAGES/*.po
+  hg resolve -l  # verify all conflicts have been resolved
+  python3 setup.py extract_messages && cp kallithea/i18n/kallithea.pot full.pot
+  scripts/i18n normalize-po-files --merge-pot-file full.pot kallithea/i18n/*/LC_MESSAGES/kallithea.po
+  hg commit  # "Merge from stable"
+
+Note: ``normalize-po-files`` can also pretty much be done manually with::
+
+  for po in kallithea/i18n/*/LC_MESSAGES/kallithea.po; do msgmerge --width=76 --backup=none --previous --update $po full.pot ; done
+
+Note: Additional merges from ``stable`` to ``i18n`` can be done any time.
+
+Verify
+^^^^^^
+
+Verify things are in sync between the full ``i18n`` branch and the stripped
+``stable`` branch::
+
+  hg up -cr stable
+  hg revert -a -r i18n
+  python3 setup.py extract_messages && cp kallithea/i18n/kallithea.pot full.pot && hg revert kallithea/i18n/kallithea.pot
+  scripts/i18n normalize-po-files --strip --merge-pot-file full.pot kallithea/i18n/*/LC_MESSAGES/kallithea.po
+  hg diff
+
+If there is a diff, figure out where it came from, go back and fix the root
+cause, and redo the graft/merge.
+
+Push
+^^^^
+
+The changes on the ``stable`` branch should now be ready for pushing - verify
+the actual changes with a thorough review of::
+
+  hg out -pvr stable
+
+When ``stable`` changes have been pushed, also push the ``i18n`` branch to the
+``kallithea-i18n`` repo so Weblate can see it.
+
+
 .. _Weblate: http://weblate.org/
--- a/kallithea/i18n/hu/LC_MESSAGES/kallithea.po	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/i18n/hu/LC_MESSAGES/kallithea.po	Mon May 04 19:24:04 2020 +0200
@@ -5,7 +5,7 @@
 msgstr ""
 "Project-Id-Version: Kallithea 0.3\n"
 "Report-Msgid-Bugs-To: translations@kallithea-scm.org\n"
-"POT-Creation-Date: 2020-01-05 04:10+0100\n"
+"POT-Creation-Date: 2020-05-04 19:12+0200\n"
 "PO-Revision-Date: 2015-04-11 00:59+0200\n"
 "Last-Translator: Balázs Úr <urbalazs@gmail.com>\n"
 "Language-Team: Hungarian <https://hosted.weblate.org/projects/kallithea/"
@@ -18,14 +18,14 @@
 "X-Generator: Weblate 2.3-dev\n"
 
 #: kallithea/controllers/changelog.py:67
-#: kallithea/controllers/pullrequests.py:250 kallithea/lib/base.py:602
+#: kallithea/controllers/pullrequests.py:247 kallithea/lib/base.py:602
 msgid "There are no changesets yet"
 msgstr ""
 
 #: kallithea/controllers/admin/permissions.py:64
 #: kallithea/controllers/admin/permissions.py:68
 #: kallithea/controllers/admin/permissions.py:72
-#: kallithea/controllers/changelog.py:137
+#: kallithea/controllers/changelog.py:136
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:7
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:88
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:7
@@ -34,36 +34,36 @@
 msgid "None"
 msgstr ""
 
-#: kallithea/controllers/changelog.py:140 kallithea/controllers/files.py:189
+#: kallithea/controllers/changelog.py:139 kallithea/controllers/files.py:189
 msgid "(closed)"
 msgstr ""
 
-#: kallithea/controllers/changeset.py:81
+#: kallithea/controllers/changeset.py:82
 msgid "Show whitespace"
 msgstr ""
 
-#: kallithea/controllers/changeset.py:88
-#: kallithea/controllers/changeset.py:95
+#: kallithea/controllers/changeset.py:89
+#: kallithea/controllers/changeset.py:96
 #: kallithea/templates/files/diff_2way.html:55
 msgid "Ignore whitespace"
 msgstr ""
 
-#: kallithea/controllers/changeset.py:161
+#: kallithea/controllers/changeset.py:162
 #, python-format
 msgid "Increase diff context to %(num)s lines"
 msgstr ""
 
-#: kallithea/controllers/changeset.py:201
+#: kallithea/controllers/changeset.py:202
 msgid "No permission to change status"
 msgstr ""
 
-#: kallithea/controllers/changeset.py:212
+#: kallithea/controllers/changeset.py:213
 #, python-format
 msgid "Successfully deleted pull request %s"
 msgstr ""
 
 #: kallithea/controllers/changeset.py:319 kallithea/controllers/files.py:89
-#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:700
+#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:697
 msgid "Such revision does not exist for this repository"
 msgstr ""
 
@@ -76,61 +76,61 @@
 msgid "Cannot compare repositories of different types"
 msgstr ""
 
-#: kallithea/controllers/compare.py:246
+#: kallithea/controllers/compare.py:247
 msgid "Cannot show empty diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:248
+#: kallithea/controllers/compare.py:249
 msgid "No ancestor found for merge diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:252
+#: kallithea/controllers/compare.py:253
 msgid "Multiple merge ancestors found for merge compare"
 msgstr ""
 
-#: kallithea/controllers/compare.py:268
+#: kallithea/controllers/compare.py:269
 msgid "Cannot compare repositories without using common ancestor"
 msgstr ""
 
-#: kallithea/controllers/error.py:71
+#: kallithea/controllers/error.py:67
 msgid "No response"
 msgstr ""
 
-#: kallithea/controllers/error.py:72
+#: kallithea/controllers/error.py:68
 msgid "Unknown error"
 msgstr ""
 
-#: kallithea/controllers/error.py:85
+#: kallithea/controllers/error.py:81
 msgid ""
 "The request could not be understood by the server due to malformed syntax."
 msgstr ""
 
+#: kallithea/controllers/error.py:84
+msgid "Unauthorized access to resource"
+msgstr ""
+
+#: kallithea/controllers/error.py:86
+msgid "You don't have permission to view this page"
+msgstr ""
+
 #: kallithea/controllers/error.py:88
-msgid "Unauthorized access to resource"
+msgid "The resource could not be found"
 msgstr ""
 
 #: kallithea/controllers/error.py:90
-msgid "You don't have permission to view this page"
-msgstr ""
-
-#: kallithea/controllers/error.py:92
-msgid "The resource could not be found"
-msgstr ""
-
-#: kallithea/controllers/error.py:94
 msgid ""
 "The server encountered an unexpected condition which prevented it from "
 "fulfilling the request."
 msgstr ""
 
-#: kallithea/controllers/feed.py:63
+#: kallithea/controllers/feed.py:59
 #, python-format
 msgid "%s committed on %s"
 msgstr ""
 
-#: kallithea/controllers/feed.py:88
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/controllers/feed.py:84
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/compare/compare_diff.html:95
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
@@ -138,12 +138,12 @@
 msgid "Changeset was too big and was cut off..."
 msgstr ""
 
-#: kallithea/controllers/feed.py:111 kallithea/controllers/feed.py:140
+#: kallithea/controllers/feed.py:107
 #, python-format
 msgid "%s %s feed"
 msgstr ""
 
-#: kallithea/controllers/feed.py:113 kallithea/controllers/feed.py:142
+#: kallithea/controllers/feed.py:109
 #, python-format
 msgid "Changes on %s repository"
 msgstr ""
@@ -161,103 +161,103 @@
 msgid "%s at %s"
 msgstr ""
 
-#: kallithea/controllers/files.py:296
+#: kallithea/controllers/files.py:295
 msgid "You can only delete files with revision being a valid branch"
 msgstr ""
 
-#: kallithea/controllers/files.py:307
+#: kallithea/controllers/files.py:306
 #, python-format
 msgid "Deleted file %s via Kallithea"
 msgstr ""
 
-#: kallithea/controllers/files.py:331
+#: kallithea/controllers/files.py:330
 #, python-format
 msgid "Successfully deleted file %s"
 msgstr ""
 
-#: kallithea/controllers/files.py:335 kallithea/controllers/files.py:394
-#: kallithea/controllers/files.py:469
+#: kallithea/controllers/files.py:334 kallithea/controllers/files.py:392
+#: kallithea/controllers/files.py:467
 msgid "Error occurred during commit"
 msgstr ""
 
-#: kallithea/controllers/files.py:350
+#: kallithea/controllers/files.py:349
 msgid "You can only edit files with revision being a valid branch"
 msgstr ""
 
-#: kallithea/controllers/files.py:364
+#: kallithea/controllers/files.py:363
 #, python-format
 msgid "Edited file %s via Kallithea"
 msgstr ""
 
-#: kallithea/controllers/files.py:380
+#: kallithea/controllers/files.py:378
 msgid "No changes"
 msgstr ""
 
-#: kallithea/controllers/files.py:390 kallithea/controllers/files.py:458
+#: kallithea/controllers/files.py:388 kallithea/controllers/files.py:456
 #, python-format
 msgid "Successfully committed to %s"
 msgstr ""
 
-#: kallithea/controllers/files.py:409
+#: kallithea/controllers/files.py:407
 msgid "Added file via Kallithea"
 msgstr ""
 
-#: kallithea/controllers/files.py:430
+#: kallithea/controllers/files.py:428
 msgid "No content"
 msgstr ""
 
-#: kallithea/controllers/files.py:434
+#: kallithea/controllers/files.py:432
 msgid "No filename"
 msgstr ""
 
-#: kallithea/controllers/files.py:461
+#: kallithea/controllers/files.py:459
 msgid "Location must be relative path and must not contain .. in path"
 msgstr ""
 
-#: kallithea/controllers/files.py:493
+#: kallithea/controllers/files.py:491
 msgid "Downloads disabled"
 msgstr ""
 
+#: kallithea/controllers/files.py:502
+#, python-format
+msgid "Unknown revision %s"
+msgstr ""
+
 #: kallithea/controllers/files.py:504
-#, python-format
-msgid "Unknown revision %s"
+msgid "Empty repository"
 msgstr ""
 
 #: kallithea/controllers/files.py:506
-msgid "Empty repository"
-msgstr ""
-
-#: kallithea/controllers/files.py:508
 msgid "Unknown archive type"
 msgstr ""
 
-#: kallithea/controllers/files.py:729
+#: kallithea/controllers/files.py:726
 #: kallithea/templates/changeset/changeset_range.html:9
 #: kallithea/templates/email_templates/pull_request.html:64
 #: kallithea/templates/pullrequests/pullrequest.html:84
 msgid "Changesets"
 msgstr ""
 
-#: kallithea/controllers/files.py:730
-#: kallithea/controllers/pullrequests.py:182 kallithea/model/scm.py:676
+#: kallithea/controllers/files.py:727
+#: kallithea/controllers/pullrequests.py:174 kallithea/model/scm.py:663
 msgid "Branches"
 msgstr ""
 
-#: kallithea/controllers/files.py:731
-#: kallithea/controllers/pullrequests.py:183 kallithea/model/scm.py:687
+#: kallithea/controllers/files.py:728
+#: kallithea/controllers/pullrequests.py:175 kallithea/model/scm.py:674
 msgid "Tags"
 msgstr ""
 
-#: kallithea/controllers/forks.py:174
+#: kallithea/controllers/forks.py:175
 #, python-format
 msgid "An error occurred during repository forking %s"
 msgstr ""
 
-#: kallithea/controllers/home.py:79
+#: kallithea/controllers/home.py:77
 msgid "Groups"
 msgstr ""
 
-#: kallithea/controllers/home.py:89
+#: kallithea/controllers/home.py:87
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:90
 #: kallithea/templates/admin/repos/repo_add.html:12
 #: kallithea/templates/admin/repos/repo_add.html:16
@@ -269,48 +269,52 @@
 msgid "Repositories"
 msgstr ""
 
-#: kallithea/controllers/home.py:122
+#: kallithea/controllers/home.py:119
 #: kallithea/templates/files/files_add.html:32
 #: kallithea/templates/files/files_delete.html:23
 #: kallithea/templates/files/files_edit.html:32
 msgid "Branch"
 msgstr ""
 
-#: kallithea/controllers/home.py:128
+#: kallithea/controllers/home.py:125
 msgid "Closed Branches"
 msgstr ""
 
-#: kallithea/controllers/home.py:134
+#: kallithea/controllers/home.py:131
 msgid "Tag"
 msgstr ""
 
-#: kallithea/controllers/home.py:140
+#: kallithea/controllers/home.py:137
 msgid "Bookmark"
 msgstr ""
 
-#: kallithea/controllers/journal.py:112 kallithea/controllers/journal.py:154
+#: kallithea/controllers/journal.py:146 kallithea/controllers/journal.py:157
 #: kallithea/templates/journal/public_journal.html:4
 #: kallithea/templates/journal/public_journal.html:18
 msgid "Public Journal"
 msgstr ""
 
-#: kallithea/controllers/journal.py:116 kallithea/controllers/journal.py:158
+#: kallithea/controllers/journal.py:150 kallithea/controllers/journal.py:161
 #: kallithea/templates/base/base.html:290
 #: kallithea/templates/journal/journal.html:5
 #: kallithea/templates/journal/journal.html:13
 msgid "Journal"
 msgstr ""
 
-#: kallithea/controllers/login.py:140 kallithea/controllers/login.py:185
+#: kallithea/controllers/login.py:109
+msgid "Authentication failed."
+msgstr ""
+
+#: kallithea/controllers/login.py:142 kallithea/controllers/login.py:187
 msgid "Bad captcha"
 msgstr ""
 
-#: kallithea/controllers/login.py:146
+#: kallithea/controllers/login.py:148
 #, python-format
 msgid "You have successfully registered with %s"
 msgstr ""
 
-#: kallithea/controllers/login.py:190
+#: kallithea/controllers/login.py:192
 msgid "A password reset confirmation code has been sent"
 msgstr ""
 
@@ -323,226 +327,226 @@
 msgid "Successfully updated password"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:68
+#: kallithea/controllers/pullrequests.py:67
 #, python-format
 msgid "Invalid reviewer \"%s\" specified"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:131
+#: kallithea/controllers/pullrequests.py:123
 #, python-format
 msgid "%s (closed)"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:158
+#: kallithea/controllers/pullrequests.py:150
 #: kallithea/templates/changeset/changeset.html:12
 msgid "Changeset"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:179
+#: kallithea/controllers/pullrequests.py:171
 msgid "Special"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:180
+#: kallithea/controllers/pullrequests.py:172
 msgid "Peer branches"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:181 kallithea/model/scm.py:682
+#: kallithea/controllers/pullrequests.py:173 kallithea/model/scm.py:669
 msgid "Bookmarks"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:318
+#: kallithea/controllers/pullrequests.py:315
 #, python-format
 msgid "Error creating pull request: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:345
-#: kallithea/controllers/pullrequests.py:368
+#: kallithea/controllers/pullrequests.py:342
+#: kallithea/controllers/pullrequests.py:365
 msgid "Error occurred while creating pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:350
+#: kallithea/controllers/pullrequests.py:347
 msgid "Successfully opened new pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:373
+#: kallithea/controllers/pullrequests.py:370
 msgid "New pull request iteration created"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:401
+#: kallithea/controllers/pullrequests.py:398
 #, python-format
 msgid "Meanwhile, the following reviewers have been added: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:405
+#: kallithea/controllers/pullrequests.py:402
 #, python-format
 msgid "Meanwhile, the following reviewers have been removed: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:421
-#: kallithea/model/pull_request.py:232
+#: kallithea/controllers/pullrequests.py:418
+#: kallithea/model/pull_request.py:230
 msgid "No description"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:430
+#: kallithea/controllers/pullrequests.py:427
 msgid "Pull request updated"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:443
+#: kallithea/controllers/pullrequests.py:440
 msgid "Successfully deleted pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:479
+#: kallithea/controllers/pullrequests.py:476
 #, python-format
 msgid "Revision %s not found in %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:506
+#: kallithea/controllers/pullrequests.py:504
 #, python-format
 msgid "Error: changesets not found when displaying pull request from %s."
 msgstr ""
 
+#: kallithea/controllers/pullrequests.py:518
+#, python-format
+msgid "This pull request has already been merged to %s."
+msgstr ""
+
 #: kallithea/controllers/pullrequests.py:520
-#, python-format
-msgid "This pull request has already been merged to %s."
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:522
 msgid "This pull request has been closed and can not be updated."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:546
+#: kallithea/controllers/pullrequests.py:539
 #, python-format
 msgid "The following additional changes are available on %s:"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:548
-#: kallithea/controllers/pullrequests.py:552
+#: kallithea/controllers/pullrequests.py:541
+#: kallithea/controllers/pullrequests.py:545
 msgid "No additional changesets found for iterating on this pull request."
 msgstr ""
 
+#: kallithea/controllers/pullrequests.py:553
+#, python-format
+msgid "Note: Branch %s has another head: %s."
+msgstr ""
+
 #: kallithea/controllers/pullrequests.py:560
-#, python-format
-msgid "Note: Branch %s has another head: %s."
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:567
 msgid "Git pull requests don't support iterating yet."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:569
+#: kallithea/controllers/pullrequests.py:562
 #, python-format
 msgid ""
 "Error: some changesets not found when displaying pull request from %s."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:593
+#: kallithea/controllers/pullrequests.py:586
 msgid "The diff can't be shown - the PR revisions could not be found."
 msgstr ""
 
+#: kallithea/controllers/search.py:132
+msgid "Invalid search query. Try quoting it."
+msgstr ""
+
 #: kallithea/controllers/search.py:136
-msgid "Invalid search query. Try quoting it."
-msgstr ""
-
-#: kallithea/controllers/search.py:140
 msgid "The server has no search index."
 msgstr ""
 
-#: kallithea/controllers/search.py:143
+#: kallithea/controllers/search.py:139
 msgid "An error occurred during search operation."
 msgstr ""
 
-#: kallithea/controllers/summary.py:168
-#: kallithea/templates/summary/summary.html:412
+#: kallithea/controllers/summary.py:169
+#: kallithea/templates/summary/summary.html:410
 msgid "No data ready yet"
 msgstr ""
 
-#: kallithea/controllers/summary.py:171
+#: kallithea/controllers/summary.py:172
 #: kallithea/templates/summary/summary.html:97
 msgid "Statistics are disabled for this repository"
 msgstr ""
 
-#: kallithea/controllers/admin/auth_settings.py:137
+#: kallithea/controllers/admin/auth_settings.py:136
 msgid "Auth settings updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/auth_settings.py:148
+#: kallithea/controllers/admin/auth_settings.py:147
 msgid "error occurred during update of auth settings"
 msgstr ""
 
-#: kallithea/controllers/admin/defaults.py:75
+#: kallithea/controllers/admin/defaults.py:74
 msgid "Default settings updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/defaults.py:90
+#: kallithea/controllers/admin/defaults.py:89
 msgid "Error occurred during update of defaults"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:59
 #: kallithea/controllers/admin/my_account.py:232
-#: kallithea/controllers/admin/users.py:248
+#: kallithea/controllers/admin/users.py:246
 msgid "Forever"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:60
 #: kallithea/controllers/admin/my_account.py:233
-#: kallithea/controllers/admin/users.py:249
+#: kallithea/controllers/admin/users.py:247
 msgid "5 minutes"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:61
 #: kallithea/controllers/admin/my_account.py:234
-#: kallithea/controllers/admin/users.py:250
+#: kallithea/controllers/admin/users.py:248
 msgid "1 hour"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:62
 #: kallithea/controllers/admin/my_account.py:235
-#: kallithea/controllers/admin/users.py:251
+#: kallithea/controllers/admin/users.py:249
 msgid "1 day"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:63
 #: kallithea/controllers/admin/my_account.py:236
-#: kallithea/controllers/admin/users.py:252
+#: kallithea/controllers/admin/users.py:250
 msgid "1 month"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:67
 #: kallithea/controllers/admin/my_account.py:238
-#: kallithea/controllers/admin/users.py:254
+#: kallithea/controllers/admin/users.py:252
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:65
 #: kallithea/templates/admin/users/user_edit_api_keys.html:65
 msgid "Lifetime"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:142
+#: kallithea/controllers/admin/gists.py:148
 msgid "Error occurred during gist creation"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:158
+#: kallithea/controllers/admin/gists.py:164
 #, python-format
 msgid "Deleted gist %s"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:198
+#: kallithea/controllers/admin/gists.py:207
 msgid "Unmodified"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:228
+#: kallithea/controllers/admin/gists.py:237
 msgid "Successfully updated gist content"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:233
+#: kallithea/controllers/admin/gists.py:242
 msgid "Successfully updated gist data"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:236
+#: kallithea/controllers/admin/gists.py:245
 #, python-format
 msgid "Error occurred during update of gist %s"
 msgstr ""
 
-#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:211
-#: kallithea/model/user.py:232
+#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:205
+#: kallithea/model/user.py:226
 msgid "You can't edit this user since it's crucial for entire application"
 msgstr ""
 
@@ -551,7 +555,7 @@
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:134
-#: kallithea/controllers/admin/users.py:181
+#: kallithea/controllers/admin/users.py:179
 #, python-format
 msgid "Error occurred during update of user %s"
 msgstr ""
@@ -561,44 +565,44 @@
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:209
-#: kallithea/controllers/admin/users.py:367
+#: kallithea/controllers/admin/users.py:365
 #, python-format
 msgid "Added email %s to user"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:215
-#: kallithea/controllers/admin/users.py:373
+#: kallithea/controllers/admin/users.py:371
 msgid "An error occurred during email saving"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:224
-#: kallithea/controllers/admin/users.py:383
+#: kallithea/controllers/admin/users.py:381
 msgid "Removed email from user"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:248
-#: kallithea/controllers/admin/users.py:271
+#: kallithea/controllers/admin/users.py:269
 msgid "API key successfully created"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:257
-#: kallithea/controllers/admin/users.py:281
+#: kallithea/controllers/admin/users.py:279
 msgid "API key successfully reset"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:261
-#: kallithea/controllers/admin/users.py:285
+#: kallithea/controllers/admin/users.py:283
 msgid "API key successfully deleted"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:281
-#: kallithea/controllers/admin/users.py:456
+#: kallithea/controllers/admin/users.py:454
 #, python-format
 msgid "SSH key %s successfully added"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:293
-#: kallithea/controllers/admin/users.py:470
+#: kallithea/controllers/admin/users.py:468
 msgid "SSH key successfully deleted"
 msgstr ""
 
@@ -674,11 +678,11 @@
 msgid "Allowed with automatic account activation"
 msgstr ""
 
-#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1673
+#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1578
 msgid "Manual activation of external account"
 msgstr ""
 
-#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1674
+#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1579
 msgid "Automatic activation of external account"
 msgstr ""
 
@@ -700,340 +704,332 @@
 msgid "Error occurred during update of permissions"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:172
+#: kallithea/controllers/admin/repo_groups.py:165
 #, python-format
 msgid "Error occurred during creation of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:177
+#: kallithea/controllers/admin/repo_groups.py:172
 #, python-format
 msgid "Created repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:224
+#: kallithea/controllers/admin/repo_groups.py:219
 #, python-format
 msgid "Updated repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:240
+#: kallithea/controllers/admin/repo_groups.py:235
 #, python-format
 msgid "Error occurred during update of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:250
+#: kallithea/controllers/admin/repo_groups.py:245
 #, python-format
 msgid "This group contains %s repositories and cannot be deleted"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:257
+#: kallithea/controllers/admin/repo_groups.py:252
 #, python-format
 msgid "This group contains %s subgroups and cannot be deleted"
 msgstr ""
 
+#: kallithea/controllers/admin/repo_groups.py:258
+#, python-format
+msgid "Removed repository group %s"
+msgstr ""
+
 #: kallithea/controllers/admin/repo_groups.py:263
 #, python-format
-msgid "Removed repository group %s"
-msgstr ""
-
-#: kallithea/controllers/admin/repo_groups.py:268
-#, python-format
 msgid "Error occurred during deletion of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:352
-#: kallithea/controllers/admin/repo_groups.py:382
-#: kallithea/controllers/admin/user_groups.py:294
+#: kallithea/controllers/admin/repo_groups.py:347
+#: kallithea/controllers/admin/repo_groups.py:377
+#: kallithea/controllers/admin/user_groups.py:290
 msgid "Cannot revoke permission for yourself as admin"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:367
+#: kallithea/controllers/admin/repo_groups.py:362
 msgid "Repository group permissions updated"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:399
-#: kallithea/controllers/admin/repos.py:358
-#: kallithea/controllers/admin/user_groups.py:306
+#: kallithea/controllers/admin/repo_groups.py:394
+#: kallithea/controllers/admin/repos.py:357
+#: kallithea/controllers/admin/user_groups.py:302
 msgid "An error occurred during revoking of permission"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:136
+#: kallithea/controllers/admin/repos.py:137
 #, python-format
 msgid "Error creating repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:194
+#: kallithea/controllers/admin/repos.py:193
 #, python-format
 msgid "Created repository %s from %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:203
+#: kallithea/controllers/admin/repos.py:202
 #, python-format
 msgid "Forked repository %s as %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:206
+#: kallithea/controllers/admin/repos.py:205
 #, python-format
 msgid "Created repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:235
+#: kallithea/controllers/admin/repos.py:234
 #, python-format
 msgid "Repository %s updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:255
+#: kallithea/controllers/admin/repos.py:254
 #, python-format
 msgid "Error occurred during update of repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:273
+#: kallithea/controllers/admin/repos.py:272
 #, python-format
 msgid "Detached %s forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:276
+#: kallithea/controllers/admin/repos.py:275
 #, python-format
 msgid "Deleted %s forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:281
+#: kallithea/controllers/admin/repos.py:280
 #, python-format
 msgid "Deleted repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:284
+#: kallithea/controllers/admin/repos.py:283
 #, python-format
 msgid "Cannot delete repository %s which still has forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:289
+#: kallithea/controllers/admin/repos.py:288
 #, python-format
 msgid "An error occurred during deletion of %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:329
+#: kallithea/controllers/admin/repos.py:328
 msgid "Repository permissions updated"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:388
+#: kallithea/controllers/admin/repos.py:387
 #, python-format
 msgid "Field validation error: %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:391
+#: kallithea/controllers/admin/repos.py:390
 #, python-format
 msgid "An error occurred during creation of field: %r"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:402
+#: kallithea/controllers/admin/repos.py:401
 msgid "An error occurred during removal of field"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:416
+#: kallithea/controllers/admin/repos.py:415
 msgid "-- Not a fork --"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:448
+#: kallithea/controllers/admin/repos.py:447
 msgid "Updated repository visibility in public journal"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:452
+#: kallithea/controllers/admin/repos.py:451
 msgid "An error occurred during setting this repository in public journal"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:468
+#: kallithea/controllers/admin/repos.py:467
 msgid "Nothing"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:470
+#: kallithea/controllers/admin/repos.py:469
 #, python-format
 msgid "Marked repository %s as fork of %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:477
+#: kallithea/controllers/admin/repos.py:476
 msgid "An error occurred during this operation"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:490
-msgid "Cache invalidation successful"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:494
-msgid "An error occurred during cache invalidation"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:507
+#: kallithea/controllers/admin/repos.py:488
 msgid "Pulled from remote location"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:510
+#: kallithea/controllers/admin/repos.py:491
 msgid "An error occurred during pull from remote location"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:541
+#: kallithea/controllers/admin/repos.py:522
 msgid "An error occurred during deletion of repository stats"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:131
+#: kallithea/controllers/admin/settings.py:132
 msgid "Updated VCS settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:135 kallithea/lib/utils.py:237
+#: kallithea/controllers/admin/settings.py:136
 msgid ""
 "Unable to activate hgsubversion support. The \"hgsubversion\" library is "
 "missing"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:141
-#: kallithea/controllers/admin/settings.py:233
+#: kallithea/controllers/admin/settings.py:142
+#: kallithea/controllers/admin/settings.py:234
 msgid "Error occurred while updating application settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:176
+#: kallithea/controllers/admin/settings.py:177
 #, python-format
 msgid "Repositories successfully rescanned. Added: %s. Removed: %s."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:188
+#: kallithea/controllers/admin/settings.py:189
 #, fuzzy, python-format
 #| msgid "Private Repository"
 msgid "Invalidated %s repositories"
 msgstr "Tároló törlése"
 
-#: kallithea/controllers/admin/settings.py:229
+#: kallithea/controllers/admin/settings.py:230
 msgid "Updated application settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:283
+#: kallithea/controllers/admin/settings.py:284
 msgid "Updated visualisation settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:288
+#: kallithea/controllers/admin/settings.py:289
 msgid "Error occurred during updating visualisation settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:312
+#: kallithea/controllers/admin/settings.py:313
 msgid "Please enter email address"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:327
+#: kallithea/controllers/admin/settings.py:328
 msgid "Send email task created"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:355
+#: kallithea/controllers/admin/settings.py:356
 msgid "Hook already exists"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:357
+#: kallithea/controllers/admin/settings.py:358
 msgid "Builtin hooks are read-only. Please use another hook name."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:360
+#: kallithea/controllers/admin/settings.py:361
 msgid "Added new hook"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:376
+#: kallithea/controllers/admin/settings.py:377
 msgid "Updated hooks"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:380
+#: kallithea/controllers/admin/settings.py:381
 msgid "Error occurred during hook creation"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:404
+#: kallithea/controllers/admin/settings.py:405
 msgid "Whoosh reindex task scheduled"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:138
+#: kallithea/controllers/admin/user_groups.py:134
 #, python-format
 msgid "Created user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:151
+#: kallithea/controllers/admin/user_groups.py:147
 #, python-format
 msgid "Error occurred during creation of user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:179
+#: kallithea/controllers/admin/user_groups.py:175
 #, python-format
 msgid "Updated user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:201
+#: kallithea/controllers/admin/user_groups.py:197
 #, python-format
 msgid "Error occurred during update of user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:212
+#: kallithea/controllers/admin/user_groups.py:208
 msgid "Successfully deleted user group"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:217
+#: kallithea/controllers/admin/user_groups.py:213
 msgid "An error occurred during deletion of user group"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:273
+#: kallithea/controllers/admin/user_groups.py:269
 msgid "Target group cannot be the same"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:279
+#: kallithea/controllers/admin/user_groups.py:275
 msgid "User group permissions updated"
 msgstr ""
 
+#: kallithea/controllers/admin/user_groups.py:384
+#: kallithea/controllers/admin/users.py:336
+msgid "Updated permissions"
+msgstr ""
+
 #: kallithea/controllers/admin/user_groups.py:388
-#: kallithea/controllers/admin/users.py:338
-msgid "Updated permissions"
-msgstr ""
-
-#: kallithea/controllers/admin/user_groups.py:392
-#: kallithea/controllers/admin/users.py:342
+#: kallithea/controllers/admin/users.py:340
 msgid "An error occurred during permissions saving"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:123
+#: kallithea/controllers/admin/users.py:121
 #, python-format
 msgid "Created user %s"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:138
+#: kallithea/controllers/admin/users.py:136
 #, python-format
 msgid "Error occurred during creation of user %s"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:162
+#: kallithea/controllers/admin/users.py:160
 msgid "User updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:190
+#: kallithea/controllers/admin/users.py:188
 msgid "Successfully deleted user"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:195
+#: kallithea/controllers/admin/users.py:193
 msgid "An error occurred during deletion of user"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:203
+#: kallithea/controllers/admin/users.py:201
 msgid "The default user cannot be edited"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:409
+#: kallithea/controllers/admin/users.py:407
 #, python-format
 msgid "Added IP address %s to user whitelist"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:415
+#: kallithea/controllers/admin/users.py:413
 msgid "An error occurred while adding IP address"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:427
+#: kallithea/controllers/admin/users.py:425
 msgid "Removed IP address from user whitelist"
 msgstr ""
 
-#: kallithea/lib/auth.py:684
+#: kallithea/lib/auth.py:634
 msgid "You need to be a registered user to perform this action"
 msgstr ""
 
-#: kallithea/lib/auth.py:712
+#: kallithea/lib/auth.py:662
 msgid "You need to be signed in to view this page"
 msgstr ""
 
@@ -1064,170 +1060,170 @@
 "Changeset was too big and was cut off, use diff menu to display this diff"
 msgstr ""
 
-#: kallithea/lib/diffs.py:224
+#: kallithea/lib/diffs.py:223
 msgid "No changes detected"
 msgstr ""
 
-#: kallithea/lib/helpers.py:653
+#: kallithea/lib/helpers.py:670
 #, python-format
 msgid "Deleted branch: %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:655
+#: kallithea/lib/helpers.py:672
 #, python-format
 msgid "Created tag: %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:666
+#: kallithea/lib/helpers.py:683
 #, python-format
 msgid "Changeset %s not found"
 msgstr ""
 
-#: kallithea/lib/helpers.py:715
+#: kallithea/lib/helpers.py:732
 #, python-format
 msgid "Show all combined changesets %s->%s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:721
+#: kallithea/lib/helpers.py:738
 msgid "Compare view"
 msgstr ""
 
-#: kallithea/lib/helpers.py:740
+#: kallithea/lib/helpers.py:757
 msgid "and"
 msgstr ""
 
-#: kallithea/lib/helpers.py:741
+#: kallithea/lib/helpers.py:758
 #, python-format
 msgid "%s more"
 msgstr ""
 
-#: kallithea/lib/helpers.py:742
+#: kallithea/lib/helpers.py:759
 #: kallithea/templates/changelog/changelog.html:43
 msgid "revisions"
 msgstr ""
 
-#: kallithea/lib/helpers.py:766
+#: kallithea/lib/helpers.py:783
 #, python-format
 msgid "Fork name %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:787
+#: kallithea/lib/helpers.py:804
 #, python-format
 msgid "Pull request %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:797
+#: kallithea/lib/helpers.py:814
 msgid "[deleted] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:799 kallithea/lib/helpers.py:811
+#: kallithea/lib/helpers.py:816 kallithea/lib/helpers.py:828
 msgid "[created] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:801
+#: kallithea/lib/helpers.py:818
 msgid "[created] repository as fork"
 msgstr ""
 
-#: kallithea/lib/helpers.py:803 kallithea/lib/helpers.py:813
+#: kallithea/lib/helpers.py:820 kallithea/lib/helpers.py:830
 msgid "[forked] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:805 kallithea/lib/helpers.py:815
+#: kallithea/lib/helpers.py:822 kallithea/lib/helpers.py:832
 msgid "[updated] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:807
+#: kallithea/lib/helpers.py:824
 msgid "[downloaded] archive from repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:809
+#: kallithea/lib/helpers.py:826
 msgid "[delete] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:817
+#: kallithea/lib/helpers.py:834
 msgid "[created] user"
 msgstr ""
 
-#: kallithea/lib/helpers.py:819
+#: kallithea/lib/helpers.py:836
 msgid "[updated] user"
 msgstr ""
 
-#: kallithea/lib/helpers.py:821
+#: kallithea/lib/helpers.py:838
 msgid "[created] user group"
 msgstr ""
 
-#: kallithea/lib/helpers.py:823
+#: kallithea/lib/helpers.py:840
 msgid "[updated] user group"
 msgstr ""
 
-#: kallithea/lib/helpers.py:825
+#: kallithea/lib/helpers.py:842
 msgid "[commented] on revision in repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:827
+#: kallithea/lib/helpers.py:844
 msgid "[commented] on pull request for"
 msgstr ""
 
-#: kallithea/lib/helpers.py:829
+#: kallithea/lib/helpers.py:846
 msgid "[closed] pull request for"
 msgstr ""
 
-#: kallithea/lib/helpers.py:831
+#: kallithea/lib/helpers.py:848
 msgid "[pushed] into"
 msgstr ""
 
-#: kallithea/lib/helpers.py:833
+#: kallithea/lib/helpers.py:850
 msgid "[committed via Kallithea] into repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:835
+#: kallithea/lib/helpers.py:852
 msgid "[pulled from remote] into repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:837
+#: kallithea/lib/helpers.py:854
 msgid "[pulled] from"
 msgstr ""
 
-#: kallithea/lib/helpers.py:839
+#: kallithea/lib/helpers.py:856
 msgid "[started following] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:841
+#: kallithea/lib/helpers.py:858
 msgid "[stopped following] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:961
+#: kallithea/lib/helpers.py:975
 #, python-format
 msgid " and %s more"
 msgstr ""
 
-#: kallithea/lib/helpers.py:965
+#: kallithea/lib/helpers.py:979
 #: kallithea/templates/compare/compare_diff.html:69
 #: kallithea/templates/pullrequests/pullrequest_show.html:297
 msgid "No files"
 msgstr ""
 
-#: kallithea/lib/helpers.py:990
+#: kallithea/lib/helpers.py:1004
 msgid "new file"
 msgstr ""
 
-#: kallithea/lib/helpers.py:993
+#: kallithea/lib/helpers.py:1007
 msgid "mod"
 msgstr ""
 
-#: kallithea/lib/helpers.py:996
+#: kallithea/lib/helpers.py:1010
 msgid "del"
 msgstr ""
 
-#: kallithea/lib/helpers.py:999
+#: kallithea/lib/helpers.py:1013
 msgid "rename"
 msgstr ""
 
-#: kallithea/lib/helpers.py:1004
+#: kallithea/lib/helpers.py:1018
 msgid "chmod"
 msgstr ""
 
-#: kallithea/lib/helpers.py:1297
+#: kallithea/lib/helpers.py:1323
 #, python-format
 msgid ""
 "%s repository is not mapped to db perhaps it was created or renamed from "
@@ -1235,96 +1231,98 @@
 "repositories"
 msgstr ""
 
-#: kallithea/lib/ssh.py:73
+#: kallithea/lib/ssh.py:75
 msgid "SSH key is missing"
 msgstr ""
 
-#: kallithea/lib/ssh.py:77
-msgid "Incorrect SSH key - it must have both a key type and a base64 part"
-msgstr ""
-
-#: kallithea/lib/ssh.py:81
+#: kallithea/lib/ssh.py:79
+msgid ""
+"Incorrect SSH key - it must have both a key type and a base64 part, like "
+"'ssh-rsa ASRNeaZu4FA...xlJp='"
+msgstr ""
+
+#: kallithea/lib/ssh.py:83
 msgid "Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'"
 msgstr ""
 
-#: kallithea/lib/ssh.py:84
+#: kallithea/lib/ssh.py:86
 #, python-format
 msgid "Incorrect SSH key - unexpected characters in base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:89
+#: kallithea/lib/ssh.py:91
 #, python-format
 msgid "Incorrect SSH key - failed to decode base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:92
+#: kallithea/lib/ssh.py:94
 #, python-format
 msgid "Incorrect SSH key - base64 part is not %r as claimed but %r"
 msgstr ""
 
-#: kallithea/lib/utils2.py:334
+#: kallithea/lib/utils2.py:253
 #, python-format
 msgid "%d year"
 msgid_plural "%d years"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:335
+#: kallithea/lib/utils2.py:254
 #, python-format
 msgid "%d month"
 msgid_plural "%d months"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:336
+#: kallithea/lib/utils2.py:255
 #, python-format
 msgid "%d day"
 msgid_plural "%d days"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:337
+#: kallithea/lib/utils2.py:256
 #, python-format
 msgid "%d hour"
 msgid_plural "%d hours"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:338
+#: kallithea/lib/utils2.py:257
 #, python-format
 msgid "%d minute"
 msgid_plural "%d minutes"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:339
+#: kallithea/lib/utils2.py:258
 #, python-format
 msgid "%d second"
 msgid_plural "%d seconds"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:355
+#: kallithea/lib/utils2.py:274
 #, python-format
 msgid "in %s"
 msgstr ""
 
-#: kallithea/lib/utils2.py:357
+#: kallithea/lib/utils2.py:276
 #, python-format
 msgid "%s ago"
 msgstr ""
 
-#: kallithea/lib/utils2.py:359
+#: kallithea/lib/utils2.py:278
 #, python-format
 msgid "in %s and %s"
 msgstr ""
 
-#: kallithea/lib/utils2.py:362
+#: kallithea/lib/utils2.py:281
 #, python-format
 msgid "%s and %s ago"
 msgstr ""
 
-#: kallithea/lib/utils2.py:365
+#: kallithea/lib/utils2.py:284
 msgid "just now"
 msgstr ""
 
@@ -1333,133 +1331,133 @@
 msgid "on line %s"
 msgstr ""
 
-#: kallithea/model/comment.py:221 kallithea/model/pull_request.py:114
+#: kallithea/model/comment.py:219 kallithea/model/pull_request.py:112
 msgid "[Mention]"
 msgstr ""
 
-#: kallithea/model/db.py:1496
+#: kallithea/model/db.py:1411
 msgid "top level"
 msgstr ""
 
-#: kallithea/model/db.py:1637
+#: kallithea/model/db.py:1542
 msgid "Kallithea Administrator"
 msgstr ""
 
-#: kallithea/model/db.py:1639
+#: kallithea/model/db.py:1544
 msgid "Default user has no access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1640
+#: kallithea/model/db.py:1545
 msgid "Default user has read access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1641
+#: kallithea/model/db.py:1546
 msgid "Default user has write access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1642
+#: kallithea/model/db.py:1547
 msgid "Default user has admin access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1644
+#: kallithea/model/db.py:1549
 msgid "Default user has no access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1645
+#: kallithea/model/db.py:1550
 msgid "Default user has read access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1646
+#: kallithea/model/db.py:1551
 msgid "Default user has write access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1647
+#: kallithea/model/db.py:1552
 msgid "Default user has admin access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1649
+#: kallithea/model/db.py:1554
 msgid "Default user has no access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1650
+#: kallithea/model/db.py:1555
 msgid "Default user has read access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1651
+#: kallithea/model/db.py:1556
 msgid "Default user has write access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1652
+#: kallithea/model/db.py:1557
 msgid "Default user has admin access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1654
+#: kallithea/model/db.py:1559
 msgid "Only admins can create repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1655
+#: kallithea/model/db.py:1560
 msgid "Non-admins can create repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1657
+#: kallithea/model/db.py:1562
 msgid "Only admins can create user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1658
+#: kallithea/model/db.py:1563
 msgid "Non-admins can create user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1660
+#: kallithea/model/db.py:1565
 msgid "Only admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1661
+#: kallithea/model/db.py:1566
 msgid "Non-admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1663
+#: kallithea/model/db.py:1568
 msgid ""
 "Repository creation enabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1664
+#: kallithea/model/db.py:1569
 msgid ""
 "Repository creation disabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1666
+#: kallithea/model/db.py:1571
 msgid "Only admins can fork repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1667
+#: kallithea/model/db.py:1572
 msgid "Non-admins can fork repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1669
+#: kallithea/model/db.py:1574
 msgid "Registration disabled"
 msgstr ""
 
-#: kallithea/model/db.py:1670
+#: kallithea/model/db.py:1575
 msgid "User registration with manual account activation"
 msgstr ""
 
-#: kallithea/model/db.py:1671
+#: kallithea/model/db.py:1576
 msgid "User registration with automatic account activation"
 msgstr ""
 
-#: kallithea/model/db.py:2206
+#: kallithea/model/db.py:1992
 msgid "Not reviewed"
 msgstr ""
 
-#: kallithea/model/db.py:2207
+#: kallithea/model/db.py:1993
 msgid "Under review"
 msgstr ""
 
-#: kallithea/model/db.py:2208
+#: kallithea/model/db.py:1994
 msgid "Not approved"
 msgstr ""
 
-#: kallithea/model/db.py:2209
+#: kallithea/model/db.py:1995
 msgid "Approved"
 msgstr ""
 
@@ -1485,313 +1483,313 @@
 msgid "Name must not contain only digits"
 msgstr ""
 
-#: kallithea/model/notification.py:164
+#: kallithea/model/notification.py:162
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s changeset %(short_id)s \"%(message_short)s\" on "
 "%(branch)s"
 msgstr ""
 
-#: kallithea/model/notification.py:167
+#: kallithea/model/notification.py:165
 #, python-format
 msgid "New user %(new_username)s registered"
 msgstr ""
 
-#: kallithea/model/notification.py:169
+#: kallithea/model/notification.py:167
 #, python-format
 msgid ""
 "[Review] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:170
+#: kallithea/model/notification.py:168
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:183
+#: kallithea/model/notification.py:188
 msgid "Closing"
 msgstr ""
 
-#: kallithea/model/pull_request.py:73
+#: kallithea/model/pull_request.py:72
 #, python-format
 msgid ""
 "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:209
+#: kallithea/model/pull_request.py:207
 msgid "Cannot create empty pull request"
 msgstr ""
 
-#: kallithea/model/pull_request.py:217
+#: kallithea/model/pull_request.py:215
 #, python-format
 msgid ""
 "Cannot create pull request - criss cross merge detected, please merge a "
 "later %s revision to %s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:245 kallithea/model/pull_request.py:380
+#: kallithea/model/pull_request.py:243 kallithea/model/pull_request.py:378
 msgid "You are not authorized to create the pull request"
 msgstr ""
 
-#: kallithea/model/pull_request.py:339
+#: kallithea/model/pull_request.py:337
 msgid "Missing changesets since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:346
+#: kallithea/model/pull_request.py:344
 #, python-format
 msgid "New changesets on %s %s since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:353
+#: kallithea/model/pull_request.py:351
 msgid "Ancestor didn't change - diff since previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:360
+#: kallithea/model/pull_request.py:358
 #, python-format
 msgid ""
 "This iteration is based on another %s revision and there is no simple "
 "diff."
 msgstr ""
 
-#: kallithea/model/pull_request.py:362
+#: kallithea/model/pull_request.py:360
 #, python-format
 msgid "No changes found on %s %s since previous iteration."
 msgstr ""
 
-#: kallithea/model/pull_request.py:388
+#: kallithea/model/pull_request.py:386
 #, python-format
 msgid "Closed, next iteration: %s ."
 msgstr ""
 
-#: kallithea/model/scm.py:668
+#: kallithea/model/scm.py:655
 msgid "latest tip"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:56
+#: kallithea/model/ssh_key.py:57
 #, python-format
 msgid "SSH key %r is invalid: %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:68
+#: kallithea/model/ssh_key.py:69
 #, python-format
 msgid "SSH key %s is already used by %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:89
-#, python-format
-msgid "SSH key %r not found"
-msgstr ""
-
-#: kallithea/model/user.py:186
+#: kallithea/model/ssh_key.py:88
+#, python-format
+msgid "SSH key with fingerprint %r found"
+msgstr ""
+
+#: kallithea/model/user.py:180
 msgid "New user registration"
 msgstr ""
 
-#: kallithea/model/user.py:250
+#: kallithea/model/user.py:244
 msgid ""
 "You can't remove this user since it is crucial for the entire application"
 msgstr ""
 
-#: kallithea/model/user.py:255
+#: kallithea/model/user.py:249
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repositories and cannot be removed. Switch "
 "owners or remove those repositories: %s"
 msgstr ""
 
-#: kallithea/model/user.py:260
+#: kallithea/model/user.py:254
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repository groups and cannot be removed. Switch "
 "owners or remove those repository groups: %s"
 msgstr ""
 
-#: kallithea/model/user.py:267
+#: kallithea/model/user.py:261
 #, python-format
 msgid ""
 "User \"%s\" still owns %s user groups and cannot be removed. Switch "
 "owners or remove those user groups: %s"
 msgstr ""
 
-#: kallithea/model/user.py:361
+#: kallithea/model/user.py:355
 msgid "Password reset link"
 msgstr ""
 
-#: kallithea/model/user.py:408
+#: kallithea/model/user.py:402
 msgid "Password reset notification"
 msgstr ""
 
-#: kallithea/model/user.py:409
+#: kallithea/model/user.py:403
 #, python-format
 msgid ""
 "The password to your account %s has been changed using password reset "
 "form."
 msgstr ""
 
-#: kallithea/model/validators.py:52 kallithea/model/validators.py:53
+#: kallithea/model/validators.py:53 kallithea/model/validators.py:54
 msgid "Value cannot be an empty list"
 msgstr ""
 
-#: kallithea/model/validators.py:72
+#: kallithea/model/validators.py:73
 #, python-format
 msgid "Username \"%(username)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:74
+#: kallithea/model/validators.py:75
 #, python-format
 msgid "Username \"%(username)s\" cannot be used"
 msgstr ""
 
-#: kallithea/model/validators.py:76
+#: kallithea/model/validators.py:77
 msgid ""
 "Username may only contain alphanumeric characters underscores, periods or "
 "dashes and must begin with an alphanumeric character or underscore"
 msgstr ""
 
-#: kallithea/model/validators.py:103
+#: kallithea/model/validators.py:104
 msgid "The input is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:110
+#: kallithea/model/validators.py:111
 #, python-format
 msgid "Username %(username)s is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:131
-msgid "Invalid user group name"
-msgstr ""
-
 #: kallithea/model/validators.py:132
+msgid "Invalid user group name"
+msgstr ""
+
+#: kallithea/model/validators.py:133
 #, python-format
 msgid "User group \"%(usergroup)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:134
+#: kallithea/model/validators.py:135
 msgid ""
 "user group name may only contain alphanumeric characters underscores, "
 "periods or dashes and must begin with alphanumeric character"
 msgstr ""
 
-#: kallithea/model/validators.py:174
-msgid "Cannot assign this group as parent"
-msgstr ""
-
 #: kallithea/model/validators.py:175
+msgid "Cannot assign this group as parent"
+msgstr ""
+
+#: kallithea/model/validators.py:176
 #, python-format
 msgid "Group \"%(group_name)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:177
+#: kallithea/model/validators.py:178
 #, python-format
 msgid "Repository with name \"%(group_name)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:233
+#: kallithea/model/validators.py:230
 msgid "Invalid characters (non-ascii) in password"
 msgstr ""
 
-#: kallithea/model/validators.py:248
+#: kallithea/model/validators.py:245
 msgid "Invalid old password"
 msgstr ""
 
-#: kallithea/model/validators.py:264
+#: kallithea/model/validators.py:261
 msgid "Passwords do not match"
 msgstr ""
 
-#: kallithea/model/validators.py:279
+#: kallithea/model/validators.py:276
 msgid "Invalid username or password"
 msgstr ""
 
+#: kallithea/model/validators.py:310
+#, python-format
+msgid "Repository name %(repo)s is not allowed"
+msgstr ""
+
+#: kallithea/model/validators.py:312
+#, python-format
+msgid "Repository named %(repo)s already exists"
+msgstr ""
+
 #: kallithea/model/validators.py:313
 #, python-format
-msgid "Repository name %(repo)s is not allowed"
+msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
 msgstr ""
 
 #: kallithea/model/validators.py:315
 #, python-format
-msgid "Repository named %(repo)s already exists"
-msgstr ""
-
-#: kallithea/model/validators.py:316
-#, python-format
-msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
-msgstr ""
-
-#: kallithea/model/validators.py:318
-#, python-format
 msgid "Repository group with name \"%(repo)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:404
+#: kallithea/model/validators.py:401
 msgid "Invalid repository URL"
 msgstr ""
 
-#: kallithea/model/validators.py:405
+#: kallithea/model/validators.py:402
 msgid ""
 "Invalid repository URL. It must be a valid http, https, ssh, svn+http or "
 "svn+https URL"
 msgstr ""
 
-#: kallithea/model/validators.py:430
+#: kallithea/model/validators.py:427
 msgid "Fork has to be the same type as parent"
 msgstr ""
 
-#: kallithea/model/validators.py:445
+#: kallithea/model/validators.py:442
 msgid "You don't have permissions to create repository in this group"
 msgstr ""
 
-#: kallithea/model/validators.py:447
+#: kallithea/model/validators.py:444
 msgid "no permission to create repository in root location"
 msgstr ""
 
-#: kallithea/model/validators.py:497
+#: kallithea/model/validators.py:494
 msgid "You don't have permissions to create a group in this location"
 msgstr ""
 
-#: kallithea/model/validators.py:537
+#: kallithea/model/validators.py:534
 msgid "This username or user group name is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:630
+#: kallithea/model/validators.py:627
 msgid "This is not a valid path"
 msgstr ""
 
-#: kallithea/model/validators.py:647
+#: kallithea/model/validators.py:644
 msgid "This email address is already in use"
 msgstr ""
 
-#: kallithea/model/validators.py:667
+#: kallithea/model/validators.py:664
 #, python-format
 msgid "Email address \"%(email)s\" not found"
 msgstr ""
 
-#: kallithea/model/validators.py:704
+#: kallithea/model/validators.py:701
 msgid ""
 "The LDAP Login attribute of the CN must be specified - this is the name "
 "of the attribute that is equivalent to \"username\""
 msgstr ""
 
-#: kallithea/model/validators.py:716
+#: kallithea/model/validators.py:713
 msgid "Please enter a valid IPv4 or IPv6 address"
 msgstr ""
 
-#: kallithea/model/validators.py:717
+#: kallithea/model/validators.py:714
 #, python-format
 msgid ""
 "The network size (bits) must be within the range of 0-32 (not %(bits)r)"
 msgstr ""
 
-#: kallithea/model/validators.py:750
+#: kallithea/model/validators.py:747
 msgid "Key name can only consist of letters, underscore, dash or numbers"
 msgstr ""
 
-#: kallithea/model/validators.py:764
+#: kallithea/model/validators.py:761
 msgid "Filename cannot be inside a directory"
 msgstr ""
 
-#: kallithea/model/validators.py:780
+#: kallithea/model/validators.py:777
 #, python-format
 msgid "Plugins %(loaded)s and %(next_to_load)s both export the same name"
 msgstr ""
@@ -1851,7 +1849,7 @@
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:60
 #: kallithea/templates/email_templates/pull_request.html:37
 #: kallithea/templates/forks/fork.html:34
-#: kallithea/templates/index_base.html:58
+#: kallithea/templates/index_base.html:59
 #: kallithea/templates/pullrequests/pullrequest.html:33
 #: kallithea/templates/pullrequests/pullrequest_show.html:38
 #: kallithea/templates/pullrequests/pullrequest_show.html:59
@@ -1859,14 +1857,14 @@
 msgid "Description"
 msgstr ""
 
-#: kallithea/templates/index_base.html:60
+#: kallithea/templates/index_base.html:61
 msgid "Last Change"
 msgstr ""
 
 #: kallithea/templates/admin/my_account/my_account_repos.html:15
 #: kallithea/templates/admin/my_account/my_account_watched.html:15
 #: kallithea/templates/admin/repos/repos.html:41
-#: kallithea/templates/index_base.html:62
+#: kallithea/templates/index_base.html:63
 msgid "Tip"
 msgstr ""
 
@@ -1876,7 +1874,7 @@
 #: kallithea/templates/admin/repos/repos.html:42
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:8
 #: kallithea/templates/admin/user_groups/user_groups.html:42
-#: kallithea/templates/index_base.html:63
+#: kallithea/templates/index_base.html:64
 #: kallithea/templates/pullrequests/pullrequest_data.html:16
 #: kallithea/templates/pullrequests/pullrequest_show.html:124
 #: kallithea/templates/pullrequests/pullrequest_show.html:219
@@ -1900,7 +1898,7 @@
 #: kallithea/templates/admin/users/user_edit_profile.html:18
 #: kallithea/templates/admin/users/users.html:37
 #: kallithea/templates/base/base.html:364
-#: kallithea/templates/email_templates/registration.html:11
+#: kallithea/templates/email_templates/registration.html:12
 #: kallithea/templates/login.html:28 kallithea/templates/register.html:31
 msgid "Username"
 msgstr ""
@@ -2024,7 +2022,7 @@
 #: kallithea/templates/admin/settings/settings.html:31
 #: kallithea/templates/admin/users/user_add.html:62
 #: kallithea/templates/admin/users/user_edit_profile.html:25
-#: kallithea/templates/email_templates/registration.html:33
+#: kallithea/templates/email_templates/registration.html:34
 #: kallithea/templates/register.html:66
 msgid "Email"
 msgstr ""
@@ -2271,7 +2269,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/gists/index.html:51
-#: kallithea/templates/data_table/_dt_elements.html:78
+#: kallithea/templates/data_table/_dt_elements.html:84
 msgid "Created"
 msgstr ""
 
@@ -2355,13 +2353,13 @@
 #: kallithea/templates/admin/users/user_edit_ips.html:21
 #: kallithea/templates/changeset/changeset_file_comment.html:30
 #: kallithea/templates/changeset/changeset_file_comment.html:121
-#: kallithea/templates/data_table/_dt_elements.html:69
-#: kallithea/templates/data_table/_dt_elements.html:89
-#: kallithea/templates/data_table/_dt_elements.html:91
-#: kallithea/templates/data_table/_dt_elements.html:101
-#: kallithea/templates/data_table/_dt_elements.html:103
-#: kallithea/templates/data_table/_dt_elements.html:120
-#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:75
+#: kallithea/templates/data_table/_dt_elements.html:95
+#: kallithea/templates/data_table/_dt_elements.html:97
+#: kallithea/templates/data_table/_dt_elements.html:107
+#: kallithea/templates/data_table/_dt_elements.html:109
+#: kallithea/templates/data_table/_dt_elements.html:126
+#: kallithea/templates/data_table/_dt_elements.html:128
 #: kallithea/templates/files/files_source.html:35
 #: kallithea/templates/files/files_source.html:38
 #: kallithea/templates/files/files_source.html:41
@@ -2377,14 +2375,14 @@
 #: kallithea/templates/base/perms_summary.html:44
 #: kallithea/templates/base/perms_summary.html:81
 #: kallithea/templates/base/perms_summary.html:83
-#: kallithea/templates/data_table/_dt_elements.html:63
-#: kallithea/templates/data_table/_dt_elements.html:64
-#: kallithea/templates/data_table/_dt_elements.html:85
-#: kallithea/templates/data_table/_dt_elements.html:86
-#: kallithea/templates/data_table/_dt_elements.html:97
-#: kallithea/templates/data_table/_dt_elements.html:98
-#: kallithea/templates/data_table/_dt_elements.html:116
-#: kallithea/templates/data_table/_dt_elements.html:117
+#: kallithea/templates/data_table/_dt_elements.html:69
+#: kallithea/templates/data_table/_dt_elements.html:70
+#: kallithea/templates/data_table/_dt_elements.html:91
+#: kallithea/templates/data_table/_dt_elements.html:92
+#: kallithea/templates/data_table/_dt_elements.html:103
+#: kallithea/templates/data_table/_dt_elements.html:104
+#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:123
 #: kallithea/templates/files/diff_2way.html:56
 #: kallithea/templates/files/files_source.html:37
 #: kallithea/templates/files/files_source.html:40
@@ -2685,7 +2683,7 @@
 #: kallithea/templates/admin/permissions/permissions_globals.html:27
 #: kallithea/templates/admin/repos/repo_add_base.html:28
 #: kallithea/templates/admin/repos/repo_edit_settings.html:33
-#: kallithea/templates/data_table/_dt_elements.html:134
+#: kallithea/templates/data_table/_dt_elements.html:140
 #: kallithea/templates/forks/fork.html:42
 msgid "Repository group"
 msgstr ""
@@ -2706,7 +2704,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:40
-#: kallithea/templates/data_table/_dt_elements.html:141
+#: kallithea/templates/data_table/_dt_elements.html:147
 msgid "User group"
 msgstr ""
 
@@ -2879,7 +2877,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:121
+#: kallithea/templates/data_table/_dt_elements.html:127
 #, python-format
 msgid "Confirm to delete this group: %s with %s repository"
 msgid_plural "Confirm to delete this group: %s with %s repositories"
@@ -3045,14 +3043,10 @@
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit.html:37
-msgid "Caches"
+msgid "Remote"
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit.html:40
-msgid "Remote"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit.html:43
 #: kallithea/templates/summary/statistics.html:8
 #: kallithea/templates/summary/summary.html:169
 #: kallithea/templates/summary/summary.html:170
@@ -3090,7 +3084,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:46
-#: kallithea/templates/data_table/_dt_elements.html:68
+#: kallithea/templates/data_table/_dt_elements.html:74
 #, python-format
 msgid "Confirm to delete this repository: %s"
 msgstr ""
@@ -3121,43 +3115,14 @@
 "it or restore it."
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:4
-msgid "Invalidate Repository Cache"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:6
-msgid ""
-"Manually invalidate cache for this repository. On first access, the "
-"repository will be cached again."
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:9
-msgid "List of Cached Values"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:12
-msgid "Prefix"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:13
+#: kallithea/templates/admin/repos/repo_edit_fields.html:6
+msgid "Label"
+msgstr ""
+
 #: kallithea/templates/admin/repos/repo_edit_fields.html:7
 msgid "Key"
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:14
-#: kallithea/templates/admin/user_groups/user_group_add.html:40
-#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
-#: kallithea/templates/admin/user_groups/user_groups.html:41
-#: kallithea/templates/admin/users/user_add.html:69
-#: kallithea/templates/admin/users/user_edit_profile.html:74
-#: kallithea/templates/admin/users/users.html:42
-msgid "Active"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_fields.html:6
-msgid "Label"
-msgstr ""
-
 #: kallithea/templates/admin/repos/repo_edit_fields.html:20
 #, python-format
 msgid "Confirm to delete this field: %s"
@@ -3675,6 +3640,15 @@
 msgid "Short, optional description for this user group."
 msgstr ""
 
+#: kallithea/templates/admin/user_groups/user_group_add.html:40
+#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
+#: kallithea/templates/admin/user_groups/user_groups.html:41
+#: kallithea/templates/admin/users/user_add.html:69
+#: kallithea/templates/admin/users/user_edit_profile.html:74
+#: kallithea/templates/admin/users/users.html:42
+msgid "Active"
+msgstr ""
+
 #: kallithea/templates/admin/user_groups/user_group_edit.html:5
 #, python-format
 msgid "%s user group settings"
@@ -3696,7 +3670,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:19
-#: kallithea/templates/data_table/_dt_elements.html:102
+#: kallithea/templates/data_table/_dt_elements.html:108
 #, python-format
 msgid "Confirm to delete this user group: %s"
 msgstr ""
@@ -3770,7 +3744,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:90
+#: kallithea/templates/data_table/_dt_elements.html:96
 #, python-format
 msgid "Confirm to delete this user: %s"
 msgstr ""
@@ -3870,10 +3844,12 @@
 msgstr ""
 
 #: kallithea/templates/base/base.html:167
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Follow"
 msgstr ""
 
 #: kallithea/templates/base/base.html:168
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Unfollow"
 msgstr ""
 
@@ -4278,23 +4254,23 @@
 msgid "Merge"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:96
+#: kallithea/templates/changeset/changeset.html:95
 msgid "Grafted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:102
+#: kallithea/templates/changeset/changeset.html:100
 msgid "Transplanted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:108
+#: kallithea/templates/changeset/changeset.html:106
 msgid "Replaced by:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:122
+#: kallithea/templates/changeset/changeset.html:120
 msgid "Preceded by:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:139
+#: kallithea/templates/changeset/changeset.html:137
 #: kallithea/templates/compare/compare_diff.html:59
 #: kallithea/templates/pullrequests/pullrequest_show.html:290
 #, python-format
@@ -4303,7 +4279,7 @@
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/templates/changeset/changeset.html:141
+#: kallithea/templates/changeset/changeset.html:139
 #: kallithea/templates/compare/compare_diff.html:61
 #: kallithea/templates/pullrequests/pullrequest_show.html:292
 #, python-format
@@ -4312,8 +4288,8 @@
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
 #: kallithea/templates/pullrequests/pullrequest_show.html:333
@@ -4546,23 +4522,23 @@
 msgid "Repository creation in progress..."
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:42
+#: kallithea/templates/data_table/_dt_elements.html:48
 msgid "No changesets yet"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:48
-#: kallithea/templates/data_table/_dt_elements.html:50
+#: kallithea/templates/data_table/_dt_elements.html:54
+#: kallithea/templates/data_table/_dt_elements.html:56
 #, python-format
 msgid "Subscribe to %s rss feed"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:56
-#: kallithea/templates/data_table/_dt_elements.html:58
+#: kallithea/templates/data_table/_dt_elements.html:62
+#: kallithea/templates/data_table/_dt_elements.html:64
 #, python-format
 msgid "Subscribe to %s atom feed"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:76
+#: kallithea/templates/data_table/_dt_elements.html:82
 msgid "Creating"
 msgstr ""
 
@@ -4596,6 +4572,13 @@
 msgid "by"
 msgstr ""
 
+#: kallithea/templates/email_templates/changeset_comment.html:36
+#: kallithea/templates/email_templates/pull_request_comment.html:43
+#, fuzzy
+#| msgid "No comments."
+msgid "View Comment"
+msgstr "Nincsenek hozzászólások."
+
 #: kallithea/templates/email_templates/comment.html:27
 #, fuzzy
 #| msgid "Status change"
@@ -4608,32 +4591,40 @@
 msgid "The pull request has been closed."
 msgstr "Ennek a tárolónak %s elágazása van"
 
-#: kallithea/templates/email_templates/password_reset.html:9
+#: kallithea/templates/email_templates/default.html:4
+msgid "Message"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:4
+msgid "Password Reset Request"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:10
 #, python-format
 msgid "Hello %s"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:16
+#: kallithea/templates/email_templates/password_reset.html:17
 msgid "We have received a request to reset the password for your account."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:25
+#: kallithea/templates/email_templates/password_reset.html:26
 msgid ""
 "This account is however managed outside this system and the password "
 "cannot be changed here."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:28
+#: kallithea/templates/email_templates/password_reset.html:29
 msgid "To set a new password, click the following link"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:33
+#: kallithea/templates/email_templates/password_reset.html:34
 msgid ""
 "Should you not be able to use the link above, please type the following "
 "code into the password reset form"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:44
+#: kallithea/templates/email_templates/password_reset.html:45
 msgid ""
 "If it weren't you who requested the password reset, just disregard this "
 "message."
@@ -4664,6 +4655,10 @@
 msgid "to"
 msgstr ""
 
+#: kallithea/templates/email_templates/pull_request.html:85
+msgid "View Pull Request"
+msgstr ""
+
 #: kallithea/templates/email_templates/pull_request_comment.html:4
 #, python-format
 msgid "Mention in Comment on Pull Request %s \"%s\""
@@ -4679,10 +4674,18 @@
 msgid "Comment on Pull Request %s \"%s\""
 msgstr ""
 
-#: kallithea/templates/email_templates/registration.html:22
+#: kallithea/templates/email_templates/registration.html:5
+msgid "New User Registration"
+msgstr ""
+
+#: kallithea/templates/email_templates/registration.html:23
 msgid "Full Name"
 msgstr ""
 
+#: kallithea/templates/email_templates/registration.html:42
+msgid "View User Profile"
+msgstr ""
+
 #: kallithea/templates/files/diff_2way.html:15
 #, python-format
 msgid "%s File side-by-side diff"
@@ -5285,45 +5288,45 @@
 msgid "Stats gathered: "
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:87
-#: kallithea/templates/summary/summary.html:371
+#: kallithea/templates/summary/statistics.html:85
+#: kallithea/templates/summary/summary.html:369
 msgid "files"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:111
-#: kallithea/templates/summary/summary.html:401
+#: kallithea/templates/summary/statistics.html:109
+#: kallithea/templates/summary/summary.html:399
 msgid "Show more"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:405
+#: kallithea/templates/summary/statistics.html:395
 msgid "commits"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:406
+#: kallithea/templates/summary/statistics.html:396
 msgid "files added"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:407
+#: kallithea/templates/summary/statistics.html:397
 msgid "files changed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:408
+#: kallithea/templates/summary/statistics.html:398
 msgid "files removed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:410
+#: kallithea/templates/summary/statistics.html:400
 msgid "commit"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:411
+#: kallithea/templates/summary/statistics.html:401
 msgid "file added"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:412
+#: kallithea/templates/summary/statistics.html:402
 msgid "file changed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:413
+#: kallithea/templates/summary/statistics.html:403
 msgid "file removed"
 msgstr ""
 
--- a/kallithea/i18n/ja/LC_MESSAGES/kallithea.po	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/i18n/ja/LC_MESSAGES/kallithea.po	Mon May 04 19:24:04 2020 +0200
@@ -4,7 +4,7 @@
 msgstr ""
 "Project-Id-Version: Kallithea 0.3\n"
 "Report-Msgid-Bugs-To: translations@kallithea-scm.org\n"
-"POT-Creation-Date: 2020-01-05 04:10+0100\n"
+"POT-Creation-Date: 2020-05-04 19:12+0200\n"
 "PO-Revision-Date: 2019-08-27 07:23+0000\n"
 "Last-Translator: leela <53352@protonmail.com>\n"
 "Language-Team: Japanese <https://hosted.weblate.org/projects/kallithea/"
@@ -17,14 +17,14 @@
 "X-Generator: Weblate 3.9-dev\n"
 
 #: kallithea/controllers/changelog.py:67
-#: kallithea/controllers/pullrequests.py:250 kallithea/lib/base.py:602
+#: kallithea/controllers/pullrequests.py:247 kallithea/lib/base.py:602
 msgid "There are no changesets yet"
 msgstr "まだチェンジセットがありません"
 
 #: kallithea/controllers/admin/permissions.py:64
 #: kallithea/controllers/admin/permissions.py:68
 #: kallithea/controllers/admin/permissions.py:72
-#: kallithea/controllers/changelog.py:137
+#: kallithea/controllers/changelog.py:136
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:7
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:88
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:7
@@ -33,38 +33,38 @@
 msgid "None"
 msgstr "なし"
 
-#: kallithea/controllers/changelog.py:140 kallithea/controllers/files.py:189
+#: kallithea/controllers/changelog.py:139 kallithea/controllers/files.py:189
 msgid "(closed)"
 msgstr "(閉鎖済み)"
 
-#: kallithea/controllers/changeset.py:81
+#: kallithea/controllers/changeset.py:82
 msgid "Show whitespace"
 msgstr "空白を表示"
 
-#: kallithea/controllers/changeset.py:88
-#: kallithea/controllers/changeset.py:95
+#: kallithea/controllers/changeset.py:89
+#: kallithea/controllers/changeset.py:96
 #: kallithea/templates/files/diff_2way.html:55
 msgid "Ignore whitespace"
 msgstr "空白を無視"
 
-#: kallithea/controllers/changeset.py:161
+#: kallithea/controllers/changeset.py:162
 #, python-format
 msgid "Increase diff context to %(num)s lines"
 msgstr "diff コンテキストを %(num)s 行増やす"
 
-#: kallithea/controllers/changeset.py:201
+#: kallithea/controllers/changeset.py:202
 #, fuzzy
 #| msgid "No permission to change pull request status"
 msgid "No permission to change status"
 msgstr "プルリクエストステータスを変更する権限がありません"
 
-#: kallithea/controllers/changeset.py:212
+#: kallithea/controllers/changeset.py:213
 #, fuzzy, python-format
 msgid "Successfully deleted pull request %s"
 msgstr "プルリクエストの削除に成功しました"
 
 #: kallithea/controllers/changeset.py:319 kallithea/controllers/files.py:89
-#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:700
+#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:697
 msgid "Such revision does not exist for this repository"
 msgstr "お探しのリビジョンはこのリポジトリにはありません"
 
@@ -80,49 +80,49 @@
 msgid "Cannot compare repositories of different types"
 msgstr "共通の祖先を持たないのでリポジトリを比較できません"
 
-#: kallithea/controllers/compare.py:246
+#: kallithea/controllers/compare.py:247
 msgid "Cannot show empty diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:248
+#: kallithea/controllers/compare.py:249
 msgid "No ancestor found for merge diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:252
+#: kallithea/controllers/compare.py:253
 msgid "Multiple merge ancestors found for merge compare"
 msgstr ""
 
-#: kallithea/controllers/compare.py:268
+#: kallithea/controllers/compare.py:269
 msgid "Cannot compare repositories without using common ancestor"
 msgstr "共通の祖先を持たないのでリポジトリを比較できません"
 
-#: kallithea/controllers/error.py:71
+#: kallithea/controllers/error.py:67
 msgid "No response"
 msgstr "応答がありません"
 
-#: kallithea/controllers/error.py:72
+#: kallithea/controllers/error.py:68
 msgid "Unknown error"
 msgstr "不明なエラー"
 
-#: kallithea/controllers/error.py:85
+#: kallithea/controllers/error.py:81
 msgid ""
 "The request could not be understood by the server due to malformed syntax."
 msgstr ""
 "形式が間違っているため、サーバーはリクエストを処理できませんでした。"
 
-#: kallithea/controllers/error.py:88
+#: kallithea/controllers/error.py:84
 msgid "Unauthorized access to resource"
 msgstr "リソースにアクセスする権限がありません"
 
-#: kallithea/controllers/error.py:90
+#: kallithea/controllers/error.py:86
 msgid "You don't have permission to view this page"
 msgstr "このページを閲覧する権限がありません"
 
-#: kallithea/controllers/error.py:92
+#: kallithea/controllers/error.py:88
 msgid "The resource could not be found"
 msgstr "リソースが見つかりません"
 
-#: kallithea/controllers/error.py:94
+#: kallithea/controllers/error.py:90
 msgid ""
 "The server encountered an unexpected condition which prevented it from "
 "fulfilling the request."
@@ -130,14 +130,14 @@
 "サーバーが不正な状態になったため、リクエストに答えることができませんでし"
 "た。"
 
-#: kallithea/controllers/feed.py:63
+#: kallithea/controllers/feed.py:59
 #, python-format
 msgid "%s committed on %s"
 msgstr "%s が %s にコミット"
 
-#: kallithea/controllers/feed.py:88
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/controllers/feed.py:84
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/compare/compare_diff.html:95
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
@@ -145,12 +145,12 @@
 msgid "Changeset was too big and was cut off..."
 msgstr "チェンジセットが大きすぎるため、省略しました..."
 
-#: kallithea/controllers/feed.py:111 kallithea/controllers/feed.py:140
+#: kallithea/controllers/feed.py:107
 #, python-format
 msgid "%s %s feed"
 msgstr "%s %s フィード"
 
-#: kallithea/controllers/feed.py:113 kallithea/controllers/feed.py:142
+#: kallithea/controllers/feed.py:109
 #, python-format
 msgid "Changes on %s repository"
 msgstr "%s リポジトリでの変更"
@@ -170,105 +170,105 @@
 msgid "%s at %s"
 msgstr "%s と %s の間"
 
-#: kallithea/controllers/files.py:296
+#: kallithea/controllers/files.py:295
 #, fuzzy
 msgid "You can only delete files with revision being a valid branch"
 msgstr "有効なブランチ上のリビジョンからしかファイルを削除できません"
 
-#: kallithea/controllers/files.py:307
+#: kallithea/controllers/files.py:306
 #, python-format
 msgid "Deleted file %s via Kallithea"
 msgstr "Kallithea経由で %s を削除"
 
-#: kallithea/controllers/files.py:331
+#: kallithea/controllers/files.py:330
 #, python-format
 msgid "Successfully deleted file %s"
 msgstr "%s ファイルの削除に成功しました"
 
-#: kallithea/controllers/files.py:335 kallithea/controllers/files.py:394
-#: kallithea/controllers/files.py:469
+#: kallithea/controllers/files.py:334 kallithea/controllers/files.py:392
+#: kallithea/controllers/files.py:467
 msgid "Error occurred during commit"
 msgstr "コミット中にエラーが発生しました"
 
-#: kallithea/controllers/files.py:350
+#: kallithea/controllers/files.py:349
 #, fuzzy
 msgid "You can only edit files with revision being a valid branch"
 msgstr "有効なブランチを示すリビジョンでのみファイルを編集できます "
 
-#: kallithea/controllers/files.py:364
+#: kallithea/controllers/files.py:363
 #, python-format
 msgid "Edited file %s via Kallithea"
 msgstr "Kallithea経由で %s を変更"
 
-#: kallithea/controllers/files.py:380
+#: kallithea/controllers/files.py:378
 msgid "No changes"
 msgstr "変更点なし"
 
-#: kallithea/controllers/files.py:390 kallithea/controllers/files.py:458
+#: kallithea/controllers/files.py:388 kallithea/controllers/files.py:456
 #, python-format
 msgid "Successfully committed to %s"
 msgstr "%s へのコミットが成功しました"
 
-#: kallithea/controllers/files.py:409
+#: kallithea/controllers/files.py:407
 msgid "Added file via Kallithea"
 msgstr "Kallithea経由でファイルを追加"
 
-#: kallithea/controllers/files.py:430
+#: kallithea/controllers/files.py:428
 msgid "No content"
 msgstr "内容がありません"
 
-#: kallithea/controllers/files.py:434
+#: kallithea/controllers/files.py:432
 msgid "No filename"
 msgstr "ファイル名がありません"
 
-#: kallithea/controllers/files.py:461
+#: kallithea/controllers/files.py:459
 msgid "Location must be relative path and must not contain .. in path"
 msgstr "場所には相対パスかつ .. を含まないパスを入力してください"
 
-#: kallithea/controllers/files.py:493
+#: kallithea/controllers/files.py:491
 msgid "Downloads disabled"
 msgstr "ダウンロードは無効化されています"
 
-#: kallithea/controllers/files.py:504
+#: kallithea/controllers/files.py:502
 #, python-format
 msgid "Unknown revision %s"
 msgstr "%s は未知のリビジョンです"
 
-#: kallithea/controllers/files.py:506
+#: kallithea/controllers/files.py:504
 msgid "Empty repository"
 msgstr "空のリポジトリ"
 
-#: kallithea/controllers/files.py:508
+#: kallithea/controllers/files.py:506
 msgid "Unknown archive type"
 msgstr "未知のアーカイブ種別です"
 
-#: kallithea/controllers/files.py:729
+#: kallithea/controllers/files.py:726
 #: kallithea/templates/changeset/changeset_range.html:9
 #: kallithea/templates/email_templates/pull_request.html:64
 #: kallithea/templates/pullrequests/pullrequest.html:84
 msgid "Changesets"
 msgstr "チェンジセット"
 
-#: kallithea/controllers/files.py:730
-#: kallithea/controllers/pullrequests.py:182 kallithea/model/scm.py:676
+#: kallithea/controllers/files.py:727
+#: kallithea/controllers/pullrequests.py:174 kallithea/model/scm.py:663
 msgid "Branches"
 msgstr "ブランチ"
 
-#: kallithea/controllers/files.py:731
-#: kallithea/controllers/pullrequests.py:183 kallithea/model/scm.py:687
+#: kallithea/controllers/files.py:728
+#: kallithea/controllers/pullrequests.py:175 kallithea/model/scm.py:674
 msgid "Tags"
 msgstr "タグ"
 
-#: kallithea/controllers/forks.py:174
+#: kallithea/controllers/forks.py:175
 #, python-format
 msgid "An error occurred during repository forking %s"
 msgstr "リポジトリ %s のフォーク中にエラーが発生しました"
 
-#: kallithea/controllers/home.py:79
+#: kallithea/controllers/home.py:77
 msgid "Groups"
 msgstr "グループ"
 
-#: kallithea/controllers/home.py:89
+#: kallithea/controllers/home.py:87
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:90
 #: kallithea/templates/admin/repos/repo_add.html:12
 #: kallithea/templates/admin/repos/repo_add.html:16
@@ -280,48 +280,54 @@
 msgid "Repositories"
 msgstr "リポジトリ"
 
-#: kallithea/controllers/home.py:122
+#: kallithea/controllers/home.py:119
 #: kallithea/templates/files/files_add.html:32
 #: kallithea/templates/files/files_delete.html:23
 #: kallithea/templates/files/files_edit.html:32
 msgid "Branch"
 msgstr "ブランチ"
 
-#: kallithea/controllers/home.py:128
+#: kallithea/controllers/home.py:125
 msgid "Closed Branches"
 msgstr "閉鎖済みブランチ"
 
-#: kallithea/controllers/home.py:134
+#: kallithea/controllers/home.py:131
 msgid "Tag"
 msgstr "タグ"
 
-#: kallithea/controllers/home.py:140
+#: kallithea/controllers/home.py:137
 msgid "Bookmark"
 msgstr "ブックマーク"
 
-#: kallithea/controllers/journal.py:112 kallithea/controllers/journal.py:154
+#: kallithea/controllers/journal.py:146 kallithea/controllers/journal.py:157
 #: kallithea/templates/journal/public_journal.html:4
 #: kallithea/templates/journal/public_journal.html:18
 msgid "Public Journal"
 msgstr "公開ジャーナル"
 
-#: kallithea/controllers/journal.py:116 kallithea/controllers/journal.py:158
+#: kallithea/controllers/journal.py:150 kallithea/controllers/journal.py:161
 #: kallithea/templates/base/base.html:290
 #: kallithea/templates/journal/journal.html:5
 #: kallithea/templates/journal/journal.html:13
 msgid "Journal"
 msgstr "ジャーナル"
 
-#: kallithea/controllers/login.py:140 kallithea/controllers/login.py:185
+#: kallithea/controllers/login.py:109
+#, fuzzy
+#| msgid "Authentication"
+msgid "Authentication failed."
+msgstr "認証"
+
+#: kallithea/controllers/login.py:142 kallithea/controllers/login.py:187
 msgid "Bad captcha"
 msgstr "キャプチャが一致しません"
 
-#: kallithea/controllers/login.py:146
+#: kallithea/controllers/login.py:148
 #, python-format
 msgid "You have successfully registered with %s"
 msgstr "%sへの登録を受け付けました"
 
-#: kallithea/controllers/login.py:190
+#: kallithea/controllers/login.py:192
 msgid "A password reset confirmation code has been sent"
 msgstr "パスワードリセットの確認コードが送信されました"
 
@@ -334,235 +340,235 @@
 msgid "Successfully updated password"
 msgstr "パスワードを更新しました"
 
-#: kallithea/controllers/pullrequests.py:68
+#: kallithea/controllers/pullrequests.py:67
 #, python-format
 msgid "Invalid reviewer \"%s\" specified"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:131
+#: kallithea/controllers/pullrequests.py:123
 #, python-format
 msgid "%s (closed)"
 msgstr "%s (閉鎖済み)"
 
-#: kallithea/controllers/pullrequests.py:158
+#: kallithea/controllers/pullrequests.py:150
 #: kallithea/templates/changeset/changeset.html:12
 msgid "Changeset"
 msgstr "チェンジセット"
 
-#: kallithea/controllers/pullrequests.py:179
+#: kallithea/controllers/pullrequests.py:171
 msgid "Special"
 msgstr "スペシャル"
 
-#: kallithea/controllers/pullrequests.py:180
+#: kallithea/controllers/pullrequests.py:172
 msgid "Peer branches"
 msgstr "相手のブランチ"
 
-#: kallithea/controllers/pullrequests.py:181 kallithea/model/scm.py:682
+#: kallithea/controllers/pullrequests.py:173 kallithea/model/scm.py:669
 msgid "Bookmarks"
 msgstr "ブックマーク"
 
-#: kallithea/controllers/pullrequests.py:318
+#: kallithea/controllers/pullrequests.py:315
 #, python-format
 msgid "Error creating pull request: %s"
 msgstr "プルリクエスト作成中にエラーが発生しました: %s"
 
-#: kallithea/controllers/pullrequests.py:345
-#: kallithea/controllers/pullrequests.py:368
+#: kallithea/controllers/pullrequests.py:342
+#: kallithea/controllers/pullrequests.py:365
 msgid "Error occurred while creating pull request"
 msgstr "プルリクエストの作成中にエラーが発生しました"
 
-#: kallithea/controllers/pullrequests.py:350
+#: kallithea/controllers/pullrequests.py:347
 msgid "Successfully opened new pull request"
 msgstr "新しいプルリクエストの作成に成功しました"
 
-#: kallithea/controllers/pullrequests.py:373
+#: kallithea/controllers/pullrequests.py:370
 #, fuzzy
 #| msgid "Pull request update created"
 msgid "New pull request iteration created"
 msgstr "プルリクエストレビュアー"
 
-#: kallithea/controllers/pullrequests.py:401
+#: kallithea/controllers/pullrequests.py:398
 #, python-format
 msgid "Meanwhile, the following reviewers have been added: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:405
+#: kallithea/controllers/pullrequests.py:402
 #, python-format
 msgid "Meanwhile, the following reviewers have been removed: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:421
-#: kallithea/model/pull_request.py:232
+#: kallithea/controllers/pullrequests.py:418
+#: kallithea/model/pull_request.py:230
 msgid "No description"
 msgstr "説明がありません"
 
-#: kallithea/controllers/pullrequests.py:430
+#: kallithea/controllers/pullrequests.py:427
 msgid "Pull request updated"
 msgstr "プルリクエストを更新しました"
 
-#: kallithea/controllers/pullrequests.py:443
+#: kallithea/controllers/pullrequests.py:440
 msgid "Successfully deleted pull request"
 msgstr "プルリクエストの削除に成功しました"
 
-#: kallithea/controllers/pullrequests.py:479
+#: kallithea/controllers/pullrequests.py:476
 #, python-format
 msgid "Revision %s not found in %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:506
+#: kallithea/controllers/pullrequests.py:504
 #, fuzzy, python-format
 #| msgid "No changesets found for updating this pull request."
 msgid "Error: changesets not found when displaying pull request from %s."
 msgstr "プルリクエストを更新するためのチェンジセットが見つかりません。"
 
-#: kallithea/controllers/pullrequests.py:520
+#: kallithea/controllers/pullrequests.py:518
 #, python-format
 msgid "This pull request has already been merged to %s."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:522
+#: kallithea/controllers/pullrequests.py:520
 msgid "This pull request has been closed and can not be updated."
 msgstr ""
 "このプルリクエストはすでにクローズされていて、更新することはできません。"
 
-#: kallithea/controllers/pullrequests.py:546
+#: kallithea/controllers/pullrequests.py:539
 #, python-format
 msgid "The following additional changes are available on %s:"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:548
-#: kallithea/controllers/pullrequests.py:552
+#: kallithea/controllers/pullrequests.py:541
+#: kallithea/controllers/pullrequests.py:545
 #, fuzzy
 #| msgid "No changesets found for updating this pull request."
 msgid "No additional changesets found for iterating on this pull request."
 msgstr "プルリクエストを更新するためのチェンジセットが見つかりません。"
 
-#: kallithea/controllers/pullrequests.py:560
+#: kallithea/controllers/pullrequests.py:553
 #, python-format
 msgid "Note: Branch %s has another head: %s."
 msgstr "ノート: ブランチ%sには別のヘッド%sがあります。"
 
-#: kallithea/controllers/pullrequests.py:567
+#: kallithea/controllers/pullrequests.py:560
 #, fuzzy
 #| msgid "Git pull requests don't support updates yet."
 msgid "Git pull requests don't support iterating yet."
 msgstr "Gitのプルリクエストはまだ更新をサポートしていません。"
 
-#: kallithea/controllers/pullrequests.py:569
+#: kallithea/controllers/pullrequests.py:562
 #, fuzzy, python-format
 #| msgid "No changesets found for updating this pull request."
 msgid ""
 "Error: some changesets not found when displaying pull request from %s."
 msgstr "プルリクエストを更新するためのチェンジセットが見つかりません。"
 
-#: kallithea/controllers/pullrequests.py:593
+#: kallithea/controllers/pullrequests.py:586
 msgid "The diff can't be shown - the PR revisions could not be found."
 msgstr ""
 
-#: kallithea/controllers/search.py:136
+#: kallithea/controllers/search.py:132
 msgid "Invalid search query. Try quoting it."
 msgstr "無効な検索クエリーです。\\\"で囲んで下さい。"
 
-#: kallithea/controllers/search.py:140
+#: kallithea/controllers/search.py:136
 msgid "The server has no search index."
 msgstr ""
 
-#: kallithea/controllers/search.py:143
+#: kallithea/controllers/search.py:139
 msgid "An error occurred during search operation."
 msgstr "検索を実行する際にエラーが発生しました。"
 
-#: kallithea/controllers/summary.py:168
-#: kallithea/templates/summary/summary.html:412
+#: kallithea/controllers/summary.py:169
+#: kallithea/templates/summary/summary.html:410
 msgid "No data ready yet"
 msgstr "まだデータの準備ができていません"
 
-#: kallithea/controllers/summary.py:171
+#: kallithea/controllers/summary.py:172
 #: kallithea/templates/summary/summary.html:97
 msgid "Statistics are disabled for this repository"
 msgstr "このリポジトリの統計は無効化されています"
 
-#: kallithea/controllers/admin/auth_settings.py:137
+#: kallithea/controllers/admin/auth_settings.py:136
 msgid "Auth settings updated successfully"
 msgstr "認証設定の更新に成功しました"
 
-#: kallithea/controllers/admin/auth_settings.py:148
+#: kallithea/controllers/admin/auth_settings.py:147
 msgid "error occurred during update of auth settings"
 msgstr "認証設定の更新中にエラーが発生しました"
 
-#: kallithea/controllers/admin/defaults.py:75
+#: kallithea/controllers/admin/defaults.py:74
 msgid "Default settings updated successfully"
 msgstr "デフォルト設定の更新に成功しました"
 
-#: kallithea/controllers/admin/defaults.py:90
+#: kallithea/controllers/admin/defaults.py:89
 msgid "Error occurred during update of defaults"
 msgstr "デフォルト設定の更新中にエラーが発生しました"
 
 #: kallithea/controllers/admin/gists.py:59
 #: kallithea/controllers/admin/my_account.py:232
-#: kallithea/controllers/admin/users.py:248
+#: kallithea/controllers/admin/users.py:246
 msgid "Forever"
 msgstr "永久"
 
 #: kallithea/controllers/admin/gists.py:60
 #: kallithea/controllers/admin/my_account.py:233
-#: kallithea/controllers/admin/users.py:249
+#: kallithea/controllers/admin/users.py:247
 msgid "5 minutes"
 msgstr "5 分"
 
 #: kallithea/controllers/admin/gists.py:61
 #: kallithea/controllers/admin/my_account.py:234
-#: kallithea/controllers/admin/users.py:250
+#: kallithea/controllers/admin/users.py:248
 msgid "1 hour"
 msgstr "1 時間"
 
 #: kallithea/controllers/admin/gists.py:62
 #: kallithea/controllers/admin/my_account.py:235
-#: kallithea/controllers/admin/users.py:251
+#: kallithea/controllers/admin/users.py:249
 msgid "1 day"
 msgstr "1 日"
 
 #: kallithea/controllers/admin/gists.py:63
 #: kallithea/controllers/admin/my_account.py:236
-#: kallithea/controllers/admin/users.py:252
+#: kallithea/controllers/admin/users.py:250
 msgid "1 month"
 msgstr "1 ヶ月"
 
 #: kallithea/controllers/admin/gists.py:67
 #: kallithea/controllers/admin/my_account.py:238
-#: kallithea/controllers/admin/users.py:254
+#: kallithea/controllers/admin/users.py:252
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:65
 #: kallithea/templates/admin/users/user_edit_api_keys.html:65
 msgid "Lifetime"
 msgstr "有効期間"
 
-#: kallithea/controllers/admin/gists.py:142
+#: kallithea/controllers/admin/gists.py:148
 msgid "Error occurred during gist creation"
 msgstr "gist の作成中にエラーが発生しました"
 
-#: kallithea/controllers/admin/gists.py:158
+#: kallithea/controllers/admin/gists.py:164
 #, python-format
 msgid "Deleted gist %s"
 msgstr "gist %s を削除しました"
 
-#: kallithea/controllers/admin/gists.py:198
+#: kallithea/controllers/admin/gists.py:207
 msgid "Unmodified"
 msgstr "変更しない"
 
-#: kallithea/controllers/admin/gists.py:228
+#: kallithea/controllers/admin/gists.py:237
 msgid "Successfully updated gist content"
 msgstr "Gist の内容を更新しました"
 
-#: kallithea/controllers/admin/gists.py:233
+#: kallithea/controllers/admin/gists.py:242
 msgid "Successfully updated gist data"
 msgstr "Gist データを更新しました"
 
-#: kallithea/controllers/admin/gists.py:236
+#: kallithea/controllers/admin/gists.py:245
 #, python-format
 msgid "Error occurred during update of gist %s"
 msgstr "Gist %s の更新中にエラーが発生しました"
 
-#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:211
-#: kallithea/model/user.py:232
+#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:205
+#: kallithea/model/user.py:226
 msgid "You can't edit this user since it's crucial for entire application"
 msgstr "このユーザーはアプリケーション全体で非常に重要なので編集できません"
 
@@ -571,7 +577,7 @@
 msgstr "アカウントの更新に成功しました"
 
 #: kallithea/controllers/admin/my_account.py:134
-#: kallithea/controllers/admin/users.py:181
+#: kallithea/controllers/admin/users.py:179
 #, python-format
 msgid "Error occurred during update of user %s"
 msgstr "ユーザー %s の更新中にエラーが発生しました"
@@ -581,45 +587,45 @@
 msgstr "パスワードの更新中にエラーが発生しました"
 
 #: kallithea/controllers/admin/my_account.py:209
-#: kallithea/controllers/admin/users.py:367
+#: kallithea/controllers/admin/users.py:365
 #, python-format
 msgid "Added email %s to user"
 msgstr "ユーザーにメールアドレス %s を追加しました"
 
 #: kallithea/controllers/admin/my_account.py:215
-#: kallithea/controllers/admin/users.py:373
+#: kallithea/controllers/admin/users.py:371
 msgid "An error occurred during email saving"
 msgstr "メールの保存時にエラーが発生しました"
 
 #: kallithea/controllers/admin/my_account.py:224
-#: kallithea/controllers/admin/users.py:383
+#: kallithea/controllers/admin/users.py:381
 msgid "Removed email from user"
 msgstr "ユーザーからメールアドレスを削除しました"
 
 #: kallithea/controllers/admin/my_account.py:248
-#: kallithea/controllers/admin/users.py:271
+#: kallithea/controllers/admin/users.py:269
 msgid "API key successfully created"
 msgstr "APIキーの作成に成功しました"
 
 #: kallithea/controllers/admin/my_account.py:257
-#: kallithea/controllers/admin/users.py:281
+#: kallithea/controllers/admin/users.py:279
 msgid "API key successfully reset"
 msgstr "APIキーのリセットに成功しました"
 
 #: kallithea/controllers/admin/my_account.py:261
-#: kallithea/controllers/admin/users.py:285
+#: kallithea/controllers/admin/users.py:283
 msgid "API key successfully deleted"
 msgstr "APIキーの削除に成功しました"
 
 #: kallithea/controllers/admin/my_account.py:281
-#: kallithea/controllers/admin/users.py:456
+#: kallithea/controllers/admin/users.py:454
 #, fuzzy, python-format
 #| msgid "API key successfully created"
 msgid "SSH key %s successfully added"
 msgstr "APIキーの作成に成功しました"
 
 #: kallithea/controllers/admin/my_account.py:293
-#: kallithea/controllers/admin/users.py:470
+#: kallithea/controllers/admin/users.py:468
 #, fuzzy
 #| msgid "API key successfully deleted"
 msgid "SSH key successfully deleted"
@@ -697,11 +703,11 @@
 msgid "Allowed with automatic account activation"
 msgstr "自動でアカウントをアクティベートする"
 
-#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1673
+#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1578
 msgid "Manual activation of external account"
 msgstr "外部アカウントを手動でアクティベートする"
 
-#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1674
+#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1579
 msgid "Automatic activation of external account"
 msgstr "外部アカウントを自動でアクティベートする"
 
@@ -723,187 +729,179 @@
 msgid "Error occurred during update of permissions"
 msgstr "権限の更新中にエラーが発生しました"
 
-#: kallithea/controllers/admin/repo_groups.py:172
+#: kallithea/controllers/admin/repo_groups.py:165
 #, python-format
 msgid "Error occurred during creation of repository group %s"
 msgstr "リポジトリグループ %s の作成中にエラーが発生しました"
 
-#: kallithea/controllers/admin/repo_groups.py:177
+#: kallithea/controllers/admin/repo_groups.py:172
 #, python-format
 msgid "Created repository group %s"
 msgstr "リポジトリグループ %s を作成しました"
 
-#: kallithea/controllers/admin/repo_groups.py:224
+#: kallithea/controllers/admin/repo_groups.py:219
 #, python-format
 msgid "Updated repository group %s"
 msgstr "リポジトリグループ %s を更新しました"
 
-#: kallithea/controllers/admin/repo_groups.py:240
+#: kallithea/controllers/admin/repo_groups.py:235
 #, python-format
 msgid "Error occurred during update of repository group %s"
 msgstr "リポジトリグループ %s の更新中にエラーが発生しました"
 
-#: kallithea/controllers/admin/repo_groups.py:250
+#: kallithea/controllers/admin/repo_groups.py:245
 #, python-format
 msgid "This group contains %s repositories and cannot be deleted"
 msgstr "このグループは %s 個のリポジトリを含んでいるため削除できません"
 
-#: kallithea/controllers/admin/repo_groups.py:257
+#: kallithea/controllers/admin/repo_groups.py:252
 #, python-format
 msgid "This group contains %s subgroups and cannot be deleted"
 msgstr "このグループは %s 個のサブグループを含んでいるため削除できません"
 
-#: kallithea/controllers/admin/repo_groups.py:263
+#: kallithea/controllers/admin/repo_groups.py:258
 #, python-format
 msgid "Removed repository group %s"
 msgstr "リポジトリグループ %s を削除しました"
 
-#: kallithea/controllers/admin/repo_groups.py:268
+#: kallithea/controllers/admin/repo_groups.py:263
 #, python-format
 msgid "Error occurred during deletion of repository group %s"
 msgstr "リポジトリグループ %s の削除中にエラーが発生しました"
 
-#: kallithea/controllers/admin/repo_groups.py:352
-#: kallithea/controllers/admin/repo_groups.py:382
-#: kallithea/controllers/admin/user_groups.py:294
+#: kallithea/controllers/admin/repo_groups.py:347
+#: kallithea/controllers/admin/repo_groups.py:377
+#: kallithea/controllers/admin/user_groups.py:290
 msgid "Cannot revoke permission for yourself as admin"
 msgstr "自分自身の管理者としての権限を取り消すことはできません"
 
-#: kallithea/controllers/admin/repo_groups.py:367
+#: kallithea/controllers/admin/repo_groups.py:362
 msgid "Repository group permissions updated"
 msgstr "リポジトリグループ権限を更新しました"
 
-#: kallithea/controllers/admin/repo_groups.py:399
-#: kallithea/controllers/admin/repos.py:358
-#: kallithea/controllers/admin/user_groups.py:306
+#: kallithea/controllers/admin/repo_groups.py:394
+#: kallithea/controllers/admin/repos.py:357
+#: kallithea/controllers/admin/user_groups.py:302
 msgid "An error occurred during revoking of permission"
 msgstr "権限の取消中にエラーが発生しました"
 
-#: kallithea/controllers/admin/repos.py:136
+#: kallithea/controllers/admin/repos.py:137
 #, python-format
 msgid "Error creating repository %s"
 msgstr "リポジトリ %s の作成中にエラーが発生しました"
 
-#: kallithea/controllers/admin/repos.py:194
+#: kallithea/controllers/admin/repos.py:193
 #, python-format
 msgid "Created repository %s from %s"
 msgstr "リポジトリ %s を %s から作成しました"
 
-#: kallithea/controllers/admin/repos.py:203
+#: kallithea/controllers/admin/repos.py:202
 #, python-format
 msgid "Forked repository %s as %s"
 msgstr "リポジトリ %s を %s としてフォークしました"
 
-#: kallithea/controllers/admin/repos.py:206
+#: kallithea/controllers/admin/repos.py:205
 #, python-format
 msgid "Created repository %s"
 msgstr "リポジトリ %s を作成しました"
 
-#: kallithea/controllers/admin/repos.py:235
+#: kallithea/controllers/admin/repos.py:234
 #, python-format
 msgid "Repository %s updated successfully"
 msgstr "リポジトリ %s の更新に成功しました"
 
-#: kallithea/controllers/admin/repos.py:255
+#: kallithea/controllers/admin/repos.py:254
 #, python-format
 msgid "Error occurred during update of repository %s"
 msgstr "リポジトリ %s の更新中にエラーが発生しました"
 
-#: kallithea/controllers/admin/repos.py:273
+#: kallithea/controllers/admin/repos.py:272
 #, python-format
 msgid "Detached %s forks"
 msgstr "%s 個のフォークを切り離しました"
 
-#: kallithea/controllers/admin/repos.py:276
+#: kallithea/controllers/admin/repos.py:275
 #, python-format
 msgid "Deleted %s forks"
 msgstr "%s 個のフォークを削除しました"
 
-#: kallithea/controllers/admin/repos.py:281
+#: kallithea/controllers/admin/repos.py:280
 #, python-format
 msgid "Deleted repository %s"
 msgstr "リポジトリ %s を削除しました"
 
-#: kallithea/controllers/admin/repos.py:284
+#: kallithea/controllers/admin/repos.py:283
 #, python-format
 msgid "Cannot delete repository %s which still has forks"
 msgstr ""
 "フォークしたリポジトリが存在するため、 リポジトリ %s は削除できません"
 
-#: kallithea/controllers/admin/repos.py:289
+#: kallithea/controllers/admin/repos.py:288
 #, python-format
 msgid "An error occurred during deletion of %s"
 msgstr "%s の削除中にエラーが発生しました"
 
-#: kallithea/controllers/admin/repos.py:329
+#: kallithea/controllers/admin/repos.py:328
 msgid "Repository permissions updated"
 msgstr "リポジトリ権限を更新しました"
 
-#: kallithea/controllers/admin/repos.py:388
+#: kallithea/controllers/admin/repos.py:387
 #, python-format
 msgid "Field validation error: %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:391
+#: kallithea/controllers/admin/repos.py:390
 #, fuzzy, python-format
 #| msgid "An error occurred during creation of field"
 msgid "An error occurred during creation of field: %r"
 msgstr "フィールドの作成中にエラーが発生しました"
 
-#: kallithea/controllers/admin/repos.py:402
+#: kallithea/controllers/admin/repos.py:401
 msgid "An error occurred during removal of field"
 msgstr "フィールドの削除中にエラーが発生しました"
 
-#: kallithea/controllers/admin/repos.py:416
+#: kallithea/controllers/admin/repos.py:415
 msgid "-- Not a fork --"
 msgstr "-- フォークではありません --"
 
-#: kallithea/controllers/admin/repos.py:448
+#: kallithea/controllers/admin/repos.py:447
 msgid "Updated repository visibility in public journal"
 msgstr "公開ジャーナルでのリポジトリの可視性を更新しました"
 
-#: kallithea/controllers/admin/repos.py:452
+#: kallithea/controllers/admin/repos.py:451
 msgid "An error occurred during setting this repository in public journal"
 msgstr "このリポジトリの公開ジャーナルの設定中にエラーが発生しました"
 
-#: kallithea/controllers/admin/repos.py:468
+#: kallithea/controllers/admin/repos.py:467
 msgid "Nothing"
 msgstr "ありません"
 
-#: kallithea/controllers/admin/repos.py:470
+#: kallithea/controllers/admin/repos.py:469
 #, python-format
 msgid "Marked repository %s as fork of %s"
 msgstr "%s リポジトリを %s のフォークとする"
 
-#: kallithea/controllers/admin/repos.py:477
+#: kallithea/controllers/admin/repos.py:476
 msgid "An error occurred during this operation"
 msgstr "操作中にエラーが発生しました"
 
-#: kallithea/controllers/admin/repos.py:490
-msgid "Cache invalidation successful"
-msgstr "キャッシュの無効化に成功しました"
-
-#: kallithea/controllers/admin/repos.py:494
-msgid "An error occurred during cache invalidation"
-msgstr "キャッシュの無効化中にエラーが発生しました"
-
-#: kallithea/controllers/admin/repos.py:507
+#: kallithea/controllers/admin/repos.py:488
 msgid "Pulled from remote location"
 msgstr "リモートから取得"
 
-#: kallithea/controllers/admin/repos.py:510
+#: kallithea/controllers/admin/repos.py:491
 msgid "An error occurred during pull from remote location"
 msgstr "リモートから取得中にエラーが発生しました"
 
-#: kallithea/controllers/admin/repos.py:541
+#: kallithea/controllers/admin/repos.py:522
 msgid "An error occurred during deletion of repository stats"
 msgstr "リポジトリステートの削除中にエラーが発生しました"
 
-#: kallithea/controllers/admin/settings.py:131
+#: kallithea/controllers/admin/settings.py:132
 msgid "Updated VCS settings"
 msgstr "VCS設定を更新しました"
 
-#: kallithea/controllers/admin/settings.py:135 kallithea/lib/utils.py:237
+#: kallithea/controllers/admin/settings.py:136
 msgid ""
 "Unable to activate hgsubversion support. The \"hgsubversion\" library is "
 "missing"
@@ -911,159 +909,159 @@
 "\"hgsubversion\"ライブラリが見つからないため、hgsubversionサポートを有効に"
 "出来ません"
 
-#: kallithea/controllers/admin/settings.py:141
-#: kallithea/controllers/admin/settings.py:233
+#: kallithea/controllers/admin/settings.py:142
+#: kallithea/controllers/admin/settings.py:234
 msgid "Error occurred while updating application settings"
 msgstr "アプリケーション設定の更新中にエラーが発生しました"
 
-#: kallithea/controllers/admin/settings.py:176
+#: kallithea/controllers/admin/settings.py:177
 #, python-format
 msgid "Repositories successfully rescanned. Added: %s. Removed: %s."
 msgstr "リポジトリの再スキャンに成功しました。 追加: %s 削除: %s。"
 
-#: kallithea/controllers/admin/settings.py:188
+#: kallithea/controllers/admin/settings.py:189
 #, fuzzy, python-format
 #| msgid "Invalidate cache for all repositories"
 msgid "Invalidated %s repositories"
 msgstr "すべてのリポジトリのキャッシュを無効化する"
 
-#: kallithea/controllers/admin/settings.py:229
+#: kallithea/controllers/admin/settings.py:230
 msgid "Updated application settings"
 msgstr "アプリケーション設定を更新しました"
 
-#: kallithea/controllers/admin/settings.py:283
+#: kallithea/controllers/admin/settings.py:284
 msgid "Updated visualisation settings"
 msgstr "表示設定を更新しました"
 
-#: kallithea/controllers/admin/settings.py:288
+#: kallithea/controllers/admin/settings.py:289
 msgid "Error occurred during updating visualisation settings"
 msgstr "表示設定の更新中にエラーが発生しました"
 
-#: kallithea/controllers/admin/settings.py:312
+#: kallithea/controllers/admin/settings.py:313
 msgid "Please enter email address"
 msgstr "メールアドレスを入力してください"
 
-#: kallithea/controllers/admin/settings.py:327
+#: kallithea/controllers/admin/settings.py:328
 msgid "Send email task created"
 msgstr "メール送信タスクを作成しました"
 
-#: kallithea/controllers/admin/settings.py:355
+#: kallithea/controllers/admin/settings.py:356
 #, fuzzy
 #| msgid "No data ready yet"
 msgid "Hook already exists"
 msgstr "まだデータの準備ができていません"
 
-#: kallithea/controllers/admin/settings.py:357
+#: kallithea/controllers/admin/settings.py:358
 msgid "Builtin hooks are read-only. Please use another hook name."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:360
+#: kallithea/controllers/admin/settings.py:361
 msgid "Added new hook"
 msgstr "新しいフックを追加しました"
 
-#: kallithea/controllers/admin/settings.py:376
+#: kallithea/controllers/admin/settings.py:377
 msgid "Updated hooks"
 msgstr "フックを更新しました"
 
-#: kallithea/controllers/admin/settings.py:380
+#: kallithea/controllers/admin/settings.py:381
 msgid "Error occurred during hook creation"
 msgstr "フックの作成中にエラーが発生しました"
 
-#: kallithea/controllers/admin/settings.py:404
+#: kallithea/controllers/admin/settings.py:405
 msgid "Whoosh reindex task scheduled"
 msgstr "Whooshの再インデックスタスクを予定に入れました"
 
-#: kallithea/controllers/admin/user_groups.py:138
+#: kallithea/controllers/admin/user_groups.py:134
 #, python-format
 msgid "Created user group %s"
 msgstr "ユーザーグループ %s を作成しました"
 
-#: kallithea/controllers/admin/user_groups.py:151
+#: kallithea/controllers/admin/user_groups.py:147
 #, python-format
 msgid "Error occurred during creation of user group %s"
 msgstr "ユーザーグループ %s の作成中にエラーが発生しました"
 
-#: kallithea/controllers/admin/user_groups.py:179
+#: kallithea/controllers/admin/user_groups.py:175
 #, python-format
 msgid "Updated user group %s"
 msgstr "ユーザーグループ %s を更新しました"
 
-#: kallithea/controllers/admin/user_groups.py:201
+#: kallithea/controllers/admin/user_groups.py:197
 #, python-format
 msgid "Error occurred during update of user group %s"
 msgstr "ユーザーグループ %s の更新中にエラーが発生しました"
 
-#: kallithea/controllers/admin/user_groups.py:212
+#: kallithea/controllers/admin/user_groups.py:208
 msgid "Successfully deleted user group"
 msgstr "ユーザーグループの削除に成功しました"
 
-#: kallithea/controllers/admin/user_groups.py:217
+#: kallithea/controllers/admin/user_groups.py:213
 msgid "An error occurred during deletion of user group"
 msgstr "ユーザーグループの削除中にエラーが発生しました"
 
-#: kallithea/controllers/admin/user_groups.py:273
+#: kallithea/controllers/admin/user_groups.py:269
 msgid "Target group cannot be the same"
 msgstr "対象に同じ物を選ぶことはできません"
 
-#: kallithea/controllers/admin/user_groups.py:279
+#: kallithea/controllers/admin/user_groups.py:275
 msgid "User group permissions updated"
 msgstr "ユーザーグループ権限を更新しました"
 
-#: kallithea/controllers/admin/user_groups.py:388
-#: kallithea/controllers/admin/users.py:338
+#: kallithea/controllers/admin/user_groups.py:384
+#: kallithea/controllers/admin/users.py:336
 msgid "Updated permissions"
 msgstr "権限を更新しました"
 
-#: kallithea/controllers/admin/user_groups.py:392
-#: kallithea/controllers/admin/users.py:342
+#: kallithea/controllers/admin/user_groups.py:388
+#: kallithea/controllers/admin/users.py:340
 msgid "An error occurred during permissions saving"
 msgstr "権限の保存時にエラーが発生しました"
 
-#: kallithea/controllers/admin/users.py:123
+#: kallithea/controllers/admin/users.py:121
 #, python-format
 msgid "Created user %s"
 msgstr "ユーザー %s を作成しました"
 
-#: kallithea/controllers/admin/users.py:138
+#: kallithea/controllers/admin/users.py:136
 #, python-format
 msgid "Error occurred during creation of user %s"
 msgstr "ユーザー %s の作成中にエラーが発生しました"
 
-#: kallithea/controllers/admin/users.py:162
+#: kallithea/controllers/admin/users.py:160
 msgid "User updated successfully"
 msgstr "ユーザーの更新に成功しました"
 
-#: kallithea/controllers/admin/users.py:190
+#: kallithea/controllers/admin/users.py:188
 msgid "Successfully deleted user"
 msgstr "ユーザーの削除に成功しました"
 
-#: kallithea/controllers/admin/users.py:195
+#: kallithea/controllers/admin/users.py:193
 msgid "An error occurred during deletion of user"
 msgstr "ユーザーの削除中にエラーが発生しました"
 
-#: kallithea/controllers/admin/users.py:203
+#: kallithea/controllers/admin/users.py:201
 msgid "The default user cannot be edited"
 msgstr "デフォルト ユーザーを編集できません"
 
-#: kallithea/controllers/admin/users.py:409
+#: kallithea/controllers/admin/users.py:407
 #, python-format
 msgid "Added IP address %s to user whitelist"
 msgstr "ユーザーホワイトリストにIP %s を追加しました"
 
-#: kallithea/controllers/admin/users.py:415
+#: kallithea/controllers/admin/users.py:413
 msgid "An error occurred while adding IP address"
 msgstr "IPアドレスの保存中にエラーが発生しました"
 
-#: kallithea/controllers/admin/users.py:427
+#: kallithea/controllers/admin/users.py:425
 msgid "Removed IP address from user whitelist"
 msgstr "ユーザーホワイトリストからIPアドレスを削除しました"
 
-#: kallithea/lib/auth.py:684
+#: kallithea/lib/auth.py:634
 msgid "You need to be a registered user to perform this action"
 msgstr ""
 "このアクションを実行するためには登録済みのユーザーである必要があります"
 
-#: kallithea/lib/auth.py:712
+#: kallithea/lib/auth.py:662
 msgid "You need to be signed in to view this page"
 msgstr "このページを閲覧するためにはサインインが必要です"
 
@@ -1099,171 +1097,171 @@
 "チェンジセットが大きすぎるため省略しました。差分を表示する場合は差分メ"
 "ニューを使用してください"
 
-#: kallithea/lib/diffs.py:224
+#: kallithea/lib/diffs.py:223
 msgid "No changes detected"
 msgstr "検出された変更はありません"
 
-#: kallithea/lib/helpers.py:653
+#: kallithea/lib/helpers.py:670
 #, python-format
 msgid "Deleted branch: %s"
 msgstr "削除されたブランチ: %s"
 
-#: kallithea/lib/helpers.py:655
+#: kallithea/lib/helpers.py:672
 #, python-format
 msgid "Created tag: %s"
 msgstr "作成したタグ: %s"
 
-#: kallithea/lib/helpers.py:666
+#: kallithea/lib/helpers.py:683
 #, fuzzy, python-format
 #| msgid "Changeset not found"
 msgid "Changeset %s not found"
 msgstr "リビジョンが見つかりません"
 
-#: kallithea/lib/helpers.py:715
+#: kallithea/lib/helpers.py:732
 #, python-format
 msgid "Show all combined changesets %s->%s"
 msgstr "%s から %s までのすべてのチェンジセットを表示"
 
-#: kallithea/lib/helpers.py:721
+#: kallithea/lib/helpers.py:738
 msgid "Compare view"
 msgstr "比較ビュー"
 
-#: kallithea/lib/helpers.py:740
+#: kallithea/lib/helpers.py:757
 msgid "and"
 msgstr "と"
 
-#: kallithea/lib/helpers.py:741
+#: kallithea/lib/helpers.py:758
 #, python-format
 msgid "%s more"
 msgstr "%s 以上"
 
-#: kallithea/lib/helpers.py:742
+#: kallithea/lib/helpers.py:759
 #: kallithea/templates/changelog/changelog.html:43
 msgid "revisions"
 msgstr "リビジョン"
 
-#: kallithea/lib/helpers.py:766
+#: kallithea/lib/helpers.py:783
 #, python-format
 msgid "Fork name %s"
 msgstr "フォーク名 %s"
 
-#: kallithea/lib/helpers.py:787
+#: kallithea/lib/helpers.py:804
 #, python-format
 msgid "Pull request %s"
 msgstr "プルリクエスト #%s"
 
-#: kallithea/lib/helpers.py:797
+#: kallithea/lib/helpers.py:814
 msgid "[deleted] repository"
 msgstr "リポジトリを[削除]"
 
-#: kallithea/lib/helpers.py:799 kallithea/lib/helpers.py:811
+#: kallithea/lib/helpers.py:816 kallithea/lib/helpers.py:828
 msgid "[created] repository"
 msgstr "リポジトリを[作成]"
 
-#: kallithea/lib/helpers.py:801
+#: kallithea/lib/helpers.py:818
 msgid "[created] repository as fork"
 msgstr "フォークしてリポジトリを[作成]"
 
-#: kallithea/lib/helpers.py:803 kallithea/lib/helpers.py:813
+#: kallithea/lib/helpers.py:820 kallithea/lib/helpers.py:830
 msgid "[forked] repository"
 msgstr "リポジトリを[フォーク]"
 
-#: kallithea/lib/helpers.py:805 kallithea/lib/helpers.py:815
+#: kallithea/lib/helpers.py:822 kallithea/lib/helpers.py:832
 msgid "[updated] repository"
 msgstr "リポジトリを[更新]"
 
-#: kallithea/lib/helpers.py:807
+#: kallithea/lib/helpers.py:824
 msgid "[downloaded] archive from repository"
 msgstr "リポジトリからアーカイブを[ダウンロード]"
 
-#: kallithea/lib/helpers.py:809
+#: kallithea/lib/helpers.py:826
 msgid "[delete] repository"
 msgstr "リポジトリを[削除]"
 
-#: kallithea/lib/helpers.py:817
+#: kallithea/lib/helpers.py:834
 msgid "[created] user"
 msgstr "ユーザーを[作成]"
 
-#: kallithea/lib/helpers.py:819
+#: kallithea/lib/helpers.py:836
 msgid "[updated] user"
 msgstr "ユーザーを[更新]"
 
-#: kallithea/lib/helpers.py:821
+#: kallithea/lib/helpers.py:838
 msgid "[created] user group"
 msgstr "ユーザーグループを[作成]"
 
-#: kallithea/lib/helpers.py:823
+#: kallithea/lib/helpers.py:840
 msgid "[updated] user group"
 msgstr "ユーザーグループを[更新]"
 
-#: kallithea/lib/helpers.py:825
+#: kallithea/lib/helpers.py:842
 msgid "[commented] on revision in repository"
 msgstr "リポジトリのリビジョンに[コメント]"
 
-#: kallithea/lib/helpers.py:827
+#: kallithea/lib/helpers.py:844
 msgid "[commented] on pull request for"
 msgstr "プルリクエストに[コメント]"
 
-#: kallithea/lib/helpers.py:829
+#: kallithea/lib/helpers.py:846
 msgid "[closed] pull request for"
 msgstr "プルリクエストを[クローズ]"
 
-#: kallithea/lib/helpers.py:831
+#: kallithea/lib/helpers.py:848
 msgid "[pushed] into"
 msgstr "[プッシュ]"
 
-#: kallithea/lib/helpers.py:833
+#: kallithea/lib/helpers.py:850
 msgid "[committed via Kallithea] into repository"
 msgstr "リポジトリに[Kallithea経由でコミット]"
 
-#: kallithea/lib/helpers.py:835
+#: kallithea/lib/helpers.py:852
 msgid "[pulled from remote] into repository"
 msgstr "リポジトリに[リモートからプル]"
 
-#: kallithea/lib/helpers.py:837
+#: kallithea/lib/helpers.py:854
 msgid "[pulled] from"
 msgstr "[プル]"
 
-#: kallithea/lib/helpers.py:839
+#: kallithea/lib/helpers.py:856
 msgid "[started following] repository"
 msgstr "リポジトリの[フォローを開始]"
 
-#: kallithea/lib/helpers.py:841
+#: kallithea/lib/helpers.py:858
 msgid "[stopped following] repository"
 msgstr "リポジトリの[フォローを停止]"
 
-#: kallithea/lib/helpers.py:961
+#: kallithea/lib/helpers.py:975
 #, python-format
 msgid " and %s more"
 msgstr " と %s 以上"
 
-#: kallithea/lib/helpers.py:965
+#: kallithea/lib/helpers.py:979
 #: kallithea/templates/compare/compare_diff.html:69
 #: kallithea/templates/pullrequests/pullrequest_show.html:297
 msgid "No files"
 msgstr "ファイルはありません"
 
-#: kallithea/lib/helpers.py:990
+#: kallithea/lib/helpers.py:1004
 msgid "new file"
 msgstr "新しいファイル"
 
-#: kallithea/lib/helpers.py:993
+#: kallithea/lib/helpers.py:1007
 msgid "mod"
 msgstr "変更"
 
-#: kallithea/lib/helpers.py:996
+#: kallithea/lib/helpers.py:1010
 msgid "del"
 msgstr "削除"
 
-#: kallithea/lib/helpers.py:999
+#: kallithea/lib/helpers.py:1013
 msgid "rename"
 msgstr "リネーム"
 
-#: kallithea/lib/helpers.py:1004
+#: kallithea/lib/helpers.py:1018
 msgid "chmod"
 msgstr "chmod"
 
-#: kallithea/lib/helpers.py:1297
+#: kallithea/lib/helpers.py:1323
 #, python-format
 msgid ""
 "%s repository is not mapped to db perhaps it was created or renamed from "
@@ -1274,90 +1272,92 @@
 "られたか名前が変更されたためです。リポジトリをもう一度チェックするためにア"
 "プリケーションを再起動してください"
 
-#: kallithea/lib/ssh.py:73
+#: kallithea/lib/ssh.py:75
 msgid "SSH key is missing"
 msgstr ""
 
-#: kallithea/lib/ssh.py:77
-msgid "Incorrect SSH key - it must have both a key type and a base64 part"
-msgstr ""
-
-#: kallithea/lib/ssh.py:81
+#: kallithea/lib/ssh.py:79
+msgid ""
+"Incorrect SSH key - it must have both a key type and a base64 part, like "
+"'ssh-rsa ASRNeaZu4FA...xlJp='"
+msgstr ""
+
+#: kallithea/lib/ssh.py:83
 msgid "Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'"
 msgstr ""
 
-#: kallithea/lib/ssh.py:84
+#: kallithea/lib/ssh.py:86
 #, python-format
 msgid "Incorrect SSH key - unexpected characters in base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:89
+#: kallithea/lib/ssh.py:91
 #, python-format
 msgid "Incorrect SSH key - failed to decode base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:92
+#: kallithea/lib/ssh.py:94
 #, python-format
 msgid "Incorrect SSH key - base64 part is not %r as claimed but %r"
 msgstr ""
 
-#: kallithea/lib/utils2.py:334
+#: kallithea/lib/utils2.py:253
 #, python-format
 msgid "%d year"
 msgid_plural "%d years"
 msgstr[0] "%d 年"
 
-#: kallithea/lib/utils2.py:335
+#: kallithea/lib/utils2.py:254
 #, python-format
 msgid "%d month"
 msgid_plural "%d months"
 msgstr[0] "%d ヶ月"
 
-#: kallithea/lib/utils2.py:336
+#: kallithea/lib/utils2.py:255
 #, python-format
 msgid "%d day"
 msgid_plural "%d days"
 msgstr[0] "%d 日"
 
-#: kallithea/lib/utils2.py:337
+#: kallithea/lib/utils2.py:256
 #, python-format
 msgid "%d hour"
 msgid_plural "%d hours"
 msgstr[0] "%d 時間"
 
-#: kallithea/lib/utils2.py:338
+#: kallithea/lib/utils2.py:257
 #, python-format
 msgid "%d minute"
 msgid_plural "%d minutes"
 msgstr[0] "%d 分"
 
-#: kallithea/lib/utils2.py:339
+#: kallithea/lib/utils2.py:258
 #, python-format
 msgid "%d second"
 msgid_plural "%d seconds"
 msgstr[0] "%d 秒"
 
-#: kallithea/lib/utils2.py:355
+#: kallithea/lib/utils2.py:274
 #, python-format
 msgid "in %s"
 msgstr "%s 以内"
 
-#: kallithea/lib/utils2.py:357
+#: kallithea/lib/utils2.py:276
 #, python-format
 msgid "%s ago"
 msgstr "%s 前"
 
-#: kallithea/lib/utils2.py:359
+#: kallithea/lib/utils2.py:278
 #, python-format
 msgid "in %s and %s"
 msgstr "%s と %s の間"
 
-#: kallithea/lib/utils2.py:362
+#: kallithea/lib/utils2.py:281
 #, python-format
 msgid "%s and %s ago"
 msgstr "%s と %s 前"
 
-#: kallithea/lib/utils2.py:365
+#: kallithea/lib/utils2.py:284
 msgid "just now"
 msgstr "たったいま"
 
@@ -1366,140 +1366,140 @@
 msgid "on line %s"
 msgstr "%s 行目"
 
-#: kallithea/model/comment.py:221 kallithea/model/pull_request.py:114
+#: kallithea/model/comment.py:219 kallithea/model/pull_request.py:112
 msgid "[Mention]"
 msgstr "[Mention]"
 
-#: kallithea/model/db.py:1496
+#: kallithea/model/db.py:1411
 msgid "top level"
 msgstr "top level"
 
-#: kallithea/model/db.py:1637
+#: kallithea/model/db.py:1542
 msgid "Kallithea Administrator"
 msgstr "Kallithea 管理者"
 
-#: kallithea/model/db.py:1639
+#: kallithea/model/db.py:1544
 msgid "Default user has no access to new repositories"
 msgstr "デフォルトユーザーは新しいリポジトリにアクセスできません"
 
-#: kallithea/model/db.py:1640
+#: kallithea/model/db.py:1545
 msgid "Default user has read access to new repositories"
 msgstr ""
 "デフォルトユーザーは新しいリポジトリに読み取りアクセスする権限があります"
 
-#: kallithea/model/db.py:1641
+#: kallithea/model/db.py:1546
 msgid "Default user has write access to new repositories"
 msgstr ""
 "デフォルトユーザーは新しいリポジトリに書き込みアクセスする権限があります"
 
-#: kallithea/model/db.py:1642
+#: kallithea/model/db.py:1547
 msgid "Default user has admin access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1644
+#: kallithea/model/db.py:1549
 msgid "Default user has no access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1645
+#: kallithea/model/db.py:1550
 msgid "Default user has read access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1646
+#: kallithea/model/db.py:1551
 msgid "Default user has write access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1647
+#: kallithea/model/db.py:1552
 msgid "Default user has admin access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1649
+#: kallithea/model/db.py:1554
 msgid "Default user has no access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1650
+#: kallithea/model/db.py:1555
 msgid "Default user has read access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1651
+#: kallithea/model/db.py:1556
 msgid "Default user has write access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1652
+#: kallithea/model/db.py:1557
 msgid "Default user has admin access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1654
+#: kallithea/model/db.py:1559
 msgid "Only admins can create repository groups"
 msgstr "管理者のみがリポジトリのグループを作成できます"
 
-#: kallithea/model/db.py:1655
+#: kallithea/model/db.py:1560
 msgid "Non-admins can create repository groups"
 msgstr "非管理者がリポジトリのグループを作成できます"
 
-#: kallithea/model/db.py:1657
+#: kallithea/model/db.py:1562
 msgid "Only admins can create user groups"
 msgstr "管理者だけがユーザー グループを作成することができます"
 
-#: kallithea/model/db.py:1658
+#: kallithea/model/db.py:1563
 msgid "Non-admins can create user groups"
 msgstr "非管理者ユーザーがグループを作成することができます"
 
-#: kallithea/model/db.py:1660
+#: kallithea/model/db.py:1565
 msgid "Only admins can create top level repositories"
 msgstr "管理者だけがトップレベルにリポジトリを作成することができます"
 
-#: kallithea/model/db.py:1661
+#: kallithea/model/db.py:1566
 msgid "Non-admins can create top level repositories"
 msgstr "非管理者がトップレベルにリポジトリを作成することができます"
 
-#: kallithea/model/db.py:1663
+#: kallithea/model/db.py:1568
 msgid ""
 "Repository creation enabled with write permission to a repository group"
 msgstr ""
 "リポジトリグループの書き込みパーミッションを使ったリポジトリ作成が有効です"
 
-#: kallithea/model/db.py:1664
+#: kallithea/model/db.py:1569
 msgid ""
 "Repository creation disabled with write permission to a repository group"
 msgstr ""
 "リポジトリグループの書き込みパーミッションを使ったリポジトリ作成は無効です"
 
-#: kallithea/model/db.py:1666
+#: kallithea/model/db.py:1571
 msgid "Only admins can fork repositories"
 msgstr "管理者のみがリポジトリをフォークすることができます"
 
-#: kallithea/model/db.py:1667
+#: kallithea/model/db.py:1572
 #, fuzzy
 msgid "Non-admins can fork repositories"
 msgstr "非管理者がリポジトリをフォークすることができます"
 
-#: kallithea/model/db.py:1669
+#: kallithea/model/db.py:1574
 msgid "Registration disabled"
 msgstr "新規登録を無効にする"
 
-#: kallithea/model/db.py:1670
+#: kallithea/model/db.py:1575
 msgid "User registration with manual account activation"
 msgstr "ユーザーの新規登録時に手動でアカウントをアクティベートする"
 
-#: kallithea/model/db.py:1671
+#: kallithea/model/db.py:1576
 msgid "User registration with automatic account activation"
 msgstr "ユーザーの新規登録時に自動でアカウントをアクティベートする"
 
-#: kallithea/model/db.py:2206
+#: kallithea/model/db.py:1992
 msgid "Not reviewed"
 msgstr "未レビュー"
 
-#: kallithea/model/db.py:2207
+#: kallithea/model/db.py:1993
 msgid "Under review"
 msgstr "レビュー中"
 
-#: kallithea/model/db.py:2208
+#: kallithea/model/db.py:1994
 #, fuzzy
 #| msgid "Approved"
 msgid "Not approved"
 msgstr "承認"
 
-#: kallithea/model/db.py:2209
+#: kallithea/model/db.py:1995
 msgid "Approved"
 msgstr "承認"
 
@@ -1525,7 +1525,7 @@
 msgid "Name must not contain only digits"
 msgstr "数字だけの名前は使えません"
 
-#: kallithea/model/notification.py:164
+#: kallithea/model/notification.py:162
 #, fuzzy, python-format
 #| msgid "[Comment] %(repo_name)s pull request %(pr_nice_id)s from %(ref)s"
 msgid ""
@@ -1533,30 +1533,30 @@
 "%(branch)s"
 msgstr "プルリクエストに[コメント]"
 
-#: kallithea/model/notification.py:167
+#: kallithea/model/notification.py:165
 #, python-format
 msgid "New user %(new_username)s registered"
 msgstr "新しいユーザー %(new_username)s が登録されました"
 
-#: kallithea/model/notification.py:169
+#: kallithea/model/notification.py:167
 #, python-format
 msgid ""
 "[Review] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:170
+#: kallithea/model/notification.py:168
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:183
+#: kallithea/model/notification.py:188
 msgid "Closing"
 msgstr "クローズ"
 
-#: kallithea/model/pull_request.py:73
+#: kallithea/model/pull_request.py:72
 #, python-format
 msgid ""
 "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s"
@@ -1564,80 +1564,80 @@
 "%(user)s がプリリクエスト #%(pr_nice_id)s: %(pr_title)s のレビューを求めて"
 "います"
 
-#: kallithea/model/pull_request.py:209
+#: kallithea/model/pull_request.py:207
 #, fuzzy
 #| msgid "Error creating pull request: %s"
 msgid "Cannot create empty pull request"
 msgstr "プルリクエスト作成中にエラーが発生しました: %s"
 
-#: kallithea/model/pull_request.py:217
+#: kallithea/model/pull_request.py:215
 #, python-format
 msgid ""
 "Cannot create pull request - criss cross merge detected, please merge a "
 "later %s revision to %s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:245 kallithea/model/pull_request.py:380
+#: kallithea/model/pull_request.py:243 kallithea/model/pull_request.py:378
 #, fuzzy
 #| msgid "Confirm to delete this pull request"
 msgid "You are not authorized to create the pull request"
 msgstr "このプルリクエストを削除してもよろしいですか?"
 
-#: kallithea/model/pull_request.py:339
+#: kallithea/model/pull_request.py:337
 msgid "Missing changesets since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:346
+#: kallithea/model/pull_request.py:344
 #, python-format
 msgid "New changesets on %s %s since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:353
+#: kallithea/model/pull_request.py:351
 msgid "Ancestor didn't change - diff since previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:360
+#: kallithea/model/pull_request.py:358
 #, python-format
 msgid ""
 "This iteration is based on another %s revision and there is no simple "
 "diff."
 msgstr ""
 
-#: kallithea/model/pull_request.py:362
+#: kallithea/model/pull_request.py:360
 #, python-format
 msgid "No changes found on %s %s since previous iteration."
 msgstr ""
 
-#: kallithea/model/pull_request.py:388
+#: kallithea/model/pull_request.py:386
 #, python-format
 msgid "Closed, next iteration: %s ."
 msgstr ""
 
-#: kallithea/model/scm.py:668
+#: kallithea/model/scm.py:655
 msgid "latest tip"
 msgstr "最新のtip"
 
-#: kallithea/model/ssh_key.py:56
+#: kallithea/model/ssh_key.py:57
 #, python-format
 msgid "SSH key %r is invalid: %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:68
+#: kallithea/model/ssh_key.py:69
 #, python-format
 msgid "SSH key %s is already used by %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:89
+#: kallithea/model/ssh_key.py:88
 #, fuzzy, python-format
 #| msgid "Changeset not found"
-msgid "SSH key %r not found"
+msgid "SSH key with fingerprint %r found"
 msgstr "リビジョンが見つかりません"
 
-#: kallithea/model/user.py:186
+#: kallithea/model/user.py:180
 msgid "New user registration"
 msgstr "新規ユーザー登録"
 
-#: kallithea/model/user.py:250
+#: kallithea/model/user.py:244
 #, fuzzy
 msgid ""
 "You can't remove this user since it is crucial for the entire application"
@@ -1645,7 +1645,7 @@
 "このユーザーを削除できません。このユーザーはアプリケーションにとって必要不"
 "可欠です。"
 
-#: kallithea/model/user.py:255
+#: kallithea/model/user.py:249
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repositories and cannot be removed. Switch "
@@ -1654,7 +1654,7 @@
 "ユーザー \"%s\" はまだ %s 個のリポジトリの所有者のため削除することはできま"
 "せん。リポジトリの所有者を変更するか削除してください: %s"
 
-#: kallithea/model/user.py:260
+#: kallithea/model/user.py:254
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repository groups and cannot be removed. Switch "
@@ -1663,7 +1663,7 @@
 "ユーザー \"%s\" はまだ %s 個のリポジトリグループの所有者のため削除すること"
 "はできません。リポジトリグループの所有者を変更するか削除してください: %s"
 
-#: kallithea/model/user.py:267
+#: kallithea/model/user.py:261
 #, python-format
 msgid ""
 "User \"%s\" still owns %s user groups and cannot be removed. Switch "
@@ -1672,36 +1672,36 @@
 "ユーザー \"%s\" はまだ %s 個のユーザーグループの所有者のため削除することは"
 "できません。ユーザーグループの所有者を変更するか削除してください。 %s"
 
-#: kallithea/model/user.py:361
+#: kallithea/model/user.py:355
 msgid "Password reset link"
 msgstr "パスワードリセットのリンク"
 
-#: kallithea/model/user.py:408
+#: kallithea/model/user.py:402
 msgid "Password reset notification"
 msgstr "パスワードの再設定通知"
 
-#: kallithea/model/user.py:409
+#: kallithea/model/user.py:403
 #, python-format
 msgid ""
 "The password to your account %s has been changed using password reset "
 "form."
 msgstr ""
 
-#: kallithea/model/validators.py:52 kallithea/model/validators.py:53
+#: kallithea/model/validators.py:53 kallithea/model/validators.py:54
 msgid "Value cannot be an empty list"
 msgstr "空のリストにはできません"
 
-#: kallithea/model/validators.py:72
+#: kallithea/model/validators.py:73
 #, python-format
 msgid "Username \"%(username)s\" already exists"
 msgstr "ユーザー名 \"%(username)s\" はすでに使われています"
 
-#: kallithea/model/validators.py:74
+#: kallithea/model/validators.py:75
 #, python-format
 msgid "Username \"%(username)s\" cannot be used"
 msgstr "ユーザー名 %(username)s は使用できません"
 
-#: kallithea/model/validators.py:76
+#: kallithea/model/validators.py:77
 msgid ""
 "Username may only contain alphanumeric characters underscores, periods or "
 "dashes and must begin with an alphanumeric character or underscore"
@@ -1710,25 +1710,25 @@
 "か使えません。また、アルファベットまたはアンダースコア(_)から始まる必要が"
 "あります"
 
-#: kallithea/model/validators.py:103
+#: kallithea/model/validators.py:104
 msgid "The input is not valid"
 msgstr "入力が正しくありません"
 
-#: kallithea/model/validators.py:110
+#: kallithea/model/validators.py:111
 #, python-format
 msgid "Username %(username)s is not valid"
 msgstr "ユーザー名 %(username)s は不正です"
 
-#: kallithea/model/validators.py:131
+#: kallithea/model/validators.py:132
 msgid "Invalid user group name"
 msgstr "不正なユーザーグループ名です"
 
-#: kallithea/model/validators.py:132
+#: kallithea/model/validators.py:133
 #, python-format
 msgid "User group \"%(usergroup)s\" already exists"
 msgstr "ユーザーグループ \"%(usergroup)s\" はすでに存在します"
 
-#: kallithea/model/validators.py:134
+#: kallithea/model/validators.py:135
 msgid ""
 "user group name may only contain alphanumeric characters underscores, "
 "periods or dashes and must begin with alphanumeric character"
@@ -1736,101 +1736,101 @@
 "ユーザーグループ名はアルファベット、アンダースコア(_)、ピリオド(.)、ダッ"
 "シュ(-)しか使えません。また、アルファベットから始まる必要があります"
 
-#: kallithea/model/validators.py:174
+#: kallithea/model/validators.py:175
 msgid "Cannot assign this group as parent"
 msgstr "このグループは親にできません"
 
-#: kallithea/model/validators.py:175
+#: kallithea/model/validators.py:176
 #, python-format
 msgid "Group \"%(group_name)s\" already exists"
 msgstr "グループ \"%(group_name)s\" はすでに存在します"
 
-#: kallithea/model/validators.py:177
+#: kallithea/model/validators.py:178
 #, python-format
 msgid "Repository with name \"%(group_name)s\" already exists"
 msgstr "グループ名 \"%(group_name)s\" を持つリポジトリはすでに存在します"
 
-#: kallithea/model/validators.py:233
+#: kallithea/model/validators.py:230
 msgid "Invalid characters (non-ascii) in password"
 msgstr "パスワードに利用出来ない文字列(non-ascii)です"
 
-#: kallithea/model/validators.py:248
+#: kallithea/model/validators.py:245
 msgid "Invalid old password"
 msgstr "古いpasswordが間違っています"
 
-#: kallithea/model/validators.py:264
+#: kallithea/model/validators.py:261
 msgid "Passwords do not match"
 msgstr "パスワードが一致しません"
 
-#: kallithea/model/validators.py:279
+#: kallithea/model/validators.py:276
 msgid "Invalid username or password"
 msgstr "ユーザー名とパスワードの組み合わせが無効です"
 
-#: kallithea/model/validators.py:313
+#: kallithea/model/validators.py:310
 #, python-format
 msgid "Repository name %(repo)s is not allowed"
 msgstr "リポジトリ名 %(repo)s は許可されていません"
 
-#: kallithea/model/validators.py:315
+#: kallithea/model/validators.py:312
 #, python-format
 msgid "Repository named %(repo)s already exists"
 msgstr "リポジトリ %(repo)s はすでに存在します"
 
-#: kallithea/model/validators.py:316
+#: kallithea/model/validators.py:313
 #, python-format
 msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
 msgstr ""
 "リポジトリ \"%(repo)s\" は グループ \"%(group)s\" にすでに存在します"
 
-#: kallithea/model/validators.py:318
+#: kallithea/model/validators.py:315
 #, python-format
 msgid "Repository group with name \"%(repo)s\" already exists"
 msgstr "リポジトリグループ名 \"%(repo)s\" はすでに存在します"
 
-#: kallithea/model/validators.py:404
+#: kallithea/model/validators.py:401
 msgid "Invalid repository URL"
 msgstr "無効なリポジトリのURL"
 
-#: kallithea/model/validators.py:405
+#: kallithea/model/validators.py:402
 msgid ""
 "Invalid repository URL. It must be a valid http, https, ssh, svn+http or "
 "svn+https URL"
 msgstr ""
 
-#: kallithea/model/validators.py:430
+#: kallithea/model/validators.py:427
 msgid "Fork has to be the same type as parent"
 msgstr "フォークは親と同じ種別の必要があります"
 
-#: kallithea/model/validators.py:445
+#: kallithea/model/validators.py:442
 msgid "You don't have permissions to create repository in this group"
 msgstr "このグループにリポジトリを作成する権限がありません"
 
-#: kallithea/model/validators.py:447
+#: kallithea/model/validators.py:444
 msgid "no permission to create repository in root location"
 msgstr "ルートにリポジトリを作成する権限がありません"
 
-#: kallithea/model/validators.py:497
+#: kallithea/model/validators.py:494
 msgid "You don't have permissions to create a group in this location"
 msgstr "この場所にグループを作成する権限がありません"
 
-#: kallithea/model/validators.py:537
+#: kallithea/model/validators.py:534
 msgid "This username or user group name is not valid"
 msgstr "ユーザー名かユーザーグループが不正です"
 
-#: kallithea/model/validators.py:630
+#: kallithea/model/validators.py:627
 msgid "This is not a valid path"
 msgstr "不正なパスです"
 
-#: kallithea/model/validators.py:647
+#: kallithea/model/validators.py:644
 msgid "This email address is already in use"
 msgstr "このメールアドレスはすでに取得されています"
 
-#: kallithea/model/validators.py:667
+#: kallithea/model/validators.py:664
 #, python-format
 msgid "Email address \"%(email)s\" not found"
 msgstr "メールアドレス \"%(email)s\" がみつかりません"
 
-#: kallithea/model/validators.py:704
+#: kallithea/model/validators.py:701
 msgid ""
 "The LDAP Login attribute of the CN must be specified - this is the name "
 "of the attribute that is equivalent to \"username\""
@@ -1838,11 +1838,11 @@
 "LDAPのこのCNに対するログイン属性は必須です。 - これは \"ユーザー名\" と同"
 "じです"
 
-#: kallithea/model/validators.py:716
+#: kallithea/model/validators.py:713
 msgid "Please enter a valid IPv4 or IPv6 address"
 msgstr "有効なIPv4かIPv6のアドレスを入力してください"
 
-#: kallithea/model/validators.py:717
+#: kallithea/model/validators.py:714
 #, python-format
 msgid ""
 "The network size (bits) must be within the range of 0-32 (not %(bits)r)"
@@ -1850,17 +1850,17 @@
 "ネットワークサイズ (bits) は0-32の範囲にする必要があります ( %(bits)r は不"
 "正です)"
 
-#: kallithea/model/validators.py:750
+#: kallithea/model/validators.py:747
 msgid "Key name can only consist of letters, underscore, dash or numbers"
 msgstr ""
 "キー名にはアルファベット、アンダースコア(_)、ピリオド(.)、ダッシュ(-)、数"
 "字が使えます"
 
-#: kallithea/model/validators.py:764
+#: kallithea/model/validators.py:761
 msgid "Filename cannot be inside a directory"
 msgstr "ファイル名はディレクトリ内にすることはできません"
 
-#: kallithea/model/validators.py:780
+#: kallithea/model/validators.py:777
 #, python-format
 msgid "Plugins %(loaded)s and %(next_to_load)s both export the same name"
 msgstr ""
@@ -1922,7 +1922,7 @@
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:60
 #: kallithea/templates/email_templates/pull_request.html:37
 #: kallithea/templates/forks/fork.html:34
-#: kallithea/templates/index_base.html:58
+#: kallithea/templates/index_base.html:59
 #: kallithea/templates/pullrequests/pullrequest.html:33
 #: kallithea/templates/pullrequests/pullrequest_show.html:38
 #: kallithea/templates/pullrequests/pullrequest_show.html:59
@@ -1930,14 +1930,14 @@
 msgid "Description"
 msgstr "説明"
 
-#: kallithea/templates/index_base.html:60
+#: kallithea/templates/index_base.html:61
 msgid "Last Change"
 msgstr "最後の変更点"
 
 #: kallithea/templates/admin/my_account/my_account_repos.html:15
 #: kallithea/templates/admin/my_account/my_account_watched.html:15
 #: kallithea/templates/admin/repos/repos.html:41
-#: kallithea/templates/index_base.html:62
+#: kallithea/templates/index_base.html:63
 msgid "Tip"
 msgstr "Tip"
 
@@ -1947,7 +1947,7 @@
 #: kallithea/templates/admin/repos/repos.html:42
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:8
 #: kallithea/templates/admin/user_groups/user_groups.html:42
-#: kallithea/templates/index_base.html:63
+#: kallithea/templates/index_base.html:64
 #: kallithea/templates/pullrequests/pullrequest_data.html:16
 #: kallithea/templates/pullrequests/pullrequest_show.html:124
 #: kallithea/templates/pullrequests/pullrequest_show.html:219
@@ -1971,7 +1971,7 @@
 #: kallithea/templates/admin/users/user_edit_profile.html:18
 #: kallithea/templates/admin/users/users.html:37
 #: kallithea/templates/base/base.html:364
-#: kallithea/templates/email_templates/registration.html:11
+#: kallithea/templates/email_templates/registration.html:12
 #: kallithea/templates/login.html:28 kallithea/templates/register.html:31
 msgid "Username"
 msgstr "ユーザー名"
@@ -2097,7 +2097,7 @@
 #: kallithea/templates/admin/settings/settings.html:31
 #: kallithea/templates/admin/users/user_add.html:62
 #: kallithea/templates/admin/users/user_edit_profile.html:25
-#: kallithea/templates/email_templates/registration.html:33
+#: kallithea/templates/email_templates/registration.html:34
 #: kallithea/templates/register.html:66
 msgid "Email"
 msgstr "メールアドレス"
@@ -2349,7 +2349,7 @@
 msgstr "新しい Gist を作成"
 
 #: kallithea/templates/admin/gists/index.html:51
-#: kallithea/templates/data_table/_dt_elements.html:78
+#: kallithea/templates/data_table/_dt_elements.html:84
 msgid "Created"
 msgstr "作成日"
 
@@ -2435,13 +2435,13 @@
 #: kallithea/templates/admin/users/user_edit_ips.html:21
 #: kallithea/templates/changeset/changeset_file_comment.html:30
 #: kallithea/templates/changeset/changeset_file_comment.html:121
-#: kallithea/templates/data_table/_dt_elements.html:69
-#: kallithea/templates/data_table/_dt_elements.html:89
-#: kallithea/templates/data_table/_dt_elements.html:91
-#: kallithea/templates/data_table/_dt_elements.html:101
-#: kallithea/templates/data_table/_dt_elements.html:103
-#: kallithea/templates/data_table/_dt_elements.html:120
-#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:75
+#: kallithea/templates/data_table/_dt_elements.html:95
+#: kallithea/templates/data_table/_dt_elements.html:97
+#: kallithea/templates/data_table/_dt_elements.html:107
+#: kallithea/templates/data_table/_dt_elements.html:109
+#: kallithea/templates/data_table/_dt_elements.html:126
+#: kallithea/templates/data_table/_dt_elements.html:128
 #: kallithea/templates/files/files_source.html:35
 #: kallithea/templates/files/files_source.html:38
 #: kallithea/templates/files/files_source.html:41
@@ -2457,14 +2457,14 @@
 #: kallithea/templates/base/perms_summary.html:44
 #: kallithea/templates/base/perms_summary.html:81
 #: kallithea/templates/base/perms_summary.html:83
-#: kallithea/templates/data_table/_dt_elements.html:63
-#: kallithea/templates/data_table/_dt_elements.html:64
-#: kallithea/templates/data_table/_dt_elements.html:85
-#: kallithea/templates/data_table/_dt_elements.html:86
-#: kallithea/templates/data_table/_dt_elements.html:97
-#: kallithea/templates/data_table/_dt_elements.html:98
-#: kallithea/templates/data_table/_dt_elements.html:116
-#: kallithea/templates/data_table/_dt_elements.html:117
+#: kallithea/templates/data_table/_dt_elements.html:69
+#: kallithea/templates/data_table/_dt_elements.html:70
+#: kallithea/templates/data_table/_dt_elements.html:91
+#: kallithea/templates/data_table/_dt_elements.html:92
+#: kallithea/templates/data_table/_dt_elements.html:103
+#: kallithea/templates/data_table/_dt_elements.html:104
+#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:123
 #: kallithea/templates/files/diff_2way.html:56
 #: kallithea/templates/files/files_source.html:37
 #: kallithea/templates/files/files_source.html:40
@@ -2783,7 +2783,7 @@
 #: kallithea/templates/admin/permissions/permissions_globals.html:27
 #: kallithea/templates/admin/repos/repo_add_base.html:28
 #: kallithea/templates/admin/repos/repo_edit_settings.html:33
-#: kallithea/templates/data_table/_dt_elements.html:134
+#: kallithea/templates/data_table/_dt_elements.html:140
 #: kallithea/templates/forks/fork.html:42
 msgid "Repository group"
 msgstr "リポジトリグループ"
@@ -2810,7 +2810,7 @@
 "親のリポジトリグループにセットされているパーミッションをコピーします。"
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:40
-#: kallithea/templates/data_table/_dt_elements.html:141
+#: kallithea/templates/data_table/_dt_elements.html:147
 msgid "User group"
 msgstr "ユーザーグループ"
 
@@ -2991,7 +2991,7 @@
 msgstr "作成日"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:121
+#: kallithea/templates/data_table/_dt_elements.html:127
 #, python-format
 msgid "Confirm to delete this group: %s with %s repository"
 msgid_plural "Confirm to delete this group: %s with %s repositories"
@@ -3170,14 +3170,10 @@
 msgstr "拡張フィールド"
 
 #: kallithea/templates/admin/repos/repo_edit.html:37
-msgid "Caches"
-msgstr "キャッシュ"
+msgid "Remote"
+msgstr "リモート"
 
 #: kallithea/templates/admin/repos/repo_edit.html:40
-msgid "Remote"
-msgstr "リモート"
-
-#: kallithea/templates/admin/repos/repo_edit.html:43
 #: kallithea/templates/summary/statistics.html:8
 #: kallithea/templates/summary/summary.html:169
 #: kallithea/templates/summary/summary.html:170
@@ -3216,7 +3212,7 @@
 "公開ジャーナルでは、このリポジトリに対して行った操作のすべてが公開されます"
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:46
-#: kallithea/templates/data_table/_dt_elements.html:68
+#: kallithea/templates/data_table/_dt_elements.html:74
 #, python-format
 msgid "Confirm to delete this repository: %s"
 msgstr "このリポジトリを削除してもよろしいですか? : %s"
@@ -3246,45 +3242,14 @@
 "it or restore it."
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:4
-msgid "Invalidate Repository Cache"
-msgstr "リポジトリのキャッシュを無効化"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:6
-msgid ""
-"Manually invalidate cache for this repository. On first access, the "
-"repository will be cached again."
-msgstr ""
-"このリポジトリのキャッシュを手動で無効化します。リポジトリへの初回アクセス"
-"時に再びキャッシュされます。"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:9
-msgid "List of Cached Values"
-msgstr "キャッシュしている値の一覧"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:12
-msgid "Prefix"
-msgstr "プレフィックス"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:13
+#: kallithea/templates/admin/repos/repo_edit_fields.html:6
+msgid "Label"
+msgstr "ラベル"
+
 #: kallithea/templates/admin/repos/repo_edit_fields.html:7
 msgid "Key"
 msgstr "キー"
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:14
-#: kallithea/templates/admin/user_groups/user_group_add.html:40
-#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
-#: kallithea/templates/admin/user_groups/user_groups.html:41
-#: kallithea/templates/admin/users/user_add.html:69
-#: kallithea/templates/admin/users/user_edit_profile.html:74
-#: kallithea/templates/admin/users/users.html:42
-msgid "Active"
-msgstr "アクティブ"
-
-#: kallithea/templates/admin/repos/repo_edit_fields.html:6
-msgid "Label"
-msgstr "ラベル"
-
 #: kallithea/templates/admin/repos/repo_edit_fields.html:20
 #, python-format
 msgid "Confirm to delete this field: %s"
@@ -3879,6 +3844,15 @@
 msgid "Short, optional description for this user group."
 msgstr "このユーザーグループの簡潔な説明を書いてください。"
 
+#: kallithea/templates/admin/user_groups/user_group_add.html:40
+#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
+#: kallithea/templates/admin/user_groups/user_groups.html:41
+#: kallithea/templates/admin/users/user_add.html:69
+#: kallithea/templates/admin/users/user_edit_profile.html:74
+#: kallithea/templates/admin/users/users.html:42
+msgid "Active"
+msgstr "アクティブ"
+
 #: kallithea/templates/admin/user_groups/user_group_edit.html:5
 #, python-format
 msgid "%s user group settings"
@@ -3900,7 +3874,7 @@
 msgstr "メンバー"
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:19
-#: kallithea/templates/data_table/_dt_elements.html:102
+#: kallithea/templates/data_table/_dt_elements.html:108
 #, python-format
 msgid "Confirm to delete this user group: %s"
 msgstr "このユーザーグループを削除してもよろしいですか?: %s"
@@ -3974,7 +3948,7 @@
 msgstr "グループのメンバー数"
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:90
+#: kallithea/templates/data_table/_dt_elements.html:96
 #, python-format
 msgid "Confirm to delete this user: %s"
 msgstr "このユーザーを削除してもよろしいですか? : %s"
@@ -4074,10 +4048,12 @@
 msgstr "検索"
 
 #: kallithea/templates/base/base.html:167
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Follow"
 msgstr "フォロー"
 
 #: kallithea/templates/base/base.html:168
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Unfollow"
 msgstr "アンフォロー"
 
@@ -4496,26 +4472,26 @@
 msgid "Merge"
 msgstr "マージ"
 
-#: kallithea/templates/changeset/changeset.html:96
+#: kallithea/templates/changeset/changeset.html:95
 #, fuzzy
 msgid "Grafted from:"
 msgstr "作成日"
 
-#: kallithea/templates/changeset/changeset.html:102
+#: kallithea/templates/changeset/changeset.html:100
 msgid "Transplanted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:108
+#: kallithea/templates/changeset/changeset.html:106
 #, fuzzy
 msgid "Replaced by:"
 msgstr "作成日"
 
-#: kallithea/templates/changeset/changeset.html:122
+#: kallithea/templates/changeset/changeset.html:120
 #, fuzzy
 msgid "Preceded by:"
 msgstr "作成日"
 
-#: kallithea/templates/changeset/changeset.html:139
+#: kallithea/templates/changeset/changeset.html:137
 #: kallithea/templates/compare/compare_diff.html:59
 #: kallithea/templates/pullrequests/pullrequest_show.html:290
 #, python-format
@@ -4523,7 +4499,7 @@
 msgid_plural "%s files changed"
 msgstr[0] "%s ファイルに影響"
 
-#: kallithea/templates/changeset/changeset.html:141
+#: kallithea/templates/changeset/changeset.html:139
 #: kallithea/templates/compare/compare_diff.html:61
 #: kallithea/templates/pullrequests/pullrequest_show.html:292
 #, python-format
@@ -4531,8 +4507,8 @@
 msgid_plural "%s files changed with %s insertions and %s deletions"
 msgstr[0] "%s ファイルに影響。 %s 個の追加と %s 個の削除"
 
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
 #: kallithea/templates/pullrequests/pullrequest_show.html:333
@@ -4775,23 +4751,23 @@
 msgid "Repository creation in progress..."
 msgstr "リポジトリを作成しています..."
 
-#: kallithea/templates/data_table/_dt_elements.html:42
+#: kallithea/templates/data_table/_dt_elements.html:48
 msgid "No changesets yet"
 msgstr "まだチェンジセットがありません"
 
-#: kallithea/templates/data_table/_dt_elements.html:48
-#: kallithea/templates/data_table/_dt_elements.html:50
+#: kallithea/templates/data_table/_dt_elements.html:54
+#: kallithea/templates/data_table/_dt_elements.html:56
 #, python-format
 msgid "Subscribe to %s rss feed"
 msgstr "%s の RSS フィードを購読"
 
-#: kallithea/templates/data_table/_dt_elements.html:56
-#: kallithea/templates/data_table/_dt_elements.html:58
+#: kallithea/templates/data_table/_dt_elements.html:62
+#: kallithea/templates/data_table/_dt_elements.html:64
 #, python-format
 msgid "Subscribe to %s atom feed"
 msgstr "%s の ATOM フィードを購読"
 
-#: kallithea/templates/data_table/_dt_elements.html:76
+#: kallithea/templates/data_table/_dt_elements.html:82
 msgid "Creating"
 msgstr "作成中"
 
@@ -4829,6 +4805,13 @@
 msgid "by"
 msgstr ""
 
+#: kallithea/templates/email_templates/changeset_comment.html:36
+#: kallithea/templates/email_templates/pull_request_comment.html:43
+#, fuzzy
+#| msgid "Comment"
+msgid "View Comment"
+msgstr "コメント"
+
 #: kallithea/templates/email_templates/comment.html:27
 #, fuzzy
 #| msgid "Status change"
@@ -4842,32 +4825,44 @@
 msgstr ""
 "このプルリクエストはすでにクローズされていて、更新することはできません。"
 
-#: kallithea/templates/email_templates/password_reset.html:9
+#: kallithea/templates/email_templates/default.html:4
+#, fuzzy
+#| msgid "Commit Message"
+msgid "Message"
+msgstr "コミットメッセージ"
+
+#: kallithea/templates/email_templates/password_reset.html:4
+#, fuzzy
+#| msgid "Password Reset"
+msgid "Password Reset Request"
+msgstr "パスワードのリセット"
+
+#: kallithea/templates/email_templates/password_reset.html:10
 #, python-format
 msgid "Hello %s"
 msgstr "こんにちは %s"
 
-#: kallithea/templates/email_templates/password_reset.html:16
+#: kallithea/templates/email_templates/password_reset.html:17
 msgid "We have received a request to reset the password for your account."
 msgstr "あなたのアカウントのパスワードリセットリクエストを受け取りました。"
 
-#: kallithea/templates/email_templates/password_reset.html:25
+#: kallithea/templates/email_templates/password_reset.html:26
 msgid ""
 "This account is however managed outside this system and the password "
 "cannot be changed here."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:28
+#: kallithea/templates/email_templates/password_reset.html:29
 msgid "To set a new password, click the following link"
 msgstr "新しいパスワードを設定するために、次のリンクをクリックしてください"
 
-#: kallithea/templates/email_templates/password_reset.html:33
+#: kallithea/templates/email_templates/password_reset.html:34
 msgid ""
 "Should you not be able to use the link above, please type the following "
 "code into the password reset form"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:44
+#: kallithea/templates/email_templates/password_reset.html:45
 msgid ""
 "If it weren't you who requested the password reset, just disregard this "
 "message."
@@ -4904,6 +4899,12 @@
 msgid "to"
 msgstr ""
 
+#: kallithea/templates/email_templates/pull_request.html:85
+#, fuzzy
+#| msgid "New Pull Request"
+msgid "View Pull Request"
+msgstr "新しいプルリクエスト"
+
 #: kallithea/templates/email_templates/pull_request_comment.html:4
 #, fuzzy, python-format
 #| msgid "%s mentioned you on %s pull request \"%s\""
@@ -4922,12 +4923,24 @@
 msgid "Comment on Pull Request %s \"%s\""
 msgstr "プルリクエストに[コメント]"
 
-#: kallithea/templates/email_templates/registration.html:22
+#: kallithea/templates/email_templates/registration.html:5
+#, fuzzy
+#| msgid "New user registration"
+msgid "New User Registration"
+msgstr "新規ユーザー登録"
+
+#: kallithea/templates/email_templates/registration.html:23
 #, fuzzy
 #| msgid "Group name"
 msgid "Full Name"
 msgstr "グループ名"
 
+#: kallithea/templates/email_templates/registration.html:42
+#, fuzzy
+#| msgid "View this user here"
+msgid "View User Profile"
+msgstr "このユーザを閲覧する"
+
 #: kallithea/templates/files/diff_2way.html:15
 #, python-format
 msgid "%s File side-by-side diff"
@@ -5547,45 +5560,45 @@
 msgid "Stats gathered: "
 msgstr "収集した統計情報: "
 
-#: kallithea/templates/summary/statistics.html:87
-#: kallithea/templates/summary/summary.html:371
+#: kallithea/templates/summary/statistics.html:85
+#: kallithea/templates/summary/summary.html:369
 msgid "files"
 msgstr "ファイル"
 
-#: kallithea/templates/summary/statistics.html:111
-#: kallithea/templates/summary/summary.html:401
+#: kallithea/templates/summary/statistics.html:109
+#: kallithea/templates/summary/summary.html:399
 msgid "Show more"
 msgstr "もっと表示"
 
-#: kallithea/templates/summary/statistics.html:405
+#: kallithea/templates/summary/statistics.html:395
 msgid "commits"
 msgstr "コミット"
 
-#: kallithea/templates/summary/statistics.html:406
+#: kallithea/templates/summary/statistics.html:396
 msgid "files added"
 msgstr "追加されたファイル"
 
-#: kallithea/templates/summary/statistics.html:407
+#: kallithea/templates/summary/statistics.html:397
 msgid "files changed"
 msgstr "変更されたファイル"
 
-#: kallithea/templates/summary/statistics.html:408
+#: kallithea/templates/summary/statistics.html:398
 msgid "files removed"
 msgstr "削除されたファイル"
 
-#: kallithea/templates/summary/statistics.html:410
+#: kallithea/templates/summary/statistics.html:400
 msgid "commit"
 msgstr "コミット"
 
-#: kallithea/templates/summary/statistics.html:411
+#: kallithea/templates/summary/statistics.html:401
 msgid "file added"
 msgstr "追加されたファイル"
 
-#: kallithea/templates/summary/statistics.html:412
+#: kallithea/templates/summary/statistics.html:402
 msgid "file changed"
 msgstr "変更されたファイル"
 
-#: kallithea/templates/summary/statistics.html:413
+#: kallithea/templates/summary/statistics.html:403
 msgid "file removed"
 msgstr "削除されたファイル"
 
@@ -5688,6 +5701,31 @@
 msgid "Download %s as %s"
 msgstr "%s を %s でダウンロード"
 
+#~ msgid "Cache invalidation successful"
+#~ msgstr "キャッシュの無効化に成功しました"
+
+#~ msgid "An error occurred during cache invalidation"
+#~ msgstr "キャッシュの無効化中にエラーが発生しました"
+
+#~ msgid "Caches"
+#~ msgstr "キャッシュ"
+
+#~ msgid "Invalidate Repository Cache"
+#~ msgstr "リポジトリのキャッシュを無効化"
+
+#~ msgid ""
+#~ "Manually invalidate cache for this repository. On first access, the "
+#~ "repository will be cached again."
+#~ msgstr ""
+#~ "このリポジトリのキャッシュを手動で無効化します。リポジトリへの初回アク"
+#~ "セス時に再びキャッシュされます。"
+
+#~ msgid "List of Cached Values"
+#~ msgstr "キャッシュしている値の一覧"
+
+#~ msgid "Prefix"
+#~ msgstr "プレフィックス"
+
 #~ msgid "This repository has been locked by %s on %s"
 #~ msgstr "このリポジトリは %s によって %s にロックされました"
 
@@ -6063,9 +6101,6 @@
 #~ msgid "The comment was made with status"
 #~ msgstr "プルリクエストを以下のステータスで閉じました:"
 
-#~ msgid "View this user here"
-#~ msgstr "このユーザを閲覧する"
-
 #~ msgid "Edit on Branch:%s"
 #~ msgstr "ブランチ:%s で編集"
 
--- a/kallithea/i18n/kallithea.pot	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/i18n/kallithea.pot	Mon May 04 19:24:04 2020 +0200
@@ -6,9 +6,9 @@
 #, fuzzy
 msgid ""
 msgstr ""
-"Project-Id-Version: Kallithea 0.5.0\n"
+"Project-Id-Version: Kallithea 0.5.99\n"
 "Report-Msgid-Bugs-To: translations@kallithea-scm.org\n"
-"POT-Creation-Date: 2020-01-05 04:10+0100\n"
+"POT-Creation-Date: 2020-05-04 19:12+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -18,14 +18,14 @@
 "Generated-By: Babel 2.7.0\n"
 
 #: kallithea/controllers/changelog.py:67
-#: kallithea/controllers/pullrequests.py:250 kallithea/lib/base.py:602
+#: kallithea/controllers/pullrequests.py:247 kallithea/lib/base.py:602
 msgid "There are no changesets yet"
 msgstr ""
 
 #: kallithea/controllers/admin/permissions.py:64
 #: kallithea/controllers/admin/permissions.py:68
 #: kallithea/controllers/admin/permissions.py:72
-#: kallithea/controllers/changelog.py:137
+#: kallithea/controllers/changelog.py:136
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:7
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:88
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:7
@@ -34,35 +34,35 @@
 msgid "None"
 msgstr ""
 
-#: kallithea/controllers/changelog.py:140 kallithea/controllers/files.py:189
+#: kallithea/controllers/changelog.py:139 kallithea/controllers/files.py:189
 msgid "(closed)"
 msgstr ""
 
-#: kallithea/controllers/changeset.py:81
+#: kallithea/controllers/changeset.py:82
 msgid "Show whitespace"
 msgstr ""
 
-#: kallithea/controllers/changeset.py:88 kallithea/controllers/changeset.py:95
+#: kallithea/controllers/changeset.py:89 kallithea/controllers/changeset.py:96
 #: kallithea/templates/files/diff_2way.html:55
 msgid "Ignore whitespace"
 msgstr ""
 
-#: kallithea/controllers/changeset.py:161
+#: kallithea/controllers/changeset.py:162
 #, python-format
 msgid "Increase diff context to %(num)s lines"
 msgstr ""
 
-#: kallithea/controllers/changeset.py:201
+#: kallithea/controllers/changeset.py:202
 msgid "No permission to change status"
 msgstr ""
 
-#: kallithea/controllers/changeset.py:212
+#: kallithea/controllers/changeset.py:213
 #, python-format
 msgid "Successfully deleted pull request %s"
 msgstr ""
 
 #: kallithea/controllers/changeset.py:319 kallithea/controllers/files.py:89
-#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:700
+#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:697
 msgid "Such revision does not exist for this repository"
 msgstr ""
 
@@ -75,60 +75,60 @@
 msgid "Cannot compare repositories of different types"
 msgstr ""
 
-#: kallithea/controllers/compare.py:246
+#: kallithea/controllers/compare.py:247
 msgid "Cannot show empty diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:248
+#: kallithea/controllers/compare.py:249
 msgid "No ancestor found for merge diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:252
+#: kallithea/controllers/compare.py:253
 msgid "Multiple merge ancestors found for merge compare"
 msgstr ""
 
-#: kallithea/controllers/compare.py:268
+#: kallithea/controllers/compare.py:269
 msgid "Cannot compare repositories without using common ancestor"
 msgstr ""
 
-#: kallithea/controllers/error.py:71
+#: kallithea/controllers/error.py:67
 msgid "No response"
 msgstr ""
 
-#: kallithea/controllers/error.py:72
+#: kallithea/controllers/error.py:68
 msgid "Unknown error"
 msgstr ""
 
-#: kallithea/controllers/error.py:85
+#: kallithea/controllers/error.py:81
 msgid "The request could not be understood by the server due to malformed syntax."
 msgstr ""
 
+#: kallithea/controllers/error.py:84
+msgid "Unauthorized access to resource"
+msgstr ""
+
+#: kallithea/controllers/error.py:86
+msgid "You don't have permission to view this page"
+msgstr ""
+
 #: kallithea/controllers/error.py:88
-msgid "Unauthorized access to resource"
+msgid "The resource could not be found"
 msgstr ""
 
 #: kallithea/controllers/error.py:90
-msgid "You don't have permission to view this page"
-msgstr ""
-
-#: kallithea/controllers/error.py:92
-msgid "The resource could not be found"
-msgstr ""
-
-#: kallithea/controllers/error.py:94
 msgid ""
 "The server encountered an unexpected condition which prevented it from "
 "fulfilling the request."
 msgstr ""
 
-#: kallithea/controllers/feed.py:63
+#: kallithea/controllers/feed.py:59
 #, python-format
 msgid "%s committed on %s"
 msgstr ""
 
-#: kallithea/controllers/feed.py:88
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/controllers/feed.py:84
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/compare/compare_diff.html:95
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
@@ -136,12 +136,12 @@
 msgid "Changeset was too big and was cut off..."
 msgstr ""
 
-#: kallithea/controllers/feed.py:111 kallithea/controllers/feed.py:140
+#: kallithea/controllers/feed.py:107
 #, python-format
 msgid "%s %s feed"
 msgstr ""
 
-#: kallithea/controllers/feed.py:113 kallithea/controllers/feed.py:142
+#: kallithea/controllers/feed.py:109
 #, python-format
 msgid "Changes on %s repository"
 msgstr ""
@@ -159,103 +159,103 @@
 msgid "%s at %s"
 msgstr ""
 
-#: kallithea/controllers/files.py:296
+#: kallithea/controllers/files.py:295
 msgid "You can only delete files with revision being a valid branch"
 msgstr ""
 
-#: kallithea/controllers/files.py:307
+#: kallithea/controllers/files.py:306
 #, python-format
 msgid "Deleted file %s via Kallithea"
 msgstr ""
 
-#: kallithea/controllers/files.py:331
+#: kallithea/controllers/files.py:330
 #, python-format
 msgid "Successfully deleted file %s"
 msgstr ""
 
-#: kallithea/controllers/files.py:335 kallithea/controllers/files.py:394
-#: kallithea/controllers/files.py:469
+#: kallithea/controllers/files.py:334 kallithea/controllers/files.py:392
+#: kallithea/controllers/files.py:467
 msgid "Error occurred during commit"
 msgstr ""
 
-#: kallithea/controllers/files.py:350
+#: kallithea/controllers/files.py:349
 msgid "You can only edit files with revision being a valid branch"
 msgstr ""
 
-#: kallithea/controllers/files.py:364
+#: kallithea/controllers/files.py:363
 #, python-format
 msgid "Edited file %s via Kallithea"
 msgstr ""
 
-#: kallithea/controllers/files.py:380
+#: kallithea/controllers/files.py:378
 msgid "No changes"
 msgstr ""
 
-#: kallithea/controllers/files.py:390 kallithea/controllers/files.py:458
+#: kallithea/controllers/files.py:388 kallithea/controllers/files.py:456
 #, python-format
 msgid "Successfully committed to %s"
 msgstr ""
 
-#: kallithea/controllers/files.py:409
+#: kallithea/controllers/files.py:407
 msgid "Added file via Kallithea"
 msgstr ""
 
-#: kallithea/controllers/files.py:430
+#: kallithea/controllers/files.py:428
 msgid "No content"
 msgstr ""
 
-#: kallithea/controllers/files.py:434
+#: kallithea/controllers/files.py:432
 msgid "No filename"
 msgstr ""
 
-#: kallithea/controllers/files.py:461
+#: kallithea/controllers/files.py:459
 msgid "Location must be relative path and must not contain .. in path"
 msgstr ""
 
-#: kallithea/controllers/files.py:493
+#: kallithea/controllers/files.py:491
 msgid "Downloads disabled"
 msgstr ""
 
+#: kallithea/controllers/files.py:502
+#, python-format
+msgid "Unknown revision %s"
+msgstr ""
+
 #: kallithea/controllers/files.py:504
-#, python-format
-msgid "Unknown revision %s"
+msgid "Empty repository"
 msgstr ""
 
 #: kallithea/controllers/files.py:506
-msgid "Empty repository"
-msgstr ""
-
-#: kallithea/controllers/files.py:508
 msgid "Unknown archive type"
 msgstr ""
 
-#: kallithea/controllers/files.py:729
+#: kallithea/controllers/files.py:726
 #: kallithea/templates/changeset/changeset_range.html:9
 #: kallithea/templates/email_templates/pull_request.html:64
 #: kallithea/templates/pullrequests/pullrequest.html:84
 msgid "Changesets"
 msgstr ""
 
-#: kallithea/controllers/files.py:730 kallithea/controllers/pullrequests.py:182
-#: kallithea/model/scm.py:676
+#: kallithea/controllers/files.py:727 kallithea/controllers/pullrequests.py:174
+#: kallithea/model/scm.py:663
 msgid "Branches"
 msgstr ""
 
-#: kallithea/controllers/files.py:731 kallithea/controllers/pullrequests.py:183
-#: kallithea/model/scm.py:687
+#: kallithea/controllers/files.py:728 kallithea/controllers/pullrequests.py:175
+#: kallithea/model/scm.py:674
 msgid "Tags"
 msgstr ""
 
-#: kallithea/controllers/forks.py:174
+#: kallithea/controllers/forks.py:175
 #, python-format
 msgid "An error occurred during repository forking %s"
 msgstr ""
 
-#: kallithea/controllers/home.py:79
+#: kallithea/controllers/home.py:77
 msgid "Groups"
 msgstr ""
 
-#: kallithea/controllers/home.py:89
+#: kallithea/controllers/home.py:87
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:90
 #: kallithea/templates/admin/repos/repo_add.html:12
 #: kallithea/templates/admin/repos/repo_add.html:16
@@ -266,48 +266,52 @@
 msgid "Repositories"
 msgstr ""
 
-#: kallithea/controllers/home.py:122
+#: kallithea/controllers/home.py:119
 #: kallithea/templates/files/files_add.html:32
 #: kallithea/templates/files/files_delete.html:23
 #: kallithea/templates/files/files_edit.html:32
 msgid "Branch"
 msgstr ""
 
-#: kallithea/controllers/home.py:128
+#: kallithea/controllers/home.py:125
 msgid "Closed Branches"
 msgstr ""
 
-#: kallithea/controllers/home.py:134
+#: kallithea/controllers/home.py:131
 msgid "Tag"
 msgstr ""
 
-#: kallithea/controllers/home.py:140
+#: kallithea/controllers/home.py:137
 msgid "Bookmark"
 msgstr ""
 
-#: kallithea/controllers/journal.py:112 kallithea/controllers/journal.py:154
+#: kallithea/controllers/journal.py:146 kallithea/controllers/journal.py:157
 #: kallithea/templates/journal/public_journal.html:4
 #: kallithea/templates/journal/public_journal.html:18
 msgid "Public Journal"
 msgstr ""
 
-#: kallithea/controllers/journal.py:116 kallithea/controllers/journal.py:158
+#: kallithea/controllers/journal.py:150 kallithea/controllers/journal.py:161
 #: kallithea/templates/base/base.html:290
 #: kallithea/templates/journal/journal.html:5
 #: kallithea/templates/journal/journal.html:13
 msgid "Journal"
 msgstr ""
 
-#: kallithea/controllers/login.py:140 kallithea/controllers/login.py:185
+#: kallithea/controllers/login.py:109
+msgid "Authentication failed."
+msgstr ""
+
+#: kallithea/controllers/login.py:142 kallithea/controllers/login.py:187
 msgid "Bad captcha"
 msgstr ""
 
-#: kallithea/controllers/login.py:146
+#: kallithea/controllers/login.py:148
 #, python-format
 msgid "You have successfully registered with %s"
 msgstr ""
 
-#: kallithea/controllers/login.py:190
+#: kallithea/controllers/login.py:192
 msgid "A password reset confirmation code has been sent"
 msgstr ""
 
@@ -320,225 +324,225 @@
 msgid "Successfully updated password"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:68
+#: kallithea/controllers/pullrequests.py:67
 #, python-format
 msgid "Invalid reviewer \"%s\" specified"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:131
+#: kallithea/controllers/pullrequests.py:123
 #, python-format
 msgid "%s (closed)"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:158
+#: kallithea/controllers/pullrequests.py:150
 #: kallithea/templates/changeset/changeset.html:12
 msgid "Changeset"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:179
+#: kallithea/controllers/pullrequests.py:171
 msgid "Special"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:180
+#: kallithea/controllers/pullrequests.py:172
 msgid "Peer branches"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:181 kallithea/model/scm.py:682
+#: kallithea/controllers/pullrequests.py:173 kallithea/model/scm.py:669
 msgid "Bookmarks"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:318
+#: kallithea/controllers/pullrequests.py:315
 #, python-format
 msgid "Error creating pull request: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:345
-#: kallithea/controllers/pullrequests.py:368
+#: kallithea/controllers/pullrequests.py:342
+#: kallithea/controllers/pullrequests.py:365
 msgid "Error occurred while creating pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:350
+#: kallithea/controllers/pullrequests.py:347
 msgid "Successfully opened new pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:373
+#: kallithea/controllers/pullrequests.py:370
 msgid "New pull request iteration created"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:401
+#: kallithea/controllers/pullrequests.py:398
 #, python-format
 msgid "Meanwhile, the following reviewers have been added: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:405
+#: kallithea/controllers/pullrequests.py:402
 #, python-format
 msgid "Meanwhile, the following reviewers have been removed: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:421
-#: kallithea/model/pull_request.py:232
+#: kallithea/controllers/pullrequests.py:418
+#: kallithea/model/pull_request.py:230
 msgid "No description"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:430
+#: kallithea/controllers/pullrequests.py:427
 msgid "Pull request updated"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:443
+#: kallithea/controllers/pullrequests.py:440
 msgid "Successfully deleted pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:479
+#: kallithea/controllers/pullrequests.py:476
 #, python-format
 msgid "Revision %s not found in %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:506
+#: kallithea/controllers/pullrequests.py:504
 #, python-format
 msgid "Error: changesets not found when displaying pull request from %s."
 msgstr ""
 
+#: kallithea/controllers/pullrequests.py:518
+#, python-format
+msgid "This pull request has already been merged to %s."
+msgstr ""
+
 #: kallithea/controllers/pullrequests.py:520
-#, python-format
-msgid "This pull request has already been merged to %s."
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:522
 msgid "This pull request has been closed and can not be updated."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:546
+#: kallithea/controllers/pullrequests.py:539
 #, python-format
 msgid "The following additional changes are available on %s:"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:548
-#: kallithea/controllers/pullrequests.py:552
+#: kallithea/controllers/pullrequests.py:541
+#: kallithea/controllers/pullrequests.py:545
 msgid "No additional changesets found for iterating on this pull request."
 msgstr ""
 
+#: kallithea/controllers/pullrequests.py:553
+#, python-format
+msgid "Note: Branch %s has another head: %s."
+msgstr ""
+
 #: kallithea/controllers/pullrequests.py:560
-#, python-format
-msgid "Note: Branch %s has another head: %s."
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:567
 msgid "Git pull requests don't support iterating yet."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:569
+#: kallithea/controllers/pullrequests.py:562
 #, python-format
 msgid "Error: some changesets not found when displaying pull request from %s."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:593
+#: kallithea/controllers/pullrequests.py:586
 msgid "The diff can't be shown - the PR revisions could not be found."
 msgstr ""
 
+#: kallithea/controllers/search.py:132
+msgid "Invalid search query. Try quoting it."
+msgstr ""
+
 #: kallithea/controllers/search.py:136
-msgid "Invalid search query. Try quoting it."
-msgstr ""
-
-#: kallithea/controllers/search.py:140
 msgid "The server has no search index."
 msgstr ""
 
-#: kallithea/controllers/search.py:143
+#: kallithea/controllers/search.py:139
 msgid "An error occurred during search operation."
 msgstr ""
 
-#: kallithea/controllers/summary.py:168
-#: kallithea/templates/summary/summary.html:412
+#: kallithea/controllers/summary.py:169
+#: kallithea/templates/summary/summary.html:410
 msgid "No data ready yet"
 msgstr ""
 
-#: kallithea/controllers/summary.py:171
+#: kallithea/controllers/summary.py:172
 #: kallithea/templates/summary/summary.html:97
 msgid "Statistics are disabled for this repository"
 msgstr ""
 
-#: kallithea/controllers/admin/auth_settings.py:137
+#: kallithea/controllers/admin/auth_settings.py:136
 msgid "Auth settings updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/auth_settings.py:148
+#: kallithea/controllers/admin/auth_settings.py:147
 msgid "error occurred during update of auth settings"
 msgstr ""
 
-#: kallithea/controllers/admin/defaults.py:75
+#: kallithea/controllers/admin/defaults.py:74
 msgid "Default settings updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/defaults.py:90
+#: kallithea/controllers/admin/defaults.py:89
 msgid "Error occurred during update of defaults"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:59
 #: kallithea/controllers/admin/my_account.py:232
-#: kallithea/controllers/admin/users.py:248
+#: kallithea/controllers/admin/users.py:246
 msgid "Forever"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:60
 #: kallithea/controllers/admin/my_account.py:233
-#: kallithea/controllers/admin/users.py:249
+#: kallithea/controllers/admin/users.py:247
 msgid "5 minutes"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:61
 #: kallithea/controllers/admin/my_account.py:234
-#: kallithea/controllers/admin/users.py:250
+#: kallithea/controllers/admin/users.py:248
 msgid "1 hour"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:62
 #: kallithea/controllers/admin/my_account.py:235
-#: kallithea/controllers/admin/users.py:251
+#: kallithea/controllers/admin/users.py:249
 msgid "1 day"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:63
 #: kallithea/controllers/admin/my_account.py:236
-#: kallithea/controllers/admin/users.py:252
+#: kallithea/controllers/admin/users.py:250
 msgid "1 month"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:67
 #: kallithea/controllers/admin/my_account.py:238
-#: kallithea/controllers/admin/users.py:254
+#: kallithea/controllers/admin/users.py:252
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:65
 #: kallithea/templates/admin/users/user_edit_api_keys.html:65
 msgid "Lifetime"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:142
+#: kallithea/controllers/admin/gists.py:148
 msgid "Error occurred during gist creation"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:158
+#: kallithea/controllers/admin/gists.py:164
 #, python-format
 msgid "Deleted gist %s"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:198
+#: kallithea/controllers/admin/gists.py:207
 msgid "Unmodified"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:228
+#: kallithea/controllers/admin/gists.py:237
 msgid "Successfully updated gist content"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:233
+#: kallithea/controllers/admin/gists.py:242
 msgid "Successfully updated gist data"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:236
+#: kallithea/controllers/admin/gists.py:245
 #, python-format
 msgid "Error occurred during update of gist %s"
 msgstr ""
 
-#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:211
-#: kallithea/model/user.py:232
+#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:205
+#: kallithea/model/user.py:226
 msgid "You can't edit this user since it's crucial for entire application"
 msgstr ""
 
@@ -547,7 +551,7 @@
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:134
-#: kallithea/controllers/admin/users.py:181
+#: kallithea/controllers/admin/users.py:179
 #, python-format
 msgid "Error occurred during update of user %s"
 msgstr ""
@@ -557,44 +561,44 @@
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:209
-#: kallithea/controllers/admin/users.py:367
+#: kallithea/controllers/admin/users.py:365
 #, python-format
 msgid "Added email %s to user"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:215
-#: kallithea/controllers/admin/users.py:373
+#: kallithea/controllers/admin/users.py:371
 msgid "An error occurred during email saving"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:224
-#: kallithea/controllers/admin/users.py:383
+#: kallithea/controllers/admin/users.py:381
 msgid "Removed email from user"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:248
-#: kallithea/controllers/admin/users.py:271
+#: kallithea/controllers/admin/users.py:269
 msgid "API key successfully created"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:257
-#: kallithea/controllers/admin/users.py:281
+#: kallithea/controllers/admin/users.py:279
 msgid "API key successfully reset"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:261
-#: kallithea/controllers/admin/users.py:285
+#: kallithea/controllers/admin/users.py:283
 msgid "API key successfully deleted"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:281
-#: kallithea/controllers/admin/users.py:456
+#: kallithea/controllers/admin/users.py:454
 #, python-format
 msgid "SSH key %s successfully added"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:293
-#: kallithea/controllers/admin/users.py:470
+#: kallithea/controllers/admin/users.py:468
 msgid "SSH key successfully deleted"
 msgstr ""
 
@@ -670,11 +674,11 @@
 msgid "Allowed with automatic account activation"
 msgstr ""
 
-#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1673
+#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1578
 msgid "Manual activation of external account"
 msgstr ""
 
-#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1674
+#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1579
 msgid "Automatic activation of external account"
 msgstr ""
 
@@ -696,339 +700,331 @@
 msgid "Error occurred during update of permissions"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:172
+#: kallithea/controllers/admin/repo_groups.py:165
 #, python-format
 msgid "Error occurred during creation of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:177
+#: kallithea/controllers/admin/repo_groups.py:172
 #, python-format
 msgid "Created repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:224
+#: kallithea/controllers/admin/repo_groups.py:219
 #, python-format
 msgid "Updated repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:240
+#: kallithea/controllers/admin/repo_groups.py:235
 #, python-format
 msgid "Error occurred during update of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:250
+#: kallithea/controllers/admin/repo_groups.py:245
 #, python-format
 msgid "This group contains %s repositories and cannot be deleted"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:257
+#: kallithea/controllers/admin/repo_groups.py:252
 #, python-format
 msgid "This group contains %s subgroups and cannot be deleted"
 msgstr ""
 
+#: kallithea/controllers/admin/repo_groups.py:258
+#, python-format
+msgid "Removed repository group %s"
+msgstr ""
+
 #: kallithea/controllers/admin/repo_groups.py:263
 #, python-format
-msgid "Removed repository group %s"
-msgstr ""
-
-#: kallithea/controllers/admin/repo_groups.py:268
-#, python-format
 msgid "Error occurred during deletion of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:352
-#: kallithea/controllers/admin/repo_groups.py:382
-#: kallithea/controllers/admin/user_groups.py:294
+#: kallithea/controllers/admin/repo_groups.py:347
+#: kallithea/controllers/admin/repo_groups.py:377
+#: kallithea/controllers/admin/user_groups.py:290
 msgid "Cannot revoke permission for yourself as admin"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:367
+#: kallithea/controllers/admin/repo_groups.py:362
 msgid "Repository group permissions updated"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:399
-#: kallithea/controllers/admin/repos.py:358
-#: kallithea/controllers/admin/user_groups.py:306
+#: kallithea/controllers/admin/repo_groups.py:394
+#: kallithea/controllers/admin/repos.py:357
+#: kallithea/controllers/admin/user_groups.py:302
 msgid "An error occurred during revoking of permission"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:136
+#: kallithea/controllers/admin/repos.py:137
 #, python-format
 msgid "Error creating repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:194
+#: kallithea/controllers/admin/repos.py:193
 #, python-format
 msgid "Created repository %s from %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:203
+#: kallithea/controllers/admin/repos.py:202
 #, python-format
 msgid "Forked repository %s as %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:206
+#: kallithea/controllers/admin/repos.py:205
 #, python-format
 msgid "Created repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:235
+#: kallithea/controllers/admin/repos.py:234
 #, python-format
 msgid "Repository %s updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:255
+#: kallithea/controllers/admin/repos.py:254
 #, python-format
 msgid "Error occurred during update of repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:273
+#: kallithea/controllers/admin/repos.py:272
 #, python-format
 msgid "Detached %s forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:276
+#: kallithea/controllers/admin/repos.py:275
 #, python-format
 msgid "Deleted %s forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:281
+#: kallithea/controllers/admin/repos.py:280
 #, python-format
 msgid "Deleted repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:284
+#: kallithea/controllers/admin/repos.py:283
 #, python-format
 msgid "Cannot delete repository %s which still has forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:289
+#: kallithea/controllers/admin/repos.py:288
 #, python-format
 msgid "An error occurred during deletion of %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:329
+#: kallithea/controllers/admin/repos.py:328
 msgid "Repository permissions updated"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:388
+#: kallithea/controllers/admin/repos.py:387
 #, python-format
 msgid "Field validation error: %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:391
+#: kallithea/controllers/admin/repos.py:390
 #, python-format
 msgid "An error occurred during creation of field: %r"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:402
+#: kallithea/controllers/admin/repos.py:401
 msgid "An error occurred during removal of field"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:416
+#: kallithea/controllers/admin/repos.py:415
 msgid "-- Not a fork --"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:448
+#: kallithea/controllers/admin/repos.py:447
 msgid "Updated repository visibility in public journal"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:452
+#: kallithea/controllers/admin/repos.py:451
 msgid "An error occurred during setting this repository in public journal"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:468
+#: kallithea/controllers/admin/repos.py:467
 msgid "Nothing"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:470
+#: kallithea/controllers/admin/repos.py:469
 #, python-format
 msgid "Marked repository %s as fork of %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:477
+#: kallithea/controllers/admin/repos.py:476
 msgid "An error occurred during this operation"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:490
-msgid "Cache invalidation successful"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:494
-msgid "An error occurred during cache invalidation"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:507
+#: kallithea/controllers/admin/repos.py:488
 msgid "Pulled from remote location"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:510
+#: kallithea/controllers/admin/repos.py:491
 msgid "An error occurred during pull from remote location"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:541
+#: kallithea/controllers/admin/repos.py:522
 msgid "An error occurred during deletion of repository stats"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:131
+#: kallithea/controllers/admin/settings.py:132
 msgid "Updated VCS settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:135 kallithea/lib/utils.py:237
+#: kallithea/controllers/admin/settings.py:136
 msgid ""
 "Unable to activate hgsubversion support. The \"hgsubversion\" library is "
 "missing"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:141
-#: kallithea/controllers/admin/settings.py:233
+#: kallithea/controllers/admin/settings.py:142
+#: kallithea/controllers/admin/settings.py:234
 msgid "Error occurred while updating application settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:176
+#: kallithea/controllers/admin/settings.py:177
 #, python-format
 msgid "Repositories successfully rescanned. Added: %s. Removed: %s."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:188
+#: kallithea/controllers/admin/settings.py:189
 #, python-format
 msgid "Invalidated %s repositories"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:229
+#: kallithea/controllers/admin/settings.py:230
 msgid "Updated application settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:283
+#: kallithea/controllers/admin/settings.py:284
 msgid "Updated visualisation settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:288
+#: kallithea/controllers/admin/settings.py:289
 msgid "Error occurred during updating visualisation settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:312
+#: kallithea/controllers/admin/settings.py:313
 msgid "Please enter email address"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:327
+#: kallithea/controllers/admin/settings.py:328
 msgid "Send email task created"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:355
+#: kallithea/controllers/admin/settings.py:356
 msgid "Hook already exists"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:357
+#: kallithea/controllers/admin/settings.py:358
 msgid "Builtin hooks are read-only. Please use another hook name."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:360
+#: kallithea/controllers/admin/settings.py:361
 msgid "Added new hook"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:376
+#: kallithea/controllers/admin/settings.py:377
 msgid "Updated hooks"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:380
+#: kallithea/controllers/admin/settings.py:381
 msgid "Error occurred during hook creation"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:404
+#: kallithea/controllers/admin/settings.py:405
 msgid "Whoosh reindex task scheduled"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:138
+#: kallithea/controllers/admin/user_groups.py:134
 #, python-format
 msgid "Created user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:151
+#: kallithea/controllers/admin/user_groups.py:147
 #, python-format
 msgid "Error occurred during creation of user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:179
+#: kallithea/controllers/admin/user_groups.py:175
 #, python-format
 msgid "Updated user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:201
+#: kallithea/controllers/admin/user_groups.py:197
 #, python-format
 msgid "Error occurred during update of user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:212
+#: kallithea/controllers/admin/user_groups.py:208
 msgid "Successfully deleted user group"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:217
+#: kallithea/controllers/admin/user_groups.py:213
 msgid "An error occurred during deletion of user group"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:273
+#: kallithea/controllers/admin/user_groups.py:269
 msgid "Target group cannot be the same"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:279
+#: kallithea/controllers/admin/user_groups.py:275
 msgid "User group permissions updated"
 msgstr ""
 
+#: kallithea/controllers/admin/user_groups.py:384
+#: kallithea/controllers/admin/users.py:336
+msgid "Updated permissions"
+msgstr ""
+
 #: kallithea/controllers/admin/user_groups.py:388
-#: kallithea/controllers/admin/users.py:338
-msgid "Updated permissions"
-msgstr ""
-
-#: kallithea/controllers/admin/user_groups.py:392
-#: kallithea/controllers/admin/users.py:342
+#: kallithea/controllers/admin/users.py:340
 msgid "An error occurred during permissions saving"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:123
+#: kallithea/controllers/admin/users.py:121
 #, python-format
 msgid "Created user %s"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:138
+#: kallithea/controllers/admin/users.py:136
 #, python-format
 msgid "Error occurred during creation of user %s"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:162
+#: kallithea/controllers/admin/users.py:160
 msgid "User updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:190
+#: kallithea/controllers/admin/users.py:188
 msgid "Successfully deleted user"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:195
+#: kallithea/controllers/admin/users.py:193
 msgid "An error occurred during deletion of user"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:203
+#: kallithea/controllers/admin/users.py:201
 msgid "The default user cannot be edited"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:409
+#: kallithea/controllers/admin/users.py:407
 #, python-format
 msgid "Added IP address %s to user whitelist"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:415
+#: kallithea/controllers/admin/users.py:413
 msgid "An error occurred while adding IP address"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:427
+#: kallithea/controllers/admin/users.py:425
 msgid "Removed IP address from user whitelist"
 msgstr ""
 
-#: kallithea/lib/auth.py:684
+#: kallithea/lib/auth.py:634
 msgid "You need to be a registered user to perform this action"
 msgstr ""
 
-#: kallithea/lib/auth.py:712
+#: kallithea/lib/auth.py:662
 msgid "You need to be signed in to view this page"
 msgstr ""
 
@@ -1057,169 +1053,169 @@
 msgid "Changeset was too big and was cut off, use diff menu to display this diff"
 msgstr ""
 
-#: kallithea/lib/diffs.py:224
+#: kallithea/lib/diffs.py:223
 msgid "No changes detected"
 msgstr ""
 
-#: kallithea/lib/helpers.py:653
+#: kallithea/lib/helpers.py:670
 #, python-format
 msgid "Deleted branch: %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:655
+#: kallithea/lib/helpers.py:672
 #, python-format
 msgid "Created tag: %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:666
+#: kallithea/lib/helpers.py:683
 #, python-format
 msgid "Changeset %s not found"
 msgstr ""
 
-#: kallithea/lib/helpers.py:715
+#: kallithea/lib/helpers.py:732
 #, python-format
 msgid "Show all combined changesets %s->%s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:721
+#: kallithea/lib/helpers.py:738
 msgid "Compare view"
 msgstr ""
 
-#: kallithea/lib/helpers.py:740
+#: kallithea/lib/helpers.py:757
 msgid "and"
 msgstr ""
 
-#: kallithea/lib/helpers.py:741
+#: kallithea/lib/helpers.py:758
 #, python-format
 msgid "%s more"
 msgstr ""
 
-#: kallithea/lib/helpers.py:742 kallithea/templates/changelog/changelog.html:43
+#: kallithea/lib/helpers.py:759 kallithea/templates/changelog/changelog.html:43
 msgid "revisions"
 msgstr ""
 
-#: kallithea/lib/helpers.py:766
+#: kallithea/lib/helpers.py:783
 #, python-format
 msgid "Fork name %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:787
+#: kallithea/lib/helpers.py:804
 #, python-format
 msgid "Pull request %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:797
+#: kallithea/lib/helpers.py:814
 msgid "[deleted] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:799 kallithea/lib/helpers.py:811
+#: kallithea/lib/helpers.py:816 kallithea/lib/helpers.py:828
 msgid "[created] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:801
+#: kallithea/lib/helpers.py:818
 msgid "[created] repository as fork"
 msgstr ""
 
-#: kallithea/lib/helpers.py:803 kallithea/lib/helpers.py:813
+#: kallithea/lib/helpers.py:820 kallithea/lib/helpers.py:830
 msgid "[forked] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:805 kallithea/lib/helpers.py:815
+#: kallithea/lib/helpers.py:822 kallithea/lib/helpers.py:832
 msgid "[updated] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:807
+#: kallithea/lib/helpers.py:824
 msgid "[downloaded] archive from repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:809
+#: kallithea/lib/helpers.py:826
 msgid "[delete] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:817
+#: kallithea/lib/helpers.py:834
 msgid "[created] user"
 msgstr ""
 
-#: kallithea/lib/helpers.py:819
+#: kallithea/lib/helpers.py:836
 msgid "[updated] user"
 msgstr ""
 
-#: kallithea/lib/helpers.py:821
+#: kallithea/lib/helpers.py:838
 msgid "[created] user group"
 msgstr ""
 
-#: kallithea/lib/helpers.py:823
+#: kallithea/lib/helpers.py:840
 msgid "[updated] user group"
 msgstr ""
 
-#: kallithea/lib/helpers.py:825
+#: kallithea/lib/helpers.py:842
 msgid "[commented] on revision in repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:827
+#: kallithea/lib/helpers.py:844
 msgid "[commented] on pull request for"
 msgstr ""
 
-#: kallithea/lib/helpers.py:829
+#: kallithea/lib/helpers.py:846
 msgid "[closed] pull request for"
 msgstr ""
 
-#: kallithea/lib/helpers.py:831
+#: kallithea/lib/helpers.py:848
 msgid "[pushed] into"
 msgstr ""
 
-#: kallithea/lib/helpers.py:833
+#: kallithea/lib/helpers.py:850
 msgid "[committed via Kallithea] into repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:835
+#: kallithea/lib/helpers.py:852
 msgid "[pulled from remote] into repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:837
+#: kallithea/lib/helpers.py:854
 msgid "[pulled] from"
 msgstr ""
 
-#: kallithea/lib/helpers.py:839
+#: kallithea/lib/helpers.py:856
 msgid "[started following] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:841
+#: kallithea/lib/helpers.py:858
 msgid "[stopped following] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:961
+#: kallithea/lib/helpers.py:975
 #, python-format
 msgid " and %s more"
 msgstr ""
 
-#: kallithea/lib/helpers.py:965
+#: kallithea/lib/helpers.py:979
 #: kallithea/templates/compare/compare_diff.html:69
 #: kallithea/templates/pullrequests/pullrequest_show.html:297
 msgid "No files"
 msgstr ""
 
-#: kallithea/lib/helpers.py:990
+#: kallithea/lib/helpers.py:1004
 msgid "new file"
 msgstr ""
 
-#: kallithea/lib/helpers.py:993
+#: kallithea/lib/helpers.py:1007
 msgid "mod"
 msgstr ""
 
-#: kallithea/lib/helpers.py:996
+#: kallithea/lib/helpers.py:1010
 msgid "del"
 msgstr ""
 
-#: kallithea/lib/helpers.py:999
+#: kallithea/lib/helpers.py:1013
 msgid "rename"
 msgstr ""
 
-#: kallithea/lib/helpers.py:1004
+#: kallithea/lib/helpers.py:1018
 msgid "chmod"
 msgstr ""
 
-#: kallithea/lib/helpers.py:1297
+#: kallithea/lib/helpers.py:1323
 #, python-format
 msgid ""
 "%s repository is not mapped to db perhaps it was created or renamed from "
@@ -1227,96 +1223,98 @@
 "repositories"
 msgstr ""
 
-#: kallithea/lib/ssh.py:73
+#: kallithea/lib/ssh.py:75
 msgid "SSH key is missing"
 msgstr ""
 
-#: kallithea/lib/ssh.py:77
-msgid "Incorrect SSH key - it must have both a key type and a base64 part"
-msgstr ""
-
-#: kallithea/lib/ssh.py:81
+#: kallithea/lib/ssh.py:79
+msgid ""
+"Incorrect SSH key - it must have both a key type and a base64 part, like "
+"'ssh-rsa ASRNeaZu4FA...xlJp='"
+msgstr ""
+
+#: kallithea/lib/ssh.py:83
 msgid "Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'"
 msgstr ""
 
-#: kallithea/lib/ssh.py:84
+#: kallithea/lib/ssh.py:86
 #, python-format
 msgid "Incorrect SSH key - unexpected characters in base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:89
+#: kallithea/lib/ssh.py:91
 #, python-format
 msgid "Incorrect SSH key - failed to decode base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:92
+#: kallithea/lib/ssh.py:94
 #, python-format
 msgid "Incorrect SSH key - base64 part is not %r as claimed but %r"
 msgstr ""
 
-#: kallithea/lib/utils2.py:334
+#: kallithea/lib/utils2.py:253
 #, python-format
 msgid "%d year"
 msgid_plural "%d years"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:335
+#: kallithea/lib/utils2.py:254
 #, python-format
 msgid "%d month"
 msgid_plural "%d months"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:336
+#: kallithea/lib/utils2.py:255
 #, python-format
 msgid "%d day"
 msgid_plural "%d days"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:337
+#: kallithea/lib/utils2.py:256
 #, python-format
 msgid "%d hour"
 msgid_plural "%d hours"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:338
+#: kallithea/lib/utils2.py:257
 #, python-format
 msgid "%d minute"
 msgid_plural "%d minutes"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:339
+#: kallithea/lib/utils2.py:258
 #, python-format
 msgid "%d second"
 msgid_plural "%d seconds"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:355
+#: kallithea/lib/utils2.py:274
 #, python-format
 msgid "in %s"
 msgstr ""
 
-#: kallithea/lib/utils2.py:357
+#: kallithea/lib/utils2.py:276
 #, python-format
 msgid "%s ago"
 msgstr ""
 
-#: kallithea/lib/utils2.py:359
+#: kallithea/lib/utils2.py:278
 #, python-format
 msgid "in %s and %s"
 msgstr ""
 
-#: kallithea/lib/utils2.py:362
+#: kallithea/lib/utils2.py:281
 #, python-format
 msgid "%s and %s ago"
 msgstr ""
 
-#: kallithea/lib/utils2.py:365
+#: kallithea/lib/utils2.py:284
 msgid "just now"
 msgstr ""
 
@@ -1325,131 +1323,131 @@
 msgid "on line %s"
 msgstr ""
 
-#: kallithea/model/comment.py:221 kallithea/model/pull_request.py:114
+#: kallithea/model/comment.py:219 kallithea/model/pull_request.py:112
 msgid "[Mention]"
 msgstr ""
 
-#: kallithea/model/db.py:1496
+#: kallithea/model/db.py:1411
 msgid "top level"
 msgstr ""
 
-#: kallithea/model/db.py:1637
+#: kallithea/model/db.py:1542
 msgid "Kallithea Administrator"
 msgstr ""
 
-#: kallithea/model/db.py:1639
+#: kallithea/model/db.py:1544
 msgid "Default user has no access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1640
+#: kallithea/model/db.py:1545
 msgid "Default user has read access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1641
+#: kallithea/model/db.py:1546
 msgid "Default user has write access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1642
+#: kallithea/model/db.py:1547
 msgid "Default user has admin access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1644
+#: kallithea/model/db.py:1549
 msgid "Default user has no access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1645
+#: kallithea/model/db.py:1550
 msgid "Default user has read access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1646
+#: kallithea/model/db.py:1551
 msgid "Default user has write access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1647
+#: kallithea/model/db.py:1552
 msgid "Default user has admin access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1649
+#: kallithea/model/db.py:1554
 msgid "Default user has no access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1650
+#: kallithea/model/db.py:1555
 msgid "Default user has read access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1651
+#: kallithea/model/db.py:1556
 msgid "Default user has write access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1652
+#: kallithea/model/db.py:1557
 msgid "Default user has admin access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1654
+#: kallithea/model/db.py:1559
 msgid "Only admins can create repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1655
+#: kallithea/model/db.py:1560
 msgid "Non-admins can create repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1657
+#: kallithea/model/db.py:1562
 msgid "Only admins can create user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1658
+#: kallithea/model/db.py:1563
 msgid "Non-admins can create user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1660
+#: kallithea/model/db.py:1565
 msgid "Only admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1661
+#: kallithea/model/db.py:1566
 msgid "Non-admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1663
+#: kallithea/model/db.py:1568
 msgid "Repository creation enabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1664
+#: kallithea/model/db.py:1569
 msgid "Repository creation disabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1666
+#: kallithea/model/db.py:1571
 msgid "Only admins can fork repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1667
+#: kallithea/model/db.py:1572
 msgid "Non-admins can fork repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1669
+#: kallithea/model/db.py:1574
 msgid "Registration disabled"
 msgstr ""
 
-#: kallithea/model/db.py:1670
+#: kallithea/model/db.py:1575
 msgid "User registration with manual account activation"
 msgstr ""
 
-#: kallithea/model/db.py:1671
+#: kallithea/model/db.py:1576
 msgid "User registration with automatic account activation"
 msgstr ""
 
-#: kallithea/model/db.py:2206
+#: kallithea/model/db.py:1992
 msgid "Not reviewed"
 msgstr ""
 
-#: kallithea/model/db.py:2207
+#: kallithea/model/db.py:1993
 msgid "Under review"
 msgstr ""
 
-#: kallithea/model/db.py:2208
+#: kallithea/model/db.py:1994
 msgid "Not approved"
 msgstr ""
 
-#: kallithea/model/db.py:2209
+#: kallithea/model/db.py:1995
 msgid "Approved"
 msgstr ""
 
@@ -1475,310 +1473,310 @@
 msgid "Name must not contain only digits"
 msgstr ""
 
-#: kallithea/model/notification.py:164
+#: kallithea/model/notification.py:162
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s changeset %(short_id)s \"%(message_short)s\" on "
 "%(branch)s"
 msgstr ""
 
-#: kallithea/model/notification.py:167
+#: kallithea/model/notification.py:165
 #, python-format
 msgid "New user %(new_username)s registered"
 msgstr ""
 
-#: kallithea/model/notification.py:169
+#: kallithea/model/notification.py:167
 #, python-format
 msgid ""
 "[Review] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:170
+#: kallithea/model/notification.py:168
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:183
+#: kallithea/model/notification.py:188
 msgid "Closing"
 msgstr ""
 
-#: kallithea/model/pull_request.py:73
+#: kallithea/model/pull_request.py:72
 #, python-format
 msgid "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:209
+#: kallithea/model/pull_request.py:207
 msgid "Cannot create empty pull request"
 msgstr ""
 
-#: kallithea/model/pull_request.py:217
+#: kallithea/model/pull_request.py:215
 #, python-format
 msgid ""
 "Cannot create pull request - criss cross merge detected, please merge a "
 "later %s revision to %s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:245 kallithea/model/pull_request.py:380
+#: kallithea/model/pull_request.py:243 kallithea/model/pull_request.py:378
 msgid "You are not authorized to create the pull request"
 msgstr ""
 
-#: kallithea/model/pull_request.py:339
+#: kallithea/model/pull_request.py:337
 msgid "Missing changesets since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:346
+#: kallithea/model/pull_request.py:344
 #, python-format
 msgid "New changesets on %s %s since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:353
+#: kallithea/model/pull_request.py:351
 msgid "Ancestor didn't change - diff since previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:360
+#: kallithea/model/pull_request.py:358
 #, python-format
 msgid ""
 "This iteration is based on another %s revision and there is no simple "
 "diff."
 msgstr ""
 
-#: kallithea/model/pull_request.py:362
+#: kallithea/model/pull_request.py:360
 #, python-format
 msgid "No changes found on %s %s since previous iteration."
 msgstr ""
 
-#: kallithea/model/pull_request.py:388
+#: kallithea/model/pull_request.py:386
 #, python-format
 msgid "Closed, next iteration: %s ."
 msgstr ""
 
-#: kallithea/model/scm.py:668
+#: kallithea/model/scm.py:655
 msgid "latest tip"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:56
+#: kallithea/model/ssh_key.py:57
 #, python-format
 msgid "SSH key %r is invalid: %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:68
+#: kallithea/model/ssh_key.py:69
 #, python-format
 msgid "SSH key %s is already used by %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:89
-#, python-format
-msgid "SSH key %r not found"
-msgstr ""
-
-#: kallithea/model/user.py:186
+#: kallithea/model/ssh_key.py:88
+#, python-format
+msgid "SSH key with fingerprint %r found"
+msgstr ""
+
+#: kallithea/model/user.py:180
 msgid "New user registration"
 msgstr ""
 
-#: kallithea/model/user.py:250
+#: kallithea/model/user.py:244
 msgid "You can't remove this user since it is crucial for the entire application"
 msgstr ""
 
-#: kallithea/model/user.py:255
+#: kallithea/model/user.py:249
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repositories and cannot be removed. Switch "
 "owners or remove those repositories: %s"
 msgstr ""
 
-#: kallithea/model/user.py:260
+#: kallithea/model/user.py:254
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repository groups and cannot be removed. Switch"
 " owners or remove those repository groups: %s"
 msgstr ""
 
-#: kallithea/model/user.py:267
+#: kallithea/model/user.py:261
 #, python-format
 msgid ""
 "User \"%s\" still owns %s user groups and cannot be removed. Switch "
 "owners or remove those user groups: %s"
 msgstr ""
 
-#: kallithea/model/user.py:361
+#: kallithea/model/user.py:355
 msgid "Password reset link"
 msgstr ""
 
-#: kallithea/model/user.py:408
+#: kallithea/model/user.py:402
 msgid "Password reset notification"
 msgstr ""
 
-#: kallithea/model/user.py:409
+#: kallithea/model/user.py:403
 #, python-format
 msgid ""
 "The password to your account %s has been changed using password reset "
 "form."
 msgstr ""
 
-#: kallithea/model/validators.py:52 kallithea/model/validators.py:53
+#: kallithea/model/validators.py:53 kallithea/model/validators.py:54
 msgid "Value cannot be an empty list"
 msgstr ""
 
-#: kallithea/model/validators.py:72
+#: kallithea/model/validators.py:73
 #, python-format
 msgid "Username \"%(username)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:74
+#: kallithea/model/validators.py:75
 #, python-format
 msgid "Username \"%(username)s\" cannot be used"
 msgstr ""
 
-#: kallithea/model/validators.py:76
+#: kallithea/model/validators.py:77
 msgid ""
 "Username may only contain alphanumeric characters underscores, periods or"
 " dashes and must begin with an alphanumeric character or underscore"
 msgstr ""
 
-#: kallithea/model/validators.py:103
+#: kallithea/model/validators.py:104
 msgid "The input is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:110
+#: kallithea/model/validators.py:111
 #, python-format
 msgid "Username %(username)s is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:131
-msgid "Invalid user group name"
-msgstr ""
-
 #: kallithea/model/validators.py:132
+msgid "Invalid user group name"
+msgstr ""
+
+#: kallithea/model/validators.py:133
 #, python-format
 msgid "User group \"%(usergroup)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:134
+#: kallithea/model/validators.py:135
 msgid ""
 "user group name may only contain alphanumeric characters underscores, "
 "periods or dashes and must begin with alphanumeric character"
 msgstr ""
 
-#: kallithea/model/validators.py:174
-msgid "Cannot assign this group as parent"
-msgstr ""
-
 #: kallithea/model/validators.py:175
+msgid "Cannot assign this group as parent"
+msgstr ""
+
+#: kallithea/model/validators.py:176
 #, python-format
 msgid "Group \"%(group_name)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:177
+#: kallithea/model/validators.py:178
 #, python-format
 msgid "Repository with name \"%(group_name)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:233
+#: kallithea/model/validators.py:230
 msgid "Invalid characters (non-ascii) in password"
 msgstr ""
 
-#: kallithea/model/validators.py:248
+#: kallithea/model/validators.py:245
 msgid "Invalid old password"
 msgstr ""
 
-#: kallithea/model/validators.py:264
+#: kallithea/model/validators.py:261
 msgid "Passwords do not match"
 msgstr ""
 
-#: kallithea/model/validators.py:279
+#: kallithea/model/validators.py:276
 msgid "Invalid username or password"
 msgstr ""
 
+#: kallithea/model/validators.py:310
+#, python-format
+msgid "Repository name %(repo)s is not allowed"
+msgstr ""
+
+#: kallithea/model/validators.py:312
+#, python-format
+msgid "Repository named %(repo)s already exists"
+msgstr ""
+
 #: kallithea/model/validators.py:313
 #, python-format
-msgid "Repository name %(repo)s is not allowed"
+msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
 msgstr ""
 
 #: kallithea/model/validators.py:315
 #, python-format
-msgid "Repository named %(repo)s already exists"
-msgstr ""
-
-#: kallithea/model/validators.py:316
-#, python-format
-msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
-msgstr ""
-
-#: kallithea/model/validators.py:318
-#, python-format
 msgid "Repository group with name \"%(repo)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:404
+#: kallithea/model/validators.py:401
 msgid "Invalid repository URL"
 msgstr ""
 
-#: kallithea/model/validators.py:405
+#: kallithea/model/validators.py:402
 msgid ""
 "Invalid repository URL. It must be a valid http, https, ssh, svn+http or "
 "svn+https URL"
 msgstr ""
 
-#: kallithea/model/validators.py:430
+#: kallithea/model/validators.py:427
 msgid "Fork has to be the same type as parent"
 msgstr ""
 
-#: kallithea/model/validators.py:445
+#: kallithea/model/validators.py:442
 msgid "You don't have permissions to create repository in this group"
 msgstr ""
 
-#: kallithea/model/validators.py:447
+#: kallithea/model/validators.py:444
 msgid "no permission to create repository in root location"
 msgstr ""
 
-#: kallithea/model/validators.py:497
+#: kallithea/model/validators.py:494
 msgid "You don't have permissions to create a group in this location"
 msgstr ""
 
-#: kallithea/model/validators.py:537
+#: kallithea/model/validators.py:534
 msgid "This username or user group name is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:630
+#: kallithea/model/validators.py:627
 msgid "This is not a valid path"
 msgstr ""
 
-#: kallithea/model/validators.py:647
+#: kallithea/model/validators.py:644
 msgid "This email address is already in use"
 msgstr ""
 
-#: kallithea/model/validators.py:667
+#: kallithea/model/validators.py:664
 #, python-format
 msgid "Email address \"%(email)s\" not found"
 msgstr ""
 
-#: kallithea/model/validators.py:704
+#: kallithea/model/validators.py:701
 msgid ""
 "The LDAP Login attribute of the CN must be specified - this is the name "
 "of the attribute that is equivalent to \"username\""
 msgstr ""
 
-#: kallithea/model/validators.py:716
+#: kallithea/model/validators.py:713
 msgid "Please enter a valid IPv4 or IPv6 address"
 msgstr ""
 
-#: kallithea/model/validators.py:717
+#: kallithea/model/validators.py:714
 #, python-format
 msgid "The network size (bits) must be within the range of 0-32 (not %(bits)r)"
 msgstr ""
 
-#: kallithea/model/validators.py:750
+#: kallithea/model/validators.py:747
 msgid "Key name can only consist of letters, underscore, dash or numbers"
 msgstr ""
 
-#: kallithea/model/validators.py:764
+#: kallithea/model/validators.py:761
 msgid "Filename cannot be inside a directory"
 msgstr ""
 
-#: kallithea/model/validators.py:780
+#: kallithea/model/validators.py:777
 #, python-format
 msgid "Plugins %(loaded)s and %(next_to_load)s both export the same name"
 msgstr ""
@@ -1838,7 +1836,7 @@
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:60
 #: kallithea/templates/email_templates/pull_request.html:37
 #: kallithea/templates/forks/fork.html:34
-#: kallithea/templates/index_base.html:58
+#: kallithea/templates/index_base.html:59
 #: kallithea/templates/pullrequests/pullrequest.html:33
 #: kallithea/templates/pullrequests/pullrequest_show.html:38
 #: kallithea/templates/pullrequests/pullrequest_show.html:59
@@ -1846,14 +1844,14 @@
 msgid "Description"
 msgstr ""
 
-#: kallithea/templates/index_base.html:60
+#: kallithea/templates/index_base.html:61
 msgid "Last Change"
 msgstr ""
 
 #: kallithea/templates/admin/my_account/my_account_repos.html:15
 #: kallithea/templates/admin/my_account/my_account_watched.html:15
 #: kallithea/templates/admin/repos/repos.html:41
-#: kallithea/templates/index_base.html:62
+#: kallithea/templates/index_base.html:63
 msgid "Tip"
 msgstr ""
 
@@ -1863,7 +1861,7 @@
 #: kallithea/templates/admin/repos/repos.html:42
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:8
 #: kallithea/templates/admin/user_groups/user_groups.html:42
-#: kallithea/templates/index_base.html:63
+#: kallithea/templates/index_base.html:64
 #: kallithea/templates/pullrequests/pullrequest_data.html:16
 #: kallithea/templates/pullrequests/pullrequest_show.html:124
 #: kallithea/templates/pullrequests/pullrequest_show.html:219
@@ -1887,7 +1885,7 @@
 #: kallithea/templates/admin/users/user_edit_profile.html:18
 #: kallithea/templates/admin/users/users.html:37
 #: kallithea/templates/base/base.html:364
-#: kallithea/templates/email_templates/registration.html:11
+#: kallithea/templates/email_templates/registration.html:12
 #: kallithea/templates/login.html:28 kallithea/templates/register.html:31
 msgid "Username"
 msgstr ""
@@ -2011,7 +2009,7 @@
 #: kallithea/templates/admin/settings/settings.html:31
 #: kallithea/templates/admin/users/user_add.html:62
 #: kallithea/templates/admin/users/user_edit_profile.html:25
-#: kallithea/templates/email_templates/registration.html:33
+#: kallithea/templates/email_templates/registration.html:34
 #: kallithea/templates/register.html:66
 msgid "Email"
 msgstr ""
@@ -2258,7 +2256,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/gists/index.html:51
-#: kallithea/templates/data_table/_dt_elements.html:78
+#: kallithea/templates/data_table/_dt_elements.html:84
 msgid "Created"
 msgstr ""
 
@@ -2342,13 +2340,13 @@
 #: kallithea/templates/admin/users/user_edit_ips.html:21
 #: kallithea/templates/changeset/changeset_file_comment.html:30
 #: kallithea/templates/changeset/changeset_file_comment.html:121
-#: kallithea/templates/data_table/_dt_elements.html:69
-#: kallithea/templates/data_table/_dt_elements.html:89
-#: kallithea/templates/data_table/_dt_elements.html:91
-#: kallithea/templates/data_table/_dt_elements.html:101
-#: kallithea/templates/data_table/_dt_elements.html:103
-#: kallithea/templates/data_table/_dt_elements.html:120
-#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:75
+#: kallithea/templates/data_table/_dt_elements.html:95
+#: kallithea/templates/data_table/_dt_elements.html:97
+#: kallithea/templates/data_table/_dt_elements.html:107
+#: kallithea/templates/data_table/_dt_elements.html:109
+#: kallithea/templates/data_table/_dt_elements.html:126
+#: kallithea/templates/data_table/_dt_elements.html:128
 #: kallithea/templates/files/files_source.html:35
 #: kallithea/templates/files/files_source.html:38
 #: kallithea/templates/files/files_source.html:41
@@ -2364,14 +2362,14 @@
 #: kallithea/templates/base/perms_summary.html:44
 #: kallithea/templates/base/perms_summary.html:81
 #: kallithea/templates/base/perms_summary.html:83
-#: kallithea/templates/data_table/_dt_elements.html:63
-#: kallithea/templates/data_table/_dt_elements.html:64
-#: kallithea/templates/data_table/_dt_elements.html:85
-#: kallithea/templates/data_table/_dt_elements.html:86
-#: kallithea/templates/data_table/_dt_elements.html:97
-#: kallithea/templates/data_table/_dt_elements.html:98
-#: kallithea/templates/data_table/_dt_elements.html:116
-#: kallithea/templates/data_table/_dt_elements.html:117
+#: kallithea/templates/data_table/_dt_elements.html:69
+#: kallithea/templates/data_table/_dt_elements.html:70
+#: kallithea/templates/data_table/_dt_elements.html:91
+#: kallithea/templates/data_table/_dt_elements.html:92
+#: kallithea/templates/data_table/_dt_elements.html:103
+#: kallithea/templates/data_table/_dt_elements.html:104
+#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:123
 #: kallithea/templates/files/diff_2way.html:56
 #: kallithea/templates/files/files_source.html:37
 #: kallithea/templates/files/files_source.html:40
@@ -2671,7 +2669,7 @@
 #: kallithea/templates/admin/permissions/permissions_globals.html:27
 #: kallithea/templates/admin/repos/repo_add_base.html:28
 #: kallithea/templates/admin/repos/repo_edit_settings.html:33
-#: kallithea/templates/data_table/_dt_elements.html:134
+#: kallithea/templates/data_table/_dt_elements.html:140
 #: kallithea/templates/forks/fork.html:42
 msgid "Repository group"
 msgstr ""
@@ -2692,7 +2690,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:40
-#: kallithea/templates/data_table/_dt_elements.html:141
+#: kallithea/templates/data_table/_dt_elements.html:147
 msgid "User group"
 msgstr ""
 
@@ -2862,7 +2860,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:121
+#: kallithea/templates/data_table/_dt_elements.html:127
 #, python-format
 msgid "Confirm to delete this group: %s with %s repository"
 msgid_plural "Confirm to delete this group: %s with %s repositories"
@@ -3024,14 +3022,10 @@
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit.html:37
-msgid "Caches"
+msgid "Remote"
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit.html:40
-msgid "Remote"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit.html:43
 #: kallithea/templates/summary/statistics.html:8
 #: kallithea/templates/summary/summary.html:169
 #: kallithea/templates/summary/summary.html:170
@@ -3069,7 +3063,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:46
-#: kallithea/templates/data_table/_dt_elements.html:68
+#: kallithea/templates/data_table/_dt_elements.html:74
 #, python-format
 msgid "Confirm to delete this repository: %s"
 msgstr ""
@@ -3100,43 +3094,14 @@
 "it or restore it."
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:4
-msgid "Invalidate Repository Cache"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:6
-msgid ""
-"Manually invalidate cache for this repository. On first access, the "
-"repository will be cached again."
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:9
-msgid "List of Cached Values"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:12
-msgid "Prefix"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:13
+#: kallithea/templates/admin/repos/repo_edit_fields.html:6
+msgid "Label"
+msgstr ""
+
 #: kallithea/templates/admin/repos/repo_edit_fields.html:7
 msgid "Key"
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:14
-#: kallithea/templates/admin/user_groups/user_group_add.html:40
-#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
-#: kallithea/templates/admin/user_groups/user_groups.html:41
-#: kallithea/templates/admin/users/user_add.html:69
-#: kallithea/templates/admin/users/user_edit_profile.html:74
-#: kallithea/templates/admin/users/users.html:42
-msgid "Active"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_fields.html:6
-msgid "Label"
-msgstr ""
-
 #: kallithea/templates/admin/repos/repo_edit_fields.html:20
 #, python-format
 msgid "Confirm to delete this field: %s"
@@ -3647,6 +3612,15 @@
 msgid "Short, optional description for this user group."
 msgstr ""
 
+#: kallithea/templates/admin/user_groups/user_group_add.html:40
+#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
+#: kallithea/templates/admin/user_groups/user_groups.html:41
+#: kallithea/templates/admin/users/user_add.html:69
+#: kallithea/templates/admin/users/user_edit_profile.html:74
+#: kallithea/templates/admin/users/users.html:42
+msgid "Active"
+msgstr ""
+
 #: kallithea/templates/admin/user_groups/user_group_edit.html:5
 #, python-format
 msgid "%s user group settings"
@@ -3668,7 +3642,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:19
-#: kallithea/templates/data_table/_dt_elements.html:102
+#: kallithea/templates/data_table/_dt_elements.html:108
 #, python-format
 msgid "Confirm to delete this user group: %s"
 msgstr ""
@@ -3742,7 +3716,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:90
+#: kallithea/templates/data_table/_dt_elements.html:96
 #, python-format
 msgid "Confirm to delete this user: %s"
 msgstr ""
@@ -3840,10 +3814,12 @@
 msgstr ""
 
 #: kallithea/templates/base/base.html:167
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Follow"
 msgstr ""
 
 #: kallithea/templates/base/base.html:168
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Unfollow"
 msgstr ""
 
@@ -4246,23 +4222,23 @@
 msgid "Merge"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:96
+#: kallithea/templates/changeset/changeset.html:95
 msgid "Grafted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:102
+#: kallithea/templates/changeset/changeset.html:100
 msgid "Transplanted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:108
+#: kallithea/templates/changeset/changeset.html:106
 msgid "Replaced by:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:122
+#: kallithea/templates/changeset/changeset.html:120
 msgid "Preceded by:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:139
+#: kallithea/templates/changeset/changeset.html:137
 #: kallithea/templates/compare/compare_diff.html:59
 #: kallithea/templates/pullrequests/pullrequest_show.html:290
 #, python-format
@@ -4271,7 +4247,7 @@
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/templates/changeset/changeset.html:141
+#: kallithea/templates/changeset/changeset.html:139
 #: kallithea/templates/compare/compare_diff.html:61
 #: kallithea/templates/pullrequests/pullrequest_show.html:292
 #, python-format
@@ -4280,8 +4256,8 @@
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
 #: kallithea/templates/pullrequests/pullrequest_show.html:333
@@ -4510,23 +4486,23 @@
 msgid "Repository creation in progress..."
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:42
+#: kallithea/templates/data_table/_dt_elements.html:48
 msgid "No changesets yet"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:48
-#: kallithea/templates/data_table/_dt_elements.html:50
+#: kallithea/templates/data_table/_dt_elements.html:54
+#: kallithea/templates/data_table/_dt_elements.html:56
 #, python-format
 msgid "Subscribe to %s rss feed"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:56
-#: kallithea/templates/data_table/_dt_elements.html:58
+#: kallithea/templates/data_table/_dt_elements.html:62
+#: kallithea/templates/data_table/_dt_elements.html:64
 #, python-format
 msgid "Subscribe to %s atom feed"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:76
+#: kallithea/templates/data_table/_dt_elements.html:82
 msgid "Creating"
 msgstr ""
 
@@ -4558,6 +4534,11 @@
 msgid "by"
 msgstr ""
 
+#: kallithea/templates/email_templates/changeset_comment.html:36
+#: kallithea/templates/email_templates/pull_request_comment.html:43
+msgid "View Comment"
+msgstr ""
+
 #: kallithea/templates/email_templates/comment.html:27
 msgid "Status change:"
 msgstr ""
@@ -4566,32 +4547,40 @@
 msgid "The pull request has been closed."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:9
+#: kallithea/templates/email_templates/default.html:4
+msgid "Message"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:4
+msgid "Password Reset Request"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:10
 #, python-format
 msgid "Hello %s"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:16
+#: kallithea/templates/email_templates/password_reset.html:17
 msgid "We have received a request to reset the password for your account."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:25
+#: kallithea/templates/email_templates/password_reset.html:26
 msgid ""
 "This account is however managed outside this system and the password "
 "cannot be changed here."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:28
+#: kallithea/templates/email_templates/password_reset.html:29
 msgid "To set a new password, click the following link"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:33
+#: kallithea/templates/email_templates/password_reset.html:34
 msgid ""
 "Should you not be able to use the link above, please type the following "
 "code into the password reset form"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:44
+#: kallithea/templates/email_templates/password_reset.html:45
 msgid ""
 "If it weren't you who requested the password reset, just disregard this "
 "message."
@@ -4622,6 +4611,10 @@
 msgid "to"
 msgstr ""
 
+#: kallithea/templates/email_templates/pull_request.html:85
+msgid "View Pull Request"
+msgstr ""
+
 #: kallithea/templates/email_templates/pull_request_comment.html:4
 #, python-format
 msgid "Mention in Comment on Pull Request %s \"%s\""
@@ -4637,10 +4630,18 @@
 msgid "Comment on Pull Request %s \"%s\""
 msgstr ""
 
-#: kallithea/templates/email_templates/registration.html:22
+#: kallithea/templates/email_templates/registration.html:5
+msgid "New User Registration"
+msgstr ""
+
+#: kallithea/templates/email_templates/registration.html:23
 msgid "Full Name"
 msgstr ""
 
+#: kallithea/templates/email_templates/registration.html:42
+msgid "View User Profile"
+msgstr ""
+
 #: kallithea/templates/files/diff_2way.html:15
 #, python-format
 msgid "%s File side-by-side diff"
@@ -5241,45 +5242,45 @@
 msgid "Stats gathered: "
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:87
-#: kallithea/templates/summary/summary.html:371
+#: kallithea/templates/summary/statistics.html:85
+#: kallithea/templates/summary/summary.html:369
 msgid "files"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:111
-#: kallithea/templates/summary/summary.html:401
+#: kallithea/templates/summary/statistics.html:109
+#: kallithea/templates/summary/summary.html:399
 msgid "Show more"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:405
+#: kallithea/templates/summary/statistics.html:395
 msgid "commits"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:406
+#: kallithea/templates/summary/statistics.html:396
 msgid "files added"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:407
+#: kallithea/templates/summary/statistics.html:397
 msgid "files changed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:408
+#: kallithea/templates/summary/statistics.html:398
 msgid "files removed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:410
+#: kallithea/templates/summary/statistics.html:400
 msgid "commit"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:411
+#: kallithea/templates/summary/statistics.html:401
 msgid "file added"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:412
+#: kallithea/templates/summary/statistics.html:402
 msgid "file changed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:413
+#: kallithea/templates/summary/statistics.html:403
 msgid "file removed"
 msgstr ""
 
--- a/kallithea/i18n/lb/LC_MESSAGES/kallithea.po	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/i18n/lb/LC_MESSAGES/kallithea.po	Mon May 04 19:24:04 2020 +0200
@@ -1,33 +1,27 @@
-# Translations template for Kallithea.
 # Copyright (C) 2020 Various authors, licensing as GPLv3
 # This file is distributed under the same license as the Kallithea project.
-# FIRST AUTHOR <EMAIL@ADDRESS>, 2020.
-#
 msgid ""
 msgstr ""
-"Project-Id-Version: Kallithea 0.5.0\n"
 "Report-Msgid-Bugs-To: translations@kallithea-scm.org\n"
-"POT-Creation-Date: 2020-01-05 04:10+0100\n"
-"PO-Revision-Date: 2020-04-13 19:26+0000\n"
+"POT-Creation-Date: 2020-05-04 19:12+0200\n"
+"PO-Revision-Date: 2020-04-13 19:42+0000\n"
 "Last-Translator: Dennis Fink <dennis.fink@c3l.lu>\n"
-"Language-Team: none\n"
 "Language: lb\n"
 "MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=utf-8\n"
+"Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=n != 1;\n"
 "X-Generator: Weblate 4.0-dev\n"
-"Generated-By: Babel 2.7.0\n"
 
 #: kallithea/controllers/changelog.py:67
-#: kallithea/controllers/pullrequests.py:250 kallithea/lib/base.py:602
+#: kallithea/controllers/pullrequests.py:247 kallithea/lib/base.py:602
 msgid "There are no changesets yet"
 msgstr "Et sinn nach keng Ännerungen do"
 
 #: kallithea/controllers/admin/permissions.py:64
 #: kallithea/controllers/admin/permissions.py:68
 #: kallithea/controllers/admin/permissions.py:72
-#: kallithea/controllers/changelog.py:137
+#: kallithea/controllers/changelog.py:136
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:7
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:88
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:7
@@ -36,35 +30,36 @@
 msgid "None"
 msgstr "Keng"
 
-#: kallithea/controllers/changelog.py:140 kallithea/controllers/files.py:189
+#: kallithea/controllers/changelog.py:139 kallithea/controllers/files.py:189
 msgid "(closed)"
 msgstr "(Zou)"
 
-#: kallithea/controllers/changeset.py:81
+#: kallithea/controllers/changeset.py:82
 msgid "Show whitespace"
 msgstr "Leerzeechen uweisen"
 
-#: kallithea/controllers/changeset.py:88 kallithea/controllers/changeset.py:95
+#: kallithea/controllers/changeset.py:89
+#: kallithea/controllers/changeset.py:96
 #: kallithea/templates/files/diff_2way.html:55
 msgid "Ignore whitespace"
 msgstr "Leerzechen ignoréieren"
 
-#: kallithea/controllers/changeset.py:161
+#: kallithea/controllers/changeset.py:162
 #, python-format
 msgid "Increase diff context to %(num)s lines"
 msgstr ""
 
-#: kallithea/controllers/changeset.py:201
+#: kallithea/controllers/changeset.py:202
 msgid "No permission to change status"
 msgstr "Keng Erlabnis fir den Status ze änneren"
 
-#: kallithea/controllers/changeset.py:212
+#: kallithea/controllers/changeset.py:213
 #, python-format
 msgid "Successfully deleted pull request %s"
 msgstr ""
 
 #: kallithea/controllers/changeset.py:319 kallithea/controllers/files.py:89
-#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:700
+#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:697
 msgid "Such revision does not exist for this repository"
 msgstr ""
 
@@ -77,60 +72,61 @@
 msgid "Cannot compare repositories of different types"
 msgstr ""
 
-#: kallithea/controllers/compare.py:246
+#: kallithea/controllers/compare.py:247
 msgid "Cannot show empty diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:248
+#: kallithea/controllers/compare.py:249
 msgid "No ancestor found for merge diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:252
+#: kallithea/controllers/compare.py:253
 msgid "Multiple merge ancestors found for merge compare"
 msgstr ""
 
-#: kallithea/controllers/compare.py:268
+#: kallithea/controllers/compare.py:269
 msgid "Cannot compare repositories without using common ancestor"
 msgstr ""
 
-#: kallithea/controllers/error.py:71
+#: kallithea/controllers/error.py:67
 msgid "No response"
-msgstr ""
-
-#: kallithea/controllers/error.py:72
+msgstr "Keng Äntwert"
+
+#: kallithea/controllers/error.py:68
 msgid "Unknown error"
-msgstr ""
-
-#: kallithea/controllers/error.py:85
-msgid "The request could not be understood by the server due to malformed syntax."
+msgstr "Onbekannten Feeler"
+
+#: kallithea/controllers/error.py:81
+msgid ""
+"The request could not be understood by the server due to malformed syntax."
+msgstr ""
+
+#: kallithea/controllers/error.py:84
+msgid "Unauthorized access to resource"
+msgstr ""
+
+#: kallithea/controllers/error.py:86
+msgid "You don't have permission to view this page"
 msgstr ""
 
 #: kallithea/controllers/error.py:88
-msgid "Unauthorized access to resource"
+msgid "The resource could not be found"
 msgstr ""
 
 #: kallithea/controllers/error.py:90
-msgid "You don't have permission to view this page"
-msgstr ""
-
-#: kallithea/controllers/error.py:92
-msgid "The resource could not be found"
-msgstr ""
-
-#: kallithea/controllers/error.py:94
 msgid ""
 "The server encountered an unexpected condition which prevented it from "
 "fulfilling the request."
 msgstr ""
 
-#: kallithea/controllers/feed.py:63
+#: kallithea/controllers/feed.py:59
 #, python-format
 msgid "%s committed on %s"
 msgstr ""
 
-#: kallithea/controllers/feed.py:88
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/controllers/feed.py:84
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/compare/compare_diff.html:95
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
@@ -138,19 +134,19 @@
 msgid "Changeset was too big and was cut off..."
 msgstr ""
 
-#: kallithea/controllers/feed.py:111 kallithea/controllers/feed.py:140
+#: kallithea/controllers/feed.py:107
 #, python-format
 msgid "%s %s feed"
 msgstr ""
 
-#: kallithea/controllers/feed.py:113 kallithea/controllers/feed.py:142
+#: kallithea/controllers/feed.py:109
 #, python-format
 msgid "Changes on %s repository"
 msgstr ""
 
 #: kallithea/controllers/files.py:85
 msgid "Click here to add new file"
-msgstr ""
+msgstr "Klick hei fir eng Datei bäizefügen"
 
 #: kallithea/controllers/files.py:86
 msgid "There are no files yet."
@@ -161,155 +157,160 @@
 msgid "%s at %s"
 msgstr ""
 
-#: kallithea/controllers/files.py:296
+#: kallithea/controllers/files.py:295
 msgid "You can only delete files with revision being a valid branch"
 msgstr ""
 
-#: kallithea/controllers/files.py:307
+#: kallithea/controllers/files.py:306
 #, python-format
 msgid "Deleted file %s via Kallithea"
 msgstr ""
 
-#: kallithea/controllers/files.py:331
+#: kallithea/controllers/files.py:330
 #, python-format
 msgid "Successfully deleted file %s"
 msgstr ""
 
-#: kallithea/controllers/files.py:335 kallithea/controllers/files.py:394
-#: kallithea/controllers/files.py:469
+#: kallithea/controllers/files.py:334 kallithea/controllers/files.py:392
+#: kallithea/controllers/files.py:467
 msgid "Error occurred during commit"
 msgstr ""
 
-#: kallithea/controllers/files.py:350
+#: kallithea/controllers/files.py:349
 msgid "You can only edit files with revision being a valid branch"
 msgstr ""
 
-#: kallithea/controllers/files.py:364
+#: kallithea/controllers/files.py:363
 #, python-format
 msgid "Edited file %s via Kallithea"
 msgstr ""
 
-#: kallithea/controllers/files.py:380
+#: kallithea/controllers/files.py:378
 msgid "No changes"
-msgstr ""
-
-#: kallithea/controllers/files.py:390 kallithea/controllers/files.py:458
+msgstr "Keng Ännerungen"
+
+#: kallithea/controllers/files.py:388 kallithea/controllers/files.py:456
 #, python-format
 msgid "Successfully committed to %s"
 msgstr ""
 
-#: kallithea/controllers/files.py:409
+#: kallithea/controllers/files.py:407
 msgid "Added file via Kallithea"
-msgstr ""
-
-#: kallithea/controllers/files.py:430
+msgstr "Datei iwwert Kallithea bäifügen"
+
+#: kallithea/controllers/files.py:428
 msgid "No content"
-msgstr ""
-
-#: kallithea/controllers/files.py:434
+msgstr "Keen Contenu"
+
+#: kallithea/controllers/files.py:432
 msgid "No filename"
-msgstr ""
-
-#: kallithea/controllers/files.py:461
+msgstr "Keen Dateinumm"
+
+#: kallithea/controllers/files.py:459
 msgid "Location must be relative path and must not contain .. in path"
 msgstr ""
 
-#: kallithea/controllers/files.py:493
+#: kallithea/controllers/files.py:491
 msgid "Downloads disabled"
+msgstr "Eroflueden ausgeschalt"
+
+#: kallithea/controllers/files.py:502
+#, python-format
+msgid "Unknown revision %s"
 msgstr ""
 
 #: kallithea/controllers/files.py:504
-#, python-format
-msgid "Unknown revision %s"
-msgstr ""
+msgid "Empty repository"
+msgstr "Eidel Quell"
 
 #: kallithea/controllers/files.py:506
-msgid "Empty repository"
-msgstr ""
-
-#: kallithea/controllers/files.py:508
 msgid "Unknown archive type"
 msgstr ""
 
-#: kallithea/controllers/files.py:729
+#: kallithea/controllers/files.py:726
 #: kallithea/templates/changeset/changeset_range.html:9
 #: kallithea/templates/email_templates/pull_request.html:64
 #: kallithea/templates/pullrequests/pullrequest.html:84
 msgid "Changesets"
-msgstr ""
-
-#: kallithea/controllers/files.py:730 kallithea/controllers/pullrequests.py:182
-#: kallithea/model/scm.py:676
+msgstr "Ännerungen"
+
+#: kallithea/controllers/files.py:727
+#: kallithea/controllers/pullrequests.py:174 kallithea/model/scm.py:663
 msgid "Branches"
-msgstr ""
-
-#: kallithea/controllers/files.py:731 kallithea/controllers/pullrequests.py:183
-#: kallithea/model/scm.py:687
+msgstr "Äascht"
+
+#: kallithea/controllers/files.py:728
+#: kallithea/controllers/pullrequests.py:175 kallithea/model/scm.py:674
 msgid "Tags"
 msgstr ""
 
-#: kallithea/controllers/forks.py:174
+#: kallithea/controllers/forks.py:175
 #, python-format
 msgid "An error occurred during repository forking %s"
 msgstr ""
 
-#: kallithea/controllers/home.py:79
+#: kallithea/controllers/home.py:77
 msgid "Groups"
-msgstr ""
-
-#: kallithea/controllers/home.py:89
+msgstr "Gruppen"
+
+#: kallithea/controllers/home.py:87
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:90
 #: kallithea/templates/admin/repos/repo_add.html:12
 #: kallithea/templates/admin/repos/repo_add.html:16
 #: kallithea/templates/admin/repos/repos.html:9
 #: kallithea/templates/admin/users/user_edit_advanced.html:6
-#: kallithea/templates/base/base.html:56 kallithea/templates/base/base.html:73
+#: kallithea/templates/base/base.html:56
+#: kallithea/templates/base/base.html:73
 #: kallithea/templates/base/base.html:437 kallithea/templates/index.html:5
 msgid "Repositories"
-msgstr ""
-
-#: kallithea/controllers/home.py:122
+msgstr "Quellen"
+
+#: kallithea/controllers/home.py:119
 #: kallithea/templates/files/files_add.html:32
 #: kallithea/templates/files/files_delete.html:23
 #: kallithea/templates/files/files_edit.html:32
 msgid "Branch"
-msgstr ""
-
-#: kallithea/controllers/home.py:128
+msgstr "Aascht"
+
+#: kallithea/controllers/home.py:125
 msgid "Closed Branches"
 msgstr ""
 
-#: kallithea/controllers/home.py:134
+#: kallithea/controllers/home.py:131
 msgid "Tag"
 msgstr ""
 
-#: kallithea/controllers/home.py:140
+#: kallithea/controllers/home.py:137
 msgid "Bookmark"
-msgstr ""
-
-#: kallithea/controllers/journal.py:112 kallithea/controllers/journal.py:154
+msgstr "Lieszeechen"
+
+#: kallithea/controllers/journal.py:146 kallithea/controllers/journal.py:157
 #: kallithea/templates/journal/public_journal.html:4
 #: kallithea/templates/journal/public_journal.html:18
 msgid "Public Journal"
-msgstr ""
-
-#: kallithea/controllers/journal.py:116 kallithea/controllers/journal.py:158
+msgstr "Ëffentleche Protokoll"
+
+#: kallithea/controllers/journal.py:150 kallithea/controllers/journal.py:161
 #: kallithea/templates/base/base.html:290
 #: kallithea/templates/journal/journal.html:5
 #: kallithea/templates/journal/journal.html:13
 msgid "Journal"
-msgstr ""
-
-#: kallithea/controllers/login.py:140 kallithea/controllers/login.py:185
+msgstr "Protokoll"
+
+#: kallithea/controllers/login.py:109
+msgid "Authentication failed."
+msgstr ""
+
+#: kallithea/controllers/login.py:142 kallithea/controllers/login.py:187
 msgid "Bad captcha"
 msgstr ""
 
-#: kallithea/controllers/login.py:146
+#: kallithea/controllers/login.py:148
 #, python-format
 msgid "You have successfully registered with %s"
-msgstr ""
-
-#: kallithea/controllers/login.py:190
+msgstr "Dir sidd erfollegräich registréiert mat %s"
+
+#: kallithea/controllers/login.py:192
 msgid "A password reset confirmation code has been sent"
 msgstr ""
 
@@ -320,227 +321,228 @@
 #: kallithea/controllers/admin/my_account.py:157
 #: kallithea/controllers/login.py:244
 msgid "Successfully updated password"
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:68
+msgstr "Passwuert erfollegräich erneiert"
+
+#: kallithea/controllers/pullrequests.py:67
 #, python-format
 msgid "Invalid reviewer \"%s\" specified"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:131
+#: kallithea/controllers/pullrequests.py:123
 #, python-format
 msgid "%s (closed)"
 msgstr "%s (Zou)"
 
-#: kallithea/controllers/pullrequests.py:158
+#: kallithea/controllers/pullrequests.py:150
 #: kallithea/templates/changeset/changeset.html:12
 msgid "Changeset"
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:179
+msgstr "Ännerung"
+
+#: kallithea/controllers/pullrequests.py:171
 msgid "Special"
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:180
+msgstr "Spezial"
+
+#: kallithea/controllers/pullrequests.py:172
 msgid "Peer branches"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:181 kallithea/model/scm.py:682
+#: kallithea/controllers/pullrequests.py:173 kallithea/model/scm.py:669
 msgid "Bookmarks"
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:318
+msgstr "Lieszeechen"
+
+#: kallithea/controllers/pullrequests.py:315
 #, python-format
 msgid "Error creating pull request: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:345
-#: kallithea/controllers/pullrequests.py:368
+#: kallithea/controllers/pullrequests.py:342
+#: kallithea/controllers/pullrequests.py:365
 msgid "Error occurred while creating pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:350
+#: kallithea/controllers/pullrequests.py:347
 msgid "Successfully opened new pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:373
+#: kallithea/controllers/pullrequests.py:370
 msgid "New pull request iteration created"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:401
+#: kallithea/controllers/pullrequests.py:398
 #, python-format
 msgid "Meanwhile, the following reviewers have been added: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:405
+#: kallithea/controllers/pullrequests.py:402
 #, python-format
 msgid "Meanwhile, the following reviewers have been removed: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:421
-#: kallithea/model/pull_request.py:232
+#: kallithea/controllers/pullrequests.py:418
+#: kallithea/model/pull_request.py:230
 msgid "No description"
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:430
+msgstr "Keng Beschreiwung"
+
+#: kallithea/controllers/pullrequests.py:427
 msgid "Pull request updated"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:443
+#: kallithea/controllers/pullrequests.py:440
 msgid "Successfully deleted pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:479
+#: kallithea/controllers/pullrequests.py:476
 #, python-format
 msgid "Revision %s not found in %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:506
+#: kallithea/controllers/pullrequests.py:504
 #, python-format
 msgid "Error: changesets not found when displaying pull request from %s."
 msgstr ""
 
+#: kallithea/controllers/pullrequests.py:518
+#, python-format
+msgid "This pull request has already been merged to %s."
+msgstr ""
+
 #: kallithea/controllers/pullrequests.py:520
-#, python-format
-msgid "This pull request has already been merged to %s."
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:522
 msgid "This pull request has been closed and can not be updated."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:546
+#: kallithea/controllers/pullrequests.py:539
 #, python-format
 msgid "The following additional changes are available on %s:"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:548
-#: kallithea/controllers/pullrequests.py:552
+#: kallithea/controllers/pullrequests.py:541
+#: kallithea/controllers/pullrequests.py:545
 msgid "No additional changesets found for iterating on this pull request."
 msgstr ""
 
+#: kallithea/controllers/pullrequests.py:553
+#, python-format
+msgid "Note: Branch %s has another head: %s."
+msgstr ""
+
 #: kallithea/controllers/pullrequests.py:560
-#, python-format
-msgid "Note: Branch %s has another head: %s."
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:567
 msgid "Git pull requests don't support iterating yet."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:569
-#, python-format
-msgid "Error: some changesets not found when displaying pull request from %s."
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:593
+#: kallithea/controllers/pullrequests.py:562
+#, python-format
+msgid ""
+"Error: some changesets not found when displaying pull request from %s."
+msgstr ""
+
+#: kallithea/controllers/pullrequests.py:586
 msgid "The diff can't be shown - the PR revisions could not be found."
 msgstr ""
 
+#: kallithea/controllers/search.py:132
+msgid "Invalid search query. Try quoting it."
+msgstr ""
+
 #: kallithea/controllers/search.py:136
-msgid "Invalid search query. Try quoting it."
-msgstr ""
-
-#: kallithea/controllers/search.py:140
 msgid "The server has no search index."
 msgstr ""
 
-#: kallithea/controllers/search.py:143
+#: kallithea/controllers/search.py:139
 msgid "An error occurred during search operation."
 msgstr ""
 
-#: kallithea/controllers/summary.py:168
-#: kallithea/templates/summary/summary.html:412
+#: kallithea/controllers/summary.py:169
+#: kallithea/templates/summary/summary.html:410
 msgid "No data ready yet"
-msgstr ""
-
-#: kallithea/controllers/summary.py:171
+msgstr "Daten sinn nach net prett"
+
+#: kallithea/controllers/summary.py:172
 #: kallithea/templates/summary/summary.html:97
 msgid "Statistics are disabled for this repository"
-msgstr ""
-
-#: kallithea/controllers/admin/auth_settings.py:137
+msgstr "Statistike si fir des Quell ausgeschalt"
+
+#: kallithea/controllers/admin/auth_settings.py:136
 msgid "Auth settings updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/auth_settings.py:148
+#: kallithea/controllers/admin/auth_settings.py:147
 msgid "error occurred during update of auth settings"
 msgstr ""
 
-#: kallithea/controllers/admin/defaults.py:75
+#: kallithea/controllers/admin/defaults.py:74
 msgid "Default settings updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/defaults.py:90
+#: kallithea/controllers/admin/defaults.py:89
 msgid "Error occurred during update of defaults"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:59
 #: kallithea/controllers/admin/my_account.py:232
-#: kallithea/controllers/admin/users.py:248
+#: kallithea/controllers/admin/users.py:246
 msgid "Forever"
-msgstr ""
+msgstr "Éiweg"
 
 #: kallithea/controllers/admin/gists.py:60
 #: kallithea/controllers/admin/my_account.py:233
-#: kallithea/controllers/admin/users.py:249
+#: kallithea/controllers/admin/users.py:247
 msgid "5 minutes"
-msgstr ""
+msgstr "5 Minutten"
 
 #: kallithea/controllers/admin/gists.py:61
 #: kallithea/controllers/admin/my_account.py:234
-#: kallithea/controllers/admin/users.py:250
+#: kallithea/controllers/admin/users.py:248
 msgid "1 hour"
-msgstr ""
+msgstr "1 Stonn"
 
 #: kallithea/controllers/admin/gists.py:62
 #: kallithea/controllers/admin/my_account.py:235
-#: kallithea/controllers/admin/users.py:251
+#: kallithea/controllers/admin/users.py:249
 msgid "1 day"
-msgstr ""
+msgstr "1 Dag"
 
 #: kallithea/controllers/admin/gists.py:63
 #: kallithea/controllers/admin/my_account.py:236
-#: kallithea/controllers/admin/users.py:252
+#: kallithea/controllers/admin/users.py:250
 msgid "1 month"
-msgstr ""
+msgstr "1 Mount"
 
 #: kallithea/controllers/admin/gists.py:67
 #: kallithea/controllers/admin/my_account.py:238
-#: kallithea/controllers/admin/users.py:254
+#: kallithea/controllers/admin/users.py:252
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:65
 #: kallithea/templates/admin/users/user_edit_api_keys.html:65
 msgid "Lifetime"
-msgstr ""
-
-#: kallithea/controllers/admin/gists.py:142
+msgstr "Liewenszäit"
+
+#: kallithea/controllers/admin/gists.py:148
 msgid "Error occurred during gist creation"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:158
+#: kallithea/controllers/admin/gists.py:164
 #, python-format
 msgid "Deleted gist %s"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:198
+#: kallithea/controllers/admin/gists.py:207
 msgid "Unmodified"
-msgstr ""
-
-#: kallithea/controllers/admin/gists.py:228
+msgstr "Onmodifizéiert"
+
+#: kallithea/controllers/admin/gists.py:237
 msgid "Successfully updated gist content"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:233
+#: kallithea/controllers/admin/gists.py:242
 msgid "Successfully updated gist data"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:236
+#: kallithea/controllers/admin/gists.py:245
 #, python-format
 msgid "Error occurred during update of gist %s"
 msgstr ""
 
-#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:211
-#: kallithea/model/user.py:232
+#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:205
+#: kallithea/model/user.py:226
 msgid "You can't edit this user since it's crucial for entire application"
 msgstr ""
 
@@ -549,7 +551,7 @@
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:134
-#: kallithea/controllers/admin/users.py:181
+#: kallithea/controllers/admin/users.py:179
 #, python-format
 msgid "Error occurred during update of user %s"
 msgstr ""
@@ -559,46 +561,46 @@
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:209
-#: kallithea/controllers/admin/users.py:367
+#: kallithea/controllers/admin/users.py:365
 #, python-format
 msgid "Added email %s to user"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:215
-#: kallithea/controllers/admin/users.py:373
+#: kallithea/controllers/admin/users.py:371
 msgid "An error occurred during email saving"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:224
-#: kallithea/controllers/admin/users.py:383
+#: kallithea/controllers/admin/users.py:381
 msgid "Removed email from user"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:248
-#: kallithea/controllers/admin/users.py:271
+#: kallithea/controllers/admin/users.py:269
 msgid "API key successfully created"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:257
-#: kallithea/controllers/admin/users.py:281
+#: kallithea/controllers/admin/users.py:279
 msgid "API key successfully reset"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:261
-#: kallithea/controllers/admin/users.py:285
+#: kallithea/controllers/admin/users.py:283
 msgid "API key successfully deleted"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:281
-#: kallithea/controllers/admin/users.py:456
+#: kallithea/controllers/admin/users.py:454
 #, python-format
 msgid "SSH key %s successfully added"
-msgstr ""
+msgstr "SSH Schlëssel %s erfollegräich bäigefüügt"
 
 #: kallithea/controllers/admin/my_account.py:293
-#: kallithea/controllers/admin/users.py:470
+#: kallithea/controllers/admin/users.py:468
 msgid "SSH key successfully deleted"
-msgstr ""
+msgstr "SSH Schlëssel erfollegräich geläscht"
 
 #: kallithea/controllers/admin/permissions.py:65
 #: kallithea/controllers/admin/permissions.py:69
@@ -608,7 +610,7 @@
 #: kallithea/templates/admin/user_groups/user_group_edit_perms.html:8
 #: kallithea/templates/base/perms_summary.html:15
 msgid "Read"
-msgstr ""
+msgstr "Liesen"
 
 #: kallithea/controllers/admin/permissions.py:66
 #: kallithea/controllers/admin/permissions.py:70
@@ -618,7 +620,7 @@
 #: kallithea/templates/admin/user_groups/user_group_edit_perms.html:9
 #: kallithea/templates/base/perms_summary.html:16
 msgid "Write"
-msgstr ""
+msgstr "Schreiwen"
 
 #: kallithea/controllers/admin/permissions.py:67
 #: kallithea/controllers/admin/permissions.py:71
@@ -651,7 +653,7 @@
 #: kallithea/templates/base/base.html:328
 #: kallithea/templates/base/perms_summary.html:17
 msgid "Admin"
-msgstr ""
+msgstr "Administrateur"
 
 #: kallithea/controllers/admin/permissions.py:78
 #: kallithea/controllers/admin/permissions.py:89
@@ -662,7 +664,7 @@
 #: kallithea/templates/admin/auth/auth_settings.html:42
 #: kallithea/templates/base/root.html:50
 msgid "Disabled"
-msgstr ""
+msgstr "Ausgeschalt"
 
 #: kallithea/controllers/admin/permissions.py:80
 msgid "Allowed with manual account activation"
@@ -672,11 +674,11 @@
 msgid "Allowed with automatic account activation"
 msgstr ""
 
-#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1673
+#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1578
 msgid "Manual activation of external account"
 msgstr ""
 
-#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1674
+#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1579
 msgid "Automatic activation of external account"
 msgstr ""
 
@@ -688,7 +690,7 @@
 #: kallithea/templates/admin/auth/auth_settings.html:42
 #: kallithea/templates/base/root.html:49
 msgid "Enabled"
-msgstr ""
+msgstr "Aktivéiert"
 
 #: kallithea/controllers/admin/permissions.py:127
 msgid "Global permissions updated successfully"
@@ -698,344 +700,337 @@
 msgid "Error occurred during update of permissions"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:172
+#: kallithea/controllers/admin/repo_groups.py:165
 #, python-format
 msgid "Error occurred during creation of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:177
+#: kallithea/controllers/admin/repo_groups.py:172
 #, python-format
 msgid "Created repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:224
+#: kallithea/controllers/admin/repo_groups.py:219
 #, python-format
 msgid "Updated repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:240
+#: kallithea/controllers/admin/repo_groups.py:235
 #, python-format
 msgid "Error occurred during update of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:250
+#: kallithea/controllers/admin/repo_groups.py:245
 #, python-format
 msgid "This group contains %s repositories and cannot be deleted"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:257
+#: kallithea/controllers/admin/repo_groups.py:252
 #, python-format
 msgid "This group contains %s subgroups and cannot be deleted"
 msgstr ""
 
+#: kallithea/controllers/admin/repo_groups.py:258
+#, python-format
+msgid "Removed repository group %s"
+msgstr ""
+
 #: kallithea/controllers/admin/repo_groups.py:263
 #, python-format
-msgid "Removed repository group %s"
-msgstr ""
-
-#: kallithea/controllers/admin/repo_groups.py:268
-#, python-format
 msgid "Error occurred during deletion of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:352
-#: kallithea/controllers/admin/repo_groups.py:382
-#: kallithea/controllers/admin/user_groups.py:294
+#: kallithea/controllers/admin/repo_groups.py:347
+#: kallithea/controllers/admin/repo_groups.py:377
+#: kallithea/controllers/admin/user_groups.py:290
 msgid "Cannot revoke permission for yourself as admin"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:367
+#: kallithea/controllers/admin/repo_groups.py:362
 msgid "Repository group permissions updated"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:399
-#: kallithea/controllers/admin/repos.py:358
-#: kallithea/controllers/admin/user_groups.py:306
+#: kallithea/controllers/admin/repo_groups.py:394
+#: kallithea/controllers/admin/repos.py:357
+#: kallithea/controllers/admin/user_groups.py:302
 msgid "An error occurred during revoking of permission"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:136
+#: kallithea/controllers/admin/repos.py:137
 #, python-format
 msgid "Error creating repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:194
+#: kallithea/controllers/admin/repos.py:193
 #, python-format
 msgid "Created repository %s from %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:203
+#: kallithea/controllers/admin/repos.py:202
 #, python-format
 msgid "Forked repository %s as %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:206
+#: kallithea/controllers/admin/repos.py:205
 #, python-format
 msgid "Created repository %s"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:235
+msgstr "Quell %s erstallt"
+
+#: kallithea/controllers/admin/repos.py:234
 #, python-format
 msgid "Repository %s updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:255
+#: kallithea/controllers/admin/repos.py:254
 #, python-format
 msgid "Error occurred during update of repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:273
+#: kallithea/controllers/admin/repos.py:272
 #, python-format
 msgid "Detached %s forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:276
+#: kallithea/controllers/admin/repos.py:275
 #, python-format
 msgid "Deleted %s forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:281
+#: kallithea/controllers/admin/repos.py:280
 #, python-format
 msgid "Deleted repository %s"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:284
+msgstr "Quell %s geläscht"
+
+#: kallithea/controllers/admin/repos.py:283
 #, python-format
 msgid "Cannot delete repository %s which still has forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:289
+#: kallithea/controllers/admin/repos.py:288
 #, python-format
 msgid "An error occurred during deletion of %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:329
+#: kallithea/controllers/admin/repos.py:328
 msgid "Repository permissions updated"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:388
+#: kallithea/controllers/admin/repos.py:387
 #, python-format
 msgid "Field validation error: %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:391
+#: kallithea/controllers/admin/repos.py:390
 #, python-format
 msgid "An error occurred during creation of field: %r"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:402
+#: kallithea/controllers/admin/repos.py:401
 msgid "An error occurred during removal of field"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:416
+#: kallithea/controllers/admin/repos.py:415
 msgid "-- Not a fork --"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:448
+#: kallithea/controllers/admin/repos.py:447
 msgid "Updated repository visibility in public journal"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:452
+#: kallithea/controllers/admin/repos.py:451
 msgid "An error occurred during setting this repository in public journal"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:468
+#: kallithea/controllers/admin/repos.py:467
 msgid "Nothing"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:470
+msgstr "Näischt"
+
+#: kallithea/controllers/admin/repos.py:469
 #, python-format
 msgid "Marked repository %s as fork of %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:477
+#: kallithea/controllers/admin/repos.py:476
 msgid "An error occurred during this operation"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:490
-msgid "Cache invalidation successful"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:494
-msgid "An error occurred during cache invalidation"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:507
+#: kallithea/controllers/admin/repos.py:488
 msgid "Pulled from remote location"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:510
+#: kallithea/controllers/admin/repos.py:491
 msgid "An error occurred during pull from remote location"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:541
+#: kallithea/controllers/admin/repos.py:522
 msgid "An error occurred during deletion of repository stats"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:131
+#: kallithea/controllers/admin/settings.py:132
 msgid "Updated VCS settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:135 kallithea/lib/utils.py:237
+#: kallithea/controllers/admin/settings.py:136
 msgid ""
 "Unable to activate hgsubversion support. The \"hgsubversion\" library is "
 "missing"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:141
-#: kallithea/controllers/admin/settings.py:233
+#: kallithea/controllers/admin/settings.py:142
+#: kallithea/controllers/admin/settings.py:234
 msgid "Error occurred while updating application settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:176
+#: kallithea/controllers/admin/settings.py:177
 #, python-format
 msgid "Repositories successfully rescanned. Added: %s. Removed: %s."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:188
+#: kallithea/controllers/admin/settings.py:189
 #, python-format
 msgid "Invalidated %s repositories"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:229
+#: kallithea/controllers/admin/settings.py:230
 msgid "Updated application settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:283
+#: kallithea/controllers/admin/settings.py:284
 msgid "Updated visualisation settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:288
+#: kallithea/controllers/admin/settings.py:289
 msgid "Error occurred during updating visualisation settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:312
+#: kallithea/controllers/admin/settings.py:313
 msgid "Please enter email address"
-msgstr ""
-
-#: kallithea/controllers/admin/settings.py:327
+msgstr "Wannechgelift E-Mail-Adress afügen"
+
+#: kallithea/controllers/admin/settings.py:328
 msgid "Send email task created"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:355
+#: kallithea/controllers/admin/settings.py:356
 msgid "Hook already exists"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:357
+#: kallithea/controllers/admin/settings.py:358
 msgid "Builtin hooks are read-only. Please use another hook name."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:360
+#: kallithea/controllers/admin/settings.py:361
 msgid "Added new hook"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:376
+#: kallithea/controllers/admin/settings.py:377
 msgid "Updated hooks"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:380
+#: kallithea/controllers/admin/settings.py:381
 msgid "Error occurred during hook creation"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:404
+#: kallithea/controllers/admin/settings.py:405
 msgid "Whoosh reindex task scheduled"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:138
+#: kallithea/controllers/admin/user_groups.py:134
 #, python-format
 msgid "Created user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:151
+#: kallithea/controllers/admin/user_groups.py:147
 #, python-format
 msgid "Error occurred during creation of user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:179
+#: kallithea/controllers/admin/user_groups.py:175
 #, python-format
 msgid "Updated user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:201
+#: kallithea/controllers/admin/user_groups.py:197
 #, python-format
 msgid "Error occurred during update of user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:212
+#: kallithea/controllers/admin/user_groups.py:208
 msgid "Successfully deleted user group"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:217
+#: kallithea/controllers/admin/user_groups.py:213
 msgid "An error occurred during deletion of user group"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:273
+#: kallithea/controllers/admin/user_groups.py:269
 msgid "Target group cannot be the same"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:279
+#: kallithea/controllers/admin/user_groups.py:275
 msgid "User group permissions updated"
 msgstr ""
 
+#: kallithea/controllers/admin/user_groups.py:384
+#: kallithea/controllers/admin/users.py:336
+msgid "Updated permissions"
+msgstr ""
+
 #: kallithea/controllers/admin/user_groups.py:388
-#: kallithea/controllers/admin/users.py:338
-msgid "Updated permissions"
-msgstr ""
-
-#: kallithea/controllers/admin/user_groups.py:392
-#: kallithea/controllers/admin/users.py:342
+#: kallithea/controllers/admin/users.py:340
 msgid "An error occurred during permissions saving"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:123
+#: kallithea/controllers/admin/users.py:121
 #, python-format
 msgid "Created user %s"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:138
+#: kallithea/controllers/admin/users.py:136
 #, python-format
 msgid "Error occurred during creation of user %s"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:162
+#: kallithea/controllers/admin/users.py:160
 msgid "User updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:190
+#: kallithea/controllers/admin/users.py:188
 msgid "Successfully deleted user"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:195
+#: kallithea/controllers/admin/users.py:193
 msgid "An error occurred during deletion of user"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:203
+#: kallithea/controllers/admin/users.py:201
 msgid "The default user cannot be edited"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:409
+#: kallithea/controllers/admin/users.py:407
 #, python-format
 msgid "Added IP address %s to user whitelist"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:415
+#: kallithea/controllers/admin/users.py:413
 msgid "An error occurred while adding IP address"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:427
+#: kallithea/controllers/admin/users.py:425
 msgid "Removed IP address from user whitelist"
 msgstr ""
 
-#: kallithea/lib/auth.py:684
+#: kallithea/lib/auth.py:634
 msgid "You need to be a registered user to perform this action"
 msgstr ""
 
-#: kallithea/lib/auth.py:712
+#: kallithea/lib/auth.py:662
 msgid "You need to be signed in to view this page"
 msgstr ""
 
 #: kallithea/lib/base.py:483
-msgid "CSRF token leak has been detected - all form tokens have been expired"
+msgid ""
+"CSRF token leak has been detected - all form tokens have been expired"
 msgstr ""
 
 #: kallithea/lib/base.py:580
@@ -1049,179 +1044,181 @@
 
 #: kallithea/lib/base.py:647
 msgid "SSH access is disabled."
-msgstr ""
+msgstr "SSH Accès ass ausgeschalt."
 
 #: kallithea/lib/diffs.py:194
 msgid "Binary file"
-msgstr ""
+msgstr "Binär Datei"
 
 #: kallithea/lib/diffs.py:214
-msgid "Changeset was too big and was cut off, use diff menu to display this diff"
-msgstr ""
-
-#: kallithea/lib/diffs.py:224
+msgid ""
+"Changeset was too big and was cut off, use diff menu to display this diff"
+msgstr ""
+
+#: kallithea/lib/diffs.py:223
 msgid "No changes detected"
 msgstr ""
 
-#: kallithea/lib/helpers.py:653
+#: kallithea/lib/helpers.py:670
 #, python-format
 msgid "Deleted branch: %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:655
+#: kallithea/lib/helpers.py:672
 #, python-format
 msgid "Created tag: %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:666
+#: kallithea/lib/helpers.py:683
 #, python-format
 msgid "Changeset %s not found"
 msgstr ""
 
-#: kallithea/lib/helpers.py:715
+#: kallithea/lib/helpers.py:732
 #, python-format
 msgid "Show all combined changesets %s->%s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:721
+#: kallithea/lib/helpers.py:738
 msgid "Compare view"
 msgstr ""
 
-#: kallithea/lib/helpers.py:740
+#: kallithea/lib/helpers.py:757
 msgid "and"
 msgstr ""
 
-#: kallithea/lib/helpers.py:741
+#: kallithea/lib/helpers.py:758
 #, python-format
 msgid "%s more"
 msgstr ""
 
-#: kallithea/lib/helpers.py:742 kallithea/templates/changelog/changelog.html:43
+#: kallithea/lib/helpers.py:759
+#: kallithea/templates/changelog/changelog.html:43
 msgid "revisions"
 msgstr ""
 
-#: kallithea/lib/helpers.py:766
+#: kallithea/lib/helpers.py:783
 #, python-format
 msgid "Fork name %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:787
+#: kallithea/lib/helpers.py:804
 #, python-format
 msgid "Pull request %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:797
+#: kallithea/lib/helpers.py:814
 msgid "[deleted] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:799 kallithea/lib/helpers.py:811
+#: kallithea/lib/helpers.py:816 kallithea/lib/helpers.py:828
 msgid "[created] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:801
+#: kallithea/lib/helpers.py:818
 msgid "[created] repository as fork"
 msgstr ""
 
-#: kallithea/lib/helpers.py:803 kallithea/lib/helpers.py:813
+#: kallithea/lib/helpers.py:820 kallithea/lib/helpers.py:830
 msgid "[forked] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:805 kallithea/lib/helpers.py:815
+#: kallithea/lib/helpers.py:822 kallithea/lib/helpers.py:832
 msgid "[updated] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:807
+#: kallithea/lib/helpers.py:824
 msgid "[downloaded] archive from repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:809
+#: kallithea/lib/helpers.py:826
 msgid "[delete] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:817
+#: kallithea/lib/helpers.py:834
 msgid "[created] user"
 msgstr ""
 
-#: kallithea/lib/helpers.py:819
+#: kallithea/lib/helpers.py:836
 msgid "[updated] user"
 msgstr ""
 
-#: kallithea/lib/helpers.py:821
+#: kallithea/lib/helpers.py:838
 msgid "[created] user group"
 msgstr ""
 
-#: kallithea/lib/helpers.py:823
+#: kallithea/lib/helpers.py:840
 msgid "[updated] user group"
 msgstr ""
 
-#: kallithea/lib/helpers.py:825
+#: kallithea/lib/helpers.py:842
 msgid "[commented] on revision in repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:827
+#: kallithea/lib/helpers.py:844
 msgid "[commented] on pull request for"
 msgstr ""
 
-#: kallithea/lib/helpers.py:829
+#: kallithea/lib/helpers.py:846
 msgid "[closed] pull request for"
 msgstr ""
 
-#: kallithea/lib/helpers.py:831
+#: kallithea/lib/helpers.py:848
 msgid "[pushed] into"
 msgstr ""
 
-#: kallithea/lib/helpers.py:833
+#: kallithea/lib/helpers.py:850
 msgid "[committed via Kallithea] into repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:835
+#: kallithea/lib/helpers.py:852
 msgid "[pulled from remote] into repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:837
+#: kallithea/lib/helpers.py:854
 msgid "[pulled] from"
 msgstr ""
 
-#: kallithea/lib/helpers.py:839
+#: kallithea/lib/helpers.py:856
 msgid "[started following] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:841
+#: kallithea/lib/helpers.py:858
 msgid "[stopped following] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:961
+#: kallithea/lib/helpers.py:975
 #, python-format
 msgid " and %s more"
 msgstr ""
 
-#: kallithea/lib/helpers.py:965
+#: kallithea/lib/helpers.py:979
 #: kallithea/templates/compare/compare_diff.html:69
 #: kallithea/templates/pullrequests/pullrequest_show.html:297
 msgid "No files"
 msgstr ""
 
-#: kallithea/lib/helpers.py:990
+#: kallithea/lib/helpers.py:1004
 msgid "new file"
 msgstr ""
 
-#: kallithea/lib/helpers.py:993
+#: kallithea/lib/helpers.py:1007
 msgid "mod"
 msgstr ""
 
-#: kallithea/lib/helpers.py:996
+#: kallithea/lib/helpers.py:1010
 msgid "del"
 msgstr ""
 
-#: kallithea/lib/helpers.py:999
+#: kallithea/lib/helpers.py:1013
 msgid "rename"
 msgstr ""
 
-#: kallithea/lib/helpers.py:1004
+#: kallithea/lib/helpers.py:1018
 msgid "chmod"
 msgstr ""
 
-#: kallithea/lib/helpers.py:1297
+#: kallithea/lib/helpers.py:1323
 #, python-format
 msgid ""
 "%s repository is not mapped to db perhaps it was created or renamed from "
@@ -1229,96 +1226,98 @@
 "repositories"
 msgstr ""
 
-#: kallithea/lib/ssh.py:73
+#: kallithea/lib/ssh.py:75
 msgid "SSH key is missing"
 msgstr ""
 
-#: kallithea/lib/ssh.py:77
-msgid "Incorrect SSH key - it must have both a key type and a base64 part"
-msgstr ""
-
-#: kallithea/lib/ssh.py:81
+#: kallithea/lib/ssh.py:79
+msgid ""
+"Incorrect SSH key - it must have both a key type and a base64 part, like "
+"'ssh-rsa ASRNeaZu4FA...xlJp='"
+msgstr ""
+
+#: kallithea/lib/ssh.py:83
 msgid "Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'"
 msgstr ""
 
-#: kallithea/lib/ssh.py:84
+#: kallithea/lib/ssh.py:86
 #, python-format
 msgid "Incorrect SSH key - unexpected characters in base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:89
+#: kallithea/lib/ssh.py:91
 #, python-format
 msgid "Incorrect SSH key - failed to decode base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:92
+#: kallithea/lib/ssh.py:94
 #, python-format
 msgid "Incorrect SSH key - base64 part is not %r as claimed but %r"
 msgstr ""
 
-#: kallithea/lib/utils2.py:334
+#: kallithea/lib/utils2.py:253
 #, python-format
 msgid "%d year"
 msgid_plural "%d years"
-msgstr[0] ""
-msgstr[1] ""
-
-#: kallithea/lib/utils2.py:335
+msgstr[0] "%d Joer"
+msgstr[1] "%d Joer"
+
+#: kallithea/lib/utils2.py:254
 #, python-format
 msgid "%d month"
 msgid_plural "%d months"
-msgstr[0] ""
-msgstr[1] ""
-
-#: kallithea/lib/utils2.py:336
+msgstr[0] "%d Mount"
+msgstr[1] "%d Méint"
+
+#: kallithea/lib/utils2.py:255
 #, python-format
 msgid "%d day"
 msgid_plural "%d days"
-msgstr[0] ""
-msgstr[1] ""
-
-#: kallithea/lib/utils2.py:337
+msgstr[0] "%d Dag"
+msgstr[1] "%d Deeg"
+
+#: kallithea/lib/utils2.py:256
 #, python-format
 msgid "%d hour"
 msgid_plural "%d hours"
-msgstr[0] ""
-msgstr[1] ""
-
-#: kallithea/lib/utils2.py:338
+msgstr[0] "%d Stonn"
+msgstr[1] "%d Stonnen"
+
+#: kallithea/lib/utils2.py:257
 #, python-format
 msgid "%d minute"
 msgid_plural "%d minutes"
-msgstr[0] ""
-msgstr[1] ""
-
-#: kallithea/lib/utils2.py:339
+msgstr[0] "%d Minutt"
+msgstr[1] "%d Minutten"
+
+#: kallithea/lib/utils2.py:258
 #, python-format
 msgid "%d second"
 msgid_plural "%d seconds"
-msgstr[0] ""
-msgstr[1] ""
-
-#: kallithea/lib/utils2.py:355
+msgstr[0] "%d Sekonn"
+msgstr[1] "%d Sekonnen"
+
+#: kallithea/lib/utils2.py:274
 #, python-format
 msgid "in %s"
 msgstr ""
 
-#: kallithea/lib/utils2.py:357
+#: kallithea/lib/utils2.py:276
 #, python-format
 msgid "%s ago"
 msgstr ""
 
-#: kallithea/lib/utils2.py:359
+#: kallithea/lib/utils2.py:278
 #, python-format
 msgid "in %s and %s"
 msgstr ""
 
-#: kallithea/lib/utils2.py:362
+#: kallithea/lib/utils2.py:281
 #, python-format
 msgid "%s and %s ago"
 msgstr ""
 
-#: kallithea/lib/utils2.py:365
+#: kallithea/lib/utils2.py:284
 msgid "just now"
 msgstr ""
 
@@ -1327,131 +1326,133 @@
 msgid "on line %s"
 msgstr ""
 
-#: kallithea/model/comment.py:221 kallithea/model/pull_request.py:114
+#: kallithea/model/comment.py:219 kallithea/model/pull_request.py:112
 msgid "[Mention]"
 msgstr ""
 
-#: kallithea/model/db.py:1496
+#: kallithea/model/db.py:1411
 msgid "top level"
 msgstr ""
 
-#: kallithea/model/db.py:1637
+#: kallithea/model/db.py:1542
 msgid "Kallithea Administrator"
 msgstr ""
 
-#: kallithea/model/db.py:1639
+#: kallithea/model/db.py:1544
 msgid "Default user has no access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1640
+#: kallithea/model/db.py:1545
 msgid "Default user has read access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1641
+#: kallithea/model/db.py:1546
 msgid "Default user has write access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1642
+#: kallithea/model/db.py:1547
 msgid "Default user has admin access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1644
+#: kallithea/model/db.py:1549
 msgid "Default user has no access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1645
+#: kallithea/model/db.py:1550
 msgid "Default user has read access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1646
+#: kallithea/model/db.py:1551
 msgid "Default user has write access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1647
+#: kallithea/model/db.py:1552
 msgid "Default user has admin access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1649
+#: kallithea/model/db.py:1554
 msgid "Default user has no access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1650
+#: kallithea/model/db.py:1555
 msgid "Default user has read access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1651
+#: kallithea/model/db.py:1556
 msgid "Default user has write access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1652
+#: kallithea/model/db.py:1557
 msgid "Default user has admin access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1654
+#: kallithea/model/db.py:1559
 msgid "Only admins can create repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1655
+#: kallithea/model/db.py:1560
 msgid "Non-admins can create repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1657
+#: kallithea/model/db.py:1562
 msgid "Only admins can create user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1658
+#: kallithea/model/db.py:1563
 msgid "Non-admins can create user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1660
+#: kallithea/model/db.py:1565
 msgid "Only admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1661
+#: kallithea/model/db.py:1566
 msgid "Non-admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1663
-msgid "Repository creation enabled with write permission to a repository group"
-msgstr ""
-
-#: kallithea/model/db.py:1664
-msgid "Repository creation disabled with write permission to a repository group"
-msgstr ""
-
-#: kallithea/model/db.py:1666
+#: kallithea/model/db.py:1568
+msgid ""
+"Repository creation enabled with write permission to a repository group"
+msgstr ""
+
+#: kallithea/model/db.py:1569
+msgid ""
+"Repository creation disabled with write permission to a repository group"
+msgstr ""
+
+#: kallithea/model/db.py:1571
 msgid "Only admins can fork repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1667
+#: kallithea/model/db.py:1572
 msgid "Non-admins can fork repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1669
+#: kallithea/model/db.py:1574
 msgid "Registration disabled"
 msgstr ""
 
-#: kallithea/model/db.py:1670
+#: kallithea/model/db.py:1575
 msgid "User registration with manual account activation"
 msgstr ""
 
-#: kallithea/model/db.py:1671
+#: kallithea/model/db.py:1576
 msgid "User registration with automatic account activation"
 msgstr ""
 
-#: kallithea/model/db.py:2206
+#: kallithea/model/db.py:1992
 msgid "Not reviewed"
 msgstr ""
 
-#: kallithea/model/db.py:2207
+#: kallithea/model/db.py:1993
 msgid "Under review"
 msgstr ""
 
-#: kallithea/model/db.py:2208
+#: kallithea/model/db.py:1994
 msgid "Not approved"
 msgstr ""
 
-#: kallithea/model/db.py:2209
+#: kallithea/model/db.py:1995
 msgid "Approved"
 msgstr ""
 
@@ -1477,310 +1478,313 @@
 msgid "Name must not contain only digits"
 msgstr ""
 
-#: kallithea/model/notification.py:164
+#: kallithea/model/notification.py:162
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s changeset %(short_id)s \"%(message_short)s\" on "
 "%(branch)s"
 msgstr ""
 
-#: kallithea/model/notification.py:167
+#: kallithea/model/notification.py:165
 #, python-format
 msgid "New user %(new_username)s registered"
 msgstr ""
 
-#: kallithea/model/notification.py:169
+#: kallithea/model/notification.py:167
 #, python-format
 msgid ""
 "[Review] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:170
+#: kallithea/model/notification.py:168
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:183
+#: kallithea/model/notification.py:188
 msgid "Closing"
 msgstr ""
 
-#: kallithea/model/pull_request.py:73
-#, python-format
-msgid "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s"
-msgstr ""
-
-#: kallithea/model/pull_request.py:209
+#: kallithea/model/pull_request.py:72
+#, python-format
+msgid ""
+"%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s"
+msgstr ""
+
+#: kallithea/model/pull_request.py:207
 msgid "Cannot create empty pull request"
 msgstr ""
 
-#: kallithea/model/pull_request.py:217
+#: kallithea/model/pull_request.py:215
 #, python-format
 msgid ""
 "Cannot create pull request - criss cross merge detected, please merge a "
 "later %s revision to %s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:245 kallithea/model/pull_request.py:380
+#: kallithea/model/pull_request.py:243 kallithea/model/pull_request.py:378
 msgid "You are not authorized to create the pull request"
 msgstr ""
 
-#: kallithea/model/pull_request.py:339
+#: kallithea/model/pull_request.py:337
 msgid "Missing changesets since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:346
+#: kallithea/model/pull_request.py:344
 #, python-format
 msgid "New changesets on %s %s since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:353
+#: kallithea/model/pull_request.py:351
 msgid "Ancestor didn't change - diff since previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:360
+#: kallithea/model/pull_request.py:358
 #, python-format
 msgid ""
 "This iteration is based on another %s revision and there is no simple "
 "diff."
 msgstr ""
 
-#: kallithea/model/pull_request.py:362
+#: kallithea/model/pull_request.py:360
 #, python-format
 msgid "No changes found on %s %s since previous iteration."
 msgstr ""
 
-#: kallithea/model/pull_request.py:388
+#: kallithea/model/pull_request.py:386
 #, python-format
 msgid "Closed, next iteration: %s ."
 msgstr ""
 
-#: kallithea/model/scm.py:668
+#: kallithea/model/scm.py:655
 msgid "latest tip"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:56
+#: kallithea/model/ssh_key.py:57
 #, python-format
 msgid "SSH key %r is invalid: %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:68
+#: kallithea/model/ssh_key.py:69
 #, python-format
 msgid "SSH key %s is already used by %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:89
-#, python-format
-msgid "SSH key %r not found"
-msgstr ""
-
-#: kallithea/model/user.py:186
+#: kallithea/model/ssh_key.py:88
+#, python-format
+msgid "SSH key with fingerprint %r found"
+msgstr ""
+
+#: kallithea/model/user.py:180
 msgid "New user registration"
 msgstr ""
 
-#: kallithea/model/user.py:250
-msgid "You can't remove this user since it is crucial for the entire application"
-msgstr ""
-
-#: kallithea/model/user.py:255
+#: kallithea/model/user.py:244
+msgid ""
+"You can't remove this user since it is crucial for the entire application"
+msgstr ""
+
+#: kallithea/model/user.py:249
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repositories and cannot be removed. Switch "
 "owners or remove those repositories: %s"
 msgstr ""
 
-#: kallithea/model/user.py:260
+#: kallithea/model/user.py:254
 #, python-format
 msgid ""
-"User \"%s\" still owns %s repository groups and cannot be removed. Switch"
-" owners or remove those repository groups: %s"
-msgstr ""
-
-#: kallithea/model/user.py:267
+"User \"%s\" still owns %s repository groups and cannot be removed. Switch "
+"owners or remove those repository groups: %s"
+msgstr ""
+
+#: kallithea/model/user.py:261
 #, python-format
 msgid ""
 "User \"%s\" still owns %s user groups and cannot be removed. Switch "
 "owners or remove those user groups: %s"
 msgstr ""
 
-#: kallithea/model/user.py:361
+#: kallithea/model/user.py:355
 msgid "Password reset link"
 msgstr ""
 
-#: kallithea/model/user.py:408
+#: kallithea/model/user.py:402
 msgid "Password reset notification"
 msgstr ""
 
-#: kallithea/model/user.py:409
+#: kallithea/model/user.py:403
 #, python-format
 msgid ""
 "The password to your account %s has been changed using password reset "
 "form."
 msgstr ""
 
-#: kallithea/model/validators.py:52 kallithea/model/validators.py:53
+#: kallithea/model/validators.py:53 kallithea/model/validators.py:54
 msgid "Value cannot be an empty list"
 msgstr ""
 
-#: kallithea/model/validators.py:72
+#: kallithea/model/validators.py:73
 #, python-format
 msgid "Username \"%(username)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:74
+#: kallithea/model/validators.py:75
 #, python-format
 msgid "Username \"%(username)s\" cannot be used"
 msgstr ""
 
-#: kallithea/model/validators.py:76
+#: kallithea/model/validators.py:77
 msgid ""
-"Username may only contain alphanumeric characters underscores, periods or"
-" dashes and must begin with an alphanumeric character or underscore"
-msgstr ""
-
-#: kallithea/model/validators.py:103
+"Username may only contain alphanumeric characters underscores, periods or "
+"dashes and must begin with an alphanumeric character or underscore"
+msgstr ""
+
+#: kallithea/model/validators.py:104
 msgid "The input is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:110
+#: kallithea/model/validators.py:111
 #, python-format
 msgid "Username %(username)s is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:131
-msgid "Invalid user group name"
-msgstr ""
-
 #: kallithea/model/validators.py:132
+msgid "Invalid user group name"
+msgstr ""
+
+#: kallithea/model/validators.py:133
 #, python-format
 msgid "User group \"%(usergroup)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:134
+#: kallithea/model/validators.py:135
 msgid ""
 "user group name may only contain alphanumeric characters underscores, "
 "periods or dashes and must begin with alphanumeric character"
 msgstr ""
 
-#: kallithea/model/validators.py:174
-msgid "Cannot assign this group as parent"
-msgstr ""
-
 #: kallithea/model/validators.py:175
+msgid "Cannot assign this group as parent"
+msgstr ""
+
+#: kallithea/model/validators.py:176
 #, python-format
 msgid "Group \"%(group_name)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:177
+#: kallithea/model/validators.py:178
 #, python-format
 msgid "Repository with name \"%(group_name)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:233
+#: kallithea/model/validators.py:230
 msgid "Invalid characters (non-ascii) in password"
 msgstr ""
 
-#: kallithea/model/validators.py:248
+#: kallithea/model/validators.py:245
 msgid "Invalid old password"
 msgstr ""
 
-#: kallithea/model/validators.py:264
+#: kallithea/model/validators.py:261
 msgid "Passwords do not match"
 msgstr ""
 
-#: kallithea/model/validators.py:279
+#: kallithea/model/validators.py:276
 msgid "Invalid username or password"
 msgstr ""
 
+#: kallithea/model/validators.py:310
+#, python-format
+msgid "Repository name %(repo)s is not allowed"
+msgstr ""
+
+#: kallithea/model/validators.py:312
+#, python-format
+msgid "Repository named %(repo)s already exists"
+msgstr ""
+
 #: kallithea/model/validators.py:313
 #, python-format
-msgid "Repository name %(repo)s is not allowed"
+msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
 msgstr ""
 
 #: kallithea/model/validators.py:315
 #, python-format
-msgid "Repository named %(repo)s already exists"
-msgstr ""
-
-#: kallithea/model/validators.py:316
-#, python-format
-msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
-msgstr ""
-
-#: kallithea/model/validators.py:318
-#, python-format
 msgid "Repository group with name \"%(repo)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:404
+#: kallithea/model/validators.py:401
 msgid "Invalid repository URL"
 msgstr ""
 
-#: kallithea/model/validators.py:405
+#: kallithea/model/validators.py:402
 msgid ""
 "Invalid repository URL. It must be a valid http, https, ssh, svn+http or "
 "svn+https URL"
 msgstr ""
 
-#: kallithea/model/validators.py:430
+#: kallithea/model/validators.py:427
 msgid "Fork has to be the same type as parent"
 msgstr ""
 
-#: kallithea/model/validators.py:445
+#: kallithea/model/validators.py:442
 msgid "You don't have permissions to create repository in this group"
 msgstr ""
 
-#: kallithea/model/validators.py:447
+#: kallithea/model/validators.py:444
 msgid "no permission to create repository in root location"
 msgstr ""
 
-#: kallithea/model/validators.py:497
+#: kallithea/model/validators.py:494
 msgid "You don't have permissions to create a group in this location"
 msgstr ""
 
-#: kallithea/model/validators.py:537
+#: kallithea/model/validators.py:534
 msgid "This username or user group name is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:630
+#: kallithea/model/validators.py:627
 msgid "This is not a valid path"
 msgstr ""
 
-#: kallithea/model/validators.py:647
+#: kallithea/model/validators.py:644
 msgid "This email address is already in use"
 msgstr ""
 
-#: kallithea/model/validators.py:667
+#: kallithea/model/validators.py:664
 #, python-format
 msgid "Email address \"%(email)s\" not found"
 msgstr ""
 
-#: kallithea/model/validators.py:704
+#: kallithea/model/validators.py:701
 msgid ""
 "The LDAP Login attribute of the CN must be specified - this is the name "
 "of the attribute that is equivalent to \"username\""
 msgstr ""
 
-#: kallithea/model/validators.py:716
+#: kallithea/model/validators.py:713
 msgid "Please enter a valid IPv4 or IPv6 address"
 msgstr ""
 
-#: kallithea/model/validators.py:717
-#, python-format
-msgid "The network size (bits) must be within the range of 0-32 (not %(bits)r)"
-msgstr ""
-
-#: kallithea/model/validators.py:750
+#: kallithea/model/validators.py:714
+#, python-format
+msgid ""
+"The network size (bits) must be within the range of 0-32 (not %(bits)r)"
+msgstr ""
+
+#: kallithea/model/validators.py:747
 msgid "Key name can only consist of letters, underscore, dash or numbers"
 msgstr ""
 
-#: kallithea/model/validators.py:764
+#: kallithea/model/validators.py:761
 msgid "Filename cannot be inside a directory"
 msgstr ""
 
-#: kallithea/model/validators.py:780
+#: kallithea/model/validators.py:777
 #, python-format
 msgid "Plugins %(loaded)s and %(next_to_load)s both export the same name"
 msgstr ""
@@ -1840,7 +1844,7 @@
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:60
 #: kallithea/templates/email_templates/pull_request.html:37
 #: kallithea/templates/forks/fork.html:34
-#: kallithea/templates/index_base.html:58
+#: kallithea/templates/index_base.html:59
 #: kallithea/templates/pullrequests/pullrequest.html:33
 #: kallithea/templates/pullrequests/pullrequest_show.html:38
 #: kallithea/templates/pullrequests/pullrequest_show.html:59
@@ -1848,14 +1852,14 @@
 msgid "Description"
 msgstr ""
 
-#: kallithea/templates/index_base.html:60
+#: kallithea/templates/index_base.html:61
 msgid "Last Change"
 msgstr ""
 
 #: kallithea/templates/admin/my_account/my_account_repos.html:15
 #: kallithea/templates/admin/my_account/my_account_watched.html:15
 #: kallithea/templates/admin/repos/repos.html:41
-#: kallithea/templates/index_base.html:62
+#: kallithea/templates/index_base.html:63
 msgid "Tip"
 msgstr ""
 
@@ -1865,7 +1869,7 @@
 #: kallithea/templates/admin/repos/repos.html:42
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:8
 #: kallithea/templates/admin/user_groups/user_groups.html:42
-#: kallithea/templates/index_base.html:63
+#: kallithea/templates/index_base.html:64
 #: kallithea/templates/pullrequests/pullrequest_data.html:16
 #: kallithea/templates/pullrequests/pullrequest_show.html:124
 #: kallithea/templates/pullrequests/pullrequest_show.html:219
@@ -1889,7 +1893,7 @@
 #: kallithea/templates/admin/users/user_edit_profile.html:18
 #: kallithea/templates/admin/users/users.html:37
 #: kallithea/templates/base/base.html:364
-#: kallithea/templates/email_templates/registration.html:11
+#: kallithea/templates/email_templates/registration.html:12
 #: kallithea/templates/login.html:28 kallithea/templates/register.html:31
 msgid "Username"
 msgstr ""
@@ -2013,7 +2017,7 @@
 #: kallithea/templates/admin/settings/settings.html:31
 #: kallithea/templates/admin/users/user_add.html:62
 #: kallithea/templates/admin/users/user_edit_profile.html:25
-#: kallithea/templates/email_templates/registration.html:33
+#: kallithea/templates/email_templates/registration.html:34
 #: kallithea/templates/register.html:66
 msgid "Email"
 msgstr ""
@@ -2260,7 +2264,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/gists/index.html:51
-#: kallithea/templates/data_table/_dt_elements.html:78
+#: kallithea/templates/data_table/_dt_elements.html:84
 msgid "Created"
 msgstr ""
 
@@ -2344,13 +2348,13 @@
 #: kallithea/templates/admin/users/user_edit_ips.html:21
 #: kallithea/templates/changeset/changeset_file_comment.html:30
 #: kallithea/templates/changeset/changeset_file_comment.html:121
-#: kallithea/templates/data_table/_dt_elements.html:69
-#: kallithea/templates/data_table/_dt_elements.html:89
-#: kallithea/templates/data_table/_dt_elements.html:91
-#: kallithea/templates/data_table/_dt_elements.html:101
-#: kallithea/templates/data_table/_dt_elements.html:103
-#: kallithea/templates/data_table/_dt_elements.html:120
-#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:75
+#: kallithea/templates/data_table/_dt_elements.html:95
+#: kallithea/templates/data_table/_dt_elements.html:97
+#: kallithea/templates/data_table/_dt_elements.html:107
+#: kallithea/templates/data_table/_dt_elements.html:109
+#: kallithea/templates/data_table/_dt_elements.html:126
+#: kallithea/templates/data_table/_dt_elements.html:128
 #: kallithea/templates/files/files_source.html:35
 #: kallithea/templates/files/files_source.html:38
 #: kallithea/templates/files/files_source.html:41
@@ -2366,14 +2370,14 @@
 #: kallithea/templates/base/perms_summary.html:44
 #: kallithea/templates/base/perms_summary.html:81
 #: kallithea/templates/base/perms_summary.html:83
-#: kallithea/templates/data_table/_dt_elements.html:63
-#: kallithea/templates/data_table/_dt_elements.html:64
-#: kallithea/templates/data_table/_dt_elements.html:85
-#: kallithea/templates/data_table/_dt_elements.html:86
-#: kallithea/templates/data_table/_dt_elements.html:97
-#: kallithea/templates/data_table/_dt_elements.html:98
-#: kallithea/templates/data_table/_dt_elements.html:116
-#: kallithea/templates/data_table/_dt_elements.html:117
+#: kallithea/templates/data_table/_dt_elements.html:69
+#: kallithea/templates/data_table/_dt_elements.html:70
+#: kallithea/templates/data_table/_dt_elements.html:91
+#: kallithea/templates/data_table/_dt_elements.html:92
+#: kallithea/templates/data_table/_dt_elements.html:103
+#: kallithea/templates/data_table/_dt_elements.html:104
+#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:123
 #: kallithea/templates/files/diff_2way.html:56
 #: kallithea/templates/files/files_source.html:37
 #: kallithea/templates/files/files_source.html:40
@@ -2545,7 +2549,8 @@
 
 #: kallithea/templates/admin/my_account/my_account_password.html:39
 #, python-format
-msgid "This account is managed with %s and the password cannot be changed here"
+msgid ""
+"This account is managed with %s and the password cannot be changed here"
 msgstr ""
 
 #: kallithea/templates/admin/my_account/my_account_perms.html:3
@@ -2673,7 +2678,7 @@
 #: kallithea/templates/admin/permissions/permissions_globals.html:27
 #: kallithea/templates/admin/repos/repo_add_base.html:28
 #: kallithea/templates/admin/repos/repo_edit_settings.html:33
-#: kallithea/templates/data_table/_dt_elements.html:134
+#: kallithea/templates/data_table/_dt_elements.html:140
 #: kallithea/templates/forks/fork.html:42
 msgid "Repository group"
 msgstr ""
@@ -2694,7 +2699,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:40
-#: kallithea/templates/data_table/_dt_elements.html:141
+#: kallithea/templates/data_table/_dt_elements.html:147
 msgid "User group"
 msgstr ""
 
@@ -2718,7 +2723,8 @@
 msgstr ""
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:56
-msgid "Enable this to allow non-admins to create repositories at the top level."
+msgid ""
+"Enable this to allow non-admins to create repositories at the top level."
 msgstr ""
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:57
@@ -2782,7 +2788,8 @@
 #: kallithea/templates/admin/repo_groups/repo_group_edit.html:11
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:89
 #: kallithea/templates/admin/repo_groups/repo_groups.html:9
-#: kallithea/templates/base/base.html:57 kallithea/templates/base/base.html:76
+#: kallithea/templates/base/base.html:57
+#: kallithea/templates/base/base.html:76
 msgid "Repository Groups"
 msgstr ""
 
@@ -2822,7 +2829,8 @@
 #: kallithea/templates/admin/repos/repo_edit.html:25
 #: kallithea/templates/admin/settings/settings.html:11
 #: kallithea/templates/admin/user_groups/user_group_edit.html:29
-#: kallithea/templates/base/base.html:63 kallithea/templates/base/base.html:152
+#: kallithea/templates/base/base.html:63
+#: kallithea/templates/base/base.html:152
 msgid "Settings"
 msgstr ""
 
@@ -2864,7 +2872,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:121
+#: kallithea/templates/data_table/_dt_elements.html:127
 #, python-format
 msgid "Confirm to delete this group: %s with %s repository"
 msgid_plural "Confirm to delete this group: %s with %s repositories"
@@ -2969,7 +2977,8 @@
 #: kallithea/templates/admin/repos/repo_add_base.html:24
 #: kallithea/templates/admin/repos/repo_edit_settings.html:57
 #: kallithea/templates/forks/fork.html:37
-msgid "Keep it short and to the point. Use a README file for longer descriptions."
+msgid ""
+"Keep it short and to the point. Use a README file for longer descriptions."
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_add_base.html:31
@@ -3026,14 +3035,10 @@
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit.html:37
-msgid "Caches"
+msgid "Remote"
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit.html:40
-msgid "Remote"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit.html:43
 #: kallithea/templates/summary/statistics.html:8
 #: kallithea/templates/summary/summary.html:169
 #: kallithea/templates/summary/summary.html:170
@@ -3071,7 +3076,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:46
-#: kallithea/templates/data_table/_dt_elements.html:68
+#: kallithea/templates/data_table/_dt_elements.html:74
 #, python-format
 msgid "Confirm to delete this repository: %s"
 msgstr ""
@@ -3102,43 +3107,14 @@
 "it or restore it."
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:4
-msgid "Invalidate Repository Cache"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:6
-msgid ""
-"Manually invalidate cache for this repository. On first access, the "
-"repository will be cached again."
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:9
-msgid "List of Cached Values"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:12
-msgid "Prefix"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:13
+#: kallithea/templates/admin/repos/repo_edit_fields.html:6
+msgid "Label"
+msgstr ""
+
 #: kallithea/templates/admin/repos/repo_edit_fields.html:7
 msgid "Key"
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:14
-#: kallithea/templates/admin/user_groups/user_group_add.html:40
-#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
-#: kallithea/templates/admin/user_groups/user_groups.html:41
-#: kallithea/templates/admin/users/user_add.html:69
-#: kallithea/templates/admin/users/user_edit_profile.html:74
-#: kallithea/templates/admin/users/users.html:42
-msgid "Active"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_fields.html:6
-msgid "Label"
-msgstr ""
-
 #: kallithea/templates/admin/repos/repo_edit_fields.html:20
 #, python-format
 msgid "Confirm to delete this field: %s"
@@ -3371,8 +3347,8 @@
 
 #: kallithea/templates/admin/settings/settings_mapping.html:12
 msgid ""
-"Check this option to remove all comments, pull requests and other records"
-" related to repositories that no longer exist in the filesystem."
+"Check this option to remove all comments, pull requests and other records "
+"related to repositories that no longer exist in the filesystem."
 msgstr ""
 
 #: kallithea/templates/admin/settings/settings_mapping.html:17
@@ -3400,8 +3376,8 @@
 #: kallithea/templates/admin/settings/settings_mapping.html:35
 msgid ""
 "If installing Git hooks, overwrite any existing hooks, even if they do "
-"not seem to come from Kallithea. WARNING: This operation will destroy any"
-" custom git hooks you may have deployed by hand!"
+"not seem to come from Kallithea. WARNING: This operation will destroy any "
+"custom git hooks you may have deployed by hand!"
 msgstr ""
 
 #: kallithea/templates/admin/settings/settings_mapping.html:41
@@ -3490,8 +3466,8 @@
 
 #: kallithea/templates/admin/settings/settings_vcs.html:52
 msgid ""
-"Click to unlock. You must restart Kallithea in order to make this setting"
-" take effect."
+"Click to unlock. You must restart Kallithea in order to make this setting "
+"take effect."
 msgstr ""
 
 #: kallithea/templates/admin/settings/settings_vcs.html:56
@@ -3517,7 +3493,8 @@
 msgstr ""
 
 #: kallithea/templates/admin/settings/settings_visual.html:20
-msgid "Shows or hides a version number of Kallithea displayed in the footer."
+msgid ""
+"Shows or hides a version number of Kallithea displayed in the footer."
 msgstr ""
 
 #: kallithea/templates/admin/settings/settings_visual.html:25
@@ -3547,14 +3524,14 @@
 
 #: kallithea/templates/admin/settings/settings_visual.html:43
 msgid ""
-"Schema of clone URL construction eg. '{scheme}://{user}@{netloc}/{repo}'."
-"\n"
+"Schema of clone URL construction eg. '{scheme}://{user}@{netloc}/"
+"{repo}'.\n"
 "                                                    The following "
 "variables are available:\n"
 "                                                    {scheme} 'http' or "
 "'https' sent from running Kallithea server,\n"
-"                                                    {user}   current user"
-" username,\n"
+"                                                    {user}   current user "
+"username,\n"
 "                                                    {netloc} network "
 "location/server host of running Kallithea server,\n"
 "                                                    {repo}   full "
@@ -3574,8 +3551,8 @@
 
 #: kallithea/templates/admin/settings/settings_visual.html:59
 msgid ""
-"Schema for constructing SSH clone URL, eg. "
-"'ssh://{system_user}@{hostname}/{repo}'."
+"Schema for constructing SSH clone URL, eg. 'ssh://{system_user}"
+"@{hostname}/{repo}'."
 msgstr ""
 
 #: kallithea/templates/admin/settings/settings_visual.html:67
@@ -3635,7 +3612,8 @@
 #: kallithea/templates/admin/user_groups/user_group_add.html:10
 #: kallithea/templates/admin/user_groups/user_group_edit.html:11
 #: kallithea/templates/admin/user_groups/user_groups.html:9
-#: kallithea/templates/base/base.html:59 kallithea/templates/base/base.html:79
+#: kallithea/templates/base/base.html:59
+#: kallithea/templates/base/base.html:79
 msgid "User Groups"
 msgstr ""
 
@@ -3649,6 +3627,15 @@
 msgid "Short, optional description for this user group."
 msgstr ""
 
+#: kallithea/templates/admin/user_groups/user_group_add.html:40
+#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
+#: kallithea/templates/admin/user_groups/user_groups.html:41
+#: kallithea/templates/admin/users/user_add.html:69
+#: kallithea/templates/admin/users/user_edit_profile.html:74
+#: kallithea/templates/admin/users/users.html:42
+msgid "Active"
+msgstr ""
+
 #: kallithea/templates/admin/user_groups/user_group_edit.html:5
 #, python-format
 msgid "%s user group settings"
@@ -3670,7 +3657,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:19
-#: kallithea/templates/data_table/_dt_elements.html:102
+#: kallithea/templates/data_table/_dt_elements.html:108
 #, python-format
 msgid "Confirm to delete this user group: %s"
 msgstr ""
@@ -3744,7 +3731,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:90
+#: kallithea/templates/data_table/_dt_elements.html:96
 #, python-format
 msgid "Confirm to delete this user: %s"
 msgstr ""
@@ -3783,11 +3770,13 @@
 msgid "Support"
 msgstr ""
 
-#: kallithea/templates/base/base.html:86 kallithea/templates/base/base.html:417
+#: kallithea/templates/base/base.html:86
+#: kallithea/templates/base/base.html:417
 msgid "Mercurial repository"
 msgstr ""
 
-#: kallithea/templates/base/base.html:89 kallithea/templates/base/base.html:420
+#: kallithea/templates/base/base.html:89
+#: kallithea/templates/base/base.html:420
 msgid "Git repository"
 msgstr ""
 
@@ -3842,14 +3831,17 @@
 msgstr ""
 
 #: kallithea/templates/base/base.html:167
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Follow"
 msgstr ""
 
 #: kallithea/templates/base/base.html:168
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Unfollow"
 msgstr ""
 
-#: kallithea/templates/base/base.html:171 kallithea/templates/forks/fork.html:9
+#: kallithea/templates/base/base.html:171
+#: kallithea/templates/forks/fork.html:9
 msgid "Fork"
 msgstr ""
 
@@ -4248,23 +4240,23 @@
 msgid "Merge"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:96
+#: kallithea/templates/changeset/changeset.html:95
 msgid "Grafted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:102
+#: kallithea/templates/changeset/changeset.html:100
 msgid "Transplanted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:108
+#: kallithea/templates/changeset/changeset.html:106
 msgid "Replaced by:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:122
+#: kallithea/templates/changeset/changeset.html:120
 msgid "Preceded by:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:139
+#: kallithea/templates/changeset/changeset.html:137
 #: kallithea/templates/compare/compare_diff.html:59
 #: kallithea/templates/pullrequests/pullrequest_show.html:290
 #, python-format
@@ -4273,7 +4265,7 @@
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/templates/changeset/changeset.html:141
+#: kallithea/templates/changeset/changeset.html:139
 #: kallithea/templates/compare/compare_diff.html:61
 #: kallithea/templates/pullrequests/pullrequest_show.html:292
 #, python-format
@@ -4282,8 +4274,8 @@
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
 #: kallithea/templates/pullrequests/pullrequest_show.html:333
@@ -4512,23 +4504,23 @@
 msgid "Repository creation in progress..."
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:42
+#: kallithea/templates/data_table/_dt_elements.html:48
 msgid "No changesets yet"
 msgstr "Nach keng Ännerungen do"
 
-#: kallithea/templates/data_table/_dt_elements.html:48
-#: kallithea/templates/data_table/_dt_elements.html:50
+#: kallithea/templates/data_table/_dt_elements.html:54
+#: kallithea/templates/data_table/_dt_elements.html:56
 #, python-format
 msgid "Subscribe to %s rss feed"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:56
-#: kallithea/templates/data_table/_dt_elements.html:58
+#: kallithea/templates/data_table/_dt_elements.html:62
+#: kallithea/templates/data_table/_dt_elements.html:64
 #, python-format
 msgid "Subscribe to %s atom feed"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:76
+#: kallithea/templates/data_table/_dt_elements.html:82
 msgid "Creating"
 msgstr ""
 
@@ -4560,6 +4552,11 @@
 msgid "by"
 msgstr ""
 
+#: kallithea/templates/email_templates/changeset_comment.html:36
+#: kallithea/templates/email_templates/pull_request_comment.html:43
+msgid "View Comment"
+msgstr ""
+
 #: kallithea/templates/email_templates/comment.html:27
 msgid "Status change:"
 msgstr ""
@@ -4568,32 +4565,40 @@
 msgid "The pull request has been closed."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:9
+#: kallithea/templates/email_templates/default.html:4
+msgid "Message"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:4
+msgid "Password Reset Request"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:10
 #, python-format
 msgid "Hello %s"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:16
+#: kallithea/templates/email_templates/password_reset.html:17
 msgid "We have received a request to reset the password for your account."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:25
+#: kallithea/templates/email_templates/password_reset.html:26
 msgid ""
 "This account is however managed outside this system and the password "
 "cannot be changed here."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:28
+#: kallithea/templates/email_templates/password_reset.html:29
 msgid "To set a new password, click the following link"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:33
+#: kallithea/templates/email_templates/password_reset.html:34
 msgid ""
 "Should you not be able to use the link above, please type the following "
 "code into the password reset form"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:44
+#: kallithea/templates/email_templates/password_reset.html:45
 msgid ""
 "If it weren't you who requested the password reset, just disregard this "
 "message."
@@ -4624,6 +4629,10 @@
 msgid "to"
 msgstr ""
 
+#: kallithea/templates/email_templates/pull_request.html:85
+msgid "View Pull Request"
+msgstr ""
+
 #: kallithea/templates/email_templates/pull_request_comment.html:4
 #, python-format
 msgid "Mention in Comment on Pull Request %s \"%s\""
@@ -4639,10 +4648,18 @@
 msgid "Comment on Pull Request %s \"%s\""
 msgstr ""
 
-#: kallithea/templates/email_templates/registration.html:22
+#: kallithea/templates/email_templates/registration.html:5
+msgid "New User Registration"
+msgstr ""
+
+#: kallithea/templates/email_templates/registration.html:23
 msgid "Full Name"
 msgstr ""
 
+#: kallithea/templates/email_templates/registration.html:42
+msgid "View User Profile"
+msgstr ""
+
 #: kallithea/templates/files/diff_2way.html:15
 #, python-format
 msgid "%s File side-by-side diff"
@@ -5243,45 +5260,45 @@
 msgid "Stats gathered: "
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:87
-#: kallithea/templates/summary/summary.html:371
+#: kallithea/templates/summary/statistics.html:85
+#: kallithea/templates/summary/summary.html:369
 msgid "files"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:111
-#: kallithea/templates/summary/summary.html:401
+#: kallithea/templates/summary/statistics.html:109
+#: kallithea/templates/summary/summary.html:399
 msgid "Show more"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:405
+#: kallithea/templates/summary/statistics.html:395
 msgid "commits"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:406
+#: kallithea/templates/summary/statistics.html:396
 msgid "files added"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:407
+#: kallithea/templates/summary/statistics.html:397
 msgid "files changed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:408
+#: kallithea/templates/summary/statistics.html:398
 msgid "files removed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:410
+#: kallithea/templates/summary/statistics.html:400
 msgid "commit"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:411
+#: kallithea/templates/summary/statistics.html:401
 msgid "file added"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:412
+#: kallithea/templates/summary/statistics.html:402
 msgid "file changed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:413
+#: kallithea/templates/summary/statistics.html:403
 msgid "file removed"
 msgstr ""
 
--- a/kallithea/i18n/nb_NO/LC_MESSAGES/kallithea.po	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/i18n/nb_NO/LC_MESSAGES/kallithea.po	Mon May 04 19:24:04 2020 +0200
@@ -4,7 +4,7 @@
 msgstr ""
 "Project-Id-Version: Kallithea 0.3.99\n"
 "Report-Msgid-Bugs-To: translations@kallithea-scm.org\n"
-"POT-Creation-Date: 2020-01-05 04:10+0100\n"
+"POT-Creation-Date: 2020-05-04 19:12+0200\n"
 "PO-Revision-Date: 2019-04-30 22:25+0000\n"
 "Last-Translator: Allan Nordhøy <epost@anotheragency.no>\n"
 "Language-Team: Norwegian Bokmål <https://hosted.weblate.org/projects/"
@@ -17,14 +17,14 @@
 "X-Generator: Weblate 3.6.1\n"
 
 #: kallithea/controllers/changelog.py:67
-#: kallithea/controllers/pullrequests.py:250 kallithea/lib/base.py:602
+#: kallithea/controllers/pullrequests.py:247 kallithea/lib/base.py:602
 msgid "There are no changesets yet"
 msgstr "Ingen endringssett enda"
 
 #: kallithea/controllers/admin/permissions.py:64
 #: kallithea/controllers/admin/permissions.py:68
 #: kallithea/controllers/admin/permissions.py:72
-#: kallithea/controllers/changelog.py:137
+#: kallithea/controllers/changelog.py:136
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:7
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:88
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:7
@@ -33,38 +33,38 @@
 msgid "None"
 msgstr "Ingen"
 
-#: kallithea/controllers/changelog.py:140 kallithea/controllers/files.py:189
+#: kallithea/controllers/changelog.py:139 kallithea/controllers/files.py:189
 msgid "(closed)"
 msgstr "(lukket)"
 
-#: kallithea/controllers/changeset.py:81
+#: kallithea/controllers/changeset.py:82
 msgid "Show whitespace"
 msgstr "Vis blanktegn"
 
-#: kallithea/controllers/changeset.py:88
-#: kallithea/controllers/changeset.py:95
+#: kallithea/controllers/changeset.py:89
+#: kallithea/controllers/changeset.py:96
 #: kallithea/templates/files/diff_2way.html:55
 msgid "Ignore whitespace"
 msgstr "Ignorer blanktegn"
 
-#: kallithea/controllers/changeset.py:161
+#: kallithea/controllers/changeset.py:162
 #, python-format
 msgid "Increase diff context to %(num)s lines"
 msgstr "Øk diff-bindeleddsinformasjon til %(num)s linjer"
 
-#: kallithea/controllers/changeset.py:201
+#: kallithea/controllers/changeset.py:202
 #, fuzzy
 #| msgid "No permission to change pull request status"
 msgid "No permission to change status"
 msgstr "Ingen tilgang til endring av innsendingsforespørselsstatus"
 
-#: kallithea/controllers/changeset.py:212
+#: kallithea/controllers/changeset.py:213
 #, python-format
 msgid "Successfully deleted pull request %s"
 msgstr "Slettet flettingsforespørsel %s"
 
 #: kallithea/controllers/changeset.py:319 kallithea/controllers/files.py:89
-#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:700
+#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:697
 msgid "Such revision does not exist for this repository"
 msgstr "En slik revisjon funnes ikke for denne pakkebrønnen"
 
@@ -77,51 +77,51 @@
 msgid "Cannot compare repositories of different types"
 msgstr "Kan ikke sammenligne pakkebrønner av forskjellige typer"
 
-#: kallithea/controllers/compare.py:246
+#: kallithea/controllers/compare.py:247
 #, fuzzy
 msgid "Cannot show empty diff"
 msgstr "Kan ikke vise tom diff"
 
-#: kallithea/controllers/compare.py:248
+#: kallithea/controllers/compare.py:249
 msgid "No ancestor found for merge diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:252
+#: kallithea/controllers/compare.py:253
 msgid "Multiple merge ancestors found for merge compare"
 msgstr ""
 
-#: kallithea/controllers/compare.py:268
+#: kallithea/controllers/compare.py:269
 msgid "Cannot compare repositories without using common ancestor"
 msgstr "Kan ikke sammenligne pakkebrønner uten bruk av felles opphav"
 
-#: kallithea/controllers/error.py:71
+#: kallithea/controllers/error.py:67
 msgid "No response"
 msgstr "Ingen respons"
 
-#: kallithea/controllers/error.py:72
+#: kallithea/controllers/error.py:68
 msgid "Unknown error"
 msgstr "Ukjent feil"
 
-#: kallithea/controllers/error.py:85
+#: kallithea/controllers/error.py:81
 msgid ""
 "The request could not be understood by the server due to malformed syntax."
 msgstr ""
 "Forespørselen kunne ikke forstås av tjeneren som følge av feilaktig "
 "syntaks."
 
-#: kallithea/controllers/error.py:88
+#: kallithea/controllers/error.py:84
 msgid "Unauthorized access to resource"
 msgstr "Uautorisert tilgang til ressurs"
 
-#: kallithea/controllers/error.py:90
+#: kallithea/controllers/error.py:86
 msgid "You don't have permission to view this page"
 msgstr "Du har ikke tilgang til å se denne siden"
 
-#: kallithea/controllers/error.py:92
+#: kallithea/controllers/error.py:88
 msgid "The resource could not be found"
 msgstr "Kunne ikke finne ressursen"
 
-#: kallithea/controllers/error.py:94
+#: kallithea/controllers/error.py:90
 msgid ""
 "The server encountered an unexpected condition which prevented it from "
 "fulfilling the request."
@@ -129,14 +129,14 @@
 "Tjeneren støtte på en uventet tilstand som forhindret utøvelse av "
 "forespørsel."
 
-#: kallithea/controllers/feed.py:63
+#: kallithea/controllers/feed.py:59
 #, python-format
 msgid "%s committed on %s"
 msgstr "%s sendte inn %s"
 
-#: kallithea/controllers/feed.py:88
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/controllers/feed.py:84
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/compare/compare_diff.html:95
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
@@ -144,12 +144,12 @@
 msgid "Changeset was too big and was cut off..."
 msgstr "Endringsettet var for stort og har blitt avskåret…"
 
-#: kallithea/controllers/feed.py:111 kallithea/controllers/feed.py:140
+#: kallithea/controllers/feed.py:107
 #, fuzzy, python-format
 msgid "%s %s feed"
 msgstr "%s %s kilde"
 
-#: kallithea/controllers/feed.py:113 kallithea/controllers/feed.py:142
+#: kallithea/controllers/feed.py:109
 #, fuzzy, python-format
 msgid "Changes on %s repository"
 msgstr "Endringer i %s-pakkebrønn"
@@ -169,107 +169,107 @@
 msgid "%s at %s"
 msgstr "%s den %s"
 
-#: kallithea/controllers/files.py:296
+#: kallithea/controllers/files.py:295
 #, fuzzy
 msgid "You can only delete files with revision being a valid branch"
 msgstr ""
 "Du kan bare slette filer med en revisjon som er en gyldig forgrening"
 
-#: kallithea/controllers/files.py:307
+#: kallithea/controllers/files.py:306
 #, python-format
 msgid "Deleted file %s via Kallithea"
 msgstr "Slettet filen %s via Kallithea"
 
-#: kallithea/controllers/files.py:331
+#: kallithea/controllers/files.py:330
 #, python-format
 msgid "Successfully deleted file %s"
 msgstr "Filen %s ble slettet"
 
-#: kallithea/controllers/files.py:335 kallithea/controllers/files.py:394
-#: kallithea/controllers/files.py:469
+#: kallithea/controllers/files.py:334 kallithea/controllers/files.py:392
+#: kallithea/controllers/files.py:467
 msgid "Error occurred during commit"
 msgstr "Feil inntraff under innsendelse"
 
-#: kallithea/controllers/files.py:350
+#: kallithea/controllers/files.py:349
 msgid "You can only edit files with revision being a valid branch"
 msgstr ""
 "Du kan bare redigere filer med en revisjon som er en gyldig avgrening"
 
-#: kallithea/controllers/files.py:364
+#: kallithea/controllers/files.py:363
 #, python-format
 msgid "Edited file %s via Kallithea"
 msgstr "Filen %s ble endret via Kallithea"
 
-#: kallithea/controllers/files.py:380
+#: kallithea/controllers/files.py:378
 msgid "No changes"
 msgstr "Ingen endringer"
 
-#: kallithea/controllers/files.py:390 kallithea/controllers/files.py:458
+#: kallithea/controllers/files.py:388 kallithea/controllers/files.py:456
 #, python-format
 msgid "Successfully committed to %s"
 msgstr "Innsendt til %s"
 
-#: kallithea/controllers/files.py:409
+#: kallithea/controllers/files.py:407
 msgid "Added file via Kallithea"
 msgstr "Fil lagt til via Kallithea"
 
-#: kallithea/controllers/files.py:430
+#: kallithea/controllers/files.py:428
 msgid "No content"
 msgstr "Inget innhold"
 
-#: kallithea/controllers/files.py:434
+#: kallithea/controllers/files.py:432
 msgid "No filename"
 msgstr "Inget filnavn"
 
-#: kallithea/controllers/files.py:461
+#: kallithea/controllers/files.py:459
 msgid "Location must be relative path and must not contain .. in path"
 msgstr ""
 "Plasseringen må være en relativ sti, og kan ikke inneholde .. i stien"
 
-#: kallithea/controllers/files.py:493
+#: kallithea/controllers/files.py:491
 msgid "Downloads disabled"
 msgstr "Nedlastinger avskrudd"
 
-#: kallithea/controllers/files.py:504
+#: kallithea/controllers/files.py:502
 #, python-format
 msgid "Unknown revision %s"
 msgstr "Ukjent revisjon %s"
 
-#: kallithea/controllers/files.py:506
+#: kallithea/controllers/files.py:504
 msgid "Empty repository"
 msgstr "Tom pakkebrønn"
 
-#: kallithea/controllers/files.py:508
+#: kallithea/controllers/files.py:506
 msgid "Unknown archive type"
 msgstr "Ukjent arkivtype"
 
-#: kallithea/controllers/files.py:729
+#: kallithea/controllers/files.py:726
 #: kallithea/templates/changeset/changeset_range.html:9
 #: kallithea/templates/email_templates/pull_request.html:64
 #: kallithea/templates/pullrequests/pullrequest.html:84
 msgid "Changesets"
 msgstr "Endringssett"
 
-#: kallithea/controllers/files.py:730
-#: kallithea/controllers/pullrequests.py:182 kallithea/model/scm.py:676
+#: kallithea/controllers/files.py:727
+#: kallithea/controllers/pullrequests.py:174 kallithea/model/scm.py:663
 msgid "Branches"
 msgstr "Forgreninger"
 
-#: kallithea/controllers/files.py:731
-#: kallithea/controllers/pullrequests.py:183 kallithea/model/scm.py:687
+#: kallithea/controllers/files.py:728
+#: kallithea/controllers/pullrequests.py:175 kallithea/model/scm.py:674
 msgid "Tags"
 msgstr "Etiketter"
 
-#: kallithea/controllers/forks.py:174
+#: kallithea/controllers/forks.py:175
 #, python-format
 msgid "An error occurred during repository forking %s"
 msgstr "En uventet feil inntraff under forgrening av pakkebrønnen %s"
 
-#: kallithea/controllers/home.py:79
+#: kallithea/controllers/home.py:77
 msgid "Groups"
 msgstr "Grupper"
 
-#: kallithea/controllers/home.py:89
+#: kallithea/controllers/home.py:87
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:90
 #: kallithea/templates/admin/repos/repo_add.html:12
 #: kallithea/templates/admin/repos/repo_add.html:16
@@ -281,7 +281,7 @@
 msgid "Repositories"
 msgstr "Pakkebrønner"
 
-#: kallithea/controllers/home.py:122
+#: kallithea/controllers/home.py:119
 #: kallithea/templates/files/files_add.html:32
 #: kallithea/templates/files/files_delete.html:23
 #: kallithea/templates/files/files_edit.html:32
@@ -289,42 +289,48 @@
 msgid "Branch"
 msgstr "Forgrening"
 
-#: kallithea/controllers/home.py:128
+#: kallithea/controllers/home.py:125
 msgid "Closed Branches"
 msgstr "Lukkede forgreninger"
 
-#: kallithea/controllers/home.py:134
+#: kallithea/controllers/home.py:131
 msgid "Tag"
 msgstr "Etikett"
 
-#: kallithea/controllers/home.py:140
+#: kallithea/controllers/home.py:137
 msgid "Bookmark"
 msgstr "Bokmerke"
 
-#: kallithea/controllers/journal.py:112 kallithea/controllers/journal.py:154
+#: kallithea/controllers/journal.py:146 kallithea/controllers/journal.py:157
 #: kallithea/templates/journal/public_journal.html:4
 #: kallithea/templates/journal/public_journal.html:18
 msgid "Public Journal"
 msgstr "Offentlig loggbok"
 
-#: kallithea/controllers/journal.py:116 kallithea/controllers/journal.py:158
+#: kallithea/controllers/journal.py:150 kallithea/controllers/journal.py:161
 #: kallithea/templates/base/base.html:290
 #: kallithea/templates/journal/journal.html:5
 #: kallithea/templates/journal/journal.html:13
 msgid "Journal"
 msgstr "Loggbok"
 
-#: kallithea/controllers/login.py:140 kallithea/controllers/login.py:185
+#: kallithea/controllers/login.py:109
+#, fuzzy
+#| msgid "Authentication"
+msgid "Authentication failed."
+msgstr "Identitetsbekreftelse"
+
+#: kallithea/controllers/login.py:142 kallithea/controllers/login.py:187
 #, fuzzy
 msgid "Bad captcha"
 msgstr "Feilaktig CAPTCHA"
 
-#: kallithea/controllers/login.py:146
+#: kallithea/controllers/login.py:148
 #, fuzzy, python-format
 msgid "You have successfully registered with %s"
 msgstr "Du har registrer deg på %s"
 
-#: kallithea/controllers/login.py:190
+#: kallithea/controllers/login.py:192
 msgid "A password reset confirmation code has been sent"
 msgstr "Passordbekreftelseskode sendt"
 
@@ -337,228 +343,228 @@
 msgid "Successfully updated password"
 msgstr "Passord oppdatert"
 
-#: kallithea/controllers/pullrequests.py:68
+#: kallithea/controllers/pullrequests.py:67
 #, fuzzy, python-format
 msgid "Invalid reviewer \"%s\" specified"
 msgstr "Ugyldig analytiker \"%s\" angitt"
 
-#: kallithea/controllers/pullrequests.py:131
+#: kallithea/controllers/pullrequests.py:123
 #, python-format
 msgid "%s (closed)"
 msgstr "%s (lukket)"
 
-#: kallithea/controllers/pullrequests.py:158
+#: kallithea/controllers/pullrequests.py:150
 #: kallithea/templates/changeset/changeset.html:12
 msgid "Changeset"
 msgstr "Endringssett"
 
-#: kallithea/controllers/pullrequests.py:179
+#: kallithea/controllers/pullrequests.py:171
 #, fuzzy
 msgid "Special"
 msgstr "Spesiell"
 
-#: kallithea/controllers/pullrequests.py:180
+#: kallithea/controllers/pullrequests.py:172
 msgid "Peer branches"
 msgstr "Likemennsforgreninger"
 
-#: kallithea/controllers/pullrequests.py:181 kallithea/model/scm.py:682
+#: kallithea/controllers/pullrequests.py:173 kallithea/model/scm.py:669
 msgid "Bookmarks"
 msgstr "Bokmerker"
 
-#: kallithea/controllers/pullrequests.py:318
+#: kallithea/controllers/pullrequests.py:315
 #, python-format
 msgid "Error creating pull request: %s"
 msgstr "Feil ved opprettelse av ny innsendelsesforespørsel: %s"
 
-#: kallithea/controllers/pullrequests.py:345
-#: kallithea/controllers/pullrequests.py:368
+#: kallithea/controllers/pullrequests.py:342
+#: kallithea/controllers/pullrequests.py:365
 msgid "Error occurred while creating pull request"
 msgstr "Feil inntraff under opprettelse av innsendelsesforespørsel"
 
-#: kallithea/controllers/pullrequests.py:350
+#: kallithea/controllers/pullrequests.py:347
 msgid "Successfully opened new pull request"
 msgstr "Åpnet en ny innsendelsesforespørsel"
 
-#: kallithea/controllers/pullrequests.py:373
+#: kallithea/controllers/pullrequests.py:370
 msgid "New pull request iteration created"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:401
+#: kallithea/controllers/pullrequests.py:398
 #, python-format
 msgid "Meanwhile, the following reviewers have been added: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:405
+#: kallithea/controllers/pullrequests.py:402
 #, python-format
 msgid "Meanwhile, the following reviewers have been removed: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:421
-#: kallithea/model/pull_request.py:232
+#: kallithea/controllers/pullrequests.py:418
+#: kallithea/model/pull_request.py:230
 msgid "No description"
 msgstr "Ingen beskrivelse"
 
-#: kallithea/controllers/pullrequests.py:430
+#: kallithea/controllers/pullrequests.py:427
 msgid "Pull request updated"
 msgstr "Innsendingsforespørsel oppdatert"
 
-#: kallithea/controllers/pullrequests.py:443
+#: kallithea/controllers/pullrequests.py:440
 msgid "Successfully deleted pull request"
 msgstr "Slettet innsendingsforespørsel"
 
-#: kallithea/controllers/pullrequests.py:479
+#: kallithea/controllers/pullrequests.py:476
 #, python-format
 msgid "Revision %s not found in %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:506
+#: kallithea/controllers/pullrequests.py:504
 #, python-format
 msgid "Error: changesets not found when displaying pull request from %s."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:520
+#: kallithea/controllers/pullrequests.py:518
 #, python-format
 msgid "This pull request has already been merged to %s."
 msgstr "Denne innsendingsforespørselen har allerede blitt flettet inn i %s."
 
-#: kallithea/controllers/pullrequests.py:522
+#: kallithea/controllers/pullrequests.py:520
 msgid "This pull request has been closed and can not be updated."
 msgstr ""
 "Denne innsendingsforespørselen har blitt lukket, og kan ikke oppdateres."
 
-#: kallithea/controllers/pullrequests.py:546
+#: kallithea/controllers/pullrequests.py:539
 #, python-format
 msgid "The following additional changes are available on %s:"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:548
-#: kallithea/controllers/pullrequests.py:552
+#: kallithea/controllers/pullrequests.py:541
+#: kallithea/controllers/pullrequests.py:545
 msgid "No additional changesets found for iterating on this pull request."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:560
+#: kallithea/controllers/pullrequests.py:553
 #, python-format
 msgid "Note: Branch %s has another head: %s."
 msgstr "Merk: Forgreningen %s har et annet hode: %s."
 
-#: kallithea/controllers/pullrequests.py:567
+#: kallithea/controllers/pullrequests.py:560
 msgid "Git pull requests don't support iterating yet."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:569
+#: kallithea/controllers/pullrequests.py:562
 #, python-format
 msgid ""
 "Error: some changesets not found when displaying pull request from %s."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:593
+#: kallithea/controllers/pullrequests.py:586
 msgid "The diff can't be shown - the PR revisions could not be found."
 msgstr ""
 
+#: kallithea/controllers/search.py:132
+msgid "Invalid search query. Try quoting it."
+msgstr "Ugyldig søkespørring. Prøv å sette den i sistattegn."
+
 #: kallithea/controllers/search.py:136
-msgid "Invalid search query. Try quoting it."
-msgstr "Ugyldig søkespørring. Prøv å sette den i sistattegn."
-
-#: kallithea/controllers/search.py:140
 msgid "The server has no search index."
 msgstr "Tjeneren har ingen søkeindeks."
 
-#: kallithea/controllers/search.py:143
+#: kallithea/controllers/search.py:139
 msgid "An error occurred during search operation."
 msgstr "Feil inntraff under søkeoperasjon."
 
-#: kallithea/controllers/summary.py:168
-#: kallithea/templates/summary/summary.html:412
+#: kallithea/controllers/summary.py:169
+#: kallithea/templates/summary/summary.html:410
 msgid "No data ready yet"
 msgstr "Ingen data klar enda"
 
-#: kallithea/controllers/summary.py:171
+#: kallithea/controllers/summary.py:172
 #: kallithea/templates/summary/summary.html:97
 msgid "Statistics are disabled for this repository"
 msgstr "Statistikk er avskrudd for denne pakkebrønnen"
 
-#: kallithea/controllers/admin/auth_settings.py:137
+#: kallithea/controllers/admin/auth_settings.py:136
 msgid "Auth settings updated successfully"
 msgstr "Autentiseringsinnstillinger oppdatert"
 
-#: kallithea/controllers/admin/auth_settings.py:148
+#: kallithea/controllers/admin/auth_settings.py:147
 msgid "error occurred during update of auth settings"
 msgstr "feil inntraff under oppdatering av autentiseringsinnstillinger"
 
-#: kallithea/controllers/admin/defaults.py:75
+#: kallithea/controllers/admin/defaults.py:74
 msgid "Default settings updated successfully"
 msgstr "Forvalgte innstillinger oppdatert"
 
-#: kallithea/controllers/admin/defaults.py:90
+#: kallithea/controllers/admin/defaults.py:89
 msgid "Error occurred during update of defaults"
 msgstr "Feil inntraff under oppdatering av forvalg"
 
 #: kallithea/controllers/admin/gists.py:59
 #: kallithea/controllers/admin/my_account.py:232
-#: kallithea/controllers/admin/users.py:248
+#: kallithea/controllers/admin/users.py:246
 msgid "Forever"
 msgstr "For alltid"
 
 #: kallithea/controllers/admin/gists.py:60
 #: kallithea/controllers/admin/my_account.py:233
-#: kallithea/controllers/admin/users.py:249
+#: kallithea/controllers/admin/users.py:247
 msgid "5 minutes"
 msgstr "Fem minutter"
 
 #: kallithea/controllers/admin/gists.py:61
 #: kallithea/controllers/admin/my_account.py:234
-#: kallithea/controllers/admin/users.py:250
+#: kallithea/controllers/admin/users.py:248
 msgid "1 hour"
 msgstr "Én time"
 
 #: kallithea/controllers/admin/gists.py:62
 #: kallithea/controllers/admin/my_account.py:235
-#: kallithea/controllers/admin/users.py:251
+#: kallithea/controllers/admin/users.py:249
 msgid "1 day"
 msgstr "Én dag"
 
 #: kallithea/controllers/admin/gists.py:63
 #: kallithea/controllers/admin/my_account.py:236
-#: kallithea/controllers/admin/users.py:252
+#: kallithea/controllers/admin/users.py:250
 msgid "1 month"
 msgstr "Én måned"
 
 #: kallithea/controllers/admin/gists.py:67
 #: kallithea/controllers/admin/my_account.py:238
-#: kallithea/controllers/admin/users.py:254
+#: kallithea/controllers/admin/users.py:252
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:65
 #: kallithea/templates/admin/users/user_edit_api_keys.html:65
 msgid "Lifetime"
 msgstr "Livstid"
 
-#: kallithea/controllers/admin/gists.py:142
+#: kallithea/controllers/admin/gists.py:148
 msgid "Error occurred during gist creation"
 msgstr "Feil inntraff under gist-opprettelse"
 
-#: kallithea/controllers/admin/gists.py:158
+#: kallithea/controllers/admin/gists.py:164
 #, python-format
 msgid "Deleted gist %s"
 msgstr "Slettet gist-en %s"
 
-#: kallithea/controllers/admin/gists.py:198
+#: kallithea/controllers/admin/gists.py:207
 msgid "Unmodified"
 msgstr "Uendret"
 
-#: kallithea/controllers/admin/gists.py:228
+#: kallithea/controllers/admin/gists.py:237
 msgid "Successfully updated gist content"
 msgstr "Oppdaterte gist-innhold"
 
-#: kallithea/controllers/admin/gists.py:233
+#: kallithea/controllers/admin/gists.py:242
 msgid "Successfully updated gist data"
 msgstr "Oppdaterte gist-data"
 
-#: kallithea/controllers/admin/gists.py:236
+#: kallithea/controllers/admin/gists.py:245
 #, fuzzy, python-format
 msgid "Error occurred during update of gist %s"
 msgstr "Feil inntraff under oppdatering av gist-en %s"
 
-#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:211
-#: kallithea/model/user.py:232
+#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:205
+#: kallithea/model/user.py:226
 msgid "You can't edit this user since it's crucial for entire application"
 msgstr ""
 "Du kan ikke endre denne brukeren siden den er avgjørende for hele "
@@ -569,7 +575,7 @@
 msgstr "Kontoen din ble oppdatert"
 
 #: kallithea/controllers/admin/my_account.py:134
-#: kallithea/controllers/admin/users.py:181
+#: kallithea/controllers/admin/users.py:179
 #, fuzzy, python-format
 msgid "Error occurred during update of user %s"
 msgstr "Feil inntraff under oppdatering av brukeren %s"
@@ -579,47 +585,47 @@
 msgstr "Feil inntraff under oppdatering av brukerpassord"
 
 #: kallithea/controllers/admin/my_account.py:209
-#: kallithea/controllers/admin/users.py:367
+#: kallithea/controllers/admin/users.py:365
 #, fuzzy, python-format
 msgid "Added email %s to user"
 msgstr "La til e-postadressen %s for bruker"
 
 #: kallithea/controllers/admin/my_account.py:215
-#: kallithea/controllers/admin/users.py:373
+#: kallithea/controllers/admin/users.py:371
 #, fuzzy
 msgid "An error occurred during email saving"
 msgstr "Feil inntraff under lagring av e-postadresse"
 
 #: kallithea/controllers/admin/my_account.py:224
-#: kallithea/controllers/admin/users.py:383
+#: kallithea/controllers/admin/users.py:381
 #, fuzzy
 msgid "Removed email from user"
 msgstr "Fjernet e-postadresse fra bruker"
 
 #: kallithea/controllers/admin/my_account.py:248
-#: kallithea/controllers/admin/users.py:271
+#: kallithea/controllers/admin/users.py:269
 msgid "API key successfully created"
 msgstr "API-nøkkel opprettet"
 
 #: kallithea/controllers/admin/my_account.py:257
-#: kallithea/controllers/admin/users.py:281
+#: kallithea/controllers/admin/users.py:279
 msgid "API key successfully reset"
 msgstr "API-nøkkel tilbakestilt"
 
 #: kallithea/controllers/admin/my_account.py:261
-#: kallithea/controllers/admin/users.py:285
+#: kallithea/controllers/admin/users.py:283
 msgid "API key successfully deleted"
 msgstr "API-nøkkel slettet"
 
 #: kallithea/controllers/admin/my_account.py:281
-#: kallithea/controllers/admin/users.py:456
+#: kallithea/controllers/admin/users.py:454
 #, fuzzy, python-format
 #| msgid "API key successfully created"
 msgid "SSH key %s successfully added"
 msgstr "API-nøkkel opprettet"
 
 #: kallithea/controllers/admin/my_account.py:293
-#: kallithea/controllers/admin/users.py:470
+#: kallithea/controllers/admin/users.py:468
 #, fuzzy
 #| msgid "API key successfully deleted"
 msgid "SSH key successfully deleted"
@@ -697,11 +703,11 @@
 msgid "Allowed with automatic account activation"
 msgstr "Tillatt med automatisk kontoaktivering"
 
-#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1673
+#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1578
 msgid "Manual activation of external account"
 msgstr "Manuell aktivering av ekstern konto"
 
-#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1674
+#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1579
 msgid "Automatic activation of external account"
 msgstr "Automatisk aktivering av ekstern konto"
 
@@ -724,346 +730,338 @@
 msgid "Error occurred during update of permissions"
 msgstr "Feil inntraff under oppdatering av tilganger"
 
-#: kallithea/controllers/admin/repo_groups.py:172
+#: kallithea/controllers/admin/repo_groups.py:165
 #, python-format
 msgid "Error occurred during creation of repository group %s"
 msgstr "Feil inntraff under opprettelse av pakkebrønnsgruppen %s"
 
-#: kallithea/controllers/admin/repo_groups.py:177
+#: kallithea/controllers/admin/repo_groups.py:172
 #, python-format
 msgid "Created repository group %s"
 msgstr "Opprettet pakkebrønnsgruppen %s"
 
-#: kallithea/controllers/admin/repo_groups.py:224
+#: kallithea/controllers/admin/repo_groups.py:219
 #, python-format
 msgid "Updated repository group %s"
 msgstr "Oppdaterte pakkebrønnsgruppen %s"
 
-#: kallithea/controllers/admin/repo_groups.py:240
+#: kallithea/controllers/admin/repo_groups.py:235
 #, python-format
 msgid "Error occurred during update of repository group %s"
 msgstr "Feil inntraff under oppdatering av pakkebrønnsgruppen %s"
 
-#: kallithea/controllers/admin/repo_groups.py:250
+#: kallithea/controllers/admin/repo_groups.py:245
 #, python-format
 msgid "This group contains %s repositories and cannot be deleted"
 msgstr "Denne gruppen inneholder %s pakkebrønner og kan ikke slettes"
 
-#: kallithea/controllers/admin/repo_groups.py:257
+#: kallithea/controllers/admin/repo_groups.py:252
 #, python-format
 msgid "This group contains %s subgroups and cannot be deleted"
 msgstr "Denne grunnen inneholder %s undergrupper og kan ikke slettes"
 
-#: kallithea/controllers/admin/repo_groups.py:263
+#: kallithea/controllers/admin/repo_groups.py:258
 #, python-format
 msgid "Removed repository group %s"
 msgstr "Fjernet pakkebrønnsgruppen %s"
 
-#: kallithea/controllers/admin/repo_groups.py:268
+#: kallithea/controllers/admin/repo_groups.py:263
 #, python-format
 msgid "Error occurred during deletion of repository group %s"
 msgstr "Feil inntraff under sletting av pakkebrønnsgruppen %s"
 
-#: kallithea/controllers/admin/repo_groups.py:352
-#: kallithea/controllers/admin/repo_groups.py:382
-#: kallithea/controllers/admin/user_groups.py:294
+#: kallithea/controllers/admin/repo_groups.py:347
+#: kallithea/controllers/admin/repo_groups.py:377
+#: kallithea/controllers/admin/user_groups.py:290
 #, fuzzy
 msgid "Cannot revoke permission for yourself as admin"
 msgstr "Kan ikke tilbakekalle egen administratortilgang"
 
-#: kallithea/controllers/admin/repo_groups.py:367
+#: kallithea/controllers/admin/repo_groups.py:362
 msgid "Repository group permissions updated"
 msgstr "Pakkebrønnsgruppetilganger oppdatert"
 
-#: kallithea/controllers/admin/repo_groups.py:399
-#: kallithea/controllers/admin/repos.py:358
-#: kallithea/controllers/admin/user_groups.py:306
+#: kallithea/controllers/admin/repo_groups.py:394
+#: kallithea/controllers/admin/repos.py:357
+#: kallithea/controllers/admin/user_groups.py:302
 msgid "An error occurred during revoking of permission"
 msgstr "En feil inntraff under tilbakekalling av tilgang"
 
-#: kallithea/controllers/admin/repos.py:136
+#: kallithea/controllers/admin/repos.py:137
 #, python-format
 msgid "Error creating repository %s"
 msgstr "Feil under opprettelse av pakkebrønnen %s"
 
-#: kallithea/controllers/admin/repos.py:194
+#: kallithea/controllers/admin/repos.py:193
 #, python-format
 msgid "Created repository %s from %s"
 msgstr "Opprettet pakkebrønnen %s fra %s"
 
-#: kallithea/controllers/admin/repos.py:203
+#: kallithea/controllers/admin/repos.py:202
 #, python-format
 msgid "Forked repository %s as %s"
 msgstr "Forgrenet pakkebrønnen %s som %s"
 
-#: kallithea/controllers/admin/repos.py:206
+#: kallithea/controllers/admin/repos.py:205
 #, python-format
 msgid "Created repository %s"
 msgstr "Opprettet pakkebrønnen %s"
 
-#: kallithea/controllers/admin/repos.py:235
+#: kallithea/controllers/admin/repos.py:234
 #, python-format
 msgid "Repository %s updated successfully"
 msgstr "Pakkebrønnen %s ble oppdatert"
 
-#: kallithea/controllers/admin/repos.py:255
+#: kallithea/controllers/admin/repos.py:254
 #, python-format
 msgid "Error occurred during update of repository %s"
 msgstr "Feil under oppdatering av pakkebrønnen %s"
 
-#: kallithea/controllers/admin/repos.py:273
+#: kallithea/controllers/admin/repos.py:272
 #, python-format
 msgid "Detached %s forks"
 msgstr "Avhektet %s forgreninger"
 
-#: kallithea/controllers/admin/repos.py:276
+#: kallithea/controllers/admin/repos.py:275
 #, python-format
 msgid "Deleted %s forks"
 msgstr "Slettet %s forgreninger"
 
-#: kallithea/controllers/admin/repos.py:281
+#: kallithea/controllers/admin/repos.py:280
 #, python-format
 msgid "Deleted repository %s"
 msgstr "Slettet pakkebrønnen %s"
 
-#: kallithea/controllers/admin/repos.py:284
+#: kallithea/controllers/admin/repos.py:283
 #, python-format
 msgid "Cannot delete repository %s which still has forks"
 msgstr "Kan ikke slette pakkebrønne %s, som fremdeles har forgreninger"
 
-#: kallithea/controllers/admin/repos.py:289
+#: kallithea/controllers/admin/repos.py:288
 #, python-format
 msgid "An error occurred during deletion of %s"
 msgstr "En feil inntraff under sletting av %s"
 
-#: kallithea/controllers/admin/repos.py:329
+#: kallithea/controllers/admin/repos.py:328
 msgid "Repository permissions updated"
 msgstr "Pakkebrønnstilganger oppdatert"
 
-#: kallithea/controllers/admin/repos.py:388
+#: kallithea/controllers/admin/repos.py:387
 #, python-format
 msgid "Field validation error: %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:391
+#: kallithea/controllers/admin/repos.py:390
 #, python-format
 msgid "An error occurred during creation of field: %r"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:402
+#: kallithea/controllers/admin/repos.py:401
 msgid "An error occurred during removal of field"
 msgstr "Feil inntraff under fjerning av felt"
 
-#: kallithea/controllers/admin/repos.py:416
+#: kallithea/controllers/admin/repos.py:415
 msgid "-- Not a fork --"
 msgstr "-- Ikke en forgrening --"
 
-#: kallithea/controllers/admin/repos.py:448
+#: kallithea/controllers/admin/repos.py:447
 msgid "Updated repository visibility in public journal"
 msgstr "Oppdaterte pakkebrønnssynlighet i offentlig loggbok"
 
-#: kallithea/controllers/admin/repos.py:452
+#: kallithea/controllers/admin/repos.py:451
 #, fuzzy
 msgid "An error occurred during setting this repository in public journal"
 msgstr ""
 "En feil inntraff under innlemmelse av denne pakkebrønnen i offentlig "
 "loggbok"
 
-#: kallithea/controllers/admin/repos.py:468
+#: kallithea/controllers/admin/repos.py:467
 msgid "Nothing"
 msgstr "Ingenting"
 
-#: kallithea/controllers/admin/repos.py:470
+#: kallithea/controllers/admin/repos.py:469
 #, python-format
 msgid "Marked repository %s as fork of %s"
 msgstr "Markerte pakkebrønnen %s som en forgrening av %s"
 
-#: kallithea/controllers/admin/repos.py:477
+#: kallithea/controllers/admin/repos.py:476
 msgid "An error occurred during this operation"
 msgstr "En feil inntraff under denne operasjonen"
 
-#: kallithea/controllers/admin/repos.py:490
-msgid "Cache invalidation successful"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:494
-msgid "An error occurred during cache invalidation"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:507
+#: kallithea/controllers/admin/repos.py:488
 msgid "Pulled from remote location"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:510
+#: kallithea/controllers/admin/repos.py:491
 msgid "An error occurred during pull from remote location"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:541
+#: kallithea/controllers/admin/repos.py:522
 msgid "An error occurred during deletion of repository stats"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:131
+#: kallithea/controllers/admin/settings.py:132
 msgid "Updated VCS settings"
 msgstr "Oppdaterte VCS-innstillinger"
 
-#: kallithea/controllers/admin/settings.py:135 kallithea/lib/utils.py:237
+#: kallithea/controllers/admin/settings.py:136
 msgid ""
 "Unable to activate hgsubversion support. The \"hgsubversion\" library is "
 "missing"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:141
-#: kallithea/controllers/admin/settings.py:233
+#: kallithea/controllers/admin/settings.py:142
+#: kallithea/controllers/admin/settings.py:234
 msgid "Error occurred while updating application settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:176
+#: kallithea/controllers/admin/settings.py:177
 #, python-format
 msgid "Repositories successfully rescanned. Added: %s. Removed: %s."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:188
+#: kallithea/controllers/admin/settings.py:189
 #, python-format
 msgid "Invalidated %s repositories"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:229
+#: kallithea/controllers/admin/settings.py:230
 msgid "Updated application settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:283
+#: kallithea/controllers/admin/settings.py:284
 msgid "Updated visualisation settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:288
+#: kallithea/controllers/admin/settings.py:289
 msgid "Error occurred during updating visualisation settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:312
+#: kallithea/controllers/admin/settings.py:313
 msgid "Please enter email address"
 msgstr "Skriv inn e-postadresse"
 
-#: kallithea/controllers/admin/settings.py:327
+#: kallithea/controllers/admin/settings.py:328
 msgid "Send email task created"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:355
+#: kallithea/controllers/admin/settings.py:356
 #, fuzzy
 #| msgid "No data ready yet"
 msgid "Hook already exists"
 msgstr "Ingen data klar enda"
 
-#: kallithea/controllers/admin/settings.py:357
+#: kallithea/controllers/admin/settings.py:358
 msgid "Builtin hooks are read-only. Please use another hook name."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:360
+#: kallithea/controllers/admin/settings.py:361
 msgid "Added new hook"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:376
+#: kallithea/controllers/admin/settings.py:377
 msgid "Updated hooks"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:380
+#: kallithea/controllers/admin/settings.py:381
 msgid "Error occurred during hook creation"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:404
+#: kallithea/controllers/admin/settings.py:405
 msgid "Whoosh reindex task scheduled"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:138
+#: kallithea/controllers/admin/user_groups.py:134
 #, python-format
 msgid "Created user group %s"
 msgstr "Opprettet brukergruppe %s"
 
-#: kallithea/controllers/admin/user_groups.py:151
+#: kallithea/controllers/admin/user_groups.py:147
 #, python-format
 msgid "Error occurred during creation of user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:179
+#: kallithea/controllers/admin/user_groups.py:175
 #, python-format
 msgid "Updated user group %s"
 msgstr "Oppdaterte brukergruppe %s"
 
-#: kallithea/controllers/admin/user_groups.py:201
+#: kallithea/controllers/admin/user_groups.py:197
 #, python-format
 msgid "Error occurred during update of user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:212
+#: kallithea/controllers/admin/user_groups.py:208
 #, fuzzy
 msgid "Successfully deleted user group"
 msgstr "Brukergruppe slettet"
 
-#: kallithea/controllers/admin/user_groups.py:217
+#: kallithea/controllers/admin/user_groups.py:213
 msgid "An error occurred during deletion of user group"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:273
+#: kallithea/controllers/admin/user_groups.py:269
 msgid "Target group cannot be the same"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:279
+#: kallithea/controllers/admin/user_groups.py:275
 msgid "User group permissions updated"
 msgstr ""
 
+#: kallithea/controllers/admin/user_groups.py:384
+#: kallithea/controllers/admin/users.py:336
+msgid "Updated permissions"
+msgstr ""
+
 #: kallithea/controllers/admin/user_groups.py:388
-#: kallithea/controllers/admin/users.py:338
-msgid "Updated permissions"
-msgstr ""
-
-#: kallithea/controllers/admin/user_groups.py:392
-#: kallithea/controllers/admin/users.py:342
+#: kallithea/controllers/admin/users.py:340
 msgid "An error occurred during permissions saving"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:123
+#: kallithea/controllers/admin/users.py:121
 #, python-format
 msgid "Created user %s"
 msgstr "Opprettet brukeren %s"
 
-#: kallithea/controllers/admin/users.py:138
+#: kallithea/controllers/admin/users.py:136
 #, python-format
 msgid "Error occurred during creation of user %s"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:162
+#: kallithea/controllers/admin/users.py:160
 msgid "User updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:190
+#: kallithea/controllers/admin/users.py:188
 msgid "Successfully deleted user"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:195
+#: kallithea/controllers/admin/users.py:193
 msgid "An error occurred during deletion of user"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:203
+#: kallithea/controllers/admin/users.py:201
 msgid "The default user cannot be edited"
 msgstr "Forvalgt bruker kan ikke redigeres"
 
-#: kallithea/controllers/admin/users.py:409
+#: kallithea/controllers/admin/users.py:407
 #, python-format
 msgid "Added IP address %s to user whitelist"
 msgstr "La til IP-adressen %s i brukerhvitlisten"
 
-#: kallithea/controllers/admin/users.py:415
+#: kallithea/controllers/admin/users.py:413
 msgid "An error occurred while adding IP address"
 msgstr "Kunne ikke legge til IP-adresse"
 
-#: kallithea/controllers/admin/users.py:427
+#: kallithea/controllers/admin/users.py:425
 msgid "Removed IP address from user whitelist"
 msgstr "Fjernet IP-adressen fra brukerhvitlisten"
 
-#: kallithea/lib/auth.py:684
+#: kallithea/lib/auth.py:634
 msgid "You need to be a registered user to perform this action"
 msgstr ""
 
-#: kallithea/lib/auth.py:712
+#: kallithea/lib/auth.py:662
 msgid "You need to be signed in to view this page"
 msgstr ""
 
@@ -1094,178 +1092,178 @@
 "Changeset was too big and was cut off, use diff menu to display this diff"
 msgstr ""
 
-#: kallithea/lib/diffs.py:224
+#: kallithea/lib/diffs.py:223
 msgid "No changes detected"
 msgstr ""
 
-#: kallithea/lib/helpers.py:653
+#: kallithea/lib/helpers.py:670
 #, python-format
 msgid "Deleted branch: %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:655
+#: kallithea/lib/helpers.py:672
 #, python-format
 msgid "Created tag: %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:666
+#: kallithea/lib/helpers.py:683
 #, python-format
 msgid "Changeset %s not found"
 msgstr ""
 
-#: kallithea/lib/helpers.py:715
+#: kallithea/lib/helpers.py:732
 #, python-format
 msgid "Show all combined changesets %s->%s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:721
+#: kallithea/lib/helpers.py:738
 msgid "Compare view"
 msgstr ""
 
-#: kallithea/lib/helpers.py:740
+#: kallithea/lib/helpers.py:757
 msgid "and"
 msgstr ""
 
-#: kallithea/lib/helpers.py:741
+#: kallithea/lib/helpers.py:758
 #, python-format
 msgid "%s more"
 msgstr ""
 
-#: kallithea/lib/helpers.py:742
+#: kallithea/lib/helpers.py:759
 #: kallithea/templates/changelog/changelog.html:43
 msgid "revisions"
 msgstr ""
 
-#: kallithea/lib/helpers.py:766
+#: kallithea/lib/helpers.py:783
 #, python-format
 msgid "Fork name %s"
 msgstr "Forgreningsnavn %s"
 
-#: kallithea/lib/helpers.py:787
+#: kallithea/lib/helpers.py:804
 #, python-format
 msgid "Pull request %s"
 msgstr "Flettingsforespørsel %s"
 
-#: kallithea/lib/helpers.py:797
+#: kallithea/lib/helpers.py:814
 #, fuzzy
 msgid "[deleted] repository"
 msgstr "[slettet] pakkebrønn"
 
-#: kallithea/lib/helpers.py:799 kallithea/lib/helpers.py:811
+#: kallithea/lib/helpers.py:816 kallithea/lib/helpers.py:828
 #, fuzzy
 msgid "[created] repository"
 msgstr "[opprettet] pakkebrønn"
 
-#: kallithea/lib/helpers.py:801
+#: kallithea/lib/helpers.py:818
 msgid "[created] repository as fork"
 msgstr "[opprettet] pakkebrønn som forgrening"
 
-#: kallithea/lib/helpers.py:803 kallithea/lib/helpers.py:813
+#: kallithea/lib/helpers.py:820 kallithea/lib/helpers.py:830
 #, fuzzy
 msgid "[forked] repository"
 msgstr "[forgrenet] pakkebrønn"
 
-#: kallithea/lib/helpers.py:805 kallithea/lib/helpers.py:815
+#: kallithea/lib/helpers.py:822 kallithea/lib/helpers.py:832
 #, fuzzy
 msgid "[updated] repository"
 msgstr "[oppdaterte] pakkebrønn"
 
-#: kallithea/lib/helpers.py:807
+#: kallithea/lib/helpers.py:824
 #, fuzzy
 msgid "[downloaded] archive from repository"
 msgstr "[lastet ned] arkiv fra pakkebrønn"
 
-#: kallithea/lib/helpers.py:809
+#: kallithea/lib/helpers.py:826
 #, fuzzy
 msgid "[delete] repository"
 msgstr "[slett] pakkebrønn"
 
-#: kallithea/lib/helpers.py:817
+#: kallithea/lib/helpers.py:834
 msgid "[created] user"
 msgstr "[opprettet] bruker"
 
-#: kallithea/lib/helpers.py:819
+#: kallithea/lib/helpers.py:836
 msgid "[updated] user"
 msgstr "[oppdaterte] bruker"
 
-#: kallithea/lib/helpers.py:821
+#: kallithea/lib/helpers.py:838
 msgid "[created] user group"
 msgstr "[opprettet] brukergruppe"
 
-#: kallithea/lib/helpers.py:823
+#: kallithea/lib/helpers.py:840
 msgid "[updated] user group"
 msgstr "[oppdaterte] brukergruppe"
 
-#: kallithea/lib/helpers.py:825
+#: kallithea/lib/helpers.py:842
 #, fuzzy
 msgid "[commented] on revision in repository"
 msgstr "[kommenterte] en revisjon i pakkebrønn"
 
-#: kallithea/lib/helpers.py:827
+#: kallithea/lib/helpers.py:844
 msgid "[commented] on pull request for"
 msgstr "[kommenterte] flettingsforespørsel for"
 
-#: kallithea/lib/helpers.py:829
+#: kallithea/lib/helpers.py:846
 msgid "[closed] pull request for"
 msgstr "[lukket] flettingsforespørsel for"
 
-#: kallithea/lib/helpers.py:831
+#: kallithea/lib/helpers.py:848
 msgid "[pushed] into"
 msgstr "[dyttet] til"
 
-#: kallithea/lib/helpers.py:833
+#: kallithea/lib/helpers.py:850
 #, fuzzy
 msgid "[committed via Kallithea] into repository"
 msgstr "[innsendt via Kallithea] inn i pakkebrønn"
 
-#: kallithea/lib/helpers.py:835
+#: kallithea/lib/helpers.py:852
 msgid "[pulled from remote] into repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:837
+#: kallithea/lib/helpers.py:854
 msgid "[pulled] from"
 msgstr ""
 
-#: kallithea/lib/helpers.py:839
+#: kallithea/lib/helpers.py:856
 msgid "[started following] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:841
+#: kallithea/lib/helpers.py:858
 msgid "[stopped following] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:961
+#: kallithea/lib/helpers.py:975
 #, python-format
 msgid " and %s more"
 msgstr ""
 
-#: kallithea/lib/helpers.py:965
+#: kallithea/lib/helpers.py:979
 #: kallithea/templates/compare/compare_diff.html:69
 #: kallithea/templates/pullrequests/pullrequest_show.html:297
 msgid "No files"
 msgstr "Ingen filer"
 
-#: kallithea/lib/helpers.py:990
+#: kallithea/lib/helpers.py:1004
 msgid "new file"
 msgstr "ny fil"
 
-#: kallithea/lib/helpers.py:993
+#: kallithea/lib/helpers.py:1007
 msgid "mod"
 msgstr ""
 
-#: kallithea/lib/helpers.py:996
+#: kallithea/lib/helpers.py:1010
 msgid "del"
 msgstr ""
 
-#: kallithea/lib/helpers.py:999
+#: kallithea/lib/helpers.py:1013
 msgid "rename"
 msgstr ""
 
-#: kallithea/lib/helpers.py:1004
+#: kallithea/lib/helpers.py:1018
 msgid "chmod"
 msgstr ""
 
-#: kallithea/lib/helpers.py:1297
+#: kallithea/lib/helpers.py:1323
 #, python-format
 msgid ""
 "%s repository is not mapped to db perhaps it was created or renamed from "
@@ -1273,96 +1271,98 @@
 "repositories"
 msgstr ""
 
-#: kallithea/lib/ssh.py:73
+#: kallithea/lib/ssh.py:75
 msgid "SSH key is missing"
 msgstr ""
 
-#: kallithea/lib/ssh.py:77
-msgid "Incorrect SSH key - it must have both a key type and a base64 part"
-msgstr ""
-
-#: kallithea/lib/ssh.py:81
+#: kallithea/lib/ssh.py:79
+msgid ""
+"Incorrect SSH key - it must have both a key type and a base64 part, like "
+"'ssh-rsa ASRNeaZu4FA...xlJp='"
+msgstr ""
+
+#: kallithea/lib/ssh.py:83
 msgid "Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'"
 msgstr ""
 
-#: kallithea/lib/ssh.py:84
+#: kallithea/lib/ssh.py:86
 #, python-format
 msgid "Incorrect SSH key - unexpected characters in base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:89
+#: kallithea/lib/ssh.py:91
 #, python-format
 msgid "Incorrect SSH key - failed to decode base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:92
+#: kallithea/lib/ssh.py:94
 #, python-format
 msgid "Incorrect SSH key - base64 part is not %r as claimed but %r"
 msgstr ""
 
-#: kallithea/lib/utils2.py:334
+#: kallithea/lib/utils2.py:253
 #, python-format
 msgid "%d year"
 msgid_plural "%d years"
 msgstr[0] "%d år"
 msgstr[1] "%d år"
 
-#: kallithea/lib/utils2.py:335
+#: kallithea/lib/utils2.py:254
 #, python-format
 msgid "%d month"
 msgid_plural "%d months"
 msgstr[0] "%d måned"
 msgstr[1] "%d måneder"
 
-#: kallithea/lib/utils2.py:336
+#: kallithea/lib/utils2.py:255
 #, python-format
 msgid "%d day"
 msgid_plural "%d days"
 msgstr[0] "%d dag"
 msgstr[1] "%d dager"
 
-#: kallithea/lib/utils2.py:337
+#: kallithea/lib/utils2.py:256
 #, python-format
 msgid "%d hour"
 msgid_plural "%d hours"
 msgstr[0] "%d time"
 msgstr[1] "%d timer"
 
-#: kallithea/lib/utils2.py:338
+#: kallithea/lib/utils2.py:257
 #, python-format
 msgid "%d minute"
 msgid_plural "%d minutes"
 msgstr[0] "%d minutt"
 msgstr[1] "%d minutter"
 
-#: kallithea/lib/utils2.py:339
+#: kallithea/lib/utils2.py:258
 #, python-format
 msgid "%d second"
 msgid_plural "%d seconds"
 msgstr[0] "%d sekund"
 msgstr[1] "%d sekunder"
 
-#: kallithea/lib/utils2.py:355
+#: kallithea/lib/utils2.py:274
 #, python-format
 msgid "in %s"
 msgstr "om %s"
 
-#: kallithea/lib/utils2.py:357
+#: kallithea/lib/utils2.py:276
 #, python-format
 msgid "%s ago"
 msgstr "for %s siden"
 
-#: kallithea/lib/utils2.py:359
+#: kallithea/lib/utils2.py:278
 #, python-format
 msgid "in %s and %s"
 msgstr "om %s og %s"
 
-#: kallithea/lib/utils2.py:362
+#: kallithea/lib/utils2.py:281
 #, python-format
 msgid "%s and %s ago"
 msgstr "%s og %s siden"
 
-#: kallithea/lib/utils2.py:365
+#: kallithea/lib/utils2.py:284
 msgid "just now"
 msgstr "akkurat nå"
 
@@ -1371,134 +1371,134 @@
 msgid "on line %s"
 msgstr "på linje %s"
 
-#: kallithea/model/comment.py:221 kallithea/model/pull_request.py:114
+#: kallithea/model/comment.py:219 kallithea/model/pull_request.py:112
 msgid "[Mention]"
 msgstr ""
 
-#: kallithea/model/db.py:1496
+#: kallithea/model/db.py:1411
 msgid "top level"
 msgstr "toppnivå"
 
-#: kallithea/model/db.py:1637
+#: kallithea/model/db.py:1542
 msgid "Kallithea Administrator"
 msgstr "Kallithea-administrator"
 
-#: kallithea/model/db.py:1639
+#: kallithea/model/db.py:1544
 #, fuzzy
 msgid "Default user has no access to new repositories"
 msgstr "Forvalgt bruker har ingen tilgang til nye pakkebrønner"
 
-#: kallithea/model/db.py:1640
+#: kallithea/model/db.py:1545
 msgid "Default user has read access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1641
+#: kallithea/model/db.py:1546
 msgid "Default user has write access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1642
+#: kallithea/model/db.py:1547
 msgid "Default user has admin access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1644
+#: kallithea/model/db.py:1549
 msgid "Default user has no access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1645
+#: kallithea/model/db.py:1550
 msgid "Default user has read access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1646
+#: kallithea/model/db.py:1551
 msgid "Default user has write access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1647
+#: kallithea/model/db.py:1552
 msgid "Default user has admin access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1649
+#: kallithea/model/db.py:1554
 msgid "Default user has no access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1650
+#: kallithea/model/db.py:1555
 msgid "Default user has read access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1651
+#: kallithea/model/db.py:1556
 msgid "Default user has write access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1652
+#: kallithea/model/db.py:1557
 msgid "Default user has admin access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1654
+#: kallithea/model/db.py:1559
 msgid "Only admins can create repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1655
+#: kallithea/model/db.py:1560
 msgid "Non-admins can create repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1657
+#: kallithea/model/db.py:1562
 msgid "Only admins can create user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1658
+#: kallithea/model/db.py:1563
 msgid "Non-admins can create user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1660
+#: kallithea/model/db.py:1565
 msgid "Only admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1661
+#: kallithea/model/db.py:1566
 msgid "Non-admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1663
+#: kallithea/model/db.py:1568
 msgid ""
 "Repository creation enabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1664
+#: kallithea/model/db.py:1569
 msgid ""
 "Repository creation disabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1666
+#: kallithea/model/db.py:1571
 msgid "Only admins can fork repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1667
+#: kallithea/model/db.py:1572
 msgid "Non-admins can fork repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1669
+#: kallithea/model/db.py:1574
 msgid "Registration disabled"
 msgstr ""
 
-#: kallithea/model/db.py:1670
+#: kallithea/model/db.py:1575
 msgid "User registration with manual account activation"
 msgstr ""
 
-#: kallithea/model/db.py:1671
+#: kallithea/model/db.py:1576
 msgid "User registration with automatic account activation"
 msgstr ""
 
-#: kallithea/model/db.py:2206
+#: kallithea/model/db.py:1992
 msgid "Not reviewed"
 msgstr ""
 
-#: kallithea/model/db.py:2207
+#: kallithea/model/db.py:1993
 msgid "Under review"
 msgstr ""
 
-#: kallithea/model/db.py:2208
+#: kallithea/model/db.py:1994
 msgid "Not approved"
 msgstr "Ikke godkjent"
 
-#: kallithea/model/db.py:2209
+#: kallithea/model/db.py:1995
 msgid "Approved"
 msgstr "Godkjent"
 
@@ -1524,313 +1524,313 @@
 msgid "Name must not contain only digits"
 msgstr ""
 
-#: kallithea/model/notification.py:164
+#: kallithea/model/notification.py:162
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s changeset %(short_id)s \"%(message_short)s\" on "
 "%(branch)s"
 msgstr ""
 
-#: kallithea/model/notification.py:167
+#: kallithea/model/notification.py:165
 #, python-format
 msgid "New user %(new_username)s registered"
 msgstr ""
 
-#: kallithea/model/notification.py:169
+#: kallithea/model/notification.py:167
 #, python-format
 msgid ""
 "[Review] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:170
+#: kallithea/model/notification.py:168
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:183
+#: kallithea/model/notification.py:188
 msgid "Closing"
 msgstr "Lukker"
 
-#: kallithea/model/pull_request.py:73
+#: kallithea/model/pull_request.py:72
 #, python-format
 msgid ""
 "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:209
+#: kallithea/model/pull_request.py:207
 msgid "Cannot create empty pull request"
 msgstr ""
 
-#: kallithea/model/pull_request.py:217
+#: kallithea/model/pull_request.py:215
 #, python-format
 msgid ""
 "Cannot create pull request - criss cross merge detected, please merge a "
 "later %s revision to %s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:245 kallithea/model/pull_request.py:380
+#: kallithea/model/pull_request.py:243 kallithea/model/pull_request.py:378
 msgid "You are not authorized to create the pull request"
 msgstr ""
 
-#: kallithea/model/pull_request.py:339
+#: kallithea/model/pull_request.py:337
 msgid "Missing changesets since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:346
+#: kallithea/model/pull_request.py:344
 #, python-format
 msgid "New changesets on %s %s since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:353
+#: kallithea/model/pull_request.py:351
 msgid "Ancestor didn't change - diff since previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:360
+#: kallithea/model/pull_request.py:358
 #, python-format
 msgid ""
 "This iteration is based on another %s revision and there is no simple "
 "diff."
 msgstr ""
 
-#: kallithea/model/pull_request.py:362
+#: kallithea/model/pull_request.py:360
 #, python-format
 msgid "No changes found on %s %s since previous iteration."
 msgstr ""
 
-#: kallithea/model/pull_request.py:388
+#: kallithea/model/pull_request.py:386
 #, python-format
 msgid "Closed, next iteration: %s ."
 msgstr ""
 
-#: kallithea/model/scm.py:668
+#: kallithea/model/scm.py:655
 msgid "latest tip"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:56
+#: kallithea/model/ssh_key.py:57
 #, python-format
 msgid "SSH key %r is invalid: %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:68
+#: kallithea/model/ssh_key.py:69
 #, python-format
 msgid "SSH key %s is already used by %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:89
-#, python-format
-msgid "SSH key %r not found"
-msgstr ""
-
-#: kallithea/model/user.py:186
+#: kallithea/model/ssh_key.py:88
+#, python-format
+msgid "SSH key with fingerprint %r found"
+msgstr ""
+
+#: kallithea/model/user.py:180
 msgid "New user registration"
 msgstr ""
 
-#: kallithea/model/user.py:250
+#: kallithea/model/user.py:244
 msgid ""
 "You can't remove this user since it is crucial for the entire application"
 msgstr ""
 
-#: kallithea/model/user.py:255
+#: kallithea/model/user.py:249
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repositories and cannot be removed. Switch "
 "owners or remove those repositories: %s"
 msgstr ""
 
-#: kallithea/model/user.py:260
+#: kallithea/model/user.py:254
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repository groups and cannot be removed. Switch "
 "owners or remove those repository groups: %s"
 msgstr ""
 
-#: kallithea/model/user.py:267
+#: kallithea/model/user.py:261
 #, python-format
 msgid ""
 "User \"%s\" still owns %s user groups and cannot be removed. Switch "
 "owners or remove those user groups: %s"
 msgstr ""
 
-#: kallithea/model/user.py:361
+#: kallithea/model/user.py:355
 msgid "Password reset link"
 msgstr ""
 
-#: kallithea/model/user.py:408
+#: kallithea/model/user.py:402
 msgid "Password reset notification"
 msgstr ""
 
-#: kallithea/model/user.py:409
+#: kallithea/model/user.py:403
 #, python-format
 msgid ""
 "The password to your account %s has been changed using password reset "
 "form."
 msgstr ""
 
-#: kallithea/model/validators.py:52 kallithea/model/validators.py:53
+#: kallithea/model/validators.py:53 kallithea/model/validators.py:54
 msgid "Value cannot be an empty list"
 msgstr ""
 
-#: kallithea/model/validators.py:72
+#: kallithea/model/validators.py:73
 #, python-format
 msgid "Username \"%(username)s\" already exists"
 msgstr "Brukernavnet \"%(username)s\" finnes allerede"
 
-#: kallithea/model/validators.py:74
+#: kallithea/model/validators.py:75
 #, python-format
 msgid "Username \"%(username)s\" cannot be used"
 msgstr "Brukernavnet \"%(username)s\" kan ikke brukes"
 
-#: kallithea/model/validators.py:76
+#: kallithea/model/validators.py:77
 msgid ""
 "Username may only contain alphanumeric characters underscores, periods or "
 "dashes and must begin with an alphanumeric character or underscore"
 msgstr ""
 
-#: kallithea/model/validators.py:103
+#: kallithea/model/validators.py:104
 msgid "The input is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:110
+#: kallithea/model/validators.py:111
 #, python-format
 msgid "Username %(username)s is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:131
+#: kallithea/model/validators.py:132
 msgid "Invalid user group name"
 msgstr "Ugyldig brukergruppenavn"
 
-#: kallithea/model/validators.py:132
+#: kallithea/model/validators.py:133
 #, python-format
 msgid "User group \"%(usergroup)s\" already exists"
 msgstr "Brukergruppen \"%(usergroup)s\" finnes allerede"
 
-#: kallithea/model/validators.py:134
+#: kallithea/model/validators.py:135
 msgid ""
 "user group name may only contain alphanumeric characters underscores, "
 "periods or dashes and must begin with alphanumeric character"
 msgstr ""
 
-#: kallithea/model/validators.py:174
-msgid "Cannot assign this group as parent"
-msgstr ""
-
 #: kallithea/model/validators.py:175
+msgid "Cannot assign this group as parent"
+msgstr ""
+
+#: kallithea/model/validators.py:176
 #, python-format
 msgid "Group \"%(group_name)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:177
+#: kallithea/model/validators.py:178
 #, python-format
 msgid "Repository with name \"%(group_name)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:233
+#: kallithea/model/validators.py:230
 msgid "Invalid characters (non-ascii) in password"
 msgstr ""
 
-#: kallithea/model/validators.py:248
+#: kallithea/model/validators.py:245
 msgid "Invalid old password"
 msgstr "Ugyldig gammelt passord"
 
-#: kallithea/model/validators.py:264
+#: kallithea/model/validators.py:261
 msgid "Passwords do not match"
 msgstr "Passordene samsvarer ikke"
 
-#: kallithea/model/validators.py:279
+#: kallithea/model/validators.py:276
 msgid "Invalid username or password"
 msgstr "Ugyldig brukernavn eller passord"
 
-#: kallithea/model/validators.py:313
+#: kallithea/model/validators.py:310
 #, python-format
 msgid "Repository name %(repo)s is not allowed"
 msgstr ""
 
+#: kallithea/model/validators.py:312
+#, python-format
+msgid "Repository named %(repo)s already exists"
+msgstr ""
+
+#: kallithea/model/validators.py:313
+#, python-format
+msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
+msgstr ""
+
 #: kallithea/model/validators.py:315
 #, python-format
-msgid "Repository named %(repo)s already exists"
-msgstr ""
-
-#: kallithea/model/validators.py:316
-#, python-format
-msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
-msgstr ""
-
-#: kallithea/model/validators.py:318
-#, python-format
 msgid "Repository group with name \"%(repo)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:404
+#: kallithea/model/validators.py:401
 msgid "Invalid repository URL"
 msgstr "Ugyldig pakkebrønnsnettadresse"
 
-#: kallithea/model/validators.py:405
+#: kallithea/model/validators.py:402
 msgid ""
 "Invalid repository URL. It must be a valid http, https, ssh, svn+http or "
 "svn+https URL"
 msgstr ""
 
-#: kallithea/model/validators.py:430
+#: kallithea/model/validators.py:427
 msgid "Fork has to be the same type as parent"
 msgstr ""
 
-#: kallithea/model/validators.py:445
+#: kallithea/model/validators.py:442
 msgid "You don't have permissions to create repository in this group"
 msgstr ""
 
-#: kallithea/model/validators.py:447
+#: kallithea/model/validators.py:444
 msgid "no permission to create repository in root location"
 msgstr ""
 
-#: kallithea/model/validators.py:497
+#: kallithea/model/validators.py:494
 msgid "You don't have permissions to create a group in this location"
 msgstr ""
 
-#: kallithea/model/validators.py:537
+#: kallithea/model/validators.py:534
 msgid "This username or user group name is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:630
+#: kallithea/model/validators.py:627
 msgid "This is not a valid path"
 msgstr "Dette er ikke en gyldig sti"
 
-#: kallithea/model/validators.py:647
+#: kallithea/model/validators.py:644
 msgid "This email address is already in use"
 msgstr "E-postadressen er allerede i bruk"
 
-#: kallithea/model/validators.py:667
+#: kallithea/model/validators.py:664
 #, fuzzy, python-format
 msgid "Email address \"%(email)s\" not found"
 msgstr "Fant ikke e-postadressen \"%(email)s\""
 
-#: kallithea/model/validators.py:704
+#: kallithea/model/validators.py:701
 msgid ""
 "The LDAP Login attribute of the CN must be specified - this is the name "
 "of the attribute that is equivalent to \"username\""
 msgstr ""
 
-#: kallithea/model/validators.py:716
+#: kallithea/model/validators.py:713
 msgid "Please enter a valid IPv4 or IPv6 address"
 msgstr "Skriv inn en gyldig IPv4- eller IPv6-adresse"
 
-#: kallithea/model/validators.py:717
+#: kallithea/model/validators.py:714
 #, python-format
 msgid ""
 "The network size (bits) must be within the range of 0-32 (not %(bits)r)"
 msgstr ""
 
-#: kallithea/model/validators.py:750
+#: kallithea/model/validators.py:747
 msgid "Key name can only consist of letters, underscore, dash or numbers"
 msgstr ""
 
-#: kallithea/model/validators.py:764
+#: kallithea/model/validators.py:761
 msgid "Filename cannot be inside a directory"
 msgstr ""
 
-#: kallithea/model/validators.py:780
+#: kallithea/model/validators.py:777
 #, python-format
 msgid "Plugins %(loaded)s and %(next_to_load)s both export the same name"
 msgstr ""
@@ -1891,7 +1891,7 @@
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:60
 #: kallithea/templates/email_templates/pull_request.html:37
 #: kallithea/templates/forks/fork.html:34
-#: kallithea/templates/index_base.html:58
+#: kallithea/templates/index_base.html:59
 #: kallithea/templates/pullrequests/pullrequest.html:33
 #: kallithea/templates/pullrequests/pullrequest_show.html:38
 #: kallithea/templates/pullrequests/pullrequest_show.html:59
@@ -1899,14 +1899,14 @@
 msgid "Description"
 msgstr "Beskrivelse"
 
-#: kallithea/templates/index_base.html:60
+#: kallithea/templates/index_base.html:61
 msgid "Last Change"
 msgstr "Siste endring"
 
 #: kallithea/templates/admin/my_account/my_account_repos.html:15
 #: kallithea/templates/admin/my_account/my_account_watched.html:15
 #: kallithea/templates/admin/repos/repos.html:41
-#: kallithea/templates/index_base.html:62
+#: kallithea/templates/index_base.html:63
 msgid "Tip"
 msgstr "Tips"
 
@@ -1916,7 +1916,7 @@
 #: kallithea/templates/admin/repos/repos.html:42
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:8
 #: kallithea/templates/admin/user_groups/user_groups.html:42
-#: kallithea/templates/index_base.html:63
+#: kallithea/templates/index_base.html:64
 #: kallithea/templates/pullrequests/pullrequest_data.html:16
 #: kallithea/templates/pullrequests/pullrequest_show.html:124
 #: kallithea/templates/pullrequests/pullrequest_show.html:219
@@ -1940,7 +1940,7 @@
 #: kallithea/templates/admin/users/user_edit_profile.html:18
 #: kallithea/templates/admin/users/users.html:37
 #: kallithea/templates/base/base.html:364
-#: kallithea/templates/email_templates/registration.html:11
+#: kallithea/templates/email_templates/registration.html:12
 #: kallithea/templates/login.html:28 kallithea/templates/register.html:31
 msgid "Username"
 msgstr "Brukernavn"
@@ -2070,7 +2070,7 @@
 #: kallithea/templates/admin/settings/settings.html:31
 #: kallithea/templates/admin/users/user_add.html:62
 #: kallithea/templates/admin/users/user_edit_profile.html:25
-#: kallithea/templates/email_templates/registration.html:33
+#: kallithea/templates/email_templates/registration.html:34
 #: kallithea/templates/register.html:66
 msgid "Email"
 msgstr "E-post"
@@ -2318,7 +2318,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/gists/index.html:51
-#: kallithea/templates/data_table/_dt_elements.html:78
+#: kallithea/templates/data_table/_dt_elements.html:84
 msgid "Created"
 msgstr "Opprettet"
 
@@ -2402,13 +2402,13 @@
 #: kallithea/templates/admin/users/user_edit_ips.html:21
 #: kallithea/templates/changeset/changeset_file_comment.html:30
 #: kallithea/templates/changeset/changeset_file_comment.html:121
-#: kallithea/templates/data_table/_dt_elements.html:69
-#: kallithea/templates/data_table/_dt_elements.html:89
-#: kallithea/templates/data_table/_dt_elements.html:91
-#: kallithea/templates/data_table/_dt_elements.html:101
-#: kallithea/templates/data_table/_dt_elements.html:103
-#: kallithea/templates/data_table/_dt_elements.html:120
-#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:75
+#: kallithea/templates/data_table/_dt_elements.html:95
+#: kallithea/templates/data_table/_dt_elements.html:97
+#: kallithea/templates/data_table/_dt_elements.html:107
+#: kallithea/templates/data_table/_dt_elements.html:109
+#: kallithea/templates/data_table/_dt_elements.html:126
+#: kallithea/templates/data_table/_dt_elements.html:128
 #: kallithea/templates/files/files_source.html:35
 #: kallithea/templates/files/files_source.html:38
 #: kallithea/templates/files/files_source.html:41
@@ -2424,14 +2424,14 @@
 #: kallithea/templates/base/perms_summary.html:44
 #: kallithea/templates/base/perms_summary.html:81
 #: kallithea/templates/base/perms_summary.html:83
-#: kallithea/templates/data_table/_dt_elements.html:63
-#: kallithea/templates/data_table/_dt_elements.html:64
-#: kallithea/templates/data_table/_dt_elements.html:85
-#: kallithea/templates/data_table/_dt_elements.html:86
-#: kallithea/templates/data_table/_dt_elements.html:97
-#: kallithea/templates/data_table/_dt_elements.html:98
-#: kallithea/templates/data_table/_dt_elements.html:116
-#: kallithea/templates/data_table/_dt_elements.html:117
+#: kallithea/templates/data_table/_dt_elements.html:69
+#: kallithea/templates/data_table/_dt_elements.html:70
+#: kallithea/templates/data_table/_dt_elements.html:91
+#: kallithea/templates/data_table/_dt_elements.html:92
+#: kallithea/templates/data_table/_dt_elements.html:103
+#: kallithea/templates/data_table/_dt_elements.html:104
+#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:123
 #: kallithea/templates/files/diff_2way.html:56
 #: kallithea/templates/files/files_source.html:37
 #: kallithea/templates/files/files_source.html:40
@@ -2739,7 +2739,7 @@
 #: kallithea/templates/admin/permissions/permissions_globals.html:27
 #: kallithea/templates/admin/repos/repo_add_base.html:28
 #: kallithea/templates/admin/repos/repo_edit_settings.html:33
-#: kallithea/templates/data_table/_dt_elements.html:134
+#: kallithea/templates/data_table/_dt_elements.html:140
 #: kallithea/templates/forks/fork.html:42
 msgid "Repository group"
 msgstr ""
@@ -2760,7 +2760,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:40
-#: kallithea/templates/data_table/_dt_elements.html:141
+#: kallithea/templates/data_table/_dt_elements.html:147
 msgid "User group"
 msgstr "Brukergruppe"
 
@@ -2933,7 +2933,7 @@
 msgstr "Opprettet den"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:121
+#: kallithea/templates/data_table/_dt_elements.html:127
 #, fuzzy, python-format
 msgid "Confirm to delete this group: %s with %s repository"
 msgid_plural "Confirm to delete this group: %s with %s repositories"
@@ -3096,14 +3096,10 @@
 msgstr "Ekstra felter"
 
 #: kallithea/templates/admin/repos/repo_edit.html:37
-msgid "Caches"
+msgid "Remote"
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit.html:40
-msgid "Remote"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit.html:43
 #: kallithea/templates/summary/statistics.html:8
 #: kallithea/templates/summary/summary.html:169
 #: kallithea/templates/summary/summary.html:170
@@ -3141,7 +3137,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:46
-#: kallithea/templates/data_table/_dt_elements.html:68
+#: kallithea/templates/data_table/_dt_elements.html:74
 #, python-format
 msgid "Confirm to delete this repository: %s"
 msgstr ""
@@ -3172,43 +3168,14 @@
 "it or restore it."
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:4
-msgid "Invalidate Repository Cache"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:6
-msgid ""
-"Manually invalidate cache for this repository. On first access, the "
-"repository will be cached again."
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:9
-msgid "List of Cached Values"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:12
-msgid "Prefix"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:13
+#: kallithea/templates/admin/repos/repo_edit_fields.html:6
+msgid "Label"
+msgstr "Etikett"
+
 #: kallithea/templates/admin/repos/repo_edit_fields.html:7
 msgid "Key"
 msgstr "Nøkkel"
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:14
-#: kallithea/templates/admin/user_groups/user_group_add.html:40
-#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
-#: kallithea/templates/admin/user_groups/user_groups.html:41
-#: kallithea/templates/admin/users/user_add.html:69
-#: kallithea/templates/admin/users/user_edit_profile.html:74
-#: kallithea/templates/admin/users/users.html:42
-msgid "Active"
-msgstr "Aktiv"
-
-#: kallithea/templates/admin/repos/repo_edit_fields.html:6
-msgid "Label"
-msgstr "Etikett"
-
 #: kallithea/templates/admin/repos/repo_edit_fields.html:20
 #, python-format
 msgid "Confirm to delete this field: %s"
@@ -3724,6 +3691,15 @@
 msgid "Short, optional description for this user group."
 msgstr ""
 
+#: kallithea/templates/admin/user_groups/user_group_add.html:40
+#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
+#: kallithea/templates/admin/user_groups/user_groups.html:41
+#: kallithea/templates/admin/users/user_add.html:69
+#: kallithea/templates/admin/users/user_edit_profile.html:74
+#: kallithea/templates/admin/users/users.html:42
+msgid "Active"
+msgstr "Aktiv"
+
 #: kallithea/templates/admin/user_groups/user_group_edit.html:5
 #, python-format
 msgid "%s user group settings"
@@ -3745,7 +3721,7 @@
 msgstr "Medlemmer"
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:19
-#: kallithea/templates/data_table/_dt_elements.html:102
+#: kallithea/templates/data_table/_dt_elements.html:108
 #, fuzzy, python-format
 msgid "Confirm to delete this user group: %s"
 msgstr "Bekreft sletting av denne brukergruppen: %s"
@@ -3819,7 +3795,7 @@
 msgstr "Medlem av brukergrupper"
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:90
+#: kallithea/templates/data_table/_dt_elements.html:96
 #, fuzzy, python-format
 msgid "Confirm to delete this user: %s"
 msgstr "Bekreft sletting av denne brukeren: %s"
@@ -3921,10 +3897,12 @@
 msgstr "Søk"
 
 #: kallithea/templates/base/base.html:167
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Follow"
 msgstr "Følg"
 
 #: kallithea/templates/base/base.html:168
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Unfollow"
 msgstr ""
 
@@ -4340,23 +4318,23 @@
 msgid "Merge"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:96
+#: kallithea/templates/changeset/changeset.html:95
 msgid "Grafted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:102
+#: kallithea/templates/changeset/changeset.html:100
 msgid "Transplanted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:108
+#: kallithea/templates/changeset/changeset.html:106
 msgid "Replaced by:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:122
+#: kallithea/templates/changeset/changeset.html:120
 msgid "Preceded by:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:139
+#: kallithea/templates/changeset/changeset.html:137
 #: kallithea/templates/compare/compare_diff.html:59
 #: kallithea/templates/pullrequests/pullrequest_show.html:290
 #, python-format
@@ -4365,7 +4343,7 @@
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/templates/changeset/changeset.html:141
+#: kallithea/templates/changeset/changeset.html:139
 #: kallithea/templates/compare/compare_diff.html:61
 #: kallithea/templates/pullrequests/pullrequest_show.html:292
 #, python-format
@@ -4374,8 +4352,8 @@
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
 #: kallithea/templates/pullrequests/pullrequest_show.html:333
@@ -4606,23 +4584,23 @@
 msgid "Repository creation in progress..."
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:42
+#: kallithea/templates/data_table/_dt_elements.html:48
 msgid "No changesets yet"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:48
-#: kallithea/templates/data_table/_dt_elements.html:50
+#: kallithea/templates/data_table/_dt_elements.html:54
+#: kallithea/templates/data_table/_dt_elements.html:56
 #, python-format
 msgid "Subscribe to %s rss feed"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:56
-#: kallithea/templates/data_table/_dt_elements.html:58
+#: kallithea/templates/data_table/_dt_elements.html:62
+#: kallithea/templates/data_table/_dt_elements.html:64
 #, python-format
 msgid "Subscribe to %s atom feed"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:76
+#: kallithea/templates/data_table/_dt_elements.html:82
 msgid "Creating"
 msgstr ""
 
@@ -4654,6 +4632,13 @@
 msgid "by"
 msgstr ""
 
+#: kallithea/templates/email_templates/changeset_comment.html:36
+#: kallithea/templates/email_templates/pull_request_comment.html:43
+#, fuzzy
+#| msgid "%s committed on %s"
+msgid "View Comment"
+msgstr "%s sendte inn %s"
+
 #: kallithea/templates/email_templates/comment.html:27
 msgid "Status change:"
 msgstr ""
@@ -4662,32 +4647,42 @@
 msgid "The pull request has been closed."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:9
+#: kallithea/templates/email_templates/default.html:4
+msgid "Message"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:4
+#, fuzzy
+#| msgid "Password Reset"
+msgid "Password Reset Request"
+msgstr "Passordstilbakestilling"
+
+#: kallithea/templates/email_templates/password_reset.html:10
 #, python-format
 msgid "Hello %s"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:16
+#: kallithea/templates/email_templates/password_reset.html:17
 msgid "We have received a request to reset the password for your account."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:25
+#: kallithea/templates/email_templates/password_reset.html:26
 msgid ""
 "This account is however managed outside this system and the password "
 "cannot be changed here."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:28
+#: kallithea/templates/email_templates/password_reset.html:29
 msgid "To set a new password, click the following link"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:33
+#: kallithea/templates/email_templates/password_reset.html:34
 msgid ""
 "Should you not be able to use the link above, please type the following "
 "code into the password reset form"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:44
+#: kallithea/templates/email_templates/password_reset.html:45
 msgid ""
 "If it weren't you who requested the password reset, just disregard this "
 "message."
@@ -4720,6 +4715,11 @@
 msgid "to"
 msgstr ""
 
+#: kallithea/templates/email_templates/pull_request.html:85
+#, fuzzy
+msgid "View Pull Request"
+msgstr "Flettingsforespørsler"
+
 #: kallithea/templates/email_templates/pull_request_comment.html:4
 #, python-format
 msgid "Mention in Comment on Pull Request %s \"%s\""
@@ -4735,10 +4735,20 @@
 msgid "Comment on Pull Request %s \"%s\""
 msgstr ""
 
-#: kallithea/templates/email_templates/registration.html:22
+#: kallithea/templates/email_templates/registration.html:5
+#, fuzzy
+#| msgid "Registration"
+msgid "New User Registration"
+msgstr "Registrering"
+
+#: kallithea/templates/email_templates/registration.html:23
 msgid "Full Name"
 msgstr ""
 
+#: kallithea/templates/email_templates/registration.html:42
+msgid "View User Profile"
+msgstr ""
+
 #: kallithea/templates/files/diff_2way.html:15
 #, python-format
 msgid "%s File side-by-side diff"
@@ -5339,45 +5349,45 @@
 msgid "Stats gathered: "
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:87
-#: kallithea/templates/summary/summary.html:371
+#: kallithea/templates/summary/statistics.html:85
+#: kallithea/templates/summary/summary.html:369
 msgid "files"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:111
-#: kallithea/templates/summary/summary.html:401
+#: kallithea/templates/summary/statistics.html:109
+#: kallithea/templates/summary/summary.html:399
 msgid "Show more"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:405
+#: kallithea/templates/summary/statistics.html:395
 msgid "commits"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:406
+#: kallithea/templates/summary/statistics.html:396
 msgid "files added"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:407
+#: kallithea/templates/summary/statistics.html:397
 msgid "files changed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:408
+#: kallithea/templates/summary/statistics.html:398
 msgid "files removed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:410
+#: kallithea/templates/summary/statistics.html:400
 msgid "commit"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:411
+#: kallithea/templates/summary/statistics.html:401
 msgid "file added"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:412
+#: kallithea/templates/summary/statistics.html:402
 msgid "file changed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:413
+#: kallithea/templates/summary/statistics.html:403
 msgid "file removed"
 msgstr ""
 
--- a/kallithea/i18n/nl_BE/LC_MESSAGES/kallithea.po	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/i18n/nl_BE/LC_MESSAGES/kallithea.po	Mon May 04 19:24:04 2020 +0200
@@ -5,7 +5,7 @@
 msgstr ""
 "Project-Id-Version: Kallithea 0.3\n"
 "Report-Msgid-Bugs-To: translations@kallithea-scm.org\n"
-"POT-Creation-Date: 2020-01-05 04:10+0100\n"
+"POT-Creation-Date: 2020-05-04 19:12+0200\n"
 "PO-Revision-Date: 2019-10-05 19:28+0000\n"
 "Last-Translator: Thomas De Schampheleire <patrickdepinguin@gmail.com>\n"
 "Language-Team: Flemish <https://hosted.weblate.org/projects/kallithea/"
@@ -18,14 +18,14 @@
 "X-Generator: Weblate 3.9-dev\n"
 
 #: kallithea/controllers/changelog.py:67
-#: kallithea/controllers/pullrequests.py:250 kallithea/lib/base.py:602
+#: kallithea/controllers/pullrequests.py:247 kallithea/lib/base.py:602
 msgid "There are no changesets yet"
 msgstr "Er zijn nog geen changesets"
 
 #: kallithea/controllers/admin/permissions.py:64
 #: kallithea/controllers/admin/permissions.py:68
 #: kallithea/controllers/admin/permissions.py:72
-#: kallithea/controllers/changelog.py:137
+#: kallithea/controllers/changelog.py:136
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:7
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:88
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:7
@@ -34,37 +34,37 @@
 msgid "None"
 msgstr "Geen"
 
-#: kallithea/controllers/changelog.py:140 kallithea/controllers/files.py:189
+#: kallithea/controllers/changelog.py:139 kallithea/controllers/files.py:189
 msgid "(closed)"
 msgstr "(gesloten)"
 
-#: kallithea/controllers/changeset.py:81
+#: kallithea/controllers/changeset.py:82
 msgid "Show whitespace"
 msgstr "Toon witruimtes"
 
-#: kallithea/controllers/changeset.py:88
-#: kallithea/controllers/changeset.py:95
+#: kallithea/controllers/changeset.py:89
+#: kallithea/controllers/changeset.py:96
 #: kallithea/templates/files/diff_2way.html:55
 #, fuzzy
 msgid "Ignore whitespace"
 msgstr "Negeer witruimtes"
 
-#: kallithea/controllers/changeset.py:161
+#: kallithea/controllers/changeset.py:162
 #, python-format
 msgid "Increase diff context to %(num)s lines"
 msgstr "Vergroot de diff context tot %(num)s lijnen"
 
-#: kallithea/controllers/changeset.py:201
+#: kallithea/controllers/changeset.py:202
 msgid "No permission to change status"
 msgstr "Geen toestemming om de status te veranderen"
 
-#: kallithea/controllers/changeset.py:212
+#: kallithea/controllers/changeset.py:213
 #, python-format
 msgid "Successfully deleted pull request %s"
 msgstr ""
 
 #: kallithea/controllers/changeset.py:319 kallithea/controllers/files.py:89
-#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:700
+#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:697
 msgid "Such revision does not exist for this repository"
 msgstr "Deze revisie bestaat niet in deze repository"
 
@@ -77,66 +77,66 @@
 msgid "Cannot compare repositories of different types"
 msgstr "Kan geen repositories van verschillende types vergelijken"
 
-#: kallithea/controllers/compare.py:246
+#: kallithea/controllers/compare.py:247
 msgid "Cannot show empty diff"
 msgstr "Kan geen lege diff tonen"
 
-#: kallithea/controllers/compare.py:248
+#: kallithea/controllers/compare.py:249
 msgid "No ancestor found for merge diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:252
+#: kallithea/controllers/compare.py:253
 msgid "Multiple merge ancestors found for merge compare"
 msgstr ""
 
-#: kallithea/controllers/compare.py:268
+#: kallithea/controllers/compare.py:269
 msgid "Cannot compare repositories without using common ancestor"
 msgstr ""
 "Kan geen repositories vergelijken zonder een gemeenschappelijke voorouder "
 "te gebruiken"
 
-#: kallithea/controllers/error.py:71
+#: kallithea/controllers/error.py:67
 msgid "No response"
 msgstr "Geen antwoord"
 
-#: kallithea/controllers/error.py:72
+#: kallithea/controllers/error.py:68
 msgid "Unknown error"
 msgstr "Ongekende fout"
 
-#: kallithea/controllers/error.py:85
+#: kallithea/controllers/error.py:81
 msgid ""
 "The request could not be understood by the server due to malformed syntax."
 msgstr ""
 "De aanvraag kon niet door de server begrepen worden wegens incorrecte "
 "syntax."
 
-#: kallithea/controllers/error.py:88
+#: kallithea/controllers/error.py:84
 msgid "Unauthorized access to resource"
 msgstr "Ongeautoriseerde toegang tot resource"
 
-#: kallithea/controllers/error.py:90
+#: kallithea/controllers/error.py:86
 msgid "You don't have permission to view this page"
 msgstr "U hebt geen permissie om deze pagina te bekijken"
 
-#: kallithea/controllers/error.py:92
+#: kallithea/controllers/error.py:88
 msgid "The resource could not be found"
 msgstr "De resource kon niet gevonden worden"
 
-#: kallithea/controllers/error.py:94
+#: kallithea/controllers/error.py:90
 msgid ""
 "The server encountered an unexpected condition which prevented it from "
 "fulfilling the request."
 msgstr ""
 "De server kon de aanvraag niet voldoen wegens een onverwachte toestand."
 
-#: kallithea/controllers/feed.py:63
+#: kallithea/controllers/feed.py:59
 #, python-format
 msgid "%s committed on %s"
 msgstr "%s committeerde op %s"
 
-#: kallithea/controllers/feed.py:88
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/controllers/feed.py:84
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/compare/compare_diff.html:95
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
@@ -144,12 +144,12 @@
 msgid "Changeset was too big and was cut off..."
 msgstr "De changeset was te groot en werd afgekort..."
 
-#: kallithea/controllers/feed.py:111 kallithea/controllers/feed.py:140
+#: kallithea/controllers/feed.py:107
 #, python-format
 msgid "%s %s feed"
 msgstr "%s %s feed"
 
-#: kallithea/controllers/feed.py:113 kallithea/controllers/feed.py:142
+#: kallithea/controllers/feed.py:109
 #, python-format
 msgid "Changes on %s repository"
 msgstr "Veranderingen in repository %s"
@@ -167,105 +167,105 @@
 msgid "%s at %s"
 msgstr "%s op %s"
 
-#: kallithea/controllers/files.py:296
+#: kallithea/controllers/files.py:295
 msgid "You can only delete files with revision being a valid branch"
 msgstr ""
 "Men kan enkel bestanden verwijderen als de revisie een geldige branch is"
 
-#: kallithea/controllers/files.py:307
+#: kallithea/controllers/files.py:306
 #, python-format
 msgid "Deleted file %s via Kallithea"
 msgstr "Bestand %s verwijderd via Kallithea"
 
-#: kallithea/controllers/files.py:331
+#: kallithea/controllers/files.py:330
 #, python-format
 msgid "Successfully deleted file %s"
 msgstr "Bestand %s succesvol verwijderd"
 
-#: kallithea/controllers/files.py:335 kallithea/controllers/files.py:394
-#: kallithea/controllers/files.py:469
+#: kallithea/controllers/files.py:334 kallithea/controllers/files.py:392
+#: kallithea/controllers/files.py:467
 msgid "Error occurred during commit"
 msgstr "Er trad een fout op tijdens het committeren"
 
-#: kallithea/controllers/files.py:350
+#: kallithea/controllers/files.py:349
 msgid "You can only edit files with revision being a valid branch"
 msgstr ""
 "Men kan enkel bestanden wijzigen als de revisie een geldige branch is"
 
-#: kallithea/controllers/files.py:364
+#: kallithea/controllers/files.py:363
 #, python-format
 msgid "Edited file %s via Kallithea"
 msgstr "Bestand %s gewijzigd via Kallithea"
 
-#: kallithea/controllers/files.py:380
+#: kallithea/controllers/files.py:378
 msgid "No changes"
 msgstr "Geen wijzigingen"
 
-#: kallithea/controllers/files.py:390 kallithea/controllers/files.py:458
+#: kallithea/controllers/files.py:388 kallithea/controllers/files.py:456
 #, python-format
 msgid "Successfully committed to %s"
 msgstr "Succesvol gecommitteerd naar %s"
 
-#: kallithea/controllers/files.py:409
+#: kallithea/controllers/files.py:407
 msgid "Added file via Kallithea"
 msgstr "Bestand toegevoegd via Kallithea"
 
-#: kallithea/controllers/files.py:430
+#: kallithea/controllers/files.py:428
 msgid "No content"
 msgstr "Geen inhoud"
 
-#: kallithea/controllers/files.py:434
+#: kallithea/controllers/files.py:432
 msgid "No filename"
 msgstr "Geen bestandsnaam"
 
-#: kallithea/controllers/files.py:461
+#: kallithea/controllers/files.py:459
 msgid "Location must be relative path and must not contain .. in path"
 msgstr "De locatie moet een relatief pad zijn en mag geen .. bevatten"
 
-#: kallithea/controllers/files.py:493
+#: kallithea/controllers/files.py:491
 msgid "Downloads disabled"
 msgstr "Downloads uitgeschakeld"
 
-#: kallithea/controllers/files.py:504
+#: kallithea/controllers/files.py:502
 #, python-format
 msgid "Unknown revision %s"
 msgstr "Ongekende revisie %s"
 
-#: kallithea/controllers/files.py:506
+#: kallithea/controllers/files.py:504
 msgid "Empty repository"
 msgstr "Lege repository"
 
-#: kallithea/controllers/files.py:508
+#: kallithea/controllers/files.py:506
 msgid "Unknown archive type"
 msgstr "Ongekende archieftype"
 
-#: kallithea/controllers/files.py:729
+#: kallithea/controllers/files.py:726
 #: kallithea/templates/changeset/changeset_range.html:9
 #: kallithea/templates/email_templates/pull_request.html:64
 #: kallithea/templates/pullrequests/pullrequest.html:84
 msgid "Changesets"
 msgstr "Changesets"
 
-#: kallithea/controllers/files.py:730
-#: kallithea/controllers/pullrequests.py:182 kallithea/model/scm.py:676
+#: kallithea/controllers/files.py:727
+#: kallithea/controllers/pullrequests.py:174 kallithea/model/scm.py:663
 msgid "Branches"
 msgstr "Branches"
 
-#: kallithea/controllers/files.py:731
-#: kallithea/controllers/pullrequests.py:183 kallithea/model/scm.py:687
+#: kallithea/controllers/files.py:728
+#: kallithea/controllers/pullrequests.py:175 kallithea/model/scm.py:674
 msgid "Tags"
 msgstr "Tags"
 
-#: kallithea/controllers/forks.py:174
+#: kallithea/controllers/forks.py:175
 #, python-format
 msgid "An error occurred during repository forking %s"
 msgstr "Er is een fout opgetreden tijdens het forken van de repository %s"
 
-#: kallithea/controllers/home.py:79
+#: kallithea/controllers/home.py:77
 msgid "Groups"
 msgstr "Groepen"
 
-#: kallithea/controllers/home.py:89
+#: kallithea/controllers/home.py:87
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:90
 #: kallithea/templates/admin/repos/repo_add.html:12
 #: kallithea/templates/admin/repos/repo_add.html:16
@@ -277,48 +277,52 @@
 msgid "Repositories"
 msgstr "Repositories"
 
-#: kallithea/controllers/home.py:122
+#: kallithea/controllers/home.py:119
 #: kallithea/templates/files/files_add.html:32
 #: kallithea/templates/files/files_delete.html:23
 #: kallithea/templates/files/files_edit.html:32
 msgid "Branch"
 msgstr "Branch"
 
-#: kallithea/controllers/home.py:128
+#: kallithea/controllers/home.py:125
 msgid "Closed Branches"
 msgstr "Gesloten branches"
 
-#: kallithea/controllers/home.py:134
+#: kallithea/controllers/home.py:131
 msgid "Tag"
 msgstr "Tag"
 
-#: kallithea/controllers/home.py:140
+#: kallithea/controllers/home.py:137
 msgid "Bookmark"
 msgstr "Bladwijzer"
 
-#: kallithea/controllers/journal.py:112 kallithea/controllers/journal.py:154
+#: kallithea/controllers/journal.py:146 kallithea/controllers/journal.py:157
 #: kallithea/templates/journal/public_journal.html:4
 #: kallithea/templates/journal/public_journal.html:18
 msgid "Public Journal"
 msgstr "Publiek logboek"
 
-#: kallithea/controllers/journal.py:116 kallithea/controllers/journal.py:158
+#: kallithea/controllers/journal.py:150 kallithea/controllers/journal.py:161
 #: kallithea/templates/base/base.html:290
 #: kallithea/templates/journal/journal.html:5
 #: kallithea/templates/journal/journal.html:13
 msgid "Journal"
 msgstr "Logboek"
 
-#: kallithea/controllers/login.py:140 kallithea/controllers/login.py:185
+#: kallithea/controllers/login.py:109
+msgid "Authentication failed."
+msgstr ""
+
+#: kallithea/controllers/login.py:142 kallithea/controllers/login.py:187
 msgid "Bad captcha"
 msgstr "Incorrecte captcha"
 
-#: kallithea/controllers/login.py:146
+#: kallithea/controllers/login.py:148
 #, python-format
 msgid "You have successfully registered with %s"
 msgstr "U bent succesvol geregistreerd bij %s"
 
-#: kallithea/controllers/login.py:190
+#: kallithea/controllers/login.py:192
 msgid "A password reset confirmation code has been sent"
 msgstr "Een paswoordherstel bevestigingscode is verzonden"
 
@@ -331,226 +335,226 @@
 msgid "Successfully updated password"
 msgstr "Paswoord succesvol aangepast"
 
-#: kallithea/controllers/pullrequests.py:68
+#: kallithea/controllers/pullrequests.py:67
 #, python-format
 msgid "Invalid reviewer \"%s\" specified"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:131
+#: kallithea/controllers/pullrequests.py:123
 #, python-format
 msgid "%s (closed)"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:158
+#: kallithea/controllers/pullrequests.py:150
 #: kallithea/templates/changeset/changeset.html:12
 msgid "Changeset"
 msgstr "Changeset"
 
-#: kallithea/controllers/pullrequests.py:179
+#: kallithea/controllers/pullrequests.py:171
 msgid "Special"
 msgstr "Bijzonder"
 
-#: kallithea/controllers/pullrequests.py:180
+#: kallithea/controllers/pullrequests.py:172
 msgid "Peer branches"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:181 kallithea/model/scm.py:682
+#: kallithea/controllers/pullrequests.py:173 kallithea/model/scm.py:669
 msgid "Bookmarks"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:318
+#: kallithea/controllers/pullrequests.py:315
 #, python-format
 msgid "Error creating pull request: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:345
-#: kallithea/controllers/pullrequests.py:368
+#: kallithea/controllers/pullrequests.py:342
+#: kallithea/controllers/pullrequests.py:365
 msgid "Error occurred while creating pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:350
+#: kallithea/controllers/pullrequests.py:347
 msgid "Successfully opened new pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:373
+#: kallithea/controllers/pullrequests.py:370
 msgid "New pull request iteration created"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:401
+#: kallithea/controllers/pullrequests.py:398
 #, python-format
 msgid "Meanwhile, the following reviewers have been added: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:405
+#: kallithea/controllers/pullrequests.py:402
 #, python-format
 msgid "Meanwhile, the following reviewers have been removed: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:421
-#: kallithea/model/pull_request.py:232
+#: kallithea/controllers/pullrequests.py:418
+#: kallithea/model/pull_request.py:230
 msgid "No description"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:430
+#: kallithea/controllers/pullrequests.py:427
 msgid "Pull request updated"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:443
+#: kallithea/controllers/pullrequests.py:440
 msgid "Successfully deleted pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:479
+#: kallithea/controllers/pullrequests.py:476
 #, python-format
 msgid "Revision %s not found in %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:506
+#: kallithea/controllers/pullrequests.py:504
 #, python-format
 msgid "Error: changesets not found when displaying pull request from %s."
 msgstr ""
 
+#: kallithea/controllers/pullrequests.py:518
+#, python-format
+msgid "This pull request has already been merged to %s."
+msgstr ""
+
 #: kallithea/controllers/pullrequests.py:520
-#, python-format
-msgid "This pull request has already been merged to %s."
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:522
 msgid "This pull request has been closed and can not be updated."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:546
+#: kallithea/controllers/pullrequests.py:539
 #, python-format
 msgid "The following additional changes are available on %s:"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:548
-#: kallithea/controllers/pullrequests.py:552
+#: kallithea/controllers/pullrequests.py:541
+#: kallithea/controllers/pullrequests.py:545
 msgid "No additional changesets found for iterating on this pull request."
 msgstr ""
 
+#: kallithea/controllers/pullrequests.py:553
+#, python-format
+msgid "Note: Branch %s has another head: %s."
+msgstr ""
+
 #: kallithea/controllers/pullrequests.py:560
-#, python-format
-msgid "Note: Branch %s has another head: %s."
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:567
 msgid "Git pull requests don't support iterating yet."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:569
+#: kallithea/controllers/pullrequests.py:562
 #, python-format
 msgid ""
 "Error: some changesets not found when displaying pull request from %s."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:593
+#: kallithea/controllers/pullrequests.py:586
 msgid "The diff can't be shown - the PR revisions could not be found."
 msgstr ""
 
+#: kallithea/controllers/search.py:132
+msgid "Invalid search query. Try quoting it."
+msgstr ""
+
 #: kallithea/controllers/search.py:136
-msgid "Invalid search query. Try quoting it."
-msgstr ""
-
-#: kallithea/controllers/search.py:140
 msgid "The server has no search index."
 msgstr ""
 
-#: kallithea/controllers/search.py:143
+#: kallithea/controllers/search.py:139
 msgid "An error occurred during search operation."
 msgstr ""
 
-#: kallithea/controllers/summary.py:168
-#: kallithea/templates/summary/summary.html:412
+#: kallithea/controllers/summary.py:169
+#: kallithea/templates/summary/summary.html:410
 msgid "No data ready yet"
 msgstr ""
 
-#: kallithea/controllers/summary.py:171
+#: kallithea/controllers/summary.py:172
 #: kallithea/templates/summary/summary.html:97
 msgid "Statistics are disabled for this repository"
 msgstr ""
 
-#: kallithea/controllers/admin/auth_settings.py:137
+#: kallithea/controllers/admin/auth_settings.py:136
 msgid "Auth settings updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/auth_settings.py:148
+#: kallithea/controllers/admin/auth_settings.py:147
 msgid "error occurred during update of auth settings"
 msgstr ""
 
-#: kallithea/controllers/admin/defaults.py:75
+#: kallithea/controllers/admin/defaults.py:74
 msgid "Default settings updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/defaults.py:90
+#: kallithea/controllers/admin/defaults.py:89
 msgid "Error occurred during update of defaults"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:59
 #: kallithea/controllers/admin/my_account.py:232
-#: kallithea/controllers/admin/users.py:248
+#: kallithea/controllers/admin/users.py:246
 msgid "Forever"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:60
 #: kallithea/controllers/admin/my_account.py:233
-#: kallithea/controllers/admin/users.py:249
+#: kallithea/controllers/admin/users.py:247
 msgid "5 minutes"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:61
 #: kallithea/controllers/admin/my_account.py:234
-#: kallithea/controllers/admin/users.py:250
+#: kallithea/controllers/admin/users.py:248
 msgid "1 hour"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:62
 #: kallithea/controllers/admin/my_account.py:235
-#: kallithea/controllers/admin/users.py:251
+#: kallithea/controllers/admin/users.py:249
 msgid "1 day"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:63
 #: kallithea/controllers/admin/my_account.py:236
-#: kallithea/controllers/admin/users.py:252
+#: kallithea/controllers/admin/users.py:250
 msgid "1 month"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:67
 #: kallithea/controllers/admin/my_account.py:238
-#: kallithea/controllers/admin/users.py:254
+#: kallithea/controllers/admin/users.py:252
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:65
 #: kallithea/templates/admin/users/user_edit_api_keys.html:65
 msgid "Lifetime"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:142
+#: kallithea/controllers/admin/gists.py:148
 msgid "Error occurred during gist creation"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:158
+#: kallithea/controllers/admin/gists.py:164
 #, python-format
 msgid "Deleted gist %s"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:198
+#: kallithea/controllers/admin/gists.py:207
 msgid "Unmodified"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:228
+#: kallithea/controllers/admin/gists.py:237
 msgid "Successfully updated gist content"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:233
+#: kallithea/controllers/admin/gists.py:242
 msgid "Successfully updated gist data"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:236
+#: kallithea/controllers/admin/gists.py:245
 #, python-format
 msgid "Error occurred during update of gist %s"
 msgstr ""
 
-#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:211
-#: kallithea/model/user.py:232
+#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:205
+#: kallithea/model/user.py:226
 msgid "You can't edit this user since it's crucial for entire application"
 msgstr ""
 
@@ -559,7 +563,7 @@
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:134
-#: kallithea/controllers/admin/users.py:181
+#: kallithea/controllers/admin/users.py:179
 #, python-format
 msgid "Error occurred during update of user %s"
 msgstr ""
@@ -569,44 +573,44 @@
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:209
-#: kallithea/controllers/admin/users.py:367
+#: kallithea/controllers/admin/users.py:365
 #, python-format
 msgid "Added email %s to user"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:215
-#: kallithea/controllers/admin/users.py:373
+#: kallithea/controllers/admin/users.py:371
 msgid "An error occurred during email saving"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:224
-#: kallithea/controllers/admin/users.py:383
+#: kallithea/controllers/admin/users.py:381
 msgid "Removed email from user"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:248
-#: kallithea/controllers/admin/users.py:271
+#: kallithea/controllers/admin/users.py:269
 msgid "API key successfully created"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:257
-#: kallithea/controllers/admin/users.py:281
+#: kallithea/controllers/admin/users.py:279
 msgid "API key successfully reset"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:261
-#: kallithea/controllers/admin/users.py:285
+#: kallithea/controllers/admin/users.py:283
 msgid "API key successfully deleted"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:281
-#: kallithea/controllers/admin/users.py:456
+#: kallithea/controllers/admin/users.py:454
 #, python-format
 msgid "SSH key %s successfully added"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:293
-#: kallithea/controllers/admin/users.py:470
+#: kallithea/controllers/admin/users.py:468
 msgid "SSH key successfully deleted"
 msgstr "SSH key succesvol verwijderd"
 
@@ -682,11 +686,11 @@
 msgid "Allowed with automatic account activation"
 msgstr ""
 
-#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1673
+#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1578
 msgid "Manual activation of external account"
 msgstr ""
 
-#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1674
+#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1579
 msgid "Automatic activation of external account"
 msgstr ""
 
@@ -708,339 +712,331 @@
 msgid "Error occurred during update of permissions"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:172
+#: kallithea/controllers/admin/repo_groups.py:165
 #, python-format
 msgid "Error occurred during creation of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:177
+#: kallithea/controllers/admin/repo_groups.py:172
 #, python-format
 msgid "Created repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:224
+#: kallithea/controllers/admin/repo_groups.py:219
 #, python-format
 msgid "Updated repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:240
+#: kallithea/controllers/admin/repo_groups.py:235
 #, python-format
 msgid "Error occurred during update of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:250
+#: kallithea/controllers/admin/repo_groups.py:245
 #, python-format
 msgid "This group contains %s repositories and cannot be deleted"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:257
+#: kallithea/controllers/admin/repo_groups.py:252
 #, python-format
 msgid "This group contains %s subgroups and cannot be deleted"
 msgstr ""
 
+#: kallithea/controllers/admin/repo_groups.py:258
+#, python-format
+msgid "Removed repository group %s"
+msgstr ""
+
 #: kallithea/controllers/admin/repo_groups.py:263
 #, python-format
-msgid "Removed repository group %s"
-msgstr ""
-
-#: kallithea/controllers/admin/repo_groups.py:268
-#, python-format
 msgid "Error occurred during deletion of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:352
-#: kallithea/controllers/admin/repo_groups.py:382
-#: kallithea/controllers/admin/user_groups.py:294
+#: kallithea/controllers/admin/repo_groups.py:347
+#: kallithea/controllers/admin/repo_groups.py:377
+#: kallithea/controllers/admin/user_groups.py:290
 msgid "Cannot revoke permission for yourself as admin"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:367
+#: kallithea/controllers/admin/repo_groups.py:362
 msgid "Repository group permissions updated"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:399
-#: kallithea/controllers/admin/repos.py:358
-#: kallithea/controllers/admin/user_groups.py:306
+#: kallithea/controllers/admin/repo_groups.py:394
+#: kallithea/controllers/admin/repos.py:357
+#: kallithea/controllers/admin/user_groups.py:302
 msgid "An error occurred during revoking of permission"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:136
+#: kallithea/controllers/admin/repos.py:137
 #, python-format
 msgid "Error creating repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:194
+#: kallithea/controllers/admin/repos.py:193
 #, python-format
 msgid "Created repository %s from %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:203
+#: kallithea/controllers/admin/repos.py:202
 #, python-format
 msgid "Forked repository %s as %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:206
+#: kallithea/controllers/admin/repos.py:205
 #, python-format
 msgid "Created repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:235
+#: kallithea/controllers/admin/repos.py:234
 #, python-format
 msgid "Repository %s updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:255
+#: kallithea/controllers/admin/repos.py:254
 #, python-format
 msgid "Error occurred during update of repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:273
+#: kallithea/controllers/admin/repos.py:272
 #, python-format
 msgid "Detached %s forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:276
+#: kallithea/controllers/admin/repos.py:275
 #, python-format
 msgid "Deleted %s forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:281
+#: kallithea/controllers/admin/repos.py:280
 #, python-format
 msgid "Deleted repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:284
+#: kallithea/controllers/admin/repos.py:283
 #, python-format
 msgid "Cannot delete repository %s which still has forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:289
+#: kallithea/controllers/admin/repos.py:288
 #, python-format
 msgid "An error occurred during deletion of %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:329
+#: kallithea/controllers/admin/repos.py:328
 msgid "Repository permissions updated"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:388
+#: kallithea/controllers/admin/repos.py:387
 #, python-format
 msgid "Field validation error: %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:391
+#: kallithea/controllers/admin/repos.py:390
 #, python-format
 msgid "An error occurred during creation of field: %r"
 msgstr "Er is een fout opgetreden tijdens het aanmaken van veld: %r"
 
-#: kallithea/controllers/admin/repos.py:402
+#: kallithea/controllers/admin/repos.py:401
 msgid "An error occurred during removal of field"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:416
+#: kallithea/controllers/admin/repos.py:415
 msgid "-- Not a fork --"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:448
+#: kallithea/controllers/admin/repos.py:447
 msgid "Updated repository visibility in public journal"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:452
+#: kallithea/controllers/admin/repos.py:451
 msgid "An error occurred during setting this repository in public journal"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:468
+#: kallithea/controllers/admin/repos.py:467
 msgid "Nothing"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:470
+#: kallithea/controllers/admin/repos.py:469
 #, python-format
 msgid "Marked repository %s as fork of %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:477
+#: kallithea/controllers/admin/repos.py:476
 msgid "An error occurred during this operation"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:490
-msgid "Cache invalidation successful"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:494
-msgid "An error occurred during cache invalidation"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:507
+#: kallithea/controllers/admin/repos.py:488
 msgid "Pulled from remote location"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:510
+#: kallithea/controllers/admin/repos.py:491
 msgid "An error occurred during pull from remote location"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:541
+#: kallithea/controllers/admin/repos.py:522
 msgid "An error occurred during deletion of repository stats"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:131
+#: kallithea/controllers/admin/settings.py:132
 msgid "Updated VCS settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:135 kallithea/lib/utils.py:237
+#: kallithea/controllers/admin/settings.py:136
 msgid ""
 "Unable to activate hgsubversion support. The \"hgsubversion\" library is "
 "missing"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:141
-#: kallithea/controllers/admin/settings.py:233
+#: kallithea/controllers/admin/settings.py:142
+#: kallithea/controllers/admin/settings.py:234
 msgid "Error occurred while updating application settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:176
+#: kallithea/controllers/admin/settings.py:177
 #, python-format
 msgid "Repositories successfully rescanned. Added: %s. Removed: %s."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:188
+#: kallithea/controllers/admin/settings.py:189
 #, python-format
 msgid "Invalidated %s repositories"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:229
+#: kallithea/controllers/admin/settings.py:230
 msgid "Updated application settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:283
+#: kallithea/controllers/admin/settings.py:284
 msgid "Updated visualisation settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:288
+#: kallithea/controllers/admin/settings.py:289
 msgid "Error occurred during updating visualisation settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:312
+#: kallithea/controllers/admin/settings.py:313
 msgid "Please enter email address"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:327
+#: kallithea/controllers/admin/settings.py:328
 msgid "Send email task created"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:355
+#: kallithea/controllers/admin/settings.py:356
 msgid "Hook already exists"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:357
+#: kallithea/controllers/admin/settings.py:358
 msgid "Builtin hooks are read-only. Please use another hook name."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:360
+#: kallithea/controllers/admin/settings.py:361
 msgid "Added new hook"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:376
+#: kallithea/controllers/admin/settings.py:377
 msgid "Updated hooks"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:380
+#: kallithea/controllers/admin/settings.py:381
 msgid "Error occurred during hook creation"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:404
+#: kallithea/controllers/admin/settings.py:405
 msgid "Whoosh reindex task scheduled"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:138
+#: kallithea/controllers/admin/user_groups.py:134
 #, python-format
 msgid "Created user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:151
+#: kallithea/controllers/admin/user_groups.py:147
 #, python-format
 msgid "Error occurred during creation of user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:179
+#: kallithea/controllers/admin/user_groups.py:175
 #, python-format
 msgid "Updated user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:201
+#: kallithea/controllers/admin/user_groups.py:197
 #, python-format
 msgid "Error occurred during update of user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:212
+#: kallithea/controllers/admin/user_groups.py:208
 msgid "Successfully deleted user group"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:217
+#: kallithea/controllers/admin/user_groups.py:213
 msgid "An error occurred during deletion of user group"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:273
+#: kallithea/controllers/admin/user_groups.py:269
 msgid "Target group cannot be the same"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:279
+#: kallithea/controllers/admin/user_groups.py:275
 msgid "User group permissions updated"
 msgstr ""
 
+#: kallithea/controllers/admin/user_groups.py:384
+#: kallithea/controllers/admin/users.py:336
+msgid "Updated permissions"
+msgstr ""
+
 #: kallithea/controllers/admin/user_groups.py:388
-#: kallithea/controllers/admin/users.py:338
-msgid "Updated permissions"
-msgstr ""
-
-#: kallithea/controllers/admin/user_groups.py:392
-#: kallithea/controllers/admin/users.py:342
+#: kallithea/controllers/admin/users.py:340
 msgid "An error occurred during permissions saving"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:123
+#: kallithea/controllers/admin/users.py:121
 #, python-format
 msgid "Created user %s"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:138
+#: kallithea/controllers/admin/users.py:136
 #, python-format
 msgid "Error occurred during creation of user %s"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:162
+#: kallithea/controllers/admin/users.py:160
 msgid "User updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:190
+#: kallithea/controllers/admin/users.py:188
 msgid "Successfully deleted user"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:195
+#: kallithea/controllers/admin/users.py:193
 msgid "An error occurred during deletion of user"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:203
+#: kallithea/controllers/admin/users.py:201
 msgid "The default user cannot be edited"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:409
+#: kallithea/controllers/admin/users.py:407
 #, python-format
 msgid "Added IP address %s to user whitelist"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:415
+#: kallithea/controllers/admin/users.py:413
 msgid "An error occurred while adding IP address"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:427
+#: kallithea/controllers/admin/users.py:425
 msgid "Removed IP address from user whitelist"
 msgstr ""
 
-#: kallithea/lib/auth.py:684
+#: kallithea/lib/auth.py:634
 msgid "You need to be a registered user to perform this action"
 msgstr ""
 
-#: kallithea/lib/auth.py:712
+#: kallithea/lib/auth.py:662
 msgid "You need to be signed in to view this page"
 msgstr ""
 
@@ -1071,170 +1067,170 @@
 "Changeset was too big and was cut off, use diff menu to display this diff"
 msgstr ""
 
-#: kallithea/lib/diffs.py:224
+#: kallithea/lib/diffs.py:223
 msgid "No changes detected"
 msgstr ""
 
-#: kallithea/lib/helpers.py:653
+#: kallithea/lib/helpers.py:670
 #, python-format
 msgid "Deleted branch: %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:655
+#: kallithea/lib/helpers.py:672
 #, python-format
 msgid "Created tag: %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:666
+#: kallithea/lib/helpers.py:683
 #, python-format
 msgid "Changeset %s not found"
 msgstr "Changeset %s werd niet gevonden"
 
-#: kallithea/lib/helpers.py:715
+#: kallithea/lib/helpers.py:732
 #, python-format
 msgid "Show all combined changesets %s->%s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:721
+#: kallithea/lib/helpers.py:738
 msgid "Compare view"
 msgstr ""
 
-#: kallithea/lib/helpers.py:740
+#: kallithea/lib/helpers.py:757
 msgid "and"
 msgstr ""
 
-#: kallithea/lib/helpers.py:741
+#: kallithea/lib/helpers.py:758
 #, python-format
 msgid "%s more"
 msgstr ""
 
-#: kallithea/lib/helpers.py:742
+#: kallithea/lib/helpers.py:759
 #: kallithea/templates/changelog/changelog.html:43
 msgid "revisions"
 msgstr ""
 
-#: kallithea/lib/helpers.py:766
+#: kallithea/lib/helpers.py:783
 #, python-format
 msgid "Fork name %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:787
+#: kallithea/lib/helpers.py:804
 #, python-format
 msgid "Pull request %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:797
+#: kallithea/lib/helpers.py:814
 msgid "[deleted] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:799 kallithea/lib/helpers.py:811
+#: kallithea/lib/helpers.py:816 kallithea/lib/helpers.py:828
 msgid "[created] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:801
+#: kallithea/lib/helpers.py:818
 msgid "[created] repository as fork"
 msgstr ""
 
-#: kallithea/lib/helpers.py:803 kallithea/lib/helpers.py:813
+#: kallithea/lib/helpers.py:820 kallithea/lib/helpers.py:830
 msgid "[forked] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:805 kallithea/lib/helpers.py:815
+#: kallithea/lib/helpers.py:822 kallithea/lib/helpers.py:832
 msgid "[updated] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:807
+#: kallithea/lib/helpers.py:824
 msgid "[downloaded] archive from repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:809
+#: kallithea/lib/helpers.py:826
 msgid "[delete] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:817
+#: kallithea/lib/helpers.py:834
 msgid "[created] user"
 msgstr ""
 
-#: kallithea/lib/helpers.py:819
+#: kallithea/lib/helpers.py:836
 msgid "[updated] user"
 msgstr ""
 
-#: kallithea/lib/helpers.py:821
+#: kallithea/lib/helpers.py:838
 msgid "[created] user group"
 msgstr ""
 
-#: kallithea/lib/helpers.py:823
+#: kallithea/lib/helpers.py:840
 msgid "[updated] user group"
 msgstr ""
 
-#: kallithea/lib/helpers.py:825
+#: kallithea/lib/helpers.py:842
 msgid "[commented] on revision in repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:827
+#: kallithea/lib/helpers.py:844
 msgid "[commented] on pull request for"
 msgstr ""
 
-#: kallithea/lib/helpers.py:829
+#: kallithea/lib/helpers.py:846
 msgid "[closed] pull request for"
 msgstr ""
 
-#: kallithea/lib/helpers.py:831
+#: kallithea/lib/helpers.py:848
 msgid "[pushed] into"
 msgstr ""
 
-#: kallithea/lib/helpers.py:833
+#: kallithea/lib/helpers.py:850
 msgid "[committed via Kallithea] into repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:835
+#: kallithea/lib/helpers.py:852
 msgid "[pulled from remote] into repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:837
+#: kallithea/lib/helpers.py:854
 msgid "[pulled] from"
 msgstr ""
 
-#: kallithea/lib/helpers.py:839
+#: kallithea/lib/helpers.py:856
 msgid "[started following] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:841
+#: kallithea/lib/helpers.py:858
 msgid "[stopped following] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:961
+#: kallithea/lib/helpers.py:975
 #, python-format
 msgid " and %s more"
 msgstr ""
 
-#: kallithea/lib/helpers.py:965
+#: kallithea/lib/helpers.py:979
 #: kallithea/templates/compare/compare_diff.html:69
 #: kallithea/templates/pullrequests/pullrequest_show.html:297
 msgid "No files"
 msgstr ""
 
-#: kallithea/lib/helpers.py:990
+#: kallithea/lib/helpers.py:1004
 msgid "new file"
 msgstr ""
 
-#: kallithea/lib/helpers.py:993
+#: kallithea/lib/helpers.py:1007
 msgid "mod"
 msgstr ""
 
-#: kallithea/lib/helpers.py:996
+#: kallithea/lib/helpers.py:1010
 msgid "del"
 msgstr ""
 
-#: kallithea/lib/helpers.py:999
+#: kallithea/lib/helpers.py:1013
 msgid "rename"
 msgstr ""
 
-#: kallithea/lib/helpers.py:1004
+#: kallithea/lib/helpers.py:1018
 msgid "chmod"
 msgstr ""
 
-#: kallithea/lib/helpers.py:1297
+#: kallithea/lib/helpers.py:1323
 #, python-format
 msgid ""
 "%s repository is not mapped to db perhaps it was created or renamed from "
@@ -1242,96 +1238,98 @@
 "repositories"
 msgstr ""
 
-#: kallithea/lib/ssh.py:73
+#: kallithea/lib/ssh.py:75
 msgid "SSH key is missing"
 msgstr ""
 
-#: kallithea/lib/ssh.py:77
-msgid "Incorrect SSH key - it must have both a key type and a base64 part"
-msgstr ""
-
-#: kallithea/lib/ssh.py:81
+#: kallithea/lib/ssh.py:79
+msgid ""
+"Incorrect SSH key - it must have both a key type and a base64 part, like "
+"'ssh-rsa ASRNeaZu4FA...xlJp='"
+msgstr ""
+
+#: kallithea/lib/ssh.py:83
 msgid "Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'"
 msgstr ""
 
-#: kallithea/lib/ssh.py:84
+#: kallithea/lib/ssh.py:86
 #, python-format
 msgid "Incorrect SSH key - unexpected characters in base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:89
+#: kallithea/lib/ssh.py:91
 #, python-format
 msgid "Incorrect SSH key - failed to decode base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:92
+#: kallithea/lib/ssh.py:94
 #, python-format
 msgid "Incorrect SSH key - base64 part is not %r as claimed but %r"
 msgstr ""
 
-#: kallithea/lib/utils2.py:334
+#: kallithea/lib/utils2.py:253
 #, python-format
 msgid "%d year"
 msgid_plural "%d years"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:335
+#: kallithea/lib/utils2.py:254
 #, python-format
 msgid "%d month"
 msgid_plural "%d months"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:336
+#: kallithea/lib/utils2.py:255
 #, python-format
 msgid "%d day"
 msgid_plural "%d days"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:337
+#: kallithea/lib/utils2.py:256
 #, python-format
 msgid "%d hour"
 msgid_plural "%d hours"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:338
+#: kallithea/lib/utils2.py:257
 #, python-format
 msgid "%d minute"
 msgid_plural "%d minutes"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:339
+#: kallithea/lib/utils2.py:258
 #, python-format
 msgid "%d second"
 msgid_plural "%d seconds"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:355
+#: kallithea/lib/utils2.py:274
 #, python-format
 msgid "in %s"
 msgstr ""
 
-#: kallithea/lib/utils2.py:357
+#: kallithea/lib/utils2.py:276
 #, python-format
 msgid "%s ago"
 msgstr ""
 
-#: kallithea/lib/utils2.py:359
+#: kallithea/lib/utils2.py:278
 #, python-format
 msgid "in %s and %s"
 msgstr ""
 
-#: kallithea/lib/utils2.py:362
+#: kallithea/lib/utils2.py:281
 #, python-format
 msgid "%s and %s ago"
 msgstr ""
 
-#: kallithea/lib/utils2.py:365
+#: kallithea/lib/utils2.py:284
 msgid "just now"
 msgstr ""
 
@@ -1340,133 +1338,133 @@
 msgid "on line %s"
 msgstr ""
 
-#: kallithea/model/comment.py:221 kallithea/model/pull_request.py:114
+#: kallithea/model/comment.py:219 kallithea/model/pull_request.py:112
 msgid "[Mention]"
 msgstr ""
 
-#: kallithea/model/db.py:1496
+#: kallithea/model/db.py:1411
 msgid "top level"
 msgstr ""
 
-#: kallithea/model/db.py:1637
+#: kallithea/model/db.py:1542
 msgid "Kallithea Administrator"
 msgstr ""
 
-#: kallithea/model/db.py:1639
+#: kallithea/model/db.py:1544
 msgid "Default user has no access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1640
+#: kallithea/model/db.py:1545
 msgid "Default user has read access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1641
+#: kallithea/model/db.py:1546
 msgid "Default user has write access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1642
+#: kallithea/model/db.py:1547
 msgid "Default user has admin access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1644
+#: kallithea/model/db.py:1549
 msgid "Default user has no access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1645
+#: kallithea/model/db.py:1550
 msgid "Default user has read access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1646
+#: kallithea/model/db.py:1551
 msgid "Default user has write access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1647
+#: kallithea/model/db.py:1552
 msgid "Default user has admin access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1649
+#: kallithea/model/db.py:1554
 msgid "Default user has no access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1650
+#: kallithea/model/db.py:1555
 msgid "Default user has read access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1651
+#: kallithea/model/db.py:1556
 msgid "Default user has write access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1652
+#: kallithea/model/db.py:1557
 msgid "Default user has admin access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1654
+#: kallithea/model/db.py:1559
 msgid "Only admins can create repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1655
+#: kallithea/model/db.py:1560
 msgid "Non-admins can create repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1657
+#: kallithea/model/db.py:1562
 msgid "Only admins can create user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1658
+#: kallithea/model/db.py:1563
 msgid "Non-admins can create user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1660
+#: kallithea/model/db.py:1565
 msgid "Only admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1661
+#: kallithea/model/db.py:1566
 msgid "Non-admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1663
+#: kallithea/model/db.py:1568
 msgid ""
 "Repository creation enabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1664
+#: kallithea/model/db.py:1569
 msgid ""
 "Repository creation disabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1666
+#: kallithea/model/db.py:1571
 msgid "Only admins can fork repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1667
+#: kallithea/model/db.py:1572
 msgid "Non-admins can fork repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1669
+#: kallithea/model/db.py:1574
 msgid "Registration disabled"
 msgstr ""
 
-#: kallithea/model/db.py:1670
+#: kallithea/model/db.py:1575
 msgid "User registration with manual account activation"
 msgstr ""
 
-#: kallithea/model/db.py:1671
+#: kallithea/model/db.py:1576
 msgid "User registration with automatic account activation"
 msgstr ""
 
-#: kallithea/model/db.py:2206
+#: kallithea/model/db.py:1992
 msgid "Not reviewed"
 msgstr ""
 
-#: kallithea/model/db.py:2207
+#: kallithea/model/db.py:1993
 msgid "Under review"
 msgstr ""
 
-#: kallithea/model/db.py:2208
+#: kallithea/model/db.py:1994
 msgid "Not approved"
 msgstr ""
 
-#: kallithea/model/db.py:2209
+#: kallithea/model/db.py:1995
 msgid "Approved"
 msgstr ""
 
@@ -1492,313 +1490,314 @@
 msgid "Name must not contain only digits"
 msgstr ""
 
-#: kallithea/model/notification.py:164
+#: kallithea/model/notification.py:162
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s changeset %(short_id)s \"%(message_short)s\" on "
 "%(branch)s"
 msgstr ""
 
-#: kallithea/model/notification.py:167
+#: kallithea/model/notification.py:165
 #, python-format
 msgid "New user %(new_username)s registered"
 msgstr ""
 
-#: kallithea/model/notification.py:169
+#: kallithea/model/notification.py:167
 #, python-format
 msgid ""
 "[Review] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:170
+#: kallithea/model/notification.py:168
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:183
+#: kallithea/model/notification.py:188
 msgid "Closing"
 msgstr ""
 
-#: kallithea/model/pull_request.py:73
+#: kallithea/model/pull_request.py:72
 #, python-format
 msgid ""
 "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:209
+#: kallithea/model/pull_request.py:207
 msgid "Cannot create empty pull request"
 msgstr ""
 
-#: kallithea/model/pull_request.py:217
+#: kallithea/model/pull_request.py:215
 #, python-format
 msgid ""
 "Cannot create pull request - criss cross merge detected, please merge a "
 "later %s revision to %s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:245 kallithea/model/pull_request.py:380
+#: kallithea/model/pull_request.py:243 kallithea/model/pull_request.py:378
 msgid "You are not authorized to create the pull request"
 msgstr ""
 
-#: kallithea/model/pull_request.py:339
+#: kallithea/model/pull_request.py:337
 msgid "Missing changesets since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:346
+#: kallithea/model/pull_request.py:344
 #, python-format
 msgid "New changesets on %s %s since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:353
+#: kallithea/model/pull_request.py:351
 msgid "Ancestor didn't change - diff since previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:360
+#: kallithea/model/pull_request.py:358
 #, python-format
 msgid ""
 "This iteration is based on another %s revision and there is no simple "
 "diff."
 msgstr ""
 
-#: kallithea/model/pull_request.py:362
+#: kallithea/model/pull_request.py:360
 #, python-format
 msgid "No changes found on %s %s since previous iteration."
 msgstr ""
 
-#: kallithea/model/pull_request.py:388
+#: kallithea/model/pull_request.py:386
 #, python-format
 msgid "Closed, next iteration: %s ."
 msgstr ""
 
-#: kallithea/model/scm.py:668
+#: kallithea/model/scm.py:655
 msgid "latest tip"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:56
+#: kallithea/model/ssh_key.py:57
 #, python-format
 msgid "SSH key %r is invalid: %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:68
+#: kallithea/model/ssh_key.py:69
 #, python-format
 msgid "SSH key %s is already used by %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:89
-#, python-format
-msgid "SSH key %r not found"
+#: kallithea/model/ssh_key.py:88
+#, fuzzy, python-format
+#| msgid "SSH key %r not found"
+msgid "SSH key with fingerprint %r found"
 msgstr "SSH key %r werd niet gevonden"
 
-#: kallithea/model/user.py:186
+#: kallithea/model/user.py:180
 msgid "New user registration"
 msgstr ""
 
-#: kallithea/model/user.py:250
+#: kallithea/model/user.py:244
 msgid ""
 "You can't remove this user since it is crucial for the entire application"
 msgstr ""
 
-#: kallithea/model/user.py:255
+#: kallithea/model/user.py:249
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repositories and cannot be removed. Switch "
 "owners or remove those repositories: %s"
 msgstr ""
 
-#: kallithea/model/user.py:260
+#: kallithea/model/user.py:254
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repository groups and cannot be removed. Switch "
 "owners or remove those repository groups: %s"
 msgstr ""
 
-#: kallithea/model/user.py:267
+#: kallithea/model/user.py:261
 #, python-format
 msgid ""
 "User \"%s\" still owns %s user groups and cannot be removed. Switch "
 "owners or remove those user groups: %s"
 msgstr ""
 
-#: kallithea/model/user.py:361
+#: kallithea/model/user.py:355
 msgid "Password reset link"
 msgstr ""
 
-#: kallithea/model/user.py:408
+#: kallithea/model/user.py:402
 msgid "Password reset notification"
 msgstr ""
 
-#: kallithea/model/user.py:409
+#: kallithea/model/user.py:403
 #, python-format
 msgid ""
 "The password to your account %s has been changed using password reset "
 "form."
 msgstr ""
 
-#: kallithea/model/validators.py:52 kallithea/model/validators.py:53
+#: kallithea/model/validators.py:53 kallithea/model/validators.py:54
 msgid "Value cannot be an empty list"
 msgstr ""
 
-#: kallithea/model/validators.py:72
+#: kallithea/model/validators.py:73
 #, python-format
 msgid "Username \"%(username)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:74
+#: kallithea/model/validators.py:75
 #, python-format
 msgid "Username \"%(username)s\" cannot be used"
 msgstr ""
 
-#: kallithea/model/validators.py:76
+#: kallithea/model/validators.py:77
 msgid ""
 "Username may only contain alphanumeric characters underscores, periods or "
 "dashes and must begin with an alphanumeric character or underscore"
 msgstr ""
 
-#: kallithea/model/validators.py:103
+#: kallithea/model/validators.py:104
 msgid "The input is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:110
+#: kallithea/model/validators.py:111
 #, python-format
 msgid "Username %(username)s is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:131
-msgid "Invalid user group name"
-msgstr ""
-
 #: kallithea/model/validators.py:132
+msgid "Invalid user group name"
+msgstr ""
+
+#: kallithea/model/validators.py:133
 #, python-format
 msgid "User group \"%(usergroup)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:134
+#: kallithea/model/validators.py:135
 msgid ""
 "user group name may only contain alphanumeric characters underscores, "
 "periods or dashes and must begin with alphanumeric character"
 msgstr ""
 
-#: kallithea/model/validators.py:174
-msgid "Cannot assign this group as parent"
-msgstr ""
-
 #: kallithea/model/validators.py:175
+msgid "Cannot assign this group as parent"
+msgstr ""
+
+#: kallithea/model/validators.py:176
 #, python-format
 msgid "Group \"%(group_name)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:177
+#: kallithea/model/validators.py:178
 #, python-format
 msgid "Repository with name \"%(group_name)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:233
+#: kallithea/model/validators.py:230
 msgid "Invalid characters (non-ascii) in password"
 msgstr ""
 
-#: kallithea/model/validators.py:248
+#: kallithea/model/validators.py:245
 msgid "Invalid old password"
 msgstr ""
 
-#: kallithea/model/validators.py:264
+#: kallithea/model/validators.py:261
 msgid "Passwords do not match"
 msgstr ""
 
-#: kallithea/model/validators.py:279
+#: kallithea/model/validators.py:276
 msgid "Invalid username or password"
 msgstr ""
 
+#: kallithea/model/validators.py:310
+#, python-format
+msgid "Repository name %(repo)s is not allowed"
+msgstr ""
+
+#: kallithea/model/validators.py:312
+#, python-format
+msgid "Repository named %(repo)s already exists"
+msgstr ""
+
 #: kallithea/model/validators.py:313
 #, python-format
-msgid "Repository name %(repo)s is not allowed"
+msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
 msgstr ""
 
 #: kallithea/model/validators.py:315
 #, python-format
-msgid "Repository named %(repo)s already exists"
-msgstr ""
-
-#: kallithea/model/validators.py:316
-#, python-format
-msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
-msgstr ""
-
-#: kallithea/model/validators.py:318
-#, python-format
 msgid "Repository group with name \"%(repo)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:404
+#: kallithea/model/validators.py:401
 msgid "Invalid repository URL"
 msgstr ""
 
-#: kallithea/model/validators.py:405
+#: kallithea/model/validators.py:402
 msgid ""
 "Invalid repository URL. It must be a valid http, https, ssh, svn+http or "
 "svn+https URL"
 msgstr ""
 
-#: kallithea/model/validators.py:430
+#: kallithea/model/validators.py:427
 msgid "Fork has to be the same type as parent"
 msgstr ""
 
-#: kallithea/model/validators.py:445
+#: kallithea/model/validators.py:442
 msgid "You don't have permissions to create repository in this group"
 msgstr ""
 
-#: kallithea/model/validators.py:447
+#: kallithea/model/validators.py:444
 msgid "no permission to create repository in root location"
 msgstr ""
 
-#: kallithea/model/validators.py:497
+#: kallithea/model/validators.py:494
 msgid "You don't have permissions to create a group in this location"
 msgstr ""
 
-#: kallithea/model/validators.py:537
+#: kallithea/model/validators.py:534
 msgid "This username or user group name is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:630
+#: kallithea/model/validators.py:627
 msgid "This is not a valid path"
 msgstr ""
 
-#: kallithea/model/validators.py:647
+#: kallithea/model/validators.py:644
 msgid "This email address is already in use"
 msgstr ""
 
-#: kallithea/model/validators.py:667
+#: kallithea/model/validators.py:664
 #, python-format
 msgid "Email address \"%(email)s\" not found"
 msgstr ""
 
-#: kallithea/model/validators.py:704
+#: kallithea/model/validators.py:701
 msgid ""
 "The LDAP Login attribute of the CN must be specified - this is the name "
 "of the attribute that is equivalent to \"username\""
 msgstr ""
 
-#: kallithea/model/validators.py:716
+#: kallithea/model/validators.py:713
 msgid "Please enter a valid IPv4 or IPv6 address"
 msgstr ""
 
-#: kallithea/model/validators.py:717
+#: kallithea/model/validators.py:714
 #, python-format
 msgid ""
 "The network size (bits) must be within the range of 0-32 (not %(bits)r)"
 msgstr ""
 
-#: kallithea/model/validators.py:750
+#: kallithea/model/validators.py:747
 msgid "Key name can only consist of letters, underscore, dash or numbers"
 msgstr ""
 
-#: kallithea/model/validators.py:764
+#: kallithea/model/validators.py:761
 msgid "Filename cannot be inside a directory"
 msgstr ""
 
-#: kallithea/model/validators.py:780
+#: kallithea/model/validators.py:777
 #, python-format
 msgid "Plugins %(loaded)s and %(next_to_load)s both export the same name"
 msgstr ""
@@ -1858,7 +1857,7 @@
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:60
 #: kallithea/templates/email_templates/pull_request.html:37
 #: kallithea/templates/forks/fork.html:34
-#: kallithea/templates/index_base.html:58
+#: kallithea/templates/index_base.html:59
 #: kallithea/templates/pullrequests/pullrequest.html:33
 #: kallithea/templates/pullrequests/pullrequest_show.html:38
 #: kallithea/templates/pullrequests/pullrequest_show.html:59
@@ -1866,14 +1865,14 @@
 msgid "Description"
 msgstr ""
 
-#: kallithea/templates/index_base.html:60
+#: kallithea/templates/index_base.html:61
 msgid "Last Change"
 msgstr ""
 
 #: kallithea/templates/admin/my_account/my_account_repos.html:15
 #: kallithea/templates/admin/my_account/my_account_watched.html:15
 #: kallithea/templates/admin/repos/repos.html:41
-#: kallithea/templates/index_base.html:62
+#: kallithea/templates/index_base.html:63
 msgid "Tip"
 msgstr ""
 
@@ -1883,7 +1882,7 @@
 #: kallithea/templates/admin/repos/repos.html:42
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:8
 #: kallithea/templates/admin/user_groups/user_groups.html:42
-#: kallithea/templates/index_base.html:63
+#: kallithea/templates/index_base.html:64
 #: kallithea/templates/pullrequests/pullrequest_data.html:16
 #: kallithea/templates/pullrequests/pullrequest_show.html:124
 #: kallithea/templates/pullrequests/pullrequest_show.html:219
@@ -1907,7 +1906,7 @@
 #: kallithea/templates/admin/users/user_edit_profile.html:18
 #: kallithea/templates/admin/users/users.html:37
 #: kallithea/templates/base/base.html:364
-#: kallithea/templates/email_templates/registration.html:11
+#: kallithea/templates/email_templates/registration.html:12
 #: kallithea/templates/login.html:28 kallithea/templates/register.html:31
 msgid "Username"
 msgstr ""
@@ -2031,7 +2030,7 @@
 #: kallithea/templates/admin/settings/settings.html:31
 #: kallithea/templates/admin/users/user_add.html:62
 #: kallithea/templates/admin/users/user_edit_profile.html:25
-#: kallithea/templates/email_templates/registration.html:33
+#: kallithea/templates/email_templates/registration.html:34
 #: kallithea/templates/register.html:66
 msgid "Email"
 msgstr ""
@@ -2278,7 +2277,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/gists/index.html:51
-#: kallithea/templates/data_table/_dt_elements.html:78
+#: kallithea/templates/data_table/_dt_elements.html:84
 msgid "Created"
 msgstr ""
 
@@ -2362,13 +2361,13 @@
 #: kallithea/templates/admin/users/user_edit_ips.html:21
 #: kallithea/templates/changeset/changeset_file_comment.html:30
 #: kallithea/templates/changeset/changeset_file_comment.html:121
-#: kallithea/templates/data_table/_dt_elements.html:69
-#: kallithea/templates/data_table/_dt_elements.html:89
-#: kallithea/templates/data_table/_dt_elements.html:91
-#: kallithea/templates/data_table/_dt_elements.html:101
-#: kallithea/templates/data_table/_dt_elements.html:103
-#: kallithea/templates/data_table/_dt_elements.html:120
-#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:75
+#: kallithea/templates/data_table/_dt_elements.html:95
+#: kallithea/templates/data_table/_dt_elements.html:97
+#: kallithea/templates/data_table/_dt_elements.html:107
+#: kallithea/templates/data_table/_dt_elements.html:109
+#: kallithea/templates/data_table/_dt_elements.html:126
+#: kallithea/templates/data_table/_dt_elements.html:128
 #: kallithea/templates/files/files_source.html:35
 #: kallithea/templates/files/files_source.html:38
 #: kallithea/templates/files/files_source.html:41
@@ -2384,14 +2383,14 @@
 #: kallithea/templates/base/perms_summary.html:44
 #: kallithea/templates/base/perms_summary.html:81
 #: kallithea/templates/base/perms_summary.html:83
-#: kallithea/templates/data_table/_dt_elements.html:63
-#: kallithea/templates/data_table/_dt_elements.html:64
-#: kallithea/templates/data_table/_dt_elements.html:85
-#: kallithea/templates/data_table/_dt_elements.html:86
-#: kallithea/templates/data_table/_dt_elements.html:97
-#: kallithea/templates/data_table/_dt_elements.html:98
-#: kallithea/templates/data_table/_dt_elements.html:116
-#: kallithea/templates/data_table/_dt_elements.html:117
+#: kallithea/templates/data_table/_dt_elements.html:69
+#: kallithea/templates/data_table/_dt_elements.html:70
+#: kallithea/templates/data_table/_dt_elements.html:91
+#: kallithea/templates/data_table/_dt_elements.html:92
+#: kallithea/templates/data_table/_dt_elements.html:103
+#: kallithea/templates/data_table/_dt_elements.html:104
+#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:123
 #: kallithea/templates/files/diff_2way.html:56
 #: kallithea/templates/files/files_source.html:37
 #: kallithea/templates/files/files_source.html:40
@@ -2692,7 +2691,7 @@
 #: kallithea/templates/admin/permissions/permissions_globals.html:27
 #: kallithea/templates/admin/repos/repo_add_base.html:28
 #: kallithea/templates/admin/repos/repo_edit_settings.html:33
-#: kallithea/templates/data_table/_dt_elements.html:134
+#: kallithea/templates/data_table/_dt_elements.html:140
 #: kallithea/templates/forks/fork.html:42
 msgid "Repository group"
 msgstr ""
@@ -2713,7 +2712,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:40
-#: kallithea/templates/data_table/_dt_elements.html:141
+#: kallithea/templates/data_table/_dt_elements.html:147
 msgid "User group"
 msgstr ""
 
@@ -2886,7 +2885,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:121
+#: kallithea/templates/data_table/_dt_elements.html:127
 #, python-format
 msgid "Confirm to delete this group: %s with %s repository"
 msgid_plural "Confirm to delete this group: %s with %s repositories"
@@ -3049,14 +3048,10 @@
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit.html:37
-msgid "Caches"
+msgid "Remote"
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit.html:40
-msgid "Remote"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit.html:43
 #: kallithea/templates/summary/statistics.html:8
 #: kallithea/templates/summary/summary.html:169
 #: kallithea/templates/summary/summary.html:170
@@ -3094,7 +3089,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:46
-#: kallithea/templates/data_table/_dt_elements.html:68
+#: kallithea/templates/data_table/_dt_elements.html:74
 #, python-format
 msgid "Confirm to delete this repository: %s"
 msgstr ""
@@ -3125,43 +3120,14 @@
 "it or restore it."
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:4
-msgid "Invalidate Repository Cache"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:6
-msgid ""
-"Manually invalidate cache for this repository. On first access, the "
-"repository will be cached again."
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:9
-msgid "List of Cached Values"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:12
-msgid "Prefix"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:13
+#: kallithea/templates/admin/repos/repo_edit_fields.html:6
+msgid "Label"
+msgstr ""
+
 #: kallithea/templates/admin/repos/repo_edit_fields.html:7
 msgid "Key"
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:14
-#: kallithea/templates/admin/user_groups/user_group_add.html:40
-#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
-#: kallithea/templates/admin/user_groups/user_groups.html:41
-#: kallithea/templates/admin/users/user_add.html:69
-#: kallithea/templates/admin/users/user_edit_profile.html:74
-#: kallithea/templates/admin/users/users.html:42
-msgid "Active"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_fields.html:6
-msgid "Label"
-msgstr ""
-
 #: kallithea/templates/admin/repos/repo_edit_fields.html:20
 #, python-format
 msgid "Confirm to delete this field: %s"
@@ -3674,6 +3640,15 @@
 msgid "Short, optional description for this user group."
 msgstr ""
 
+#: kallithea/templates/admin/user_groups/user_group_add.html:40
+#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
+#: kallithea/templates/admin/user_groups/user_groups.html:41
+#: kallithea/templates/admin/users/user_add.html:69
+#: kallithea/templates/admin/users/user_edit_profile.html:74
+#: kallithea/templates/admin/users/users.html:42
+msgid "Active"
+msgstr ""
+
 #: kallithea/templates/admin/user_groups/user_group_edit.html:5
 #, python-format
 msgid "%s user group settings"
@@ -3695,7 +3670,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:19
-#: kallithea/templates/data_table/_dt_elements.html:102
+#: kallithea/templates/data_table/_dt_elements.html:108
 #, python-format
 msgid "Confirm to delete this user group: %s"
 msgstr ""
@@ -3769,7 +3744,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:90
+#: kallithea/templates/data_table/_dt_elements.html:96
 #, python-format
 msgid "Confirm to delete this user: %s"
 msgstr ""
@@ -3869,10 +3844,12 @@
 msgstr ""
 
 #: kallithea/templates/base/base.html:167
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Follow"
 msgstr ""
 
 #: kallithea/templates/base/base.html:168
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Unfollow"
 msgstr ""
 
@@ -4276,23 +4253,23 @@
 msgid "Merge"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:96
+#: kallithea/templates/changeset/changeset.html:95
 msgid "Grafted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:102
+#: kallithea/templates/changeset/changeset.html:100
 msgid "Transplanted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:108
+#: kallithea/templates/changeset/changeset.html:106
 msgid "Replaced by:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:122
+#: kallithea/templates/changeset/changeset.html:120
 msgid "Preceded by:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:139
+#: kallithea/templates/changeset/changeset.html:137
 #: kallithea/templates/compare/compare_diff.html:59
 #: kallithea/templates/pullrequests/pullrequest_show.html:290
 #, python-format
@@ -4301,7 +4278,7 @@
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/templates/changeset/changeset.html:141
+#: kallithea/templates/changeset/changeset.html:139
 #: kallithea/templates/compare/compare_diff.html:61
 #: kallithea/templates/pullrequests/pullrequest_show.html:292
 #, python-format
@@ -4310,8 +4287,8 @@
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
 #: kallithea/templates/pullrequests/pullrequest_show.html:333
@@ -4540,23 +4517,23 @@
 msgid "Repository creation in progress..."
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:42
+#: kallithea/templates/data_table/_dt_elements.html:48
 msgid "No changesets yet"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:48
-#: kallithea/templates/data_table/_dt_elements.html:50
+#: kallithea/templates/data_table/_dt_elements.html:54
+#: kallithea/templates/data_table/_dt_elements.html:56
 #, python-format
 msgid "Subscribe to %s rss feed"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:56
-#: kallithea/templates/data_table/_dt_elements.html:58
+#: kallithea/templates/data_table/_dt_elements.html:62
+#: kallithea/templates/data_table/_dt_elements.html:64
 #, python-format
 msgid "Subscribe to %s atom feed"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:76
+#: kallithea/templates/data_table/_dt_elements.html:82
 msgid "Creating"
 msgstr ""
 
@@ -4588,6 +4565,13 @@
 msgid "by"
 msgstr ""
 
+#: kallithea/templates/email_templates/changeset_comment.html:36
+#: kallithea/templates/email_templates/pull_request_comment.html:43
+#, fuzzy
+#| msgid "comment"
+msgid "View Comment"
+msgstr "opmerking"
+
 #: kallithea/templates/email_templates/comment.html:27
 msgid "Status change:"
 msgstr "Statuswijziging:"
@@ -4596,32 +4580,40 @@
 msgid "The pull request has been closed."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:9
+#: kallithea/templates/email_templates/default.html:4
+msgid "Message"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:4
+msgid "Password Reset Request"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:10
 #, python-format
 msgid "Hello %s"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:16
+#: kallithea/templates/email_templates/password_reset.html:17
 msgid "We have received a request to reset the password for your account."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:25
+#: kallithea/templates/email_templates/password_reset.html:26
 msgid ""
 "This account is however managed outside this system and the password "
 "cannot be changed here."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:28
+#: kallithea/templates/email_templates/password_reset.html:29
 msgid "To set a new password, click the following link"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:33
+#: kallithea/templates/email_templates/password_reset.html:34
 msgid ""
 "Should you not be able to use the link above, please type the following "
 "code into the password reset form"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:44
+#: kallithea/templates/email_templates/password_reset.html:45
 msgid ""
 "If it weren't you who requested the password reset, just disregard this "
 "message."
@@ -4652,6 +4644,12 @@
 msgid "to"
 msgstr ""
 
+#: kallithea/templates/email_templates/pull_request.html:85
+#, fuzzy
+#| msgid "Pull request"
+msgid "View Pull Request"
+msgstr "Pull request"
+
 #: kallithea/templates/email_templates/pull_request_comment.html:4
 #, python-format
 msgid "Mention in Comment on Pull Request %s \"%s\""
@@ -4667,10 +4665,18 @@
 msgid "Comment on Pull Request %s \"%s\""
 msgstr "Opmerking bij pull request %s \"%s\""
 
-#: kallithea/templates/email_templates/registration.html:22
+#: kallithea/templates/email_templates/registration.html:5
+msgid "New User Registration"
+msgstr ""
+
+#: kallithea/templates/email_templates/registration.html:23
 msgid "Full Name"
 msgstr ""
 
+#: kallithea/templates/email_templates/registration.html:42
+msgid "View User Profile"
+msgstr ""
+
 #: kallithea/templates/files/diff_2way.html:15
 #, python-format
 msgid "%s File side-by-side diff"
@@ -5271,45 +5277,45 @@
 msgid "Stats gathered: "
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:87
-#: kallithea/templates/summary/summary.html:371
+#: kallithea/templates/summary/statistics.html:85
+#: kallithea/templates/summary/summary.html:369
 msgid "files"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:111
-#: kallithea/templates/summary/summary.html:401
+#: kallithea/templates/summary/statistics.html:109
+#: kallithea/templates/summary/summary.html:399
 msgid "Show more"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:405
+#: kallithea/templates/summary/statistics.html:395
 msgid "commits"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:406
+#: kallithea/templates/summary/statistics.html:396
 msgid "files added"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:407
+#: kallithea/templates/summary/statistics.html:397
 msgid "files changed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:408
+#: kallithea/templates/summary/statistics.html:398
 msgid "files removed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:410
+#: kallithea/templates/summary/statistics.html:400
 msgid "commit"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:411
+#: kallithea/templates/summary/statistics.html:401
 msgid "file added"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:412
+#: kallithea/templates/summary/statistics.html:402
 msgid "file changed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:413
+#: kallithea/templates/summary/statistics.html:403
 msgid "file removed"
 msgstr ""
 
--- a/kallithea/i18n/pl/LC_MESSAGES/kallithea.po	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/i18n/pl/LC_MESSAGES/kallithea.po	Mon May 04 19:24:04 2020 +0200
@@ -4,9 +4,9 @@
 msgstr ""
 "Project-Id-Version: Kallithea 0.3\n"
 "Report-Msgid-Bugs-To: translations@kallithea-scm.org\n"
-"POT-Creation-Date: 2020-01-05 04:10+0100\n"
-"PO-Revision-Date: 2019-08-17 19:53+0000\n"
-"Last-Translator: Mateusz Mendel <mendelm9@gmail.com>\n"
+"POT-Creation-Date: 2020-05-04 19:12+0200\n"
+"PO-Revision-Date: 2020-01-27 15:21+0000\n"
+"Last-Translator: robertus <robertuss12@gmail.com>\n"
 "Language-Team: Polish <https://hosted.weblate.org/projects/kallithea/"
 "kallithea/pl/>\n"
 "Language: pl\n"
@@ -15,17 +15,17 @@
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n"
 "%100<10 || n%100>=20) ? 1 : 2;\n"
-"X-Generator: Weblate 3.8\n"
+"X-Generator: Weblate 3.11-dev\n"
 
 #: kallithea/controllers/changelog.py:67
-#: kallithea/controllers/pullrequests.py:250 kallithea/lib/base.py:602
+#: kallithea/controllers/pullrequests.py:247 kallithea/lib/base.py:602
 msgid "There are no changesets yet"
 msgstr "Brak zestawienia zmian"
 
 #: kallithea/controllers/admin/permissions.py:64
 #: kallithea/controllers/admin/permissions.py:68
 #: kallithea/controllers/admin/permissions.py:72
-#: kallithea/controllers/changelog.py:137
+#: kallithea/controllers/changelog.py:136
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:7
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:88
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:7
@@ -34,38 +34,38 @@
 msgid "None"
 msgstr "Brak"
 
-#: kallithea/controllers/changelog.py:140 kallithea/controllers/files.py:189
+#: kallithea/controllers/changelog.py:139 kallithea/controllers/files.py:189
 msgid "(closed)"
 msgstr "(zamknięty)"
 
-#: kallithea/controllers/changeset.py:81
+#: kallithea/controllers/changeset.py:82
 msgid "Show whitespace"
 msgstr "pokazuj spacje"
 
-#: kallithea/controllers/changeset.py:88
-#: kallithea/controllers/changeset.py:95
+#: kallithea/controllers/changeset.py:89
+#: kallithea/controllers/changeset.py:96
 #: kallithea/templates/files/diff_2way.html:55
 msgid "Ignore whitespace"
 msgstr "Ignoruj pokazywanie spacji"
 
-#: kallithea/controllers/changeset.py:161
+#: kallithea/controllers/changeset.py:162
 #, python-format
 msgid "Increase diff context to %(num)s lines"
 msgstr ""
 
-#: kallithea/controllers/changeset.py:201
+#: kallithea/controllers/changeset.py:202
 #, fuzzy
 msgid "No permission to change status"
 msgstr "Brak uprawnień do zmiany statusu"
 
-#: kallithea/controllers/changeset.py:212
+#: kallithea/controllers/changeset.py:213
 #, python-format
 msgid "Successfully deleted pull request %s"
 msgstr ""
 "Prośba o skasowanie połączenia gałęzi %s została wykonana prawidłowo"
 
 #: kallithea/controllers/changeset.py:319 kallithea/controllers/files.py:89
-#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:700
+#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:697
 msgid "Such revision does not exist for this repository"
 msgstr ""
 
@@ -78,50 +78,50 @@
 msgid "Cannot compare repositories of different types"
 msgstr ""
 
-#: kallithea/controllers/compare.py:246
+#: kallithea/controllers/compare.py:247
 msgid "Cannot show empty diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:248
+#: kallithea/controllers/compare.py:249
 msgid "No ancestor found for merge diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:252
+#: kallithea/controllers/compare.py:253
 msgid "Multiple merge ancestors found for merge compare"
 msgstr ""
 
-#: kallithea/controllers/compare.py:268
+#: kallithea/controllers/compare.py:269
 msgid "Cannot compare repositories without using common ancestor"
 msgstr ""
 
-#: kallithea/controllers/error.py:71
+#: kallithea/controllers/error.py:67
 msgid "No response"
 msgstr "Brak odpowiedzi"
 
-#: kallithea/controllers/error.py:72
+#: kallithea/controllers/error.py:68
 msgid "Unknown error"
-msgstr ""
-
-#: kallithea/controllers/error.py:85
+msgstr "Nieznany błąd"
+
+#: kallithea/controllers/error.py:81
 msgid ""
 "The request could not be understood by the server due to malformed syntax."
 msgstr ""
 "Żądanie nie może być rozumiane przez serwer z powodu zniekształconej "
 "składni."
 
-#: kallithea/controllers/error.py:88
+#: kallithea/controllers/error.py:84
 msgid "Unauthorized access to resource"
 msgstr "Nieautoryzowany dostęp do zasobów"
 
-#: kallithea/controllers/error.py:90
+#: kallithea/controllers/error.py:86
 msgid "You don't have permission to view this page"
 msgstr "Nie masz uprawnień do przeglądania tej strony"
 
-#: kallithea/controllers/error.py:92
+#: kallithea/controllers/error.py:88
 msgid "The resource could not be found"
 msgstr "Zasób nie został znaleziony"
 
-#: kallithea/controllers/error.py:94
+#: kallithea/controllers/error.py:90
 msgid ""
 "The server encountered an unexpected condition which prevented it from "
 "fulfilling the request."
@@ -129,14 +129,14 @@
 "Serwer napotkał niespodziewany warunek, który uniemożliwia spełnienie "
 "żądania."
 
-#: kallithea/controllers/feed.py:63
+#: kallithea/controllers/feed.py:59
 #, python-format
 msgid "%s committed on %s"
 msgstr "%s zakomitowal w %s"
 
-#: kallithea/controllers/feed.py:88
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/controllers/feed.py:84
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/compare/compare_diff.html:95
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
@@ -144,12 +144,12 @@
 msgid "Changeset was too big and was cut off..."
 msgstr "Lista zmian była zbyt duża i została ucięta..."
 
-#: kallithea/controllers/feed.py:111 kallithea/controllers/feed.py:140
+#: kallithea/controllers/feed.py:107
 #, python-format
 msgid "%s %s feed"
 msgstr "%s %s zasilać"
 
-#: kallithea/controllers/feed.py:113 kallithea/controllers/feed.py:142
+#: kallithea/controllers/feed.py:109
 #, python-format
 msgid "Changes on %s repository"
 msgstr "Zmiany w %s repozytorium"
@@ -167,106 +167,106 @@
 msgid "%s at %s"
 msgstr "w %s i %s"
 
-#: kallithea/controllers/files.py:296
+#: kallithea/controllers/files.py:295
 #, fuzzy
 msgid "You can only delete files with revision being a valid branch"
 msgstr "Można tylko usuwać pliki po sprawdzeniu obecnej gałęzi"
 
-#: kallithea/controllers/files.py:307
+#: kallithea/controllers/files.py:306
 #, python-format
 msgid "Deleted file %s via Kallithea"
 msgstr ""
 
-#: kallithea/controllers/files.py:331
+#: kallithea/controllers/files.py:330
 #, python-format
 msgid "Successfully deleted file %s"
 msgstr ""
 
-#: kallithea/controllers/files.py:335 kallithea/controllers/files.py:394
-#: kallithea/controllers/files.py:469
+#: kallithea/controllers/files.py:334 kallithea/controllers/files.py:392
+#: kallithea/controllers/files.py:467
 msgid "Error occurred during commit"
 msgstr "Wystąpił błąd w trakcie zatwierdzania"
 
-#: kallithea/controllers/files.py:350
+#: kallithea/controllers/files.py:349
 #, fuzzy
 msgid "You can only edit files with revision being a valid branch"
 msgstr "Można tylko edytować pliki z rewizji obecnej gałęzi"
 
-#: kallithea/controllers/files.py:364
+#: kallithea/controllers/files.py:363
 #, python-format
 msgid "Edited file %s via Kallithea"
 msgstr "Edytowanie %s w Kallithea"
 
-#: kallithea/controllers/files.py:380
+#: kallithea/controllers/files.py:378
 msgid "No changes"
 msgstr "Bez zmian"
 
-#: kallithea/controllers/files.py:390 kallithea/controllers/files.py:458
+#: kallithea/controllers/files.py:388 kallithea/controllers/files.py:456
 #, python-format
 msgid "Successfully committed to %s"
 msgstr "Committ wykonany do %s"
 
-#: kallithea/controllers/files.py:409
+#: kallithea/controllers/files.py:407
 msgid "Added file via Kallithea"
 msgstr "Dodano %s poprzez Kallithea"
 
-#: kallithea/controllers/files.py:430
+#: kallithea/controllers/files.py:428
 msgid "No content"
 msgstr "Brak treści"
 
-#: kallithea/controllers/files.py:434
+#: kallithea/controllers/files.py:432
 msgid "No filename"
 msgstr "Brak nazwy pliku"
 
-#: kallithea/controllers/files.py:461
+#: kallithea/controllers/files.py:459
 msgid "Location must be relative path and must not contain .. in path"
 msgstr ""
 "Lokalizacja musi być ścieżką względną i nie może zawierać .. ścieżki"
 
-#: kallithea/controllers/files.py:493
+#: kallithea/controllers/files.py:491
 msgid "Downloads disabled"
 msgstr "Pobieranie wyłączone"
 
-#: kallithea/controllers/files.py:504
+#: kallithea/controllers/files.py:502
 #, python-format
 msgid "Unknown revision %s"
 msgstr "Nieznana wersja %s"
 
-#: kallithea/controllers/files.py:506
+#: kallithea/controllers/files.py:504
 msgid "Empty repository"
 msgstr "Puste repozytorium"
 
-#: kallithea/controllers/files.py:508
+#: kallithea/controllers/files.py:506
 msgid "Unknown archive type"
 msgstr "Nieznany typ archiwum"
 
-#: kallithea/controllers/files.py:729
+#: kallithea/controllers/files.py:726
 #: kallithea/templates/changeset/changeset_range.html:9
 #: kallithea/templates/email_templates/pull_request.html:64
 #: kallithea/templates/pullrequests/pullrequest.html:84
 msgid "Changesets"
 msgstr "Różnice"
 
-#: kallithea/controllers/files.py:730
-#: kallithea/controllers/pullrequests.py:182 kallithea/model/scm.py:676
+#: kallithea/controllers/files.py:727
+#: kallithea/controllers/pullrequests.py:174 kallithea/model/scm.py:663
 msgid "Branches"
 msgstr "Gałęzie"
 
-#: kallithea/controllers/files.py:731
-#: kallithea/controllers/pullrequests.py:183 kallithea/model/scm.py:687
+#: kallithea/controllers/files.py:728
+#: kallithea/controllers/pullrequests.py:175 kallithea/model/scm.py:674
 msgid "Tags"
 msgstr "Etykiety"
 
-#: kallithea/controllers/forks.py:174
+#: kallithea/controllers/forks.py:175
 #, python-format
 msgid "An error occurred during repository forking %s"
 msgstr "Wystąpił błąd podczas rozgałęzienia %s repozytorium"
 
-#: kallithea/controllers/home.py:79
+#: kallithea/controllers/home.py:77
 msgid "Groups"
-msgstr ""
-
-#: kallithea/controllers/home.py:89
+msgstr "Grupy"
+
+#: kallithea/controllers/home.py:87
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:90
 #: kallithea/templates/admin/repos/repo_add.html:12
 #: kallithea/templates/admin/repos/repo_add.html:16
@@ -278,48 +278,54 @@
 msgid "Repositories"
 msgstr "Repozytoria"
 
-#: kallithea/controllers/home.py:122
+#: kallithea/controllers/home.py:119
 #: kallithea/templates/files/files_add.html:32
 #: kallithea/templates/files/files_delete.html:23
 #: kallithea/templates/files/files_edit.html:32
 msgid "Branch"
 msgstr "gałąź"
 
-#: kallithea/controllers/home.py:128
+#: kallithea/controllers/home.py:125
 msgid "Closed Branches"
 msgstr "Zamknięte Gałęzie"
 
-#: kallithea/controllers/home.py:134
+#: kallithea/controllers/home.py:131
 msgid "Tag"
 msgstr "Tag"
 
-#: kallithea/controllers/home.py:140
+#: kallithea/controllers/home.py:137
 msgid "Bookmark"
 msgstr "Zakładka"
 
-#: kallithea/controllers/journal.py:112 kallithea/controllers/journal.py:154
+#: kallithea/controllers/journal.py:146 kallithea/controllers/journal.py:157
 #: kallithea/templates/journal/public_journal.html:4
 #: kallithea/templates/journal/public_journal.html:18
 msgid "Public Journal"
 msgstr "Dziennik Publiczny"
 
-#: kallithea/controllers/journal.py:116 kallithea/controllers/journal.py:158
+#: kallithea/controllers/journal.py:150 kallithea/controllers/journal.py:161
 #: kallithea/templates/base/base.html:290
 #: kallithea/templates/journal/journal.html:5
 #: kallithea/templates/journal/journal.html:13
 msgid "Journal"
 msgstr "Dziennik"
 
-#: kallithea/controllers/login.py:140 kallithea/controllers/login.py:185
+#: kallithea/controllers/login.py:109
+#, fuzzy
+#| msgid "Authentication"
+msgid "Authentication failed."
+msgstr "Autentykacja"
+
+#: kallithea/controllers/login.py:142 kallithea/controllers/login.py:187
 msgid "Bad captcha"
 msgstr ""
 
-#: kallithea/controllers/login.py:146
+#: kallithea/controllers/login.py:148
 #, python-format
 msgid "You have successfully registered with %s"
 msgstr "Udało Ci się zarejestrować w %s"
 
-#: kallithea/controllers/login.py:190
+#: kallithea/controllers/login.py:192
 msgid "A password reset confirmation code has been sent"
 msgstr "Twój link zresetowania hasła został wysłany"
 
@@ -330,230 +336,230 @@
 #: kallithea/controllers/admin/my_account.py:157
 #: kallithea/controllers/login.py:244
 msgid "Successfully updated password"
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:68
+msgstr "Pomyślnie zaktualizowano hasło"
+
+#: kallithea/controllers/pullrequests.py:67
 #, python-format
 msgid "Invalid reviewer \"%s\" specified"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:131
+#: kallithea/controllers/pullrequests.py:123
 #, python-format
 msgid "%s (closed)"
 msgstr "%s (zamknięty)"
 
-#: kallithea/controllers/pullrequests.py:158
+#: kallithea/controllers/pullrequests.py:150
 #: kallithea/templates/changeset/changeset.html:12
 msgid "Changeset"
 msgstr "Grupy zmian"
 
-#: kallithea/controllers/pullrequests.py:179
+#: kallithea/controllers/pullrequests.py:171
 msgid "Special"
 msgstr "Specjalne"
 
-#: kallithea/controllers/pullrequests.py:180
+#: kallithea/controllers/pullrequests.py:172
 msgid "Peer branches"
 msgstr "gałęzie"
 
-#: kallithea/controllers/pullrequests.py:181 kallithea/model/scm.py:682
+#: kallithea/controllers/pullrequests.py:173 kallithea/model/scm.py:669
 msgid "Bookmarks"
 msgstr "Zakładki"
 
-#: kallithea/controllers/pullrequests.py:318
+#: kallithea/controllers/pullrequests.py:315
 #, python-format
 msgid "Error creating pull request: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:345
-#: kallithea/controllers/pullrequests.py:368
+#: kallithea/controllers/pullrequests.py:342
+#: kallithea/controllers/pullrequests.py:365
 msgid "Error occurred while creating pull request"
 msgstr "Wystąpił błąd podczas prośby o połączenie gałęzi"
 
-#: kallithea/controllers/pullrequests.py:350
+#: kallithea/controllers/pullrequests.py:347
 msgid "Successfully opened new pull request"
 msgstr "Prośba o wykonanie połączenia gałęzi została wykonana prawidłowo"
 
-#: kallithea/controllers/pullrequests.py:373
+#: kallithea/controllers/pullrequests.py:370
 #, fuzzy
 #| msgid "Pull request update created"
 msgid "New pull request iteration created"
 msgstr "Recenzje wniosków połączenia gałęzi"
 
-#: kallithea/controllers/pullrequests.py:401
+#: kallithea/controllers/pullrequests.py:398
 #, python-format
 msgid "Meanwhile, the following reviewers have been added: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:405
+#: kallithea/controllers/pullrequests.py:402
 #, python-format
 msgid "Meanwhile, the following reviewers have been removed: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:421
-#: kallithea/model/pull_request.py:232
+#: kallithea/controllers/pullrequests.py:418
+#: kallithea/model/pull_request.py:230
 msgid "No description"
 msgstr "Brak opisu"
 
-#: kallithea/controllers/pullrequests.py:430
+#: kallithea/controllers/pullrequests.py:427
 msgid "Pull request updated"
 msgstr "Połączone gałęzie zaktualizowane"
 
-#: kallithea/controllers/pullrequests.py:443
+#: kallithea/controllers/pullrequests.py:440
 msgid "Successfully deleted pull request"
 msgstr "Prośba o skasowanie połączenia gałęzi została wykonana prawidłowo"
 
-#: kallithea/controllers/pullrequests.py:479
+#: kallithea/controllers/pullrequests.py:476
 #, python-format
 msgid "Revision %s not found in %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:506
+#: kallithea/controllers/pullrequests.py:504
 #, python-format
 msgid "Error: changesets not found when displaying pull request from %s."
 msgstr ""
 
+#: kallithea/controllers/pullrequests.py:518
+#, python-format
+msgid "This pull request has already been merged to %s."
+msgstr ""
+
 #: kallithea/controllers/pullrequests.py:520
-#, python-format
-msgid "This pull request has already been merged to %s."
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:522
 msgid "This pull request has been closed and can not be updated."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:546
+#: kallithea/controllers/pullrequests.py:539
 #, python-format
 msgid "The following additional changes are available on %s:"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:548
-#: kallithea/controllers/pullrequests.py:552
+#: kallithea/controllers/pullrequests.py:541
+#: kallithea/controllers/pullrequests.py:545
 msgid "No additional changesets found for iterating on this pull request."
 msgstr ""
 
+#: kallithea/controllers/pullrequests.py:553
+#, python-format
+msgid "Note: Branch %s has another head: %s."
+msgstr ""
+
 #: kallithea/controllers/pullrequests.py:560
-#, python-format
-msgid "Note: Branch %s has another head: %s."
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:567
 msgid "Git pull requests don't support iterating yet."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:569
+#: kallithea/controllers/pullrequests.py:562
 #, python-format
 msgid ""
 "Error: some changesets not found when displaying pull request from %s."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:593
+#: kallithea/controllers/pullrequests.py:586
 msgid "The diff can't be shown - the PR revisions could not be found."
 msgstr ""
 
-#: kallithea/controllers/search.py:136
+#: kallithea/controllers/search.py:132
 msgid "Invalid search query. Try quoting it."
 msgstr "Nieprawidłowe zapytanie. Spróbuj zacytować je."
 
-#: kallithea/controllers/search.py:140
+#: kallithea/controllers/search.py:136
 msgid "The server has no search index."
 msgstr ""
 
-#: kallithea/controllers/search.py:143
+#: kallithea/controllers/search.py:139
 msgid "An error occurred during search operation."
 msgstr "Wystąpił błąd podczas operacji wyszukiwania."
 
-#: kallithea/controllers/summary.py:168
-#: kallithea/templates/summary/summary.html:412
+#: kallithea/controllers/summary.py:169
+#: kallithea/templates/summary/summary.html:410
 msgid "No data ready yet"
 msgstr "Żadne dane nie zostały załadowane"
 
-#: kallithea/controllers/summary.py:171
+#: kallithea/controllers/summary.py:172
 #: kallithea/templates/summary/summary.html:97
 msgid "Statistics are disabled for this repository"
 msgstr "Statystyki są wyłączone dla tego repozytorium"
 
-#: kallithea/controllers/admin/auth_settings.py:137
+#: kallithea/controllers/admin/auth_settings.py:136
 msgid "Auth settings updated successfully"
 msgstr "Ustawienia autentykacji poprawnie zaktualizowane"
 
-#: kallithea/controllers/admin/auth_settings.py:148
+#: kallithea/controllers/admin/auth_settings.py:147
 msgid "error occurred during update of auth settings"
 msgstr "wystąpił błąd podczas uaktualniania ustawień autentykacji"
 
-#: kallithea/controllers/admin/defaults.py:75
+#: kallithea/controllers/admin/defaults.py:74
 msgid "Default settings updated successfully"
 msgstr "Domyślne ustawienia zostały pomyślnie zaktualizowane"
 
-#: kallithea/controllers/admin/defaults.py:90
+#: kallithea/controllers/admin/defaults.py:89
 msgid "Error occurred during update of defaults"
 msgstr "wystąpił błąd podczas aktualizacji wartości domyślnych"
 
 #: kallithea/controllers/admin/gists.py:59
 #: kallithea/controllers/admin/my_account.py:232
-#: kallithea/controllers/admin/users.py:248
+#: kallithea/controllers/admin/users.py:246
 msgid "Forever"
 msgstr "na zawsze"
 
 #: kallithea/controllers/admin/gists.py:60
 #: kallithea/controllers/admin/my_account.py:233
-#: kallithea/controllers/admin/users.py:249
+#: kallithea/controllers/admin/users.py:247
 msgid "5 minutes"
 msgstr "5 minut"
 
 #: kallithea/controllers/admin/gists.py:61
 #: kallithea/controllers/admin/my_account.py:234
-#: kallithea/controllers/admin/users.py:250
+#: kallithea/controllers/admin/users.py:248
 msgid "1 hour"
 msgstr "1 godzina"
 
 #: kallithea/controllers/admin/gists.py:62
 #: kallithea/controllers/admin/my_account.py:235
-#: kallithea/controllers/admin/users.py:251
+#: kallithea/controllers/admin/users.py:249
 msgid "1 day"
 msgstr "1 dzień"
 
 #: kallithea/controllers/admin/gists.py:63
 #: kallithea/controllers/admin/my_account.py:236
-#: kallithea/controllers/admin/users.py:252
+#: kallithea/controllers/admin/users.py:250
 msgid "1 month"
 msgstr "1 miesiąc"
 
 #: kallithea/controllers/admin/gists.py:67
 #: kallithea/controllers/admin/my_account.py:238
-#: kallithea/controllers/admin/users.py:254
+#: kallithea/controllers/admin/users.py:252
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:65
 #: kallithea/templates/admin/users/user_edit_api_keys.html:65
 msgid "Lifetime"
 msgstr "Czas życia"
 
-#: kallithea/controllers/admin/gists.py:142
+#: kallithea/controllers/admin/gists.py:148
 msgid "Error occurred during gist creation"
 msgstr "Wystąpił błąd podczas tworzenia gist"
 
-#: kallithea/controllers/admin/gists.py:158
+#: kallithea/controllers/admin/gists.py:164
 #, python-format
 msgid "Deleted gist %s"
 msgstr "Usuń gist %s"
 
-#: kallithea/controllers/admin/gists.py:198
+#: kallithea/controllers/admin/gists.py:207
 msgid "Unmodified"
 msgstr "Niemodyfikowany"
 
-#: kallithea/controllers/admin/gists.py:228
+#: kallithea/controllers/admin/gists.py:237
 msgid "Successfully updated gist content"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:233
+#: kallithea/controllers/admin/gists.py:242
 msgid "Successfully updated gist data"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:236
+#: kallithea/controllers/admin/gists.py:245
 #, python-format
 msgid "Error occurred during update of gist %s"
 msgstr ""
 
-#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:211
-#: kallithea/model/user.py:232
+#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:205
+#: kallithea/model/user.py:226
 msgid "You can't edit this user since it's crucial for entire application"
 msgstr ""
 "Nie możesz edytować tego użytkownika ponieważ jest kluczowy dla całej "
@@ -564,54 +570,54 @@
 msgstr "Twoje konto zostało pomyślnie zaktualizowane"
 
 #: kallithea/controllers/admin/my_account.py:134
-#: kallithea/controllers/admin/users.py:181
+#: kallithea/controllers/admin/users.py:179
 #, python-format
 msgid "Error occurred during update of user %s"
 msgstr "wystąpił błąd podczas aktualizacji użytkownika %s"
 
 #: kallithea/controllers/admin/my_account.py:168
 msgid "Error occurred during update of user password"
-msgstr ""
+msgstr "Wystąpił błąd w trakcie aktualizacji hasła użytkownika"
 
 #: kallithea/controllers/admin/my_account.py:209
-#: kallithea/controllers/admin/users.py:367
+#: kallithea/controllers/admin/users.py:365
 #, python-format
 msgid "Added email %s to user"
 msgstr "Dodano e-mail %s do użytkownika"
 
 #: kallithea/controllers/admin/my_account.py:215
-#: kallithea/controllers/admin/users.py:373
+#: kallithea/controllers/admin/users.py:371
 msgid "An error occurred during email saving"
 msgstr "Wystąpił błąd podczas zapisywania e-maila"
 
 #: kallithea/controllers/admin/my_account.py:224
-#: kallithea/controllers/admin/users.py:383
+#: kallithea/controllers/admin/users.py:381
 msgid "Removed email from user"
 msgstr "Usunięto e-mail użytkownikowi"
 
 #: kallithea/controllers/admin/my_account.py:248
-#: kallithea/controllers/admin/users.py:271
+#: kallithea/controllers/admin/users.py:269
 msgid "API key successfully created"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:257
-#: kallithea/controllers/admin/users.py:281
+#: kallithea/controllers/admin/users.py:279
 msgid "API key successfully reset"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:261
-#: kallithea/controllers/admin/users.py:285
+#: kallithea/controllers/admin/users.py:283
 msgid "API key successfully deleted"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:281
-#: kallithea/controllers/admin/users.py:456
+#: kallithea/controllers/admin/users.py:454
 #, python-format
 msgid "SSH key %s successfully added"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:293
-#: kallithea/controllers/admin/users.py:470
+#: kallithea/controllers/admin/users.py:468
 #, fuzzy
 #| msgid "Successfully deleted user"
 msgid "SSH key successfully deleted"
@@ -689,11 +695,11 @@
 msgid "Allowed with automatic account activation"
 msgstr "Dozwolona z automatyczną aktywacją konta"
 
-#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1673
+#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1578
 msgid "Manual activation of external account"
 msgstr "Ręczna aktywacja nowego konta"
 
-#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1674
+#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1579
 msgid "Automatic activation of external account"
 msgstr "Automatyczna aktywacja nowego konta"
 
@@ -715,343 +721,335 @@
 msgid "Error occurred during update of permissions"
 msgstr "Wystąpił błąd podczas aktualizacji uprawnień"
 
-#: kallithea/controllers/admin/repo_groups.py:172
+#: kallithea/controllers/admin/repo_groups.py:165
 #, python-format
 msgid "Error occurred during creation of repository group %s"
 msgstr "Wystąpił błąd podczas tworzenia grupy repo %s"
 
-#: kallithea/controllers/admin/repo_groups.py:177
+#: kallithea/controllers/admin/repo_groups.py:172
 #, python-format
 msgid "Created repository group %s"
 msgstr "Utworzono grupę repo %s"
 
-#: kallithea/controllers/admin/repo_groups.py:224
+#: kallithea/controllers/admin/repo_groups.py:219
 #, python-format
 msgid "Updated repository group %s"
 msgstr "Zaktualizowano grupę repo %s"
 
-#: kallithea/controllers/admin/repo_groups.py:240
+#: kallithea/controllers/admin/repo_groups.py:235
 #, python-format
 msgid "Error occurred during update of repository group %s"
 msgstr "Wystąpił błąd podczas aktualizacji grupy repo %s"
 
-#: kallithea/controllers/admin/repo_groups.py:250
+#: kallithea/controllers/admin/repo_groups.py:245
 #, python-format
 msgid "This group contains %s repositories and cannot be deleted"
 msgstr "Ta grupa zawiera %s repozytorium i nie może być usunięta"
 
-#: kallithea/controllers/admin/repo_groups.py:257
+#: kallithea/controllers/admin/repo_groups.py:252
 #, python-format
 msgid "This group contains %s subgroups and cannot be deleted"
 msgstr "Ta grupa zawiera %s repozytorium i nie może być usunięta"
 
-#: kallithea/controllers/admin/repo_groups.py:263
+#: kallithea/controllers/admin/repo_groups.py:258
 #, python-format
 msgid "Removed repository group %s"
 msgstr "Usunięto grupę repo %s"
 
-#: kallithea/controllers/admin/repo_groups.py:268
+#: kallithea/controllers/admin/repo_groups.py:263
 #, python-format
 msgid "Error occurred during deletion of repository group %s"
 msgstr "Wystąpił błąd podczas usuwania z repozytorium grupy %s"
 
-#: kallithea/controllers/admin/repo_groups.py:352
-#: kallithea/controllers/admin/repo_groups.py:382
-#: kallithea/controllers/admin/user_groups.py:294
+#: kallithea/controllers/admin/repo_groups.py:347
+#: kallithea/controllers/admin/repo_groups.py:377
+#: kallithea/controllers/admin/user_groups.py:290
 msgid "Cannot revoke permission for yourself as admin"
 msgstr "Nie można cofnąć zezwolenia dla admina jako admin"
 
-#: kallithea/controllers/admin/repo_groups.py:367
+#: kallithea/controllers/admin/repo_groups.py:362
 msgid "Repository group permissions updated"
 msgstr "Aktualizacja uprawnień grup repozytorium"
 
-#: kallithea/controllers/admin/repo_groups.py:399
-#: kallithea/controllers/admin/repos.py:358
-#: kallithea/controllers/admin/user_groups.py:306
+#: kallithea/controllers/admin/repo_groups.py:394
+#: kallithea/controllers/admin/repos.py:357
+#: kallithea/controllers/admin/user_groups.py:302
 msgid "An error occurred during revoking of permission"
 msgstr "Wystąpił błąd podczas cofania zezwolenia"
 
-#: kallithea/controllers/admin/repos.py:136
+#: kallithea/controllers/admin/repos.py:137
 #, python-format
 msgid "Error creating repository %s"
 msgstr "Błąd podczas tworzenia repozytorium %s"
 
-#: kallithea/controllers/admin/repos.py:194
+#: kallithea/controllers/admin/repos.py:193
 #, python-format
 msgid "Created repository %s from %s"
 msgstr "utworzone repozytorium %s z %s"
 
-#: kallithea/controllers/admin/repos.py:203
+#: kallithea/controllers/admin/repos.py:202
 #, python-format
 msgid "Forked repository %s as %s"
 msgstr "Gałęzi %s w repozytorium %s"
 
-#: kallithea/controllers/admin/repos.py:206
+#: kallithea/controllers/admin/repos.py:205
 #, python-format
 msgid "Created repository %s"
 msgstr "Utworzone repozytorium %s"
 
-#: kallithea/controllers/admin/repos.py:235
+#: kallithea/controllers/admin/repos.py:234
 #, python-format
 msgid "Repository %s updated successfully"
 msgstr "Repozytorium %s zostało pomyślnie zaktualizowane"
 
-#: kallithea/controllers/admin/repos.py:255
+#: kallithea/controllers/admin/repos.py:254
 #, python-format
 msgid "Error occurred during update of repository %s"
 msgstr "Wystąpił błąd podczas aktualizacji repozytorium %s"
 
-#: kallithea/controllers/admin/repos.py:273
+#: kallithea/controllers/admin/repos.py:272
 #, python-format
 msgid "Detached %s forks"
 msgstr "Oderwane rozgałęzienie %s"
 
-#: kallithea/controllers/admin/repos.py:276
+#: kallithea/controllers/admin/repos.py:275
 #, python-format
 msgid "Deleted %s forks"
 msgstr "Usunięte rozgałęzienia %s"
 
-#: kallithea/controllers/admin/repos.py:281
+#: kallithea/controllers/admin/repos.py:280
 #, python-format
 msgid "Deleted repository %s"
 msgstr "Usunięte repozytorium %s"
 
-#: kallithea/controllers/admin/repos.py:284
+#: kallithea/controllers/admin/repos.py:283
 #, python-format
 msgid "Cannot delete repository %s which still has forks"
 msgstr ""
 "Nie można usunąć repozytorium %s nadal zawiera załączniki rozgałęzienia"
 
-#: kallithea/controllers/admin/repos.py:289
+#: kallithea/controllers/admin/repos.py:288
 #, python-format
 msgid "An error occurred during deletion of %s"
 msgstr "Wystąpił błąd podczas usuwania %s"
 
-#: kallithea/controllers/admin/repos.py:329
+#: kallithea/controllers/admin/repos.py:328
 msgid "Repository permissions updated"
 msgstr "Uprawnienia repozytorium zostały zaktualizowane"
 
-#: kallithea/controllers/admin/repos.py:388
+#: kallithea/controllers/admin/repos.py:387
 #, python-format
 msgid "Field validation error: %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:391
+#: kallithea/controllers/admin/repos.py:390
 #, python-format
 msgid "An error occurred during creation of field: %r"
 msgstr "Wystąpił błąd podczas tworzenia pola: %r"
 
-#: kallithea/controllers/admin/repos.py:402
+#: kallithea/controllers/admin/repos.py:401
 msgid "An error occurred during removal of field"
 msgstr "Wystąpił błąd podczas usuwania pola"
 
-#: kallithea/controllers/admin/repos.py:416
+#: kallithea/controllers/admin/repos.py:415
 msgid "-- Not a fork --"
 msgstr "-- Brak rozgałęzienia --"
 
-#: kallithea/controllers/admin/repos.py:448
+#: kallithea/controllers/admin/repos.py:447
 msgid "Updated repository visibility in public journal"
 msgstr "Zaktualizowano widoczność stron w publicznym dzienniku"
 
-#: kallithea/controllers/admin/repos.py:452
+#: kallithea/controllers/admin/repos.py:451
 msgid "An error occurred during setting this repository in public journal"
 msgstr ""
 "Wystąpił błąd podczas ustawiania tego repozytorium w dzienniku publicznym"
 
-#: kallithea/controllers/admin/repos.py:468
+#: kallithea/controllers/admin/repos.py:467
 msgid "Nothing"
 msgstr "Brak"
 
-#: kallithea/controllers/admin/repos.py:470
+#: kallithea/controllers/admin/repos.py:469
 #, python-format
 msgid "Marked repository %s as fork of %s"
 msgstr "Oznaczono %s repo jako rozwidlenie %s"
 
-#: kallithea/controllers/admin/repos.py:477
+#: kallithea/controllers/admin/repos.py:476
 msgid "An error occurred during this operation"
 msgstr "Wystąpił błąd podczas tej operacji"
 
-#: kallithea/controllers/admin/repos.py:490
-msgid "Cache invalidation successful"
-msgstr "Cache wyczyszczony poprawnie"
-
-#: kallithea/controllers/admin/repos.py:494
-msgid "An error occurred during cache invalidation"
-msgstr "Wystąpił błąd podczas unieważniania cache"
-
-#: kallithea/controllers/admin/repos.py:507
+#: kallithea/controllers/admin/repos.py:488
 msgid "Pulled from remote location"
 msgstr "Pobieranie z lokalizacji zdalnej"
 
-#: kallithea/controllers/admin/repos.py:510
+#: kallithea/controllers/admin/repos.py:491
 msgid "An error occurred during pull from remote location"
 msgstr "Wystąpił błąd podczas pobierania z lokalizacji zdalnej"
 
-#: kallithea/controllers/admin/repos.py:541
+#: kallithea/controllers/admin/repos.py:522
 msgid "An error occurred during deletion of repository stats"
 msgstr "Wystąpił błąd podczas usuwania z repozytorium statystyk"
 
-#: kallithea/controllers/admin/settings.py:131
+#: kallithea/controllers/admin/settings.py:132
 msgid "Updated VCS settings"
 msgstr "Aktualizacja ustawień VCS"
 
-#: kallithea/controllers/admin/settings.py:135 kallithea/lib/utils.py:237
+#: kallithea/controllers/admin/settings.py:136
 msgid ""
 "Unable to activate hgsubversion support. The \"hgsubversion\" library is "
 "missing"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:141
-#: kallithea/controllers/admin/settings.py:233
+#: kallithea/controllers/admin/settings.py:142
+#: kallithea/controllers/admin/settings.py:234
 msgid "Error occurred while updating application settings"
 msgstr "Wystąpił błąd podczas aktualizacji ustawień aplikacji"
 
-#: kallithea/controllers/admin/settings.py:176
+#: kallithea/controllers/admin/settings.py:177
 #, python-format
 msgid "Repositories successfully rescanned. Added: %s. Removed: %s."
 msgstr ""
 "Repozytoria z powodzeniem zostały ponownie zeskanowane dodano: %s, "
 "usunięto: %s."
 
-#: kallithea/controllers/admin/settings.py:188
+#: kallithea/controllers/admin/settings.py:189
 #, python-format
 msgid "Invalidated %s repositories"
 msgstr "Unieważnione %s repozytoria"
 
-#: kallithea/controllers/admin/settings.py:229
+#: kallithea/controllers/admin/settings.py:230
 msgid "Updated application settings"
 msgstr "Aktualizacja ustawień aplikacji"
 
-#: kallithea/controllers/admin/settings.py:283
+#: kallithea/controllers/admin/settings.py:284
 msgid "Updated visualisation settings"
 msgstr "Aktualizacja ustawień wizualizacji"
 
-#: kallithea/controllers/admin/settings.py:288
+#: kallithea/controllers/admin/settings.py:289
 msgid "Error occurred during updating visualisation settings"
 msgstr "Wystąpił błąd podczas aktualizacji ustawień wizualizacji"
 
-#: kallithea/controllers/admin/settings.py:312
+#: kallithea/controllers/admin/settings.py:313
 msgid "Please enter email address"
 msgstr "Proszę podać adres email"
 
-#: kallithea/controllers/admin/settings.py:327
+#: kallithea/controllers/admin/settings.py:328
 msgid "Send email task created"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:355
+#: kallithea/controllers/admin/settings.py:356
 msgid "Hook already exists"
 msgstr "Hook już istnieje"
 
-#: kallithea/controllers/admin/settings.py:357
+#: kallithea/controllers/admin/settings.py:358
 msgid "Builtin hooks are read-only. Please use another hook name."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:360
+#: kallithea/controllers/admin/settings.py:361
 msgid "Added new hook"
 msgstr "Dodano nowy hook"
 
-#: kallithea/controllers/admin/settings.py:376
+#: kallithea/controllers/admin/settings.py:377
 msgid "Updated hooks"
 msgstr "Aktualizacja hooku"
 
-#: kallithea/controllers/admin/settings.py:380
+#: kallithea/controllers/admin/settings.py:381
 msgid "Error occurred during hook creation"
 msgstr "Wystąpił błąd podczas tworzenia hooku"
 
-#: kallithea/controllers/admin/settings.py:404
+#: kallithea/controllers/admin/settings.py:405
 msgid "Whoosh reindex task scheduled"
 msgstr "Zadanie ponownej indeksacji whoosh zostało zaplanowane"
 
-#: kallithea/controllers/admin/user_groups.py:138
+#: kallithea/controllers/admin/user_groups.py:134
 #, python-format
 msgid "Created user group %s"
 msgstr "Utworzono grupę użytkowników %s"
 
-#: kallithea/controllers/admin/user_groups.py:151
+#: kallithea/controllers/admin/user_groups.py:147
 #, python-format
 msgid "Error occurred during creation of user group %s"
 msgstr "Wystąpił błąd podczas tworzenia grupy użytkowników %s"
 
-#: kallithea/controllers/admin/user_groups.py:179
+#: kallithea/controllers/admin/user_groups.py:175
 #, python-format
 msgid "Updated user group %s"
 msgstr "Zaktualizowano grupę użytkowników %s"
 
-#: kallithea/controllers/admin/user_groups.py:201
+#: kallithea/controllers/admin/user_groups.py:197
 #, python-format
 msgid "Error occurred during update of user group %s"
 msgstr "Wystąpił błąd podczas aktualizacji grupy użytkowników %s"
 
-#: kallithea/controllers/admin/user_groups.py:212
+#: kallithea/controllers/admin/user_groups.py:208
 msgid "Successfully deleted user group"
 msgstr "Grupa użytkowników została usunięta z powodzeniem"
 
-#: kallithea/controllers/admin/user_groups.py:217
+#: kallithea/controllers/admin/user_groups.py:213
 msgid "An error occurred during deletion of user group"
 msgstr "Wystąpił błąd podczas usuwania grupy użytkowników"
 
-#: kallithea/controllers/admin/user_groups.py:273
+#: kallithea/controllers/admin/user_groups.py:269
 msgid "Target group cannot be the same"
 msgstr "Grupa docelowa nie może być taka sama"
 
-#: kallithea/controllers/admin/user_groups.py:279
+#: kallithea/controllers/admin/user_groups.py:275
 msgid "User group permissions updated"
 msgstr "Aktualizacja uprawnień grupy użytkowników"
 
-#: kallithea/controllers/admin/user_groups.py:388
-#: kallithea/controllers/admin/users.py:338
+#: kallithea/controllers/admin/user_groups.py:384
+#: kallithea/controllers/admin/users.py:336
 msgid "Updated permissions"
 msgstr "Aktualizacja uprawnień"
 
-#: kallithea/controllers/admin/user_groups.py:392
-#: kallithea/controllers/admin/users.py:342
+#: kallithea/controllers/admin/user_groups.py:388
+#: kallithea/controllers/admin/users.py:340
 msgid "An error occurred during permissions saving"
 msgstr "Wystąpił błąd podczas zapisywania uprawnień"
 
-#: kallithea/controllers/admin/users.py:123
+#: kallithea/controllers/admin/users.py:121
 #, python-format
 msgid "Created user %s"
 msgstr "Utworzono użytkownika %s"
 
-#: kallithea/controllers/admin/users.py:138
+#: kallithea/controllers/admin/users.py:136
 #, python-format
 msgid "Error occurred during creation of user %s"
 msgstr "Wystąpił błąd podczas tworzenia użytkownika %s"
 
-#: kallithea/controllers/admin/users.py:162
+#: kallithea/controllers/admin/users.py:160
 msgid "User updated successfully"
 msgstr "Użytkownik został zaktualizowany"
 
-#: kallithea/controllers/admin/users.py:190
+#: kallithea/controllers/admin/users.py:188
 msgid "Successfully deleted user"
 msgstr "Użytkownik został usunięty"
 
-#: kallithea/controllers/admin/users.py:195
+#: kallithea/controllers/admin/users.py:193
 msgid "An error occurred during deletion of user"
 msgstr "Wystąpił błąd podczas usuwania użytkownika"
 
-#: kallithea/controllers/admin/users.py:203
+#: kallithea/controllers/admin/users.py:201
 msgid "The default user cannot be edited"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:409
+#: kallithea/controllers/admin/users.py:407
 #, python-format
 msgid "Added IP address %s to user whitelist"
 msgstr "Dodano ip %s do listy dozwolonych adresów użytkownika"
 
-#: kallithea/controllers/admin/users.py:415
+#: kallithea/controllers/admin/users.py:413
 msgid "An error occurred while adding IP address"
 msgstr "Wystąpił błąd podczas zapisywania adresu IP"
 
-#: kallithea/controllers/admin/users.py:427
+#: kallithea/controllers/admin/users.py:425
 msgid "Removed IP address from user whitelist"
 msgstr "Usunięto adres ip z listy dozwolonych adresów dla użytkownika"
 
-#: kallithea/lib/auth.py:684
+#: kallithea/lib/auth.py:634
 msgid "You need to be a registered user to perform this action"
 msgstr "Musisz być zarejestrowanym użytkownikiem, żeby wykonać to działanie"
 
-#: kallithea/lib/auth.py:712
+#: kallithea/lib/auth.py:662
 msgid "You need to be signed in to view this page"
 msgstr "Musisz być zalogowany, żeby oglądać stronę"
 
@@ -1087,170 +1085,170 @@
 "Lista zmian była zbyt duża i została obcięta, użyj menu porównań żeby "
 "wyświetlić różnice"
 
-#: kallithea/lib/diffs.py:224
+#: kallithea/lib/diffs.py:223
 msgid "No changes detected"
 msgstr "Nie wykryto zmian"
 
-#: kallithea/lib/helpers.py:653
+#: kallithea/lib/helpers.py:670
 #, python-format
 msgid "Deleted branch: %s"
 msgstr "Usunięta gałąź: %s"
 
-#: kallithea/lib/helpers.py:655
+#: kallithea/lib/helpers.py:672
 #, python-format
 msgid "Created tag: %s"
 msgstr "Utworzony tag: %s"
 
-#: kallithea/lib/helpers.py:666
+#: kallithea/lib/helpers.py:683
 #, python-format
 msgid "Changeset %s not found"
 msgstr "Nie znaleziono changeset %s"
 
-#: kallithea/lib/helpers.py:715
+#: kallithea/lib/helpers.py:732
 #, python-format
 msgid "Show all combined changesets %s->%s"
 msgstr "Pokaż wszystkie zestawienia zmian changesets %s->%s"
 
-#: kallithea/lib/helpers.py:721
+#: kallithea/lib/helpers.py:738
 msgid "Compare view"
 msgstr "Wyświetl porównanie"
 
-#: kallithea/lib/helpers.py:740
+#: kallithea/lib/helpers.py:757
 msgid "and"
 msgstr "i"
 
-#: kallithea/lib/helpers.py:741
+#: kallithea/lib/helpers.py:758
 #, python-format
 msgid "%s more"
 msgstr "%s więcej"
 
-#: kallithea/lib/helpers.py:742
+#: kallithea/lib/helpers.py:759
 #: kallithea/templates/changelog/changelog.html:43
 msgid "revisions"
 msgstr "rewizja"
 
-#: kallithea/lib/helpers.py:766
+#: kallithea/lib/helpers.py:783
 #, python-format
 msgid "Fork name %s"
 msgstr "nazwa rozgałęzienia %s"
 
-#: kallithea/lib/helpers.py:787
+#: kallithea/lib/helpers.py:804
 #, fuzzy, python-format
 msgid "Pull request %s"
 msgstr "Połączonych gałęzi #%s"
 
-#: kallithea/lib/helpers.py:797
+#: kallithea/lib/helpers.py:814
 msgid "[deleted] repository"
 msgstr "[usunięte] repozytorium"
 
-#: kallithea/lib/helpers.py:799 kallithea/lib/helpers.py:811
+#: kallithea/lib/helpers.py:816 kallithea/lib/helpers.py:828
 msgid "[created] repository"
 msgstr "[utworzone] repozytorium"
 
-#: kallithea/lib/helpers.py:801
+#: kallithea/lib/helpers.py:818
 msgid "[created] repository as fork"
 msgstr "[utworzone] repozytorium jako rozgałęzienie"
 
-#: kallithea/lib/helpers.py:803 kallithea/lib/helpers.py:813
+#: kallithea/lib/helpers.py:820 kallithea/lib/helpers.py:830
 msgid "[forked] repository"
 msgstr "[rozgałęzione] repozytorium"
 
-#: kallithea/lib/helpers.py:805 kallithea/lib/helpers.py:815
+#: kallithea/lib/helpers.py:822 kallithea/lib/helpers.py:832
 msgid "[updated] repository"
 msgstr "[zaktualizowane] repozytorium"
 
-#: kallithea/lib/helpers.py:807
+#: kallithea/lib/helpers.py:824
 msgid "[downloaded] archive from repository"
 msgstr "[pobierz] archiwum z repozytorium"
 
-#: kallithea/lib/helpers.py:809
+#: kallithea/lib/helpers.py:826
 msgid "[delete] repository"
 msgstr "[skasowane] repozytorium"
 
-#: kallithea/lib/helpers.py:817
+#: kallithea/lib/helpers.py:834
 msgid "[created] user"
 msgstr "[utworzony] użytkownik"
 
-#: kallithea/lib/helpers.py:819
+#: kallithea/lib/helpers.py:836
 msgid "[updated] user"
 msgstr "[zaktualizowany] użytkownik"
 
-#: kallithea/lib/helpers.py:821
+#: kallithea/lib/helpers.py:838
 msgid "[created] user group"
 msgstr "[utworzona] grupa użytkowników"
 
-#: kallithea/lib/helpers.py:823
+#: kallithea/lib/helpers.py:840
 msgid "[updated] user group"
 msgstr "[zaktualizowana] grupa użytkowników"
 
-#: kallithea/lib/helpers.py:825
+#: kallithea/lib/helpers.py:842
 msgid "[commented] on revision in repository"
 msgstr "[komentarz] do zmiany w repozytorium"
 
-#: kallithea/lib/helpers.py:827
+#: kallithea/lib/helpers.py:844
 msgid "[commented] on pull request for"
 msgstr "[komentarz] wniosek o połączenie gałęzi"
 
-#: kallithea/lib/helpers.py:829
+#: kallithea/lib/helpers.py:846
 msgid "[closed] pull request for"
 msgstr "[zamknięty] wniosek o połączenie gałęzi"
 
-#: kallithea/lib/helpers.py:831
+#: kallithea/lib/helpers.py:848
 msgid "[pushed] into"
 msgstr "[wysłane zmiany] w"
 
-#: kallithea/lib/helpers.py:833
+#: kallithea/lib/helpers.py:850
 msgid "[committed via Kallithea] into repository"
 msgstr "[synchronizacja przez Kallithea] z repozytorium"
 
-#: kallithea/lib/helpers.py:835
+#: kallithea/lib/helpers.py:852
 msgid "[pulled from remote] into repository"
 msgstr "[pobieranie z zdalnego] do repozytorium"
 
-#: kallithea/lib/helpers.py:837
+#: kallithea/lib/helpers.py:854
 msgid "[pulled] from"
 msgstr "[pobrano]"
 
-#: kallithea/lib/helpers.py:839
+#: kallithea/lib/helpers.py:856
 msgid "[started following] repository"
 msgstr "[start następnego] repozytorium"
 
-#: kallithea/lib/helpers.py:841
+#: kallithea/lib/helpers.py:858
 msgid "[stopped following] repository"
 msgstr "[zatrzymany po] repozytorium"
 
-#: kallithea/lib/helpers.py:961
+#: kallithea/lib/helpers.py:975
 #, python-format
 msgid " and %s more"
 msgstr " i %s więcej"
 
-#: kallithea/lib/helpers.py:965
+#: kallithea/lib/helpers.py:979
 #: kallithea/templates/compare/compare_diff.html:69
 #: kallithea/templates/pullrequests/pullrequest_show.html:297
 msgid "No files"
 msgstr "Brak plików"
 
-#: kallithea/lib/helpers.py:990
+#: kallithea/lib/helpers.py:1004
 msgid "new file"
 msgstr "nowy plik"
 
-#: kallithea/lib/helpers.py:993
+#: kallithea/lib/helpers.py:1007
 msgid "mod"
 msgstr "modyfikuj"
 
-#: kallithea/lib/helpers.py:996
+#: kallithea/lib/helpers.py:1010
 msgid "del"
 msgstr "kasuj"
 
-#: kallithea/lib/helpers.py:999
+#: kallithea/lib/helpers.py:1013
 msgid "rename"
 msgstr "zmień nazwę"
 
-#: kallithea/lib/helpers.py:1004
+#: kallithea/lib/helpers.py:1018
 msgid "chmod"
 msgstr "chmod"
 
-#: kallithea/lib/helpers.py:1297
+#: kallithea/lib/helpers.py:1323
 #, python-format
 msgid ""
 "%s repository is not mapped to db perhaps it was created or renamed from "
@@ -1261,34 +1259,36 @@
 "zmienione z systemie plików proszę uruchomić aplikację ponownie, aby "
 "ponownie przeskanować repozytoria"
 
-#: kallithea/lib/ssh.py:73
+#: kallithea/lib/ssh.py:75
 msgid "SSH key is missing"
 msgstr ""
 
-#: kallithea/lib/ssh.py:77
-msgid "Incorrect SSH key - it must have both a key type and a base64 part"
-msgstr ""
-
-#: kallithea/lib/ssh.py:81
+#: kallithea/lib/ssh.py:79
+msgid ""
+"Incorrect SSH key - it must have both a key type and a base64 part, like "
+"'ssh-rsa ASRNeaZu4FA...xlJp='"
+msgstr ""
+
+#: kallithea/lib/ssh.py:83
 msgid "Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'"
 msgstr ""
 
-#: kallithea/lib/ssh.py:84
+#: kallithea/lib/ssh.py:86
 #, python-format
 msgid "Incorrect SSH key - unexpected characters in base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:89
+#: kallithea/lib/ssh.py:91
 #, python-format
 msgid "Incorrect SSH key - failed to decode base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:92
+#: kallithea/lib/ssh.py:94
 #, python-format
 msgid "Incorrect SSH key - base64 part is not %r as claimed but %r"
 msgstr ""
 
-#: kallithea/lib/utils2.py:334
+#: kallithea/lib/utils2.py:253
 #, python-format
 msgid "%d year"
 msgid_plural "%d years"
@@ -1296,7 +1296,7 @@
 msgstr[1] "%d lata"
 msgstr[2] "%d lat"
 
-#: kallithea/lib/utils2.py:335
+#: kallithea/lib/utils2.py:254
 #, python-format
 msgid "%d month"
 msgid_plural "%d months"
@@ -1304,7 +1304,7 @@
 msgstr[1] "%d miesięcy"
 msgstr[2] "%d miesięcy"
 
-#: kallithea/lib/utils2.py:336
+#: kallithea/lib/utils2.py:255
 #, python-format
 msgid "%d day"
 msgid_plural "%d days"
@@ -1312,7 +1312,7 @@
 msgstr[1] "%d dni"
 msgstr[2] "%d dni"
 
-#: kallithea/lib/utils2.py:337
+#: kallithea/lib/utils2.py:256
 #, python-format
 msgid "%d hour"
 msgid_plural "%d hours"
@@ -1320,7 +1320,7 @@
 msgstr[1] "%d godziny"
 msgstr[2] "%d godzin"
 
-#: kallithea/lib/utils2.py:338
+#: kallithea/lib/utils2.py:257
 #, python-format
 msgid "%d minute"
 msgid_plural "%d minutes"
@@ -1328,7 +1328,7 @@
 msgstr[1] "%d minuty"
 msgstr[2] "%d minut"
 
-#: kallithea/lib/utils2.py:339
+#: kallithea/lib/utils2.py:258
 #, python-format
 msgid "%d second"
 msgid_plural "%d seconds"
@@ -1336,27 +1336,27 @@
 msgstr[1] "%d sekund"
 msgstr[2] "%d sekund"
 
-#: kallithea/lib/utils2.py:355
+#: kallithea/lib/utils2.py:274
 #, python-format
 msgid "in %s"
 msgstr "w %s"
 
-#: kallithea/lib/utils2.py:357
+#: kallithea/lib/utils2.py:276
 #, python-format
 msgid "%s ago"
 msgstr "%s temu"
 
-#: kallithea/lib/utils2.py:359
+#: kallithea/lib/utils2.py:278
 #, python-format
 msgid "in %s and %s"
 msgstr "w %s i %s"
 
-#: kallithea/lib/utils2.py:362
+#: kallithea/lib/utils2.py:281
 #, python-format
 msgid "%s and %s ago"
 msgstr "%s i %s temu"
 
-#: kallithea/lib/utils2.py:365
+#: kallithea/lib/utils2.py:284
 msgid "just now"
 msgstr "przed chwilą"
 
@@ -1365,144 +1365,144 @@
 msgid "on line %s"
 msgstr "widziany %s"
 
-#: kallithea/model/comment.py:221 kallithea/model/pull_request.py:114
+#: kallithea/model/comment.py:219 kallithea/model/pull_request.py:112
 msgid "[Mention]"
 msgstr "[Wymieniony]"
 
-#: kallithea/model/db.py:1496
+#: kallithea/model/db.py:1411
 msgid "top level"
 msgstr "najwyższy poziom"
 
-#: kallithea/model/db.py:1637
+#: kallithea/model/db.py:1542
 msgid "Kallithea Administrator"
 msgstr "Administrator Kallithea"
 
-#: kallithea/model/db.py:1639
+#: kallithea/model/db.py:1544
 msgid "Default user has no access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1640
+#: kallithea/model/db.py:1545
 msgid "Default user has read access to new repositories"
 msgstr "Użytkownik domyślny ma dostęp do odczytu nowych repozytoriów"
 
-#: kallithea/model/db.py:1641
+#: kallithea/model/db.py:1546
 msgid "Default user has write access to new repositories"
 msgstr "Użytkownik domyślny ma dostęp do zapisu nowych repozytoriów"
 
-#: kallithea/model/db.py:1642
+#: kallithea/model/db.py:1547
 msgid "Default user has admin access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1644
+#: kallithea/model/db.py:1549
 msgid "Default user has no access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1645
+#: kallithea/model/db.py:1550
 msgid "Default user has read access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1646
+#: kallithea/model/db.py:1551
 msgid "Default user has write access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1647
+#: kallithea/model/db.py:1552
 msgid "Default user has admin access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1649
+#: kallithea/model/db.py:1554
 msgid "Default user has no access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1650
+#: kallithea/model/db.py:1555
 msgid "Default user has read access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1651
+#: kallithea/model/db.py:1556
 msgid "Default user has write access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1652
+#: kallithea/model/db.py:1557
 msgid "Default user has admin access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1654
+#: kallithea/model/db.py:1559
 #, fuzzy
 msgid "Only admins can create repository groups"
 msgstr "Tylko admini mogą tworzyć grupy repozytoriów"
 
-#: kallithea/model/db.py:1655
+#: kallithea/model/db.py:1560
 #, fuzzy
 msgid "Non-admins can create repository groups"
 msgstr ""
 "Użytkownicy bez uprawnień administratora mogą tworzyć grupy repozytoriów"
 
-#: kallithea/model/db.py:1657
+#: kallithea/model/db.py:1562
 #, fuzzy
 msgid "Only admins can create user groups"
 msgstr "Tylko admini mogą tworzyć grupy użytkowników"
 
-#: kallithea/model/db.py:1658
+#: kallithea/model/db.py:1563
 #, fuzzy
 msgid "Non-admins can create user groups"
 msgstr ""
 "Użytkownicy bez uprawnień administratora mogą tworzyć grupy użytkowników"
 
-#: kallithea/model/db.py:1660
+#: kallithea/model/db.py:1565
 msgid "Only admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1661
+#: kallithea/model/db.py:1566
 msgid "Non-admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1663
+#: kallithea/model/db.py:1568
 msgid ""
 "Repository creation enabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1664
+#: kallithea/model/db.py:1569
 msgid ""
 "Repository creation disabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1666
+#: kallithea/model/db.py:1571
 #, fuzzy
 msgid "Only admins can fork repositories"
 msgstr "Tylko admini mogą rozgałęziać repozytoria"
 
-#: kallithea/model/db.py:1667
+#: kallithea/model/db.py:1572
 #, fuzzy
 msgid "Non-admins can fork repositories"
 msgstr ""
 "Użytkownicy bez uprawnień administratora mogą rozgałęziać repozytoria"
 
-#: kallithea/model/db.py:1669
+#: kallithea/model/db.py:1574
 msgid "Registration disabled"
 msgstr "Rejestracja wyłączona"
 
-#: kallithea/model/db.py:1670
+#: kallithea/model/db.py:1575
 msgid "User registration with manual account activation"
 msgstr "Rejestracja użytkownika z ręczną aktywacją konta"
 
-#: kallithea/model/db.py:1671
+#: kallithea/model/db.py:1576
 msgid "User registration with automatic account activation"
 msgstr "Rejestracja użytkownika z automatyczną aktywacją konta"
 
-#: kallithea/model/db.py:2206
+#: kallithea/model/db.py:1992
 msgid "Not reviewed"
 msgstr "Brak Korekty"
 
-#: kallithea/model/db.py:2207
+#: kallithea/model/db.py:1993
 msgid "Under review"
 msgstr "Objęty Przeglądem"
 
-#: kallithea/model/db.py:2208
+#: kallithea/model/db.py:1994
 #, fuzzy
 #| msgid "Approved"
 msgid "Not approved"
 msgstr "Niezaakceptowano"
 
-#: kallithea/model/db.py:2209
+#: kallithea/model/db.py:1995
 msgid "Approved"
 msgstr "Zaakceptowano"
 
@@ -1528,7 +1528,7 @@
 msgid "Name must not contain only digits"
 msgstr ""
 
-#: kallithea/model/notification.py:164
+#: kallithea/model/notification.py:162
 #, fuzzy, python-format
 #| msgid "[Comment] %(repo_name)s pull request %(pr_nice_id)s from %(ref)s"
 msgid ""
@@ -1536,118 +1536,118 @@
 "%(branch)s"
 msgstr "[komentarz] wniosek o połączenie gałęzi"
 
-#: kallithea/model/notification.py:167
+#: kallithea/model/notification.py:165
 #, python-format
 msgid "New user %(new_username)s registered"
 msgstr "Użytkownik %(new_username)s zarejestrował się"
 
-#: kallithea/model/notification.py:169
+#: kallithea/model/notification.py:167
 #, python-format
 msgid ""
 "[Review] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:170
+#: kallithea/model/notification.py:168
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:183
+#: kallithea/model/notification.py:188
 #, fuzzy
 msgid "Closing"
 msgstr "Zamykanie"
 
-#: kallithea/model/pull_request.py:73
+#: kallithea/model/pull_request.py:72
 #, fuzzy, python-format
 msgid ""
 "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s"
 msgstr ""
 "%(user)s chce żeby przejrzeć nowe gałęzie %(pr_nice_id)s: %(pr_title)s"
 
-#: kallithea/model/pull_request.py:209
+#: kallithea/model/pull_request.py:207
 #, fuzzy
 #| msgid "Create Pull Request"
 msgid "Cannot create empty pull request"
 msgstr "Nie można stworzyć pustego żądania połączenia gałęzi"
 
-#: kallithea/model/pull_request.py:217
+#: kallithea/model/pull_request.py:215
 #, python-format
 msgid ""
 "Cannot create pull request - criss cross merge detected, please merge a "
 "later %s revision to %s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:245 kallithea/model/pull_request.py:380
+#: kallithea/model/pull_request.py:243 kallithea/model/pull_request.py:378
 #, fuzzy
 #| msgid "Confirm to delete this pull request"
 msgid "You are not authorized to create the pull request"
 msgstr "Nie masz uprawnień, aby stworzyć żądanie połączenia gałęzi"
 
-#: kallithea/model/pull_request.py:339
+#: kallithea/model/pull_request.py:337
 msgid "Missing changesets since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:346
+#: kallithea/model/pull_request.py:344
 #, python-format
 msgid "New changesets on %s %s since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:353
+#: kallithea/model/pull_request.py:351
 msgid "Ancestor didn't change - diff since previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:360
+#: kallithea/model/pull_request.py:358
 #, python-format
 msgid ""
 "This iteration is based on another %s revision and there is no simple "
 "diff."
 msgstr ""
 
-#: kallithea/model/pull_request.py:362
+#: kallithea/model/pull_request.py:360
 #, python-format
 msgid "No changes found on %s %s since previous iteration."
 msgstr ""
 
-#: kallithea/model/pull_request.py:388
+#: kallithea/model/pull_request.py:386
 #, python-format
 msgid "Closed, next iteration: %s ."
 msgstr ""
 
-#: kallithea/model/scm.py:668
+#: kallithea/model/scm.py:655
 msgid "latest tip"
 msgstr "ostatni tip"
 
-#: kallithea/model/ssh_key.py:56
+#: kallithea/model/ssh_key.py:57
 #, python-format
 msgid "SSH key %r is invalid: %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:68
+#: kallithea/model/ssh_key.py:69
 #, python-format
 msgid "SSH key %s is already used by %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:89
+#: kallithea/model/ssh_key.py:88
 #, fuzzy, python-format
 #| msgid "Changeset %s not found"
-msgid "SSH key %r not found"
+msgid "SSH key with fingerprint %r found"
 msgstr "Nie znaleziono changeset %s"
 
-#: kallithea/model/user.py:186
+#: kallithea/model/user.py:180
 msgid "New user registration"
 msgstr "nowy użytkownik się zarejestrował"
 
-#: kallithea/model/user.py:250
+#: kallithea/model/user.py:244
 msgid ""
 "You can't remove this user since it is crucial for the entire application"
 msgstr ""
 "Nie możesz usunąć tego użytkownika ponieważ jest kluczowy dla całej "
 "aplikacji"
 
-#: kallithea/model/user.py:255
+#: kallithea/model/user.py:249
 #, fuzzy, python-format
 msgid ""
 "User \"%s\" still owns %s repositories and cannot be removed. Switch "
@@ -1656,7 +1656,7 @@
 "użytkownik \"%s\" wciąż posiada repozytoria następujące %s i nie może "
 "zostać usunięty. Zmień właściciela lub usuń te repozytoria: %s"
 
-#: kallithea/model/user.py:260
+#: kallithea/model/user.py:254
 #, fuzzy, python-format
 msgid ""
 "User \"%s\" still owns %s repository groups and cannot be removed. Switch "
@@ -1665,7 +1665,7 @@
 "użytkownik \"%s\" wciąż posiada repozytoria następujące %s i nie może "
 "zostać usunięty. Zmień właściciela lub usuń te repozytoria: %s"
 
-#: kallithea/model/user.py:267
+#: kallithea/model/user.py:261
 #, fuzzy, python-format
 msgid ""
 "User \"%s\" still owns %s user groups and cannot be removed. Switch "
@@ -1674,37 +1674,37 @@
 "użytkownik \"%s\" wciąż posiada repozytoria następujące %s i nie może "
 "zostać usunięty. Zmień właściciela lub usuń te grupy użytkowników: %s"
 
-#: kallithea/model/user.py:361
+#: kallithea/model/user.py:355
 msgid "Password reset link"
 msgstr "łącze resetowania hasła"
 
-#: kallithea/model/user.py:408
+#: kallithea/model/user.py:402
 #, fuzzy
 msgid "Password reset notification"
 msgstr "Powiadomienie o resetowaniu hasła"
 
-#: kallithea/model/user.py:409
+#: kallithea/model/user.py:403
 #, python-format
 msgid ""
 "The password to your account %s has been changed using password reset "
 "form."
 msgstr ""
 
-#: kallithea/model/validators.py:52 kallithea/model/validators.py:53
+#: kallithea/model/validators.py:53 kallithea/model/validators.py:54
 msgid "Value cannot be an empty list"
 msgstr "Wartość listy nie może być pusta"
 
-#: kallithea/model/validators.py:72
+#: kallithea/model/validators.py:73
 #, python-format
 msgid "Username \"%(username)s\" already exists"
 msgstr "Użytkownik \"%(username)s\" już istnieje"
 
-#: kallithea/model/validators.py:74
+#: kallithea/model/validators.py:75
 #, fuzzy, python-format
 msgid "Username \"%(username)s\" cannot be used"
 msgstr "Nazwa użytkownika \"%(username)s\" nie może być użyta"
 
-#: kallithea/model/validators.py:76
+#: kallithea/model/validators.py:77
 #, fuzzy
 msgid ""
 "Username may only contain alphanumeric characters underscores, periods or "
@@ -1714,25 +1714,25 @@
 "kropki lub myślniki i musi zaczynać się znakiem alfanumerycznym lub "
 "podkreśleniem"
 
-#: kallithea/model/validators.py:103
+#: kallithea/model/validators.py:104
 msgid "The input is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:110
+#: kallithea/model/validators.py:111
 #, python-format
 msgid "Username %(username)s is not valid"
 msgstr "Nazwa użytkownika %(username)s jest nieprawidłowa"
 
-#: kallithea/model/validators.py:131
+#: kallithea/model/validators.py:132
 msgid "Invalid user group name"
 msgstr "Niewłaściwa nazwa grupy"
 
-#: kallithea/model/validators.py:132
+#: kallithea/model/validators.py:133
 #, python-format
 msgid "User group \"%(usergroup)s\" already exists"
 msgstr "Nazwa grupy \"%(usergroup)s\" już istnieje"
 
-#: kallithea/model/validators.py:134
+#: kallithea/model/validators.py:135
 msgid ""
 "user group name may only contain alphanumeric characters underscores, "
 "periods or dashes and must begin with alphanumeric character"
@@ -1740,102 +1740,102 @@
 "nazwa grupy może zawierać tylko znaki alfanumeryczne, podkreślenia, "
 "kropki lub myślniki i musi zaczynać się znakiem alfanumerycznym"
 
-#: kallithea/model/validators.py:174
+#: kallithea/model/validators.py:175
 msgid "Cannot assign this group as parent"
 msgstr "Nie można przypisać do tej grupy jako rodzic"
 
-#: kallithea/model/validators.py:175
+#: kallithea/model/validators.py:176
 #, python-format
 msgid "Group \"%(group_name)s\" already exists"
 msgstr "Nazwa grupy \"%(group_name)s\" już istnieje"
 
-#: kallithea/model/validators.py:177
+#: kallithea/model/validators.py:178
 #, python-format
 msgid "Repository with name \"%(group_name)s\" already exists"
 msgstr "Repozytorium o nazwie \"%(group_name)s\" już istnieje"
 
-#: kallithea/model/validators.py:233
+#: kallithea/model/validators.py:230
 msgid "Invalid characters (non-ascii) in password"
 msgstr "Nieprawidłowe znaki (nie-ascii) w haśle"
 
-#: kallithea/model/validators.py:248
+#: kallithea/model/validators.py:245
 msgid "Invalid old password"
 msgstr ""
 
-#: kallithea/model/validators.py:264
+#: kallithea/model/validators.py:261
 msgid "Passwords do not match"
 msgstr "Hasła różnią się"
 
-#: kallithea/model/validators.py:279
+#: kallithea/model/validators.py:276
 #, fuzzy
 msgid "Invalid username or password"
 msgstr "nieprawidłowa nazwa użytkownika lub hasło"
 
-#: kallithea/model/validators.py:313
+#: kallithea/model/validators.py:310
 #, fuzzy, python-format
 msgid "Repository name %(repo)s is not allowed"
 msgstr "Nazwa repozytorium  %(repo)s jest zabroniona"
 
-#: kallithea/model/validators.py:315
+#: kallithea/model/validators.py:312
 #, python-format
 msgid "Repository named %(repo)s already exists"
 msgstr "Repozytorium o nazwie %(repo)s już istnieje"
 
-#: kallithea/model/validators.py:316
+#: kallithea/model/validators.py:313
 #, python-format
 msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
 msgstr "Repozytorium  \"%(repo)s\" już istnieje w grupie \"%(group)s\""
 
-#: kallithea/model/validators.py:318
+#: kallithea/model/validators.py:315
 #, python-format
 msgid "Repository group with name \"%(repo)s\" already exists"
 msgstr "Grupa repozytoriów z nazwą \"%(repo)s\" już istnieje"
 
-#: kallithea/model/validators.py:404
+#: kallithea/model/validators.py:401
 #, fuzzy
 msgid "Invalid repository URL"
 msgstr "Nieprawidłowy URL repozytorium"
 
-#: kallithea/model/validators.py:405
+#: kallithea/model/validators.py:402
 msgid ""
 "Invalid repository URL. It must be a valid http, https, ssh, svn+http or "
 "svn+https URL"
 msgstr ""
 
-#: kallithea/model/validators.py:430
+#: kallithea/model/validators.py:427
 msgid "Fork has to be the same type as parent"
 msgstr "Fork musi być tego samego typu, jak rodzic"
 
-#: kallithea/model/validators.py:445
+#: kallithea/model/validators.py:442
 msgid "You don't have permissions to create repository in this group"
 msgstr "Nie masz uprawnień do tworzenia repozytorium w tej grupie"
 
-#: kallithea/model/validators.py:447
+#: kallithea/model/validators.py:444
 msgid "no permission to create repository in root location"
 msgstr "nie masz uprawnień do tworzenia repozytorium w tej lokacji roota"
 
-#: kallithea/model/validators.py:497
+#: kallithea/model/validators.py:494
 msgid "You don't have permissions to create a group in this location"
 msgstr "Nie masz uprawnień do tworzenia grupy w tym miejscu"
 
-#: kallithea/model/validators.py:537
+#: kallithea/model/validators.py:534
 msgid "This username or user group name is not valid"
 msgstr "Ta nazwa użytkownika lub grupy użytkowników nie jest prawidłowa"
 
-#: kallithea/model/validators.py:630
+#: kallithea/model/validators.py:627
 msgid "This is not a valid path"
 msgstr "To nie jest prawidłowa ścieżka"
 
-#: kallithea/model/validators.py:647
+#: kallithea/model/validators.py:644
 msgid "This email address is already in use"
 msgstr "Ten adres e-mail jest już zajęty"
 
-#: kallithea/model/validators.py:667
+#: kallithea/model/validators.py:664
 #, python-format
 msgid "Email address \"%(email)s\" not found"
 msgstr "e-mail \"%(email)s\" nie istnieje"
 
-#: kallithea/model/validators.py:704
+#: kallithea/model/validators.py:701
 msgid ""
 "The LDAP Login attribute of the CN must be specified - this is the name "
 "of the attribute that is equivalent to \"username\""
@@ -1843,28 +1843,28 @@
 "Atrybut logowania CN do LDAP należy określić, jest to nazwa atrybutu, "
 "który jest odpowiednikiem  \"username\""
 
-#: kallithea/model/validators.py:716
+#: kallithea/model/validators.py:713
 msgid "Please enter a valid IPv4 or IPv6 address"
 msgstr "Proszę podać poprawny adres IPv4 lub IPv6"
 
-#: kallithea/model/validators.py:717
+#: kallithea/model/validators.py:714
 #, python-format
 msgid ""
 "The network size (bits) must be within the range of 0-32 (not %(bits)r)"
 msgstr ""
 "Rozmiar sieci (bits) może mieścić się w zakresie od 0-32 (nie %(bits)r)"
 
-#: kallithea/model/validators.py:750
+#: kallithea/model/validators.py:747
 msgid "Key name can only consist of letters, underscore, dash or numbers"
 msgstr ""
 "Klucz nazwy może składać się tylko z liter, podkreślenia, myślnika lub "
 "numerów"
 
-#: kallithea/model/validators.py:764
+#: kallithea/model/validators.py:761
 msgid "Filename cannot be inside a directory"
 msgstr "Nazwa pliku nie może znajdować się w katalogu"
 
-#: kallithea/model/validators.py:780
+#: kallithea/model/validators.py:777
 #, python-format
 msgid "Plugins %(loaded)s and %(next_to_load)s both export the same name"
 msgstr ""
@@ -1924,7 +1924,7 @@
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:60
 #: kallithea/templates/email_templates/pull_request.html:37
 #: kallithea/templates/forks/fork.html:34
-#: kallithea/templates/index_base.html:58
+#: kallithea/templates/index_base.html:59
 #: kallithea/templates/pullrequests/pullrequest.html:33
 #: kallithea/templates/pullrequests/pullrequest_show.html:38
 #: kallithea/templates/pullrequests/pullrequest_show.html:59
@@ -1932,14 +1932,14 @@
 msgid "Description"
 msgstr "Opis"
 
-#: kallithea/templates/index_base.html:60
+#: kallithea/templates/index_base.html:61
 msgid "Last Change"
 msgstr "Ostatnia aktywność"
 
 #: kallithea/templates/admin/my_account/my_account_repos.html:15
 #: kallithea/templates/admin/my_account/my_account_watched.html:15
 #: kallithea/templates/admin/repos/repos.html:41
-#: kallithea/templates/index_base.html:62
+#: kallithea/templates/index_base.html:63
 msgid "Tip"
 msgstr "Ostatnia zmiana"
 
@@ -1949,7 +1949,7 @@
 #: kallithea/templates/admin/repos/repos.html:42
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:8
 #: kallithea/templates/admin/user_groups/user_groups.html:42
-#: kallithea/templates/index_base.html:63
+#: kallithea/templates/index_base.html:64
 #: kallithea/templates/pullrequests/pullrequest_data.html:16
 #: kallithea/templates/pullrequests/pullrequest_show.html:124
 #: kallithea/templates/pullrequests/pullrequest_show.html:219
@@ -1973,7 +1973,7 @@
 #: kallithea/templates/admin/users/user_edit_profile.html:18
 #: kallithea/templates/admin/users/users.html:37
 #: kallithea/templates/base/base.html:364
-#: kallithea/templates/email_templates/registration.html:11
+#: kallithea/templates/email_templates/registration.html:12
 #: kallithea/templates/login.html:28 kallithea/templates/register.html:31
 msgid "Username"
 msgstr "Nazwa użytkownika"
@@ -2100,7 +2100,7 @@
 #: kallithea/templates/admin/settings/settings.html:31
 #: kallithea/templates/admin/users/user_add.html:62
 #: kallithea/templates/admin/users/user_edit_profile.html:25
-#: kallithea/templates/email_templates/registration.html:33
+#: kallithea/templates/email_templates/registration.html:34
 #: kallithea/templates/register.html:66
 msgid "Email"
 msgstr "E-mail"
@@ -2350,7 +2350,7 @@
 msgstr "Utwórz Nowy Gist"
 
 #: kallithea/templates/admin/gists/index.html:51
-#: kallithea/templates/data_table/_dt_elements.html:78
+#: kallithea/templates/data_table/_dt_elements.html:84
 msgid "Created"
 msgstr "Utworzono"
 
@@ -2434,13 +2434,13 @@
 #: kallithea/templates/admin/users/user_edit_ips.html:21
 #: kallithea/templates/changeset/changeset_file_comment.html:30
 #: kallithea/templates/changeset/changeset_file_comment.html:121
-#: kallithea/templates/data_table/_dt_elements.html:69
-#: kallithea/templates/data_table/_dt_elements.html:89
-#: kallithea/templates/data_table/_dt_elements.html:91
-#: kallithea/templates/data_table/_dt_elements.html:101
-#: kallithea/templates/data_table/_dt_elements.html:103
-#: kallithea/templates/data_table/_dt_elements.html:120
-#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:75
+#: kallithea/templates/data_table/_dt_elements.html:95
+#: kallithea/templates/data_table/_dt_elements.html:97
+#: kallithea/templates/data_table/_dt_elements.html:107
+#: kallithea/templates/data_table/_dt_elements.html:109
+#: kallithea/templates/data_table/_dt_elements.html:126
+#: kallithea/templates/data_table/_dt_elements.html:128
 #: kallithea/templates/files/files_source.html:35
 #: kallithea/templates/files/files_source.html:38
 #: kallithea/templates/files/files_source.html:41
@@ -2456,14 +2456,14 @@
 #: kallithea/templates/base/perms_summary.html:44
 #: kallithea/templates/base/perms_summary.html:81
 #: kallithea/templates/base/perms_summary.html:83
-#: kallithea/templates/data_table/_dt_elements.html:63
-#: kallithea/templates/data_table/_dt_elements.html:64
-#: kallithea/templates/data_table/_dt_elements.html:85
-#: kallithea/templates/data_table/_dt_elements.html:86
-#: kallithea/templates/data_table/_dt_elements.html:97
-#: kallithea/templates/data_table/_dt_elements.html:98
-#: kallithea/templates/data_table/_dt_elements.html:116
-#: kallithea/templates/data_table/_dt_elements.html:117
+#: kallithea/templates/data_table/_dt_elements.html:69
+#: kallithea/templates/data_table/_dt_elements.html:70
+#: kallithea/templates/data_table/_dt_elements.html:91
+#: kallithea/templates/data_table/_dt_elements.html:92
+#: kallithea/templates/data_table/_dt_elements.html:103
+#: kallithea/templates/data_table/_dt_elements.html:104
+#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:123
 #: kallithea/templates/files/diff_2way.html:56
 #: kallithea/templates/files/files_source.html:37
 #: kallithea/templates/files/files_source.html:40
@@ -2775,7 +2775,7 @@
 #: kallithea/templates/admin/permissions/permissions_globals.html:27
 #: kallithea/templates/admin/repos/repo_add_base.html:28
 #: kallithea/templates/admin/repos/repo_edit_settings.html:33
-#: kallithea/templates/data_table/_dt_elements.html:134
+#: kallithea/templates/data_table/_dt_elements.html:140
 #: kallithea/templates/forks/fork.html:42
 msgid "Repository group"
 msgstr "Repozytorium grupy"
@@ -2799,7 +2799,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:40
-#: kallithea/templates/data_table/_dt_elements.html:141
+#: kallithea/templates/data_table/_dt_elements.html:147
 msgid "User group"
 msgstr "Grupa użytkownika"
 
@@ -2976,7 +2976,7 @@
 msgstr "Utworzono"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:121
+#: kallithea/templates/data_table/_dt_elements.html:127
 #, python-format
 msgid "Confirm to delete this group: %s with %s repository"
 msgid_plural "Confirm to delete this group: %s with %s repositories"
@@ -3158,14 +3158,10 @@
 msgstr "Dodatkowe pola"
 
 #: kallithea/templates/admin/repos/repo_edit.html:37
-msgid "Caches"
-msgstr ""
+msgid "Remote"
+msgstr "Zdalnie"
 
 #: kallithea/templates/admin/repos/repo_edit.html:40
-msgid "Remote"
-msgstr "Zdalnie"
-
-#: kallithea/templates/admin/repos/repo_edit.html:43
 #: kallithea/templates/summary/statistics.html:8
 #: kallithea/templates/summary/summary.html:169
 #: kallithea/templates/summary/summary.html:170
@@ -3210,7 +3206,7 @@
 "wszystkich w dzienniku publicznym."
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:46
-#: kallithea/templates/data_table/_dt_elements.html:68
+#: kallithea/templates/data_table/_dt_elements.html:74
 #, python-format
 msgid "Confirm to delete this repository: %s"
 msgstr "Potwierdź usunięcie repozytorium: %s"
@@ -3243,48 +3239,14 @@
 "it or restore it."
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:4
-#, fuzzy
-msgid "Invalidate Repository Cache"
-msgstr "Unieważnij pamięć podręczną repozytorium"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:6
-#, fuzzy
-msgid ""
-"Manually invalidate cache for this repository. On first access, the "
-"repository will be cached again."
-msgstr ""
-"Ręcznie unieważnienie cache dla tego repozytorium. Przy pierwszym "
-"dostępie do repozytorium zostanie dodane do bufora ponownie."
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:9
-#, fuzzy
-msgid "List of Cached Values"
-msgstr "Lista buforowanych wartości"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:12
-msgid "Prefix"
-msgstr "Prefiks"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:13
+#: kallithea/templates/admin/repos/repo_edit_fields.html:6
+msgid "Label"
+msgstr ""
+
 #: kallithea/templates/admin/repos/repo_edit_fields.html:7
 msgid "Key"
 msgstr "Klucz"
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:14
-#: kallithea/templates/admin/user_groups/user_group_add.html:40
-#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
-#: kallithea/templates/admin/user_groups/user_groups.html:41
-#: kallithea/templates/admin/users/user_add.html:69
-#: kallithea/templates/admin/users/user_edit_profile.html:74
-#: kallithea/templates/admin/users/users.html:42
-msgid "Active"
-msgstr "Aktywny"
-
-#: kallithea/templates/admin/repos/repo_edit_fields.html:6
-msgid "Label"
-msgstr ""
-
 #: kallithea/templates/admin/repos/repo_edit_fields.html:20
 #, python-format
 msgid "Confirm to delete this field: %s"
@@ -3829,6 +3791,15 @@
 msgid "Short, optional description for this user group."
 msgstr ""
 
+#: kallithea/templates/admin/user_groups/user_group_add.html:40
+#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
+#: kallithea/templates/admin/user_groups/user_groups.html:41
+#: kallithea/templates/admin/users/user_add.html:69
+#: kallithea/templates/admin/users/user_edit_profile.html:74
+#: kallithea/templates/admin/users/users.html:42
+msgid "Active"
+msgstr "Aktywny"
+
 #: kallithea/templates/admin/user_groups/user_group_edit.html:5
 #, python-format
 msgid "%s user group settings"
@@ -3851,7 +3822,7 @@
 msgstr "Użytkownicy"
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:19
-#: kallithea/templates/data_table/_dt_elements.html:102
+#: kallithea/templates/data_table/_dt_elements.html:108
 #, python-format
 msgid "Confirm to delete this user group: %s"
 msgstr "Potwierdź usunięcie grupy użytkowników: %s"
@@ -3926,7 +3897,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:90
+#: kallithea/templates/data_table/_dt_elements.html:96
 #, python-format
 msgid "Confirm to delete this user: %s"
 msgstr "Potwierdź usunięcie tego użytkownika: %s"
@@ -4029,10 +4000,12 @@
 msgstr "Szukaj"
 
 #: kallithea/templates/base/base.html:167
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Follow"
 msgstr "Obserwuj"
 
 #: kallithea/templates/base/base.html:168
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Unfollow"
 msgstr "Nie obserwuj"
 
@@ -4470,26 +4443,26 @@
 msgid "Merge"
 msgstr "połącz"
 
-#: kallithea/templates/changeset/changeset.html:96
+#: kallithea/templates/changeset/changeset.html:95
 #, fuzzy
 msgid "Grafted from:"
 msgstr "Połączono z:"
 
-#: kallithea/templates/changeset/changeset.html:102
+#: kallithea/templates/changeset/changeset.html:100
 msgid "Transplanted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:108
+#: kallithea/templates/changeset/changeset.html:106
 #, fuzzy
 msgid "Replaced by:"
 msgstr "Zastąpiono przez:"
 
-#: kallithea/templates/changeset/changeset.html:122
+#: kallithea/templates/changeset/changeset.html:120
 #, fuzzy
 msgid "Preceded by:"
 msgstr "Poprzedzone przez:"
 
-#: kallithea/templates/changeset/changeset.html:139
+#: kallithea/templates/changeset/changeset.html:137
 #: kallithea/templates/compare/compare_diff.html:59
 #: kallithea/templates/pullrequests/pullrequest_show.html:290
 #, python-format
@@ -4499,7 +4472,7 @@
 msgstr[1] "%s pliki zostały zmienione"
 msgstr[2] "%s plików zostało zmienionych"
 
-#: kallithea/templates/changeset/changeset.html:141
+#: kallithea/templates/changeset/changeset.html:139
 #: kallithea/templates/compare/compare_diff.html:61
 #: kallithea/templates/pullrequests/pullrequest_show.html:292
 #, python-format
@@ -4509,8 +4482,8 @@
 msgstr[1] "%s plików zostało zmienionych z %s inercjami i %s usunięciami"
 msgstr[2] "%s plików zostało zmienionych z %s inercjami i %s usunięciami"
 
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
 #: kallithea/templates/pullrequests/pullrequest_show.html:333
@@ -4774,23 +4747,23 @@
 msgid "Repository creation in progress..."
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:42
+#: kallithea/templates/data_table/_dt_elements.html:48
 msgid "No changesets yet"
 msgstr "Nie ma jeszcze zestawienia zmian"
 
-#: kallithea/templates/data_table/_dt_elements.html:48
-#: kallithea/templates/data_table/_dt_elements.html:50
+#: kallithea/templates/data_table/_dt_elements.html:54
+#: kallithea/templates/data_table/_dt_elements.html:56
 #, python-format
 msgid "Subscribe to %s rss feed"
 msgstr "Subskrybuj %s kanał rss"
 
-#: kallithea/templates/data_table/_dt_elements.html:56
-#: kallithea/templates/data_table/_dt_elements.html:58
+#: kallithea/templates/data_table/_dt_elements.html:62
+#: kallithea/templates/data_table/_dt_elements.html:64
 #, python-format
 msgid "Subscribe to %s atom feed"
 msgstr "Subskrybuj %s kanał atom"
 
-#: kallithea/templates/data_table/_dt_elements.html:76
+#: kallithea/templates/data_table/_dt_elements.html:82
 msgid "Creating"
 msgstr ""
 
@@ -4828,6 +4801,13 @@
 msgid "by"
 msgstr ""
 
+#: kallithea/templates/email_templates/changeset_comment.html:36
+#: kallithea/templates/email_templates/pull_request_comment.html:43
+#, fuzzy
+#| msgid "Comment"
+msgid "View Comment"
+msgstr "Komentarz"
+
 #: kallithea/templates/email_templates/comment.html:27
 #, fuzzy
 #| msgid "Status change"
@@ -4840,33 +4820,43 @@
 msgid "The pull request has been closed."
 msgstr "Żądanie połączenia zmian zostało zamknięte."
 
-#: kallithea/templates/email_templates/password_reset.html:9
+#: kallithea/templates/email_templates/default.html:4
+msgid "Message"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:4
+#, fuzzy
+#| msgid "Password Reset"
+msgid "Password Reset Request"
+msgstr "Łącze resetowania hasła"
+
+#: kallithea/templates/email_templates/password_reset.html:10
 #, python-format
 msgid "Hello %s"
 msgstr "Witaj %s"
 
-#: kallithea/templates/email_templates/password_reset.html:16
+#: kallithea/templates/email_templates/password_reset.html:17
 #, fuzzy
 msgid "We have received a request to reset the password for your account."
 msgstr "Otrzymaliśmy prośbę o utworzenie nowego hasła do twojego konta."
 
-#: kallithea/templates/email_templates/password_reset.html:25
+#: kallithea/templates/email_templates/password_reset.html:26
 msgid ""
 "This account is however managed outside this system and the password "
 "cannot be changed here."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:28
+#: kallithea/templates/email_templates/password_reset.html:29
 msgid "To set a new password, click the following link"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:33
+#: kallithea/templates/email_templates/password_reset.html:34
 msgid ""
 "Should you not be able to use the link above, please type the following "
 "code into the password reset form"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:44
+#: kallithea/templates/email_templates/password_reset.html:45
 msgid ""
 "If it weren't you who requested the password reset, just disregard this "
 "message."
@@ -4899,6 +4889,11 @@
 msgid "to"
 msgstr ""
 
+#: kallithea/templates/email_templates/pull_request.html:85
+#, fuzzy
+msgid "View Pull Request"
+msgstr "Nowa prośba o połączenie gałęzi"
+
 #: kallithea/templates/email_templates/pull_request_comment.html:4
 #, fuzzy, python-format
 #| msgid "%(user)s commented on pull request %(age)s"
@@ -4917,12 +4912,24 @@
 msgid "Comment on Pull Request %s \"%s\""
 msgstr "[komentarz] wniosek o połączenie gałęzi"
 
-#: kallithea/templates/email_templates/registration.html:22
+#: kallithea/templates/email_templates/registration.html:5
+#, fuzzy
+#| msgid "New user registration"
+msgid "New User Registration"
+msgstr "nowy użytkownik się zarejestrował"
+
+#: kallithea/templates/email_templates/registration.html:23
 #, fuzzy
 #| msgid "Group name"
 msgid "Full Name"
 msgstr "Nazwa grupy"
 
+#: kallithea/templates/email_templates/registration.html:42
+#, fuzzy
+#| msgid "View this user here"
+msgid "View User Profile"
+msgstr "Zobacz tego użytkownika tutaj"
+
 #: kallithea/templates/files/diff_2way.html:15
 #, python-format
 msgid "%s File side-by-side diff"
@@ -5558,45 +5565,45 @@
 msgid "Stats gathered: "
 msgstr "Statystyki zebrane: "
 
-#: kallithea/templates/summary/statistics.html:87
-#: kallithea/templates/summary/summary.html:371
+#: kallithea/templates/summary/statistics.html:85
+#: kallithea/templates/summary/summary.html:369
 msgid "files"
 msgstr "pliki"
 
-#: kallithea/templates/summary/statistics.html:111
-#: kallithea/templates/summary/summary.html:401
+#: kallithea/templates/summary/statistics.html:109
+#: kallithea/templates/summary/summary.html:399
 msgid "Show more"
 msgstr "Pokaż więcej"
 
-#: kallithea/templates/summary/statistics.html:405
+#: kallithea/templates/summary/statistics.html:395
 msgid "commits"
 msgstr "komunikaty"
 
-#: kallithea/templates/summary/statistics.html:406
+#: kallithea/templates/summary/statistics.html:396
 msgid "files added"
 msgstr "pliki dodane"
 
-#: kallithea/templates/summary/statistics.html:407
+#: kallithea/templates/summary/statistics.html:397
 msgid "files changed"
 msgstr "pliki zmienione"
 
-#: kallithea/templates/summary/statistics.html:408
+#: kallithea/templates/summary/statistics.html:398
 msgid "files removed"
 msgstr "pliki usunięte"
 
-#: kallithea/templates/summary/statistics.html:410
+#: kallithea/templates/summary/statistics.html:400
 msgid "commit"
 msgstr "komunikaty"
 
-#: kallithea/templates/summary/statistics.html:411
+#: kallithea/templates/summary/statistics.html:401
 msgid "file added"
 msgstr "plik dodany"
 
-#: kallithea/templates/summary/statistics.html:412
+#: kallithea/templates/summary/statistics.html:402
 msgid "file changed"
 msgstr "plik zmieniony"
 
-#: kallithea/templates/summary/statistics.html:413
+#: kallithea/templates/summary/statistics.html:403
 msgid "file removed"
 msgstr "plik usunięty"
 
@@ -5704,6 +5711,31 @@
 msgid "Download %s as %s"
 msgstr "Pobierz %s jak %s"
 
+#~ msgid "Cache invalidation successful"
+#~ msgstr "Cache wyczyszczony poprawnie"
+
+#~ msgid "An error occurred during cache invalidation"
+#~ msgstr "Wystąpił błąd podczas unieważniania cache"
+
+#, fuzzy
+#~ msgid "Invalidate Repository Cache"
+#~ msgstr "Unieważnij pamięć podręczną repozytorium"
+
+#, fuzzy
+#~ msgid ""
+#~ "Manually invalidate cache for this repository. On first access, the "
+#~ "repository will be cached again."
+#~ msgstr ""
+#~ "Ręcznie unieważnienie cache dla tego repozytorium. Przy pierwszym "
+#~ "dostępie do repozytorium zostanie dodane do bufora ponownie."
+
+#, fuzzy
+#~ msgid "List of Cached Values"
+#~ msgstr "Lista buforowanych wartości"
+
+#~ msgid "Prefix"
+#~ msgstr "Prefiks"
+
 #~ msgid "This repository has been locked by %s on %s"
 #~ msgstr "Repozytorium zostało zablokowane przez %s na %s"
 
@@ -5978,9 +6010,6 @@
 #~ msgid "The comment was made with status"
 #~ msgstr "Wniosek połączenia został zamknięty ze statusem"
 
-#~ msgid "View this user here"
-#~ msgstr "Zobacz tego użytkownika tutaj"
-
 #~ msgid "Repository Size"
 #~ msgstr "Rozmiar Repozytorium"
 
--- a/kallithea/i18n/pt_BR/LC_MESSAGES/kallithea.po	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/i18n/pt_BR/LC_MESSAGES/kallithea.po	Mon May 04 19:24:04 2020 +0200
@@ -4,7 +4,7 @@
 msgstr ""
 "Project-Id-Version: Kallithea 0.3\n"
 "Report-Msgid-Bugs-To: translations@kallithea-scm.org\n"
-"POT-Creation-Date: 2020-01-05 04:10+0100\n"
+"POT-Creation-Date: 2020-05-04 19:12+0200\n"
 "PO-Revision-Date: 2014-02-13 14:34+0000\n"
 "Last-Translator: Marcin Kuźmiński <marcin@python-works.com>\n"
 "Language-Team: Portuguese (Brazil) <https://hosted.weblate.org/projects/"
@@ -16,14 +16,14 @@
 "Plural-Forms: nplurals=2; plural=(n > 1)\n"
 
 #: kallithea/controllers/changelog.py:67
-#: kallithea/controllers/pullrequests.py:250 kallithea/lib/base.py:602
+#: kallithea/controllers/pullrequests.py:247 kallithea/lib/base.py:602
 msgid "There are no changesets yet"
 msgstr "Não há nenhum changeset ainda"
 
 #: kallithea/controllers/admin/permissions.py:64
 #: kallithea/controllers/admin/permissions.py:68
 #: kallithea/controllers/admin/permissions.py:72
-#: kallithea/controllers/changelog.py:137
+#: kallithea/controllers/changelog.py:136
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:7
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:88
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:7
@@ -32,37 +32,37 @@
 msgid "None"
 msgstr "Nenhum"
 
-#: kallithea/controllers/changelog.py:140 kallithea/controllers/files.py:189
+#: kallithea/controllers/changelog.py:139 kallithea/controllers/files.py:189
 msgid "(closed)"
 msgstr "(fechado)"
 
-#: kallithea/controllers/changeset.py:81
+#: kallithea/controllers/changeset.py:82
 msgid "Show whitespace"
 msgstr "Mostrar espaços em branco"
 
-#: kallithea/controllers/changeset.py:88
-#: kallithea/controllers/changeset.py:95
+#: kallithea/controllers/changeset.py:89
+#: kallithea/controllers/changeset.py:96
 #: kallithea/templates/files/diff_2way.html:55
 msgid "Ignore whitespace"
 msgstr "Ignorar espaços em branco"
 
-#: kallithea/controllers/changeset.py:161
+#: kallithea/controllers/changeset.py:162
 #, python-format
 msgid "Increase diff context to %(num)s lines"
 msgstr ""
 
-#: kallithea/controllers/changeset.py:201
+#: kallithea/controllers/changeset.py:202
 #, fuzzy
 msgid "No permission to change status"
 msgstr "Vote para estado do pull request"
 
-#: kallithea/controllers/changeset.py:212
+#: kallithea/controllers/changeset.py:213
 #, fuzzy, python-format
 msgid "Successfully deleted pull request %s"
 msgstr "Pull request excluído com sucesso"
 
 #: kallithea/controllers/changeset.py:319 kallithea/controllers/files.py:89
-#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:700
+#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:697
 msgid "Such revision does not exist for this repository"
 msgstr ""
 
@@ -76,51 +76,51 @@
 msgid "Cannot compare repositories of different types"
 msgstr ""
 
-#: kallithea/controllers/compare.py:246
+#: kallithea/controllers/compare.py:247
 msgid "Cannot show empty diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:248
+#: kallithea/controllers/compare.py:249
 msgid "No ancestor found for merge diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:252
+#: kallithea/controllers/compare.py:253
 msgid "Multiple merge ancestors found for merge compare"
 msgstr ""
 
-#: kallithea/controllers/compare.py:268
+#: kallithea/controllers/compare.py:269
 msgid "Cannot compare repositories without using common ancestor"
 msgstr ""
 
-#: kallithea/controllers/error.py:71
+#: kallithea/controllers/error.py:67
 #, fuzzy
 msgid "No response"
 msgstr "revisões"
 
-#: kallithea/controllers/error.py:72
+#: kallithea/controllers/error.py:68
 msgid "Unknown error"
 msgstr ""
 
-#: kallithea/controllers/error.py:85
+#: kallithea/controllers/error.py:81
 msgid ""
 "The request could not be understood by the server due to malformed syntax."
 msgstr ""
 "A requisição não pôde ser compreendida pelo servidor devido à sintaxe mal "
 "formada."
 
-#: kallithea/controllers/error.py:88
+#: kallithea/controllers/error.py:84
 msgid "Unauthorized access to resource"
 msgstr "Acesso não autorizado ao recurso"
 
-#: kallithea/controllers/error.py:90
+#: kallithea/controllers/error.py:86
 msgid "You don't have permission to view this page"
 msgstr "Você não tem permissão para ver esta página"
 
-#: kallithea/controllers/error.py:92
+#: kallithea/controllers/error.py:88
 msgid "The resource could not be found"
 msgstr "O recurso não pôde ser encontrado"
 
-#: kallithea/controllers/error.py:94
+#: kallithea/controllers/error.py:90
 msgid ""
 "The server encountered an unexpected condition which prevented it from "
 "fulfilling the request."
@@ -128,14 +128,14 @@
 "O servidor encontrou uma condição inesperada que o impediu de satisfazer "
 "a requisição."
 
-#: kallithea/controllers/feed.py:63
+#: kallithea/controllers/feed.py:59
 #, python-format
 msgid "%s committed on %s"
 msgstr "%s commitados em %s"
 
-#: kallithea/controllers/feed.py:88
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/controllers/feed.py:84
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/compare/compare_diff.html:95
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
@@ -143,12 +143,12 @@
 msgid "Changeset was too big and was cut off..."
 msgstr "Conjunto de mudanças era grande demais e foi cortado..."
 
-#: kallithea/controllers/feed.py:111 kallithea/controllers/feed.py:140
+#: kallithea/controllers/feed.py:107
 #, python-format
 msgid "%s %s feed"
 msgstr "%s - feed %s"
 
-#: kallithea/controllers/feed.py:113 kallithea/controllers/feed.py:142
+#: kallithea/controllers/feed.py:109
 #, python-format
 msgid "Changes on %s repository"
 msgstr "Modificações no repositório %s"
@@ -168,105 +168,105 @@
 msgid "%s at %s"
 msgstr "em %s e %s"
 
-#: kallithea/controllers/files.py:296
+#: kallithea/controllers/files.py:295
 #, fuzzy
 msgid "You can only delete files with revision being a valid branch"
 msgstr "Só é possível editar arquivos quando a revisão é um ramo válido"
 
-#: kallithea/controllers/files.py:307
+#: kallithea/controllers/files.py:306
 #, python-format
 msgid "Deleted file %s via Kallithea"
 msgstr ""
 
-#: kallithea/controllers/files.py:331
+#: kallithea/controllers/files.py:330
 #, python-format
 msgid "Successfully deleted file %s"
 msgstr ""
 
-#: kallithea/controllers/files.py:335 kallithea/controllers/files.py:394
-#: kallithea/controllers/files.py:469
+#: kallithea/controllers/files.py:334 kallithea/controllers/files.py:392
+#: kallithea/controllers/files.py:467
 msgid "Error occurred during commit"
 msgstr "Ocorreu um erro ao realizar commit"
 
-#: kallithea/controllers/files.py:350
+#: kallithea/controllers/files.py:349
 #, fuzzy
 msgid "You can only edit files with revision being a valid branch"
 msgstr "Só é possível editar arquivos quando a revisão é um ramo válido"
 
-#: kallithea/controllers/files.py:364
+#: kallithea/controllers/files.py:363
 #, python-format
 msgid "Edited file %s via Kallithea"
 msgstr "Arquivo %s editado via Kallithea"
 
-#: kallithea/controllers/files.py:380
+#: kallithea/controllers/files.py:378
 msgid "No changes"
 msgstr "Sem modificações"
 
-#: kallithea/controllers/files.py:390 kallithea/controllers/files.py:458
+#: kallithea/controllers/files.py:388 kallithea/controllers/files.py:456
 #, python-format
 msgid "Successfully committed to %s"
 msgstr "Commit realizado com sucesso para %s"
 
-#: kallithea/controllers/files.py:409
+#: kallithea/controllers/files.py:407
 msgid "Added file via Kallithea"
 msgstr "Arquivo adicionado via Kallithea"
 
-#: kallithea/controllers/files.py:430
+#: kallithea/controllers/files.py:428
 msgid "No content"
 msgstr "Nenhum conteúdo"
 
-#: kallithea/controllers/files.py:434
+#: kallithea/controllers/files.py:432
 msgid "No filename"
 msgstr "Nenhum nome de arquivo"
 
-#: kallithea/controllers/files.py:461
+#: kallithea/controllers/files.py:459
 msgid "Location must be relative path and must not contain .. in path"
 msgstr "O caminho deve ser relativo e não pode conter .."
 
-#: kallithea/controllers/files.py:493
+#: kallithea/controllers/files.py:491
 msgid "Downloads disabled"
 msgstr "Downloads desabilitados"
 
-#: kallithea/controllers/files.py:504
+#: kallithea/controllers/files.py:502
 #, python-format
 msgid "Unknown revision %s"
 msgstr "Revisão desconhecida %s"
 
-#: kallithea/controllers/files.py:506
+#: kallithea/controllers/files.py:504
 msgid "Empty repository"
 msgstr "Repositório vazio"
 
-#: kallithea/controllers/files.py:508
+#: kallithea/controllers/files.py:506
 msgid "Unknown archive type"
 msgstr "Tipo de arquivo desconhecido"
 
-#: kallithea/controllers/files.py:729
+#: kallithea/controllers/files.py:726
 #: kallithea/templates/changeset/changeset_range.html:9
 #: kallithea/templates/email_templates/pull_request.html:64
 #: kallithea/templates/pullrequests/pullrequest.html:84
 msgid "Changesets"
 msgstr "Conjuntos de mudanças"
 
-#: kallithea/controllers/files.py:730
-#: kallithea/controllers/pullrequests.py:182 kallithea/model/scm.py:676
+#: kallithea/controllers/files.py:727
+#: kallithea/controllers/pullrequests.py:174 kallithea/model/scm.py:663
 msgid "Branches"
 msgstr "Ramos"
 
-#: kallithea/controllers/files.py:731
-#: kallithea/controllers/pullrequests.py:183 kallithea/model/scm.py:687
+#: kallithea/controllers/files.py:728
+#: kallithea/controllers/pullrequests.py:175 kallithea/model/scm.py:674
 msgid "Tags"
 msgstr "Etiquetas"
 
-#: kallithea/controllers/forks.py:174
+#: kallithea/controllers/forks.py:175
 #, python-format
 msgid "An error occurred during repository forking %s"
 msgstr "Ocorreu um erro ao bifurcar o repositório %s"
 
-#: kallithea/controllers/home.py:79
+#: kallithea/controllers/home.py:77
 msgid "Groups"
 msgstr ""
 
-#: kallithea/controllers/home.py:89
+#: kallithea/controllers/home.py:87
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:90
 #: kallithea/templates/admin/repos/repo_add.html:12
 #: kallithea/templates/admin/repos/repo_add.html:16
@@ -278,48 +278,54 @@
 msgid "Repositories"
 msgstr "Repositórios"
 
-#: kallithea/controllers/home.py:122
+#: kallithea/controllers/home.py:119
 #: kallithea/templates/files/files_add.html:32
 #: kallithea/templates/files/files_delete.html:23
 #: kallithea/templates/files/files_edit.html:32
 msgid "Branch"
 msgstr "Ramo"
 
-#: kallithea/controllers/home.py:128
+#: kallithea/controllers/home.py:125
 msgid "Closed Branches"
 msgstr "Ramos Fechados"
 
-#: kallithea/controllers/home.py:134
+#: kallithea/controllers/home.py:131
 msgid "Tag"
 msgstr ""
 
-#: kallithea/controllers/home.py:140
+#: kallithea/controllers/home.py:137
 msgid "Bookmark"
 msgstr ""
 
-#: kallithea/controllers/journal.py:112 kallithea/controllers/journal.py:154
+#: kallithea/controllers/journal.py:146 kallithea/controllers/journal.py:157
 #: kallithea/templates/journal/public_journal.html:4
 #: kallithea/templates/journal/public_journal.html:18
 msgid "Public Journal"
 msgstr "Diário Público"
 
-#: kallithea/controllers/journal.py:116 kallithea/controllers/journal.py:158
+#: kallithea/controllers/journal.py:150 kallithea/controllers/journal.py:161
 #: kallithea/templates/base/base.html:290
 #: kallithea/templates/journal/journal.html:5
 #: kallithea/templates/journal/journal.html:13
 msgid "Journal"
 msgstr "Diário"
 
-#: kallithea/controllers/login.py:140 kallithea/controllers/login.py:185
+#: kallithea/controllers/login.py:109
+#, fuzzy
+#| msgid "HTTP authentication realm"
+msgid "Authentication failed."
+msgstr "Realm de autenticação HTTP"
+
+#: kallithea/controllers/login.py:142 kallithea/controllers/login.py:187
 msgid "Bad captcha"
 msgstr ""
 
-#: kallithea/controllers/login.py:146
+#: kallithea/controllers/login.py:148
 #, python-format
 msgid "You have successfully registered with %s"
 msgstr "Você foi registrado no %s com sucesso"
 
-#: kallithea/controllers/login.py:190
+#: kallithea/controllers/login.py:192
 #, fuzzy
 msgid "A password reset confirmation code has been sent"
 msgstr "Seu link de reinicialização de senha foi enviado"
@@ -334,235 +340,235 @@
 msgid "Successfully updated password"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:68
+#: kallithea/controllers/pullrequests.py:67
 #, python-format
 msgid "Invalid reviewer \"%s\" specified"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:131
+#: kallithea/controllers/pullrequests.py:123
 #, python-format
 msgid "%s (closed)"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:158
+#: kallithea/controllers/pullrequests.py:150
 #: kallithea/templates/changeset/changeset.html:12
 msgid "Changeset"
 msgstr "Conjunto de Mudanças"
 
-#: kallithea/controllers/pullrequests.py:179
+#: kallithea/controllers/pullrequests.py:171
 msgid "Special"
 msgstr "Especial"
 
-#: kallithea/controllers/pullrequests.py:180
+#: kallithea/controllers/pullrequests.py:172
 msgid "Peer branches"
 msgstr "Ramos pares"
 
-#: kallithea/controllers/pullrequests.py:181 kallithea/model/scm.py:682
+#: kallithea/controllers/pullrequests.py:173 kallithea/model/scm.py:669
 msgid "Bookmarks"
 msgstr "Marcadores"
 
-#: kallithea/controllers/pullrequests.py:318
+#: kallithea/controllers/pullrequests.py:315
 #, python-format
 msgid "Error creating pull request: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:345
-#: kallithea/controllers/pullrequests.py:368
+#: kallithea/controllers/pullrequests.py:342
+#: kallithea/controllers/pullrequests.py:365
 #, fuzzy
 msgid "Error occurred while creating pull request"
 msgstr "Ocorreu um erro durante o envio do pull request"
 
-#: kallithea/controllers/pullrequests.py:350
+#: kallithea/controllers/pullrequests.py:347
 msgid "Successfully opened new pull request"
 msgstr "Novo pull request criado com sucesso"
 
-#: kallithea/controllers/pullrequests.py:373
+#: kallithea/controllers/pullrequests.py:370
 #, fuzzy
 #| msgid "Pull request update created"
 msgid "New pull request iteration created"
 msgstr "Revisores do pull request"
 
-#: kallithea/controllers/pullrequests.py:401
+#: kallithea/controllers/pullrequests.py:398
 #, python-format
 msgid "Meanwhile, the following reviewers have been added: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:405
+#: kallithea/controllers/pullrequests.py:402
 #, python-format
 msgid "Meanwhile, the following reviewers have been removed: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:421
-#: kallithea/model/pull_request.py:232
+#: kallithea/controllers/pullrequests.py:418
+#: kallithea/model/pull_request.py:230
 #, fuzzy
 msgid "No description"
 msgstr "Descrição"
 
-#: kallithea/controllers/pullrequests.py:430
+#: kallithea/controllers/pullrequests.py:427
 #, fuzzy
 msgid "Pull request updated"
 msgstr "Pull requests para %s"
 
-#: kallithea/controllers/pullrequests.py:443
+#: kallithea/controllers/pullrequests.py:440
 msgid "Successfully deleted pull request"
 msgstr "Pull request excluído com sucesso"
 
-#: kallithea/controllers/pullrequests.py:479
+#: kallithea/controllers/pullrequests.py:476
 #, python-format
 msgid "Revision %s not found in %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:506
+#: kallithea/controllers/pullrequests.py:504
 #, python-format
 msgid "Error: changesets not found when displaying pull request from %s."
 msgstr ""
 
+#: kallithea/controllers/pullrequests.py:518
+#, python-format
+msgid "This pull request has already been merged to %s."
+msgstr ""
+
 #: kallithea/controllers/pullrequests.py:520
-#, python-format
-msgid "This pull request has already been merged to %s."
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:522
 msgid "This pull request has been closed and can not be updated."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:546
+#: kallithea/controllers/pullrequests.py:539
 #, python-format
 msgid "The following additional changes are available on %s:"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:548
-#: kallithea/controllers/pullrequests.py:552
+#: kallithea/controllers/pullrequests.py:541
+#: kallithea/controllers/pullrequests.py:545
 msgid "No additional changesets found for iterating on this pull request."
 msgstr ""
 
+#: kallithea/controllers/pullrequests.py:553
+#, python-format
+msgid "Note: Branch %s has another head: %s."
+msgstr ""
+
 #: kallithea/controllers/pullrequests.py:560
-#, python-format
-msgid "Note: Branch %s has another head: %s."
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:567
 msgid "Git pull requests don't support iterating yet."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:569
+#: kallithea/controllers/pullrequests.py:562
 #, python-format
 msgid ""
 "Error: some changesets not found when displaying pull request from %s."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:593
+#: kallithea/controllers/pullrequests.py:586
 msgid "The diff can't be shown - the PR revisions could not be found."
 msgstr ""
 
-#: kallithea/controllers/search.py:136
+#: kallithea/controllers/search.py:132
 msgid "Invalid search query. Try quoting it."
 msgstr "Consulta de busca inválida. Tente usar aspas."
 
-#: kallithea/controllers/search.py:140
+#: kallithea/controllers/search.py:136
 msgid "The server has no search index."
 msgstr ""
 
-#: kallithea/controllers/search.py:143
+#: kallithea/controllers/search.py:139
 #, fuzzy
 msgid "An error occurred during search operation."
 msgstr "Ocorreu um erro durante essa operação de busca"
 
-#: kallithea/controllers/summary.py:168
-#: kallithea/templates/summary/summary.html:412
+#: kallithea/controllers/summary.py:169
+#: kallithea/templates/summary/summary.html:410
 #, fuzzy
 msgid "No data ready yet"
 msgstr "Ainda não há dados carregados"
 
-#: kallithea/controllers/summary.py:171
+#: kallithea/controllers/summary.py:172
 #: kallithea/templates/summary/summary.html:97
 msgid "Statistics are disabled for this repository"
 msgstr "As estatísticas estão desabillitadas para este repositório"
 
-#: kallithea/controllers/admin/auth_settings.py:137
+#: kallithea/controllers/admin/auth_settings.py:136
 msgid "Auth settings updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/auth_settings.py:148
+#: kallithea/controllers/admin/auth_settings.py:147
 msgid "error occurred during update of auth settings"
 msgstr ""
 
-#: kallithea/controllers/admin/defaults.py:75
+#: kallithea/controllers/admin/defaults.py:74
 msgid "Default settings updated successfully"
 msgstr "Configurações padrão atualizadas com sucesso"
 
-#: kallithea/controllers/admin/defaults.py:90
+#: kallithea/controllers/admin/defaults.py:89
 msgid "Error occurred during update of defaults"
 msgstr "Ocorreu um erro durnge a atualização dos padrões"
 
 #: kallithea/controllers/admin/gists.py:59
 #: kallithea/controllers/admin/my_account.py:232
-#: kallithea/controllers/admin/users.py:248
+#: kallithea/controllers/admin/users.py:246
 #, fuzzy
 msgid "Forever"
 msgstr "para sempre"
 
 #: kallithea/controllers/admin/gists.py:60
 #: kallithea/controllers/admin/my_account.py:233
-#: kallithea/controllers/admin/users.py:249
+#: kallithea/controllers/admin/users.py:247
 msgid "5 minutes"
 msgstr "cinco minutos"
 
 #: kallithea/controllers/admin/gists.py:61
 #: kallithea/controllers/admin/my_account.py:234
-#: kallithea/controllers/admin/users.py:250
+#: kallithea/controllers/admin/users.py:248
 msgid "1 hour"
 msgstr "uma hora"
 
 #: kallithea/controllers/admin/gists.py:62
 #: kallithea/controllers/admin/my_account.py:235
-#: kallithea/controllers/admin/users.py:251
+#: kallithea/controllers/admin/users.py:249
 msgid "1 day"
 msgstr "um dia"
 
 #: kallithea/controllers/admin/gists.py:63
 #: kallithea/controllers/admin/my_account.py:236
-#: kallithea/controllers/admin/users.py:252
+#: kallithea/controllers/admin/users.py:250
 msgid "1 month"
 msgstr "um mês"
 
 #: kallithea/controllers/admin/gists.py:67
 #: kallithea/controllers/admin/my_account.py:238
-#: kallithea/controllers/admin/users.py:254
+#: kallithea/controllers/admin/users.py:252
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:65
 #: kallithea/templates/admin/users/user_edit_api_keys.html:65
 msgid "Lifetime"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:142
+#: kallithea/controllers/admin/gists.py:148
 msgid "Error occurred during gist creation"
 msgstr "Ocorreu um erro durante a criação de um gist"
 
-#: kallithea/controllers/admin/gists.py:158
+#: kallithea/controllers/admin/gists.py:164
 #, python-format
 msgid "Deleted gist %s"
 msgstr "Gist %s excluído"
 
-#: kallithea/controllers/admin/gists.py:198
+#: kallithea/controllers/admin/gists.py:207
 #, fuzzy
 msgid "Unmodified"
 msgstr "Última alteração"
 
-#: kallithea/controllers/admin/gists.py:228
+#: kallithea/controllers/admin/gists.py:237
 msgid "Successfully updated gist content"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:233
+#: kallithea/controllers/admin/gists.py:242
 msgid "Successfully updated gist data"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:236
+#: kallithea/controllers/admin/gists.py:245
 #, python-format
 msgid "Error occurred during update of gist %s"
 msgstr ""
 
-#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:211
-#: kallithea/model/user.py:232
+#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:205
+#: kallithea/model/user.py:226
 msgid "You can't edit this user since it's crucial for entire application"
 msgstr ""
 "Você não pode editar esse usuário pois ele é crucial para toda a aplicação"
@@ -572,7 +578,7 @@
 msgstr "Sua conta foi atualizada com sucesso"
 
 #: kallithea/controllers/admin/my_account.py:134
-#: kallithea/controllers/admin/users.py:181
+#: kallithea/controllers/admin/users.py:179
 #, python-format
 msgid "Error occurred during update of user %s"
 msgstr "Ocorreu um erro durante a atualização do usuário %s"
@@ -582,44 +588,44 @@
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:209
-#: kallithea/controllers/admin/users.py:367
+#: kallithea/controllers/admin/users.py:365
 #, python-format
 msgid "Added email %s to user"
 msgstr "Email %s adicionado ao usuário"
 
 #: kallithea/controllers/admin/my_account.py:215
-#: kallithea/controllers/admin/users.py:373
+#: kallithea/controllers/admin/users.py:371
 msgid "An error occurred during email saving"
 msgstr "Ocorreu um erro durante o salvamento do email"
 
 #: kallithea/controllers/admin/my_account.py:224
-#: kallithea/controllers/admin/users.py:383
+#: kallithea/controllers/admin/users.py:381
 msgid "Removed email from user"
 msgstr "Email removido do usuário"
 
 #: kallithea/controllers/admin/my_account.py:248
-#: kallithea/controllers/admin/users.py:271
+#: kallithea/controllers/admin/users.py:269
 msgid "API key successfully created"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:257
-#: kallithea/controllers/admin/users.py:281
+#: kallithea/controllers/admin/users.py:279
 msgid "API key successfully reset"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:261
-#: kallithea/controllers/admin/users.py:285
+#: kallithea/controllers/admin/users.py:283
 msgid "API key successfully deleted"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:281
-#: kallithea/controllers/admin/users.py:456
+#: kallithea/controllers/admin/users.py:454
 #, python-format
 msgid "SSH key %s successfully added"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:293
-#: kallithea/controllers/admin/users.py:470
+#: kallithea/controllers/admin/users.py:468
 #, fuzzy
 #| msgid "Successfully deleted user"
 msgid "SSH key successfully deleted"
@@ -697,11 +703,11 @@
 msgid "Allowed with automatic account activation"
 msgstr "Permitido com ativação automática de conta"
 
-#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1673
+#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1578
 msgid "Manual activation of external account"
 msgstr "Ativação manual de conta externa"
 
-#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1674
+#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1579
 msgid "Automatic activation of external account"
 msgstr "Ativação automática de conta externa"
 
@@ -723,345 +729,337 @@
 msgid "Error occurred during update of permissions"
 msgstr "Ocorreu um erro durante a atualização das permissões"
 
-#: kallithea/controllers/admin/repo_groups.py:172
+#: kallithea/controllers/admin/repo_groups.py:165
 #, python-format
 msgid "Error occurred during creation of repository group %s"
 msgstr "Ocorreu um erro durante a criação do grupo de repositórios %s"
 
-#: kallithea/controllers/admin/repo_groups.py:177
+#: kallithea/controllers/admin/repo_groups.py:172
 #, python-format
 msgid "Created repository group %s"
 msgstr "Grupo de repositórios %s criado"
 
-#: kallithea/controllers/admin/repo_groups.py:224
+#: kallithea/controllers/admin/repo_groups.py:219
 #, python-format
 msgid "Updated repository group %s"
 msgstr "Grupo de repositórios %s atualizado"
 
-#: kallithea/controllers/admin/repo_groups.py:240
+#: kallithea/controllers/admin/repo_groups.py:235
 #, python-format
 msgid "Error occurred during update of repository group %s"
 msgstr "Ocorreu um erro durante a atualização do grupo de repositórios %s"
 
-#: kallithea/controllers/admin/repo_groups.py:250
+#: kallithea/controllers/admin/repo_groups.py:245
 #, python-format
 msgid "This group contains %s repositories and cannot be deleted"
 msgstr "Esse grupo contém %s repositórios e não pode ser excluído"
 
-#: kallithea/controllers/admin/repo_groups.py:257
+#: kallithea/controllers/admin/repo_groups.py:252
 #, python-format
 msgid "This group contains %s subgroups and cannot be deleted"
 msgstr "Este grupo contém %s subgrupos e não pode ser excluído"
 
-#: kallithea/controllers/admin/repo_groups.py:263
+#: kallithea/controllers/admin/repo_groups.py:258
 #, python-format
 msgid "Removed repository group %s"
 msgstr "Grupo de repositórios %s excluído"
 
-#: kallithea/controllers/admin/repo_groups.py:268
+#: kallithea/controllers/admin/repo_groups.py:263
 #, python-format
 msgid "Error occurred during deletion of repository group %s"
 msgstr "Ocorreu um erro durante a exclusão do grupo de repositórios %s"
 
-#: kallithea/controllers/admin/repo_groups.py:352
-#: kallithea/controllers/admin/repo_groups.py:382
-#: kallithea/controllers/admin/user_groups.py:294
+#: kallithea/controllers/admin/repo_groups.py:347
+#: kallithea/controllers/admin/repo_groups.py:377
+#: kallithea/controllers/admin/user_groups.py:290
 msgid "Cannot revoke permission for yourself as admin"
 msgstr "Você não pode revocar sua própria permissão de administrador"
 
-#: kallithea/controllers/admin/repo_groups.py:367
+#: kallithea/controllers/admin/repo_groups.py:362
 msgid "Repository group permissions updated"
 msgstr "Permissões atualizadas do Grupo de Repositórios"
 
-#: kallithea/controllers/admin/repo_groups.py:399
-#: kallithea/controllers/admin/repos.py:358
-#: kallithea/controllers/admin/user_groups.py:306
+#: kallithea/controllers/admin/repo_groups.py:394
+#: kallithea/controllers/admin/repos.py:357
+#: kallithea/controllers/admin/user_groups.py:302
 msgid "An error occurred during revoking of permission"
 msgstr "Ocorreu um erro durante a revocação das permissões"
 
-#: kallithea/controllers/admin/repos.py:136
+#: kallithea/controllers/admin/repos.py:137
 #, python-format
 msgid "Error creating repository %s"
 msgstr "Erro ao criar repositório %s"
 
-#: kallithea/controllers/admin/repos.py:194
+#: kallithea/controllers/admin/repos.py:193
 #, python-format
 msgid "Created repository %s from %s"
 msgstr "Repositório %s criado de %s"
 
-#: kallithea/controllers/admin/repos.py:203
+#: kallithea/controllers/admin/repos.py:202
 #, python-format
 msgid "Forked repository %s as %s"
 msgstr "Repositório %s bifurcado como %s"
 
-#: kallithea/controllers/admin/repos.py:206
+#: kallithea/controllers/admin/repos.py:205
 #, python-format
 msgid "Created repository %s"
 msgstr "Repositório %s criado"
 
-#: kallithea/controllers/admin/repos.py:235
+#: kallithea/controllers/admin/repos.py:234
 #, python-format
 msgid "Repository %s updated successfully"
 msgstr "Repositório %s atualizado com sucesso"
 
-#: kallithea/controllers/admin/repos.py:255
+#: kallithea/controllers/admin/repos.py:254
 #, python-format
 msgid "Error occurred during update of repository %s"
 msgstr "Ocorreu um erro durante a atualização do repositório %s"
 
-#: kallithea/controllers/admin/repos.py:273
+#: kallithea/controllers/admin/repos.py:272
 #, python-format
 msgid "Detached %s forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:276
+#: kallithea/controllers/admin/repos.py:275
 #, python-format
 msgid "Deleted %s forks"
 msgstr "%s bifurcações excluídas"
 
-#: kallithea/controllers/admin/repos.py:281
+#: kallithea/controllers/admin/repos.py:280
 #, python-format
 msgid "Deleted repository %s"
 msgstr "Repositório %s excluído"
 
-#: kallithea/controllers/admin/repos.py:284
+#: kallithea/controllers/admin/repos.py:283
 #, fuzzy, python-format
 msgid "Cannot delete repository %s which still has forks"
 msgstr ""
 "Nao é possível excluir %s pois ele ainda contém bifurcações vinculadas"
 
-#: kallithea/controllers/admin/repos.py:289
+#: kallithea/controllers/admin/repos.py:288
 #, python-format
 msgid "An error occurred during deletion of %s"
 msgstr "Ocorreu um erro durante a exclusão de %s"
 
-#: kallithea/controllers/admin/repos.py:329
+#: kallithea/controllers/admin/repos.py:328
 msgid "Repository permissions updated"
 msgstr "Permissões do repositório atualizadas"
 
-#: kallithea/controllers/admin/repos.py:388
+#: kallithea/controllers/admin/repos.py:387
 #, python-format
 msgid "Field validation error: %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:391
+#: kallithea/controllers/admin/repos.py:390
 #, fuzzy, python-format
 #| msgid "An error occurred during creation of field"
 msgid "An error occurred during creation of field: %r"
 msgstr "Ocorreu um erro durante a criação do campo"
 
-#: kallithea/controllers/admin/repos.py:402
+#: kallithea/controllers/admin/repos.py:401
 msgid "An error occurred during removal of field"
 msgstr "Ocorreu um erro durante a remoção do campo"
 
-#: kallithea/controllers/admin/repos.py:416
+#: kallithea/controllers/admin/repos.py:415
 msgid "-- Not a fork --"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:448
+#: kallithea/controllers/admin/repos.py:447
 msgid "Updated repository visibility in public journal"
 msgstr "Atualizada a visibilidade do repositório no diário público"
 
-#: kallithea/controllers/admin/repos.py:452
+#: kallithea/controllers/admin/repos.py:451
 msgid "An error occurred during setting this repository in public journal"
 msgstr "Ocorreu um erro ao ajustar esse repositório no diário público"
 
-#: kallithea/controllers/admin/repos.py:468
+#: kallithea/controllers/admin/repos.py:467
 msgid "Nothing"
 msgstr "Nada"
 
-#: kallithea/controllers/admin/repos.py:470
+#: kallithea/controllers/admin/repos.py:469
 #, python-format
 msgid "Marked repository %s as fork of %s"
 msgstr "Marcado repositório %s como bifurcação de %s"
 
-#: kallithea/controllers/admin/repos.py:477
+#: kallithea/controllers/admin/repos.py:476
 msgid "An error occurred during this operation"
 msgstr "Ocorreu um erro durante essa operação"
 
-#: kallithea/controllers/admin/repos.py:490
-msgid "Cache invalidation successful"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:494
-msgid "An error occurred during cache invalidation"
-msgstr "Ocorreu um erro ao invalidar o cache"
-
-#: kallithea/controllers/admin/repos.py:507
+#: kallithea/controllers/admin/repos.py:488
 msgid "Pulled from remote location"
 msgstr "Realizado pull de localização remota"
 
-#: kallithea/controllers/admin/repos.py:510
+#: kallithea/controllers/admin/repos.py:491
 msgid "An error occurred during pull from remote location"
 msgstr "Ocorreu um erro ao realizar pull de localização remota"
 
-#: kallithea/controllers/admin/repos.py:541
+#: kallithea/controllers/admin/repos.py:522
 msgid "An error occurred during deletion of repository stats"
 msgstr "Ocorreu um erro ao excluir estatísticas de repositório"
 
-#: kallithea/controllers/admin/settings.py:131
+#: kallithea/controllers/admin/settings.py:132
 msgid "Updated VCS settings"
 msgstr "Configurações de VCS atualizadas"
 
-#: kallithea/controllers/admin/settings.py:135 kallithea/lib/utils.py:237
+#: kallithea/controllers/admin/settings.py:136
 msgid ""
 "Unable to activate hgsubversion support. The \"hgsubversion\" library is "
 "missing"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:141
-#: kallithea/controllers/admin/settings.py:233
+#: kallithea/controllers/admin/settings.py:142
+#: kallithea/controllers/admin/settings.py:234
 msgid "Error occurred while updating application settings"
 msgstr ""
 "Ocorreu um erro durante a atualização das configurações da aplicação"
 
-#: kallithea/controllers/admin/settings.py:176
+#: kallithea/controllers/admin/settings.py:177
 #, fuzzy, python-format
 msgid "Repositories successfully rescanned. Added: %s. Removed: %s."
 msgstr "Repositórios varridos com sucesso adicionados: %s ; removidos: %s"
 
-#: kallithea/controllers/admin/settings.py:188
+#: kallithea/controllers/admin/settings.py:189
 #, fuzzy, python-format
 #| msgid "Invalidate cache for all repositories"
 msgid "Invalidated %s repositories"
 msgstr "Invalidar o cache para todos os repositórios"
 
-#: kallithea/controllers/admin/settings.py:229
+#: kallithea/controllers/admin/settings.py:230
 msgid "Updated application settings"
 msgstr "Configurações da aplicação atualizadas"
 
-#: kallithea/controllers/admin/settings.py:283
+#: kallithea/controllers/admin/settings.py:284
 msgid "Updated visualisation settings"
 msgstr "Configurações de visualização atualizadas"
 
-#: kallithea/controllers/admin/settings.py:288
+#: kallithea/controllers/admin/settings.py:289
 msgid "Error occurred during updating visualisation settings"
 msgstr ""
 "Ocorreu um erro durante a atualização das configurações de visualização"
 
-#: kallithea/controllers/admin/settings.py:312
+#: kallithea/controllers/admin/settings.py:313
 msgid "Please enter email address"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:327
+#: kallithea/controllers/admin/settings.py:328
 msgid "Send email task created"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:355
+#: kallithea/controllers/admin/settings.py:356
 #, fuzzy
 msgid "Hook already exists"
 msgstr "Ainda não há dados carregados"
 
-#: kallithea/controllers/admin/settings.py:357
+#: kallithea/controllers/admin/settings.py:358
 msgid "Builtin hooks are read-only. Please use another hook name."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:360
+#: kallithea/controllers/admin/settings.py:361
 msgid "Added new hook"
 msgstr "Adicionado novo gancho"
 
-#: kallithea/controllers/admin/settings.py:376
+#: kallithea/controllers/admin/settings.py:377
 msgid "Updated hooks"
 msgstr "Atualizados os ganchos"
 
-#: kallithea/controllers/admin/settings.py:380
+#: kallithea/controllers/admin/settings.py:381
 msgid "Error occurred during hook creation"
 msgstr "Ocorreu um erro durante a criação do hook"
 
-#: kallithea/controllers/admin/settings.py:404
+#: kallithea/controllers/admin/settings.py:405
 msgid "Whoosh reindex task scheduled"
 msgstr "Tarefa de reindexação do whoosh agendada"
 
-#: kallithea/controllers/admin/user_groups.py:138
+#: kallithea/controllers/admin/user_groups.py:134
 #, python-format
 msgid "Created user group %s"
 msgstr "Grupo de usuários %s criado"
 
-#: kallithea/controllers/admin/user_groups.py:151
+#: kallithea/controllers/admin/user_groups.py:147
 #, python-format
 msgid "Error occurred during creation of user group %s"
 msgstr "Ocorreu um erro durante a criação do grupo de usuários %s"
 
-#: kallithea/controllers/admin/user_groups.py:179
+#: kallithea/controllers/admin/user_groups.py:175
 #, python-format
 msgid "Updated user group %s"
 msgstr "Grupo de usuários %s atualizado"
 
-#: kallithea/controllers/admin/user_groups.py:201
+#: kallithea/controllers/admin/user_groups.py:197
 #, python-format
 msgid "Error occurred during update of user group %s"
 msgstr "Ocorreu um erro durante a atualização do grupo de usuários %s"
 
-#: kallithea/controllers/admin/user_groups.py:212
+#: kallithea/controllers/admin/user_groups.py:208
 msgid "Successfully deleted user group"
 msgstr "Grupo de usuários excluído com sucesso"
 
-#: kallithea/controllers/admin/user_groups.py:217
+#: kallithea/controllers/admin/user_groups.py:213
 msgid "An error occurred during deletion of user group"
 msgstr "Ocorreu um erro durante a exclusão do grupo de usuários"
 
-#: kallithea/controllers/admin/user_groups.py:273
+#: kallithea/controllers/admin/user_groups.py:269
 msgid "Target group cannot be the same"
 msgstr "O grupo destino não pode ser o mesmo"
 
-#: kallithea/controllers/admin/user_groups.py:279
+#: kallithea/controllers/admin/user_groups.py:275
 msgid "User group permissions updated"
 msgstr "Permissões do Grupo de Usuários atualizadas"
 
-#: kallithea/controllers/admin/user_groups.py:388
-#: kallithea/controllers/admin/users.py:338
+#: kallithea/controllers/admin/user_groups.py:384
+#: kallithea/controllers/admin/users.py:336
 msgid "Updated permissions"
 msgstr "Permissões atualizadas"
 
-#: kallithea/controllers/admin/user_groups.py:392
-#: kallithea/controllers/admin/users.py:342
+#: kallithea/controllers/admin/user_groups.py:388
+#: kallithea/controllers/admin/users.py:340
 msgid "An error occurred during permissions saving"
 msgstr "Ocorreu um erro durante o salvamento das permissões"
 
-#: kallithea/controllers/admin/users.py:123
+#: kallithea/controllers/admin/users.py:121
 #, python-format
 msgid "Created user %s"
 msgstr "Usuário %s criado"
 
-#: kallithea/controllers/admin/users.py:138
+#: kallithea/controllers/admin/users.py:136
 #, python-format
 msgid "Error occurred during creation of user %s"
 msgstr "Ocorreu um erro durante a criação do usuário %s"
 
-#: kallithea/controllers/admin/users.py:162
+#: kallithea/controllers/admin/users.py:160
 msgid "User updated successfully"
 msgstr "Usuário atualizado com sucesso"
 
-#: kallithea/controllers/admin/users.py:190
+#: kallithea/controllers/admin/users.py:188
 msgid "Successfully deleted user"
 msgstr "Usuário excluído com sucesso"
 
-#: kallithea/controllers/admin/users.py:195
+#: kallithea/controllers/admin/users.py:193
 msgid "An error occurred during deletion of user"
 msgstr "Ocorreu um erro ao excluir o usuário"
 
-#: kallithea/controllers/admin/users.py:203
+#: kallithea/controllers/admin/users.py:201
 msgid "The default user cannot be edited"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:409
+#: kallithea/controllers/admin/users.py:407
 #, python-format
 msgid "Added IP address %s to user whitelist"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:415
+#: kallithea/controllers/admin/users.py:413
 msgid "An error occurred while adding IP address"
 msgstr "Ocorreu um erro durante o salvamento do IP"
 
-#: kallithea/controllers/admin/users.py:427
+#: kallithea/controllers/admin/users.py:425
 msgid "Removed IP address from user whitelist"
 msgstr ""
 
-#: kallithea/lib/auth.py:684
+#: kallithea/lib/auth.py:634
 msgid "You need to be a registered user to perform this action"
 msgstr "Você precisa ser um usuário registrado para realizar essa ação"
 
-#: kallithea/lib/auth.py:712
+#: kallithea/lib/auth.py:662
 msgid "You need to be signed in to view this page"
 msgstr "Você precisa estar logado para ver essa página"
 
@@ -1097,172 +1095,172 @@
 "Conjunto de mudanças é grande demais e foi cortado, use o menu de "
 "diferenças para ver as diferenças"
 
-#: kallithea/lib/diffs.py:224
+#: kallithea/lib/diffs.py:223
 msgid "No changes detected"
 msgstr "Nenhuma alteração detectada"
 
-#: kallithea/lib/helpers.py:653
+#: kallithea/lib/helpers.py:670
 #, python-format
 msgid "Deleted branch: %s"
 msgstr "Excluído ramo: %s"
 
-#: kallithea/lib/helpers.py:655
+#: kallithea/lib/helpers.py:672
 #, python-format
 msgid "Created tag: %s"
 msgstr "Tag criada: %s"
 
-#: kallithea/lib/helpers.py:666
+#: kallithea/lib/helpers.py:683
 #, fuzzy, python-format
 #| msgid "Changeset not found"
 msgid "Changeset %s not found"
 msgstr "Conjunto de alterações não encontrado"
 
-#: kallithea/lib/helpers.py:715
+#: kallithea/lib/helpers.py:732
 #, python-format
 msgid "Show all combined changesets %s->%s"
 msgstr "Ver todos os conjuntos de mudanças combinados %s->%s"
 
-#: kallithea/lib/helpers.py:721
+#: kallithea/lib/helpers.py:738
 #, fuzzy
 msgid "Compare view"
 msgstr "comparar exibir"
 
-#: kallithea/lib/helpers.py:740
+#: kallithea/lib/helpers.py:757
 msgid "and"
 msgstr "e"
 
-#: kallithea/lib/helpers.py:741
+#: kallithea/lib/helpers.py:758
 #, python-format
 msgid "%s more"
 msgstr "%s mais"
 
-#: kallithea/lib/helpers.py:742
+#: kallithea/lib/helpers.py:759
 #: kallithea/templates/changelog/changelog.html:43
 msgid "revisions"
 msgstr "revisões"
 
-#: kallithea/lib/helpers.py:766
+#: kallithea/lib/helpers.py:783
 #, fuzzy, python-format
 msgid "Fork name %s"
 msgstr "nome da bifurcação %s"
 
-#: kallithea/lib/helpers.py:787
+#: kallithea/lib/helpers.py:804
 #, fuzzy, python-format
 msgid "Pull request %s"
 msgstr "Pull request #%s"
 
-#: kallithea/lib/helpers.py:797
+#: kallithea/lib/helpers.py:814
 msgid "[deleted] repository"
 msgstr "repositório [excluído]"
 
-#: kallithea/lib/helpers.py:799 kallithea/lib/helpers.py:811
+#: kallithea/lib/helpers.py:816 kallithea/lib/helpers.py:828
 msgid "[created] repository"
 msgstr "repositório [criado]"
 
-#: kallithea/lib/helpers.py:801
+#: kallithea/lib/helpers.py:818
 msgid "[created] repository as fork"
 msgstr "repositório [criado] como uma bifurcação"
 
-#: kallithea/lib/helpers.py:803 kallithea/lib/helpers.py:813
+#: kallithea/lib/helpers.py:820 kallithea/lib/helpers.py:830
 msgid "[forked] repository"
 msgstr "repositório [bifurcado]"
 
-#: kallithea/lib/helpers.py:805 kallithea/lib/helpers.py:815
+#: kallithea/lib/helpers.py:822 kallithea/lib/helpers.py:832
 msgid "[updated] repository"
 msgstr "repositório [atualizado]"
 
-#: kallithea/lib/helpers.py:807
+#: kallithea/lib/helpers.py:824
 msgid "[downloaded] archive from repository"
 msgstr "[baixado] archive do repositório"
 
-#: kallithea/lib/helpers.py:809
+#: kallithea/lib/helpers.py:826
 msgid "[delete] repository"
 msgstr "[excluir] repositório"
 
-#: kallithea/lib/helpers.py:817
+#: kallithea/lib/helpers.py:834
 msgid "[created] user"
 msgstr "usuário [criado]"
 
-#: kallithea/lib/helpers.py:819
+#: kallithea/lib/helpers.py:836
 msgid "[updated] user"
 msgstr "usuário [atualizado]"
 
-#: kallithea/lib/helpers.py:821
+#: kallithea/lib/helpers.py:838
 msgid "[created] user group"
 msgstr "[criado] grupo de usuários"
 
-#: kallithea/lib/helpers.py:823
+#: kallithea/lib/helpers.py:840
 msgid "[updated] user group"
 msgstr "[atualizado] grupo de usuários"
 
-#: kallithea/lib/helpers.py:825
+#: kallithea/lib/helpers.py:842
 msgid "[commented] on revision in repository"
 msgstr "[comentado] em revisão no repositório"
 
-#: kallithea/lib/helpers.py:827
+#: kallithea/lib/helpers.py:844
 msgid "[commented] on pull request for"
 msgstr "[comentado] no pull request para"
 
-#: kallithea/lib/helpers.py:829
+#: kallithea/lib/helpers.py:846
 msgid "[closed] pull request for"
 msgstr "[fechado] pull request para"
 
-#: kallithea/lib/helpers.py:831
+#: kallithea/lib/helpers.py:848
 msgid "[pushed] into"
 msgstr "[realizado push] para"
 
-#: kallithea/lib/helpers.py:833
+#: kallithea/lib/helpers.py:850
 msgid "[committed via Kallithea] into repository"
 msgstr "[commitado via Kallithea] no repositório"
 
-#: kallithea/lib/helpers.py:835
+#: kallithea/lib/helpers.py:852
 msgid "[pulled from remote] into repository"
 msgstr "[pulled do remote] no repositório"
 
-#: kallithea/lib/helpers.py:837
+#: kallithea/lib/helpers.py:854
 msgid "[pulled] from"
 msgstr "[realizado pull] a partir de"
 
-#: kallithea/lib/helpers.py:839
+#: kallithea/lib/helpers.py:856
 msgid "[started following] repository"
 msgstr "[passou a seguir] o repositório"
 
-#: kallithea/lib/helpers.py:841
+#: kallithea/lib/helpers.py:858
 msgid "[stopped following] repository"
 msgstr "[parou de seguir] o repositório"
 
-#: kallithea/lib/helpers.py:961
+#: kallithea/lib/helpers.py:975
 #, python-format
 msgid " and %s more"
 msgstr " e mais %s"
 
-#: kallithea/lib/helpers.py:965
+#: kallithea/lib/helpers.py:979
 #: kallithea/templates/compare/compare_diff.html:69
 #: kallithea/templates/pullrequests/pullrequest_show.html:297
 msgid "No files"
 msgstr "Nenhum arquivo"
 
-#: kallithea/lib/helpers.py:990
+#: kallithea/lib/helpers.py:1004
 msgid "new file"
 msgstr "novo arquivo"
 
-#: kallithea/lib/helpers.py:993
+#: kallithea/lib/helpers.py:1007
 msgid "mod"
 msgstr "mod"
 
-#: kallithea/lib/helpers.py:996
+#: kallithea/lib/helpers.py:1010
 msgid "del"
 msgstr "excluir"
 
-#: kallithea/lib/helpers.py:999
+#: kallithea/lib/helpers.py:1013
 msgid "rename"
 msgstr "renomear"
 
-#: kallithea/lib/helpers.py:1004
+#: kallithea/lib/helpers.py:1018
 msgid "chmod"
 msgstr "chmod"
 
-#: kallithea/lib/helpers.py:1297
+#: kallithea/lib/helpers.py:1323
 #, python-format
 msgid ""
 "%s repository is not mapped to db perhaps it was created or renamed from "
@@ -1273,96 +1271,98 @@
 "renomeado a partir do sistema de arquivos. Por favor, execute a aplicação "
 "outra vez para varrer novamente por repositórios"
 
-#: kallithea/lib/ssh.py:73
+#: kallithea/lib/ssh.py:75
 msgid "SSH key is missing"
 msgstr ""
 
-#: kallithea/lib/ssh.py:77
-msgid "Incorrect SSH key - it must have both a key type and a base64 part"
-msgstr ""
-
-#: kallithea/lib/ssh.py:81
+#: kallithea/lib/ssh.py:79
+msgid ""
+"Incorrect SSH key - it must have both a key type and a base64 part, like "
+"'ssh-rsa ASRNeaZu4FA...xlJp='"
+msgstr ""
+
+#: kallithea/lib/ssh.py:83
 msgid "Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'"
 msgstr ""
 
-#: kallithea/lib/ssh.py:84
+#: kallithea/lib/ssh.py:86
 #, python-format
 msgid "Incorrect SSH key - unexpected characters in base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:89
+#: kallithea/lib/ssh.py:91
 #, python-format
 msgid "Incorrect SSH key - failed to decode base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:92
+#: kallithea/lib/ssh.py:94
 #, python-format
 msgid "Incorrect SSH key - base64 part is not %r as claimed but %r"
 msgstr ""
 
-#: kallithea/lib/utils2.py:334
+#: kallithea/lib/utils2.py:253
 #, python-format
 msgid "%d year"
 msgid_plural "%d years"
 msgstr[0] "%d ano"
 msgstr[1] "%d anos"
 
-#: kallithea/lib/utils2.py:335
+#: kallithea/lib/utils2.py:254
 #, python-format
 msgid "%d month"
 msgid_plural "%d months"
 msgstr[0] "%d mês"
 msgstr[1] "%d meses"
 
-#: kallithea/lib/utils2.py:336
+#: kallithea/lib/utils2.py:255
 #, python-format
 msgid "%d day"
 msgid_plural "%d days"
 msgstr[0] "%d dia"
 msgstr[1] "%d dias"
 
-#: kallithea/lib/utils2.py:337
+#: kallithea/lib/utils2.py:256
 #, python-format
 msgid "%d hour"
 msgid_plural "%d hours"
 msgstr[0] "%d hora"
 msgstr[1] "%d horas"
 
-#: kallithea/lib/utils2.py:338
+#: kallithea/lib/utils2.py:257
 #, python-format
 msgid "%d minute"
 msgid_plural "%d minutes"
 msgstr[0] "%d minuto"
 msgstr[1] "%d minutos"
 
-#: kallithea/lib/utils2.py:339
+#: kallithea/lib/utils2.py:258
 #, python-format
 msgid "%d second"
 msgid_plural "%d seconds"
 msgstr[0] "%d segundo"
 msgstr[1] "%d segundos"
 
-#: kallithea/lib/utils2.py:355
+#: kallithea/lib/utils2.py:274
 #, python-format
 msgid "in %s"
 msgstr "em %s"
 
-#: kallithea/lib/utils2.py:357
+#: kallithea/lib/utils2.py:276
 #, python-format
 msgid "%s ago"
 msgstr "%s atrás"
 
-#: kallithea/lib/utils2.py:359
+#: kallithea/lib/utils2.py:278
 #, python-format
 msgid "in %s and %s"
 msgstr "em %s e %s"
 
-#: kallithea/lib/utils2.py:362
+#: kallithea/lib/utils2.py:281
 #, python-format
 msgid "%s and %s ago"
 msgstr "%s e %s atrás"
 
-#: kallithea/lib/utils2.py:365
+#: kallithea/lib/utils2.py:284
 msgid "just now"
 msgstr "agora há pouco"
 
@@ -1371,147 +1371,147 @@
 msgid "on line %s"
 msgstr "na linha %s"
 
-#: kallithea/model/comment.py:221 kallithea/model/pull_request.py:114
+#: kallithea/model/comment.py:219 kallithea/model/pull_request.py:112
 msgid "[Mention]"
 msgstr "[Menção]"
 
-#: kallithea/model/db.py:1496
+#: kallithea/model/db.py:1411
 msgid "top level"
 msgstr "nível superior"
 
-#: kallithea/model/db.py:1637
+#: kallithea/model/db.py:1542
 msgid "Kallithea Administrator"
 msgstr "Administrador do Kallithea"
 
-#: kallithea/model/db.py:1639
+#: kallithea/model/db.py:1544
 msgid "Default user has no access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1640
+#: kallithea/model/db.py:1545
 #, fuzzy
 msgid "Default user has read access to new repositories"
 msgstr "Acesso não autorizado ao recurso"
 
-#: kallithea/model/db.py:1641
+#: kallithea/model/db.py:1546
 #, fuzzy
 msgid "Default user has write access to new repositories"
 msgstr "Acesso não autorizado ao recurso"
 
-#: kallithea/model/db.py:1642
+#: kallithea/model/db.py:1547
 msgid "Default user has admin access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1644
+#: kallithea/model/db.py:1549
 msgid "Default user has no access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1645
+#: kallithea/model/db.py:1550
 msgid "Default user has read access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1646
+#: kallithea/model/db.py:1551
 msgid "Default user has write access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1647
+#: kallithea/model/db.py:1552
 msgid "Default user has admin access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1649
+#: kallithea/model/db.py:1554
 msgid "Default user has no access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1650
+#: kallithea/model/db.py:1555
 msgid "Default user has read access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1651
+#: kallithea/model/db.py:1556
 msgid "Default user has write access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1652
+#: kallithea/model/db.py:1557
 msgid "Default user has admin access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1654
+#: kallithea/model/db.py:1559
 #, fuzzy
 msgid "Only admins can create repository groups"
 msgstr "Grupo de repositórios %s criado"
 
-#: kallithea/model/db.py:1655
+#: kallithea/model/db.py:1560
 #, fuzzy
 msgid "Non-admins can create repository groups"
 msgstr "Grupo de repositórios %s criado"
 
-#: kallithea/model/db.py:1657
+#: kallithea/model/db.py:1562
 #, fuzzy
 msgid "Only admins can create user groups"
 msgstr "Criar grupos de usuários"
 
-#: kallithea/model/db.py:1658
+#: kallithea/model/db.py:1563
 #, fuzzy
 msgid "Non-admins can create user groups"
 msgstr "Criar grupos de usuários"
 
-#: kallithea/model/db.py:1660
+#: kallithea/model/db.py:1565
 msgid "Only admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1661
+#: kallithea/model/db.py:1566
 msgid "Non-admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1663
+#: kallithea/model/db.py:1568
 msgid ""
 "Repository creation enabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1664
+#: kallithea/model/db.py:1569
 msgid ""
 "Repository creation disabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1666
+#: kallithea/model/db.py:1571
 #, fuzzy
 msgid "Only admins can fork repositories"
 msgstr "Criar repositórios"
 
-#: kallithea/model/db.py:1667
+#: kallithea/model/db.py:1572
 #, fuzzy
 msgid "Non-admins can fork repositories"
 msgstr "Invalidar o cache para todos os repositórios"
 
-#: kallithea/model/db.py:1669
+#: kallithea/model/db.py:1574
 msgid "Registration disabled"
 msgstr "Registro desatilitado"
 
-#: kallithea/model/db.py:1670
+#: kallithea/model/db.py:1575
 #, fuzzy
 msgid "User registration with manual account activation"
 msgstr "Registro de Usuário com ativação manual de conta"
 
-#: kallithea/model/db.py:1671
+#: kallithea/model/db.py:1576
 #, fuzzy
 msgid "User registration with automatic account activation"
 msgstr "Registro de Usuário com ativação automática de conta"
 
-#: kallithea/model/db.py:2206
+#: kallithea/model/db.py:1992
 #, fuzzy
 msgid "Not reviewed"
 msgstr "Não Revisado"
 
-#: kallithea/model/db.py:2207
+#: kallithea/model/db.py:1993
 #, fuzzy
 msgid "Under review"
 msgstr "Sob Revisão"
 
-#: kallithea/model/db.py:2208
+#: kallithea/model/db.py:1994
 #, fuzzy
 #| msgid "Approved"
 msgid "Not approved"
 msgstr "Aprovado"
 
-#: kallithea/model/db.py:2209
+#: kallithea/model/db.py:1995
 msgid "Approved"
 msgstr "Aprovado"
 
@@ -1537,7 +1537,7 @@
 msgid "Name must not contain only digits"
 msgstr ""
 
-#: kallithea/model/notification.py:164
+#: kallithea/model/notification.py:162
 #, fuzzy, python-format
 #| msgid "[Comment] %(repo_name)s pull request %(pr_nice_id)s from %(ref)s"
 msgid ""
@@ -1545,111 +1545,111 @@
 "%(branch)s"
 msgstr "[comentado] no pull request para"
 
-#: kallithea/model/notification.py:167
+#: kallithea/model/notification.py:165
 #, fuzzy, python-format
 msgid "New user %(new_username)s registered"
 msgstr "O username \"%(new_username)s\" não é válido"
 
-#: kallithea/model/notification.py:169
+#: kallithea/model/notification.py:167
 #, python-format
 msgid ""
 "[Review] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:170
+#: kallithea/model/notification.py:168
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:183
+#: kallithea/model/notification.py:188
 #, fuzzy
 msgid "Closing"
 msgstr "Usando"
 
-#: kallithea/model/pull_request.py:73
+#: kallithea/model/pull_request.py:72
 #, fuzzy, python-format
 msgid ""
 "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s"
 msgstr ""
 "%(user)s solicita sua revisão no pull request $%(pr_id)s: %(pr_title)s"
 
-#: kallithea/model/pull_request.py:209
+#: kallithea/model/pull_request.py:207
 #, fuzzy
 #| msgid "Create Pull Request"
 msgid "Cannot create empty pull request"
 msgstr "Criar Pull Request"
 
-#: kallithea/model/pull_request.py:217
+#: kallithea/model/pull_request.py:215
 #, python-format
 msgid ""
 "Cannot create pull request - criss cross merge detected, please merge a "
 "later %s revision to %s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:245 kallithea/model/pull_request.py:380
+#: kallithea/model/pull_request.py:243 kallithea/model/pull_request.py:378
 #, fuzzy
 #| msgid "Confirm to delete this pull request"
 msgid "You are not authorized to create the pull request"
 msgstr "Confirme para excluir este pull request"
 
-#: kallithea/model/pull_request.py:339
+#: kallithea/model/pull_request.py:337
 msgid "Missing changesets since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:346
+#: kallithea/model/pull_request.py:344
 #, python-format
 msgid "New changesets on %s %s since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:353
+#: kallithea/model/pull_request.py:351
 msgid "Ancestor didn't change - diff since previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:360
+#: kallithea/model/pull_request.py:358
 #, python-format
 msgid ""
 "This iteration is based on another %s revision and there is no simple "
 "diff."
 msgstr ""
 
-#: kallithea/model/pull_request.py:362
+#: kallithea/model/pull_request.py:360
 #, python-format
 msgid "No changes found on %s %s since previous iteration."
 msgstr ""
 
-#: kallithea/model/pull_request.py:388
+#: kallithea/model/pull_request.py:386
 #, python-format
 msgid "Closed, next iteration: %s ."
 msgstr ""
 
-#: kallithea/model/scm.py:668
+#: kallithea/model/scm.py:655
 msgid "latest tip"
 msgstr "tip mais recente"
 
-#: kallithea/model/ssh_key.py:56
+#: kallithea/model/ssh_key.py:57
 #, python-format
 msgid "SSH key %r is invalid: %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:68
+#: kallithea/model/ssh_key.py:69
 #, python-format
 msgid "SSH key %s is already used by %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:89
+#: kallithea/model/ssh_key.py:88
 #, fuzzy, python-format
 #| msgid "Changeset not found"
-msgid "SSH key %r not found"
+msgid "SSH key with fingerprint %r found"
 msgstr "Conjunto de alterações não encontrado"
 
-#: kallithea/model/user.py:186
+#: kallithea/model/user.py:180
 msgid "New user registration"
 msgstr "Novo registro de usuário"
 
-#: kallithea/model/user.py:250
+#: kallithea/model/user.py:244
 #, fuzzy
 msgid ""
 "You can't remove this user since it is crucial for the entire application"
@@ -1657,7 +1657,7 @@
 "Você não pode remover esse usuário, pois ele é crucial para toda a "
 "aplicação"
 
-#: kallithea/model/user.py:255
+#: kallithea/model/user.py:249
 #, fuzzy, python-format
 msgid ""
 "User \"%s\" still owns %s repositories and cannot be removed. Switch "
@@ -1666,7 +1666,7 @@
 "usuário \"%s\" ainda é dono de %s repositórios e não pode ser removido. "
 "Troque os donos ou remova esses repositórios. %s"
 
-#: kallithea/model/user.py:260
+#: kallithea/model/user.py:254
 #, fuzzy, python-format
 msgid ""
 "User \"%s\" still owns %s repository groups and cannot be removed. Switch "
@@ -1675,7 +1675,7 @@
 "usuário \"%s\" ainda é dono de %s repositórios e não pode ser removido. "
 "Troque os donos ou remova esses repositórios. %s"
 
-#: kallithea/model/user.py:267
+#: kallithea/model/user.py:261
 #, fuzzy, python-format
 msgid ""
 "User \"%s\" still owns %s user groups and cannot be removed. Switch "
@@ -1684,37 +1684,37 @@
 "usuário \"%s\" ainda é dono de %s repositórios e não pode ser removido. "
 "Troque os donos ou remova esses repositórios. %s"
 
-#: kallithea/model/user.py:361
+#: kallithea/model/user.py:355
 msgid "Password reset link"
 msgstr "Link para trocar senha"
 
-#: kallithea/model/user.py:408
+#: kallithea/model/user.py:402
 #, fuzzy
 msgid "Password reset notification"
 msgstr "Link para trocar senha"
 
-#: kallithea/model/user.py:409
+#: kallithea/model/user.py:403
 #, python-format
 msgid ""
 "The password to your account %s has been changed using password reset "
 "form."
 msgstr ""
 
-#: kallithea/model/validators.py:52 kallithea/model/validators.py:53
+#: kallithea/model/validators.py:53 kallithea/model/validators.py:54
 msgid "Value cannot be an empty list"
 msgstr "O valor não pode ser uma lista vazia"
 
-#: kallithea/model/validators.py:72
+#: kallithea/model/validators.py:73
 #, python-format
 msgid "Username \"%(username)s\" already exists"
 msgstr "O username \\\"%(username)s\\\" já existe"
 
-#: kallithea/model/validators.py:74
+#: kallithea/model/validators.py:75
 #, fuzzy, python-format
 msgid "Username \"%(username)s\" cannot be used"
 msgstr "O username \"%(username)s\" não é válido"
 
-#: kallithea/model/validators.py:76
+#: kallithea/model/validators.py:77
 #, fuzzy
 msgid ""
 "Username may only contain alphanumeric characters underscores, periods or "
@@ -1723,25 +1723,25 @@
 "Nome de usuário pode conter somente caracteres alfanuméricos, sublinha, "
 "pontos e hífens e deve iniciar com caractere alfanumérico"
 
-#: kallithea/model/validators.py:103
+#: kallithea/model/validators.py:104
 msgid "The input is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:110
+#: kallithea/model/validators.py:111
 #, python-format
 msgid "Username %(username)s is not valid"
 msgstr "O username \"%(username)s\" não é válido"
 
-#: kallithea/model/validators.py:131
+#: kallithea/model/validators.py:132
 msgid "Invalid user group name"
 msgstr "Nome inválido de grupo de usuários"
 
-#: kallithea/model/validators.py:132
+#: kallithea/model/validators.py:133
 #, python-format
 msgid "User group \"%(usergroup)s\" already exists"
 msgstr "O grupo de usuários \"%(usergroup)s\" já existe"
 
-#: kallithea/model/validators.py:134
+#: kallithea/model/validators.py:135
 msgid ""
 "user group name may only contain alphanumeric characters underscores, "
 "periods or dashes and must begin with alphanumeric character"
@@ -1750,103 +1750,103 @@
 "underscores, pontos ou hífens, e deve começar om um caractere alfa-"
 "numérico"
 
-#: kallithea/model/validators.py:174
+#: kallithea/model/validators.py:175
 msgid "Cannot assign this group as parent"
 msgstr "Não é possível associar esse grupo como progenitor"
 
-#: kallithea/model/validators.py:175
+#: kallithea/model/validators.py:176
 #, python-format
 msgid "Group \"%(group_name)s\" already exists"
 msgstr "O grupo \\\"%(group_name)s\\\" já existe"
 
-#: kallithea/model/validators.py:177
+#: kallithea/model/validators.py:178
 #, python-format
 msgid "Repository with name \"%(group_name)s\" already exists"
 msgstr "Um repositório com o nome \"%(group_name)s\" já existe"
 
-#: kallithea/model/validators.py:233
+#: kallithea/model/validators.py:230
 msgid "Invalid characters (non-ascii) in password"
 msgstr "Caracteres inválidos (não-ascii) na senha"
 
-#: kallithea/model/validators.py:248
+#: kallithea/model/validators.py:245
 msgid "Invalid old password"
 msgstr ""
 
-#: kallithea/model/validators.py:264
+#: kallithea/model/validators.py:261
 msgid "Passwords do not match"
 msgstr "Senhas não conferem"
 
-#: kallithea/model/validators.py:279
+#: kallithea/model/validators.py:276
 #, fuzzy
 msgid "Invalid username or password"
 msgstr "senha inválida"
 
-#: kallithea/model/validators.py:313
+#: kallithea/model/validators.py:310
 #, fuzzy, python-format
 msgid "Repository name %(repo)s is not allowed"
 msgstr "O nome de repositório %(repo)s não é permitido"
 
-#: kallithea/model/validators.py:315
+#: kallithea/model/validators.py:312
 #, python-format
 msgid "Repository named %(repo)s already exists"
 msgstr "Um repositório chamado %(repo)s já existe"
 
-#: kallithea/model/validators.py:316
+#: kallithea/model/validators.py:313
 #, python-format
 msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
 msgstr "Um repositório \"%(repo)s\" já existe no grupo \"%(group)s\""
 
-#: kallithea/model/validators.py:318
+#: kallithea/model/validators.py:315
 #, python-format
 msgid "Repository group with name \"%(repo)s\" already exists"
 msgstr "Um Grupo de Repositórios chamado \"%(repo)s\" já existe"
 
-#: kallithea/model/validators.py:404
+#: kallithea/model/validators.py:401
 #, fuzzy
 msgid "Invalid repository URL"
 msgstr "repositório privado"
 
-#: kallithea/model/validators.py:405
+#: kallithea/model/validators.py:402
 msgid ""
 "Invalid repository URL. It must be a valid http, https, ssh, svn+http or "
 "svn+https URL"
 msgstr ""
 
-#: kallithea/model/validators.py:430
+#: kallithea/model/validators.py:427
 msgid "Fork has to be the same type as parent"
 msgstr "A bifurcação deve ser do mesmo tipo que o pai"
 
-#: kallithea/model/validators.py:445
+#: kallithea/model/validators.py:442
 msgid "You don't have permissions to create repository in this group"
 msgstr "Você não tem permissão para criar um repositório neste grupo"
 
-#: kallithea/model/validators.py:447
+#: kallithea/model/validators.py:444
 msgid "no permission to create repository in root location"
 msgstr "você não tem permissão para criar um repositório na raiz"
 
-#: kallithea/model/validators.py:497
+#: kallithea/model/validators.py:494
 msgid "You don't have permissions to create a group in this location"
 msgstr "Você não tem permissão para criar um grupo neste local"
 
-#: kallithea/model/validators.py:537
+#: kallithea/model/validators.py:534
 msgid "This username or user group name is not valid"
 msgstr "Este nome de usuário ou de grupo de usuários não é válido"
 
-#: kallithea/model/validators.py:630
+#: kallithea/model/validators.py:627
 msgid "This is not a valid path"
 msgstr "Esse não é um caminho válido"
 
-#: kallithea/model/validators.py:647
+#: kallithea/model/validators.py:644
 #, fuzzy
 msgid "This email address is already in use"
 msgstr "Esse endereço de e-mail já está tomado"
 
-#: kallithea/model/validators.py:667
+#: kallithea/model/validators.py:664
 #, fuzzy, python-format
 msgid "Email address \"%(email)s\" not found"
 msgstr "o e-mail \"%(email)s\" não existe."
 
-#: kallithea/model/validators.py:704
+#: kallithea/model/validators.py:701
 msgid ""
 "The LDAP Login attribute of the CN must be specified - this is the name "
 "of the attribute that is equivalent to \"username\""
@@ -1854,26 +1854,26 @@
 "O atributo de login LDAP do CN deve ser especificado - isto é o nome do "
 "atributo que é equivalente ao 'nome de usuário'"
 
-#: kallithea/model/validators.py:716
+#: kallithea/model/validators.py:713
 msgid "Please enter a valid IPv4 or IPv6 address"
 msgstr "Por favor, forneça um endereço válido IPv4 ou IPv6"
 
-#: kallithea/model/validators.py:717
+#: kallithea/model/validators.py:714
 #, python-format
 msgid ""
 "The network size (bits) must be within the range of 0-32 (not %(bits)r)"
 msgstr ""
 "O tamanho da rede (bits) deve estar no intervalo 0-32 (não %(bits)r)"
 
-#: kallithea/model/validators.py:750
+#: kallithea/model/validators.py:747
 msgid "Key name can only consist of letters, underscore, dash or numbers"
 msgstr "O nome da chave só pode conter letras, underscore, hífen ou dígitos"
 
-#: kallithea/model/validators.py:764
+#: kallithea/model/validators.py:761
 msgid "Filename cannot be inside a directory"
 msgstr "O nome de arquivo não pode estar dentro de um diretório"
 
-#: kallithea/model/validators.py:780
+#: kallithea/model/validators.py:777
 #, python-format
 msgid "Plugins %(loaded)s and %(next_to_load)s both export the same name"
 msgstr ""
@@ -1933,7 +1933,7 @@
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:60
 #: kallithea/templates/email_templates/pull_request.html:37
 #: kallithea/templates/forks/fork.html:34
-#: kallithea/templates/index_base.html:58
+#: kallithea/templates/index_base.html:59
 #: kallithea/templates/pullrequests/pullrequest.html:33
 #: kallithea/templates/pullrequests/pullrequest_show.html:38
 #: kallithea/templates/pullrequests/pullrequest_show.html:59
@@ -1941,14 +1941,14 @@
 msgid "Description"
 msgstr "Descrição"
 
-#: kallithea/templates/index_base.html:60
+#: kallithea/templates/index_base.html:61
 msgid "Last Change"
 msgstr "Última Alteração"
 
 #: kallithea/templates/admin/my_account/my_account_repos.html:15
 #: kallithea/templates/admin/my_account/my_account_watched.html:15
 #: kallithea/templates/admin/repos/repos.html:41
-#: kallithea/templates/index_base.html:62
+#: kallithea/templates/index_base.html:63
 msgid "Tip"
 msgstr "Ponta"
 
@@ -1958,7 +1958,7 @@
 #: kallithea/templates/admin/repos/repos.html:42
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:8
 #: kallithea/templates/admin/user_groups/user_groups.html:42
-#: kallithea/templates/index_base.html:63
+#: kallithea/templates/index_base.html:64
 #: kallithea/templates/pullrequests/pullrequest_data.html:16
 #: kallithea/templates/pullrequests/pullrequest_show.html:124
 #: kallithea/templates/pullrequests/pullrequest_show.html:219
@@ -1982,7 +1982,7 @@
 #: kallithea/templates/admin/users/user_edit_profile.html:18
 #: kallithea/templates/admin/users/users.html:37
 #: kallithea/templates/base/base.html:364
-#: kallithea/templates/email_templates/registration.html:11
+#: kallithea/templates/email_templates/registration.html:12
 #: kallithea/templates/login.html:28 kallithea/templates/register.html:31
 msgid "Username"
 msgstr "Nome de usuário"
@@ -2113,7 +2113,7 @@
 #: kallithea/templates/admin/settings/settings.html:31
 #: kallithea/templates/admin/users/user_add.html:62
 #: kallithea/templates/admin/users/user_edit_profile.html:25
-#: kallithea/templates/email_templates/registration.html:33
+#: kallithea/templates/email_templates/registration.html:34
 #: kallithea/templates/register.html:66
 msgid "Email"
 msgstr "E-mail"
@@ -2366,7 +2366,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/gists/index.html:51
-#: kallithea/templates/data_table/_dt_elements.html:78
+#: kallithea/templates/data_table/_dt_elements.html:84
 msgid "Created"
 msgstr "Criado"
 
@@ -2450,13 +2450,13 @@
 #: kallithea/templates/admin/users/user_edit_ips.html:21
 #: kallithea/templates/changeset/changeset_file_comment.html:30
 #: kallithea/templates/changeset/changeset_file_comment.html:121
-#: kallithea/templates/data_table/_dt_elements.html:69
-#: kallithea/templates/data_table/_dt_elements.html:89
-#: kallithea/templates/data_table/_dt_elements.html:91
-#: kallithea/templates/data_table/_dt_elements.html:101
-#: kallithea/templates/data_table/_dt_elements.html:103
-#: kallithea/templates/data_table/_dt_elements.html:120
-#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:75
+#: kallithea/templates/data_table/_dt_elements.html:95
+#: kallithea/templates/data_table/_dt_elements.html:97
+#: kallithea/templates/data_table/_dt_elements.html:107
+#: kallithea/templates/data_table/_dt_elements.html:109
+#: kallithea/templates/data_table/_dt_elements.html:126
+#: kallithea/templates/data_table/_dt_elements.html:128
 #: kallithea/templates/files/files_source.html:35
 #: kallithea/templates/files/files_source.html:38
 #: kallithea/templates/files/files_source.html:41
@@ -2472,14 +2472,14 @@
 #: kallithea/templates/base/perms_summary.html:44
 #: kallithea/templates/base/perms_summary.html:81
 #: kallithea/templates/base/perms_summary.html:83
-#: kallithea/templates/data_table/_dt_elements.html:63
-#: kallithea/templates/data_table/_dt_elements.html:64
-#: kallithea/templates/data_table/_dt_elements.html:85
-#: kallithea/templates/data_table/_dt_elements.html:86
-#: kallithea/templates/data_table/_dt_elements.html:97
-#: kallithea/templates/data_table/_dt_elements.html:98
-#: kallithea/templates/data_table/_dt_elements.html:116
-#: kallithea/templates/data_table/_dt_elements.html:117
+#: kallithea/templates/data_table/_dt_elements.html:69
+#: kallithea/templates/data_table/_dt_elements.html:70
+#: kallithea/templates/data_table/_dt_elements.html:91
+#: kallithea/templates/data_table/_dt_elements.html:92
+#: kallithea/templates/data_table/_dt_elements.html:103
+#: kallithea/templates/data_table/_dt_elements.html:104
+#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:123
 #: kallithea/templates/files/diff_2way.html:56
 #: kallithea/templates/files/files_source.html:37
 #: kallithea/templates/files/files_source.html:40
@@ -2803,7 +2803,7 @@
 #: kallithea/templates/admin/permissions/permissions_globals.html:27
 #: kallithea/templates/admin/repos/repo_add_base.html:28
 #: kallithea/templates/admin/repos/repo_edit_settings.html:33
-#: kallithea/templates/data_table/_dt_elements.html:134
+#: kallithea/templates/data_table/_dt_elements.html:140
 #: kallithea/templates/forks/fork.html:42
 msgid "Repository group"
 msgstr "Grupo de repositórios"
@@ -2828,7 +2828,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:40
-#: kallithea/templates/data_table/_dt_elements.html:141
+#: kallithea/templates/data_table/_dt_elements.html:147
 msgid "User group"
 msgstr "Grupo de usuários"
 
@@ -3007,7 +3007,7 @@
 msgstr "Criado em"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:121
+#: kallithea/templates/data_table/_dt_elements.html:127
 #, python-format
 msgid "Confirm to delete this group: %s with %s repository"
 msgid_plural "Confirm to delete this group: %s with %s repositories"
@@ -3189,14 +3189,10 @@
 msgstr "Campos extras"
 
 #: kallithea/templates/admin/repos/repo_edit.html:37
-msgid "Caches"
-msgstr ""
+msgid "Remote"
+msgstr "Remoto"
 
 #: kallithea/templates/admin/repos/repo_edit.html:40
-msgid "Remote"
-msgstr "Remoto"
-
-#: kallithea/templates/admin/repos/repo_edit.html:43
 #: kallithea/templates/summary/statistics.html:8
 #: kallithea/templates/summary/summary.html:169
 #: kallithea/templates/summary/summary.html:170
@@ -3243,7 +3239,7 @@
 "diário público"
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:46
-#: kallithea/templates/data_table/_dt_elements.html:68
+#: kallithea/templates/data_table/_dt_elements.html:74
 #, python-format
 msgid "Confirm to delete this repository: %s"
 msgstr "Confirma excluir esse repositório: %s"
@@ -3275,48 +3271,14 @@
 "it or restore it."
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:4
-#, fuzzy
-msgid "Invalidate Repository Cache"
-msgstr "Invalidar cache do repositório"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:6
-#, fuzzy
-msgid ""
-"Manually invalidate cache for this repository. On first access, the "
-"repository will be cached again."
-msgstr ""
-"Invalidar manualmente o cache deste repositório. No próximo acesso o "
-"repositório será cacheado novamente"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:9
-#, fuzzy
-msgid "List of Cached Values"
-msgstr "Lista de valores cacheados"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:12
-msgid "Prefix"
-msgstr "Prefixo"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:13
+#: kallithea/templates/admin/repos/repo_edit_fields.html:6
+msgid "Label"
+msgstr ""
+
 #: kallithea/templates/admin/repos/repo_edit_fields.html:7
 msgid "Key"
 msgstr "Chave"
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:14
-#: kallithea/templates/admin/user_groups/user_group_add.html:40
-#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
-#: kallithea/templates/admin/user_groups/user_groups.html:41
-#: kallithea/templates/admin/users/user_add.html:69
-#: kallithea/templates/admin/users/user_edit_profile.html:74
-#: kallithea/templates/admin/users/users.html:42
-msgid "Active"
-msgstr "Ativo"
-
-#: kallithea/templates/admin/repos/repo_edit_fields.html:6
-msgid "Label"
-msgstr ""
-
 #: kallithea/templates/admin/repos/repo_edit_fields.html:20
 #, python-format
 msgid "Confirm to delete this field: %s"
@@ -3860,6 +3822,15 @@
 msgid "Short, optional description for this user group."
 msgstr ""
 
+#: kallithea/templates/admin/user_groups/user_group_add.html:40
+#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
+#: kallithea/templates/admin/user_groups/user_groups.html:41
+#: kallithea/templates/admin/users/user_add.html:69
+#: kallithea/templates/admin/users/user_edit_profile.html:74
+#: kallithea/templates/admin/users/users.html:42
+msgid "Active"
+msgstr "Ativo"
+
 #: kallithea/templates/admin/user_groups/user_group_edit.html:5
 #, python-format
 msgid "%s user group settings"
@@ -3882,7 +3853,7 @@
 msgstr "Membros"
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:19
-#: kallithea/templates/data_table/_dt_elements.html:102
+#: kallithea/templates/data_table/_dt_elements.html:108
 #, python-format
 msgid "Confirm to delete this user group: %s"
 msgstr "Confirme para excluir este grupo de usuário: %s"
@@ -3957,7 +3928,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:90
+#: kallithea/templates/data_table/_dt_elements.html:96
 #, python-format
 msgid "Confirm to delete this user: %s"
 msgstr "Confirma excluir este usuário: %s"
@@ -4060,10 +4031,12 @@
 msgstr "Pesquisar"
 
 #: kallithea/templates/base/base.html:167
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Follow"
 msgstr "Seguir"
 
 #: kallithea/templates/base/base.html:168
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Unfollow"
 msgstr "Parar de seguir"
 
@@ -4502,26 +4475,26 @@
 msgid "Merge"
 msgstr "mesclar"
 
-#: kallithea/templates/changeset/changeset.html:96
+#: kallithea/templates/changeset/changeset.html:95
 #, fuzzy
 msgid "Grafted from:"
 msgstr "Criado em"
 
-#: kallithea/templates/changeset/changeset.html:102
+#: kallithea/templates/changeset/changeset.html:100
 msgid "Transplanted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:108
+#: kallithea/templates/changeset/changeset.html:106
 #, fuzzy
 msgid "Replaced by:"
 msgstr "criado"
 
-#: kallithea/templates/changeset/changeset.html:122
+#: kallithea/templates/changeset/changeset.html:120
 #, fuzzy
 msgid "Preceded by:"
 msgstr "criado"
 
-#: kallithea/templates/changeset/changeset.html:139
+#: kallithea/templates/changeset/changeset.html:137
 #: kallithea/templates/compare/compare_diff.html:59
 #: kallithea/templates/pullrequests/pullrequest_show.html:290
 #, python-format
@@ -4530,7 +4503,7 @@
 msgstr[0] "%s arquivo modificado"
 msgstr[1] "%s arquivos modificados"
 
-#: kallithea/templates/changeset/changeset.html:141
+#: kallithea/templates/changeset/changeset.html:139
 #: kallithea/templates/compare/compare_diff.html:61
 #: kallithea/templates/pullrequests/pullrequest_show.html:292
 #, python-format
@@ -4539,8 +4512,8 @@
 msgstr[0] "%s arquivo modificado com %s inserções e %s exclusões"
 msgstr[1] "%s arquivos modificados com %s inserções e %s exclusões"
 
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
 #: kallithea/templates/pullrequests/pullrequest_show.html:333
@@ -4800,23 +4773,23 @@
 msgid "Repository creation in progress..."
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:42
+#: kallithea/templates/data_table/_dt_elements.html:48
 msgid "No changesets yet"
 msgstr "Nenhum conjunto de alterações ainda."
 
-#: kallithea/templates/data_table/_dt_elements.html:48
-#: kallithea/templates/data_table/_dt_elements.html:50
+#: kallithea/templates/data_table/_dt_elements.html:54
+#: kallithea/templates/data_table/_dt_elements.html:56
 #, python-format
 msgid "Subscribe to %s rss feed"
 msgstr "Assinar o feed rss de %s"
 
-#: kallithea/templates/data_table/_dt_elements.html:56
-#: kallithea/templates/data_table/_dt_elements.html:58
+#: kallithea/templates/data_table/_dt_elements.html:62
+#: kallithea/templates/data_table/_dt_elements.html:64
 #, python-format
 msgid "Subscribe to %s atom feed"
 msgstr "Assinar o feed atom de %s"
 
-#: kallithea/templates/data_table/_dt_elements.html:76
+#: kallithea/templates/data_table/_dt_elements.html:82
 msgid "Creating"
 msgstr ""
 
@@ -4854,6 +4827,13 @@
 msgid "by"
 msgstr ""
 
+#: kallithea/templates/email_templates/changeset_comment.html:36
+#: kallithea/templates/email_templates/pull_request_comment.html:43
+#, fuzzy
+#| msgid "Comment"
+msgid "View Comment"
+msgstr "Comentário"
+
 #: kallithea/templates/email_templates/comment.html:27
 #, fuzzy
 #| msgid "Status change"
@@ -4866,33 +4846,43 @@
 msgid "The pull request has been closed."
 msgstr "Repositório não está travado"
 
-#: kallithea/templates/email_templates/password_reset.html:9
+#: kallithea/templates/email_templates/default.html:4
+msgid "Message"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:4
+#, fuzzy
+#| msgid "Password Reset"
+msgid "Password Reset Request"
+msgstr "Senha Trocada"
+
+#: kallithea/templates/email_templates/password_reset.html:10
 #, python-format
 msgid "Hello %s"
 msgstr "Olá %s"
 
-#: kallithea/templates/email_templates/password_reset.html:16
+#: kallithea/templates/email_templates/password_reset.html:17
 #, fuzzy
 msgid "We have received a request to reset the password for your account."
 msgstr "Recebemos uma requisição para criar uma nova senha para sua conta."
 
-#: kallithea/templates/email_templates/password_reset.html:25
+#: kallithea/templates/email_templates/password_reset.html:26
 msgid ""
 "This account is however managed outside this system and the password "
 "cannot be changed here."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:28
+#: kallithea/templates/email_templates/password_reset.html:29
 msgid "To set a new password, click the following link"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:33
+#: kallithea/templates/email_templates/password_reset.html:34
 msgid ""
 "Should you not be able to use the link above, please type the following "
 "code into the password reset form"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:44
+#: kallithea/templates/email_templates/password_reset.html:45
 msgid ""
 "If it weren't you who requested the password reset, just disregard this "
 "message."
@@ -4925,6 +4915,11 @@
 msgid "to"
 msgstr ""
 
+#: kallithea/templates/email_templates/pull_request.html:85
+#, fuzzy
+msgid "View Pull Request"
+msgstr "Novo pull request"
+
 #: kallithea/templates/email_templates/pull_request_comment.html:4
 #, fuzzy, python-format
 #| msgid "%(user)s commented on pull request %(age)s"
@@ -4943,12 +4938,24 @@
 msgid "Comment on Pull Request %s \"%s\""
 msgstr "[comentado] no pull request para"
 
-#: kallithea/templates/email_templates/registration.html:22
+#: kallithea/templates/email_templates/registration.html:5
+#, fuzzy
+#| msgid "New user registration"
+msgid "New User Registration"
+msgstr "Novo registro de usuário"
+
+#: kallithea/templates/email_templates/registration.html:23
 #, fuzzy
 #| msgid "Group name"
 msgid "Full Name"
 msgstr "Nome do grupo"
 
+#: kallithea/templates/email_templates/registration.html:42
+#, fuzzy
+#| msgid "View this user here"
+msgid "View User Profile"
+msgstr "Veja este usuário aqui"
+
 #: kallithea/templates/files/diff_2way.html:15
 #, python-format
 msgid "%s File side-by-side diff"
@@ -5582,45 +5589,45 @@
 msgid "Stats gathered: "
 msgstr "Estatísticas coletadas:"
 
-#: kallithea/templates/summary/statistics.html:87
-#: kallithea/templates/summary/summary.html:371
+#: kallithea/templates/summary/statistics.html:85
+#: kallithea/templates/summary/summary.html:369
 msgid "files"
 msgstr "arquivos"
 
-#: kallithea/templates/summary/statistics.html:111
-#: kallithea/templates/summary/summary.html:401
+#: kallithea/templates/summary/statistics.html:109
+#: kallithea/templates/summary/summary.html:399
 msgid "Show more"
 msgstr "Mostrar mais"
 
-#: kallithea/templates/summary/statistics.html:405
+#: kallithea/templates/summary/statistics.html:395
 msgid "commits"
 msgstr "commits"
 
-#: kallithea/templates/summary/statistics.html:406
+#: kallithea/templates/summary/statistics.html:396
 msgid "files added"
 msgstr "arquivos adicionados"
 
-#: kallithea/templates/summary/statistics.html:407
+#: kallithea/templates/summary/statistics.html:397
 msgid "files changed"
 msgstr "arquivos alterados"
 
-#: kallithea/templates/summary/statistics.html:408
+#: kallithea/templates/summary/statistics.html:398
 msgid "files removed"
 msgstr "arquivos removidos"
 
-#: kallithea/templates/summary/statistics.html:410
+#: kallithea/templates/summary/statistics.html:400
 msgid "commit"
 msgstr "commit"
 
-#: kallithea/templates/summary/statistics.html:411
+#: kallithea/templates/summary/statistics.html:401
 msgid "file added"
 msgstr "arquivo adicionado"
 
-#: kallithea/templates/summary/statistics.html:412
+#: kallithea/templates/summary/statistics.html:402
 msgid "file changed"
 msgstr "arquivo alterado"
 
-#: kallithea/templates/summary/statistics.html:413
+#: kallithea/templates/summary/statistics.html:403
 msgid "file removed"
 msgstr "arquivo removido"
 
@@ -5728,6 +5735,28 @@
 msgid "Download %s as %s"
 msgstr "Descarregar %s como %s"
 
+#~ msgid "An error occurred during cache invalidation"
+#~ msgstr "Ocorreu um erro ao invalidar o cache"
+
+#, fuzzy
+#~ msgid "Invalidate Repository Cache"
+#~ msgstr "Invalidar cache do repositório"
+
+#, fuzzy
+#~ msgid ""
+#~ "Manually invalidate cache for this repository. On first access, the "
+#~ "repository will be cached again."
+#~ msgstr ""
+#~ "Invalidar manualmente o cache deste repositório. No próximo acesso o "
+#~ "repositório será cacheado novamente"
+
+#, fuzzy
+#~ msgid "List of Cached Values"
+#~ msgstr "Lista de valores cacheados"
+
+#~ msgid "Prefix"
+#~ msgstr "Prefixo"
+
 #~ msgid "This repository has been locked by %s on %s"
 #~ msgstr "Este repositório foi travado por %s em %s"
 
@@ -6000,9 +6029,6 @@
 #~ msgid "The comment was made with status"
 #~ msgstr "O pull request foi fechado com o estado"
 
-#~ msgid "View this user here"
-#~ msgstr "Veja este usuário aqui"
-
 #~ msgid "Repository Size"
 #~ msgstr "Tamanho do Repositório"
 
--- a/kallithea/i18n/ru/LC_MESSAGES/kallithea.po	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/i18n/ru/LC_MESSAGES/kallithea.po	Mon May 04 19:24:04 2020 +0200
@@ -4,9 +4,9 @@
 msgstr ""
 "Project-Id-Version: Kallithea 0.3\n"
 "Report-Msgid-Bugs-To: translations@kallithea-scm.org\n"
-"POT-Creation-Date: 2020-01-05 04:10+0100\n"
-"PO-Revision-Date: 2017-01-05 14:58+0000\n"
-"Last-Translator: Andrej Shadura <andrew@shadura.me>\n"
+"POT-Creation-Date: 2020-05-04 19:12+0200\n"
+"PO-Revision-Date: 2020-02-13 13:50+0000\n"
+"Last-Translator: Private <adamantine.sword@gmail.com>\n"
 "Language-Team: Russian <https://hosted.weblate.org/projects/kallithea/"
 "kallithea/ru/>\n"
 "Language: ru\n"
@@ -15,17 +15,17 @@
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
-"X-Generator: Weblate 2.11-dev\n"
+"X-Generator: Weblate 3.11-dev\n"
 
 #: kallithea/controllers/changelog.py:67
-#: kallithea/controllers/pullrequests.py:250 kallithea/lib/base.py:602
+#: kallithea/controllers/pullrequests.py:247 kallithea/lib/base.py:602
 msgid "There are no changesets yet"
-msgstr "Ещё не было изменений"
+msgstr "Наборы изменений отсутствуют"
 
 #: kallithea/controllers/admin/permissions.py:64
 #: kallithea/controllers/admin/permissions.py:68
 #: kallithea/controllers/admin/permissions.py:72
-#: kallithea/controllers/changelog.py:137
+#: kallithea/controllers/changelog.py:136
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:7
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:88
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:7
@@ -34,122 +34,119 @@
 msgid "None"
 msgstr "Ничего"
 
-#: kallithea/controllers/changelog.py:140 kallithea/controllers/files.py:189
+#: kallithea/controllers/changelog.py:139 kallithea/controllers/files.py:189
 msgid "(closed)"
 msgstr "(закрыто)"
 
-#: kallithea/controllers/changeset.py:81
+#: kallithea/controllers/changeset.py:82
 msgid "Show whitespace"
 msgstr "Отображать пробелы"
 
-#: kallithea/controllers/changeset.py:88
-#: kallithea/controllers/changeset.py:95
+#: kallithea/controllers/changeset.py:89
+#: kallithea/controllers/changeset.py:96
 #: kallithea/templates/files/diff_2way.html:55
 msgid "Ignore whitespace"
 msgstr "Игнорировать пробелы"
 
-#: kallithea/controllers/changeset.py:161
+#: kallithea/controllers/changeset.py:162
 #, python-format
 msgid "Increase diff context to %(num)s lines"
 msgstr "Увеличить контекст до %(num)s строк"
 
-#: kallithea/controllers/changeset.py:201
-#, fuzzy
-#| msgid "No permissions defined yet"
+#: kallithea/controllers/changeset.py:202
 msgid "No permission to change status"
-msgstr "Привилегии еще не назначены"
-
-#: kallithea/controllers/changeset.py:212
-#, fuzzy, python-format
+msgstr "Недостаточно привилегий для изменения статуса"
+
+#: kallithea/controllers/changeset.py:213
+#, python-format
 msgid "Successfully deleted pull request %s"
-msgstr "Pull-запрос успешно удалён"
+msgstr "Pull-запрос %s успешно удалён"
 
 #: kallithea/controllers/changeset.py:319 kallithea/controllers/files.py:89
-#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:700
+#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:697
 msgid "Such revision does not exist for this repository"
 msgstr "Нет такой ревизии в этом репозитории"
 
 #: kallithea/controllers/compare.py:68
-#, fuzzy, python-format
-#| msgid "Go to tip of repository"
+#, python-format
 msgid "Could not find other repository %s"
-msgstr "Перейти на верхушку репозитория"
+msgstr "Не найден другой репозиторий %s"
 
 #: kallithea/controllers/compare.py:74
-#, fuzzy
-#| msgid "Cannot compare repositories without using common ancestor"
 msgid "Cannot compare repositories of different types"
-msgstr "Невозможно сравнивать репозитории без общего предка"
-
-#: kallithea/controllers/compare.py:246
+msgstr "Невозможно сравнивать репозитории различных типов"
+
+#: kallithea/controllers/compare.py:247
 msgid "Cannot show empty diff"
-msgstr ""
-
-#: kallithea/controllers/compare.py:248
+msgstr "Отсутствуют изменения для отображения"
+
+#: kallithea/controllers/compare.py:249
 msgid "No ancestor found for merge diff"
-msgstr ""
-
-#: kallithea/controllers/compare.py:252
+msgstr "Не найдено предка для слияния"
+
+#: kallithea/controllers/compare.py:253
 msgid "Multiple merge ancestors found for merge compare"
-msgstr ""
-
-#: kallithea/controllers/compare.py:268
+msgstr "Найдено несколько предков для сравнения слияния"
+
+#: kallithea/controllers/compare.py:269
 msgid "Cannot compare repositories without using common ancestor"
 msgstr "Невозможно сравнивать репозитории без общего предка"
 
-#: kallithea/controllers/error.py:71
+#: kallithea/controllers/error.py:67
 msgid "No response"
 msgstr "Нет ответа"
 
-#: kallithea/controllers/error.py:72
+#: kallithea/controllers/error.py:68
 msgid "Unknown error"
 msgstr "Неизвестная ошибка"
 
-#: kallithea/controllers/error.py:85
+#: kallithea/controllers/error.py:81
 msgid ""
 "The request could not be understood by the server due to malformed syntax."
 msgstr "Запрос не распознан сервером из-за неправильного синтаксиса."
 
-#: kallithea/controllers/error.py:88
+#: kallithea/controllers/error.py:84
 msgid "Unauthorized access to resource"
 msgstr "Несанкционированный доступ к ресурсу"
 
-#: kallithea/controllers/error.py:90
+#: kallithea/controllers/error.py:86
 msgid "You don't have permission to view this page"
 msgstr "У вас нет прав для просмотра этой страницы"
 
-#: kallithea/controllers/error.py:92
+#: kallithea/controllers/error.py:88
 msgid "The resource could not be found"
 msgstr "Ресурс не найден"
 
-#: kallithea/controllers/error.py:94
+#: kallithea/controllers/error.py:90
 msgid ""
 "The server encountered an unexpected condition which prevented it from "
 "fulfilling the request."
 msgstr ""
 "Сервер не может выполнить запрос из-за неправильного условия в запросе."
 
-#: kallithea/controllers/feed.py:63
+#: kallithea/controllers/feed.py:59
 #, python-format
 msgid "%s committed on %s"
 msgstr "%s выполнил коммит в %s"
 
-#: kallithea/controllers/feed.py:88
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/controllers/feed.py:84
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/compare/compare_diff.html:95
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
 #: kallithea/templates/pullrequests/pullrequest_show.html:333
 msgid "Changeset was too big and was cut off..."
-msgstr "Изменения оказались слишком большими и были вырезаны..."
-
-#: kallithea/controllers/feed.py:111 kallithea/controllers/feed.py:140
+msgstr ""
+"Список изменений оказался слишком большим для отображения и был "
+"сокращён..."
+
+#: kallithea/controllers/feed.py:107
 #, python-format
 msgid "%s %s feed"
 msgstr "Лента новостей %s %s"
 
-#: kallithea/controllers/feed.py:113 kallithea/controllers/feed.py:142
+#: kallithea/controllers/feed.py:109
 #, python-format
 msgid "Changes on %s repository"
 msgstr "Изменения в репозитории %s"
@@ -159,118 +156,116 @@
 msgstr "Нажмите чтобы добавить новый файл"
 
 #: kallithea/controllers/files.py:86
-#, fuzzy
-#| msgid "There are no files yet. %s"
 msgid "There are no files yet."
-msgstr "Нет файлов. %s"
+msgstr "Нет файлов."
 
 #: kallithea/controllers/files.py:186
 #, python-format
 msgid "%s at %s"
 msgstr "%s (%s)"
 
-#: kallithea/controllers/files.py:296
+#: kallithea/controllers/files.py:295
 msgid "You can only delete files with revision being a valid branch"
 msgstr ""
 "Вы можете удалять файлы только в ревизии, являющейся корректной веткой"
 
-#: kallithea/controllers/files.py:307
+#: kallithea/controllers/files.py:306
 #, python-format
 msgid "Deleted file %s via Kallithea"
 msgstr "Файл %s удалён с помощью Kallithea"
 
-#: kallithea/controllers/files.py:331
+#: kallithea/controllers/files.py:330
 #, python-format
 msgid "Successfully deleted file %s"
 msgstr "Файл %s удалён"
 
-#: kallithea/controllers/files.py:335 kallithea/controllers/files.py:394
-#: kallithea/controllers/files.py:469
+#: kallithea/controllers/files.py:334 kallithea/controllers/files.py:392
+#: kallithea/controllers/files.py:467
 msgid "Error occurred during commit"
 msgstr "Во время коммита произошла ошибка"
 
-#: kallithea/controllers/files.py:350
+#: kallithea/controllers/files.py:349
 msgid "You can only edit files with revision being a valid branch"
 msgstr ""
 "Вы можете редактировать файлы только в ревизии, связанной с существующей "
 "веткой"
 
-#: kallithea/controllers/files.py:364
+#: kallithea/controllers/files.py:363
 #, python-format
 msgid "Edited file %s via Kallithea"
 msgstr "Файл %s отредактирован с помощью Kallithea"
 
-#: kallithea/controllers/files.py:380
+#: kallithea/controllers/files.py:378
 msgid "No changes"
 msgstr "Без изменений"
 
-#: kallithea/controllers/files.py:390 kallithea/controllers/files.py:458
+#: kallithea/controllers/files.py:388 kallithea/controllers/files.py:456
 #, python-format
 msgid "Successfully committed to %s"
 msgstr "Изменения применены в %s"
 
-#: kallithea/controllers/files.py:409
+#: kallithea/controllers/files.py:407
 msgid "Added file via Kallithea"
 msgstr "Файл добавлен с помощью Kallithea"
 
-#: kallithea/controllers/files.py:430
+#: kallithea/controllers/files.py:428
 msgid "No content"
 msgstr "Пусто"
 
-#: kallithea/controllers/files.py:434
+#: kallithea/controllers/files.py:432
 msgid "No filename"
 msgstr "Безымянный"
 
-#: kallithea/controllers/files.py:461
+#: kallithea/controllers/files.py:459
 msgid "Location must be relative path and must not contain .. in path"
 msgstr ""
 "Расположение должно быть относительным путем, и не должно содержать \".."
 "\" в пути"
 
-#: kallithea/controllers/files.py:493
+#: kallithea/controllers/files.py:491
 msgid "Downloads disabled"
 msgstr "Возможность скачивать отключена"
 
-#: kallithea/controllers/files.py:504
+#: kallithea/controllers/files.py:502
 #, python-format
 msgid "Unknown revision %s"
 msgstr "Неизвестная ревизия %s"
 
-#: kallithea/controllers/files.py:506
+#: kallithea/controllers/files.py:504
 msgid "Empty repository"
 msgstr "Пустой репозиторий"
 
-#: kallithea/controllers/files.py:508
+#: kallithea/controllers/files.py:506
 msgid "Unknown archive type"
 msgstr "Неизвестный тип архива"
 
-#: kallithea/controllers/files.py:729
+#: kallithea/controllers/files.py:726
 #: kallithea/templates/changeset/changeset_range.html:9
 #: kallithea/templates/email_templates/pull_request.html:64
 #: kallithea/templates/pullrequests/pullrequest.html:84
 msgid "Changesets"
 msgstr "Набор изменений"
 
-#: kallithea/controllers/files.py:730
-#: kallithea/controllers/pullrequests.py:182 kallithea/model/scm.py:676
+#: kallithea/controllers/files.py:727
+#: kallithea/controllers/pullrequests.py:174 kallithea/model/scm.py:663
 msgid "Branches"
 msgstr "Ветки"
 
-#: kallithea/controllers/files.py:731
-#: kallithea/controllers/pullrequests.py:183 kallithea/model/scm.py:687
+#: kallithea/controllers/files.py:728
+#: kallithea/controllers/pullrequests.py:175 kallithea/model/scm.py:674
 msgid "Tags"
 msgstr "Метки"
 
-#: kallithea/controllers/forks.py:174
+#: kallithea/controllers/forks.py:175
 #, python-format
 msgid "An error occurred during repository forking %s"
 msgstr "Произошла ошибка во время создания форка репозитория %s"
 
-#: kallithea/controllers/home.py:79
+#: kallithea/controllers/home.py:77
 msgid "Groups"
 msgstr "Группы"
 
-#: kallithea/controllers/home.py:89
+#: kallithea/controllers/home.py:87
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:90
 #: kallithea/templates/admin/repos/repo_add.html:12
 #: kallithea/templates/admin/repos/repo_add.html:16
@@ -282,48 +277,54 @@
 msgid "Repositories"
 msgstr "Репозитории"
 
-#: kallithea/controllers/home.py:122
+#: kallithea/controllers/home.py:119
 #: kallithea/templates/files/files_add.html:32
 #: kallithea/templates/files/files_delete.html:23
 #: kallithea/templates/files/files_edit.html:32
 msgid "Branch"
 msgstr "Ветка"
 
-#: kallithea/controllers/home.py:128
+#: kallithea/controllers/home.py:125
 msgid "Closed Branches"
 msgstr "Закрытые ветки"
 
-#: kallithea/controllers/home.py:134
+#: kallithea/controllers/home.py:131
 msgid "Tag"
 msgstr "Тэги"
 
-#: kallithea/controllers/home.py:140
+#: kallithea/controllers/home.py:137
 msgid "Bookmark"
 msgstr "Закладки"
 
-#: kallithea/controllers/journal.py:112 kallithea/controllers/journal.py:154
+#: kallithea/controllers/journal.py:146 kallithea/controllers/journal.py:157
 #: kallithea/templates/journal/public_journal.html:4
 #: kallithea/templates/journal/public_journal.html:18
 msgid "Public Journal"
 msgstr "Публичный журнал"
 
-#: kallithea/controllers/journal.py:116 kallithea/controllers/journal.py:158
+#: kallithea/controllers/journal.py:150 kallithea/controllers/journal.py:161
 #: kallithea/templates/base/base.html:290
 #: kallithea/templates/journal/journal.html:5
 #: kallithea/templates/journal/journal.html:13
 msgid "Journal"
 msgstr "Журнал"
 
-#: kallithea/controllers/login.py:140 kallithea/controllers/login.py:185
+#: kallithea/controllers/login.py:109
+#, fuzzy
+#| msgid "Authentication"
+msgid "Authentication failed."
+msgstr "Аутентификация"
+
+#: kallithea/controllers/login.py:142 kallithea/controllers/login.py:187
 msgid "Bad captcha"
 msgstr "Неверная капча"
 
-#: kallithea/controllers/login.py:146
+#: kallithea/controllers/login.py:148
 #, python-format
 msgid "You have successfully registered with %s"
 msgstr "Регистрация в %s прошла успешно"
 
-#: kallithea/controllers/login.py:190
+#: kallithea/controllers/login.py:192
 msgid "A password reset confirmation code has been sent"
 msgstr "Код для сброса пароля отправлена"
 
@@ -336,236 +337,227 @@
 msgid "Successfully updated password"
 msgstr "Пароль обновлён"
 
-#: kallithea/controllers/pullrequests.py:68
+#: kallithea/controllers/pullrequests.py:67
 #, python-format
 msgid "Invalid reviewer \"%s\" specified"
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:131
+msgstr "Некорректно задан ревьювер «%s»"
+
+#: kallithea/controllers/pullrequests.py:123
 #, python-format
 msgid "%s (closed)"
 msgstr "%s (закрыта)"
 
-#: kallithea/controllers/pullrequests.py:158
+#: kallithea/controllers/pullrequests.py:150
 #: kallithea/templates/changeset/changeset.html:12
 msgid "Changeset"
 msgstr "Изменения"
 
-#: kallithea/controllers/pullrequests.py:179
+#: kallithea/controllers/pullrequests.py:171
 msgid "Special"
 msgstr "Специальный"
 
-#: kallithea/controllers/pullrequests.py:180
+#: kallithea/controllers/pullrequests.py:172
 msgid "Peer branches"
-msgstr "Ветки участника"
-
-#: kallithea/controllers/pullrequests.py:181 kallithea/model/scm.py:682
+msgstr "Ветви участника"
+
+#: kallithea/controllers/pullrequests.py:173 kallithea/model/scm.py:669
 msgid "Bookmarks"
 msgstr "Закладки"
 
-#: kallithea/controllers/pullrequests.py:318
+#: kallithea/controllers/pullrequests.py:315
 #, python-format
 msgid "Error creating pull request: %s"
 msgstr "Ошибка при создании pull-запроса: %s"
 
-#: kallithea/controllers/pullrequests.py:345
-#: kallithea/controllers/pullrequests.py:368
+#: kallithea/controllers/pullrequests.py:342
+#: kallithea/controllers/pullrequests.py:365
 msgid "Error occurred while creating pull request"
 msgstr "Произошла ошибка при создании pull-запроса"
 
-#: kallithea/controllers/pullrequests.py:350
+#: kallithea/controllers/pullrequests.py:347
 msgid "Successfully opened new pull request"
-msgstr "Pull-запрос создан успешно"
-
-#: kallithea/controllers/pullrequests.py:373
-#, fuzzy
-#| msgid "Pull request update created"
+msgstr "Pull-запрос успешно открыт"
+
+#: kallithea/controllers/pullrequests.py:370
 msgid "New pull request iteration created"
-msgstr "Обновление для pull-запроса создано"
-
-#: kallithea/controllers/pullrequests.py:401
+msgstr "Создана новая итерация pull-запросов"
+
+#: kallithea/controllers/pullrequests.py:398
 #, python-format
 msgid "Meanwhile, the following reviewers have been added: %s"
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:405
+msgstr "В то же время, добавлены следующие ревьюверы: %s"
+
+#: kallithea/controllers/pullrequests.py:402
 #, python-format
 msgid "Meanwhile, the following reviewers have been removed: %s"
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:421
-#: kallithea/model/pull_request.py:232
+msgstr "В то же время, удалены следующие ревьюверы: %s"
+
+#: kallithea/controllers/pullrequests.py:418
+#: kallithea/model/pull_request.py:230
 msgid "No description"
 msgstr "Нет описания"
 
-#: kallithea/controllers/pullrequests.py:430
+#: kallithea/controllers/pullrequests.py:427
 msgid "Pull request updated"
 msgstr "Pull-запрос обновлён"
 
-#: kallithea/controllers/pullrequests.py:443
+#: kallithea/controllers/pullrequests.py:440
 msgid "Successfully deleted pull request"
 msgstr "Pull-запрос успешно удалён"
 
-#: kallithea/controllers/pullrequests.py:479
-#, fuzzy, python-format
-#| msgid "Changeset for %s %s not found in %s"
+#: kallithea/controllers/pullrequests.py:476
+#, python-format
 msgid "Revision %s not found in %s"
-msgstr "Набор изменений не найден"
-
-#: kallithea/controllers/pullrequests.py:506
-#, fuzzy, python-format
-#| msgid "No changesets found for updating this pull request."
+msgstr "Ревизия %s не найдена в %s"
+
+#: kallithea/controllers/pullrequests.py:504
+#, python-format
 msgid "Error: changesets not found when displaying pull request from %s."
-msgstr "Нет изменений для обновления этого pull-запроса."
-
-#: kallithea/controllers/pullrequests.py:520
+msgstr "Ошибка: не найдены изменения при отображении pull-запроса от %s."
+
+#: kallithea/controllers/pullrequests.py:518
 #, python-format
 msgid "This pull request has already been merged to %s."
 msgstr "Этот pull-запрос уже принят на ветку %s."
 
-#: kallithea/controllers/pullrequests.py:522
+#: kallithea/controllers/pullrequests.py:520
 msgid "This pull request has been closed and can not be updated."
 msgstr "Этот pull-запрос был закрыт и не может быть обновлён."
 
-#: kallithea/controllers/pullrequests.py:546
+#: kallithea/controllers/pullrequests.py:539
 #, python-format
 msgid "The following additional changes are available on %s:"
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:548
-#: kallithea/controllers/pullrequests.py:552
-#, fuzzy
-#| msgid "No changesets found for updating this pull request."
+msgstr "Следующие дополнительные изменения доступны на %s:"
+
+#: kallithea/controllers/pullrequests.py:541
+#: kallithea/controllers/pullrequests.py:545
 msgid "No additional changesets found for iterating on this pull request."
-msgstr "Нет изменений для обновления этого pull-запроса."
-
-#: kallithea/controllers/pullrequests.py:560
+msgstr "Нет дополнительных изменений для итерации в этом pull-запросе."
+
+#: kallithea/controllers/pullrequests.py:553
 #, python-format
 msgid "Note: Branch %s has another head: %s."
 msgstr "Внимание: Ветка %s имеет ещё одну верхушку: %s."
 
-#: kallithea/controllers/pullrequests.py:567
-#, fuzzy
-#| msgid "Git pull requests don't support updates yet."
+#: kallithea/controllers/pullrequests.py:560
 msgid "Git pull requests don't support iterating yet."
-msgstr "Обновление pull-запросы git не поддерживается."
-
-#: kallithea/controllers/pullrequests.py:569
-#, fuzzy, python-format
-#| msgid "No changesets found for updating this pull request."
+msgstr "Pull-запросы git пока не поддерживают итерации."
+
+#: kallithea/controllers/pullrequests.py:562
+#, python-format
 msgid ""
 "Error: some changesets not found when displaying pull request from %s."
-msgstr "Нет изменений для обновления этого pull-запроса."
-
-#: kallithea/controllers/pullrequests.py:593
+msgstr ""
+"Ошибка: не найдены некоторые изменения при отображении pull-запроса от %s."
+
+#: kallithea/controllers/pullrequests.py:586
 msgid "The diff can't be shown - the PR revisions could not be found."
-msgstr ""
+msgstr "Невозможно отобразить различия — не найдены ревизии PR."
+
+#: kallithea/controllers/search.py:132
+msgid "Invalid search query. Try quoting it."
+msgstr "Недопустимый поисковый запрос. Попробуйте заключить его в кавычки."
 
 #: kallithea/controllers/search.py:136
-msgid "Invalid search query. Try quoting it."
-msgstr "Недопустимый поисковый запрос. Попробуйте заключить его в кавычки."
-
-#: kallithea/controllers/search.py:140
 msgid "The server has no search index."
-msgstr ""
-
-#: kallithea/controllers/search.py:143
+msgstr "На сервере отсутствует поисковый индекс."
+
+#: kallithea/controllers/search.py:139
 msgid "An error occurred during search operation."
 msgstr "Произошла ошибка при выполнении этого поиска."
 
-#: kallithea/controllers/summary.py:168
-#: kallithea/templates/summary/summary.html:412
+#: kallithea/controllers/summary.py:169
+#: kallithea/templates/summary/summary.html:410
 msgid "No data ready yet"
 msgstr "Нет данных"
 
-#: kallithea/controllers/summary.py:171
+#: kallithea/controllers/summary.py:172
 #: kallithea/templates/summary/summary.html:97
 msgid "Statistics are disabled for this repository"
 msgstr "Статистические данные отключены для этого репозитария"
 
-#: kallithea/controllers/admin/auth_settings.py:137
+#: kallithea/controllers/admin/auth_settings.py:136
 msgid "Auth settings updated successfully"
 msgstr "Настройки авторизации успешно обновлены"
 
-#: kallithea/controllers/admin/auth_settings.py:148
+#: kallithea/controllers/admin/auth_settings.py:147
 msgid "error occurred during update of auth settings"
 msgstr "произошла ошибка при обновлении настроек авторизации"
 
-#: kallithea/controllers/admin/defaults.py:75
+#: kallithea/controllers/admin/defaults.py:74
 msgid "Default settings updated successfully"
-msgstr "Стандартные настройки успешно обновлены"
-
-#: kallithea/controllers/admin/defaults.py:90
+msgstr "Настройки по умолчанию успешно обновлены"
+
+#: kallithea/controllers/admin/defaults.py:89
 msgid "Error occurred during update of defaults"
 msgstr "Произошла ошибка при обновлении стандартных настроек"
 
 #: kallithea/controllers/admin/gists.py:59
 #: kallithea/controllers/admin/my_account.py:232
-#: kallithea/controllers/admin/users.py:248
-#, fuzzy
+#: kallithea/controllers/admin/users.py:246
 msgid "Forever"
-msgstr "навсегда"
+msgstr "Не ограничено"
 
 #: kallithea/controllers/admin/gists.py:60
 #: kallithea/controllers/admin/my_account.py:233
-#: kallithea/controllers/admin/users.py:249
+#: kallithea/controllers/admin/users.py:247
 msgid "5 minutes"
 msgstr "5 минут"
 
 #: kallithea/controllers/admin/gists.py:61
 #: kallithea/controllers/admin/my_account.py:234
-#: kallithea/controllers/admin/users.py:250
+#: kallithea/controllers/admin/users.py:248
 msgid "1 hour"
 msgstr "1 час"
 
 #: kallithea/controllers/admin/gists.py:62
 #: kallithea/controllers/admin/my_account.py:235
-#: kallithea/controllers/admin/users.py:251
+#: kallithea/controllers/admin/users.py:249
 msgid "1 day"
 msgstr "1 день"
 
 #: kallithea/controllers/admin/gists.py:63
 #: kallithea/controllers/admin/my_account.py:236
-#: kallithea/controllers/admin/users.py:252
+#: kallithea/controllers/admin/users.py:250
 msgid "1 month"
 msgstr "1 месяц"
 
 #: kallithea/controllers/admin/gists.py:67
 #: kallithea/controllers/admin/my_account.py:238
-#: kallithea/controllers/admin/users.py:254
+#: kallithea/controllers/admin/users.py:252
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:65
 #: kallithea/templates/admin/users/user_edit_api_keys.html:65
 msgid "Lifetime"
 msgstr "Срок"
 
-#: kallithea/controllers/admin/gists.py:142
+#: kallithea/controllers/admin/gists.py:148
 msgid "Error occurred during gist creation"
 msgstr "Произошла ошибка во время создания gist-записи"
 
-#: kallithea/controllers/admin/gists.py:158
+#: kallithea/controllers/admin/gists.py:164
 #, python-format
 msgid "Deleted gist %s"
 msgstr "Gist-запись %s удалена"
 
-#: kallithea/controllers/admin/gists.py:198
+#: kallithea/controllers/admin/gists.py:207
 msgid "Unmodified"
 msgstr "Неизменный"
 
-#: kallithea/controllers/admin/gists.py:228
+#: kallithea/controllers/admin/gists.py:237
 msgid "Successfully updated gist content"
-msgstr ""
-
-#: kallithea/controllers/admin/gists.py:233
+msgstr "Содержимое gist-записи обновлено"
+
+#: kallithea/controllers/admin/gists.py:242
 msgid "Successfully updated gist data"
 msgstr "Данные gist-записи обновлены"
 
-#: kallithea/controllers/admin/gists.py:236
+#: kallithea/controllers/admin/gists.py:245
 #, python-format
 msgid "Error occurred during update of gist %s"
 msgstr "Произошла ошибка при обновлении gist-записи %s"
 
-#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:211
-#: kallithea/model/user.py:232
+#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:205
+#: kallithea/model/user.py:226
 msgid "You can't edit this user since it's crucial for entire application"
 msgstr ""
 "Вы не можете изменить данные этого пользователя, поскольку он важен для "
@@ -576,7 +568,7 @@
 msgstr "Ваша учетная запись успешно обновлена"
 
 #: kallithea/controllers/admin/my_account.py:134
-#: kallithea/controllers/admin/users.py:181
+#: kallithea/controllers/admin/users.py:179
 #, python-format
 msgid "Error occurred during update of user %s"
 msgstr "Произошла ошибка при обновлении пользователя %s"
@@ -586,49 +578,46 @@
 msgstr "Ошибка при обновлении пароля"
 
 #: kallithea/controllers/admin/my_account.py:209
-#: kallithea/controllers/admin/users.py:367
+#: kallithea/controllers/admin/users.py:365
 #, python-format
 msgid "Added email %s to user"
 msgstr "Пользователю добавлен e-mail %s"
 
 #: kallithea/controllers/admin/my_account.py:215
-#: kallithea/controllers/admin/users.py:373
+#: kallithea/controllers/admin/users.py:371
 msgid "An error occurred during email saving"
 msgstr "Произошла ошибка при сохранении e-mail"
 
 #: kallithea/controllers/admin/my_account.py:224
-#: kallithea/controllers/admin/users.py:383
+#: kallithea/controllers/admin/users.py:381
 msgid "Removed email from user"
 msgstr "E-mail пользователя удалён"
 
 #: kallithea/controllers/admin/my_account.py:248
-#: kallithea/controllers/admin/users.py:271
+#: kallithea/controllers/admin/users.py:269
 msgid "API key successfully created"
 msgstr "API-ключ успешно создан"
 
 #: kallithea/controllers/admin/my_account.py:257
-#: kallithea/controllers/admin/users.py:281
+#: kallithea/controllers/admin/users.py:279
 msgid "API key successfully reset"
 msgstr "API-ключ успешно сброшен"
 
 #: kallithea/controllers/admin/my_account.py:261
-#: kallithea/controllers/admin/users.py:285
+#: kallithea/controllers/admin/users.py:283
 msgid "API key successfully deleted"
 msgstr "API-ключ успешно удалён"
 
 #: kallithea/controllers/admin/my_account.py:281
-#: kallithea/controllers/admin/users.py:456
-#, fuzzy, python-format
-#| msgid "API key successfully created"
+#: kallithea/controllers/admin/users.py:454
+#, python-format
 msgid "SSH key %s successfully added"
-msgstr "API-ключ успешно создан"
+msgstr "Ключ SSH %s успешно добавлен"
 
 #: kallithea/controllers/admin/my_account.py:293
-#: kallithea/controllers/admin/users.py:470
-#, fuzzy
-#| msgid "API key successfully deleted"
+#: kallithea/controllers/admin/users.py:468
 msgid "SSH key successfully deleted"
-msgstr "API-ключ успешно удалён"
+msgstr "Ключ SSH успешно удалён"
 
 #: kallithea/controllers/admin/permissions.py:65
 #: kallithea/controllers/admin/permissions.py:69
@@ -702,11 +691,11 @@
 msgid "Allowed with automatic account activation"
 msgstr "Разрешена, с автоматической активацией учётной записи"
 
-#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1673
+#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1578
 msgid "Manual activation of external account"
 msgstr "Ручная активация внешней учетной записи"
 
-#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1674
+#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1579
 msgid "Automatic activation of external account"
 msgstr "Автоматическая активация внешней учетной записи"
 
@@ -728,186 +717,177 @@
 msgid "Error occurred during update of permissions"
 msgstr "Произошла ошибка во время обновления привилегий"
 
-#: kallithea/controllers/admin/repo_groups.py:172
+#: kallithea/controllers/admin/repo_groups.py:165
 #, python-format
 msgid "Error occurred during creation of repository group %s"
 msgstr "Произошла ошибка при создании группы репозиториев %s"
 
-#: kallithea/controllers/admin/repo_groups.py:177
+#: kallithea/controllers/admin/repo_groups.py:172
 #, python-format
 msgid "Created repository group %s"
 msgstr "Создана новая группа репозиториев %s"
 
-#: kallithea/controllers/admin/repo_groups.py:224
+#: kallithea/controllers/admin/repo_groups.py:219
 #, python-format
 msgid "Updated repository group %s"
 msgstr "Группа репозиториев %s обновлена"
 
-#: kallithea/controllers/admin/repo_groups.py:240
+#: kallithea/controllers/admin/repo_groups.py:235
 #, python-format
 msgid "Error occurred during update of repository group %s"
 msgstr "Произошла ошибка при обновлении группы репозиториев %s"
 
-#: kallithea/controllers/admin/repo_groups.py:250
+#: kallithea/controllers/admin/repo_groups.py:245
 #, python-format
 msgid "This group contains %s repositories and cannot be deleted"
 msgstr "Данная группа содержит %s репозитариев и не может быть удалена"
 
-#: kallithea/controllers/admin/repo_groups.py:257
+#: kallithea/controllers/admin/repo_groups.py:252
 #, python-format
 msgid "This group contains %s subgroups and cannot be deleted"
 msgstr "Группа содержит в себе %s подгрупп и не может быть удалён"
 
-#: kallithea/controllers/admin/repo_groups.py:263
+#: kallithea/controllers/admin/repo_groups.py:258
 #, python-format
 msgid "Removed repository group %s"
 msgstr "Группа репозиториев %s удалена"
 
-#: kallithea/controllers/admin/repo_groups.py:268
+#: kallithea/controllers/admin/repo_groups.py:263
 #, python-format
 msgid "Error occurred during deletion of repository group %s"
 msgstr "Произошла ошибка при удалении группы репозиториев %s"
 
-#: kallithea/controllers/admin/repo_groups.py:352
-#: kallithea/controllers/admin/repo_groups.py:382
-#: kallithea/controllers/admin/user_groups.py:294
+#: kallithea/controllers/admin/repo_groups.py:347
+#: kallithea/controllers/admin/repo_groups.py:377
+#: kallithea/controllers/admin/user_groups.py:290
 msgid "Cannot revoke permission for yourself as admin"
 msgstr "Администратор не может отозвать свои привелегии"
 
-#: kallithea/controllers/admin/repo_groups.py:367
+#: kallithea/controllers/admin/repo_groups.py:362
 msgid "Repository group permissions updated"
 msgstr "Привилегии группы репозиториев обновлены"
 
-#: kallithea/controllers/admin/repo_groups.py:399
-#: kallithea/controllers/admin/repos.py:358
-#: kallithea/controllers/admin/user_groups.py:306
+#: kallithea/controllers/admin/repo_groups.py:394
+#: kallithea/controllers/admin/repos.py:357
+#: kallithea/controllers/admin/user_groups.py:302
 msgid "An error occurred during revoking of permission"
 msgstr "Произошла ошибка при отзыве привелегии"
 
-#: kallithea/controllers/admin/repos.py:136
+#: kallithea/controllers/admin/repos.py:137
 #, python-format
 msgid "Error creating repository %s"
 msgstr "Произошла ошибка при создании репозитория %s"
 
-#: kallithea/controllers/admin/repos.py:194
+#: kallithea/controllers/admin/repos.py:193
 #, python-format
 msgid "Created repository %s from %s"
 msgstr "Репозиторий %s создан из %s"
 
-#: kallithea/controllers/admin/repos.py:203
+#: kallithea/controllers/admin/repos.py:202
 #, python-format
 msgid "Forked repository %s as %s"
-msgstr "Сделан форк(копия) репозитория %s на %s"
-
-#: kallithea/controllers/admin/repos.py:206
+msgstr "Создан форк репозитория %s с именем %s"
+
+#: kallithea/controllers/admin/repos.py:205
 #, python-format
 msgid "Created repository %s"
 msgstr "Репозиторий %s создан"
 
-#: kallithea/controllers/admin/repos.py:235
+#: kallithea/controllers/admin/repos.py:234
 #, python-format
 msgid "Repository %s updated successfully"
 msgstr "Репозитарий %s успешно обновлён"
 
-#: kallithea/controllers/admin/repos.py:255
+#: kallithea/controllers/admin/repos.py:254
 #, python-format
 msgid "Error occurred during update of repository %s"
 msgstr "Произошла ошибка во время обновления репозитория %s"
 
-#: kallithea/controllers/admin/repos.py:273
+#: kallithea/controllers/admin/repos.py:272
 #, python-format
 msgid "Detached %s forks"
 msgstr "Форки %s отсоединены"
 
-#: kallithea/controllers/admin/repos.py:276
+#: kallithea/controllers/admin/repos.py:275
 #, python-format
 msgid "Deleted %s forks"
 msgstr "Удалены форки репозитория %s"
 
-#: kallithea/controllers/admin/repos.py:281
+#: kallithea/controllers/admin/repos.py:280
 #, python-format
 msgid "Deleted repository %s"
 msgstr "Репозиторий %s удалён"
 
-#: kallithea/controllers/admin/repos.py:284
+#: kallithea/controllers/admin/repos.py:283
 #, python-format
 msgid "Cannot delete repository %s which still has forks"
-msgstr "Невозможно удалить %s, у него всё ещё есть форки"
-
-#: kallithea/controllers/admin/repos.py:289
+msgstr "Невозможно удалить репозиторий %s, поскольку существуют его форки"
+
+#: kallithea/controllers/admin/repos.py:288
 #, python-format
 msgid "An error occurred during deletion of %s"
 msgstr "Произошла ошибка во время удаления %s"
 
-#: kallithea/controllers/admin/repos.py:329
+#: kallithea/controllers/admin/repos.py:328
 msgid "Repository permissions updated"
 msgstr "Привилегии репозитория обновлены"
 
-#: kallithea/controllers/admin/repos.py:388
+#: kallithea/controllers/admin/repos.py:387
 #, python-format
 msgid "Field validation error: %s"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:391
-#, fuzzy, python-format
-#| msgid "An error occurred during creation of field"
+msgstr "Ошибка валидации поля: %s"
+
+#: kallithea/controllers/admin/repos.py:390
+#, python-format
 msgid "An error occurred during creation of field: %r"
-msgstr "Произошла ошибка при создании поля"
-
-#: kallithea/controllers/admin/repos.py:402
+msgstr "Произошла ошибка при создании поля: %r"
+
+#: kallithea/controllers/admin/repos.py:401
 msgid "An error occurred during removal of field"
 msgstr "Произошла ошибка при удалении поля"
 
-#: kallithea/controllers/admin/repos.py:416
+#: kallithea/controllers/admin/repos.py:415
 msgid "-- Not a fork --"
-msgstr "-- Не форк --"
-
-#: kallithea/controllers/admin/repos.py:448
+msgstr "-- Не является форком --"
+
+#: kallithea/controllers/admin/repos.py:447
 msgid "Updated repository visibility in public journal"
 msgstr "Видимость репозитория в публичном журнале обновлена"
 
-#: kallithea/controllers/admin/repos.py:452
+#: kallithea/controllers/admin/repos.py:451
 msgid "An error occurred during setting this repository in public journal"
 msgstr "Произошла ошибка при установке репозитария в общедоступный журнал"
 
-#: kallithea/controllers/admin/repos.py:468
+#: kallithea/controllers/admin/repos.py:467
 msgid "Nothing"
-msgstr "Ничего"
-
-#: kallithea/controllers/admin/repos.py:470
+msgstr "Отсутствуют"
+
+#: kallithea/controllers/admin/repos.py:469
 #, python-format
 msgid "Marked repository %s as fork of %s"
-msgstr "Репозиторий %s отмечен как форк %s"
-
-#: kallithea/controllers/admin/repos.py:477
+msgstr "Репозиторий %s отмечен как форк от %s"
+
+#: kallithea/controllers/admin/repos.py:476
 msgid "An error occurred during this operation"
 msgstr "Произошла ошибка при выполнении операции"
 
-#: kallithea/controllers/admin/repos.py:490
-msgid "Cache invalidation successful"
-msgstr "Кэш сброшен"
-
-#: kallithea/controllers/admin/repos.py:494
-msgid "An error occurred during cache invalidation"
-msgstr "Произошла ошибка при очистке кэша"
-
-#: kallithea/controllers/admin/repos.py:507
+#: kallithea/controllers/admin/repos.py:488
 msgid "Pulled from remote location"
 msgstr "Внесены изменения из удалённого репозитория"
 
-#: kallithea/controllers/admin/repos.py:510
+#: kallithea/controllers/admin/repos.py:491
 msgid "An error occurred during pull from remote location"
 msgstr "Произошла ошибка при внесении изменений из удалённого репозитория"
 
-#: kallithea/controllers/admin/repos.py:541
+#: kallithea/controllers/admin/repos.py:522
 msgid "An error occurred during deletion of repository stats"
 msgstr "Произошла ошибка при удалении статистики репозитория"
 
-#: kallithea/controllers/admin/settings.py:131
+#: kallithea/controllers/admin/settings.py:132
 msgid "Updated VCS settings"
 msgstr "Обновлены настройки VCS"
 
-#: kallithea/controllers/admin/settings.py:135 kallithea/lib/utils.py:237
+#: kallithea/controllers/admin/settings.py:136
 msgid ""
 "Unable to activate hgsubversion support. The \"hgsubversion\" library is "
 "missing"
@@ -915,181 +895,179 @@
 "Невозможно включить поддержку hgsubversion. Библиотека «hgsubversion» "
 "отсутствует"
 
-#: kallithea/controllers/admin/settings.py:141
-#: kallithea/controllers/admin/settings.py:233
+#: kallithea/controllers/admin/settings.py:142
+#: kallithea/controllers/admin/settings.py:234
 msgid "Error occurred while updating application settings"
 msgstr "Произошла ошибка при обновлении настроек приложения"
 
-#: kallithea/controllers/admin/settings.py:176
+#: kallithea/controllers/admin/settings.py:177
 #, python-format
 msgid "Repositories successfully rescanned. Added: %s. Removed: %s."
 msgstr "Репозитории успешно пересканированы, добавлено: %s, удалено: %s."
 
-#: kallithea/controllers/admin/settings.py:188
-#, fuzzy, python-format
+#: kallithea/controllers/admin/settings.py:189
+#, python-format
 msgid "Invalidated %s repositories"
-msgstr "Сбросить кэш для всех репозиториев"
-
-#: kallithea/controllers/admin/settings.py:229
+msgstr "Сброшена валидация для %s репозиториев"
+
+#: kallithea/controllers/admin/settings.py:230
 msgid "Updated application settings"
 msgstr "Обновленные параметры настройки приложения"
 
-#: kallithea/controllers/admin/settings.py:283
+#: kallithea/controllers/admin/settings.py:284
 msgid "Updated visualisation settings"
 msgstr "Настройки визуализации обновлены"
 
-#: kallithea/controllers/admin/settings.py:288
+#: kallithea/controllers/admin/settings.py:289
 msgid "Error occurred during updating visualisation settings"
 msgstr "Произошла ошибка при обновлении настроек визуализации"
 
-#: kallithea/controllers/admin/settings.py:312
+#: kallithea/controllers/admin/settings.py:313
 msgid "Please enter email address"
 msgstr "Пожалуйста, введите адрес электронной почты"
 
-#: kallithea/controllers/admin/settings.py:327
+#: kallithea/controllers/admin/settings.py:328
 msgid "Send email task created"
 msgstr "Задача отправки Email создана"
 
-#: kallithea/controllers/admin/settings.py:355
-#, fuzzy
-#| msgid "No data ready yet"
+#: kallithea/controllers/admin/settings.py:356
 msgid "Hook already exists"
-msgstr "Нет данных"
-
-#: kallithea/controllers/admin/settings.py:357
+msgstr "Хук уже существует"
+
+#: kallithea/controllers/admin/settings.py:358
 msgid "Builtin hooks are read-only. Please use another hook name."
 msgstr ""
-
-#: kallithea/controllers/admin/settings.py:360
+"Встроенные хуки предназначены только для чтения. Пожалуйста, используйте "
+"другое имя."
+
+#: kallithea/controllers/admin/settings.py:361
 msgid "Added new hook"
 msgstr "Добавлена новая ловушка"
 
-#: kallithea/controllers/admin/settings.py:376
+#: kallithea/controllers/admin/settings.py:377
 msgid "Updated hooks"
 msgstr "Обновлённые ловушки"
 
-#: kallithea/controllers/admin/settings.py:380
+#: kallithea/controllers/admin/settings.py:381
 msgid "Error occurred during hook creation"
 msgstr "произошла ошибка при создании хука"
 
-#: kallithea/controllers/admin/settings.py:404
+#: kallithea/controllers/admin/settings.py:405
 msgid "Whoosh reindex task scheduled"
-msgstr "Запланирована переиндексация базы Whoosh"
-
-#: kallithea/controllers/admin/user_groups.py:138
+msgstr "Переиндексация базы Whoosh успешно запланирована"
+
+#: kallithea/controllers/admin/user_groups.py:134
 #, python-format
 msgid "Created user group %s"
 msgstr "Создана группа пользователей %s"
 
-#: kallithea/controllers/admin/user_groups.py:151
+#: kallithea/controllers/admin/user_groups.py:147
 #, python-format
 msgid "Error occurred during creation of user group %s"
 msgstr "Произошла ошибка при создании группы пользователей %s"
 
-#: kallithea/controllers/admin/user_groups.py:179
+#: kallithea/controllers/admin/user_groups.py:175
 #, python-format
 msgid "Updated user group %s"
 msgstr "Группа пользователей %s обновлена"
 
-#: kallithea/controllers/admin/user_groups.py:201
+#: kallithea/controllers/admin/user_groups.py:197
 #, python-format
 msgid "Error occurred during update of user group %s"
 msgstr "Произошла ошибка при обновлении группы пользователей %s"
 
-#: kallithea/controllers/admin/user_groups.py:212
+#: kallithea/controllers/admin/user_groups.py:208
 msgid "Successfully deleted user group"
 msgstr "Группа пользователей успешно удалена"
 
-#: kallithea/controllers/admin/user_groups.py:217
+#: kallithea/controllers/admin/user_groups.py:213
 msgid "An error occurred during deletion of user group"
 msgstr "Произошла ошибка при удалении группы пользователей"
 
-#: kallithea/controllers/admin/user_groups.py:273
+#: kallithea/controllers/admin/user_groups.py:269
 msgid "Target group cannot be the same"
 msgstr "Целевая группа не может быть такой же"
 
-#: kallithea/controllers/admin/user_groups.py:279
+#: kallithea/controllers/admin/user_groups.py:275
 msgid "User group permissions updated"
 msgstr "Привилегии группы пользователей обновлены"
 
-#: kallithea/controllers/admin/user_groups.py:388
-#: kallithea/controllers/admin/users.py:338
+#: kallithea/controllers/admin/user_groups.py:384
+#: kallithea/controllers/admin/users.py:336
 msgid "Updated permissions"
 msgstr "Обновлены привилегии"
 
-#: kallithea/controllers/admin/user_groups.py:392
-#: kallithea/controllers/admin/users.py:342
+#: kallithea/controllers/admin/user_groups.py:388
+#: kallithea/controllers/admin/users.py:340
 msgid "An error occurred during permissions saving"
 msgstr "Произошла ошибка при сохранении привилегий"
 
-#: kallithea/controllers/admin/users.py:123
+#: kallithea/controllers/admin/users.py:121
 #, python-format
 msgid "Created user %s"
 msgstr "Пользователь %s создан"
 
-#: kallithea/controllers/admin/users.py:138
+#: kallithea/controllers/admin/users.py:136
 #, python-format
 msgid "Error occurred during creation of user %s"
 msgstr "Произошла ошибка при создании пользователя %s"
 
-#: kallithea/controllers/admin/users.py:162
+#: kallithea/controllers/admin/users.py:160
 msgid "User updated successfully"
 msgstr "Пользователь успешно обновлён"
 
-#: kallithea/controllers/admin/users.py:190
+#: kallithea/controllers/admin/users.py:188
 msgid "Successfully deleted user"
 msgstr "Пользователь успешно удалён"
 
-#: kallithea/controllers/admin/users.py:195
+#: kallithea/controllers/admin/users.py:193
 msgid "An error occurred during deletion of user"
 msgstr "Произошла ошибка при удалении пользователя"
 
-#: kallithea/controllers/admin/users.py:203
+#: kallithea/controllers/admin/users.py:201
 msgid "The default user cannot be edited"
-msgstr ""
-
-#: kallithea/controllers/admin/users.py:409
+msgstr "Нельзя редактировать пользователя по умолчанию"
+
+#: kallithea/controllers/admin/users.py:407
 #, python-format
 msgid "Added IP address %s to user whitelist"
 msgstr "Добавлен IP %s в белый список пользователя"
 
-#: kallithea/controllers/admin/users.py:415
+#: kallithea/controllers/admin/users.py:413
 msgid "An error occurred while adding IP address"
 msgstr "Произошла ошибка при сохранении IP"
 
-#: kallithea/controllers/admin/users.py:427
+#: kallithea/controllers/admin/users.py:425
 msgid "Removed IP address from user whitelist"
 msgstr "Удален IP %s из белого списка пользователя"
 
-#: kallithea/lib/auth.py:684
+#: kallithea/lib/auth.py:634
 msgid "You need to be a registered user to perform this action"
 msgstr ""
 "Вы должны быть зарегистрированным пользователем, чтобы выполнить это "
 "действие"
 
-#: kallithea/lib/auth.py:712
+#: kallithea/lib/auth.py:662
 msgid "You need to be signed in to view this page"
 msgstr "Страница доступна только авторизованным пользователям"
 
 #: kallithea/lib/base.py:483
 msgid ""
 "CSRF token leak has been detected - all form tokens have been expired"
-msgstr ""
+msgstr "Обнаружена утечка CSRF-токена — истёк срок действия токенов форм"
 
 #: kallithea/lib/base.py:580
 msgid "Repository not found in the filesystem"
 msgstr "Репозиторий не найден на файловой системе"
 
 #: kallithea/lib/base.py:605
-#, fuzzy, python-format
+#, python-format
 msgid "Changeset for %s %s not found in %s"
-msgstr "Набор изменений не найден"
+msgstr "Набор изменений для %s %s не найден в %s"
 
 #: kallithea/lib/base.py:647
-#, fuzzy
-#| msgid "Your account is disabled"
 msgid "SSH access is disabled."
-msgstr "Ваш аккаунт выключен"
+msgstr "Доступ по SSH отключен."
 
 #: kallithea/lib/diffs.py:194
 msgid "Binary file"
@@ -1102,171 +1080,170 @@
 "Набор изменения оказался слишком большими и был урезан, используйте меню "
 "сравнения для показа результата сравнения"
 
-#: kallithea/lib/diffs.py:224
+#: kallithea/lib/diffs.py:223
 msgid "No changes detected"
 msgstr "Изменений не обнаружено"
 
-#: kallithea/lib/helpers.py:653
+#: kallithea/lib/helpers.py:670
 #, python-format
 msgid "Deleted branch: %s"
 msgstr "Удалена ветка: %s"
 
-#: kallithea/lib/helpers.py:655
+#: kallithea/lib/helpers.py:672
 #, python-format
 msgid "Created tag: %s"
 msgstr "Создан тег: %s"
 
-#: kallithea/lib/helpers.py:666
-#, fuzzy, python-format
+#: kallithea/lib/helpers.py:683
+#, python-format
 msgid "Changeset %s not found"
-msgstr "Набор изменений не найден"
-
-#: kallithea/lib/helpers.py:715
+msgstr "Набор изменений %s не найден"
+
+#: kallithea/lib/helpers.py:732
 #, python-format
 msgid "Show all combined changesets %s->%s"
 msgstr "Показать отличия вместе %s->%s"
 
-#: kallithea/lib/helpers.py:721
-#, fuzzy
+#: kallithea/lib/helpers.py:738
 msgid "Compare view"
-msgstr "сравнение"
-
-#: kallithea/lib/helpers.py:740
+msgstr "Сравнить вид"
+
+#: kallithea/lib/helpers.py:757
 msgid "and"
 msgstr "и"
 
-#: kallithea/lib/helpers.py:741
+#: kallithea/lib/helpers.py:758
 #, python-format
 msgid "%s more"
 msgstr "на %s больше"
 
-#: kallithea/lib/helpers.py:742
+#: kallithea/lib/helpers.py:759
 #: kallithea/templates/changelog/changelog.html:43
 msgid "revisions"
 msgstr "версии"
 
-#: kallithea/lib/helpers.py:766
-#, fuzzy, python-format
+#: kallithea/lib/helpers.py:783
+#, python-format
 msgid "Fork name %s"
-msgstr "имя форка %s"
-
-#: kallithea/lib/helpers.py:787
+msgstr "Имя форка %s"
+
+#: kallithea/lib/helpers.py:804
 #, python-format
 msgid "Pull request %s"
 msgstr "Pull-запрос %s"
 
-#: kallithea/lib/helpers.py:797
+#: kallithea/lib/helpers.py:814
 msgid "[deleted] repository"
 msgstr "[удален] репозиторий"
 
-#: kallithea/lib/helpers.py:799 kallithea/lib/helpers.py:811
+#: kallithea/lib/helpers.py:816 kallithea/lib/helpers.py:828
 msgid "[created] repository"
 msgstr "[создан] репозиторий"
 
-#: kallithea/lib/helpers.py:801
+#: kallithea/lib/helpers.py:818
 msgid "[created] repository as fork"
-msgstr "[создан] репозиторий как форк"
-
-#: kallithea/lib/helpers.py:803 kallithea/lib/helpers.py:813
+msgstr "[создан] репозиторий в качестве форка"
+
+#: kallithea/lib/helpers.py:820 kallithea/lib/helpers.py:830
 msgid "[forked] repository"
-msgstr "[форкнут] репозиторий"
-
-#: kallithea/lib/helpers.py:805 kallithea/lib/helpers.py:815
+msgstr "[создан форк] репозитория"
+
+#: kallithea/lib/helpers.py:822 kallithea/lib/helpers.py:832
 msgid "[updated] repository"
 msgstr "[обновлён] репозиторий"
 
-#: kallithea/lib/helpers.py:807
+#: kallithea/lib/helpers.py:824
 msgid "[downloaded] archive from repository"
 msgstr "[загружен] архив из репозитория"
 
-#: kallithea/lib/helpers.py:809
+#: kallithea/lib/helpers.py:826
 msgid "[delete] repository"
 msgstr "[удален] репозиторий"
 
-#: kallithea/lib/helpers.py:817
+#: kallithea/lib/helpers.py:834
 msgid "[created] user"
 msgstr "[создан] пользователь"
 
-#: kallithea/lib/helpers.py:819
+#: kallithea/lib/helpers.py:836
 msgid "[updated] user"
 msgstr "[обновлён] пользователь"
 
-#: kallithea/lib/helpers.py:821
+#: kallithea/lib/helpers.py:838
 msgid "[created] user group"
 msgstr "[создана] группа пользователей"
 
-#: kallithea/lib/helpers.py:823
+#: kallithea/lib/helpers.py:840
 msgid "[updated] user group"
 msgstr "[обновлена] группа пользователей"
 
-#: kallithea/lib/helpers.py:825
+#: kallithea/lib/helpers.py:842
 msgid "[commented] on revision in repository"
 msgstr "[комментарий] к ревизии в репозитории"
 
-#: kallithea/lib/helpers.py:827
+#: kallithea/lib/helpers.py:844
 msgid "[commented] on pull request for"
-msgstr "[прокомментировано] в запросе на внесение изменений для"
-
-#: kallithea/lib/helpers.py:829
+msgstr "[прокомментировано] в pull-запросе для"
+
+#: kallithea/lib/helpers.py:846
 msgid "[closed] pull request for"
-msgstr "[закрыт] Pull-запрос для"
-
-#: kallithea/lib/helpers.py:831
+msgstr "[закрыт] pull-запрос для"
+
+#: kallithea/lib/helpers.py:848
 msgid "[pushed] into"
 msgstr "[отправлено] в"
 
-#: kallithea/lib/helpers.py:833
+#: kallithea/lib/helpers.py:850
 msgid "[committed via Kallithea] into repository"
 msgstr "[внесены изменения с помощью Kallithea] в репозитории"
 
-#: kallithea/lib/helpers.py:835
+#: kallithea/lib/helpers.py:852
 msgid "[pulled from remote] into repository"
 msgstr "[внесены изменения из удалённого репозитория] в репозиторий"
 
-#: kallithea/lib/helpers.py:837
+#: kallithea/lib/helpers.py:854
 msgid "[pulled] from"
 msgstr "[внесены изменения] из"
 
-#: kallithea/lib/helpers.py:839
+#: kallithea/lib/helpers.py:856
 msgid "[started following] repository"
-msgstr "[добавлен в наблюдения] репозиторий"
-
-#: kallithea/lib/helpers.py:841
+msgstr "[подписка] на репозиторий"
+
+#: kallithea/lib/helpers.py:858
 msgid "[stopped following] repository"
-msgstr "[удалён из наблюдения] репозиторий"
-
-#: kallithea/lib/helpers.py:961
+msgstr "[отписка] от репозитория"
+
+#: kallithea/lib/helpers.py:975
 #, python-format
 msgid " and %s more"
 msgstr " и на %s больше"
 
-#: kallithea/lib/helpers.py:965
+#: kallithea/lib/helpers.py:979
 #: kallithea/templates/compare/compare_diff.html:69
 #: kallithea/templates/pullrequests/pullrequest_show.html:297
 msgid "No files"
 msgstr "Нет файлов"
 
-#: kallithea/lib/helpers.py:990
+#: kallithea/lib/helpers.py:1004
 msgid "new file"
 msgstr "новый файл"
 
-#: kallithea/lib/helpers.py:993
+#: kallithea/lib/helpers.py:1007
 msgid "mod"
 msgstr "изменён"
 
-#: kallithea/lib/helpers.py:996
+#: kallithea/lib/helpers.py:1010
 msgid "del"
 msgstr "удалён"
 
-#: kallithea/lib/helpers.py:999
+#: kallithea/lib/helpers.py:1013
 msgid "rename"
 msgstr "переименован"
 
-#: kallithea/lib/helpers.py:1004
+#: kallithea/lib/helpers.py:1018
 msgid "chmod"
 msgstr "chmod"
 
-#: kallithea/lib/helpers.py:1297
+#: kallithea/lib/helpers.py:1323
 #, python-format
 msgid ""
 "%s repository is not mapped to db perhaps it was created or renamed from "
@@ -1277,42 +1254,48 @@
 "переименован из файловой системы. Пожалуйста, перезапустите приложение "
 "для сканирования репозиториев"
 
-#: kallithea/lib/ssh.py:73
+#: kallithea/lib/ssh.py:75
 msgid "SSH key is missing"
+msgstr "Отсутствует ключ SSH"
+
+#: kallithea/lib/ssh.py:79
+msgid ""
+"Incorrect SSH key - it must have both a key type and a base64 part, like "
+"'ssh-rsa ASRNeaZu4FA...xlJp='"
 msgstr ""
-
-#: kallithea/lib/ssh.py:77
-msgid "Incorrect SSH key - it must have both a key type and a base64 part"
-msgstr ""
-
-#: kallithea/lib/ssh.py:81
+"Некорректный ключ SSH — должен присутствовать тип ключа и код base64, "
+"например 'ssh-rsa ASRNeaZu4FA...xlJp='"
+
+#: kallithea/lib/ssh.py:83
 msgid "Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'"
 msgstr ""
-
-#: kallithea/lib/ssh.py:84
+"Некорректный ключ SSH — он должен начинаться с 'ssh-(rsa|dss|ed25519)'"
+
+#: kallithea/lib/ssh.py:86
 #, python-format
 msgid "Incorrect SSH key - unexpected characters in base64 part %r"
 msgstr ""
-
-#: kallithea/lib/ssh.py:89
+"Некорректный ключ SSH — присутствуют некорректные символы в коде base64 %r"
+
+#: kallithea/lib/ssh.py:91
 #, python-format
 msgid "Incorrect SSH key - failed to decode base64 part %r"
-msgstr ""
-
-#: kallithea/lib/ssh.py:92
+msgstr "Некорректный ключ SSH — ошибка декодирования кода base64 %r"
+
+#: kallithea/lib/ssh.py:94
 #, python-format
 msgid "Incorrect SSH key - base64 part is not %r as claimed but %r"
-msgstr ""
-
-#: kallithea/lib/utils2.py:334
+msgstr "Некорректный ключ SSH — код base64 соответствует не %r, а %r"
+
+#: kallithea/lib/utils2.py:253
 #, python-format
 msgid "%d year"
 msgid_plural "%d years"
 msgstr[0] "%d год"
-msgstr[1] "%d лет"
-msgstr[2] "%d года"
-
-#: kallithea/lib/utils2.py:335
+msgstr[1] "%d года"
+msgstr[2] "%d лет"
+
+#: kallithea/lib/utils2.py:254
 #, python-format
 msgid "%d month"
 msgid_plural "%d months"
@@ -1320,7 +1303,7 @@
 msgstr[1] "%d месяца"
 msgstr[2] "%d месяцев"
 
-#: kallithea/lib/utils2.py:336
+#: kallithea/lib/utils2.py:255
 #, python-format
 msgid "%d day"
 msgid_plural "%d days"
@@ -1328,23 +1311,23 @@
 msgstr[1] "%d дня"
 msgstr[2] "%d дней"
 
-#: kallithea/lib/utils2.py:337
+#: kallithea/lib/utils2.py:256
 #, python-format
 msgid "%d hour"
 msgid_plural "%d hours"
 msgstr[0] "%d час"
-msgstr[1] "%d часов"
-msgstr[2] "%d часа"
-
-#: kallithea/lib/utils2.py:338
+msgstr[1] "%d часа"
+msgstr[2] "%d часов"
+
+#: kallithea/lib/utils2.py:257
 #, python-format
 msgid "%d minute"
 msgid_plural "%d minutes"
 msgstr[0] "%d минута"
-msgstr[1] "%d минут"
-msgstr[2] "%d минуты"
-
-#: kallithea/lib/utils2.py:339
+msgstr[1] "%d минуты"
+msgstr[2] "%d минут"
+
+#: kallithea/lib/utils2.py:258
 #, python-format
 msgid "%d second"
 msgid_plural "%d seconds"
@@ -1352,174 +1335,184 @@
 msgstr[1] "%d секунды"
 msgstr[2] "%d секунд"
 
-#: kallithea/lib/utils2.py:355
+#: kallithea/lib/utils2.py:274
 #, python-format
 msgid "in %s"
 msgstr "в %s"
 
-#: kallithea/lib/utils2.py:357
+#: kallithea/lib/utils2.py:276
 #, python-format
 msgid "%s ago"
 msgstr "%s назад"
 
-#: kallithea/lib/utils2.py:359
+#: kallithea/lib/utils2.py:278
 #, python-format
 msgid "in %s and %s"
 msgstr "в %s и %s"
 
-#: kallithea/lib/utils2.py:362
+#: kallithea/lib/utils2.py:281
 #, python-format
 msgid "%s and %s ago"
 msgstr "%s и %s назад"
 
-#: kallithea/lib/utils2.py:365
+#: kallithea/lib/utils2.py:284
 msgid "just now"
-msgstr "прямо сейчас"
+msgstr "только что"
 
 #: kallithea/model/comment.py:68
 #, python-format
 msgid "on line %s"
 msgstr "на строке %s"
 
-#: kallithea/model/comment.py:221 kallithea/model/pull_request.py:114
+#: kallithea/model/comment.py:219 kallithea/model/pull_request.py:112
 msgid "[Mention]"
 msgstr "[Упоминание]"
 
-#: kallithea/model/db.py:1496
+#: kallithea/model/db.py:1411
 msgid "top level"
 msgstr "верхний уровень"
 
-#: kallithea/model/db.py:1637
+#: kallithea/model/db.py:1542
 msgid "Kallithea Administrator"
 msgstr "Администратор Kallithea"
 
-#: kallithea/model/db.py:1639
+#: kallithea/model/db.py:1544
 msgid "Default user has no access to new repositories"
 msgstr ""
-
-#: kallithea/model/db.py:1640
+"Неавторизованные пользователи не имеют прав доступа к новым репозиториям"
+
+#: kallithea/model/db.py:1545
 msgid "Default user has read access to new repositories"
 msgstr "Неавторизованные пользователи имеют право чтения новых репозиториев"
 
-#: kallithea/model/db.py:1641
+#: kallithea/model/db.py:1546
 msgid "Default user has write access to new repositories"
 msgstr ""
 "Неавторизованные пользователи имеют право записи в новые репозитории"
 
-#: kallithea/model/db.py:1642
+#: kallithea/model/db.py:1547
 msgid "Default user has admin access to new repositories"
 msgstr ""
-
-#: kallithea/model/db.py:1644
+"Неавторизованные пользователи имеют права администратора к новым "
+"репозиториям"
+
+#: kallithea/model/db.py:1549
 msgid "Default user has no access to new repository groups"
 msgstr ""
-
-#: kallithea/model/db.py:1645
+"Неавторизованные пользователи не имеют прав доступа к новым группам "
+"репозиториев"
+
+#: kallithea/model/db.py:1550
 msgid "Default user has read access to new repository groups"
 msgstr ""
-
-#: kallithea/model/db.py:1646
+"Неавторизованные пользователи имеют право чтения в новых группах "
+"репозиториев"
+
+#: kallithea/model/db.py:1551
 msgid "Default user has write access to new repository groups"
 msgstr ""
-
-#: kallithea/model/db.py:1647
+"Неавторизованные пользователи имеют право записи в новых группах "
+"репозиториев"
+
+#: kallithea/model/db.py:1552
 msgid "Default user has admin access to new repository groups"
 msgstr ""
-
-#: kallithea/model/db.py:1649
+"Неавторизованные пользователи имеют права администратора к новым групппам "
+"репозиториев"
+
+#: kallithea/model/db.py:1554
 msgid "Default user has no access to new user groups"
 msgstr ""
-
-#: kallithea/model/db.py:1650
+"Неавторизованные пользователи не имеют прав доступа к новым группам "
+"пользователей"
+
+#: kallithea/model/db.py:1555
 msgid "Default user has read access to new user groups"
 msgstr ""
-
-#: kallithea/model/db.py:1651
+"Неавторизованные пользователи имеют право чтения в новых группах "
+"пользователей"
+
+#: kallithea/model/db.py:1556
 msgid "Default user has write access to new user groups"
 msgstr ""
-
-#: kallithea/model/db.py:1652
+"Неавторизованные пользователи имеют право записи в новых группах "
+"пользователей"
+
+#: kallithea/model/db.py:1557
 msgid "Default user has admin access to new user groups"
 msgstr ""
-
-#: kallithea/model/db.py:1654
+"Неавторизованные пользователи имеют права администратора к новым групппам "
+"пользователей"
+
+#: kallithea/model/db.py:1559
 msgid "Only admins can create repository groups"
-msgstr "Только администраторы могут создавать группы"
-
-#: kallithea/model/db.py:1655
-#, fuzzy
+msgstr "Только администраторы могут создавать группы репозиториев"
+
+#: kallithea/model/db.py:1560
 msgid "Non-admins can create repository groups"
-msgstr "Создана новая группа репозиториев %s"
-
-#: kallithea/model/db.py:1657
-#, fuzzy
+msgstr "Группы репозиториев могут создаваться любыми пользователями"
+
+#: kallithea/model/db.py:1562
 msgid "Only admins can create user groups"
-msgstr "Создавать группы пользователей"
-
-#: kallithea/model/db.py:1658
-#, fuzzy
+msgstr "Группы пользователей могут создаваться только администраторами"
+
+#: kallithea/model/db.py:1563
 msgid "Non-admins can create user groups"
-msgstr "Создавать группы пользователей"
-
-#: kallithea/model/db.py:1660
+msgstr "Группы пользователей могут создаваться любыми пользователями"
+
+#: kallithea/model/db.py:1565
 msgid "Only admins can create top level repositories"
-msgstr ""
-
-#: kallithea/model/db.py:1661
+msgstr "Только администраторы могут создавать репозитории верхнего уровня"
+
+#: kallithea/model/db.py:1566
 msgid "Non-admins can create top level repositories"
-msgstr ""
-
-#: kallithea/model/db.py:1663
+msgstr "Любой пользователь может создавать репозитории верхнего уровня"
+
+#: kallithea/model/db.py:1568
 msgid ""
 "Repository creation enabled with write permission to a repository group"
 msgstr ""
-
-#: kallithea/model/db.py:1664
+"Создание репозиториев доступно с правом на запись в группу репозиториев"
+
+#: kallithea/model/db.py:1569
 msgid ""
 "Repository creation disabled with write permission to a repository group"
 msgstr ""
-
-#: kallithea/model/db.py:1666
-#, fuzzy
+"Создание репозиториев недоступно с правом на запись в группу репозиториев"
+
+#: kallithea/model/db.py:1571
 msgid "Only admins can fork repositories"
-msgstr "Местонахождение репозиториев"
-
-#: kallithea/model/db.py:1667
-#, fuzzy
+msgstr "Форки репозиториев могут создаваться только администраторами"
+
+#: kallithea/model/db.py:1572
 msgid "Non-admins can fork repositories"
-msgstr "Сбросить кэш для всех репозиториев"
-
-#: kallithea/model/db.py:1669
+msgstr "Форки репозиториев могут создаваться любыми пользователями"
+
+#: kallithea/model/db.py:1574
 msgid "Registration disabled"
 msgstr "Регистрация отключена"
 
-#: kallithea/model/db.py:1670
-#, fuzzy
+#: kallithea/model/db.py:1575
 msgid "User registration with manual account activation"
 msgstr "Регистрация пользователя с ручной активацией учётной записи"
 
-#: kallithea/model/db.py:1671
-#, fuzzy
+#: kallithea/model/db.py:1576
 msgid "User registration with automatic account activation"
 msgstr "Регистрация пользователя с автоматической активацией"
 
-#: kallithea/model/db.py:2206
-#, fuzzy
+#: kallithea/model/db.py:1992
 msgid "Not reviewed"
-msgstr "Не просмотрено"
-
-#: kallithea/model/db.py:2207
-#, fuzzy
+msgstr "Не проверено"
+
+#: kallithea/model/db.py:1993
 msgid "Under review"
-msgstr "На рассмотрении"
-
-#: kallithea/model/db.py:2208
-#, fuzzy
-#| msgid "Approved"
+msgstr "На проверке"
+
+#: kallithea/model/db.py:1994
 msgid "Not approved"
-msgstr "Одобрено"
-
-#: kallithea/model/db.py:2209
+msgstr "Не одобрено"
+
+#: kallithea/model/db.py:1995
 msgid "Approved"
 msgstr "Одобрено"
 
@@ -1543,135 +1536,130 @@
 
 #: kallithea/model/forms.py:170
 msgid "Name must not contain only digits"
-msgstr ""
-
-#: kallithea/model/notification.py:164
-#, fuzzy, python-format
-#| msgid "[Comment] %(repo_name)s pull request %(pr_nice_id)s from %(ref)s"
+msgstr "Имя не может состоять только из цифр"
+
+#: kallithea/model/notification.py:162
+#, python-format
 msgid ""
 "[Comment] %(repo_name)s changeset %(short_id)s \"%(message_short)s\" on "
 "%(branch)s"
-msgstr "[прокомментировано] в запросе на внесение изменений для"
-
-#: kallithea/model/notification.py:167
+msgstr ""
+"[Комментарий] к набору изменений %(short_id)s «%(message_short)s» "
+"репозитория %(repo_name)s в %(branch)s"
+
+#: kallithea/model/notification.py:165
 #, python-format
 msgid "New user %(new_username)s registered"
 msgstr "Новый пользователь \"%(new_username)s\" зарегистрирован"
 
-#: kallithea/model/notification.py:169
+#: kallithea/model/notification.py:167
 #, python-format
 msgid ""
 "[Review] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
-
-#: kallithea/model/notification.py:170
+"[Ревью] к PR %(pr_nice_id)s «%(pr_title_short)s» из %(pr_source_branch)s "
+"репозитория %(repo_name)s от %(pr_owner_username)s"
+
+#: kallithea/model/notification.py:168
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
-
-#: kallithea/model/notification.py:183
+"[Комментарий] к PR %(pr_nice_id)s «%(pr_title_short)s» из "
+"%(pr_source_branch)s репозитория %(repo_name)s от %(pr_owner_username)s"
+
+#: kallithea/model/notification.py:188
 msgid "Closing"
 msgstr "Закрыт"
 
-#: kallithea/model/pull_request.py:73
-#, fuzzy, python-format
+#: kallithea/model/pull_request.py:72
+#, python-format
 msgid ""
 "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s"
 msgstr ""
-"%(user)s просит вас рассмотреть pull request #%(pr_id)s: %(pr_title)s"
-
-#: kallithea/model/pull_request.py:209
-#, fuzzy
-#| msgid "Error creating pull request: %s"
+"%(user)s просит вас рассмотреть pull-запрос %(pr_nice_id)s: %(pr_title)s"
+
+#: kallithea/model/pull_request.py:207
 msgid "Cannot create empty pull request"
-msgstr "Ошибка при создании pull-запроса: %s"
-
-#: kallithea/model/pull_request.py:217
+msgstr "Невозможно создать пустой pull-запрос"
+
+#: kallithea/model/pull_request.py:215
 #, python-format
 msgid ""
 "Cannot create pull request - criss cross merge detected, please merge a "
 "later %s revision to %s"
 msgstr ""
-
-#: kallithea/model/pull_request.py:245 kallithea/model/pull_request.py:380
-#, fuzzy
-#| msgid "Confirm to delete this pull request"
+"Невозможно создать pull-запрос — обнаружено перекрёстное слияние. "
+"Попробуйте слить более позднюю ревизию %s с %s"
+
+#: kallithea/model/pull_request.py:243 kallithea/model/pull_request.py:378
 msgid "You are not authorized to create the pull request"
-msgstr "Подтвердите удаление этого pull-request'а"
-
-#: kallithea/model/pull_request.py:339
-#, fuzzy
-#| msgid "Missing changesets since the previous pull request:"
+msgstr "Недостаточно привилегий для создания pull-запроса"
+
+#: kallithea/model/pull_request.py:337
 msgid "Missing changesets since the previous iteration:"
-msgstr "Отсутствующие ревизии относительно предыдущего pull-запроса:"
-
-#: kallithea/model/pull_request.py:346
-#, fuzzy, python-format
-#| msgid "New changesets on %s %s since the previous pull request:"
+msgstr "Отсутствующие ревизии относительно предыдущей итерации:"
+
+#: kallithea/model/pull_request.py:344
+#, python-format
 msgid "New changesets on %s %s since the previous iteration:"
-msgstr "Новые ревизии на %s %s относительно предыдущего pull-запроса:"
-
-#: kallithea/model/pull_request.py:353
+msgstr "Новые наборы изменений в %s %s относительно предыдущей итерации:"
+
+#: kallithea/model/pull_request.py:351
 msgid "Ancestor didn't change - diff since previous iteration:"
-msgstr ""
-
-#: kallithea/model/pull_request.py:360
-#, fuzzy, python-format
-#| msgid ""
-#| "This pull request is based on another %s revision and there is no "
-#| "simple diff."
+msgstr "Предок не изменился — разница с момента последней итерации:"
+
+#: kallithea/model/pull_request.py:358
+#, python-format
 msgid ""
 "This iteration is based on another %s revision and there is no simple "
 "diff."
 msgstr ""
-"Этот pull-запрос основан на другой ревизии %s, простой diff невозможен."
-
-#: kallithea/model/pull_request.py:362
-#, fuzzy, python-format
-#| msgid "No changes found on %s %s since previous version."
+"Эта итерация основана на другой ревизии %s, простой diff невозможен."
+
+#: kallithea/model/pull_request.py:360
+#, python-format
 msgid "No changes found on %s %s since previous iteration."
-msgstr "Нет изменений на %s %s относительно предыдущей версии."
-
-#: kallithea/model/pull_request.py:388
+msgstr "Нет изменений на %s %s относительно предыдущей итерации."
+
+#: kallithea/model/pull_request.py:386
 #, python-format
 msgid "Closed, next iteration: %s ."
-msgstr ""
-
-#: kallithea/model/scm.py:668
+msgstr "Закрыто. Следующая итерация: %s."
+
+#: kallithea/model/scm.py:655
 msgid "latest tip"
 msgstr "последняя версия"
 
-#: kallithea/model/ssh_key.py:56
+#: kallithea/model/ssh_key.py:57
 #, python-format
 msgid "SSH key %r is invalid: %s"
-msgstr ""
-
-#: kallithea/model/ssh_key.py:68
+msgstr "Ошибка ключа SSH %r: %s"
+
+#: kallithea/model/ssh_key.py:69
 #, python-format
 msgid "SSH key %s is already used by %s"
-msgstr ""
-
-#: kallithea/model/ssh_key.py:89
-#, fuzzy, python-format
-msgid "SSH key %r not found"
-msgstr "Набор изменений не найден"
-
-#: kallithea/model/user.py:186
+msgstr "Ключ SSH %s уже используется пользователем %s"
+
+#: kallithea/model/ssh_key.py:88
+#, python-format
+msgid "SSH key with fingerprint %r found"
+msgstr "Найден ключ SSH с отпечатком %r"
+
+#: kallithea/model/user.py:180
 msgid "New user registration"
 msgstr "Регистрация нового пользователя"
 
-#: kallithea/model/user.py:250
-#, fuzzy
+#: kallithea/model/user.py:244
 msgid ""
 "You can't remove this user since it is crucial for the entire application"
 msgstr ""
-"Вы не можете удалить пользователя, поскольку это критично для работы "
-"всего приложения"
-
-#: kallithea/model/user.py:255
+"Вы не можете удалить этого пользователя, поскольку это критично для "
+"работы всего приложения"
+
+#: kallithea/model/user.py:249
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repositories and cannot be removed. Switch "
@@ -1680,7 +1668,7 @@
 "Пользователь \"%s\" всё ещё является владельцем %s репозиториев и поэтому "
 "не может быть удалён. Смените владельца или удалите эти репозитории: %s"
 
-#: kallithea/model/user.py:260
+#: kallithea/model/user.py:254
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repository groups and cannot be removed. Switch "
@@ -1690,7 +1678,7 @@
 "поэтому не может быть удалён. Смените владельца или удалите данные "
 "группы: %s"
 
-#: kallithea/model/user.py:267
+#: kallithea/model/user.py:261
 #, python-format
 msgid ""
 "User \"%s\" still owns %s user groups and cannot be removed. Switch "
@@ -1700,65 +1688,63 @@
 "поэтому не может быть удалён. Смените владельца или удалите данные "
 "группы: %s"
 
-#: kallithea/model/user.py:361
+#: kallithea/model/user.py:355
 msgid "Password reset link"
 msgstr "Ссылка сброса пароля"
 
-#: kallithea/model/user.py:408
-#, fuzzy
+#: kallithea/model/user.py:402
 msgid "Password reset notification"
-msgstr "Ссылка сброса пароля"
-
-#: kallithea/model/user.py:409
+msgstr "Уведомление о сбросе пароля"
+
+#: kallithea/model/user.py:403
 #, python-format
 msgid ""
 "The password to your account %s has been changed using password reset "
 "form."
-msgstr ""
-
-#: kallithea/model/validators.py:52 kallithea/model/validators.py:53
+msgstr "Пароль к вашему аккаунту %s был изменён через форму сброса пароля."
+
+#: kallithea/model/validators.py:53 kallithea/model/validators.py:54
 msgid "Value cannot be an empty list"
 msgstr "Значение не может быть пустым списком"
 
-#: kallithea/model/validators.py:72
+#: kallithea/model/validators.py:73
 #, python-format
 msgid "Username \"%(username)s\" already exists"
 msgstr "Пользователь с именем \"%(username)s\" уже существует"
 
-#: kallithea/model/validators.py:74
-#, fuzzy, python-format
+#: kallithea/model/validators.py:75
+#, python-format
 msgid "Username \"%(username)s\" cannot be used"
-msgstr "Имя \"%(username)s\" недопустимо"
-
-#: kallithea/model/validators.py:76
-#, fuzzy
+msgstr "Имя «%(username)s» недопустимо"
+
+#: kallithea/model/validators.py:77
 msgid ""
 "Username may only contain alphanumeric characters underscores, periods or "
 "dashes and must begin with an alphanumeric character or underscore"
 msgstr ""
 "Имя пользователя может содержать только буквы, цифры, символы "
-"подчеркивания, точки и тире; а так же должно начинаться с буквы, цифры "
-"либо с символа подчеркивания"
-
-#: kallithea/model/validators.py:103
+"подчеркивания, точки и тире, а также должно начинаться с буквы, цифры или "
+"с символа подчеркивания"
+
+#: kallithea/model/validators.py:104
 msgid "The input is not valid"
-msgstr ""
-
-#: kallithea/model/validators.py:110
+msgstr "Введено некорректное значение"
+
+#: kallithea/model/validators.py:111
 #, python-format
 msgid "Username %(username)s is not valid"
 msgstr "Имя \"%(username)s\" недопустимо"
 
-#: kallithea/model/validators.py:131
+#: kallithea/model/validators.py:132
 msgid "Invalid user group name"
 msgstr "Неверное имя группы пользователей"
 
-#: kallithea/model/validators.py:132
+#: kallithea/model/validators.py:133
 #, python-format
 msgid "User group \"%(usergroup)s\" already exists"
 msgstr "Группа пользователей \"%(usergroup)s\" уже существует"
 
-#: kallithea/model/validators.py:134
+#: kallithea/model/validators.py:135
 msgid ""
 "user group name may only contain alphanumeric characters underscores, "
 "periods or dashes and must begin with alphanumeric character"
@@ -1766,103 +1752,102 @@
 "имя группы пользователей может содержать только буквы, цифры, символы "
 "подчеркивания, точки и тире; а так же должно начинаться с буквы или цифры"
 
-#: kallithea/model/validators.py:174
+#: kallithea/model/validators.py:175
 msgid "Cannot assign this group as parent"
 msgstr "Невозможно использовать эту группу как родителя"
 
-#: kallithea/model/validators.py:175
+#: kallithea/model/validators.py:176
 #, python-format
 msgid "Group \"%(group_name)s\" already exists"
 msgstr "Группа \"%(group_name)s\" уже существует"
 
-#: kallithea/model/validators.py:177
+#: kallithea/model/validators.py:178
 #, python-format
 msgid "Repository with name \"%(group_name)s\" already exists"
 msgstr "Репозиторий с именем «%(group_name)s» уже существует"
 
-#: kallithea/model/validators.py:233
+#: kallithea/model/validators.py:230
 msgid "Invalid characters (non-ascii) in password"
 msgstr "Недопустимые символы (не ascii) в пароле"
 
-#: kallithea/model/validators.py:248
+#: kallithea/model/validators.py:245
 msgid "Invalid old password"
 msgstr "Неверно задан старый пароль"
 
-#: kallithea/model/validators.py:264
+#: kallithea/model/validators.py:261
 msgid "Passwords do not match"
 msgstr "Пароли не совпадают"
 
-#: kallithea/model/validators.py:279
-#, fuzzy
+#: kallithea/model/validators.py:276
 msgid "Invalid username or password"
-msgstr "неверный пароль"
-
-#: kallithea/model/validators.py:313
-#, fuzzy, python-format
+msgstr "Неверное имя пользователя или пароль"
+
+#: kallithea/model/validators.py:310
+#, python-format
 msgid "Repository name %(repo)s is not allowed"
-msgstr "Имя репозитория %(repo)s запрещено"
-
-#: kallithea/model/validators.py:315
+msgstr "Имя репозитория %(repo)s недопустимо"
+
+#: kallithea/model/validators.py:312
 #, python-format
 msgid "Repository named %(repo)s already exists"
 msgstr "Репозитарий %(repo)s уже существует"
 
-#: kallithea/model/validators.py:316
+#: kallithea/model/validators.py:313
 #, python-format
 msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
 msgstr "Репозитарий \"%(repo)s\" уже существует в группе \"%(group)s\""
 
-#: kallithea/model/validators.py:318
+#: kallithea/model/validators.py:315
 #, python-format
 msgid "Repository group with name \"%(repo)s\" already exists"
 msgstr "Группа репозиториев \"%(repo)s\" уже существует"
 
-#: kallithea/model/validators.py:404
-#, fuzzy
+#: kallithea/model/validators.py:401
 msgid "Invalid repository URL"
-msgstr "приватный репозиторий"
-
-#: kallithea/model/validators.py:405
+msgstr "Недопустимый URL репозитория"
+
+#: kallithea/model/validators.py:402
 msgid ""
 "Invalid repository URL. It must be a valid http, https, ssh, svn+http or "
 "svn+https URL"
 msgstr ""
-
-#: kallithea/model/validators.py:430
+"Недопустимый URL репозитория. Требуется корректный http, https, ssh, svn"
+"+http или svn+https URL"
+
+#: kallithea/model/validators.py:427
 msgid "Fork has to be the same type as parent"
-msgstr "Тип форка будет совпадать с родительским"
-
-#: kallithea/model/validators.py:445
+msgstr "Форк будет иметь тот же тип, что и родительский"
+
+#: kallithea/model/validators.py:442
 msgid "You don't have permissions to create repository in this group"
 msgstr "У вас недостаточно прав для создания репозиториев в этой группе"
 
-#: kallithea/model/validators.py:447
+#: kallithea/model/validators.py:444
 msgid "no permission to create repository in root location"
 msgstr "недостаточно прав для создания репозитория в корневом каталоге"
 
-#: kallithea/model/validators.py:497
+#: kallithea/model/validators.py:494
 msgid "You don't have permissions to create a group in this location"
 msgstr "У Вас недостаточно привилегий для создания группы в этом месте"
 
-#: kallithea/model/validators.py:537
+#: kallithea/model/validators.py:534
 msgid "This username or user group name is not valid"
 msgstr "Данное имя пользователя или группы пользователей недопустимо"
 
-#: kallithea/model/validators.py:630
+#: kallithea/model/validators.py:627
 msgid "This is not a valid path"
 msgstr "Этот путь ошибочен"
 
-#: kallithea/model/validators.py:647
-#, fuzzy
+#: kallithea/model/validators.py:644
 msgid "This email address is already in use"
-msgstr "Этот E-mail уже занят"
-
-#: kallithea/model/validators.py:667
-#, fuzzy, python-format
+msgstr "Этот адрес почты уже занят"
+
+#: kallithea/model/validators.py:664
+#, python-format
 msgid "Email address \"%(email)s\" not found"
-msgstr "\"%(email)s\" не существует."
-
-#: kallithea/model/validators.py:704
+msgstr "Адрес «%(email)s» не зарегистрирован"
+
+#: kallithea/model/validators.py:701
 msgid ""
 "The LDAP Login attribute of the CN must be specified - this is the name "
 "of the attribute that is equivalent to \"username\""
@@ -1870,11 +1855,11 @@
 "Для входа по LDAP должно быть указано значение аттрибута CN - это "
 "эквивалент имени пользователя"
 
-#: kallithea/model/validators.py:716
+#: kallithea/model/validators.py:713
 msgid "Please enter a valid IPv4 or IPv6 address"
 msgstr "Пожалуйста, введите существующий IPv4 или IPv6 адре"
 
-#: kallithea/model/validators.py:717
+#: kallithea/model/validators.py:714
 #, python-format
 msgid ""
 "The network size (bits) must be within the range of 0-32 (not %(bits)r)"
@@ -1882,20 +1867,20 @@
 "Значение маски подсети должно быть в пределах от 0 до 32 (%(bits)r - "
 "неверно)"
 
-#: kallithea/model/validators.py:750
+#: kallithea/model/validators.py:747
 msgid "Key name can only consist of letters, underscore, dash or numbers"
 msgstr ""
 "Ключевое имя может только состоять из букв, символа подчеркивания, тире "
 "или чисел"
 
-#: kallithea/model/validators.py:764
+#: kallithea/model/validators.py:761
 msgid "Filename cannot be inside a directory"
 msgstr "Файла нет в каталоге"
 
-#: kallithea/model/validators.py:780
+#: kallithea/model/validators.py:777
 #, python-format
 msgid "Plugins %(loaded)s and %(next_to_load)s both export the same name"
-msgstr ""
+msgstr "Плагины %(loaded)s и %(next_to_load)s экспортируют одно и то же имя"
 
 #: kallithea/templates/about.html:4 kallithea/templates/about.html:13
 msgid "About"
@@ -1953,7 +1938,7 @@
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:60
 #: kallithea/templates/email_templates/pull_request.html:37
 #: kallithea/templates/forks/fork.html:34
-#: kallithea/templates/index_base.html:58
+#: kallithea/templates/index_base.html:59
 #: kallithea/templates/pullrequests/pullrequest.html:33
 #: kallithea/templates/pullrequests/pullrequest_show.html:38
 #: kallithea/templates/pullrequests/pullrequest_show.html:59
@@ -1961,14 +1946,14 @@
 msgid "Description"
 msgstr "Описание"
 
-#: kallithea/templates/index_base.html:60
+#: kallithea/templates/index_base.html:61
 msgid "Last Change"
 msgstr "Последнее изменение"
 
 #: kallithea/templates/admin/my_account/my_account_repos.html:15
 #: kallithea/templates/admin/my_account/my_account_watched.html:15
 #: kallithea/templates/admin/repos/repos.html:41
-#: kallithea/templates/index_base.html:62
+#: kallithea/templates/index_base.html:63
 msgid "Tip"
 msgstr "Состояние"
 
@@ -1978,7 +1963,7 @@
 #: kallithea/templates/admin/repos/repos.html:42
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:8
 #: kallithea/templates/admin/user_groups/user_groups.html:42
-#: kallithea/templates/index_base.html:63
+#: kallithea/templates/index_base.html:64
 #: kallithea/templates/pullrequests/pullrequest_data.html:16
 #: kallithea/templates/pullrequests/pullrequest_show.html:124
 #: kallithea/templates/pullrequests/pullrequest_show.html:219
@@ -2002,7 +1987,7 @@
 #: kallithea/templates/admin/users/user_edit_profile.html:18
 #: kallithea/templates/admin/users/users.html:37
 #: kallithea/templates/base/base.html:364
-#: kallithea/templates/email_templates/registration.html:11
+#: kallithea/templates/email_templates/registration.html:12
 #: kallithea/templates/login.html:28 kallithea/templates/register.html:31
 msgid "Username"
 msgstr "Имя пользователя"
@@ -2016,7 +2001,7 @@
 
 #: kallithea/templates/login.html:44
 msgid "Stay logged in after browser restart"
-msgstr ""
+msgstr "Оставаться авторизованным"
 
 #: kallithea/templates/login.html:52
 msgid "Forgot your password ?"
@@ -2060,40 +2045,41 @@
 msgstr "Послать ссылку сброса пароля"
 
 #: kallithea/templates/password_reset.html:52
-#, fuzzy
 msgid ""
 "A password reset link will be sent to the specified email address if it "
 "is registered in the system."
-msgstr "Ссылка для сброса пароля была отправлена на соответствующий e-mail."
+msgstr ""
+"Ссылка для сброса пароля была отправлена на соответствующий e-mail, если "
+"он был зарегистрирован в системе."
 
 #: kallithea/templates/password_reset_confirmation.html:23
 #, python-format
 msgid "You are about to set a new password for the email address %s."
-msgstr ""
+msgstr "Вы собираетесь установить новый пароль для адреса %s."
 
 #: kallithea/templates/password_reset_confirmation.html:24
 msgid ""
 "Note that you must use the same browser session for this as the one used "
 "to request the password reset."
 msgstr ""
+"Обратите внимание, что вы должны оставаться в пределах этой сессии "
+"браузера, поскольку в ней был запрошен сброс пароля."
 
 #: kallithea/templates/password_reset_confirmation.html:29
 msgid "Code you received in the email"
-msgstr ""
+msgstr "Код, который вы получили по почте"
 
 #: kallithea/templates/password_reset_confirmation.html:36
-#, fuzzy
 msgid "New Password"
 msgstr "Новый пароль"
 
 #: kallithea/templates/password_reset_confirmation.html:43
-#, fuzzy
 msgid "Confirm New Password"
-msgstr "Подтвердите новый пароль"
+msgstr "Подтверждение пароля"
 
 #: kallithea/templates/password_reset_confirmation.html:51
 msgid "Confirm"
-msgstr ""
+msgstr "Подтвердить"
 
 #: kallithea/templates/register.html:5 kallithea/templates/register.html:24
 #: kallithea/templates/register.html:83
@@ -2129,7 +2115,7 @@
 #: kallithea/templates/admin/settings/settings.html:31
 #: kallithea/templates/admin/users/user_add.html:62
 #: kallithea/templates/admin/users/user_edit_profile.html:25
-#: kallithea/templates/email_templates/registration.html:33
+#: kallithea/templates/email_templates/registration.html:34
 #: kallithea/templates/register.html:66
 msgid "Email"
 msgstr "E-mail"
@@ -2137,6 +2123,8 @@
 #: kallithea/templates/register.html:85
 msgid "Registered accounts are ready to use and need no further action."
 msgstr ""
+"Зарегистрированные аккаунты готовы к использованию и не требуют "
+"дальнейших действий."
 
 #: kallithea/templates/register.html:87
 msgid "Please wait for an administrator to activate your account."
@@ -2152,7 +2140,7 @@
 #: kallithea/templates/admin/admin.html:10
 #: kallithea/templates/journal/journal.html:10
 msgid "journal filter..."
-msgstr "Фильтр журнала..."
+msgstr "фильтр..."
 
 #: kallithea/templates/admin/admin.html:12
 #: kallithea/templates/journal/journal.html:12
@@ -2165,8 +2153,8 @@
 msgid "%s Entry"
 msgid_plural "%s Entries"
 msgstr[0] "%s запись"
-msgstr[1] "%s записей"
-msgstr[2] "%s записи"
+msgstr[1] "%s записи"
+msgstr[2] "%s записей"
 
 #: kallithea/templates/admin/admin_log.html:6
 #: kallithea/templates/admin/my_account/my_account_repos.html:16
@@ -2186,7 +2174,7 @@
 
 #: kallithea/templates/admin/admin_log.html:9
 msgid "From IP"
-msgstr "С IP"
+msgstr "IP"
 
 #: kallithea/templates/admin/admin_log.html:61
 msgid "No actions yet"
@@ -2210,13 +2198,12 @@
 msgstr "Включенные плагины"
 
 #: kallithea/templates/admin/auth/auth_settings.html:32
-#, fuzzy
 msgid ""
 "Comma-separated list of plugins; Kallithea will try user authentication "
 "in plugin order"
 msgstr ""
-"Список плагинов, разделенных запятой. Kallithea будет пробовать "
-"аутентифицировать пользователя в порядке указания плагинов"
+"Список плагинов через запятую. Kallithea будет аутентифицировать "
+"пользователя в порядке указания плагинов"
 
 #: kallithea/templates/admin/auth/auth_settings.html:36
 msgid "Available built-in plugins"
@@ -2305,10 +2292,12 @@
 "Gist was updated since you started editing. Copy your changes and click "
 "%(here)s to reload new version."
 msgstr ""
+"Gist был изменён с момента начала редактирования. Скопируйте свои правки "
+"и нажмите %(here)s для загрузки новой версии."
 
 #: kallithea/templates/admin/gists/edit.html:36
 msgid "here"
-msgstr ""
+msgstr "сюда"
 
 #: kallithea/templates/admin/gists/edit.html:51
 #: kallithea/templates/admin/gists/new.html:35
@@ -2318,7 +2307,7 @@
 #: kallithea/templates/admin/gists/edit.html:54
 #: kallithea/templates/admin/gists/new.html:38
 msgid "Gist lifetime"
-msgstr ""
+msgstr "Время жизни gist`а"
 
 #: kallithea/templates/admin/gists/edit.html:59
 #: kallithea/templates/admin/gists/edit.html:61
@@ -2344,7 +2333,6 @@
 #: kallithea/templates/admin/users/user_edit_api_keys.html:7
 #: kallithea/templates/admin/users/user_edit_api_keys.html:26
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:21
-#, fuzzy
 msgid "Never"
 msgstr "никогда"
 
@@ -2382,7 +2370,7 @@
 msgstr "Создать новую gist-запись"
 
 #: kallithea/templates/admin/gists/index.html:51
-#: kallithea/templates/data_table/_dt_elements.html:78
+#: kallithea/templates/data_table/_dt_elements.html:84
 msgid "Created"
 msgstr "Создано"
 
@@ -2393,19 +2381,19 @@
 #: kallithea/templates/admin/gists/new.html:5
 #: kallithea/templates/admin/gists/new.html:18
 msgid "New Gist"
-msgstr ""
+msgstr "Новый gist"
 
 #: kallithea/templates/admin/gists/new.html:45
 msgid "Name this gist ..."
-msgstr ""
+msgstr "Назовите этот gist…"
 
 #: kallithea/templates/admin/gists/new.html:53
 msgid "Create Private Gist"
-msgstr ""
+msgstr "Создать приватный gist"
 
 #: kallithea/templates/admin/gists/new.html:54
 msgid "Create Public Gist"
-msgstr ""
+msgstr "Создать публичный gist"
 
 #: kallithea/templates/admin/gists/new.html:55
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:14
@@ -2450,11 +2438,11 @@
 
 #: kallithea/templates/admin/gists/show.html:35
 msgid "Public Gist"
-msgstr ""
+msgstr "Публичный Gist"
 
 #: kallithea/templates/admin/gists/show.html:37
 msgid "Private Gist"
-msgstr ""
+msgstr "Приватный Gist"
 
 #: kallithea/templates/admin/gists/show.html:54
 #: kallithea/templates/admin/my_account/my_account_emails.html:23
@@ -2466,13 +2454,13 @@
 #: kallithea/templates/admin/users/user_edit_ips.html:21
 #: kallithea/templates/changeset/changeset_file_comment.html:30
 #: kallithea/templates/changeset/changeset_file_comment.html:121
-#: kallithea/templates/data_table/_dt_elements.html:69
-#: kallithea/templates/data_table/_dt_elements.html:89
-#: kallithea/templates/data_table/_dt_elements.html:91
-#: kallithea/templates/data_table/_dt_elements.html:101
-#: kallithea/templates/data_table/_dt_elements.html:103
-#: kallithea/templates/data_table/_dt_elements.html:120
-#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:75
+#: kallithea/templates/data_table/_dt_elements.html:95
+#: kallithea/templates/data_table/_dt_elements.html:97
+#: kallithea/templates/data_table/_dt_elements.html:107
+#: kallithea/templates/data_table/_dt_elements.html:109
+#: kallithea/templates/data_table/_dt_elements.html:126
+#: kallithea/templates/data_table/_dt_elements.html:128
 #: kallithea/templates/files/files_source.html:35
 #: kallithea/templates/files/files_source.html:38
 #: kallithea/templates/files/files_source.html:41
@@ -2488,14 +2476,14 @@
 #: kallithea/templates/base/perms_summary.html:44
 #: kallithea/templates/base/perms_summary.html:81
 #: kallithea/templates/base/perms_summary.html:83
-#: kallithea/templates/data_table/_dt_elements.html:63
-#: kallithea/templates/data_table/_dt_elements.html:64
-#: kallithea/templates/data_table/_dt_elements.html:85
-#: kallithea/templates/data_table/_dt_elements.html:86
-#: kallithea/templates/data_table/_dt_elements.html:97
-#: kallithea/templates/data_table/_dt_elements.html:98
-#: kallithea/templates/data_table/_dt_elements.html:116
-#: kallithea/templates/data_table/_dt_elements.html:117
+#: kallithea/templates/data_table/_dt_elements.html:69
+#: kallithea/templates/data_table/_dt_elements.html:70
+#: kallithea/templates/data_table/_dt_elements.html:91
+#: kallithea/templates/data_table/_dt_elements.html:92
+#: kallithea/templates/data_table/_dt_elements.html:103
+#: kallithea/templates/data_table/_dt_elements.html:104
+#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:123
 #: kallithea/templates/files/diff_2way.html:56
 #: kallithea/templates/files/files_source.html:37
 #: kallithea/templates/files/files_source.html:40
@@ -2521,7 +2509,7 @@
 #: kallithea/templates/admin/my_account/my_account.html:9
 #: kallithea/templates/base/base.html:390
 msgid "My Account"
-msgstr "Мой Аккаунт"
+msgstr "Мой аккаунт"
 
 #: kallithea/templates/admin/my_account/my_account.html:25
 #: kallithea/templates/admin/users/user_edit.html:29
@@ -2529,16 +2517,13 @@
 msgstr "Профиль"
 
 #: kallithea/templates/admin/my_account/my_account.html:26
-#, fuzzy
 msgid "Email Addresses"
 msgstr "Новый E-mail"
 
 #: kallithea/templates/admin/my_account/my_account.html:29
 #: kallithea/templates/admin/users/user_edit.html:32
-#, fuzzy
-#| msgid "API Keys"
 msgid "SSH Keys"
-msgstr "API-ключи"
+msgstr "Ключи SSH"
 
 #: kallithea/templates/admin/my_account/my_account.html:31
 #: kallithea/templates/admin/users/user_edit.html:34
@@ -2546,44 +2531,40 @@
 msgstr "API-ключи"
 
 #: kallithea/templates/admin/my_account/my_account.html:32
-#, fuzzy
 msgid "Owned Repositories"
-msgstr "репозитории"
+msgstr "Свои репозитории"
 
 #: kallithea/templates/admin/my_account/my_account.html:33
 #: kallithea/templates/journal/journal.html:33
-#, fuzzy
 msgid "Watched Repositories"
-msgstr "Создать репозитории"
+msgstr "Наблюдаемые репозитории"
 
 #: kallithea/templates/admin/my_account/my_account.html:34
 #: kallithea/templates/admin/permissions/permissions.html:30
 #: kallithea/templates/admin/user_groups/user_group_edit.html:32
 #: kallithea/templates/admin/users/user_edit.html:37
-#, fuzzy
 msgid "Show Permissions"
-msgstr "Скопировать привилегии"
+msgstr "Права доступа"
 
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:5
 #: kallithea/templates/admin/users/user_edit_api_keys.html:5
 msgid "Built-in"
-msgstr ""
+msgstr "Встроенный"
 
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:13
 #: kallithea/templates/admin/users/user_edit_api_keys.html:13
-#, fuzzy, python-format
+#, python-format
 msgid "Confirm to reset this API key: %s"
 msgstr "Подтвердите сброс этого API-ключа: %s"
 
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:29
 #: kallithea/templates/admin/users/user_edit_api_keys.html:29
-#, fuzzy
 msgid "Expired"
-msgstr "Истекает"
+msgstr "Срок действия истёк"
 
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:39
 #: kallithea/templates/admin/users/user_edit_api_keys.html:39
-#, fuzzy, python-format
+#, python-format
 msgid "Confirm to remove this API key: %s"
 msgstr "Подтвердите удаление этого API-ключа: %s"
 
@@ -2591,21 +2572,18 @@
 #: kallithea/templates/admin/my_account/my_account_ssh_keys.html:30
 #: kallithea/templates/admin/users/user_edit_api_keys.html:41
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:30
-#, fuzzy
 msgid "Remove"
-msgstr "Удалено"
+msgstr "Удалить"
 
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:48
 #: kallithea/templates/admin/users/user_edit_api_keys.html:48
-#, fuzzy
 msgid "No additional API keys specified"
-msgstr "Дополнительные адреса e-mail не указаны"
+msgstr "Дополнительные API-ключи не указаны"
 
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:56
 #: kallithea/templates/admin/users/user_edit_api_keys.html:56
-#, fuzzy
 msgid "New API key"
-msgstr "Ключ"
+msgstr "Новый API-ключ"
 
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:72
 #: kallithea/templates/admin/my_account/my_account_emails.html:46
@@ -2628,6 +2606,10 @@
 "account, as if you had provided the script or service with your actual\n"
 "password.\n"
 msgstr ""
+"\n"
+"Ключи API позволяют скриптам или сервисам получать \n"
+"доступ к %s от имени вашего аккаунта, как если бы вы \n"
+"указали в скрипте или сервисе свой реальный пароль.\n"
 
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:86
 msgid ""
@@ -2636,6 +2618,11 @@
 "nor passed to untrusted scripts or services. If such sharing should\n"
 "happen anyway, reset the API key on this page to prevent further use.\n"
 msgstr ""
+"\n"
+"Как и пароли, ключи API не следует передавать третьим лицам,\n"
+"ненадёжным скриптам и сервисам. Если это всё же произошло, \n"
+"сбросьте ключ на этой странице, чтобы предотвратить\n"
+"его дальнейшее использование.\n"
 
 #: kallithea/templates/admin/my_account/my_account_emails.html:9
 #: kallithea/templates/admin/users/user_edit_emails.html:9
@@ -2679,24 +2666,22 @@
 #, python-format
 msgid ""
 "This account is managed with %s and the password cannot be changed here"
-msgstr ""
+msgstr "Этим аккаунтом управляет %s, поэтому здесь нельзя сменить пароль"
 
 #: kallithea/templates/admin/my_account/my_account_perms.html:3
-#, fuzzy
 msgid "Current IP"
-msgstr "текущий IP-адрес"
+msgstr "Текущий IP-адрес"
 
 #: kallithea/templates/admin/my_account/my_account_profile.html:4
 #: kallithea/templates/admin/users/user_edit_profile.html:4
 msgid "Gravatar"
-msgstr ""
+msgstr "Grаvatar"
 
 #: kallithea/templates/admin/my_account/my_account_profile.html:10
 #: kallithea/templates/admin/users/user_edit_profile.html:10
-#, fuzzy, python-format
-#| msgid "Change your avatar at"
+#, python-format
 msgid "Change %s avatar at"
-msgstr "Измените аватар через сайт"
+msgstr "Измените аватар %s на"
 
 #: kallithea/templates/admin/my_account/my_account_profile.html:12
 #: kallithea/templates/admin/users/user_edit_profile.html:12
@@ -2705,7 +2690,7 @@
 
 #: kallithea/templates/admin/my_account/my_account_repos.html:1
 msgid "Repositories You Own"
-msgstr "Репозитории, где Вы — владелец"
+msgstr "Ваши репозитории"
 
 #: kallithea/templates/admin/my_account/my_account_repos.html:13
 #: kallithea/templates/admin/my_account/my_account_watched.html:13
@@ -2722,58 +2707,53 @@
 #: kallithea/templates/admin/my_account/my_account_ssh_keys.html:4
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:4
 msgid "Fingerprint"
-msgstr ""
+msgstr "Отпечаток"
 
 #: kallithea/templates/admin/my_account/my_account_ssh_keys.html:6
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:6
-#, fuzzy
-#| msgid "Last Name"
 msgid "Last Used"
-msgstr "Фамилия"
+msgstr "Использовался в предыдущий раз"
 
 #: kallithea/templates/admin/my_account/my_account_ssh_keys.html:28
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:28
-#, fuzzy, python-format
+#, python-format
 msgid "Confirm to remove this SSH key: %s"
-msgstr "Подтвердите удаление этого API-ключа: %s"
+msgstr "Подтвердите удаление этого ключа SSH: %s"
 
 #: kallithea/templates/admin/my_account/my_account_ssh_keys.html:39
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:39
 msgid "No SSH keys have been added"
-msgstr ""
+msgstr "Ключи SSH не были добавлены"
 
 #: kallithea/templates/admin/my_account/my_account_ssh_keys.html:49
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:49
-#, fuzzy
 msgid "New SSH key"
-msgstr "Ключ"
+msgstr "Новый ключ SSH"
 
 #: kallithea/templates/admin/my_account/my_account_ssh_keys.html:52
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:52
-#, fuzzy
-#| msgid "Public repository"
 msgid "Public key"
-msgstr "Публичный репозиторий"
+msgstr "Публичный ключ"
 
 #: kallithea/templates/admin/my_account/my_account_ssh_keys.html:54
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:54
 msgid "Public key (contents of e.g. ~/.ssh/id_rsa.pub)"
-msgstr ""
+msgstr "Публичный ключ (например, из файла ~/.ssh/id_rsa.pub)"
 
 #: kallithea/templates/admin/my_account/my_account_watched.html:1
 msgid "Repositories You are Watching"
-msgstr "Репозитории, за которыми Вы наблюдаете"
+msgstr "Репозитории, за которыми вы наблюдаете"
 
 #: kallithea/templates/admin/permissions/permissions.html:5
 #: kallithea/templates/admin/permissions/permissions.html:11
 #: kallithea/templates/base/base.html:60
 msgid "Default Permissions"
-msgstr "Стандартные привилегии"
+msgstr "Права по умолчанию"
 
 #: kallithea/templates/admin/permissions/permissions.html:28
 #: kallithea/templates/admin/settings/settings.html:29
 msgid "Global"
-msgstr ""
+msgstr "Глобальные"
 
 #: kallithea/templates/admin/permissions/permissions.html:29
 #: kallithea/templates/admin/users/user_edit.html:35
@@ -2785,10 +2765,8 @@
 msgstr "Анонимный доступ"
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:8
-#, fuzzy
-#| msgid "Anonymous access"
 msgid "Allow anonymous access"
-msgstr "Анонимный доступ"
+msgstr "Разрешить анонимный доступ"
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:10
 #, python-format
@@ -2796,6 +2774,8 @@
 "Allow access to Kallithea without needing to log in. Anonymous users use "
 "%s user permissions."
 msgstr ""
+"Разрешить доступ к Kallithea без авторизации. Анонимные пользователи "
+"будут использовать права доступа пользователя %s."
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:19
 msgid ""
@@ -2808,18 +2788,17 @@
 "будут сброшены"
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:20
-#, fuzzy
 msgid "Apply to all existing repositories"
-msgstr "Существующий репозиторий?"
+msgstr "Применить ко всем репозиториям"
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:23
 msgid "Permissions for the Default user on new repositories."
-msgstr ""
+msgstr "Права пользователя по умолчанию для новых репозиториев."
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:27
 #: kallithea/templates/admin/repos/repo_add_base.html:28
 #: kallithea/templates/admin/repos/repo_edit_settings.html:33
-#: kallithea/templates/data_table/_dt_elements.html:134
+#: kallithea/templates/data_table/_dt_elements.html:140
 #: kallithea/templates/forks/fork.html:42
 msgid "Repository group"
 msgstr "Группа репозиториев"
@@ -2835,21 +2814,19 @@
 "групп репозиториев будут сброшены"
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:33
-#, fuzzy
 msgid "Apply to all existing repository groups"
-msgstr "Удалить эту группу репозиториев"
+msgstr "Применить ко всем группам репозиториев"
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:36
 msgid "Permissions for the Default user on new repository groups."
-msgstr ""
+msgstr "Права пользователя по умолчанию для новых групп репозиториев."
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:40
-#: kallithea/templates/data_table/_dt_elements.html:141
+#: kallithea/templates/data_table/_dt_elements.html:147
 msgid "User group"
 msgstr "Группа пользователей"
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:45
-#, fuzzy
 msgid ""
 "All default permissions on each user group will be reset to chosen "
 "permission, note that all custom default permission on user groups will "
@@ -2861,31 +2838,34 @@
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:46
 msgid "Apply to all existing user groups"
-msgstr ""
+msgstr "Применить ко всем группам пользователей"
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:49
 msgid "Permissions for the Default user on new user groups."
-msgstr ""
+msgstr "Права пользователя по умолчанию для новых групп пользователей."
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:53
-#, fuzzy
 msgid "Top level repository creation"
-msgstr "Создание репозитория"
+msgstr "Создание репозитория верхнего уровня"
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:56
 msgid ""
 "Enable this to allow non-admins to create repositories at the top level."
 msgstr ""
+"Включите, чтобы разрешить всем пользователям создавать репозитории на "
+"верхнем уровне."
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:57
 msgid ""
 "Note: This will also give all users API access to create repositories "
 "everywhere. That might change in future versions."
 msgstr ""
+"Внимание: это также позволит всем пользователям с помощью API создавать "
+"репозитории где угодно. Это может измениться в будущих версиях."
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:61
 msgid "Repository creation with group write access"
-msgstr ""
+msgstr "Создание репозитория с правом записи в группы"
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:64
 msgid ""
@@ -2893,6 +2873,9 @@
 "repositories inside that group. Without this, group write permissions "
 "mean nothing."
 msgstr ""
+"С этой опцией, право записи в группу репозиториев позволяет создавать "
+"репозитории в этой группе. Без неё, право записи в группу не имеет "
+"действия."
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:68
 msgid "User group creation"
@@ -2901,6 +2884,8 @@
 #: kallithea/templates/admin/permissions/permissions_globals.html:71
 msgid "Enable this to allow non-admins to create user groups."
 msgstr ""
+"Включите для возможности создавать группы пользователей любым "
+"пользователям."
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:75
 msgid "Repository forking"
@@ -2909,6 +2894,7 @@
 #: kallithea/templates/admin/permissions/permissions_globals.html:78
 msgid "Enable this to allow non-admins to fork repositories."
 msgstr ""
+"Включите для возможности создавать форки репозиториев любым пользователем."
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:82
 msgid "Registration"
@@ -2920,9 +2906,9 @@
 
 #: kallithea/templates/admin/permissions/permissions_ips.html:12
 #: kallithea/templates/admin/users/user_edit_ips.html:22
-#, fuzzy, python-format
+#, python-format
 msgid "Confirm to delete this IP address: %s"
-msgstr "Подтвердите удаление IP %s"
+msgstr "Подтвердите удаление IP-адреса: %s"
 
 #: kallithea/templates/admin/permissions/permissions_ips.html:18
 #: kallithea/templates/admin/users/user_edit_ips.html:29
@@ -2958,12 +2944,12 @@
 #: kallithea/templates/admin/repo_groups/repo_group_add.html:49
 #: kallithea/templates/admin/repos/repo_add_base.html:35
 msgid "Copy parent group permissions"
-msgstr ""
+msgstr "Скопировать родительские права доступа"
 
 #: kallithea/templates/admin/repo_groups/repo_group_add.html:52
 #: kallithea/templates/admin/repos/repo_add_base.html:38
 msgid "Copy permission set from parent repository group."
-msgstr ""
+msgstr "Скопировать набор прав доступа из родительской группы репозиториев."
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit.html:5
 #, python-format
@@ -2989,13 +2975,13 @@
 #: kallithea/templates/admin/user_groups/user_group_edit.html:30
 #: kallithea/templates/admin/users/user_edit.html:36
 msgid "Advanced"
-msgstr "Дополнительно"
+msgstr "Продвинутые"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit.html:38
 #: kallithea/templates/admin/repos/repo_edit.html:28
 #: kallithea/templates/admin/user_groups/user_group_edit.html:31
 msgid "Permissions"
-msgstr "Привилегии"
+msgstr "Права доступа"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:1
 #, python-format
@@ -3004,15 +2990,15 @@
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:6
 msgid "Top level repositories"
-msgstr ""
+msgstr "Репозитории верхнего уровня"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:7
 msgid "Total repositories"
-msgstr ""
+msgstr "Всего репозиториев"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:8
 msgid "Children groups"
-msgstr ""
+msgstr "Дочерние группы"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:9
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:7
@@ -3022,7 +3008,7 @@
 msgstr "Создано"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:121
+#: kallithea/templates/data_table/_dt_elements.html:127
 #, python-format
 msgid "Confirm to delete this group: %s with %s repository"
 msgid_plural "Confirm to delete this group: %s with %s repositories"
@@ -3032,36 +3018,29 @@
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:25
 msgid "Delete this repository group"
-msgstr "Удалить эту группу репозиториев"
+msgstr "Удалить группу репозиториев"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:7
 msgid "Not visible"
-msgstr ""
+msgstr "Невидимый"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:8
-#, fuzzy
-#| msgid "disabled"
 msgid "Visible"
-msgstr "отключено"
+msgstr "Видимый"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:9
-#, fuzzy
-#| msgid "Add Repository"
 msgid "Add repos"
-msgstr "Добавить репозиторий"
+msgstr "Добавлять репозитории"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:10
-#, fuzzy
-#| msgid "Add user group"
 msgid "Add/Edit groups"
-msgstr "Добавить группу пользователей"
+msgstr "Добавлять/Редактировать группы"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:11
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:11
 #: kallithea/templates/admin/user_groups/user_group_edit_perms.html:11
-#, fuzzy
 msgid "User/User Group"
-msgstr "Группа пользователей"
+msgstr "Пользователь/Группа"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:28
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:45
@@ -3069,9 +3048,8 @@
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:36
 #: kallithea/templates/admin/user_groups/user_group_edit_perms.html:28
 #: kallithea/templates/admin/user_groups/user_group_edit_perms.html:45
-#, fuzzy
 msgid "Default"
-msgstr "по умолчанию"
+msgstr "По умолчанию"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:34
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:71
@@ -3079,9 +3057,8 @@
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:67
 #: kallithea/templates/admin/user_groups/user_group_edit_perms.html:34
 #: kallithea/templates/admin/user_groups/user_group_edit_perms.html:71
-#, fuzzy
 msgid "Revoke"
-msgstr "отозвать"
+msgstr "Отозвать"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:81
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:77
@@ -3090,33 +3067,33 @@
 msgstr "Добавить новый"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:87
-#, fuzzy
 msgid "Apply to children"
-msgstr "применить к дочерним"
+msgstr "Применить к дочерним"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:91
 msgid "Both"
-msgstr ""
+msgstr "Все"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:92
 msgid ""
 "Set or revoke permission to all children of that group, including non-"
 "private repositories and other groups if selected."
 msgstr ""
+"Установить или отозвать права всех дочерних элементов этой группы, "
+"включая публичные репозитории и другие группы, если они выбраны."
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_settings.html:38
 msgid "Remove this group"
-msgstr "Удалить эту группу"
+msgstr "Удалить группу"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_settings.html:38
 msgid "Confirm to delete this group"
 msgstr "Подтвердите удаление этой группы пользователей"
 
 #: kallithea/templates/admin/repo_groups/repo_group_show.html:4
-#, fuzzy, python-format
-#| msgid "Repository Group: %s"
+#, python-format
 msgid "Repository group %s"
-msgstr "Группа репозиториев: %s"
+msgstr "Группа репозиториев %s"
 
 #: kallithea/templates/admin/repo_groups/repo_groups.html:5
 msgid "Repository Groups Administration"
@@ -3127,15 +3104,16 @@
 msgstr "Число репозиториев верхнего уровня"
 
 #: kallithea/templates/admin/repos/repo_add_base.html:12
-#, fuzzy
 msgid "Clone remote repository"
-msgstr "[создан] репозиторий"
+msgstr "Клонировать удалённый репозиторий"
 
 #: kallithea/templates/admin/repos/repo_add_base.html:16
 msgid ""
 "Optional: URL of a remote repository. If set, the repository will be "
 "created as a clone from this URL."
 msgstr ""
+"Опционально: URL удалённого репозитория. Если параметр задан, то будет "
+"создан клон репозитория, расположенного по этому адресу."
 
 #: kallithea/templates/admin/repos/repo_add_base.html:24
 #: kallithea/templates/admin/repos/repo_edit_settings.html:57
@@ -3166,6 +3144,8 @@
 "Default revision for files page, downloads, full text search index and "
 "readme generation"
 msgstr ""
+"Ревизия по умолчанию для страницы файлов, загрузки, полнотекстовый "
+"поисковый индекс и генерация readme"
 
 #: kallithea/templates/admin/repos/repo_creating.html:9
 #, python-format
@@ -3174,7 +3154,7 @@
 
 #: kallithea/templates/admin/repos/repo_creating.html:13
 msgid "Creating repository"
-msgstr ""
+msgstr "Создание репозитория"
 
 #: kallithea/templates/admin/repos/repo_creating.html:27
 #, python-format
@@ -3182,12 +3162,16 @@
 "Repository \"%(repo_name)s\" is being created, you will be redirected "
 "when this process is finished.repo_name"
 msgstr ""
+"Репозиторий «%(repo_name)s» создаётся. Вы будете перенаправлены, когда "
+"процесс завершится."
 
 #: kallithea/templates/admin/repos/repo_creating.html:39
 msgid ""
 "We're sorry but error occurred during this operation. Please check your "
 "Kallithea server logs, or contact administrator."
 msgstr ""
+"К сожалению, во время данной операции произошла ошибка. Пожалуйста, "
+"проверьте журнал сервера Kallithea или свяжитесь с администратором."
 
 #: kallithea/templates/admin/repos/repo_edit.html:8
 #, python-format
@@ -3199,14 +3183,10 @@
 msgstr "Дополнительные поля"
 
 #: kallithea/templates/admin/repos/repo_edit.html:37
-msgid "Caches"
-msgstr ""
+msgid "Remote"
+msgstr "Удалённый репозиторий"
 
 #: kallithea/templates/admin/repos/repo_edit.html:40
-msgid "Remote"
-msgstr "Удалённый"
-
-#: kallithea/templates/admin/repos/repo_edit.html:43
 #: kallithea/templates/summary/statistics.html:8
 #: kallithea/templates/summary/summary.html:169
 #: kallithea/templates/summary/summary.html:170
@@ -3223,7 +3203,7 @@
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:7
 msgid "Manually set this repository as a fork of another from the list."
-msgstr "Вручную сделать этот репозиторий форком выбранного из списка."
+msgstr "Вручную задать этот репозиторий форком репозитория из этого списка."
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:20
 msgid "Public Journal Visibility"
@@ -3246,7 +3226,7 @@
 "публичном журнале."
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:46
-#: kallithea/templates/data_table/_dt_elements.html:68
+#: kallithea/templates/data_table/_dt_elements.html:74
 #, python-format
 msgid "Confirm to delete this repository: %s"
 msgstr "Подтвердите удаление этого репозитория: %s"
@@ -3259,17 +3239,17 @@
 #, python-format
 msgid "This repository has %s fork"
 msgid_plural "This repository has %s forks"
-msgstr[0] "Данный репозиторий имеет %s копию"
-msgstr[1] "Данный репозиторий имеет %s копии"
-msgstr[2] "Данный репозиторий имеет %s копий"
+msgstr[0] "Данный репозиторий имеет %s форк"
+msgstr[1] "Данный репозиторий имеет %s форка"
+msgstr[2] "Данный репозиторий имеет %s форков"
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:54
 msgid "Detach forks"
-msgstr "Отсоединить fork'и"
+msgstr "Отделить форки"
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:58
 msgid "Delete forks"
-msgstr "Удалить fork'и"
+msgstr "Удалить форки"
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:62
 msgid ""
@@ -3277,45 +3257,18 @@
 "administrator expires it. The administrator can both permanently delete "
 "it or restore it."
 msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:4
-msgid "Invalidate Repository Cache"
-msgstr "Сбросить кэш репозитория"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:6
-msgid ""
-"Manually invalidate cache for this repository. On first access, the "
-"repository will be cached again."
-msgstr ""
-"Ручной сброс кэша репозитория. При первом доступе кэш восстановится."
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:9
-msgid "List of Cached Values"
-msgstr "Список кешированных значений"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:12
-msgid "Prefix"
-msgstr "Префикс"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:13
+"Удаляемый репозиторий будет перемещён и скрыт на срок, определяемый "
+"администратором. Администратор может либо удалить, либо восстановить "
+"репозиторий."
+
+#: kallithea/templates/admin/repos/repo_edit_fields.html:6
+msgid "Label"
+msgstr "Имя"
+
 #: kallithea/templates/admin/repos/repo_edit_fields.html:7
 msgid "Key"
 msgstr "Ключ"
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:14
-#: kallithea/templates/admin/user_groups/user_group_add.html:40
-#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
-#: kallithea/templates/admin/user_groups/user_groups.html:41
-#: kallithea/templates/admin/users/user_add.html:69
-#: kallithea/templates/admin/users/user_edit_profile.html:74
-#: kallithea/templates/admin/users/users.html:42
-msgid "Active"
-msgstr "Активный"
-
-#: kallithea/templates/admin/repos/repo_edit_fields.html:6
-msgid "Label"
-msgstr ""
-
 #: kallithea/templates/admin/repos/repo_edit_fields.html:20
 #, python-format
 msgid "Confirm to delete this field: %s"
@@ -3346,39 +3299,32 @@
 msgstr "Дополнительные поля отключены."
 
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:20
-#, fuzzy
 msgid "Private Repository"
-msgstr "приватный репозиторий"
+msgstr "Приватный репозиторий"
 
 #: kallithea/templates/admin/repos/repo_edit_remote.html:4
-#, fuzzy
-#| msgid "[forked] repository"
 msgid "Fork of repository"
-msgstr "[форкнут] репозиторий"
+msgstr "Форк репозитория"
 
 #: kallithea/templates/admin/repos/repo_edit_remote.html:7
-#, fuzzy
 msgid "Remote repository URL"
-msgstr "Репозиторий %s создан"
+msgstr "Ссылка на удалённый репозиторий"
 
 #: kallithea/templates/admin/repos/repo_edit_remote.html:15
-#, fuzzy
 msgid "Pull Changes from Remote Repository"
-msgstr "[внесены изменения из удалённого репозитория] в репозиторий"
+msgstr "Изменения из удалённого репозитория"
 
 #: kallithea/templates/admin/repos/repo_edit_remote.html:17
-#, fuzzy
 msgid "Confirm to pull changes from remote repository."
-msgstr "Подтвердите скачивание изменений."
+msgstr "Подтвердите применение изменений из удалённого репозитория."
 
 #: kallithea/templates/admin/repos/repo_edit_remote.html:23
 msgid "This repository does not have a remote repository URL."
-msgstr ""
+msgstr "Данный репозиторий не имеет URL удалённого репозитория."
 
 #: kallithea/templates/admin/repos/repo_edit_settings.html:10
-#, fuzzy
 msgid "Permanent URL"
-msgstr "приватный репозиторий"
+msgstr "Постоянный URL"
 
 #: kallithea/templates/admin/repos/repo_edit_settings.html:14
 msgid ""
@@ -3389,22 +3335,28 @@
 "                               This is useful for CI systems, or any "
 "other cases that you need to hardcode the URL into a 3rd party service."
 msgstr ""
+"В случае, когда репозиторий переименовывается или перемещается в другую "
+"группу, URL репозитория изменяется.\n"
+"                               Использование постоянного URL гарантирует, "
+"что данный репозиторий всегда будет доступен по этому URL.\n"
+"                               Это может быть полезно в CI-системах, или "
+"в любом другом случае, требующем встраивания URL в код ПО."
 
 #: kallithea/templates/admin/repos/repo_edit_settings.html:21
-#, fuzzy
 msgid "Remote repository"
-msgstr "[создан] репозиторий"
+msgstr "Удалённый репозиторий"
 
 #: kallithea/templates/admin/repos/repo_edit_settings.html:24
-#, fuzzy
 msgid "Repository URL"
-msgstr "Репозиторий"
+msgstr "URL репозитория"
 
 #: kallithea/templates/admin/repos/repo_edit_settings.html:28
 msgid ""
 "Optional: URL of a remote repository. If set, the repository can be "
 "pulled from this URL."
 msgstr ""
+"Опционально: URL удалённого репозитория. Если задан, то репозиторий можно "
+"получить по заданному адресу."
 
 #: kallithea/templates/admin/repos/repo_edit_settings.html:43
 msgid "Default revision for files page, downloads, whoosh and readme"
@@ -3415,7 +3367,7 @@
 #: kallithea/templates/admin/repos/repo_edit_settings.html:49
 #: kallithea/templates/pullrequests/pullrequest_show.html:131
 msgid "Type name of user"
-msgstr ""
+msgstr "Введите имя пользователя"
 
 #: kallithea/templates/admin/repos/repo_edit_settings.html:50
 msgid "Change owner of this repository."
@@ -3423,11 +3375,11 @@
 
 #: kallithea/templates/admin/repos/repo_edit_statistics.html:5
 msgid "Processed commits"
-msgstr ""
+msgstr "Обработанные фиксации"
 
 #: kallithea/templates/admin/repos/repo_edit_statistics.html:6
 msgid "Processed progress"
-msgstr ""
+msgstr "Обработанный прогресс"
 
 #: kallithea/templates/admin/repos/repo_edit_statistics.html:10
 msgid "Reset Statistics"
@@ -3443,7 +3395,7 @@
 
 #: kallithea/templates/admin/repos/repos.html:43
 msgid "State"
-msgstr ""
+msgstr "Состояние"
 
 #: kallithea/templates/admin/settings/settings.html:5
 msgid "Settings Administration"
@@ -3451,15 +3403,15 @@
 
 #: kallithea/templates/admin/settings/settings.html:27
 msgid "VCS"
-msgstr ""
+msgstr "Контроль версий"
 
 #: kallithea/templates/admin/settings/settings.html:28
 msgid "Remap and Rescan"
-msgstr ""
+msgstr "Пересканирование"
 
 #: kallithea/templates/admin/settings/settings.html:30
 msgid "Visual"
-msgstr ""
+msgstr "Вид"
 
 #: kallithea/templates/admin/settings/settings.html:32
 #: kallithea/templates/admin/settings/settings_vcs.html:4
@@ -3468,15 +3420,15 @@
 
 #: kallithea/templates/admin/settings/settings.html:33
 msgid "Full Text Search"
-msgstr ""
+msgstr "Полнотекстовый поиск"
 
 #: kallithea/templates/admin/settings/settings.html:34
 msgid "System Info"
-msgstr ""
+msgstr "О системе"
 
 #: kallithea/templates/admin/settings/settings_email.html:4
 msgid "Send test email to"
-msgstr ""
+msgstr "Отправлять пробное сообщение на адрес"
 
 #: kallithea/templates/admin/settings/settings_email.html:12
 msgid "Send"
@@ -3484,11 +3436,11 @@
 
 #: kallithea/templates/admin/settings/settings_global.html:4
 msgid "Site branding"
-msgstr "Брендинг сайта"
+msgstr "Заголовок сайта"
 
 #: kallithea/templates/admin/settings/settings_global.html:7
 msgid "Set a custom title for your Kallithea Service."
-msgstr ""
+msgstr "Задать другое имя для Kallithea Service."
 
 #: kallithea/templates/admin/settings/settings_global.html:12
 msgid "HTTP authentication realm"
@@ -3496,7 +3448,7 @@
 
 #: kallithea/templates/admin/settings/settings_global.html:19
 msgid "HTML/JavaScript/CSS customization block"
-msgstr ""
+msgstr "Блок редактирования HTML/JavaScript/CSS"
 
 #: kallithea/templates/admin/settings/settings_global.html:22
 msgid ""
@@ -3506,24 +3458,32 @@
 "to                         perform instance-specific customizations like "
 "adding a                         project banner at the top of every page."
 msgstr ""
+"Код HTML (можно с                         JavaScript и/или CSS), который "
+"будет добавлен внизу                         каждой страницы. Может "
+"использоваться для размещения                         веб-аналитики, но "
+"также                         и для создания индивидуальных "
+"модификаций,                         например, для размещения баннера "
+"проекта                         на каждой странице."
 
 #: kallithea/templates/admin/settings/settings_global.html:32
 msgid "ReCaptcha public key"
-msgstr ""
+msgstr "Открытый ключ reCaptcha"
 
 #: kallithea/templates/admin/settings/settings_global.html:35
 msgid "Public key for reCaptcha system."
-msgstr ""
+msgstr "Открытый ключ системы reCaptcha."
 
 #: kallithea/templates/admin/settings/settings_global.html:40
 msgid "ReCaptcha private key"
-msgstr ""
+msgstr "Закрытый ключ reCaptcha"
 
 #: kallithea/templates/admin/settings/settings_global.html:43
 msgid ""
 "Private key for reCaptcha system. Setting this value will enable captcha "
 "on registration."
 msgstr ""
+"Закрытый ключ системы reCaptcha. Задание этого значения включит капчу при "
+"регистрации."
 
 #: kallithea/templates/admin/settings/settings_global.html:49
 #: kallithea/templates/admin/settings/settings_vcs.html:65
@@ -3533,7 +3493,7 @@
 
 #: kallithea/templates/admin/settings/settings_hooks.html:3
 msgid "Built-in Mercurial Hooks (Read-Only)"
-msgstr ""
+msgstr "Встроенные хуки Mercurial (только чтение)"
 
 #: kallithea/templates/admin/settings/settings_hooks.html:17
 msgid "Custom Hooks"
@@ -3544,27 +3504,30 @@
 "Hooks can be used to trigger actions on certain events such as push / "
 "pull. They can trigger Python functions or external applications."
 msgstr ""
+"Хуки используются для активации действий при определённых событиях, "
+"например, push/pull-запросах. Могут активироваться функции Python либо "
+"внешние приложения."
 
 #: kallithea/templates/admin/settings/settings_hooks.html:60
 msgid "Failed to remove hook"
 msgstr "Не удалось удалить хук"
 
 #: kallithea/templates/admin/settings/settings_mapping.html:4
-#, fuzzy
-#| msgid "Rescan option"
 msgid "Rescan options"
 msgstr "Опции пересканирования"
 
 #: kallithea/templates/admin/settings/settings_mapping.html:9
-#, fuzzy
 msgid "Delete records of missing repositories"
-msgstr "Поиск по репозиториям"
+msgstr "Удалить записи об отсутствующих репозиториях"
 
 #: kallithea/templates/admin/settings/settings_mapping.html:12
 msgid ""
 "Check this option to remove all comments, pull requests and other records "
 "related to repositories that no longer exist in the filesystem."
 msgstr ""
+"Отметьте для удаления всех комментариев, pull-запросов и других записей, "
+"связанных с репозиториями, которые больше не существуют в файловой "
+"системе."
 
 #: kallithea/templates/admin/settings/settings_mapping.html:17
 msgid "Invalidate cache for all repositories"
@@ -3572,21 +3535,25 @@
 
 #: kallithea/templates/admin/settings/settings_mapping.html:20
 msgid "Check this to reload data and clear cache keys for all repositories."
-msgstr "Сбросить кэш для всех репозиториев."
+msgstr ""
+"Отметьте, чтобы перезагрузить данные и очистить ключи кэша у всех "
+"репозиториев."
 
 #: kallithea/templates/admin/settings/settings_mapping.html:25
 msgid "Install Git hooks"
-msgstr ""
+msgstr "Установить хуки Git"
 
 #: kallithea/templates/admin/settings/settings_mapping.html:28
 msgid ""
 "Verify if Kallithea's Git hooks are installed for each repository. "
 "Current hooks will be updated to the latest version."
 msgstr ""
+"Проверяет установку Git хуков от Kallithea у каждого репозитория. Текущие "
+"хуки будут обновлены до последней версии."
 
 #: kallithea/templates/admin/settings/settings_mapping.html:32
 msgid "Overwrite existing Git hooks"
-msgstr ""
+msgstr "Перезаписать существующие хуки"
 
 #: kallithea/templates/admin/settings/settings_mapping.html:35
 msgid ""
@@ -3594,10 +3561,13 @@
 "not seem to come from Kallithea. WARNING: This operation will destroy any "
 "custom git hooks you may have deployed by hand!"
 msgstr ""
+"Перезаписывает все существующие хуки при установке хуков Git, даже если "
+"они не поставляются с Kallithea. ПРЕДУПРЕЖДЕНИЕ: это действие уничтожит "
+"любые Git хуки, которые могли быть созданы вручную!"
 
 #: kallithea/templates/admin/settings/settings_mapping.html:41
 msgid "Rescan Repositories"
-msgstr ""
+msgstr "Пересканировать репозитории"
 
 #: kallithea/templates/admin/settings/settings_search.html:4
 msgid "Index build option"
@@ -3605,13 +3575,15 @@
 
 #: kallithea/templates/admin/settings/settings_search.html:9
 msgid "Build from scratch"
-msgstr "Сборка с нуля"
+msgstr "Пересобрать"
 
 #: kallithea/templates/admin/settings/settings_search.html:12
 msgid ""
 "This option completely reindexeses all of the repositories for proper "
 "fulltext search capabilities."
 msgstr ""
+"Эта опция полностью переиндексирует все репозитории для корректной работы "
+"полнотекстового поиска."
 
 #: kallithea/templates/admin/settings/settings_search.html:18
 msgid "Reindex"
@@ -3619,23 +3591,23 @@
 
 #: kallithea/templates/admin/settings/settings_system.html:2
 msgid "Checking for updates..."
-msgstr ""
+msgstr "Поиск обновлений..."
 
 #: kallithea/templates/admin/settings/settings_system.html:7
 msgid "Kallithea version"
-msgstr ""
+msgstr "Версия Kallithea"
 
 #: kallithea/templates/admin/settings/settings_system.html:8
 msgid "Kallithea configuration file"
-msgstr ""
+msgstr "Конфиг. Kallithea"
 
 #: kallithea/templates/admin/settings/settings_system.html:9
 msgid "Python version"
-msgstr ""
+msgstr "Версия Python"
 
 #: kallithea/templates/admin/settings/settings_system.html:10
 msgid "Platform"
-msgstr ""
+msgstr "Платформа"
 
 #: kallithea/templates/admin/settings/settings_system.html:11
 msgid "Git version"
@@ -3643,11 +3615,11 @@
 
 #: kallithea/templates/admin/settings/settings_system.html:12
 msgid "Git path"
-msgstr ""
+msgstr "Путь к Git"
 
 #: kallithea/templates/admin/settings/settings_system.html:22
 msgid "Python Packages"
-msgstr ""
+msgstr "Пакеты Python"
 
 #: kallithea/templates/admin/settings/settings_vcs.html:9
 msgid "Show repository size after push"
@@ -3674,6 +3646,8 @@
 "Requires hgsubversion library to be installed. Enables cloning of remote "
 "Subversion repositories while converting them to Mercurial."
 msgstr ""
+"Требует наличия библиотеки hgsubversion. Включает клонирование удалённых "
+"репозиториев Subversion с последующим конвертированием в Mercurial."
 
 #: kallithea/templates/admin/settings/settings_vcs.html:47
 msgid "Location of repositories"
@@ -3692,6 +3666,8 @@
 "Filesystem location where repositories are stored. After changing this "
 "value, a restart and rescan of the repository folder are both required."
 msgstr ""
+"Путь к репозиториям в файловой системе. После изменения значения "
+"требуется перезапуск и пересканирование папки с репозиториями."
 
 #: kallithea/templates/admin/settings/settings_visual.html:4
 msgid "General"
@@ -3712,11 +3688,11 @@
 #: kallithea/templates/admin/settings/settings_visual.html:20
 msgid ""
 "Shows or hides a version number of Kallithea displayed in the footer."
-msgstr ""
+msgstr "Показывает или скрывает версию Kallithea внизу страницы."
 
 #: kallithea/templates/admin/settings/settings_visual.html:25
 msgid "Show user Gravatars"
-msgstr ""
+msgstr "Отображать Gravatars пользователя"
 
 #: kallithea/templates/admin/settings/settings_visual.html:29
 msgid ""
@@ -3734,12 +3710,23 @@
 "                                                        {netloc}    "
 "network location/server host of running Kallithea server"
 msgstr ""
+"Поле Gravatar URL позволяет использовать любой другой сервис аватаров.\n"
+"                                                        В URL можно "
+"использовать следующие переменные:\n"
+"                                                        {scheme}    "
+"используемый протокол, 'http' или 'https',\n"
+"                                                        {email}     e-"
+"mail пользователя,\n"
+"                                                        {md5email}  хэш "
+"md5 адреса почты пользователя (как на gravatar.com),\n"
+"                                                        {size}      "
+"ожидаемый размер изображения,\n"
+"                                                        {netloc}    "
+"сетевой путь/адрес хоста сервера Kallithea"
 
 #: kallithea/templates/admin/settings/settings_visual.html:40
-#, fuzzy
-#| msgid "Clone URL"
 msgid "HTTP Clone URL"
-msgstr "Ссылка для клонирования"
+msgstr "Ссылка для клонирования по HTTP"
 
 #: kallithea/templates/admin/settings/settings_visual.html:43
 msgid ""
@@ -3763,40 +3750,61 @@
 "hostname\n"
 "                                                    "
 msgstr ""
+"Схема URL для клонирования, например: '{scheme}://{user}@{netloc}/"
+"{repo}'.\n"
+"                                                    Доступны следующие "
+"переменные:\n"
+"                                                    {scheme} используемый "
+"протокол, 'http' or 'https',\n"
+"                                                    {user}   имя текущего "
+"пользователя,\n"
+"                                                    {netloc} сетевой путь/"
+"адрес хоста сервера Kallithea,\n"
+"                                                    {repo}   полное имя "
+"репозитория,\n"
+"                                                    {repoid} ID "
+"репозитория, может применяться для клонирования по идентификатору,\n"
+"                                                    {system_user}  имя "
+"пользователя Kallithea в системе,\n"
+"                                                    {hostname}  имя хоста "
+"севера\n"
+"                                                    "
 
 #: kallithea/templates/admin/settings/settings_visual.html:56
-#, fuzzy
-#| msgid "Clone URL"
 msgid "SSH Clone URL"
-msgstr "Ссылка для клонирования"
+msgstr "Ссылка для клонирования по SSH"
 
 #: kallithea/templates/admin/settings/settings_visual.html:59
 msgid ""
 "Schema for constructing SSH clone URL, eg. 'ssh://{system_user}"
 "@{hostname}/{repo}'."
 msgstr ""
+"Схема URL для клонирования по SSH, например: 'ssh://{system_user}"
+"@{hostname}/{repo}'."
 
 #: kallithea/templates/admin/settings/settings_visual.html:67
-#, fuzzy
-#| msgid "Repository Size"
 msgid "Repository page size"
-msgstr "Размер репозитория"
+msgstr "Размер страницы репозитория"
 
 #: kallithea/templates/admin/settings/settings_visual.html:70
 msgid ""
 "Number of items displayed in the repository pages before pagination is "
 "shown."
 msgstr ""
+"Количество элементов на странице репозитория до появления нумерации "
+"страниц."
 
 #: kallithea/templates/admin/settings/settings_visual.html:75
 msgid "Admin page size"
-msgstr ""
+msgstr "Размер страницы администратора"
 
 #: kallithea/templates/admin/settings/settings_visual.html:78
 msgid ""
 "Number of items displayed in the admin pages grids before pagination is "
 "shown."
 msgstr ""
+"Количество элементов в сетке страницы администратора до появления "
+"нумерации страниц."
 
 #: kallithea/templates/admin/settings/settings_visual.html:83
 msgid "Icons"
@@ -3815,7 +3823,6 @@
 msgstr "Показывать иконки публичных репозиториев."
 
 #: kallithea/templates/admin/settings/settings_visual.html:102
-#, fuzzy
 msgid "Meta Tagging"
 msgstr "Метатегирование"
 
@@ -3824,10 +3831,12 @@
 "Parses meta tags from the repository description field and turns them "
 "into colored tags."
 msgstr ""
+"Анализирует мета-теги в поле описания репозитория и отображает их в виде "
+"цветных тегов."
 
 #: kallithea/templates/admin/settings/settings_visual.html:111
 msgid "Stylify recognised meta tags:"
-msgstr ""
+msgstr "Стилизовать обнаруженные мета-теги:"
 
 #: kallithea/templates/admin/user_groups/user_group_add.html:5
 msgid "Add user group"
@@ -3839,32 +3848,40 @@
 #: kallithea/templates/base/base.html:59
 #: kallithea/templates/base/base.html:79
 msgid "User Groups"
-msgstr ""
+msgstr "Группы пользователей"
 
 #: kallithea/templates/admin/user_groups/user_group_add.html:12
 #: kallithea/templates/admin/user_groups/user_groups.html:24
 msgid "Add User Group"
-msgstr ""
+msgstr "Добавить группу пользователей"
 
 #: kallithea/templates/admin/user_groups/user_group_add.html:36
 #: kallithea/templates/admin/user_groups/user_group_edit_settings.html:13
 msgid "Short, optional description for this user group."
-msgstr ""
+msgstr "Краткое, опциональное описание этой группы."
+
+#: kallithea/templates/admin/user_groups/user_group_add.html:40
+#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
+#: kallithea/templates/admin/user_groups/user_groups.html:41
+#: kallithea/templates/admin/users/user_add.html:69
+#: kallithea/templates/admin/users/user_edit_profile.html:74
+#: kallithea/templates/admin/users/users.html:42
+msgid "Active"
+msgstr "Активный"
 
 #: kallithea/templates/admin/user_groups/user_group_edit.html:5
 #, python-format
 msgid "%s user group settings"
-msgstr ""
+msgstr "Настройки группы %s"
 
 #: kallithea/templates/admin/user_groups/user_group_edit.html:33
-#, fuzzy
 msgid "Show Members"
-msgstr "участники"
+msgstr "Участники"
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:1
 #, python-format
 msgid "User Group: %s"
-msgstr ""
+msgstr "Группа пользователей: %s"
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:6
 #: kallithea/templates/admin/user_groups/user_group_edit_settings.html:23
@@ -3873,14 +3890,14 @@
 msgstr "Участники"
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:19
-#: kallithea/templates/data_table/_dt_elements.html:102
+#: kallithea/templates/data_table/_dt_elements.html:108
 #, python-format
 msgid "Confirm to delete this user group: %s"
 msgstr "Подтвердите удаление следующей группы пользователей: %s"
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:21
 msgid "Delete this user group"
-msgstr ""
+msgstr "Удалить группу"
 
 #: kallithea/templates/admin/user_groups/user_group_edit_members.html:11
 msgid "No members yet"
@@ -3912,7 +3929,7 @@
 #: kallithea/templates/admin/users/user_add.html:12
 #: kallithea/templates/admin/users/users.html:23
 msgid "Add User"
-msgstr ""
+msgstr "Добавить пользователя"
 
 #: kallithea/templates/admin/users/user_add.html:41
 msgid "Password confirmation"
@@ -3921,49 +3938,49 @@
 #: kallithea/templates/admin/users/user_edit.html:5
 #, python-format
 msgid "%s user settings"
-msgstr ""
+msgstr "Настройки пользователя %s"
 
 #: kallithea/templates/admin/users/user_edit.html:30
 msgid "Emails"
-msgstr ""
+msgstr "Электронная почта"
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:1
 #, python-format
 msgid "User: %s"
-msgstr ""
+msgstr "Пользователь: %s"
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:7
 #: kallithea/templates/admin/users/user_edit_profile.html:32
 msgid "Source of Record"
-msgstr ""
+msgstr "Источник записи"
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:9
 #: kallithea/templates/admin/users/users.html:41
 msgid "Last Login"
-msgstr ""
+msgstr "Последний вход"
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:10
 msgid "Member of User Groups"
-msgstr ""
+msgstr "Член группы"
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:90
+#: kallithea/templates/data_table/_dt_elements.html:96
 #, python-format
 msgid "Confirm to delete this user: %s"
 msgstr "Подтвердите удаление пользователя %s"
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:23
 msgid "Delete this user"
-msgstr ""
+msgstr "Удалить пользователя"
 
 #: kallithea/templates/admin/users/user_edit_ips.html:7
 #, python-format
 msgid "Inherited from %s"
-msgstr ""
+msgstr "Унаследовано от %s"
 
 #: kallithea/templates/admin/users/user_edit_profile.html:39
 msgid "Name in Source of Record"
-msgstr ""
+msgstr "Имя в источнике записи"
 
 #: kallithea/templates/admin/users/user_edit_profile.html:53
 msgid "New password confirmation"
@@ -3975,7 +3992,7 @@
 
 #: kallithea/templates/admin/users/users.html:44
 msgid "Auth Type"
-msgstr ""
+msgstr "Тип авторизации"
 
 #: kallithea/templates/base/base.html:16
 #, python-format
@@ -3984,7 +4001,7 @@
 
 #: kallithea/templates/base/base.html:28
 msgid "Support"
-msgstr ""
+msgstr "Поддержка"
 
 #: kallithea/templates/base/base.html:86
 #: kallithea/templates/base/base.html:417
@@ -4033,7 +4050,7 @@
 #: kallithea/templates/base/base.html:156
 #: kallithea/templates/forks/forks_data.html:18
 msgid "Compare Fork"
-msgstr "Сравнить форк"
+msgstr "Сравнить форки"
 
 #: kallithea/templates/base/base.html:158
 msgid "Compare"
@@ -4047,12 +4064,14 @@
 msgstr "Поиск"
 
 #: kallithea/templates/base/base.html:167
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Follow"
-msgstr "Наблюдать"
+msgstr "Подписаться"
 
 #: kallithea/templates/base/base.html:168
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Unfollow"
-msgstr "Не наблюдать"
+msgstr "Отписаться"
 
 #: kallithea/templates/base/base.html:171
 #: kallithea/templates/forks/fork.html:9
@@ -4062,7 +4081,7 @@
 #: kallithea/templates/base/base.html:172
 #: kallithea/templates/pullrequests/pullrequest.html:77
 msgid "Create Pull Request"
-msgstr "Создать Pull запрос"
+msgstr "Создать pull-запрос"
 
 #: kallithea/templates/base/base.html:184
 msgid "Switch To"
@@ -4071,7 +4090,7 @@
 #: kallithea/templates/base/base.html:196
 #: kallithea/templates/base/base.html:445
 msgid "No matches found"
-msgstr ""
+msgstr "Совпадений не найдено"
 
 #: kallithea/templates/base/base.html:289
 msgid "Show recent activity"
@@ -4111,7 +4130,7 @@
 #: kallithea/templates/pullrequests/pullrequest_show_my.html:6
 #: kallithea/templates/pullrequests/pullrequest_show_my.html:10
 msgid "My Pull Requests"
-msgstr "Мои Pull-запросы"
+msgstr "Мои pull-запросы"
 
 #: kallithea/templates/base/base.html:353
 msgid "Not Logged In"
@@ -4122,14 +4141,10 @@
 msgstr "Авторизоваться"
 
 #: kallithea/templates/base/base.html:372
-#, fuzzy
-#| msgid "Forgot password ?"
 msgid "Forgot password?"
 msgstr "Забыли пароль?"
 
 #: kallithea/templates/base/base.html:376
-#, fuzzy
-#| msgid "Don't have an account ?"
 msgid "Don't have an account?"
 msgstr "Нет аккаунта?"
 
@@ -4139,11 +4154,11 @@
 
 #: kallithea/templates/base/base.html:517
 msgid "Parent rev."
-msgstr ""
+msgstr "Ревизия предка"
 
 #: kallithea/templates/base/base.html:526
 msgid "Child rev."
-msgstr ""
+msgstr "Ревизия потомка"
 
 #: kallithea/templates/base/default_perms_box.html:11
 msgid "Create repositories"
@@ -4163,12 +4178,12 @@
 
 #: kallithea/templates/base/default_perms_box.html:31
 msgid "Fork repositories"
-msgstr "Создавать fork от репозиториев"
+msgstr "Создавать форки"
 
 #: kallithea/templates/base/default_perms_box.html:35
 msgid "Select this option to allow repository forking for this user"
 msgstr ""
-"Выберите эту опцию чтобы разрешить данному пользователю создавать fork'и "
+"Выберите, чтобы разрешить данному пользователю создавать форки "
 "репозиториев"
 
 #: kallithea/templates/base/perms_summary.html:13
@@ -4192,11 +4207,11 @@
 
 #: kallithea/templates/base/perms_summary.html:92
 msgid "No permission defined"
-msgstr ""
+msgstr "Права не заданы"
 
 #: kallithea/templates/base/root.html:28
 msgid "Retry"
-msgstr ""
+msgstr "Повторить"
 
 #: kallithea/templates/base/root.html:29
 #: kallithea/templates/changeset/changeset_file_comment.html:65
@@ -4204,10 +4219,8 @@
 msgstr "Применение..."
 
 #: kallithea/templates/base/root.html:30
-#, fuzzy
-#| msgid "Enable downloads"
 msgid "Unable to post"
-msgstr "Включить скачивание"
+msgstr "Не удалось отправить"
 
 #: kallithea/templates/base/root.html:31
 msgid "Add Another Comment"
@@ -4215,11 +4228,11 @@
 
 #: kallithea/templates/base/root.html:32
 msgid "Stop following this repository"
-msgstr "Отменить наблюдение за репозиторием"
+msgstr "Отписаться от этого репозитория"
 
 #: kallithea/templates/base/root.html:33
 msgid "Start following this repository"
-msgstr "Наблюдать за репозиторием"
+msgstr "Подписаться к этому репозиторию"
 
 #: kallithea/templates/base/root.html:34
 msgid "Group"
@@ -4242,18 +4255,16 @@
 msgstr "Нет совпадений"
 
 #: kallithea/templates/base/root.html:39
-#, fuzzy
 msgid "Open New Pull Request from {0}"
-msgstr "Комментарий в pull-запросе"
+msgstr "Открыть новый pull-запрос от {0}"
 
 #: kallithea/templates/base/root.html:40
 msgid "Open New Pull Request for {0} &rarr; {1}"
-msgstr ""
+msgstr "Открыть новый pull-запрос для {0} &rarr; {1}"
 
 #: kallithea/templates/base/root.html:41
-#, fuzzy
 msgid "Show Selected Changesets {0} &rarr; {1}"
-msgstr "Показать выбранные наборы изменений: __S &rarr; __E"
+msgstr "Показать выбранные наборы изменений: {0} &rarr; {1}"
 
 #: kallithea/templates/base/root.html:42
 msgid "Selection Link"
@@ -4269,13 +4280,13 @@
 msgstr "Раскрыть сравнение"
 
 #: kallithea/templates/base/root.html:45
-#, fuzzy
 msgid "No revisions"
-msgstr "версии"
+msgstr "Нет ревизий"
 
 #: kallithea/templates/base/root.html:46
 msgid "Type name of user or member to grant permission"
 msgstr ""
+"Введите имя пользователя или члена группы для предоставления доступа"
 
 #: kallithea/templates/base/root.html:47
 msgid "Failed to revoke permission"
@@ -4333,7 +4344,7 @@
 
 #: kallithea/templates/changelog/changelog.html:54
 msgid "Go to tip of repository"
-msgstr "Перейти на верхушку репозитория"
+msgstr "Перейти к началу репозитория"
 
 #: kallithea/templates/changelog/changelog.html:59
 #: kallithea/templates/forks/forks_data.html:16
@@ -4383,40 +4394,38 @@
 
 #: kallithea/templates/changelog/changelog_table.html:20
 msgid "First (oldest) changeset in this list"
-msgstr ""
+msgstr "Первый (самый старый) набор изменений в списке"
 
 #: kallithea/templates/changelog/changelog_table.html:22
 msgid "Last (most recent) changeset in this list"
-msgstr ""
+msgstr "Последний (самый свежий) набор изменений в списке"
 
 #: kallithea/templates/changelog/changelog_table.html:24
 msgid "Position in this list of changesets"
-msgstr ""
+msgstr "Позиция в списке наборов изменений"
 
 #: kallithea/templates/changelog/changelog_table.html:35
-#, fuzzy, python-format
+#, python-format
 msgid ""
 "Changeset status: %s by %s\n"
 "Click to open associated pull request %s"
 msgstr ""
-"Статус набора изенений: %s⏎\n"
-"Кликрните, чтобы перейти к соответствующему pull-request'у #%s"
+"Статус набора изменений: %s от %s\n"
+"Кликните, чтобы открыть соответствующий pull-запрос %s"
 
 #: kallithea/templates/changelog/changelog_table.html:41
-#, fuzzy, python-format
+#, python-format
 msgid "Changeset status: %s by %s"
-msgstr "Статус набора изменений: %s"
+msgstr "Статус набора изменений: %s от %s"
 
 #: kallithea/templates/changelog/changelog_table.html:60
 msgid "Expand commit message"
-msgstr ""
+msgstr "Развернуть сообщение фиксации"
 
 #: kallithea/templates/changelog/changelog_table.html:76
-#, fuzzy, python-format
-#| msgid "%d comment"
-#| msgid_plural "%d comments"
+#, python-format
 msgid "%s comments"
-msgstr "%d комментарий"
+msgstr "%s комментариев"
 
 #: kallithea/templates/changelog/changelog_table.html:80
 #: kallithea/templates/changeset/changeset.html:63
@@ -4447,7 +4456,7 @@
 
 #: kallithea/templates/changeset/changeset.html:34
 msgid "Changeset status"
-msgstr "Статут изменений"
+msgstr "Статус изменений"
 
 #: kallithea/templates/changeset/changeset.html:43
 #: kallithea/templates/changeset/diff_block.html:64
@@ -4467,29 +4476,26 @@
 
 #: kallithea/templates/changeset/changeset.html:59
 #: kallithea/templates/changeset/changeset_range.html:80
-#, fuzzy
 msgid "Merge"
-msgstr "свести"
-
-#: kallithea/templates/changeset/changeset.html:96
+msgstr "Слить"
+
+#: kallithea/templates/changeset/changeset.html:95
 msgid "Grafted from:"
 msgstr "Перенесено из:"
 
-#: kallithea/templates/changeset/changeset.html:102
+#: kallithea/templates/changeset/changeset.html:100
 msgid "Transplanted from:"
-msgstr ""
-
-#: kallithea/templates/changeset/changeset.html:108
-#, fuzzy
+msgstr "Трансплантировано из:"
+
+#: kallithea/templates/changeset/changeset.html:106
 msgid "Replaced by:"
-msgstr "Создано"
-
-#: kallithea/templates/changeset/changeset.html:122
-#, fuzzy
+msgstr "Заменено:"
+
+#: kallithea/templates/changeset/changeset.html:120
 msgid "Preceded by:"
-msgstr "Создано"
-
-#: kallithea/templates/changeset/changeset.html:139
+msgstr "Предшествует:"
+
+#: kallithea/templates/changeset/changeset.html:137
 #: kallithea/templates/compare/compare_diff.html:59
 #: kallithea/templates/pullrequests/pullrequest_show.html:290
 #, python-format
@@ -4499,7 +4505,7 @@
 msgstr[1] "%s файлов изменено"
 msgstr[2] "%s файла изменено"
 
-#: kallithea/templates/changeset/changeset.html:141
+#: kallithea/templates/changeset/changeset.html:139
 #: kallithea/templates/compare/compare_diff.html:61
 #: kallithea/templates/pullrequests/pullrequest_show.html:292
 #, python-format
@@ -4509,8 +4515,8 @@
 msgstr[1] "%s файла изменёно: %s добавления, %s удаления"
 msgstr[2] "%s файлов изменёно: %s добавлений, %s удалений"
 
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
 #: kallithea/templates/pullrequests/pullrequest_show.html:333
@@ -4518,24 +4524,20 @@
 msgstr "Показать полный diff"
 
 #: kallithea/templates/changeset/changeset_file_comment.html:20
-#, fuzzy
-#| msgid "Comment"
 msgid "comment"
-msgstr "Комментировать"
+msgstr "комментарий"
 
 #: kallithea/templates/changeset/changeset_file_comment.html:21
-#, fuzzy
 msgid "on pull request"
-msgstr "Комментарий в pull-запросе"
+msgstr "в pull-запросе"
 
 #: kallithea/templates/changeset/changeset_file_comment.html:22
 msgid "No title"
 msgstr "Нет заголовка"
 
 #: kallithea/templates/changeset/changeset_file_comment.html:24
-#, fuzzy
 msgid "on this changeset"
-msgstr "Нет изменений"
+msgstr "в этом наборе изменений"
 
 #: kallithea/templates/changeset/changeset_file_comment.html:30
 msgid "Delete comment?"
@@ -4543,12 +4545,10 @@
 
 #: kallithea/templates/changeset/changeset_file_comment.html:38
 #: kallithea/templates/changeset/changeset_file_comment.html:71
-#, fuzzy
 msgid "Status change"
-msgstr "Последние изменения"
+msgstr "Изменение статуса"
 
 #: kallithea/templates/changeset/changeset_file_comment.html:87
-#, fuzzy
 msgid "Comments are in plain text. Use @username to notify another user."
 msgstr ""
 "Используйте @имя_пользователя в тексте, чтобы отправить оповещение "
@@ -4560,7 +4560,7 @@
 
 #: kallithea/templates/changeset/changeset_file_comment.html:95
 msgid "Vote for pull request status"
-msgstr ""
+msgstr "Голосовать за статус pull-запроса"
 
 #: kallithea/templates/changeset/changeset_file_comment.html:101
 #: kallithea/templates/changeset/diff_block.html:46
@@ -4568,9 +4568,8 @@
 msgstr "Без изменений"
 
 #: kallithea/templates/changeset/changeset_file_comment.html:114
-#, fuzzy
 msgid "Finish pull request"
-msgstr "Комментарий в pull-запросе"
+msgstr "Завершить pull-запрос"
 
 #: kallithea/templates/changeset/changeset_file_comment.html:117
 msgid "Close"
@@ -4612,9 +4611,9 @@
 #, python-format
 msgid "%d general"
 msgid_plural "%d general"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
+msgstr[0] "%d общий"
+msgstr[1] "%d общих"
+msgstr[2] "%d общих"
 
 #: kallithea/templates/changeset/changeset_range.html:5
 #, python-format
@@ -4632,19 +4631,15 @@
 
 #: kallithea/templates/changeset/diff_block.html:30
 msgid "No file before"
-msgstr ""
+msgstr "Нет предшествующего файла"
 
 #: kallithea/templates/changeset/diff_block.html:33
-#, fuzzy
-#| msgid "file removed"
 msgid "File before"
-msgstr "файл удалён"
+msgstr "Файл до"
 
 #: kallithea/templates/changeset/diff_block.html:40
-#, fuzzy
-#| msgid "Unmodified"
 msgid "Modified"
-msgstr "Неизменный"
+msgstr "Модифицирован"
 
 #: kallithea/templates/changeset/diff_block.html:42
 msgid "Deleted"
@@ -4655,22 +4650,17 @@
 msgstr "Переименован"
 
 #: kallithea/templates/changeset/diff_block.html:48
-#, fuzzy, python-format
-#| msgid "Unknown revision %s"
+#, python-format
 msgid "Unknown operation: %r"
-msgstr "Неизвестная ревизия %s"
+msgstr "Неизвестная операция: %r"
 
 #: kallithea/templates/changeset/diff_block.html:52
-#, fuzzy
-#| msgid "No filename"
 msgid "No file after"
-msgstr "Безымянный"
+msgstr "Нет последующего файла"
 
 #: kallithea/templates/changeset/diff_block.html:55
-#, fuzzy
-#| msgid "file added"
 msgid "File after"
-msgstr "файл удалён"
+msgstr "Файл после"
 
 #: kallithea/templates/changeset/diff_block.html:60
 #: kallithea/templates/files/diff_2way.html:43
@@ -4684,7 +4674,7 @@
 
 #: kallithea/templates/changeset/diff_block.html:72
 msgid "Show inline comments"
-msgstr "Показать комментарии в строках"
+msgstr "Показать комментарии к строкам"
 
 #: kallithea/templates/compare/compare_cs.html:5
 msgid "No changesets"
@@ -4692,19 +4682,19 @@
 
 #: kallithea/templates/compare/compare_cs.html:12
 msgid "Criss cross merge situation with multiple merge ancestors detected!"
-msgstr ""
+msgstr "Обнаружено перекрёстное слияние с различными предками!"
 
 #: kallithea/templates/compare/compare_cs.html:15
 msgid ""
 "Please merge the target branch to your branch before creating a pull "
 "request."
 msgstr ""
+"Прежде чем создавать pull-запрос, выполните слияние целевой ветви с вашей "
+"ветвью."
 
 #: kallithea/templates/compare/compare_cs.html:19
-#, fuzzy
-#| msgid "Common ancestor"
 msgid "Merge Ancestor"
-msgstr "Общий предок"
+msgstr "Слияние с предком"
 
 #: kallithea/templates/compare/compare_cs.html:40
 msgid "Show merge diff"
@@ -4732,15 +4722,15 @@
 #: kallithea/templates/compare/compare_diff.html:13
 #: kallithea/templates/compare/compare_diff.html:41
 msgid "Compare Revisions"
-msgstr ""
+msgstr "Сравнить ревизии"
 
 #: kallithea/templates/compare/compare_diff.html:39
 msgid "Swap"
-msgstr ""
+msgstr "Поменять местами"
 
 #: kallithea/templates/compare/compare_diff.html:48
 msgid "Compare revisions, branches, bookmarks, or tags."
-msgstr ""
+msgstr "Сравнение ревизий, ветвей, закладок и тегов."
 
 #: kallithea/templates/compare/compare_diff.html:53
 #: kallithea/templates/pullrequests/pullrequest_show.html:278
@@ -4761,163 +4751,186 @@
 
 #: kallithea/templates/data_table/_dt_elements.html:29
 msgid "Repository creation in progress..."
-msgstr ""
-
-#: kallithea/templates/data_table/_dt_elements.html:42
+msgstr "Создание репозитория в процессе..."
+
+#: kallithea/templates/data_table/_dt_elements.html:48
 msgid "No changesets yet"
 msgstr "Изменений ещё не было"
 
-#: kallithea/templates/data_table/_dt_elements.html:48
-#: kallithea/templates/data_table/_dt_elements.html:50
+#: kallithea/templates/data_table/_dt_elements.html:54
+#: kallithea/templates/data_table/_dt_elements.html:56
 #, python-format
 msgid "Subscribe to %s rss feed"
 msgstr "Подписаться на ленту RSS %s"
 
-#: kallithea/templates/data_table/_dt_elements.html:56
-#: kallithea/templates/data_table/_dt_elements.html:58
+#: kallithea/templates/data_table/_dt_elements.html:62
+#: kallithea/templates/data_table/_dt_elements.html:64
 #, python-format
 msgid "Subscribe to %s atom feed"
 msgstr "Подписаться на ленту Atom %s"
 
-#: kallithea/templates/data_table/_dt_elements.html:76
+#: kallithea/templates/data_table/_dt_elements.html:82
 msgid "Creating"
-msgstr ""
-
-#: kallithea/templates/email_templates/changeset_comment.html:4
-#, fuzzy, python-format
-#| msgid "%(user)s commented on changeset %(age)s"
-msgid "Mention in Comment on Changeset \"%s\""
-msgstr "%(user)s оставил комментарий к набору изменений %(age)s"
+msgstr "Создание"
 
 #: kallithea/templates/email_templates/changeset_comment.html:4
-#, fuzzy, python-format
-#| msgid "Comment from %s on %s changeset %s"
+#, python-format
+msgid "Mention in Comment on Changeset \"%s\""
+msgstr "Упоминание в комментарии к набору изменений «%s»"
+
+#: kallithea/templates/email_templates/changeset_comment.html:4
+#, python-format
 msgid "Comment on Changeset \"%s\""
-msgstr "Комментарий от %s к набору изменений %s %s"
+msgstr "Комментарий к набору изменений «%s»"
 
 #: kallithea/templates/email_templates/changeset_comment.html:20
-#, fuzzy
-#| msgid "Changeset flow"
 msgid "Changeset on"
-msgstr "Поток изменений"
+msgstr "Набр изменений для"
 
 #: kallithea/templates/email_templates/changeset_comment.html:23
 #: kallithea/templates/email_templates/pull_request.html:22
 #: kallithea/templates/email_templates/pull_request.html:28
 #: kallithea/templates/email_templates/pull_request_comment.html:30
 #: kallithea/templates/email_templates/pull_request_comment.html:36
-#, fuzzy
-#| msgid "Branch"
 msgid "branch"
-msgstr "Ветка"
+msgstr "ветви"
 
 #: kallithea/templates/email_templates/changeset_comment.html:29
 #: kallithea/templates/email_templates/pull_request.html:15
 #: kallithea/templates/email_templates/pull_request_comment.html:23
 msgid "by"
-msgstr ""
+msgstr "от"
+
+#: kallithea/templates/email_templates/changeset_comment.html:36
+#: kallithea/templates/email_templates/pull_request_comment.html:43
+#, fuzzy
+#| msgid "Comment"
+msgid "View Comment"
+msgstr "Комментировать"
 
 #: kallithea/templates/email_templates/comment.html:27
-#, fuzzy
-#| msgid "Status change"
 msgid "Status change:"
-msgstr "Последние изменения"
+msgstr "Изменение статуса:"
 
 #: kallithea/templates/email_templates/comment.html:33
+msgid "The pull request has been closed."
+msgstr "Этот pull-запрос закрыт."
+
+#: kallithea/templates/email_templates/default.html:4
 #, fuzzy
-#| msgid "This pull request has been closed and can not be updated."
-msgid "The pull request has been closed."
-msgstr "Этот pull-запрос был закрыт и не может быть обновлён."
-
-#: kallithea/templates/email_templates/password_reset.html:9
+#| msgid "Commit Message"
+msgid "Message"
+msgstr "Зафиксировать сообщение"
+
+#: kallithea/templates/email_templates/password_reset.html:4
+#, fuzzy
+#| msgid "Password Reset"
+msgid "Password Reset Request"
+msgstr "Сброс пароля"
+
+#: kallithea/templates/email_templates/password_reset.html:10
 #, python-format
 msgid "Hello %s"
 msgstr "Здравствуйте, %s"
 
-#: kallithea/templates/email_templates/password_reset.html:16
-#, fuzzy
+#: kallithea/templates/email_templates/password_reset.html:17
 msgid "We have received a request to reset the password for your account."
-msgstr "Мы отправили запрос на создание нового пароля для вашего аккаунта."
-
-#: kallithea/templates/email_templates/password_reset.html:25
+msgstr "Мы получили запрос на сброс пароля для вашего аккаунта."
+
+#: kallithea/templates/email_templates/password_reset.html:26
 msgid ""
 "This account is however managed outside this system and the password "
 "cannot be changed here."
 msgstr ""
-
-#: kallithea/templates/email_templates/password_reset.html:28
+"Однако, поскольку этот аккаунт управляется извне, мы не можем изменить "
+"пароль здесь."
+
+#: kallithea/templates/email_templates/password_reset.html:29
 msgid "To set a new password, click the following link"
-msgstr ""
-
-#: kallithea/templates/email_templates/password_reset.html:33
+msgstr "Перейдите по ссылке, чтобы задать новый пароль"
+
+#: kallithea/templates/email_templates/password_reset.html:34
 msgid ""
 "Should you not be able to use the link above, please type the following "
 "code into the password reset form"
 msgstr ""
-
-#: kallithea/templates/email_templates/password_reset.html:44
+"В случае, если перейти по ссылке не удаётся, введите в форме сброса "
+"пароля следующий код"
+
+#: kallithea/templates/email_templates/password_reset.html:45
 msgid ""
 "If it weren't you who requested the password reset, just disregard this "
 "message."
 msgstr ""
+"Если вы не запрашивали сброс пароля, то просто проигнорируйте это "
+"сообщение."
 
 #: kallithea/templates/email_templates/pull_request.html:4
-#, fuzzy, python-format
-#| msgid "%s mentioned you on %s pull request \"%s\""
+#, python-format
 msgid "Mention on Pull Request %s \"%s\" by %s"
-msgstr "%s упомянул Вас в комментарии к pull-запросу %s \"%s\""
+msgstr "Упоминание в pull-запросе %s «%s» от %s"
 
 #: kallithea/templates/email_templates/pull_request.html:4
-#, fuzzy, python-format
-#| msgid "%s requested your review of %s pull request \"%s\""
+#, python-format
 msgid "Added as Reviewer of Pull Request %s \"%s\" by %s"
-msgstr "%s запросил рецензирование pull-запроса %s \"%s\""
+msgstr "Добавлен в качестве ревьювера pull-запроса %s «%s» пользователем %s"
 
 #: kallithea/templates/email_templates/pull_request.html:12
 #: kallithea/templates/email_templates/pull_request_comment.html:20
-#, fuzzy
-#| msgid "Pull request %s"
 msgid "Pull request"
-msgstr "Pull-запрос %s"
+msgstr "Pull-запрос"
 
 #: kallithea/templates/email_templates/pull_request.html:19
 #: kallithea/templates/email_templates/pull_request_comment.html:27
 msgid "from"
-msgstr ""
+msgstr "от"
 
 #: kallithea/templates/email_templates/pull_request.html:25
 #: kallithea/templates/email_templates/pull_request_comment.html:33
 msgid "to"
-msgstr ""
+msgstr "к"
+
+#: kallithea/templates/email_templates/pull_request.html:85
+#, fuzzy
+#| msgid "New Pull Request"
+msgid "View Pull Request"
+msgstr "Новый pull-запрос"
 
 #: kallithea/templates/email_templates/pull_request_comment.html:4
-#, fuzzy, python-format
-#| msgid "%s mentioned you on %s pull request \"%s\""
+#, python-format
 msgid "Mention in Comment on Pull Request %s \"%s\""
-msgstr "%s упомянул Вас в комментарии к pull-запросу %s \"%s\""
+msgstr "Упоминание в комментарии к pull-запросу %s «%s»"
+
+#: kallithea/templates/email_templates/pull_request_comment.html:4
+#, python-format
+msgid "Pull Request %s \"%s\" Closed"
+msgstr "Pull-запрос %s «%s» закрыт"
 
 #: kallithea/templates/email_templates/pull_request_comment.html:4
-#, fuzzy, python-format
-#| msgid "Pull request %s from %s#%s"
-msgid "Pull Request %s \"%s\" Closed"
-msgstr "Pull-запросы %s от %s#%s"
-
-#: kallithea/templates/email_templates/pull_request_comment.html:4
-#, fuzzy, python-format
-#| msgid "[commented] on pull request for"
+#, python-format
 msgid "Comment on Pull Request %s \"%s\""
-msgstr "[прокомментировано] в запросе на внесение изменений для"
-
-#: kallithea/templates/email_templates/registration.html:22
+msgstr "Комментарий к pull-запросу %s «%s»"
+
+#: kallithea/templates/email_templates/registration.html:5
 #, fuzzy
-#| msgid "Group name"
+#| msgid "New user registration"
+msgid "New User Registration"
+msgstr "Регистрация нового пользователя"
+
+#: kallithea/templates/email_templates/registration.html:23
 msgid "Full Name"
-msgstr "Имя группы"
+msgstr "Полное имя"
+
+#: kallithea/templates/email_templates/registration.html:42
+#, fuzzy
+#| msgid "View this user here"
+msgid "View User Profile"
+msgstr "Подробнее о пользователе"
 
 #: kallithea/templates/files/diff_2way.html:15
 #, python-format
 msgid "%s File side-by-side diff"
-msgstr ""
+msgstr "Построчное сравнение файла %s"
 
 #: kallithea/templates/files/diff_2way.html:19
 #: kallithea/templates/files/file_diff.html:8
@@ -4933,7 +4946,7 @@
 #: kallithea/templates/files/files.html:74
 #, python-format
 msgid "%s Files"
-msgstr "%s Файлы"
+msgstr "%s файлов"
 
 #: kallithea/templates/files/files_add.html:4
 #, python-format
@@ -4944,7 +4957,7 @@
 #: kallithea/templates/files/files_ypjax.html:9
 #: kallithea/templates/summary/summary.html:199
 msgid "Add New File"
-msgstr ""
+msgstr "Добавить новый файл"
 
 #: kallithea/templates/files/files_add.html:39
 #: kallithea/templates/files/files_edit.html:39
@@ -4954,7 +4967,7 @@
 
 #: kallithea/templates/files/files_add.html:41
 msgid "Enter filename..."
-msgstr ""
+msgstr "Введите имя файла..."
 
 #: kallithea/templates/files/files_add.html:43
 #: kallithea/templates/files/files_add.html:47
@@ -4963,22 +4976,21 @@
 
 #: kallithea/templates/files/files_add.html:43
 msgid "Upload File"
-msgstr ""
+msgstr "Загрузить файл"
 
 #: kallithea/templates/files/files_add.html:47
 msgid "Create New File"
-msgstr ""
+msgstr "Создать новый файл"
 
 #: kallithea/templates/files/files_add.html:53
-#, fuzzy
 msgid "New file type"
-msgstr "новый файл"
+msgstr "Тип файла"
 
 #: kallithea/templates/files/files_add.html:64
 #: kallithea/templates/files/files_delete.html:34
 #: kallithea/templates/files/files_edit.html:67
 msgid "Commit Message"
-msgstr ""
+msgstr "Зафиксировать сообщение"
 
 #: kallithea/templates/files/files_add.html:68
 #: kallithea/templates/files/files_delete.html:40
@@ -4988,7 +5000,7 @@
 
 #: kallithea/templates/files/files_browser.html:40
 msgid "Search File List"
-msgstr ""
+msgstr "Искать в списке файлов"
 
 #: kallithea/templates/files/files_browser.html:45
 msgid "Loading file list..."
@@ -5014,17 +5026,17 @@
 #: kallithea/templates/files/files_delete.html:4
 #, python-format
 msgid "%s Files Delete"
-msgstr ""
+msgstr "Удаление файлов %s"
 
 #: kallithea/templates/files/files_delete.html:12
 #: kallithea/templates/files/files_delete.html:30
 msgid "Delete file"
-msgstr ""
+msgstr "Удалить файл"
 
 #: kallithea/templates/files/files_edit.html:4
 #, python-format
 msgid "%s File Edit"
-msgstr ""
+msgstr "Правка файла %s"
 
 #: kallithea/templates/files/files_edit.html:21
 msgid "Edit file"
@@ -5033,12 +5045,12 @@
 #: kallithea/templates/files/files_edit.html:51
 #: kallithea/templates/files/files_source.html:28
 msgid "Show Annotation"
-msgstr ""
+msgstr "Показать аннотацию"
 
 #: kallithea/templates/files/files_edit.html:53
 #: kallithea/templates/files/files_source.html:31
 msgid "Download as Raw"
-msgstr ""
+msgstr "Загрузить в исходном виде"
 
 #: kallithea/templates/files/files_edit.html:56
 msgid "Source"
@@ -5054,33 +5066,32 @@
 
 #: kallithea/templates/files/files_source.html:6
 msgid "Diff to Revision"
-msgstr ""
+msgstr "Разница с ревизией"
 
 #: kallithea/templates/files/files_source.html:7
 msgid "Show at Revision"
-msgstr ""
+msgstr "Показать в ревизии"
 
 #: kallithea/templates/files/files_source.html:9
 msgid "Show Full History"
-msgstr ""
+msgstr "Показать всю историю"
 
 #: kallithea/templates/files/files_source.html:10
 msgid "Show Authors"
-msgstr ""
+msgstr "Показать авторов"
 
 #: kallithea/templates/files/files_source.html:26
 msgid "Show Source"
-msgstr ""
+msgstr "Показать источник"
 
 #: kallithea/templates/files/files_source.html:34
-#, fuzzy, python-format
-#| msgid "Deleted branch: %s"
+#, python-format
 msgid "Edit on Branch: %s"
-msgstr "Удалена ветка: %s"
+msgstr "Правка в ветке: %s"
 
 #: kallithea/templates/files/files_source.html:37
 msgid "Editing binary files not allowed"
-msgstr ""
+msgstr "Редактирование бинарных файлов не допускается"
 
 #: kallithea/templates/files/files_source.html:40
 msgid "Editing files allowed only when on branch head revision"
@@ -5089,6 +5100,8 @@
 #: kallithea/templates/files/files_source.html:41
 msgid "Deleting files allowed only when on branch head revision"
 msgstr ""
+"Удаление файлов допускается только при нахождении в текущей ветке (branch "
+"head)"
 
 #: kallithea/templates/files/files_source.html:58
 #, python-format
@@ -5113,7 +5126,7 @@
 
 #: kallithea/templates/files/files_ypjax.html:23
 msgid "Go Back"
-msgstr ""
+msgstr "Назад"
 
 #: kallithea/templates/files/files_ypjax.html:24
 msgid "No files at given path"
@@ -5122,22 +5135,22 @@
 #: kallithea/templates/followers/followers.html:5
 #, python-format
 msgid "%s Followers"
-msgstr "%s Наблюдатели"
+msgstr "%s Подписчики"
 
 #: kallithea/templates/followers/followers.html:9
 #: kallithea/templates/summary/summary.html:138
 #: kallithea/templates/summary/summary.html:139
 msgid "Followers"
-msgstr "Наблюдатели"
+msgstr "Подписчики"
 
 #: kallithea/templates/followers/followers_data.html:9
 msgid "Started following -"
-msgstr "Наблюдать за репозиторием"
+msgstr "Подписался -"
 
 #: kallithea/templates/forks/fork.html:5
 #, python-format
 msgid "Fork repository %s"
-msgstr ""
+msgstr "Создать форк репозитория %s"
 
 #: kallithea/templates/forks/fork.html:25
 msgid "Fork name"
@@ -5159,7 +5172,7 @@
 
 #: kallithea/templates/forks/fork.html:69
 msgid "Copy permissions from forked repository"
-msgstr "Скопировать привилегии с форкнутого репозитория"
+msgstr "Скопировать права доступа с форка репозитория"
 
 #: kallithea/templates/forks/fork.html:75
 msgid "Update after clone"
@@ -5171,7 +5184,7 @@
 
 #: kallithea/templates/forks/fork.html:85
 msgid "Fork this Repository"
-msgstr ""
+msgstr "Создать форк"
 
 #: kallithea/templates/forks/forks.html:5
 #, python-format
@@ -5182,15 +5195,15 @@
 #: kallithea/templates/summary/summary.html:144
 #: kallithea/templates/summary/summary.html:145
 msgid "Forks"
-msgstr "Ответвления"
+msgstr "Форки"
 
 #: kallithea/templates/forks/forks_data.html:14
 msgid "Forked"
-msgstr "Форкнуто"
+msgstr "Форк создан"
 
 #: kallithea/templates/forks/forks_data.html:24
 msgid "There are no forks yet"
-msgstr "Форки ещё не созданы"
+msgstr "Форков пока нет"
 
 #: kallithea/templates/journal/journal.html:22
 msgid "ATOM journal feed"
@@ -5230,12 +5243,12 @@
 
 #: kallithea/templates/pullrequests/pullrequest.html:28
 msgid "Summarize the changes - or leave empty"
-msgstr ""
+msgstr "Опишите изменения — или оставьте пустым"
 
 #: kallithea/templates/pullrequests/pullrequest.html:35
 #: kallithea/templates/pullrequests/pullrequest_show.html:61
 msgid "Write a short description on this pull request"
-msgstr "Написать короткое писание по этому запросу"
+msgstr "Оставьте краткое описание этого pull-запроса"
 
 #: kallithea/templates/pullrequests/pullrequest.html:40
 msgid "Changeset flow"
@@ -5259,9 +5272,8 @@
 msgstr "Записи отсуствуют"
 
 #: kallithea/templates/pullrequests/pullrequest_data.html:14
-#, fuzzy
 msgid "Vote"
-msgstr "отозвать"
+msgstr "Голосовать"
 
 #: kallithea/templates/pullrequests/pullrequest_data.html:17
 msgid "Age"
@@ -5269,24 +5281,24 @@
 
 #: kallithea/templates/pullrequests/pullrequest_data.html:18
 msgid "From"
-msgstr ""
+msgstr "От"
 
 #: kallithea/templates/pullrequests/pullrequest_data.html:19
 msgid "To"
-msgstr ""
+msgstr "К"
 
 #: kallithea/templates/pullrequests/pullrequest_data.html:28
 #, python-format
 msgid "You voted: %s"
-msgstr ""
+msgstr "Ваш выбор: %s"
 
 #: kallithea/templates/pullrequests/pullrequest_data.html:30
 msgid "You didn't vote"
-msgstr ""
+msgstr "Вы не голосовали"
 
 #: kallithea/templates/pullrequests/pullrequest_data.html:35
 msgid "(no title)"
-msgstr ""
+msgstr "(без заголовка)"
 
 #: kallithea/templates/pullrequests/pullrequest_data.html:37
 #: kallithea/templates/pullrequests/pullrequest_show.html:31
@@ -5300,17 +5312,18 @@
 
 #: kallithea/templates/pullrequests/pullrequest_data.html:68
 msgid "Confirm to delete this pull request"
-msgstr "Подтвердите удаление этого pull-request'а"
+msgstr "Подтвердите удаление этого pull-запроса"
 
 #: kallithea/templates/pullrequests/pullrequest_data.html:70
-#, fuzzy, python-format
+#, python-format
 msgid "Confirm again to delete this pull request with %s comments"
-msgstr "Подтвердите удаление этого pull-request'а"
+msgstr ""
+"Ещё раз подтвердите удаление pull-запроса со всеми (%s) комментариями"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:6
-#, fuzzy, python-format
+#, python-format
 msgid "%s Pull Request %s"
-msgstr "%s Pull-запрос #%s"
+msgstr "%s pull-запрос %s"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:10
 #, python-format
@@ -5318,57 +5331,57 @@
 msgstr "Pull-запросы %s от %s#%s"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:54
-#, fuzzy
 msgid "Summarize the changes"
-msgstr "Применить изменения"
+msgstr "Опишите изменения"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:67
 msgid "Voting Result"
-msgstr ""
+msgstr "Результаты голосования"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:70
 #: kallithea/templates/pullrequests/pullrequest_show.html:71
 msgid "Pull request status calculated from votes"
-msgstr ""
+msgstr "Статус pull-запроса определён по голосованию"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:81
 msgid "Origin"
-msgstr ""
+msgstr "Происхождение"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:86
-#, fuzzy
 msgid "on"
-msgstr "ничего"
+msgstr "на"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:92
 msgid "Target"
-msgstr ""
+msgstr "Цель"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:95
 msgid ""
 "This is just a range of changesets and doesn't have a target or a real "
 "merge ancestor."
 msgstr ""
+"Это всего лишь перечень наборов изменений, который не имеет цели или "
+"реального предка для слияния."
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:103
 msgid "Pull changes"
 msgstr "Принять изменения"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:136
-#, fuzzy
-#| msgid "Registration"
 msgid "Next iteration"
-msgstr "Регистрация"
+msgstr "Следующая итерация"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:153
 msgid "Current revision - no change"
-msgstr ""
+msgstr "Текущая ревизия — без изменений"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:177
 msgid ""
 "Pull request iterations do not change content once created. Select a "
 "revision to create a new iteration."
 msgstr ""
+"Итерации pull-запросов не изменяются после создания. Выберите ревизию для "
+"создания новой итерации."
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:187
 msgid "Save Changes"
@@ -5376,17 +5389,15 @@
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:188
 msgid "Create New Iteration with Changes"
-msgstr ""
+msgstr "Создать итерацию с изменениями"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:189
 msgid "Cancel Changes"
 msgstr "Отменить изменения"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:197
-#, fuzzy
-#| msgid "reviewer"
 msgid "Reviewers"
-msgstr "рецензент"
+msgstr "Ревьюверы"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:223
 msgid "Remove reviewer"
@@ -5394,7 +5405,7 @@
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:234
 msgid "Type name of reviewer to add"
-msgstr ""
+msgstr "Введите имя добавляемого ревьювера"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:240
 msgid "Potential Reviewers"
@@ -5402,12 +5413,11 @@
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:243
 msgid "Click to add the repository owner as reviewer:"
-msgstr ""
+msgstr "Нажмите, чтобы добавить владельца репозитория в качестве ревьювера:"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:268
-#, fuzzy
 msgid "Pull Request Content"
-msgstr "Статус pull-request'а был изменен"
+msgstr "Содержимое pull-запроса"
 
 #: kallithea/templates/pullrequests/pullrequest_show.html:283
 msgid "Common ancestor"
@@ -5416,63 +5426,60 @@
 #: kallithea/templates/pullrequests/pullrequest_show_all.html:6
 #, python-format
 msgid "%s Pull Requests"
-msgstr "%s Запросы на внесение изменений"
+msgstr "Pull-запросы %s"
 
 #: kallithea/templates/pullrequests/pullrequest_show_all.html:11
-#, fuzzy, python-format
+#, python-format
 msgid "Pull Requests from '%s'"
-msgstr "Pull-запросы от %s"
+msgstr "Pull-запросы от '%s'"
 
 #: kallithea/templates/pullrequests/pullrequest_show_all.html:13
 #, python-format
 msgid "Pull Requests to '%s'"
-msgstr "Pull-запросы для %s"
+msgstr "Pull-запросы для '%s'"
 
 #: kallithea/templates/pullrequests/pullrequest_show_all.html:31
 msgid "Open New Pull Request"
 msgstr "Создать новый pull-запрос"
 
 #: kallithea/templates/pullrequests/pullrequest_show_all.html:34
-#, fuzzy, python-format
+#, python-format
 msgid "Show Pull Requests to %s"
-msgstr "Pull-запросы для %s"
+msgstr "Показать pull-запросы для '%s'"
 
 #: kallithea/templates/pullrequests/pullrequest_show_all.html:36
-#, fuzzy, python-format
+#, python-format
 msgid "Show Pull Requests from '%s'"
-msgstr "Pull запросы от %s"
+msgstr "Показать pull-запросы от '%s'"
 
 #: kallithea/templates/pullrequests/pullrequest_show_all.html:44
 #: kallithea/templates/pullrequests/pullrequest_show_my.html:28
 msgid "Hide closed pull requests (only show open pull requests)"
 msgstr ""
+"Спрятать закрытые pull-запросы (показывать только открытые pull-запросы)"
 
 #: kallithea/templates/pullrequests/pullrequest_show_all.html:46
 #: kallithea/templates/pullrequests/pullrequest_show_my.html:30
-#, fuzzy
 msgid "Show closed pull requests (in addition to open pull requests)"
-msgstr "Показать закрытые pull-запросы"
+msgstr ""
+"Показывать закрытые pull-запросы (в дополнение к открытым pull-запросам)"
 
 #: kallithea/templates/pullrequests/pullrequest_show_my.html:34
-#, fuzzy
 msgid "Pull Requests Created by Me"
-msgstr "Рецензенты запросов на внесение изменений Pull request"
+msgstr "Pull-запросы, созданные мной"
 
 #: kallithea/templates/pullrequests/pullrequest_show_my.html:37
-#, fuzzy
-#| msgid "Pull Request Reviewers"
 msgid "Pull Requests Needing My Review"
-msgstr "Рецензенты pull-запросов"
+msgstr "Pull-запросы, требующие моего рассмотрения"
 
 #: kallithea/templates/pullrequests/pullrequest_show_my.html:40
-#, fuzzy
 msgid "Pull Requests I Participate In"
-msgstr "Моё участие"
+msgstr "Pull-запросы, в которых я участвую"
 
 #: kallithea/templates/search/search.html:6
 #, python-format
 msgid "%s Search"
-msgstr ""
+msgstr "Поиск %s"
 
 #: kallithea/templates/search/search.html:8
 #: kallithea/templates/search/search.html:16
@@ -5481,7 +5488,7 @@
 
 #: kallithea/templates/search/search.html:47
 msgid "Search term"
-msgstr "Фраза для поиска"
+msgstr "Поисковый запрос"
 
 #: kallithea/templates/search/search.html:54
 msgid "Search in"
@@ -5508,7 +5515,7 @@
 #: kallithea/templates/summary/statistics.html:4
 #, python-format
 msgid "%s Statistics"
-msgstr ""
+msgstr "Статистика %s"
 
 #: kallithea/templates/summary/statistics.html:16
 #: kallithea/templates/summary/summary.html:27
@@ -5532,45 +5539,45 @@
 msgid "Stats gathered: "
 msgstr "Полученная статистика: "
 
-#: kallithea/templates/summary/statistics.html:87
-#: kallithea/templates/summary/summary.html:371
+#: kallithea/templates/summary/statistics.html:85
+#: kallithea/templates/summary/summary.html:369
 msgid "files"
 msgstr "файлы"
 
-#: kallithea/templates/summary/statistics.html:111
-#: kallithea/templates/summary/summary.html:401
+#: kallithea/templates/summary/statistics.html:109
+#: kallithea/templates/summary/summary.html:399
 msgid "Show more"
 msgstr "Показать еще"
 
-#: kallithea/templates/summary/statistics.html:405
+#: kallithea/templates/summary/statistics.html:395
 msgid "commits"
 msgstr "commit'ы"
 
-#: kallithea/templates/summary/statistics.html:406
+#: kallithea/templates/summary/statistics.html:396
 msgid "files added"
 msgstr "файлы добавлены"
 
-#: kallithea/templates/summary/statistics.html:407
+#: kallithea/templates/summary/statistics.html:397
 msgid "files changed"
 msgstr "файлы изменены"
 
-#: kallithea/templates/summary/statistics.html:408
+#: kallithea/templates/summary/statistics.html:398
 msgid "files removed"
 msgstr "файлы удалены"
 
-#: kallithea/templates/summary/statistics.html:410
+#: kallithea/templates/summary/statistics.html:400
 msgid "commit"
 msgstr "commit"
 
-#: kallithea/templates/summary/statistics.html:411
+#: kallithea/templates/summary/statistics.html:401
 msgid "file added"
 msgstr "файл удалён"
 
-#: kallithea/templates/summary/statistics.html:412
+#: kallithea/templates/summary/statistics.html:402
 msgid "file changed"
 msgstr "файл изменён"
 
-#: kallithea/templates/summary/statistics.html:413
+#: kallithea/templates/summary/statistics.html:403
 msgid "file removed"
 msgstr "файл удалён"
 
@@ -5581,7 +5588,7 @@
 
 #: kallithea/templates/summary/summary.html:13
 msgid "Fork of"
-msgstr "Форк от"
+msgstr "Форк репозитория"
 
 #: kallithea/templates/summary/summary.html:18
 msgid "Clone from"
@@ -5593,22 +5600,20 @@
 
 #: kallithea/templates/summary/summary.html:63
 msgid "Use ID"
-msgstr ""
+msgstr "Использовать ID"
 
 #: kallithea/templates/summary/summary.html:65
 #: kallithea/templates/summary/summary.html:73
 msgid "Use SSH"
-msgstr ""
+msgstr "Использовать SSH"
 
 #: kallithea/templates/summary/summary.html:71
-#, fuzzy
-#| msgid "Last Name"
 msgid "Use Name"
-msgstr "Фамилия"
+msgstr "Использовать имя"
 
 #: kallithea/templates/summary/summary.html:80
 msgid "Use HTTP"
-msgstr ""
+msgstr "Использовать HTTP"
 
 #: kallithea/templates/summary/summary.html:92
 msgid "Trending files"
@@ -5656,7 +5661,6 @@
 msgstr "Добавить или загрузить файлы через Kallithea"
 
 #: kallithea/templates/summary/summary.html:204
-#, fuzzy
 msgid "Push new repository"
 msgstr "Отправить новый репозиторий"
 
@@ -5667,13 +5671,37 @@
 #: kallithea/templates/summary/summary.html:230
 #, python-format
 msgid "Readme file from revision %s:%s"
-msgstr ""
+msgstr "Файл readme из ревизии %s:%s"
 
 #: kallithea/templates/summary/summary.html:315
 #, python-format
 msgid "Download %s as %s"
 msgstr "Скачать %s как %s"
 
+#~ msgid "Cache invalidation successful"
+#~ msgstr "Кэш сброшен"
+
+#~ msgid "An error occurred during cache invalidation"
+#~ msgstr "Произошла ошибка при очистке кэша"
+
+#~ msgid "Caches"
+#~ msgstr "Кэш"
+
+#~ msgid "Invalidate Repository Cache"
+#~ msgstr "Сбросить кэш репозитория"
+
+#~ msgid ""
+#~ "Manually invalidate cache for this repository. On first access, the "
+#~ "repository will be cached again."
+#~ msgstr ""
+#~ "Ручной сброс кэша репозитория. При первом доступе кэш восстановится."
+
+#~ msgid "List of Cached Values"
+#~ msgstr "Список кешированных значений"
+
+#~ msgid "Prefix"
+#~ msgstr "Префикс"
+
 #~ msgid "This repository has been locked by %s on %s"
 #~ msgstr "Репозиторий заблокировал %s в %s"
 
@@ -5942,9 +5970,6 @@
 #~ msgid "The comment was made with status"
 #~ msgstr "Комментарий оставлен со статусом"
 
-#~ msgid "View this user here"
-#~ msgstr "Подробнее о пользователе"
-
 #~ msgid "Repository Size"
 #~ msgstr "Размер репозитория"
 
--- a/kallithea/i18n/sk/LC_MESSAGES/kallithea.po	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/i18n/sk/LC_MESSAGES/kallithea.po	Mon May 04 19:24:04 2020 +0200
@@ -5,7 +5,7 @@
 msgstr ""
 "Project-Id-Version: Kallithea 0.3\n"
 "Report-Msgid-Bugs-To: translations@kallithea-scm.org\n"
-"POT-Creation-Date: 2020-01-05 04:10+0100\n"
+"POT-Creation-Date: 2020-05-04 19:12+0200\n"
 "PO-Revision-Date: 2015-04-01 12:59+0200\n"
 "Last-Translator: Andrej Shadura <andrew@shadura.me>\n"
 "Language-Team: Slovak <https://hosted.weblate.org/projects/kallithea/"
@@ -18,14 +18,14 @@
 "Generated-By: Babel 1.3\n"
 
 #: kallithea/controllers/changelog.py:67
-#: kallithea/controllers/pullrequests.py:250 kallithea/lib/base.py:602
+#: kallithea/controllers/pullrequests.py:247 kallithea/lib/base.py:602
 msgid "There are no changesets yet"
 msgstr "Zatiaľ nie sú žiadne zmeny"
 
 #: kallithea/controllers/admin/permissions.py:64
 #: kallithea/controllers/admin/permissions.py:68
 #: kallithea/controllers/admin/permissions.py:72
-#: kallithea/controllers/changelog.py:137
+#: kallithea/controllers/changelog.py:136
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:7
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:88
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:7
@@ -34,37 +34,37 @@
 msgid "None"
 msgstr ""
 
-#: kallithea/controllers/changelog.py:140 kallithea/controllers/files.py:189
+#: kallithea/controllers/changelog.py:139 kallithea/controllers/files.py:189
 msgid "(closed)"
 msgstr "(zatvorené)"
 
-#: kallithea/controllers/changeset.py:81
+#: kallithea/controllers/changeset.py:82
 msgid "Show whitespace"
 msgstr "Ukázať medzery"
 
-#: kallithea/controllers/changeset.py:88
-#: kallithea/controllers/changeset.py:95
+#: kallithea/controllers/changeset.py:89
+#: kallithea/controllers/changeset.py:96
 #: kallithea/templates/files/diff_2way.html:55
 msgid "Ignore whitespace"
 msgstr "Ignorovať medzery"
 
-#: kallithea/controllers/changeset.py:161
+#: kallithea/controllers/changeset.py:162
 #, python-format
 msgid "Increase diff context to %(num)s lines"
 msgstr ""
 
-#: kallithea/controllers/changeset.py:201
+#: kallithea/controllers/changeset.py:202
 #, fuzzy
 msgid "No permission to change status"
 msgstr "Zmeny"
 
-#: kallithea/controllers/changeset.py:212
+#: kallithea/controllers/changeset.py:213
 #, fuzzy, python-format
 msgid "Successfully deleted pull request %s"
 msgstr "Úspešne zmazaný súbor %s"
 
 #: kallithea/controllers/changeset.py:319 kallithea/controllers/files.py:89
-#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:700
+#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:697
 msgid "Such revision does not exist for this repository"
 msgstr "Taká revízia neexistuje"
 
@@ -78,62 +78,62 @@
 msgid "Cannot compare repositories of different types"
 msgstr ""
 
-#: kallithea/controllers/compare.py:246
+#: kallithea/controllers/compare.py:247
 msgid "Cannot show empty diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:248
+#: kallithea/controllers/compare.py:249
 msgid "No ancestor found for merge diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:252
+#: kallithea/controllers/compare.py:253
 msgid "Multiple merge ancestors found for merge compare"
 msgstr ""
 
-#: kallithea/controllers/compare.py:268
+#: kallithea/controllers/compare.py:269
 msgid "Cannot compare repositories without using common ancestor"
 msgstr ""
 
-#: kallithea/controllers/error.py:71
+#: kallithea/controllers/error.py:67
 #, fuzzy
 msgid "No response"
 msgstr "Neznáma revízia %s"
 
-#: kallithea/controllers/error.py:72
+#: kallithea/controllers/error.py:68
 msgid "Unknown error"
 msgstr ""
 
-#: kallithea/controllers/error.py:85
+#: kallithea/controllers/error.py:81
 msgid ""
 "The request could not be understood by the server due to malformed syntax."
 msgstr ""
 
+#: kallithea/controllers/error.py:84
+msgid "Unauthorized access to resource"
+msgstr ""
+
+#: kallithea/controllers/error.py:86
+msgid "You don't have permission to view this page"
+msgstr "Nemáte oprávnenie na zobrazenie tejto stránky"
+
 #: kallithea/controllers/error.py:88
-msgid "Unauthorized access to resource"
+msgid "The resource could not be found"
 msgstr ""
 
 #: kallithea/controllers/error.py:90
-msgid "You don't have permission to view this page"
-msgstr "Nemáte oprávnenie na zobrazenie tejto stránky"
-
-#: kallithea/controllers/error.py:92
-msgid "The resource could not be found"
-msgstr ""
-
-#: kallithea/controllers/error.py:94
 msgid ""
 "The server encountered an unexpected condition which prevented it from "
 "fulfilling the request."
 msgstr ""
 
-#: kallithea/controllers/feed.py:63
+#: kallithea/controllers/feed.py:59
 #, python-format
 msgid "%s committed on %s"
 msgstr ""
 
-#: kallithea/controllers/feed.py:88
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/controllers/feed.py:84
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/compare/compare_diff.html:95
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
@@ -141,12 +141,12 @@
 msgid "Changeset was too big and was cut off..."
 msgstr ""
 
-#: kallithea/controllers/feed.py:111 kallithea/controllers/feed.py:140
+#: kallithea/controllers/feed.py:107
 #, python-format
 msgid "%s %s feed"
 msgstr ""
 
-#: kallithea/controllers/feed.py:113 kallithea/controllers/feed.py:142
+#: kallithea/controllers/feed.py:109
 #, python-format
 msgid "Changes on %s repository"
 msgstr "Zmeny na repozitáre %s"
@@ -166,103 +166,103 @@
 msgid "%s at %s"
 msgstr ""
 
-#: kallithea/controllers/files.py:296
+#: kallithea/controllers/files.py:295
 msgid "You can only delete files with revision being a valid branch"
 msgstr ""
 
-#: kallithea/controllers/files.py:307
+#: kallithea/controllers/files.py:306
 #, python-format
 msgid "Deleted file %s via Kallithea"
 msgstr "Zmazaný súbor %s cez Kallithea"
 
-#: kallithea/controllers/files.py:331
+#: kallithea/controllers/files.py:330
 #, python-format
 msgid "Successfully deleted file %s"
 msgstr "Úspešne zmazaný súbor %s"
 
-#: kallithea/controllers/files.py:335 kallithea/controllers/files.py:394
-#: kallithea/controllers/files.py:469
+#: kallithea/controllers/files.py:334 kallithea/controllers/files.py:392
+#: kallithea/controllers/files.py:467
 msgid "Error occurred during commit"
 msgstr "Došlo k chybe pri ukladaní"
 
-#: kallithea/controllers/files.py:350
+#: kallithea/controllers/files.py:349
 msgid "You can only edit files with revision being a valid branch"
 msgstr ""
 
-#: kallithea/controllers/files.py:364
+#: kallithea/controllers/files.py:363
 #, python-format
 msgid "Edited file %s via Kallithea"
 msgstr ""
 
-#: kallithea/controllers/files.py:380
+#: kallithea/controllers/files.py:378
 msgid "No changes"
 msgstr "Žiadne zmeny"
 
-#: kallithea/controllers/files.py:390 kallithea/controllers/files.py:458
+#: kallithea/controllers/files.py:388 kallithea/controllers/files.py:456
 #, python-format
 msgid "Successfully committed to %s"
 msgstr ""
 
-#: kallithea/controllers/files.py:409
+#: kallithea/controllers/files.py:407
 msgid "Added file via Kallithea"
 msgstr "Pridaný súbor cez Kallithea"
 
-#: kallithea/controllers/files.py:430
+#: kallithea/controllers/files.py:428
 msgid "No content"
 msgstr "Žiadny obsah"
 
-#: kallithea/controllers/files.py:434
+#: kallithea/controllers/files.py:432
 msgid "No filename"
 msgstr ""
 
-#: kallithea/controllers/files.py:461
+#: kallithea/controllers/files.py:459
 msgid "Location must be relative path and must not contain .. in path"
 msgstr ""
 
-#: kallithea/controllers/files.py:493
+#: kallithea/controllers/files.py:491
 msgid "Downloads disabled"
 msgstr "Sťahovanie vypnuté"
 
-#: kallithea/controllers/files.py:504
+#: kallithea/controllers/files.py:502
 #, python-format
 msgid "Unknown revision %s"
 msgstr "Neznáma revízia %s"
 
-#: kallithea/controllers/files.py:506
+#: kallithea/controllers/files.py:504
 msgid "Empty repository"
 msgstr "Prázdny repozitár"
 
-#: kallithea/controllers/files.py:508
+#: kallithea/controllers/files.py:506
 msgid "Unknown archive type"
 msgstr ""
 
-#: kallithea/controllers/files.py:729
+#: kallithea/controllers/files.py:726
 #: kallithea/templates/changeset/changeset_range.html:9
 #: kallithea/templates/email_templates/pull_request.html:64
 #: kallithea/templates/pullrequests/pullrequest.html:84
 msgid "Changesets"
 msgstr "Zmeny"
 
-#: kallithea/controllers/files.py:730
-#: kallithea/controllers/pullrequests.py:182 kallithea/model/scm.py:676
+#: kallithea/controllers/files.py:727
+#: kallithea/controllers/pullrequests.py:174 kallithea/model/scm.py:663
 msgid "Branches"
 msgstr "Vetvy"
 
-#: kallithea/controllers/files.py:731
-#: kallithea/controllers/pullrequests.py:183 kallithea/model/scm.py:687
+#: kallithea/controllers/files.py:728
+#: kallithea/controllers/pullrequests.py:175 kallithea/model/scm.py:674
 msgid "Tags"
 msgstr "Tagy"
 
-#: kallithea/controllers/forks.py:174
+#: kallithea/controllers/forks.py:175
 #, python-format
 msgid "An error occurred during repository forking %s"
 msgstr ""
 
-#: kallithea/controllers/home.py:79
+#: kallithea/controllers/home.py:77
 msgid "Groups"
 msgstr "Skupiny"
 
-#: kallithea/controllers/home.py:89
+#: kallithea/controllers/home.py:87
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:90
 #: kallithea/templates/admin/repos/repo_add.html:12
 #: kallithea/templates/admin/repos/repo_add.html:16
@@ -274,49 +274,53 @@
 msgid "Repositories"
 msgstr "Repozitáre"
 
-#: kallithea/controllers/home.py:122
+#: kallithea/controllers/home.py:119
 #: kallithea/templates/files/files_add.html:32
 #: kallithea/templates/files/files_delete.html:23
 #: kallithea/templates/files/files_edit.html:32
 msgid "Branch"
 msgstr "Vetva"
 
-#: kallithea/controllers/home.py:128
+#: kallithea/controllers/home.py:125
 msgid "Closed Branches"
 msgstr ""
 
-#: kallithea/controllers/home.py:134
+#: kallithea/controllers/home.py:131
 msgid "Tag"
 msgstr ""
 
-#: kallithea/controllers/home.py:140
+#: kallithea/controllers/home.py:137
 msgid "Bookmark"
 msgstr "Záložka"
 
-#: kallithea/controllers/journal.py:112 kallithea/controllers/journal.py:154
+#: kallithea/controllers/journal.py:146 kallithea/controllers/journal.py:157
 #: kallithea/templates/journal/public_journal.html:4
 #: kallithea/templates/journal/public_journal.html:18
 msgid "Public Journal"
 msgstr ""
 
-#: kallithea/controllers/journal.py:116 kallithea/controllers/journal.py:158
+#: kallithea/controllers/journal.py:150 kallithea/controllers/journal.py:161
 #: kallithea/templates/base/base.html:290
 #: kallithea/templates/journal/journal.html:5
 #: kallithea/templates/journal/journal.html:13
 msgid "Journal"
 msgstr ""
 
-#: kallithea/controllers/login.py:140 kallithea/controllers/login.py:185
+#: kallithea/controllers/login.py:109
+msgid "Authentication failed."
+msgstr ""
+
+#: kallithea/controllers/login.py:142 kallithea/controllers/login.py:187
 #, fuzzy
 msgid "Bad captcha"
 msgstr "zlá captcha"
 
-#: kallithea/controllers/login.py:146
+#: kallithea/controllers/login.py:148
 #, python-format
 msgid "You have successfully registered with %s"
 msgstr ""
 
-#: kallithea/controllers/login.py:190
+#: kallithea/controllers/login.py:192
 msgid "A password reset confirmation code has been sent"
 msgstr ""
 
@@ -329,226 +333,226 @@
 msgid "Successfully updated password"
 msgstr "Úspešne aktualizované heslo"
 
-#: kallithea/controllers/pullrequests.py:68
+#: kallithea/controllers/pullrequests.py:67
 #, python-format
 msgid "Invalid reviewer \"%s\" specified"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:131
+#: kallithea/controllers/pullrequests.py:123
 #, python-format
 msgid "%s (closed)"
 msgstr "%s (zatvorené)"
 
-#: kallithea/controllers/pullrequests.py:158
+#: kallithea/controllers/pullrequests.py:150
 #: kallithea/templates/changeset/changeset.html:12
 msgid "Changeset"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:179
+#: kallithea/controllers/pullrequests.py:171
 msgid "Special"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:180
+#: kallithea/controllers/pullrequests.py:172
 msgid "Peer branches"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:181 kallithea/model/scm.py:682
+#: kallithea/controllers/pullrequests.py:173 kallithea/model/scm.py:669
 msgid "Bookmarks"
 msgstr "Záložky"
 
-#: kallithea/controllers/pullrequests.py:318
+#: kallithea/controllers/pullrequests.py:315
 #, python-format
 msgid "Error creating pull request: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:345
-#: kallithea/controllers/pullrequests.py:368
+#: kallithea/controllers/pullrequests.py:342
+#: kallithea/controllers/pullrequests.py:365
 msgid "Error occurred while creating pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:350
+#: kallithea/controllers/pullrequests.py:347
 msgid "Successfully opened new pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:373
+#: kallithea/controllers/pullrequests.py:370
 msgid "New pull request iteration created"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:401
+#: kallithea/controllers/pullrequests.py:398
 #, python-format
 msgid "Meanwhile, the following reviewers have been added: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:405
+#: kallithea/controllers/pullrequests.py:402
 #, python-format
 msgid "Meanwhile, the following reviewers have been removed: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:421
-#: kallithea/model/pull_request.py:232
+#: kallithea/controllers/pullrequests.py:418
+#: kallithea/model/pull_request.py:230
 msgid "No description"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:430
+#: kallithea/controllers/pullrequests.py:427
 msgid "Pull request updated"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:443
+#: kallithea/controllers/pullrequests.py:440
 msgid "Successfully deleted pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:479
+#: kallithea/controllers/pullrequests.py:476
 #, python-format
 msgid "Revision %s not found in %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:506
+#: kallithea/controllers/pullrequests.py:504
 #, python-format
 msgid "Error: changesets not found when displaying pull request from %s."
 msgstr ""
 
+#: kallithea/controllers/pullrequests.py:518
+#, python-format
+msgid "This pull request has already been merged to %s."
+msgstr ""
+
 #: kallithea/controllers/pullrequests.py:520
-#, python-format
-msgid "This pull request has already been merged to %s."
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:522
 msgid "This pull request has been closed and can not be updated."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:546
+#: kallithea/controllers/pullrequests.py:539
 #, python-format
 msgid "The following additional changes are available on %s:"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:548
-#: kallithea/controllers/pullrequests.py:552
+#: kallithea/controllers/pullrequests.py:541
+#: kallithea/controllers/pullrequests.py:545
 msgid "No additional changesets found for iterating on this pull request."
 msgstr ""
 
+#: kallithea/controllers/pullrequests.py:553
+#, python-format
+msgid "Note: Branch %s has another head: %s."
+msgstr ""
+
 #: kallithea/controllers/pullrequests.py:560
-#, python-format
-msgid "Note: Branch %s has another head: %s."
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:567
 msgid "Git pull requests don't support iterating yet."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:569
+#: kallithea/controllers/pullrequests.py:562
 #, python-format
 msgid ""
 "Error: some changesets not found when displaying pull request from %s."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:593
+#: kallithea/controllers/pullrequests.py:586
 msgid "The diff can't be shown - the PR revisions could not be found."
 msgstr ""
 
+#: kallithea/controllers/search.py:132
+msgid "Invalid search query. Try quoting it."
+msgstr ""
+
 #: kallithea/controllers/search.py:136
-msgid "Invalid search query. Try quoting it."
-msgstr ""
-
-#: kallithea/controllers/search.py:140
 msgid "The server has no search index."
 msgstr ""
 
-#: kallithea/controllers/search.py:143
+#: kallithea/controllers/search.py:139
 msgid "An error occurred during search operation."
 msgstr "Došlo k chybe počas vyhľadávania."
 
-#: kallithea/controllers/summary.py:168
-#: kallithea/templates/summary/summary.html:412
+#: kallithea/controllers/summary.py:169
+#: kallithea/templates/summary/summary.html:410
 msgid "No data ready yet"
 msgstr ""
 
-#: kallithea/controllers/summary.py:171
+#: kallithea/controllers/summary.py:172
 #: kallithea/templates/summary/summary.html:97
 msgid "Statistics are disabled for this repository"
 msgstr ""
 
-#: kallithea/controllers/admin/auth_settings.py:137
+#: kallithea/controllers/admin/auth_settings.py:136
 msgid "Auth settings updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/auth_settings.py:148
+#: kallithea/controllers/admin/auth_settings.py:147
 msgid "error occurred during update of auth settings"
 msgstr ""
 
-#: kallithea/controllers/admin/defaults.py:75
+#: kallithea/controllers/admin/defaults.py:74
 msgid "Default settings updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/defaults.py:90
+#: kallithea/controllers/admin/defaults.py:89
 msgid "Error occurred during update of defaults"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:59
 #: kallithea/controllers/admin/my_account.py:232
-#: kallithea/controllers/admin/users.py:248
+#: kallithea/controllers/admin/users.py:246
 msgid "Forever"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:60
 #: kallithea/controllers/admin/my_account.py:233
-#: kallithea/controllers/admin/users.py:249
+#: kallithea/controllers/admin/users.py:247
 msgid "5 minutes"
 msgstr "5 minút"
 
 #: kallithea/controllers/admin/gists.py:61
 #: kallithea/controllers/admin/my_account.py:234
-#: kallithea/controllers/admin/users.py:250
+#: kallithea/controllers/admin/users.py:248
 msgid "1 hour"
 msgstr "1 hodina"
 
 #: kallithea/controllers/admin/gists.py:62
 #: kallithea/controllers/admin/my_account.py:235
-#: kallithea/controllers/admin/users.py:251
+#: kallithea/controllers/admin/users.py:249
 msgid "1 day"
 msgstr "1 deň"
 
 #: kallithea/controllers/admin/gists.py:63
 #: kallithea/controllers/admin/my_account.py:236
-#: kallithea/controllers/admin/users.py:252
+#: kallithea/controllers/admin/users.py:250
 msgid "1 month"
 msgstr "1 mesiac"
 
 #: kallithea/controllers/admin/gists.py:67
 #: kallithea/controllers/admin/my_account.py:238
-#: kallithea/controllers/admin/users.py:254
+#: kallithea/controllers/admin/users.py:252
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:65
 #: kallithea/templates/admin/users/user_edit_api_keys.html:65
 msgid "Lifetime"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:142
+#: kallithea/controllers/admin/gists.py:148
 msgid "Error occurred during gist creation"
 msgstr "Došlo k chybe pri vytváraní gist"
 
-#: kallithea/controllers/admin/gists.py:158
+#: kallithea/controllers/admin/gists.py:164
 #, python-format
 msgid "Deleted gist %s"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:198
+#: kallithea/controllers/admin/gists.py:207
 msgid "Unmodified"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:228
+#: kallithea/controllers/admin/gists.py:237
 msgid "Successfully updated gist content"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:233
+#: kallithea/controllers/admin/gists.py:242
 msgid "Successfully updated gist data"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:236
+#: kallithea/controllers/admin/gists.py:245
 #, python-format
 msgid "Error occurred during update of gist %s"
 msgstr "Došlo k chybe pri aktualizácii gist %s"
 
-#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:211
-#: kallithea/model/user.py:232
+#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:205
+#: kallithea/model/user.py:226
 msgid "You can't edit this user since it's crucial for entire application"
 msgstr ""
 
@@ -557,7 +561,7 @@
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:134
-#: kallithea/controllers/admin/users.py:181
+#: kallithea/controllers/admin/users.py:179
 #, python-format
 msgid "Error occurred during update of user %s"
 msgstr ""
@@ -567,44 +571,44 @@
 msgstr "Došlo k chybe pri aktualizácii hesla užívateľa"
 
 #: kallithea/controllers/admin/my_account.py:209
-#: kallithea/controllers/admin/users.py:367
+#: kallithea/controllers/admin/users.py:365
 #, python-format
 msgid "Added email %s to user"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:215
-#: kallithea/controllers/admin/users.py:373
+#: kallithea/controllers/admin/users.py:371
 msgid "An error occurred during email saving"
 msgstr "Došlo k chybe pri ukladaní e-mailovej adresy"
 
 #: kallithea/controllers/admin/my_account.py:224
-#: kallithea/controllers/admin/users.py:383
+#: kallithea/controllers/admin/users.py:381
 msgid "Removed email from user"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:248
-#: kallithea/controllers/admin/users.py:271
+#: kallithea/controllers/admin/users.py:269
 msgid "API key successfully created"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:257
-#: kallithea/controllers/admin/users.py:281
+#: kallithea/controllers/admin/users.py:279
 msgid "API key successfully reset"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:261
-#: kallithea/controllers/admin/users.py:285
+#: kallithea/controllers/admin/users.py:283
 msgid "API key successfully deleted"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:281
-#: kallithea/controllers/admin/users.py:456
+#: kallithea/controllers/admin/users.py:454
 #, python-format
 msgid "SSH key %s successfully added"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:293
-#: kallithea/controllers/admin/users.py:470
+#: kallithea/controllers/admin/users.py:468
 #, fuzzy
 #| msgid "Successfully deleted file %s"
 msgid "SSH key successfully deleted"
@@ -682,11 +686,11 @@
 msgid "Allowed with automatic account activation"
 msgstr ""
 
-#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1673
+#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1578
 msgid "Manual activation of external account"
 msgstr ""
 
-#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1674
+#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1579
 msgid "Automatic activation of external account"
 msgstr ""
 
@@ -708,341 +712,333 @@
 msgid "Error occurred during update of permissions"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:172
+#: kallithea/controllers/admin/repo_groups.py:165
 #, python-format
 msgid "Error occurred during creation of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:177
+#: kallithea/controllers/admin/repo_groups.py:172
 #, python-format
 msgid "Created repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:224
+#: kallithea/controllers/admin/repo_groups.py:219
 #, python-format
 msgid "Updated repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:240
+#: kallithea/controllers/admin/repo_groups.py:235
 #, python-format
 msgid "Error occurred during update of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:250
+#: kallithea/controllers/admin/repo_groups.py:245
 #, python-format
 msgid "This group contains %s repositories and cannot be deleted"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:257
+#: kallithea/controllers/admin/repo_groups.py:252
 #, python-format
 msgid "This group contains %s subgroups and cannot be deleted"
 msgstr ""
 
+#: kallithea/controllers/admin/repo_groups.py:258
+#, python-format
+msgid "Removed repository group %s"
+msgstr ""
+
 #: kallithea/controllers/admin/repo_groups.py:263
 #, python-format
-msgid "Removed repository group %s"
-msgstr ""
-
-#: kallithea/controllers/admin/repo_groups.py:268
-#, python-format
 msgid "Error occurred during deletion of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:352
-#: kallithea/controllers/admin/repo_groups.py:382
-#: kallithea/controllers/admin/user_groups.py:294
+#: kallithea/controllers/admin/repo_groups.py:347
+#: kallithea/controllers/admin/repo_groups.py:377
+#: kallithea/controllers/admin/user_groups.py:290
 msgid "Cannot revoke permission for yourself as admin"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:367
+#: kallithea/controllers/admin/repo_groups.py:362
 msgid "Repository group permissions updated"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:399
-#: kallithea/controllers/admin/repos.py:358
-#: kallithea/controllers/admin/user_groups.py:306
+#: kallithea/controllers/admin/repo_groups.py:394
+#: kallithea/controllers/admin/repos.py:357
+#: kallithea/controllers/admin/user_groups.py:302
 msgid "An error occurred during revoking of permission"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:136
+#: kallithea/controllers/admin/repos.py:137
 #, python-format
 msgid "Error creating repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:194
+#: kallithea/controllers/admin/repos.py:193
 #, python-format
 msgid "Created repository %s from %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:203
+#: kallithea/controllers/admin/repos.py:202
 #, python-format
 msgid "Forked repository %s as %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:206
+#: kallithea/controllers/admin/repos.py:205
 #, python-format
 msgid "Created repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:235
+#: kallithea/controllers/admin/repos.py:234
 #, python-format
 msgid "Repository %s updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:255
+#: kallithea/controllers/admin/repos.py:254
 #, python-format
 msgid "Error occurred during update of repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:273
+#: kallithea/controllers/admin/repos.py:272
 #, python-format
 msgid "Detached %s forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:276
+#: kallithea/controllers/admin/repos.py:275
 #, python-format
 msgid "Deleted %s forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:281
+#: kallithea/controllers/admin/repos.py:280
 #, python-format
 msgid "Deleted repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:284
+#: kallithea/controllers/admin/repos.py:283
 #, python-format
 msgid "Cannot delete repository %s which still has forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:289
+#: kallithea/controllers/admin/repos.py:288
 #, python-format
 msgid "An error occurred during deletion of %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:329
+#: kallithea/controllers/admin/repos.py:328
 msgid "Repository permissions updated"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:388
+#: kallithea/controllers/admin/repos.py:387
 #, python-format
 msgid "Field validation error: %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:391
+#: kallithea/controllers/admin/repos.py:390
 #, fuzzy, python-format
 #| msgid "Error occurred during gist creation"
 msgid "An error occurred during creation of field: %r"
 msgstr "Došlo k chybe pri vytváraní gist"
 
-#: kallithea/controllers/admin/repos.py:402
+#: kallithea/controllers/admin/repos.py:401
 msgid "An error occurred during removal of field"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:416
+#: kallithea/controllers/admin/repos.py:415
 msgid "-- Not a fork --"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:448
+#: kallithea/controllers/admin/repos.py:447
 msgid "Updated repository visibility in public journal"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:452
+#: kallithea/controllers/admin/repos.py:451
 msgid "An error occurred during setting this repository in public journal"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:468
+#: kallithea/controllers/admin/repos.py:467
 msgid "Nothing"
 msgstr "Nič"
 
-#: kallithea/controllers/admin/repos.py:470
+#: kallithea/controllers/admin/repos.py:469
 #, python-format
 msgid "Marked repository %s as fork of %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:477
+#: kallithea/controllers/admin/repos.py:476
 msgid "An error occurred during this operation"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:490
-msgid "Cache invalidation successful"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:494
-msgid "An error occurred during cache invalidation"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:507
+#: kallithea/controllers/admin/repos.py:488
 msgid "Pulled from remote location"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:510
+#: kallithea/controllers/admin/repos.py:491
 msgid "An error occurred during pull from remote location"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:541
+#: kallithea/controllers/admin/repos.py:522
 msgid "An error occurred during deletion of repository stats"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:131
+#: kallithea/controllers/admin/settings.py:132
 msgid "Updated VCS settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:135 kallithea/lib/utils.py:237
+#: kallithea/controllers/admin/settings.py:136
 msgid ""
 "Unable to activate hgsubversion support. The \"hgsubversion\" library is "
 "missing"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:141
-#: kallithea/controllers/admin/settings.py:233
+#: kallithea/controllers/admin/settings.py:142
+#: kallithea/controllers/admin/settings.py:234
 msgid "Error occurred while updating application settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:176
+#: kallithea/controllers/admin/settings.py:177
 #, python-format
 msgid "Repositories successfully rescanned. Added: %s. Removed: %s."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:188
+#: kallithea/controllers/admin/settings.py:189
 #, fuzzy, python-format
 #| msgid "Watched Repositories"
 msgid "Invalidated %s repositories"
 msgstr "Repozitáre"
 
-#: kallithea/controllers/admin/settings.py:229
+#: kallithea/controllers/admin/settings.py:230
 msgid "Updated application settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:283
+#: kallithea/controllers/admin/settings.py:284
 msgid "Updated visualisation settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:288
+#: kallithea/controllers/admin/settings.py:289
 msgid "Error occurred during updating visualisation settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:312
+#: kallithea/controllers/admin/settings.py:313
 msgid "Please enter email address"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:327
+#: kallithea/controllers/admin/settings.py:328
 msgid "Send email task created"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:355
+#: kallithea/controllers/admin/settings.py:356
 msgid "Hook already exists"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:357
+#: kallithea/controllers/admin/settings.py:358
 msgid "Builtin hooks are read-only. Please use another hook name."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:360
+#: kallithea/controllers/admin/settings.py:361
 msgid "Added new hook"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:376
+#: kallithea/controllers/admin/settings.py:377
 msgid "Updated hooks"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:380
+#: kallithea/controllers/admin/settings.py:381
 msgid "Error occurred during hook creation"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:404
+#: kallithea/controllers/admin/settings.py:405
 msgid "Whoosh reindex task scheduled"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:138
+#: kallithea/controllers/admin/user_groups.py:134
 #, python-format
 msgid "Created user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:151
+#: kallithea/controllers/admin/user_groups.py:147
 #, python-format
 msgid "Error occurred during creation of user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:179
+#: kallithea/controllers/admin/user_groups.py:175
 #, python-format
 msgid "Updated user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:201
+#: kallithea/controllers/admin/user_groups.py:197
 #, python-format
 msgid "Error occurred during update of user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:212
+#: kallithea/controllers/admin/user_groups.py:208
 msgid "Successfully deleted user group"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:217
+#: kallithea/controllers/admin/user_groups.py:213
 msgid "An error occurred during deletion of user group"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:273
+#: kallithea/controllers/admin/user_groups.py:269
 msgid "Target group cannot be the same"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:279
+#: kallithea/controllers/admin/user_groups.py:275
 msgid "User group permissions updated"
 msgstr ""
 
+#: kallithea/controllers/admin/user_groups.py:384
+#: kallithea/controllers/admin/users.py:336
+msgid "Updated permissions"
+msgstr ""
+
 #: kallithea/controllers/admin/user_groups.py:388
-#: kallithea/controllers/admin/users.py:338
-msgid "Updated permissions"
-msgstr ""
-
-#: kallithea/controllers/admin/user_groups.py:392
-#: kallithea/controllers/admin/users.py:342
+#: kallithea/controllers/admin/users.py:340
 msgid "An error occurred during permissions saving"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:123
+#: kallithea/controllers/admin/users.py:121
 #, python-format
 msgid "Created user %s"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:138
+#: kallithea/controllers/admin/users.py:136
 #, python-format
 msgid "Error occurred during creation of user %s"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:162
+#: kallithea/controllers/admin/users.py:160
 msgid "User updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:190
+#: kallithea/controllers/admin/users.py:188
 msgid "Successfully deleted user"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:195
+#: kallithea/controllers/admin/users.py:193
 msgid "An error occurred during deletion of user"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:203
+#: kallithea/controllers/admin/users.py:201
 msgid "The default user cannot be edited"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:409
+#: kallithea/controllers/admin/users.py:407
 #, python-format
 msgid "Added IP address %s to user whitelist"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:415
+#: kallithea/controllers/admin/users.py:413
 msgid "An error occurred while adding IP address"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:427
+#: kallithea/controllers/admin/users.py:425
 msgid "Removed IP address from user whitelist"
 msgstr ""
 
-#: kallithea/lib/auth.py:684
+#: kallithea/lib/auth.py:634
 msgid "You need to be a registered user to perform this action"
 msgstr ""
 
-#: kallithea/lib/auth.py:712
+#: kallithea/lib/auth.py:662
 msgid "You need to be signed in to view this page"
 msgstr ""
 
@@ -1073,171 +1069,171 @@
 "Changeset was too big and was cut off, use diff menu to display this diff"
 msgstr ""
 
-#: kallithea/lib/diffs.py:224
+#: kallithea/lib/diffs.py:223
 msgid "No changes detected"
 msgstr ""
 
-#: kallithea/lib/helpers.py:653
+#: kallithea/lib/helpers.py:670
 #, python-format
 msgid "Deleted branch: %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:655
+#: kallithea/lib/helpers.py:672
 #, python-format
 msgid "Created tag: %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:666
+#: kallithea/lib/helpers.py:683
 #, fuzzy, python-format
 #| msgid "Set changeset status"
 msgid "Changeset %s not found"
 msgstr "Zmeny"
 
-#: kallithea/lib/helpers.py:715
+#: kallithea/lib/helpers.py:732
 #, python-format
 msgid "Show all combined changesets %s->%s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:721
+#: kallithea/lib/helpers.py:738
 msgid "Compare view"
 msgstr ""
 
-#: kallithea/lib/helpers.py:740
+#: kallithea/lib/helpers.py:757
 msgid "and"
 msgstr ""
 
-#: kallithea/lib/helpers.py:741
+#: kallithea/lib/helpers.py:758
 #, python-format
 msgid "%s more"
 msgstr ""
 
-#: kallithea/lib/helpers.py:742
+#: kallithea/lib/helpers.py:759
 #: kallithea/templates/changelog/changelog.html:43
 msgid "revisions"
 msgstr ""
 
-#: kallithea/lib/helpers.py:766
+#: kallithea/lib/helpers.py:783
 #, python-format
 msgid "Fork name %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:787
+#: kallithea/lib/helpers.py:804
 #, python-format
 msgid "Pull request %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:797
+#: kallithea/lib/helpers.py:814
 msgid "[deleted] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:799 kallithea/lib/helpers.py:811
+#: kallithea/lib/helpers.py:816 kallithea/lib/helpers.py:828
 msgid "[created] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:801
+#: kallithea/lib/helpers.py:818
 msgid "[created] repository as fork"
 msgstr ""
 
-#: kallithea/lib/helpers.py:803 kallithea/lib/helpers.py:813
+#: kallithea/lib/helpers.py:820 kallithea/lib/helpers.py:830
 msgid "[forked] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:805 kallithea/lib/helpers.py:815
+#: kallithea/lib/helpers.py:822 kallithea/lib/helpers.py:832
 msgid "[updated] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:807
+#: kallithea/lib/helpers.py:824
 msgid "[downloaded] archive from repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:809
+#: kallithea/lib/helpers.py:826
 msgid "[delete] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:817
+#: kallithea/lib/helpers.py:834
 msgid "[created] user"
 msgstr ""
 
-#: kallithea/lib/helpers.py:819
+#: kallithea/lib/helpers.py:836
 msgid "[updated] user"
 msgstr ""
 
-#: kallithea/lib/helpers.py:821
+#: kallithea/lib/helpers.py:838
 msgid "[created] user group"
 msgstr ""
 
-#: kallithea/lib/helpers.py:823
+#: kallithea/lib/helpers.py:840
 msgid "[updated] user group"
 msgstr ""
 
-#: kallithea/lib/helpers.py:825
+#: kallithea/lib/helpers.py:842
 msgid "[commented] on revision in repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:827
+#: kallithea/lib/helpers.py:844
 msgid "[commented] on pull request for"
 msgstr ""
 
-#: kallithea/lib/helpers.py:829
+#: kallithea/lib/helpers.py:846
 msgid "[closed] pull request for"
 msgstr ""
 
-#: kallithea/lib/helpers.py:831
+#: kallithea/lib/helpers.py:848
 msgid "[pushed] into"
 msgstr ""
 
-#: kallithea/lib/helpers.py:833
+#: kallithea/lib/helpers.py:850
 msgid "[committed via Kallithea] into repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:835
+#: kallithea/lib/helpers.py:852
 msgid "[pulled from remote] into repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:837
+#: kallithea/lib/helpers.py:854
 msgid "[pulled] from"
 msgstr ""
 
-#: kallithea/lib/helpers.py:839
+#: kallithea/lib/helpers.py:856
 msgid "[started following] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:841
+#: kallithea/lib/helpers.py:858
 msgid "[stopped following] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:961
+#: kallithea/lib/helpers.py:975
 #, python-format
 msgid " and %s more"
 msgstr ""
 
-#: kallithea/lib/helpers.py:965
+#: kallithea/lib/helpers.py:979
 #: kallithea/templates/compare/compare_diff.html:69
 #: kallithea/templates/pullrequests/pullrequest_show.html:297
 msgid "No files"
 msgstr ""
 
-#: kallithea/lib/helpers.py:990
+#: kallithea/lib/helpers.py:1004
 msgid "new file"
 msgstr ""
 
-#: kallithea/lib/helpers.py:993
+#: kallithea/lib/helpers.py:1007
 msgid "mod"
 msgstr ""
 
-#: kallithea/lib/helpers.py:996
+#: kallithea/lib/helpers.py:1010
 msgid "del"
 msgstr ""
 
-#: kallithea/lib/helpers.py:999
+#: kallithea/lib/helpers.py:1013
 msgid "rename"
 msgstr ""
 
-#: kallithea/lib/helpers.py:1004
+#: kallithea/lib/helpers.py:1018
 msgid "chmod"
 msgstr ""
 
-#: kallithea/lib/helpers.py:1297
+#: kallithea/lib/helpers.py:1323
 #, python-format
 msgid ""
 "%s repository is not mapped to db perhaps it was created or renamed from "
@@ -1245,34 +1241,36 @@
 "repositories"
 msgstr ""
 
-#: kallithea/lib/ssh.py:73
+#: kallithea/lib/ssh.py:75
 msgid "SSH key is missing"
 msgstr ""
 
-#: kallithea/lib/ssh.py:77
-msgid "Incorrect SSH key - it must have both a key type and a base64 part"
-msgstr ""
-
-#: kallithea/lib/ssh.py:81
+#: kallithea/lib/ssh.py:79
+msgid ""
+"Incorrect SSH key - it must have both a key type and a base64 part, like "
+"'ssh-rsa ASRNeaZu4FA...xlJp='"
+msgstr ""
+
+#: kallithea/lib/ssh.py:83
 msgid "Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'"
 msgstr ""
 
-#: kallithea/lib/ssh.py:84
+#: kallithea/lib/ssh.py:86
 #, python-format
 msgid "Incorrect SSH key - unexpected characters in base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:89
+#: kallithea/lib/ssh.py:91
 #, python-format
 msgid "Incorrect SSH key - failed to decode base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:92
+#: kallithea/lib/ssh.py:94
 #, python-format
 msgid "Incorrect SSH key - base64 part is not %r as claimed but %r"
 msgstr ""
 
-#: kallithea/lib/utils2.py:334
+#: kallithea/lib/utils2.py:253
 #, python-format
 msgid "%d year"
 msgid_plural "%d years"
@@ -1280,7 +1278,7 @@
 msgstr[1] ""
 msgstr[2] ""
 
-#: kallithea/lib/utils2.py:335
+#: kallithea/lib/utils2.py:254
 #, python-format
 msgid "%d month"
 msgid_plural "%d months"
@@ -1288,7 +1286,7 @@
 msgstr[1] ""
 msgstr[2] ""
 
-#: kallithea/lib/utils2.py:336
+#: kallithea/lib/utils2.py:255
 #, python-format
 msgid "%d day"
 msgid_plural "%d days"
@@ -1296,7 +1294,7 @@
 msgstr[1] ""
 msgstr[2] ""
 
-#: kallithea/lib/utils2.py:337
+#: kallithea/lib/utils2.py:256
 #, python-format
 msgid "%d hour"
 msgid_plural "%d hours"
@@ -1304,7 +1302,7 @@
 msgstr[1] ""
 msgstr[2] ""
 
-#: kallithea/lib/utils2.py:338
+#: kallithea/lib/utils2.py:257
 #, python-format
 msgid "%d minute"
 msgid_plural "%d minutes"
@@ -1312,7 +1310,7 @@
 msgstr[1] ""
 msgstr[2] ""
 
-#: kallithea/lib/utils2.py:339
+#: kallithea/lib/utils2.py:258
 #, python-format
 msgid "%d second"
 msgid_plural "%d seconds"
@@ -1320,27 +1318,27 @@
 msgstr[1] ""
 msgstr[2] ""
 
-#: kallithea/lib/utils2.py:355
+#: kallithea/lib/utils2.py:274
 #, python-format
 msgid "in %s"
 msgstr ""
 
-#: kallithea/lib/utils2.py:357
+#: kallithea/lib/utils2.py:276
 #, python-format
 msgid "%s ago"
 msgstr ""
 
-#: kallithea/lib/utils2.py:359
+#: kallithea/lib/utils2.py:278
 #, python-format
 msgid "in %s and %s"
 msgstr ""
 
-#: kallithea/lib/utils2.py:362
+#: kallithea/lib/utils2.py:281
 #, python-format
 msgid "%s and %s ago"
 msgstr ""
 
-#: kallithea/lib/utils2.py:365
+#: kallithea/lib/utils2.py:284
 msgid "just now"
 msgstr ""
 
@@ -1349,135 +1347,135 @@
 msgid "on line %s"
 msgstr ""
 
-#: kallithea/model/comment.py:221 kallithea/model/pull_request.py:114
+#: kallithea/model/comment.py:219 kallithea/model/pull_request.py:112
 msgid "[Mention]"
 msgstr ""
 
-#: kallithea/model/db.py:1496
+#: kallithea/model/db.py:1411
 msgid "top level"
 msgstr ""
 
-#: kallithea/model/db.py:1637
+#: kallithea/model/db.py:1542
 msgid "Kallithea Administrator"
 msgstr ""
 
-#: kallithea/model/db.py:1639
+#: kallithea/model/db.py:1544
 msgid "Default user has no access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1640
+#: kallithea/model/db.py:1545
 msgid "Default user has read access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1641
+#: kallithea/model/db.py:1546
 msgid "Default user has write access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1642
+#: kallithea/model/db.py:1547
 msgid "Default user has admin access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1644
+#: kallithea/model/db.py:1549
 msgid "Default user has no access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1645
+#: kallithea/model/db.py:1550
 msgid "Default user has read access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1646
+#: kallithea/model/db.py:1551
 msgid "Default user has write access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1647
+#: kallithea/model/db.py:1552
 msgid "Default user has admin access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1649
+#: kallithea/model/db.py:1554
 msgid "Default user has no access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1650
+#: kallithea/model/db.py:1555
 msgid "Default user has read access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1651
+#: kallithea/model/db.py:1556
 msgid "Default user has write access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1652
+#: kallithea/model/db.py:1557
 msgid "Default user has admin access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1654
+#: kallithea/model/db.py:1559
 msgid "Only admins can create repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1655
+#: kallithea/model/db.py:1560
 msgid "Non-admins can create repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1657
+#: kallithea/model/db.py:1562
 msgid "Only admins can create user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1658
+#: kallithea/model/db.py:1563
 msgid "Non-admins can create user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1660
+#: kallithea/model/db.py:1565
 msgid "Only admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1661
+#: kallithea/model/db.py:1566
 msgid "Non-admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1663
+#: kallithea/model/db.py:1568
 msgid ""
 "Repository creation enabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1664
+#: kallithea/model/db.py:1569
 msgid ""
 "Repository creation disabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1666
+#: kallithea/model/db.py:1571
 #, fuzzy
 msgid "Only admins can fork repositories"
 msgstr "Repozitáre"
 
-#: kallithea/model/db.py:1667
+#: kallithea/model/db.py:1572
 #, fuzzy
 msgid "Non-admins can fork repositories"
 msgstr "Repozitáre"
 
-#: kallithea/model/db.py:1669
+#: kallithea/model/db.py:1574
 msgid "Registration disabled"
 msgstr ""
 
-#: kallithea/model/db.py:1670
+#: kallithea/model/db.py:1575
 msgid "User registration with manual account activation"
 msgstr ""
 
-#: kallithea/model/db.py:1671
+#: kallithea/model/db.py:1576
 msgid "User registration with automatic account activation"
 msgstr ""
 
-#: kallithea/model/db.py:2206
+#: kallithea/model/db.py:1992
 msgid "Not reviewed"
 msgstr ""
 
-#: kallithea/model/db.py:2207
+#: kallithea/model/db.py:1993
 msgid "Under review"
 msgstr ""
 
-#: kallithea/model/db.py:2208
+#: kallithea/model/db.py:1994
 msgid "Not approved"
 msgstr ""
 
-#: kallithea/model/db.py:2209
+#: kallithea/model/db.py:1995
 msgid "Approved"
 msgstr ""
 
@@ -1503,315 +1501,315 @@
 msgid "Name must not contain only digits"
 msgstr ""
 
-#: kallithea/model/notification.py:164
+#: kallithea/model/notification.py:162
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s changeset %(short_id)s \"%(message_short)s\" on "
 "%(branch)s"
 msgstr ""
 
-#: kallithea/model/notification.py:167
+#: kallithea/model/notification.py:165
 #, python-format
 msgid "New user %(new_username)s registered"
 msgstr ""
 
-#: kallithea/model/notification.py:169
+#: kallithea/model/notification.py:167
 #, python-format
 msgid ""
 "[Review] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:170
+#: kallithea/model/notification.py:168
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:183
+#: kallithea/model/notification.py:188
 msgid "Closing"
 msgstr ""
 
-#: kallithea/model/pull_request.py:73
+#: kallithea/model/pull_request.py:72
 #, python-format
 msgid ""
 "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:209
+#: kallithea/model/pull_request.py:207
 msgid "Cannot create empty pull request"
 msgstr ""
 
-#: kallithea/model/pull_request.py:217
+#: kallithea/model/pull_request.py:215
 #, python-format
 msgid ""
 "Cannot create pull request - criss cross merge detected, please merge a "
 "later %s revision to %s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:245 kallithea/model/pull_request.py:380
+#: kallithea/model/pull_request.py:243 kallithea/model/pull_request.py:378
 msgid "You are not authorized to create the pull request"
 msgstr ""
 
-#: kallithea/model/pull_request.py:339
+#: kallithea/model/pull_request.py:337
 msgid "Missing changesets since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:346
+#: kallithea/model/pull_request.py:344
 #, python-format
 msgid "New changesets on %s %s since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:353
+#: kallithea/model/pull_request.py:351
 msgid "Ancestor didn't change - diff since previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:360
+#: kallithea/model/pull_request.py:358
 #, python-format
 msgid ""
 "This iteration is based on another %s revision and there is no simple "
 "diff."
 msgstr ""
 
-#: kallithea/model/pull_request.py:362
+#: kallithea/model/pull_request.py:360
 #, python-format
 msgid "No changes found on %s %s since previous iteration."
 msgstr ""
 
-#: kallithea/model/pull_request.py:388
+#: kallithea/model/pull_request.py:386
 #, python-format
 msgid "Closed, next iteration: %s ."
 msgstr ""
 
-#: kallithea/model/scm.py:668
+#: kallithea/model/scm.py:655
 msgid "latest tip"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:56
+#: kallithea/model/ssh_key.py:57
 #, python-format
 msgid "SSH key %r is invalid: %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:68
+#: kallithea/model/ssh_key.py:69
 #, python-format
 msgid "SSH key %s is already used by %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:89
+#: kallithea/model/ssh_key.py:88
 #, fuzzy, python-format
 #| msgid "Set changeset status"
-msgid "SSH key %r not found"
+msgid "SSH key with fingerprint %r found"
 msgstr "Zmeny"
 
-#: kallithea/model/user.py:186
+#: kallithea/model/user.py:180
 msgid "New user registration"
 msgstr ""
 
-#: kallithea/model/user.py:250
+#: kallithea/model/user.py:244
 msgid ""
 "You can't remove this user since it is crucial for the entire application"
 msgstr ""
 
-#: kallithea/model/user.py:255
+#: kallithea/model/user.py:249
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repositories and cannot be removed. Switch "
 "owners or remove those repositories: %s"
 msgstr ""
 
-#: kallithea/model/user.py:260
+#: kallithea/model/user.py:254
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repository groups and cannot be removed. Switch "
 "owners or remove those repository groups: %s"
 msgstr ""
 
-#: kallithea/model/user.py:267
+#: kallithea/model/user.py:261
 #, python-format
 msgid ""
 "User \"%s\" still owns %s user groups and cannot be removed. Switch "
 "owners or remove those user groups: %s"
 msgstr ""
 
-#: kallithea/model/user.py:361
+#: kallithea/model/user.py:355
 msgid "Password reset link"
 msgstr ""
 
-#: kallithea/model/user.py:408
+#: kallithea/model/user.py:402
 msgid "Password reset notification"
 msgstr ""
 
-#: kallithea/model/user.py:409
+#: kallithea/model/user.py:403
 #, python-format
 msgid ""
 "The password to your account %s has been changed using password reset "
 "form."
 msgstr ""
 
-#: kallithea/model/validators.py:52 kallithea/model/validators.py:53
+#: kallithea/model/validators.py:53 kallithea/model/validators.py:54
 msgid "Value cannot be an empty list"
 msgstr ""
 
-#: kallithea/model/validators.py:72
+#: kallithea/model/validators.py:73
 #, python-format
 msgid "Username \"%(username)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:74
+#: kallithea/model/validators.py:75
 #, python-format
 msgid "Username \"%(username)s\" cannot be used"
 msgstr ""
 
-#: kallithea/model/validators.py:76
+#: kallithea/model/validators.py:77
 msgid ""
 "Username may only contain alphanumeric characters underscores, periods or "
 "dashes and must begin with an alphanumeric character or underscore"
 msgstr ""
 
-#: kallithea/model/validators.py:103
+#: kallithea/model/validators.py:104
 msgid "The input is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:110
+#: kallithea/model/validators.py:111
 #, python-format
 msgid "Username %(username)s is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:131
-msgid "Invalid user group name"
-msgstr ""
-
 #: kallithea/model/validators.py:132
+msgid "Invalid user group name"
+msgstr ""
+
+#: kallithea/model/validators.py:133
 #, python-format
 msgid "User group \"%(usergroup)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:134
+#: kallithea/model/validators.py:135
 msgid ""
 "user group name may only contain alphanumeric characters underscores, "
 "periods or dashes and must begin with alphanumeric character"
 msgstr ""
 
-#: kallithea/model/validators.py:174
-msgid "Cannot assign this group as parent"
-msgstr ""
-
 #: kallithea/model/validators.py:175
+msgid "Cannot assign this group as parent"
+msgstr ""
+
+#: kallithea/model/validators.py:176
 #, python-format
 msgid "Group \"%(group_name)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:177
+#: kallithea/model/validators.py:178
 #, python-format
 msgid "Repository with name \"%(group_name)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:233
+#: kallithea/model/validators.py:230
 msgid "Invalid characters (non-ascii) in password"
 msgstr ""
 
-#: kallithea/model/validators.py:248
+#: kallithea/model/validators.py:245
 msgid "Invalid old password"
 msgstr ""
 
-#: kallithea/model/validators.py:264
+#: kallithea/model/validators.py:261
 msgid "Passwords do not match"
 msgstr ""
 
-#: kallithea/model/validators.py:279
+#: kallithea/model/validators.py:276
 msgid "Invalid username or password"
 msgstr ""
 
+#: kallithea/model/validators.py:310
+#, python-format
+msgid "Repository name %(repo)s is not allowed"
+msgstr ""
+
+#: kallithea/model/validators.py:312
+#, python-format
+msgid "Repository named %(repo)s already exists"
+msgstr ""
+
 #: kallithea/model/validators.py:313
 #, python-format
-msgid "Repository name %(repo)s is not allowed"
+msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
 msgstr ""
 
 #: kallithea/model/validators.py:315
 #, python-format
-msgid "Repository named %(repo)s already exists"
-msgstr ""
-
-#: kallithea/model/validators.py:316
-#, python-format
-msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
-msgstr ""
-
-#: kallithea/model/validators.py:318
-#, python-format
 msgid "Repository group with name \"%(repo)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:404
+#: kallithea/model/validators.py:401
 #, fuzzy
 msgid "Invalid repository URL"
 msgstr "Odblokovať repozitár"
 
-#: kallithea/model/validators.py:405
+#: kallithea/model/validators.py:402
 msgid ""
 "Invalid repository URL. It must be a valid http, https, ssh, svn+http or "
 "svn+https URL"
 msgstr ""
 
-#: kallithea/model/validators.py:430
+#: kallithea/model/validators.py:427
 msgid "Fork has to be the same type as parent"
 msgstr ""
 
-#: kallithea/model/validators.py:445
+#: kallithea/model/validators.py:442
 msgid "You don't have permissions to create repository in this group"
 msgstr ""
 
-#: kallithea/model/validators.py:447
+#: kallithea/model/validators.py:444
 msgid "no permission to create repository in root location"
 msgstr ""
 
-#: kallithea/model/validators.py:497
+#: kallithea/model/validators.py:494
 msgid "You don't have permissions to create a group in this location"
 msgstr ""
 
-#: kallithea/model/validators.py:537
+#: kallithea/model/validators.py:534
 msgid "This username or user group name is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:630
+#: kallithea/model/validators.py:627
 msgid "This is not a valid path"
 msgstr ""
 
-#: kallithea/model/validators.py:647
+#: kallithea/model/validators.py:644
 msgid "This email address is already in use"
 msgstr ""
 
-#: kallithea/model/validators.py:667
+#: kallithea/model/validators.py:664
 #, python-format
 msgid "Email address \"%(email)s\" not found"
 msgstr ""
 
-#: kallithea/model/validators.py:704
+#: kallithea/model/validators.py:701
 msgid ""
 "The LDAP Login attribute of the CN must be specified - this is the name "
 "of the attribute that is equivalent to \"username\""
 msgstr ""
 
-#: kallithea/model/validators.py:716
+#: kallithea/model/validators.py:713
 msgid "Please enter a valid IPv4 or IPv6 address"
 msgstr ""
 
-#: kallithea/model/validators.py:717
+#: kallithea/model/validators.py:714
 #, python-format
 msgid ""
 "The network size (bits) must be within the range of 0-32 (not %(bits)r)"
 msgstr ""
 
-#: kallithea/model/validators.py:750
+#: kallithea/model/validators.py:747
 msgid "Key name can only consist of letters, underscore, dash or numbers"
 msgstr ""
 
-#: kallithea/model/validators.py:764
+#: kallithea/model/validators.py:761
 msgid "Filename cannot be inside a directory"
 msgstr ""
 
-#: kallithea/model/validators.py:780
+#: kallithea/model/validators.py:777
 #, python-format
 msgid "Plugins %(loaded)s and %(next_to_load)s both export the same name"
 msgstr ""
@@ -1871,7 +1869,7 @@
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:60
 #: kallithea/templates/email_templates/pull_request.html:37
 #: kallithea/templates/forks/fork.html:34
-#: kallithea/templates/index_base.html:58
+#: kallithea/templates/index_base.html:59
 #: kallithea/templates/pullrequests/pullrequest.html:33
 #: kallithea/templates/pullrequests/pullrequest_show.html:38
 #: kallithea/templates/pullrequests/pullrequest_show.html:59
@@ -1879,14 +1877,14 @@
 msgid "Description"
 msgstr ""
 
-#: kallithea/templates/index_base.html:60
+#: kallithea/templates/index_base.html:61
 msgid "Last Change"
 msgstr ""
 
 #: kallithea/templates/admin/my_account/my_account_repos.html:15
 #: kallithea/templates/admin/my_account/my_account_watched.html:15
 #: kallithea/templates/admin/repos/repos.html:41
-#: kallithea/templates/index_base.html:62
+#: kallithea/templates/index_base.html:63
 msgid "Tip"
 msgstr ""
 
@@ -1896,7 +1894,7 @@
 #: kallithea/templates/admin/repos/repos.html:42
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:8
 #: kallithea/templates/admin/user_groups/user_groups.html:42
-#: kallithea/templates/index_base.html:63
+#: kallithea/templates/index_base.html:64
 #: kallithea/templates/pullrequests/pullrequest_data.html:16
 #: kallithea/templates/pullrequests/pullrequest_show.html:124
 #: kallithea/templates/pullrequests/pullrequest_show.html:219
@@ -1920,7 +1918,7 @@
 #: kallithea/templates/admin/users/user_edit_profile.html:18
 #: kallithea/templates/admin/users/users.html:37
 #: kallithea/templates/base/base.html:364
-#: kallithea/templates/email_templates/registration.html:11
+#: kallithea/templates/email_templates/registration.html:12
 #: kallithea/templates/login.html:28 kallithea/templates/register.html:31
 msgid "Username"
 msgstr ""
@@ -2044,7 +2042,7 @@
 #: kallithea/templates/admin/settings/settings.html:31
 #: kallithea/templates/admin/users/user_add.html:62
 #: kallithea/templates/admin/users/user_edit_profile.html:25
-#: kallithea/templates/email_templates/registration.html:33
+#: kallithea/templates/email_templates/registration.html:34
 #: kallithea/templates/register.html:66
 msgid "Email"
 msgstr ""
@@ -2293,7 +2291,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/gists/index.html:51
-#: kallithea/templates/data_table/_dt_elements.html:78
+#: kallithea/templates/data_table/_dt_elements.html:84
 msgid "Created"
 msgstr ""
 
@@ -2377,13 +2375,13 @@
 #: kallithea/templates/admin/users/user_edit_ips.html:21
 #: kallithea/templates/changeset/changeset_file_comment.html:30
 #: kallithea/templates/changeset/changeset_file_comment.html:121
-#: kallithea/templates/data_table/_dt_elements.html:69
-#: kallithea/templates/data_table/_dt_elements.html:89
-#: kallithea/templates/data_table/_dt_elements.html:91
-#: kallithea/templates/data_table/_dt_elements.html:101
-#: kallithea/templates/data_table/_dt_elements.html:103
-#: kallithea/templates/data_table/_dt_elements.html:120
-#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:75
+#: kallithea/templates/data_table/_dt_elements.html:95
+#: kallithea/templates/data_table/_dt_elements.html:97
+#: kallithea/templates/data_table/_dt_elements.html:107
+#: kallithea/templates/data_table/_dt_elements.html:109
+#: kallithea/templates/data_table/_dt_elements.html:126
+#: kallithea/templates/data_table/_dt_elements.html:128
 #: kallithea/templates/files/files_source.html:35
 #: kallithea/templates/files/files_source.html:38
 #: kallithea/templates/files/files_source.html:41
@@ -2399,14 +2397,14 @@
 #: kallithea/templates/base/perms_summary.html:44
 #: kallithea/templates/base/perms_summary.html:81
 #: kallithea/templates/base/perms_summary.html:83
-#: kallithea/templates/data_table/_dt_elements.html:63
-#: kallithea/templates/data_table/_dt_elements.html:64
-#: kallithea/templates/data_table/_dt_elements.html:85
-#: kallithea/templates/data_table/_dt_elements.html:86
-#: kallithea/templates/data_table/_dt_elements.html:97
-#: kallithea/templates/data_table/_dt_elements.html:98
-#: kallithea/templates/data_table/_dt_elements.html:116
-#: kallithea/templates/data_table/_dt_elements.html:117
+#: kallithea/templates/data_table/_dt_elements.html:69
+#: kallithea/templates/data_table/_dt_elements.html:70
+#: kallithea/templates/data_table/_dt_elements.html:91
+#: kallithea/templates/data_table/_dt_elements.html:92
+#: kallithea/templates/data_table/_dt_elements.html:103
+#: kallithea/templates/data_table/_dt_elements.html:104
+#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:123
 #: kallithea/templates/files/diff_2way.html:56
 #: kallithea/templates/files/files_source.html:37
 #: kallithea/templates/files/files_source.html:40
@@ -2710,7 +2708,7 @@
 #: kallithea/templates/admin/permissions/permissions_globals.html:27
 #: kallithea/templates/admin/repos/repo_add_base.html:28
 #: kallithea/templates/admin/repos/repo_edit_settings.html:33
-#: kallithea/templates/data_table/_dt_elements.html:134
+#: kallithea/templates/data_table/_dt_elements.html:140
 #: kallithea/templates/forks/fork.html:42
 msgid "Repository group"
 msgstr ""
@@ -2731,7 +2729,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:40
-#: kallithea/templates/data_table/_dt_elements.html:141
+#: kallithea/templates/data_table/_dt_elements.html:147
 msgid "User group"
 msgstr ""
 
@@ -2904,7 +2902,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:121
+#: kallithea/templates/data_table/_dt_elements.html:127
 #, python-format
 msgid "Confirm to delete this group: %s with %s repository"
 msgid_plural "Confirm to delete this group: %s with %s repositories"
@@ -3072,14 +3070,10 @@
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit.html:37
-msgid "Caches"
+msgid "Remote"
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit.html:40
-msgid "Remote"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit.html:43
 #: kallithea/templates/summary/statistics.html:8
 #: kallithea/templates/summary/summary.html:169
 #: kallithea/templates/summary/summary.html:170
@@ -3117,7 +3111,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:46
-#: kallithea/templates/data_table/_dt_elements.html:68
+#: kallithea/templates/data_table/_dt_elements.html:74
 #, python-format
 msgid "Confirm to delete this repository: %s"
 msgstr ""
@@ -3149,43 +3143,14 @@
 "it or restore it."
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:4
-msgid "Invalidate Repository Cache"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:6
-msgid ""
-"Manually invalidate cache for this repository. On first access, the "
-"repository will be cached again."
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:9
-msgid "List of Cached Values"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:12
-msgid "Prefix"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:13
+#: kallithea/templates/admin/repos/repo_edit_fields.html:6
+msgid "Label"
+msgstr ""
+
 #: kallithea/templates/admin/repos/repo_edit_fields.html:7
 msgid "Key"
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:14
-#: kallithea/templates/admin/user_groups/user_group_add.html:40
-#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
-#: kallithea/templates/admin/user_groups/user_groups.html:41
-#: kallithea/templates/admin/users/user_add.html:69
-#: kallithea/templates/admin/users/user_edit_profile.html:74
-#: kallithea/templates/admin/users/users.html:42
-msgid "Active"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_fields.html:6
-msgid "Label"
-msgstr ""
-
 #: kallithea/templates/admin/repos/repo_edit_fields.html:20
 #, python-format
 msgid "Confirm to delete this field: %s"
@@ -3710,6 +3675,15 @@
 msgid "Short, optional description for this user group."
 msgstr ""
 
+#: kallithea/templates/admin/user_groups/user_group_add.html:40
+#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
+#: kallithea/templates/admin/user_groups/user_groups.html:41
+#: kallithea/templates/admin/users/user_add.html:69
+#: kallithea/templates/admin/users/user_edit_profile.html:74
+#: kallithea/templates/admin/users/users.html:42
+msgid "Active"
+msgstr ""
+
 #: kallithea/templates/admin/user_groups/user_group_edit.html:5
 #, python-format
 msgid "%s user group settings"
@@ -3731,7 +3705,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:19
-#: kallithea/templates/data_table/_dt_elements.html:102
+#: kallithea/templates/data_table/_dt_elements.html:108
 #, python-format
 msgid "Confirm to delete this user group: %s"
 msgstr ""
@@ -3805,7 +3779,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:90
+#: kallithea/templates/data_table/_dt_elements.html:96
 #, python-format
 msgid "Confirm to delete this user: %s"
 msgstr ""
@@ -3905,10 +3879,12 @@
 msgstr ""
 
 #: kallithea/templates/base/base.html:167
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Follow"
 msgstr ""
 
 #: kallithea/templates/base/base.html:168
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Unfollow"
 msgstr ""
 
@@ -4317,23 +4293,23 @@
 msgid "Merge"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:96
+#: kallithea/templates/changeset/changeset.html:95
 msgid "Grafted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:102
+#: kallithea/templates/changeset/changeset.html:100
 msgid "Transplanted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:108
+#: kallithea/templates/changeset/changeset.html:106
 msgid "Replaced by:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:122
+#: kallithea/templates/changeset/changeset.html:120
 msgid "Preceded by:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:139
+#: kallithea/templates/changeset/changeset.html:137
 #: kallithea/templates/compare/compare_diff.html:59
 #: kallithea/templates/pullrequests/pullrequest_show.html:290
 #, python-format
@@ -4343,7 +4319,7 @@
 msgstr[1] ""
 msgstr[2] ""
 
-#: kallithea/templates/changeset/changeset.html:141
+#: kallithea/templates/changeset/changeset.html:139
 #: kallithea/templates/compare/compare_diff.html:61
 #: kallithea/templates/pullrequests/pullrequest_show.html:292
 #, python-format
@@ -4353,8 +4329,8 @@
 msgstr[1] ""
 msgstr[2] ""
 
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
 #: kallithea/templates/pullrequests/pullrequest_show.html:333
@@ -4597,23 +4573,23 @@
 msgid "Repository creation in progress..."
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:42
+#: kallithea/templates/data_table/_dt_elements.html:48
 msgid "No changesets yet"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:48
-#: kallithea/templates/data_table/_dt_elements.html:50
+#: kallithea/templates/data_table/_dt_elements.html:54
+#: kallithea/templates/data_table/_dt_elements.html:56
 #, python-format
 msgid "Subscribe to %s rss feed"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:56
-#: kallithea/templates/data_table/_dt_elements.html:58
+#: kallithea/templates/data_table/_dt_elements.html:62
+#: kallithea/templates/data_table/_dt_elements.html:64
 #, python-format
 msgid "Subscribe to %s atom feed"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:76
+#: kallithea/templates/data_table/_dt_elements.html:82
 msgid "Creating"
 msgstr ""
 
@@ -4650,6 +4626,13 @@
 msgid "by"
 msgstr ""
 
+#: kallithea/templates/email_templates/changeset_comment.html:36
+#: kallithea/templates/email_templates/pull_request_comment.html:43
+#, fuzzy
+#| msgid "No comments."
+msgid "View Comment"
+msgstr "Nie sú žiadne komentáre."
+
 #: kallithea/templates/email_templates/comment.html:27
 #, fuzzy
 #| msgid "Status change"
@@ -4662,32 +4645,40 @@
 msgid "The pull request has been closed."
 msgstr "Tento repozitár bol uzamknutý používateľom %s dňa %s"
 
-#: kallithea/templates/email_templates/password_reset.html:9
+#: kallithea/templates/email_templates/default.html:4
+msgid "Message"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:4
+msgid "Password Reset Request"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:10
 #, python-format
 msgid "Hello %s"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:16
+#: kallithea/templates/email_templates/password_reset.html:17
 msgid "We have received a request to reset the password for your account."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:25
+#: kallithea/templates/email_templates/password_reset.html:26
 msgid ""
 "This account is however managed outside this system and the password "
 "cannot be changed here."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:28
+#: kallithea/templates/email_templates/password_reset.html:29
 msgid "To set a new password, click the following link"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:33
+#: kallithea/templates/email_templates/password_reset.html:34
 msgid ""
 "Should you not be able to use the link above, please type the following "
 "code into the password reset form"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:44
+#: kallithea/templates/email_templates/password_reset.html:45
 msgid ""
 "If it weren't you who requested the password reset, just disregard this "
 "message."
@@ -4720,6 +4711,12 @@
 msgid "to"
 msgstr ""
 
+#: kallithea/templates/email_templates/pull_request.html:85
+#, fuzzy
+#| msgid "on pull request"
+msgid "View Pull Request"
+msgstr "Zmena stavu"
+
 #: kallithea/templates/email_templates/pull_request_comment.html:4
 #, python-format
 msgid "Mention in Comment on Pull Request %s \"%s\""
@@ -4736,10 +4733,18 @@
 msgid "Comment on Pull Request %s \"%s\""
 msgstr "Zmena stavu"
 
-#: kallithea/templates/email_templates/registration.html:22
+#: kallithea/templates/email_templates/registration.html:5
+msgid "New User Registration"
+msgstr ""
+
+#: kallithea/templates/email_templates/registration.html:23
 msgid "Full Name"
 msgstr ""
 
+#: kallithea/templates/email_templates/registration.html:42
+msgid "View User Profile"
+msgstr ""
+
 #: kallithea/templates/files/diff_2way.html:15
 #, python-format
 msgid "%s File side-by-side diff"
@@ -5342,45 +5347,45 @@
 msgid "Stats gathered: "
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:87
-#: kallithea/templates/summary/summary.html:371
+#: kallithea/templates/summary/statistics.html:85
+#: kallithea/templates/summary/summary.html:369
 msgid "files"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:111
-#: kallithea/templates/summary/summary.html:401
+#: kallithea/templates/summary/statistics.html:109
+#: kallithea/templates/summary/summary.html:399
 msgid "Show more"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:405
+#: kallithea/templates/summary/statistics.html:395
 msgid "commits"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:406
+#: kallithea/templates/summary/statistics.html:396
 msgid "files added"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:407
+#: kallithea/templates/summary/statistics.html:397
 msgid "files changed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:408
+#: kallithea/templates/summary/statistics.html:398
 msgid "files removed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:410
+#: kallithea/templates/summary/statistics.html:400
 msgid "commit"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:411
+#: kallithea/templates/summary/statistics.html:401
 msgid "file added"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:412
+#: kallithea/templates/summary/statistics.html:402
 msgid "file changed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:413
+#: kallithea/templates/summary/statistics.html:403
 msgid "file removed"
 msgstr ""
 
--- a/kallithea/i18n/tr/LC_MESSAGES/kallithea.po	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/i18n/tr/LC_MESSAGES/kallithea.po	Mon May 04 19:24:04 2020 +0200
@@ -7,7 +7,7 @@
 msgstr ""
 "Project-Id-Version: Kallithea 0.4.99\n"
 "Report-Msgid-Bugs-To: translations@kallithea-scm.org\n"
-"POT-Creation-Date: 2020-01-05 04:10+0100\n"
+"POT-Creation-Date: 2020-05-04 19:12+0200\n"
 "PO-Revision-Date: 2019-11-05 08:03+0000\n"
 "Last-Translator: Hüseyin Tunç <huseyin.tunc@bulutfon.com>\n"
 "Language-Team: Turkish <https://hosted.weblate.org/projects/kallithea/"
@@ -21,14 +21,14 @@
 "Generated-By: Babel 2.6.0\n"
 
 #: kallithea/controllers/changelog.py:67
-#: kallithea/controllers/pullrequests.py:250 kallithea/lib/base.py:602
+#: kallithea/controllers/pullrequests.py:247 kallithea/lib/base.py:602
 msgid "There are no changesets yet"
 msgstr ""
 
 #: kallithea/controllers/admin/permissions.py:64
 #: kallithea/controllers/admin/permissions.py:68
 #: kallithea/controllers/admin/permissions.py:72
-#: kallithea/controllers/changelog.py:137
+#: kallithea/controllers/changelog.py:136
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:7
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:88
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:7
@@ -37,36 +37,36 @@
 msgid "None"
 msgstr "Yok"
 
-#: kallithea/controllers/changelog.py:140 kallithea/controllers/files.py:189
+#: kallithea/controllers/changelog.py:139 kallithea/controllers/files.py:189
 msgid "(closed)"
 msgstr "(kapalı)"
 
-#: kallithea/controllers/changeset.py:81
+#: kallithea/controllers/changeset.py:82
 msgid "Show whitespace"
 msgstr "Boşlukları göster"
 
-#: kallithea/controllers/changeset.py:88
-#: kallithea/controllers/changeset.py:95
+#: kallithea/controllers/changeset.py:89
+#: kallithea/controllers/changeset.py:96
 #: kallithea/templates/files/diff_2way.html:55
 msgid "Ignore whitespace"
 msgstr ""
 
-#: kallithea/controllers/changeset.py:161
+#: kallithea/controllers/changeset.py:162
 #, python-format
 msgid "Increase diff context to %(num)s lines"
 msgstr ""
 
-#: kallithea/controllers/changeset.py:201
+#: kallithea/controllers/changeset.py:202
 msgid "No permission to change status"
 msgstr ""
 
-#: kallithea/controllers/changeset.py:212
+#: kallithea/controllers/changeset.py:213
 #, python-format
 msgid "Successfully deleted pull request %s"
 msgstr ""
 
 #: kallithea/controllers/changeset.py:319 kallithea/controllers/files.py:89
-#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:700
+#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:697
 msgid "Such revision does not exist for this repository"
 msgstr ""
 
@@ -79,61 +79,61 @@
 msgid "Cannot compare repositories of different types"
 msgstr ""
 
-#: kallithea/controllers/compare.py:246
+#: kallithea/controllers/compare.py:247
 msgid "Cannot show empty diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:248
+#: kallithea/controllers/compare.py:249
 msgid "No ancestor found for merge diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:252
+#: kallithea/controllers/compare.py:253
 msgid "Multiple merge ancestors found for merge compare"
 msgstr ""
 
-#: kallithea/controllers/compare.py:268
+#: kallithea/controllers/compare.py:269
 msgid "Cannot compare repositories without using common ancestor"
 msgstr ""
 
-#: kallithea/controllers/error.py:71
+#: kallithea/controllers/error.py:67
 msgid "No response"
 msgstr ""
 
-#: kallithea/controllers/error.py:72
+#: kallithea/controllers/error.py:68
 msgid "Unknown error"
 msgstr ""
 
-#: kallithea/controllers/error.py:85
+#: kallithea/controllers/error.py:81
 msgid ""
 "The request could not be understood by the server due to malformed syntax."
 msgstr ""
 
+#: kallithea/controllers/error.py:84
+msgid "Unauthorized access to resource"
+msgstr ""
+
+#: kallithea/controllers/error.py:86
+msgid "You don't have permission to view this page"
+msgstr ""
+
 #: kallithea/controllers/error.py:88
-msgid "Unauthorized access to resource"
+msgid "The resource could not be found"
 msgstr ""
 
 #: kallithea/controllers/error.py:90
-msgid "You don't have permission to view this page"
-msgstr ""
-
-#: kallithea/controllers/error.py:92
-msgid "The resource could not be found"
-msgstr ""
-
-#: kallithea/controllers/error.py:94
 msgid ""
 "The server encountered an unexpected condition which prevented it from "
 "fulfilling the request."
 msgstr ""
 
-#: kallithea/controllers/feed.py:63
+#: kallithea/controllers/feed.py:59
 #, python-format
 msgid "%s committed on %s"
 msgstr ""
 
-#: kallithea/controllers/feed.py:88
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/controllers/feed.py:84
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/compare/compare_diff.html:95
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
@@ -141,12 +141,12 @@
 msgid "Changeset was too big and was cut off..."
 msgstr ""
 
-#: kallithea/controllers/feed.py:111 kallithea/controllers/feed.py:140
+#: kallithea/controllers/feed.py:107
 #, python-format
 msgid "%s %s feed"
 msgstr ""
 
-#: kallithea/controllers/feed.py:113 kallithea/controllers/feed.py:142
+#: kallithea/controllers/feed.py:109
 #, python-format
 msgid "Changes on %s repository"
 msgstr ""
@@ -164,103 +164,103 @@
 msgid "%s at %s"
 msgstr ""
 
-#: kallithea/controllers/files.py:296
+#: kallithea/controllers/files.py:295
 msgid "You can only delete files with revision being a valid branch"
 msgstr ""
 
-#: kallithea/controllers/files.py:307
+#: kallithea/controllers/files.py:306
 #, python-format
 msgid "Deleted file %s via Kallithea"
 msgstr ""
 
-#: kallithea/controllers/files.py:331
+#: kallithea/controllers/files.py:330
 #, python-format
 msgid "Successfully deleted file %s"
 msgstr ""
 
-#: kallithea/controllers/files.py:335 kallithea/controllers/files.py:394
-#: kallithea/controllers/files.py:469
+#: kallithea/controllers/files.py:334 kallithea/controllers/files.py:392
+#: kallithea/controllers/files.py:467
 msgid "Error occurred during commit"
 msgstr ""
 
-#: kallithea/controllers/files.py:350
+#: kallithea/controllers/files.py:349
 msgid "You can only edit files with revision being a valid branch"
 msgstr ""
 
-#: kallithea/controllers/files.py:364
+#: kallithea/controllers/files.py:363
 #, python-format
 msgid "Edited file %s via Kallithea"
 msgstr ""
 
-#: kallithea/controllers/files.py:380
+#: kallithea/controllers/files.py:378
 msgid "No changes"
 msgstr ""
 
-#: kallithea/controllers/files.py:390 kallithea/controllers/files.py:458
+#: kallithea/controllers/files.py:388 kallithea/controllers/files.py:456
 #, python-format
 msgid "Successfully committed to %s"
 msgstr ""
 
-#: kallithea/controllers/files.py:409
+#: kallithea/controllers/files.py:407
 msgid "Added file via Kallithea"
 msgstr ""
 
-#: kallithea/controllers/files.py:430
+#: kallithea/controllers/files.py:428
 msgid "No content"
 msgstr ""
 
-#: kallithea/controllers/files.py:434
+#: kallithea/controllers/files.py:432
 msgid "No filename"
 msgstr ""
 
-#: kallithea/controllers/files.py:461
+#: kallithea/controllers/files.py:459
 msgid "Location must be relative path and must not contain .. in path"
 msgstr ""
 
-#: kallithea/controllers/files.py:493
+#: kallithea/controllers/files.py:491
 msgid "Downloads disabled"
 msgstr ""
 
+#: kallithea/controllers/files.py:502
+#, python-format
+msgid "Unknown revision %s"
+msgstr ""
+
 #: kallithea/controllers/files.py:504
-#, python-format
-msgid "Unknown revision %s"
+msgid "Empty repository"
 msgstr ""
 
 #: kallithea/controllers/files.py:506
-msgid "Empty repository"
-msgstr ""
-
-#: kallithea/controllers/files.py:508
 msgid "Unknown archive type"
 msgstr ""
 
-#: kallithea/controllers/files.py:729
+#: kallithea/controllers/files.py:726
 #: kallithea/templates/changeset/changeset_range.html:9
 #: kallithea/templates/email_templates/pull_request.html:64
 #: kallithea/templates/pullrequests/pullrequest.html:84
 msgid "Changesets"
 msgstr ""
 
-#: kallithea/controllers/files.py:730
-#: kallithea/controllers/pullrequests.py:182 kallithea/model/scm.py:676
+#: kallithea/controllers/files.py:727
+#: kallithea/controllers/pullrequests.py:174 kallithea/model/scm.py:663
 msgid "Branches"
 msgstr ""
 
-#: kallithea/controllers/files.py:731
-#: kallithea/controllers/pullrequests.py:183 kallithea/model/scm.py:687
+#: kallithea/controllers/files.py:728
+#: kallithea/controllers/pullrequests.py:175 kallithea/model/scm.py:674
 msgid "Tags"
 msgstr ""
 
-#: kallithea/controllers/forks.py:174
+#: kallithea/controllers/forks.py:175
 #, python-format
 msgid "An error occurred during repository forking %s"
 msgstr ""
 
-#: kallithea/controllers/home.py:79
+#: kallithea/controllers/home.py:77
 msgid "Groups"
 msgstr ""
 
-#: kallithea/controllers/home.py:89
+#: kallithea/controllers/home.py:87
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:90
 #: kallithea/templates/admin/repos/repo_add.html:12
 #: kallithea/templates/admin/repos/repo_add.html:16
@@ -272,48 +272,52 @@
 msgid "Repositories"
 msgstr ""
 
-#: kallithea/controllers/home.py:122
+#: kallithea/controllers/home.py:119
 #: kallithea/templates/files/files_add.html:32
 #: kallithea/templates/files/files_delete.html:23
 #: kallithea/templates/files/files_edit.html:32
 msgid "Branch"
 msgstr ""
 
-#: kallithea/controllers/home.py:128
+#: kallithea/controllers/home.py:125
 msgid "Closed Branches"
 msgstr ""
 
-#: kallithea/controllers/home.py:134
+#: kallithea/controllers/home.py:131
 msgid "Tag"
 msgstr ""
 
-#: kallithea/controllers/home.py:140
+#: kallithea/controllers/home.py:137
 msgid "Bookmark"
 msgstr ""
 
-#: kallithea/controllers/journal.py:112 kallithea/controllers/journal.py:154
+#: kallithea/controllers/journal.py:146 kallithea/controllers/journal.py:157
 #: kallithea/templates/journal/public_journal.html:4
 #: kallithea/templates/journal/public_journal.html:18
 msgid "Public Journal"
 msgstr ""
 
-#: kallithea/controllers/journal.py:116 kallithea/controllers/journal.py:158
+#: kallithea/controllers/journal.py:150 kallithea/controllers/journal.py:161
 #: kallithea/templates/base/base.html:290
 #: kallithea/templates/journal/journal.html:5
 #: kallithea/templates/journal/journal.html:13
 msgid "Journal"
 msgstr ""
 
-#: kallithea/controllers/login.py:140 kallithea/controllers/login.py:185
+#: kallithea/controllers/login.py:109
+msgid "Authentication failed."
+msgstr ""
+
+#: kallithea/controllers/login.py:142 kallithea/controllers/login.py:187
 msgid "Bad captcha"
 msgstr ""
 
-#: kallithea/controllers/login.py:146
+#: kallithea/controllers/login.py:148
 #, python-format
 msgid "You have successfully registered with %s"
 msgstr ""
 
-#: kallithea/controllers/login.py:190
+#: kallithea/controllers/login.py:192
 msgid "A password reset confirmation code has been sent"
 msgstr ""
 
@@ -326,226 +330,226 @@
 msgid "Successfully updated password"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:68
+#: kallithea/controllers/pullrequests.py:67
 #, python-format
 msgid "Invalid reviewer \"%s\" specified"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:131
+#: kallithea/controllers/pullrequests.py:123
 #, python-format
 msgid "%s (closed)"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:158
+#: kallithea/controllers/pullrequests.py:150
 #: kallithea/templates/changeset/changeset.html:12
 msgid "Changeset"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:179
+#: kallithea/controllers/pullrequests.py:171
 msgid "Special"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:180
+#: kallithea/controllers/pullrequests.py:172
 msgid "Peer branches"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:181 kallithea/model/scm.py:682
+#: kallithea/controllers/pullrequests.py:173 kallithea/model/scm.py:669
 msgid "Bookmarks"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:318
+#: kallithea/controllers/pullrequests.py:315
 #, python-format
 msgid "Error creating pull request: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:345
-#: kallithea/controllers/pullrequests.py:368
+#: kallithea/controllers/pullrequests.py:342
+#: kallithea/controllers/pullrequests.py:365
 msgid "Error occurred while creating pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:350
+#: kallithea/controllers/pullrequests.py:347
 msgid "Successfully opened new pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:373
+#: kallithea/controllers/pullrequests.py:370
 msgid "New pull request iteration created"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:401
+#: kallithea/controllers/pullrequests.py:398
 #, python-format
 msgid "Meanwhile, the following reviewers have been added: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:405
+#: kallithea/controllers/pullrequests.py:402
 #, python-format
 msgid "Meanwhile, the following reviewers have been removed: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:421
-#: kallithea/model/pull_request.py:232
+#: kallithea/controllers/pullrequests.py:418
+#: kallithea/model/pull_request.py:230
 msgid "No description"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:430
+#: kallithea/controllers/pullrequests.py:427
 msgid "Pull request updated"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:443
+#: kallithea/controllers/pullrequests.py:440
 msgid "Successfully deleted pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:479
+#: kallithea/controllers/pullrequests.py:476
 #, python-format
 msgid "Revision %s not found in %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:506
+#: kallithea/controllers/pullrequests.py:504
 #, python-format
 msgid "Error: changesets not found when displaying pull request from %s."
 msgstr ""
 
+#: kallithea/controllers/pullrequests.py:518
+#, python-format
+msgid "This pull request has already been merged to %s."
+msgstr ""
+
 #: kallithea/controllers/pullrequests.py:520
-#, python-format
-msgid "This pull request has already been merged to %s."
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:522
 msgid "This pull request has been closed and can not be updated."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:546
+#: kallithea/controllers/pullrequests.py:539
 #, python-format
 msgid "The following additional changes are available on %s:"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:548
-#: kallithea/controllers/pullrequests.py:552
+#: kallithea/controllers/pullrequests.py:541
+#: kallithea/controllers/pullrequests.py:545
 msgid "No additional changesets found for iterating on this pull request."
 msgstr ""
 
+#: kallithea/controllers/pullrequests.py:553
+#, python-format
+msgid "Note: Branch %s has another head: %s."
+msgstr ""
+
 #: kallithea/controllers/pullrequests.py:560
-#, python-format
-msgid "Note: Branch %s has another head: %s."
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:567
 msgid "Git pull requests don't support iterating yet."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:569
+#: kallithea/controllers/pullrequests.py:562
 #, python-format
 msgid ""
 "Error: some changesets not found when displaying pull request from %s."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:593
+#: kallithea/controllers/pullrequests.py:586
 msgid "The diff can't be shown - the PR revisions could not be found."
 msgstr ""
 
+#: kallithea/controllers/search.py:132
+msgid "Invalid search query. Try quoting it."
+msgstr ""
+
 #: kallithea/controllers/search.py:136
-msgid "Invalid search query. Try quoting it."
-msgstr ""
-
-#: kallithea/controllers/search.py:140
 msgid "The server has no search index."
 msgstr ""
 
-#: kallithea/controllers/search.py:143
+#: kallithea/controllers/search.py:139
 msgid "An error occurred during search operation."
 msgstr ""
 
-#: kallithea/controllers/summary.py:168
-#: kallithea/templates/summary/summary.html:412
+#: kallithea/controllers/summary.py:169
+#: kallithea/templates/summary/summary.html:410
 msgid "No data ready yet"
 msgstr ""
 
-#: kallithea/controllers/summary.py:171
+#: kallithea/controllers/summary.py:172
 #: kallithea/templates/summary/summary.html:97
 msgid "Statistics are disabled for this repository"
 msgstr ""
 
-#: kallithea/controllers/admin/auth_settings.py:137
+#: kallithea/controllers/admin/auth_settings.py:136
 msgid "Auth settings updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/auth_settings.py:148
+#: kallithea/controllers/admin/auth_settings.py:147
 msgid "error occurred during update of auth settings"
 msgstr ""
 
-#: kallithea/controllers/admin/defaults.py:75
+#: kallithea/controllers/admin/defaults.py:74
 msgid "Default settings updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/defaults.py:90
+#: kallithea/controllers/admin/defaults.py:89
 msgid "Error occurred during update of defaults"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:59
 #: kallithea/controllers/admin/my_account.py:232
-#: kallithea/controllers/admin/users.py:248
+#: kallithea/controllers/admin/users.py:246
 msgid "Forever"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:60
 #: kallithea/controllers/admin/my_account.py:233
-#: kallithea/controllers/admin/users.py:249
+#: kallithea/controllers/admin/users.py:247
 msgid "5 minutes"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:61
 #: kallithea/controllers/admin/my_account.py:234
-#: kallithea/controllers/admin/users.py:250
+#: kallithea/controllers/admin/users.py:248
 msgid "1 hour"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:62
 #: kallithea/controllers/admin/my_account.py:235
-#: kallithea/controllers/admin/users.py:251
+#: kallithea/controllers/admin/users.py:249
 msgid "1 day"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:63
 #: kallithea/controllers/admin/my_account.py:236
-#: kallithea/controllers/admin/users.py:252
+#: kallithea/controllers/admin/users.py:250
 msgid "1 month"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:67
 #: kallithea/controllers/admin/my_account.py:238
-#: kallithea/controllers/admin/users.py:254
+#: kallithea/controllers/admin/users.py:252
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:65
 #: kallithea/templates/admin/users/user_edit_api_keys.html:65
 msgid "Lifetime"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:142
+#: kallithea/controllers/admin/gists.py:148
 msgid "Error occurred during gist creation"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:158
+#: kallithea/controllers/admin/gists.py:164
 #, python-format
 msgid "Deleted gist %s"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:198
+#: kallithea/controllers/admin/gists.py:207
 msgid "Unmodified"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:228
+#: kallithea/controllers/admin/gists.py:237
 msgid "Successfully updated gist content"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:233
+#: kallithea/controllers/admin/gists.py:242
 msgid "Successfully updated gist data"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:236
+#: kallithea/controllers/admin/gists.py:245
 #, python-format
 msgid "Error occurred during update of gist %s"
 msgstr ""
 
-#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:211
-#: kallithea/model/user.py:232
+#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:205
+#: kallithea/model/user.py:226
 msgid "You can't edit this user since it's crucial for entire application"
 msgstr ""
 
@@ -554,7 +558,7 @@
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:134
-#: kallithea/controllers/admin/users.py:181
+#: kallithea/controllers/admin/users.py:179
 #, python-format
 msgid "Error occurred during update of user %s"
 msgstr ""
@@ -564,44 +568,44 @@
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:209
-#: kallithea/controllers/admin/users.py:367
+#: kallithea/controllers/admin/users.py:365
 #, python-format
 msgid "Added email %s to user"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:215
-#: kallithea/controllers/admin/users.py:373
+#: kallithea/controllers/admin/users.py:371
 msgid "An error occurred during email saving"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:224
-#: kallithea/controllers/admin/users.py:383
+#: kallithea/controllers/admin/users.py:381
 msgid "Removed email from user"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:248
-#: kallithea/controllers/admin/users.py:271
+#: kallithea/controllers/admin/users.py:269
 msgid "API key successfully created"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:257
-#: kallithea/controllers/admin/users.py:281
+#: kallithea/controllers/admin/users.py:279
 msgid "API key successfully reset"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:261
-#: kallithea/controllers/admin/users.py:285
+#: kallithea/controllers/admin/users.py:283
 msgid "API key successfully deleted"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:281
-#: kallithea/controllers/admin/users.py:456
+#: kallithea/controllers/admin/users.py:454
 #, python-format
 msgid "SSH key %s successfully added"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:293
-#: kallithea/controllers/admin/users.py:470
+#: kallithea/controllers/admin/users.py:468
 msgid "SSH key successfully deleted"
 msgstr ""
 
@@ -677,11 +681,11 @@
 msgid "Allowed with automatic account activation"
 msgstr ""
 
-#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1673
+#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1578
 msgid "Manual activation of external account"
 msgstr ""
 
-#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1674
+#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1579
 msgid "Automatic activation of external account"
 msgstr ""
 
@@ -703,339 +707,331 @@
 msgid "Error occurred during update of permissions"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:172
+#: kallithea/controllers/admin/repo_groups.py:165
 #, python-format
 msgid "Error occurred during creation of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:177
+#: kallithea/controllers/admin/repo_groups.py:172
 #, python-format
 msgid "Created repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:224
+#: kallithea/controllers/admin/repo_groups.py:219
 #, python-format
 msgid "Updated repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:240
+#: kallithea/controllers/admin/repo_groups.py:235
 #, python-format
 msgid "Error occurred during update of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:250
+#: kallithea/controllers/admin/repo_groups.py:245
 #, python-format
 msgid "This group contains %s repositories and cannot be deleted"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:257
+#: kallithea/controllers/admin/repo_groups.py:252
 #, python-format
 msgid "This group contains %s subgroups and cannot be deleted"
 msgstr ""
 
+#: kallithea/controllers/admin/repo_groups.py:258
+#, python-format
+msgid "Removed repository group %s"
+msgstr ""
+
 #: kallithea/controllers/admin/repo_groups.py:263
 #, python-format
-msgid "Removed repository group %s"
-msgstr ""
-
-#: kallithea/controllers/admin/repo_groups.py:268
-#, python-format
 msgid "Error occurred during deletion of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:352
-#: kallithea/controllers/admin/repo_groups.py:382
-#: kallithea/controllers/admin/user_groups.py:294
+#: kallithea/controllers/admin/repo_groups.py:347
+#: kallithea/controllers/admin/repo_groups.py:377
+#: kallithea/controllers/admin/user_groups.py:290
 msgid "Cannot revoke permission for yourself as admin"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:367
+#: kallithea/controllers/admin/repo_groups.py:362
 msgid "Repository group permissions updated"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:399
-#: kallithea/controllers/admin/repos.py:358
-#: kallithea/controllers/admin/user_groups.py:306
+#: kallithea/controllers/admin/repo_groups.py:394
+#: kallithea/controllers/admin/repos.py:357
+#: kallithea/controllers/admin/user_groups.py:302
 msgid "An error occurred during revoking of permission"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:136
+#: kallithea/controllers/admin/repos.py:137
 #, python-format
 msgid "Error creating repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:194
+#: kallithea/controllers/admin/repos.py:193
 #, python-format
 msgid "Created repository %s from %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:203
+#: kallithea/controllers/admin/repos.py:202
 #, python-format
 msgid "Forked repository %s as %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:206
+#: kallithea/controllers/admin/repos.py:205
 #, python-format
 msgid "Created repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:235
+#: kallithea/controllers/admin/repos.py:234
 #, python-format
 msgid "Repository %s updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:255
+#: kallithea/controllers/admin/repos.py:254
 #, python-format
 msgid "Error occurred during update of repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:273
+#: kallithea/controllers/admin/repos.py:272
 #, python-format
 msgid "Detached %s forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:276
+#: kallithea/controllers/admin/repos.py:275
 #, python-format
 msgid "Deleted %s forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:281
+#: kallithea/controllers/admin/repos.py:280
 #, python-format
 msgid "Deleted repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:284
+#: kallithea/controllers/admin/repos.py:283
 #, python-format
 msgid "Cannot delete repository %s which still has forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:289
+#: kallithea/controllers/admin/repos.py:288
 #, python-format
 msgid "An error occurred during deletion of %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:329
+#: kallithea/controllers/admin/repos.py:328
 msgid "Repository permissions updated"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:388
+#: kallithea/controllers/admin/repos.py:387
 #, python-format
 msgid "Field validation error: %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:391
+#: kallithea/controllers/admin/repos.py:390
 #, python-format
 msgid "An error occurred during creation of field: %r"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:402
+#: kallithea/controllers/admin/repos.py:401
 msgid "An error occurred during removal of field"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:416
+#: kallithea/controllers/admin/repos.py:415
 msgid "-- Not a fork --"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:448
+#: kallithea/controllers/admin/repos.py:447
 msgid "Updated repository visibility in public journal"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:452
+#: kallithea/controllers/admin/repos.py:451
 msgid "An error occurred during setting this repository in public journal"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:468
+#: kallithea/controllers/admin/repos.py:467
 msgid "Nothing"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:470
+#: kallithea/controllers/admin/repos.py:469
 #, python-format
 msgid "Marked repository %s as fork of %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:477
+#: kallithea/controllers/admin/repos.py:476
 msgid "An error occurred during this operation"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:490
-msgid "Cache invalidation successful"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:494
-msgid "An error occurred during cache invalidation"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:507
+#: kallithea/controllers/admin/repos.py:488
 msgid "Pulled from remote location"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:510
+#: kallithea/controllers/admin/repos.py:491
 msgid "An error occurred during pull from remote location"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:541
+#: kallithea/controllers/admin/repos.py:522
 msgid "An error occurred during deletion of repository stats"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:131
+#: kallithea/controllers/admin/settings.py:132
 msgid "Updated VCS settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:135 kallithea/lib/utils.py:237
+#: kallithea/controllers/admin/settings.py:136
 msgid ""
 "Unable to activate hgsubversion support. The \"hgsubversion\" library is "
 "missing"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:141
-#: kallithea/controllers/admin/settings.py:233
+#: kallithea/controllers/admin/settings.py:142
+#: kallithea/controllers/admin/settings.py:234
 msgid "Error occurred while updating application settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:176
+#: kallithea/controllers/admin/settings.py:177
 #, python-format
 msgid "Repositories successfully rescanned. Added: %s. Removed: %s."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:188
+#: kallithea/controllers/admin/settings.py:189
 #, python-format
 msgid "Invalidated %s repositories"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:229
+#: kallithea/controllers/admin/settings.py:230
 msgid "Updated application settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:283
+#: kallithea/controllers/admin/settings.py:284
 msgid "Updated visualisation settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:288
+#: kallithea/controllers/admin/settings.py:289
 msgid "Error occurred during updating visualisation settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:312
+#: kallithea/controllers/admin/settings.py:313
 msgid "Please enter email address"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:327
+#: kallithea/controllers/admin/settings.py:328
 msgid "Send email task created"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:355
+#: kallithea/controllers/admin/settings.py:356
 msgid "Hook already exists"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:357
+#: kallithea/controllers/admin/settings.py:358
 msgid "Builtin hooks are read-only. Please use another hook name."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:360
+#: kallithea/controllers/admin/settings.py:361
 msgid "Added new hook"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:376
+#: kallithea/controllers/admin/settings.py:377
 msgid "Updated hooks"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:380
+#: kallithea/controllers/admin/settings.py:381
 msgid "Error occurred during hook creation"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:404
+#: kallithea/controllers/admin/settings.py:405
 msgid "Whoosh reindex task scheduled"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:138
+#: kallithea/controllers/admin/user_groups.py:134
 #, python-format
 msgid "Created user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:151
+#: kallithea/controllers/admin/user_groups.py:147
 #, python-format
 msgid "Error occurred during creation of user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:179
+#: kallithea/controllers/admin/user_groups.py:175
 #, python-format
 msgid "Updated user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:201
+#: kallithea/controllers/admin/user_groups.py:197
 #, python-format
 msgid "Error occurred during update of user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:212
+#: kallithea/controllers/admin/user_groups.py:208
 msgid "Successfully deleted user group"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:217
+#: kallithea/controllers/admin/user_groups.py:213
 msgid "An error occurred during deletion of user group"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:273
+#: kallithea/controllers/admin/user_groups.py:269
 msgid "Target group cannot be the same"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:279
+#: kallithea/controllers/admin/user_groups.py:275
 msgid "User group permissions updated"
 msgstr ""
 
+#: kallithea/controllers/admin/user_groups.py:384
+#: kallithea/controllers/admin/users.py:336
+msgid "Updated permissions"
+msgstr ""
+
 #: kallithea/controllers/admin/user_groups.py:388
-#: kallithea/controllers/admin/users.py:338
-msgid "Updated permissions"
-msgstr ""
-
-#: kallithea/controllers/admin/user_groups.py:392
-#: kallithea/controllers/admin/users.py:342
+#: kallithea/controllers/admin/users.py:340
 msgid "An error occurred during permissions saving"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:123
+#: kallithea/controllers/admin/users.py:121
 #, python-format
 msgid "Created user %s"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:138
+#: kallithea/controllers/admin/users.py:136
 #, python-format
 msgid "Error occurred during creation of user %s"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:162
+#: kallithea/controllers/admin/users.py:160
 msgid "User updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:190
+#: kallithea/controllers/admin/users.py:188
 msgid "Successfully deleted user"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:195
+#: kallithea/controllers/admin/users.py:193
 msgid "An error occurred during deletion of user"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:203
+#: kallithea/controllers/admin/users.py:201
 msgid "The default user cannot be edited"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:409
+#: kallithea/controllers/admin/users.py:407
 #, python-format
 msgid "Added IP address %s to user whitelist"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:415
+#: kallithea/controllers/admin/users.py:413
 msgid "An error occurred while adding IP address"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:427
+#: kallithea/controllers/admin/users.py:425
 msgid "Removed IP address from user whitelist"
 msgstr ""
 
-#: kallithea/lib/auth.py:684
+#: kallithea/lib/auth.py:634
 msgid "You need to be a registered user to perform this action"
 msgstr ""
 
-#: kallithea/lib/auth.py:712
+#: kallithea/lib/auth.py:662
 msgid "You need to be signed in to view this page"
 msgstr ""
 
@@ -1066,170 +1062,170 @@
 "Changeset was too big and was cut off, use diff menu to display this diff"
 msgstr ""
 
-#: kallithea/lib/diffs.py:224
+#: kallithea/lib/diffs.py:223
 msgid "No changes detected"
 msgstr ""
 
-#: kallithea/lib/helpers.py:653
+#: kallithea/lib/helpers.py:670
 #, python-format
 msgid "Deleted branch: %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:655
+#: kallithea/lib/helpers.py:672
 #, python-format
 msgid "Created tag: %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:666
+#: kallithea/lib/helpers.py:683
 #, python-format
 msgid "Changeset %s not found"
 msgstr ""
 
-#: kallithea/lib/helpers.py:715
+#: kallithea/lib/helpers.py:732
 #, python-format
 msgid "Show all combined changesets %s->%s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:721
+#: kallithea/lib/helpers.py:738
 msgid "Compare view"
 msgstr ""
 
-#: kallithea/lib/helpers.py:740
+#: kallithea/lib/helpers.py:757
 msgid "and"
 msgstr ""
 
-#: kallithea/lib/helpers.py:741
+#: kallithea/lib/helpers.py:758
 #, python-format
 msgid "%s more"
 msgstr ""
 
-#: kallithea/lib/helpers.py:742
+#: kallithea/lib/helpers.py:759
 #: kallithea/templates/changelog/changelog.html:43
 msgid "revisions"
 msgstr ""
 
-#: kallithea/lib/helpers.py:766
+#: kallithea/lib/helpers.py:783
 #, python-format
 msgid "Fork name %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:787
+#: kallithea/lib/helpers.py:804
 #, python-format
 msgid "Pull request %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:797
+#: kallithea/lib/helpers.py:814
 msgid "[deleted] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:799 kallithea/lib/helpers.py:811
+#: kallithea/lib/helpers.py:816 kallithea/lib/helpers.py:828
 msgid "[created] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:801
+#: kallithea/lib/helpers.py:818
 msgid "[created] repository as fork"
 msgstr ""
 
-#: kallithea/lib/helpers.py:803 kallithea/lib/helpers.py:813
+#: kallithea/lib/helpers.py:820 kallithea/lib/helpers.py:830
 msgid "[forked] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:805 kallithea/lib/helpers.py:815
+#: kallithea/lib/helpers.py:822 kallithea/lib/helpers.py:832
 msgid "[updated] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:807
+#: kallithea/lib/helpers.py:824
 msgid "[downloaded] archive from repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:809
+#: kallithea/lib/helpers.py:826
 msgid "[delete] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:817
+#: kallithea/lib/helpers.py:834
 msgid "[created] user"
 msgstr ""
 
-#: kallithea/lib/helpers.py:819
+#: kallithea/lib/helpers.py:836
 msgid "[updated] user"
 msgstr ""
 
-#: kallithea/lib/helpers.py:821
+#: kallithea/lib/helpers.py:838
 msgid "[created] user group"
 msgstr ""
 
-#: kallithea/lib/helpers.py:823
+#: kallithea/lib/helpers.py:840
 msgid "[updated] user group"
 msgstr ""
 
-#: kallithea/lib/helpers.py:825
+#: kallithea/lib/helpers.py:842
 msgid "[commented] on revision in repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:827
+#: kallithea/lib/helpers.py:844
 msgid "[commented] on pull request for"
 msgstr ""
 
-#: kallithea/lib/helpers.py:829
+#: kallithea/lib/helpers.py:846
 msgid "[closed] pull request for"
 msgstr ""
 
-#: kallithea/lib/helpers.py:831
+#: kallithea/lib/helpers.py:848
 msgid "[pushed] into"
 msgstr ""
 
-#: kallithea/lib/helpers.py:833
+#: kallithea/lib/helpers.py:850
 msgid "[committed via Kallithea] into repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:835
+#: kallithea/lib/helpers.py:852
 msgid "[pulled from remote] into repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:837
+#: kallithea/lib/helpers.py:854
 msgid "[pulled] from"
 msgstr ""
 
-#: kallithea/lib/helpers.py:839
+#: kallithea/lib/helpers.py:856
 msgid "[started following] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:841
+#: kallithea/lib/helpers.py:858
 msgid "[stopped following] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:961
+#: kallithea/lib/helpers.py:975
 #, python-format
 msgid " and %s more"
 msgstr ""
 
-#: kallithea/lib/helpers.py:965
+#: kallithea/lib/helpers.py:979
 #: kallithea/templates/compare/compare_diff.html:69
 #: kallithea/templates/pullrequests/pullrequest_show.html:297
 msgid "No files"
 msgstr ""
 
-#: kallithea/lib/helpers.py:990
+#: kallithea/lib/helpers.py:1004
 msgid "new file"
 msgstr ""
 
-#: kallithea/lib/helpers.py:993
+#: kallithea/lib/helpers.py:1007
 msgid "mod"
 msgstr ""
 
-#: kallithea/lib/helpers.py:996
+#: kallithea/lib/helpers.py:1010
 msgid "del"
 msgstr ""
 
-#: kallithea/lib/helpers.py:999
+#: kallithea/lib/helpers.py:1013
 msgid "rename"
 msgstr ""
 
-#: kallithea/lib/helpers.py:1004
+#: kallithea/lib/helpers.py:1018
 msgid "chmod"
 msgstr ""
 
-#: kallithea/lib/helpers.py:1297
+#: kallithea/lib/helpers.py:1323
 #, python-format
 msgid ""
 "%s repository is not mapped to db perhaps it was created or renamed from "
@@ -1237,96 +1233,98 @@
 "repositories"
 msgstr ""
 
-#: kallithea/lib/ssh.py:73
+#: kallithea/lib/ssh.py:75
 msgid "SSH key is missing"
 msgstr ""
 
-#: kallithea/lib/ssh.py:77
-msgid "Incorrect SSH key - it must have both a key type and a base64 part"
-msgstr ""
-
-#: kallithea/lib/ssh.py:81
+#: kallithea/lib/ssh.py:79
+msgid ""
+"Incorrect SSH key - it must have both a key type and a base64 part, like "
+"'ssh-rsa ASRNeaZu4FA...xlJp='"
+msgstr ""
+
+#: kallithea/lib/ssh.py:83
 msgid "Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'"
 msgstr ""
 
-#: kallithea/lib/ssh.py:84
+#: kallithea/lib/ssh.py:86
 #, python-format
 msgid "Incorrect SSH key - unexpected characters in base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:89
+#: kallithea/lib/ssh.py:91
 #, python-format
 msgid "Incorrect SSH key - failed to decode base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:92
+#: kallithea/lib/ssh.py:94
 #, python-format
 msgid "Incorrect SSH key - base64 part is not %r as claimed but %r"
 msgstr ""
 
-#: kallithea/lib/utils2.py:334
+#: kallithea/lib/utils2.py:253
 #, python-format
 msgid "%d year"
 msgid_plural "%d years"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:335
+#: kallithea/lib/utils2.py:254
 #, python-format
 msgid "%d month"
 msgid_plural "%d months"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:336
+#: kallithea/lib/utils2.py:255
 #, python-format
 msgid "%d day"
 msgid_plural "%d days"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:337
+#: kallithea/lib/utils2.py:256
 #, python-format
 msgid "%d hour"
 msgid_plural "%d hours"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:338
+#: kallithea/lib/utils2.py:257
 #, python-format
 msgid "%d minute"
 msgid_plural "%d minutes"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:339
+#: kallithea/lib/utils2.py:258
 #, python-format
 msgid "%d second"
 msgid_plural "%d seconds"
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/lib/utils2.py:355
+#: kallithea/lib/utils2.py:274
 #, python-format
 msgid "in %s"
 msgstr ""
 
-#: kallithea/lib/utils2.py:357
+#: kallithea/lib/utils2.py:276
 #, python-format
 msgid "%s ago"
 msgstr ""
 
-#: kallithea/lib/utils2.py:359
+#: kallithea/lib/utils2.py:278
 #, python-format
 msgid "in %s and %s"
 msgstr ""
 
-#: kallithea/lib/utils2.py:362
+#: kallithea/lib/utils2.py:281
 #, python-format
 msgid "%s and %s ago"
 msgstr ""
 
-#: kallithea/lib/utils2.py:365
+#: kallithea/lib/utils2.py:284
 msgid "just now"
 msgstr ""
 
@@ -1335,133 +1333,133 @@
 msgid "on line %s"
 msgstr ""
 
-#: kallithea/model/comment.py:221 kallithea/model/pull_request.py:114
+#: kallithea/model/comment.py:219 kallithea/model/pull_request.py:112
 msgid "[Mention]"
 msgstr ""
 
-#: kallithea/model/db.py:1496
+#: kallithea/model/db.py:1411
 msgid "top level"
 msgstr ""
 
-#: kallithea/model/db.py:1637
+#: kallithea/model/db.py:1542
 msgid "Kallithea Administrator"
 msgstr ""
 
-#: kallithea/model/db.py:1639
+#: kallithea/model/db.py:1544
 msgid "Default user has no access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1640
+#: kallithea/model/db.py:1545
 msgid "Default user has read access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1641
+#: kallithea/model/db.py:1546
 msgid "Default user has write access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1642
+#: kallithea/model/db.py:1547
 msgid "Default user has admin access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1644
+#: kallithea/model/db.py:1549
 msgid "Default user has no access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1645
+#: kallithea/model/db.py:1550
 msgid "Default user has read access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1646
+#: kallithea/model/db.py:1551
 msgid "Default user has write access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1647
+#: kallithea/model/db.py:1552
 msgid "Default user has admin access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1649
+#: kallithea/model/db.py:1554
 msgid "Default user has no access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1650
+#: kallithea/model/db.py:1555
 msgid "Default user has read access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1651
+#: kallithea/model/db.py:1556
 msgid "Default user has write access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1652
+#: kallithea/model/db.py:1557
 msgid "Default user has admin access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1654
+#: kallithea/model/db.py:1559
 msgid "Only admins can create repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1655
+#: kallithea/model/db.py:1560
 msgid "Non-admins can create repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1657
+#: kallithea/model/db.py:1562
 msgid "Only admins can create user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1658
+#: kallithea/model/db.py:1563
 msgid "Non-admins can create user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1660
+#: kallithea/model/db.py:1565
 msgid "Only admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1661
+#: kallithea/model/db.py:1566
 msgid "Non-admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1663
+#: kallithea/model/db.py:1568
 msgid ""
 "Repository creation enabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1664
+#: kallithea/model/db.py:1569
 msgid ""
 "Repository creation disabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1666
+#: kallithea/model/db.py:1571
 msgid "Only admins can fork repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1667
+#: kallithea/model/db.py:1572
 msgid "Non-admins can fork repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1669
+#: kallithea/model/db.py:1574
 msgid "Registration disabled"
 msgstr ""
 
-#: kallithea/model/db.py:1670
+#: kallithea/model/db.py:1575
 msgid "User registration with manual account activation"
 msgstr ""
 
-#: kallithea/model/db.py:1671
+#: kallithea/model/db.py:1576
 msgid "User registration with automatic account activation"
 msgstr ""
 
-#: kallithea/model/db.py:2206
+#: kallithea/model/db.py:1992
 msgid "Not reviewed"
 msgstr ""
 
-#: kallithea/model/db.py:2207
+#: kallithea/model/db.py:1993
 msgid "Under review"
 msgstr ""
 
-#: kallithea/model/db.py:2208
+#: kallithea/model/db.py:1994
 msgid "Not approved"
 msgstr ""
 
-#: kallithea/model/db.py:2209
+#: kallithea/model/db.py:1995
 msgid "Approved"
 msgstr ""
 
@@ -1487,313 +1485,313 @@
 msgid "Name must not contain only digits"
 msgstr ""
 
-#: kallithea/model/notification.py:164
+#: kallithea/model/notification.py:162
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s changeset %(short_id)s \"%(message_short)s\" on "
 "%(branch)s"
 msgstr ""
 
-#: kallithea/model/notification.py:167
+#: kallithea/model/notification.py:165
 #, python-format
 msgid "New user %(new_username)s registered"
 msgstr ""
 
-#: kallithea/model/notification.py:169
+#: kallithea/model/notification.py:167
 #, python-format
 msgid ""
 "[Review] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:170
+#: kallithea/model/notification.py:168
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:183
+#: kallithea/model/notification.py:188
 msgid "Closing"
 msgstr ""
 
-#: kallithea/model/pull_request.py:73
+#: kallithea/model/pull_request.py:72
 #, python-format
 msgid ""
 "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:209
+#: kallithea/model/pull_request.py:207
 msgid "Cannot create empty pull request"
 msgstr ""
 
-#: kallithea/model/pull_request.py:217
+#: kallithea/model/pull_request.py:215
 #, python-format
 msgid ""
 "Cannot create pull request - criss cross merge detected, please merge a "
 "later %s revision to %s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:245 kallithea/model/pull_request.py:380
+#: kallithea/model/pull_request.py:243 kallithea/model/pull_request.py:378
 msgid "You are not authorized to create the pull request"
 msgstr ""
 
-#: kallithea/model/pull_request.py:339
+#: kallithea/model/pull_request.py:337
 msgid "Missing changesets since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:346
+#: kallithea/model/pull_request.py:344
 #, python-format
 msgid "New changesets on %s %s since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:353
+#: kallithea/model/pull_request.py:351
 msgid "Ancestor didn't change - diff since previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:360
+#: kallithea/model/pull_request.py:358
 #, python-format
 msgid ""
 "This iteration is based on another %s revision and there is no simple "
 "diff."
 msgstr ""
 
-#: kallithea/model/pull_request.py:362
+#: kallithea/model/pull_request.py:360
 #, python-format
 msgid "No changes found on %s %s since previous iteration."
 msgstr ""
 
-#: kallithea/model/pull_request.py:388
+#: kallithea/model/pull_request.py:386
 #, python-format
 msgid "Closed, next iteration: %s ."
 msgstr ""
 
-#: kallithea/model/scm.py:668
+#: kallithea/model/scm.py:655
 msgid "latest tip"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:56
+#: kallithea/model/ssh_key.py:57
 #, python-format
 msgid "SSH key %r is invalid: %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:68
+#: kallithea/model/ssh_key.py:69
 #, python-format
 msgid "SSH key %s is already used by %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:89
-#, python-format
-msgid "SSH key %r not found"
-msgstr ""
-
-#: kallithea/model/user.py:186
+#: kallithea/model/ssh_key.py:88
+#, python-format
+msgid "SSH key with fingerprint %r found"
+msgstr ""
+
+#: kallithea/model/user.py:180
 msgid "New user registration"
 msgstr ""
 
-#: kallithea/model/user.py:250
+#: kallithea/model/user.py:244
 msgid ""
 "You can't remove this user since it is crucial for the entire application"
 msgstr ""
 
-#: kallithea/model/user.py:255
+#: kallithea/model/user.py:249
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repositories and cannot be removed. Switch "
 "owners or remove those repositories: %s"
 msgstr ""
 
-#: kallithea/model/user.py:260
+#: kallithea/model/user.py:254
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repository groups and cannot be removed. Switch "
 "owners or remove those repository groups: %s"
 msgstr ""
 
-#: kallithea/model/user.py:267
+#: kallithea/model/user.py:261
 #, python-format
 msgid ""
 "User \"%s\" still owns %s user groups and cannot be removed. Switch "
 "owners or remove those user groups: %s"
 msgstr ""
 
-#: kallithea/model/user.py:361
+#: kallithea/model/user.py:355
 msgid "Password reset link"
 msgstr ""
 
-#: kallithea/model/user.py:408
+#: kallithea/model/user.py:402
 msgid "Password reset notification"
 msgstr ""
 
-#: kallithea/model/user.py:409
+#: kallithea/model/user.py:403
 #, python-format
 msgid ""
 "The password to your account %s has been changed using password reset "
 "form."
 msgstr ""
 
-#: kallithea/model/validators.py:52 kallithea/model/validators.py:53
+#: kallithea/model/validators.py:53 kallithea/model/validators.py:54
 msgid "Value cannot be an empty list"
 msgstr ""
 
-#: kallithea/model/validators.py:72
+#: kallithea/model/validators.py:73
 #, python-format
 msgid "Username \"%(username)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:74
+#: kallithea/model/validators.py:75
 #, python-format
 msgid "Username \"%(username)s\" cannot be used"
 msgstr ""
 
-#: kallithea/model/validators.py:76
+#: kallithea/model/validators.py:77
 msgid ""
 "Username may only contain alphanumeric characters underscores, periods or "
 "dashes and must begin with an alphanumeric character or underscore"
 msgstr ""
 
-#: kallithea/model/validators.py:103
+#: kallithea/model/validators.py:104
 msgid "The input is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:110
+#: kallithea/model/validators.py:111
 #, python-format
 msgid "Username %(username)s is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:131
-msgid "Invalid user group name"
-msgstr ""
-
 #: kallithea/model/validators.py:132
+msgid "Invalid user group name"
+msgstr ""
+
+#: kallithea/model/validators.py:133
 #, python-format
 msgid "User group \"%(usergroup)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:134
+#: kallithea/model/validators.py:135
 msgid ""
 "user group name may only contain alphanumeric characters underscores, "
 "periods or dashes and must begin with alphanumeric character"
 msgstr ""
 
-#: kallithea/model/validators.py:174
-msgid "Cannot assign this group as parent"
-msgstr ""
-
 #: kallithea/model/validators.py:175
+msgid "Cannot assign this group as parent"
+msgstr ""
+
+#: kallithea/model/validators.py:176
 #, python-format
 msgid "Group \"%(group_name)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:177
+#: kallithea/model/validators.py:178
 #, python-format
 msgid "Repository with name \"%(group_name)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:233
+#: kallithea/model/validators.py:230
 msgid "Invalid characters (non-ascii) in password"
 msgstr ""
 
-#: kallithea/model/validators.py:248
+#: kallithea/model/validators.py:245
 msgid "Invalid old password"
 msgstr ""
 
-#: kallithea/model/validators.py:264
+#: kallithea/model/validators.py:261
 msgid "Passwords do not match"
 msgstr ""
 
-#: kallithea/model/validators.py:279
+#: kallithea/model/validators.py:276
 msgid "Invalid username or password"
 msgstr ""
 
+#: kallithea/model/validators.py:310
+#, python-format
+msgid "Repository name %(repo)s is not allowed"
+msgstr ""
+
+#: kallithea/model/validators.py:312
+#, python-format
+msgid "Repository named %(repo)s already exists"
+msgstr ""
+
 #: kallithea/model/validators.py:313
 #, python-format
-msgid "Repository name %(repo)s is not allowed"
+msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
 msgstr ""
 
 #: kallithea/model/validators.py:315
 #, python-format
-msgid "Repository named %(repo)s already exists"
-msgstr ""
-
-#: kallithea/model/validators.py:316
-#, python-format
-msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
-msgstr ""
-
-#: kallithea/model/validators.py:318
-#, python-format
 msgid "Repository group with name \"%(repo)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:404
+#: kallithea/model/validators.py:401
 msgid "Invalid repository URL"
 msgstr ""
 
-#: kallithea/model/validators.py:405
+#: kallithea/model/validators.py:402
 msgid ""
 "Invalid repository URL. It must be a valid http, https, ssh, svn+http or "
 "svn+https URL"
 msgstr ""
 
-#: kallithea/model/validators.py:430
+#: kallithea/model/validators.py:427
 msgid "Fork has to be the same type as parent"
 msgstr ""
 
-#: kallithea/model/validators.py:445
+#: kallithea/model/validators.py:442
 msgid "You don't have permissions to create repository in this group"
 msgstr ""
 
-#: kallithea/model/validators.py:447
+#: kallithea/model/validators.py:444
 msgid "no permission to create repository in root location"
 msgstr ""
 
-#: kallithea/model/validators.py:497
+#: kallithea/model/validators.py:494
 msgid "You don't have permissions to create a group in this location"
 msgstr ""
 
-#: kallithea/model/validators.py:537
+#: kallithea/model/validators.py:534
 msgid "This username or user group name is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:630
+#: kallithea/model/validators.py:627
 msgid "This is not a valid path"
 msgstr ""
 
-#: kallithea/model/validators.py:647
+#: kallithea/model/validators.py:644
 msgid "This email address is already in use"
 msgstr ""
 
-#: kallithea/model/validators.py:667
+#: kallithea/model/validators.py:664
 #, python-format
 msgid "Email address \"%(email)s\" not found"
 msgstr ""
 
-#: kallithea/model/validators.py:704
+#: kallithea/model/validators.py:701
 msgid ""
 "The LDAP Login attribute of the CN must be specified - this is the name "
 "of the attribute that is equivalent to \"username\""
 msgstr ""
 
-#: kallithea/model/validators.py:716
+#: kallithea/model/validators.py:713
 msgid "Please enter a valid IPv4 or IPv6 address"
 msgstr ""
 
-#: kallithea/model/validators.py:717
+#: kallithea/model/validators.py:714
 #, python-format
 msgid ""
 "The network size (bits) must be within the range of 0-32 (not %(bits)r)"
 msgstr ""
 
-#: kallithea/model/validators.py:750
+#: kallithea/model/validators.py:747
 msgid "Key name can only consist of letters, underscore, dash or numbers"
 msgstr ""
 
-#: kallithea/model/validators.py:764
+#: kallithea/model/validators.py:761
 msgid "Filename cannot be inside a directory"
 msgstr ""
 
-#: kallithea/model/validators.py:780
+#: kallithea/model/validators.py:777
 #, python-format
 msgid "Plugins %(loaded)s and %(next_to_load)s both export the same name"
 msgstr ""
@@ -1853,7 +1851,7 @@
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:60
 #: kallithea/templates/email_templates/pull_request.html:37
 #: kallithea/templates/forks/fork.html:34
-#: kallithea/templates/index_base.html:58
+#: kallithea/templates/index_base.html:59
 #: kallithea/templates/pullrequests/pullrequest.html:33
 #: kallithea/templates/pullrequests/pullrequest_show.html:38
 #: kallithea/templates/pullrequests/pullrequest_show.html:59
@@ -1861,14 +1859,14 @@
 msgid "Description"
 msgstr ""
 
-#: kallithea/templates/index_base.html:60
+#: kallithea/templates/index_base.html:61
 msgid "Last Change"
 msgstr ""
 
 #: kallithea/templates/admin/my_account/my_account_repos.html:15
 #: kallithea/templates/admin/my_account/my_account_watched.html:15
 #: kallithea/templates/admin/repos/repos.html:41
-#: kallithea/templates/index_base.html:62
+#: kallithea/templates/index_base.html:63
 msgid "Tip"
 msgstr ""
 
@@ -1878,7 +1876,7 @@
 #: kallithea/templates/admin/repos/repos.html:42
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:8
 #: kallithea/templates/admin/user_groups/user_groups.html:42
-#: kallithea/templates/index_base.html:63
+#: kallithea/templates/index_base.html:64
 #: kallithea/templates/pullrequests/pullrequest_data.html:16
 #: kallithea/templates/pullrequests/pullrequest_show.html:124
 #: kallithea/templates/pullrequests/pullrequest_show.html:219
@@ -1902,7 +1900,7 @@
 #: kallithea/templates/admin/users/user_edit_profile.html:18
 #: kallithea/templates/admin/users/users.html:37
 #: kallithea/templates/base/base.html:364
-#: kallithea/templates/email_templates/registration.html:11
+#: kallithea/templates/email_templates/registration.html:12
 #: kallithea/templates/login.html:28 kallithea/templates/register.html:31
 msgid "Username"
 msgstr ""
@@ -2026,7 +2024,7 @@
 #: kallithea/templates/admin/settings/settings.html:31
 #: kallithea/templates/admin/users/user_add.html:62
 #: kallithea/templates/admin/users/user_edit_profile.html:25
-#: kallithea/templates/email_templates/registration.html:33
+#: kallithea/templates/email_templates/registration.html:34
 #: kallithea/templates/register.html:66
 msgid "Email"
 msgstr ""
@@ -2273,7 +2271,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/gists/index.html:51
-#: kallithea/templates/data_table/_dt_elements.html:78
+#: kallithea/templates/data_table/_dt_elements.html:84
 msgid "Created"
 msgstr ""
 
@@ -2357,13 +2355,13 @@
 #: kallithea/templates/admin/users/user_edit_ips.html:21
 #: kallithea/templates/changeset/changeset_file_comment.html:30
 #: kallithea/templates/changeset/changeset_file_comment.html:121
-#: kallithea/templates/data_table/_dt_elements.html:69
-#: kallithea/templates/data_table/_dt_elements.html:89
-#: kallithea/templates/data_table/_dt_elements.html:91
-#: kallithea/templates/data_table/_dt_elements.html:101
-#: kallithea/templates/data_table/_dt_elements.html:103
-#: kallithea/templates/data_table/_dt_elements.html:120
-#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:75
+#: kallithea/templates/data_table/_dt_elements.html:95
+#: kallithea/templates/data_table/_dt_elements.html:97
+#: kallithea/templates/data_table/_dt_elements.html:107
+#: kallithea/templates/data_table/_dt_elements.html:109
+#: kallithea/templates/data_table/_dt_elements.html:126
+#: kallithea/templates/data_table/_dt_elements.html:128
 #: kallithea/templates/files/files_source.html:35
 #: kallithea/templates/files/files_source.html:38
 #: kallithea/templates/files/files_source.html:41
@@ -2379,14 +2377,14 @@
 #: kallithea/templates/base/perms_summary.html:44
 #: kallithea/templates/base/perms_summary.html:81
 #: kallithea/templates/base/perms_summary.html:83
-#: kallithea/templates/data_table/_dt_elements.html:63
-#: kallithea/templates/data_table/_dt_elements.html:64
-#: kallithea/templates/data_table/_dt_elements.html:85
-#: kallithea/templates/data_table/_dt_elements.html:86
-#: kallithea/templates/data_table/_dt_elements.html:97
-#: kallithea/templates/data_table/_dt_elements.html:98
-#: kallithea/templates/data_table/_dt_elements.html:116
-#: kallithea/templates/data_table/_dt_elements.html:117
+#: kallithea/templates/data_table/_dt_elements.html:69
+#: kallithea/templates/data_table/_dt_elements.html:70
+#: kallithea/templates/data_table/_dt_elements.html:91
+#: kallithea/templates/data_table/_dt_elements.html:92
+#: kallithea/templates/data_table/_dt_elements.html:103
+#: kallithea/templates/data_table/_dt_elements.html:104
+#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:123
 #: kallithea/templates/files/diff_2way.html:56
 #: kallithea/templates/files/files_source.html:37
 #: kallithea/templates/files/files_source.html:40
@@ -2687,7 +2685,7 @@
 #: kallithea/templates/admin/permissions/permissions_globals.html:27
 #: kallithea/templates/admin/repos/repo_add_base.html:28
 #: kallithea/templates/admin/repos/repo_edit_settings.html:33
-#: kallithea/templates/data_table/_dt_elements.html:134
+#: kallithea/templates/data_table/_dt_elements.html:140
 #: kallithea/templates/forks/fork.html:42
 msgid "Repository group"
 msgstr ""
@@ -2708,7 +2706,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:40
-#: kallithea/templates/data_table/_dt_elements.html:141
+#: kallithea/templates/data_table/_dt_elements.html:147
 msgid "User group"
 msgstr ""
 
@@ -2881,7 +2879,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:121
+#: kallithea/templates/data_table/_dt_elements.html:127
 #, python-format
 msgid "Confirm to delete this group: %s with %s repository"
 msgid_plural "Confirm to delete this group: %s with %s repositories"
@@ -3044,14 +3042,10 @@
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit.html:37
-msgid "Caches"
+msgid "Remote"
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit.html:40
-msgid "Remote"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit.html:43
 #: kallithea/templates/summary/statistics.html:8
 #: kallithea/templates/summary/summary.html:169
 #: kallithea/templates/summary/summary.html:170
@@ -3089,7 +3083,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:46
-#: kallithea/templates/data_table/_dt_elements.html:68
+#: kallithea/templates/data_table/_dt_elements.html:74
 #, python-format
 msgid "Confirm to delete this repository: %s"
 msgstr ""
@@ -3120,43 +3114,14 @@
 "it or restore it."
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:4
-msgid "Invalidate Repository Cache"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:6
-msgid ""
-"Manually invalidate cache for this repository. On first access, the "
-"repository will be cached again."
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:9
-msgid "List of Cached Values"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:12
-msgid "Prefix"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:13
+#: kallithea/templates/admin/repos/repo_edit_fields.html:6
+msgid "Label"
+msgstr ""
+
 #: kallithea/templates/admin/repos/repo_edit_fields.html:7
 msgid "Key"
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:14
-#: kallithea/templates/admin/user_groups/user_group_add.html:40
-#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
-#: kallithea/templates/admin/user_groups/user_groups.html:41
-#: kallithea/templates/admin/users/user_add.html:69
-#: kallithea/templates/admin/users/user_edit_profile.html:74
-#: kallithea/templates/admin/users/users.html:42
-msgid "Active"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_fields.html:6
-msgid "Label"
-msgstr ""
-
 #: kallithea/templates/admin/repos/repo_edit_fields.html:20
 #, python-format
 msgid "Confirm to delete this field: %s"
@@ -3669,6 +3634,15 @@
 msgid "Short, optional description for this user group."
 msgstr ""
 
+#: kallithea/templates/admin/user_groups/user_group_add.html:40
+#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
+#: kallithea/templates/admin/user_groups/user_groups.html:41
+#: kallithea/templates/admin/users/user_add.html:69
+#: kallithea/templates/admin/users/user_edit_profile.html:74
+#: kallithea/templates/admin/users/users.html:42
+msgid "Active"
+msgstr ""
+
 #: kallithea/templates/admin/user_groups/user_group_edit.html:5
 #, python-format
 msgid "%s user group settings"
@@ -3690,7 +3664,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:19
-#: kallithea/templates/data_table/_dt_elements.html:102
+#: kallithea/templates/data_table/_dt_elements.html:108
 #, python-format
 msgid "Confirm to delete this user group: %s"
 msgstr ""
@@ -3764,7 +3738,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:90
+#: kallithea/templates/data_table/_dt_elements.html:96
 #, python-format
 msgid "Confirm to delete this user: %s"
 msgstr ""
@@ -3864,10 +3838,12 @@
 msgstr ""
 
 #: kallithea/templates/base/base.html:167
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Follow"
 msgstr ""
 
 #: kallithea/templates/base/base.html:168
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Unfollow"
 msgstr ""
 
@@ -4271,23 +4247,23 @@
 msgid "Merge"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:96
+#: kallithea/templates/changeset/changeset.html:95
 msgid "Grafted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:102
+#: kallithea/templates/changeset/changeset.html:100
 msgid "Transplanted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:108
+#: kallithea/templates/changeset/changeset.html:106
 msgid "Replaced by:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:122
+#: kallithea/templates/changeset/changeset.html:120
 msgid "Preceded by:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:139
+#: kallithea/templates/changeset/changeset.html:137
 #: kallithea/templates/compare/compare_diff.html:59
 #: kallithea/templates/pullrequests/pullrequest_show.html:290
 #, python-format
@@ -4296,7 +4272,7 @@
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/templates/changeset/changeset.html:141
+#: kallithea/templates/changeset/changeset.html:139
 #: kallithea/templates/compare/compare_diff.html:61
 #: kallithea/templates/pullrequests/pullrequest_show.html:292
 #, python-format
@@ -4305,8 +4281,8 @@
 msgstr[0] ""
 msgstr[1] ""
 
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
 #: kallithea/templates/pullrequests/pullrequest_show.html:333
@@ -4535,23 +4511,23 @@
 msgid "Repository creation in progress..."
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:42
+#: kallithea/templates/data_table/_dt_elements.html:48
 msgid "No changesets yet"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:48
-#: kallithea/templates/data_table/_dt_elements.html:50
+#: kallithea/templates/data_table/_dt_elements.html:54
+#: kallithea/templates/data_table/_dt_elements.html:56
 #, python-format
 msgid "Subscribe to %s rss feed"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:56
-#: kallithea/templates/data_table/_dt_elements.html:58
+#: kallithea/templates/data_table/_dt_elements.html:62
+#: kallithea/templates/data_table/_dt_elements.html:64
 #, python-format
 msgid "Subscribe to %s atom feed"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:76
+#: kallithea/templates/data_table/_dt_elements.html:82
 msgid "Creating"
 msgstr ""
 
@@ -4583,6 +4559,11 @@
 msgid "by"
 msgstr ""
 
+#: kallithea/templates/email_templates/changeset_comment.html:36
+#: kallithea/templates/email_templates/pull_request_comment.html:43
+msgid "View Comment"
+msgstr ""
+
 #: kallithea/templates/email_templates/comment.html:27
 msgid "Status change:"
 msgstr ""
@@ -4591,32 +4572,40 @@
 msgid "The pull request has been closed."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:9
+#: kallithea/templates/email_templates/default.html:4
+msgid "Message"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:4
+msgid "Password Reset Request"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:10
 #, python-format
 msgid "Hello %s"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:16
+#: kallithea/templates/email_templates/password_reset.html:17
 msgid "We have received a request to reset the password for your account."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:25
+#: kallithea/templates/email_templates/password_reset.html:26
 msgid ""
 "This account is however managed outside this system and the password "
 "cannot be changed here."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:28
+#: kallithea/templates/email_templates/password_reset.html:29
 msgid "To set a new password, click the following link"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:33
+#: kallithea/templates/email_templates/password_reset.html:34
 msgid ""
 "Should you not be able to use the link above, please type the following "
 "code into the password reset form"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:44
+#: kallithea/templates/email_templates/password_reset.html:45
 msgid ""
 "If it weren't you who requested the password reset, just disregard this "
 "message."
@@ -4647,6 +4636,10 @@
 msgid "to"
 msgstr ""
 
+#: kallithea/templates/email_templates/pull_request.html:85
+msgid "View Pull Request"
+msgstr ""
+
 #: kallithea/templates/email_templates/pull_request_comment.html:4
 #, python-format
 msgid "Mention in Comment on Pull Request %s \"%s\""
@@ -4662,10 +4655,18 @@
 msgid "Comment on Pull Request %s \"%s\""
 msgstr ""
 
-#: kallithea/templates/email_templates/registration.html:22
+#: kallithea/templates/email_templates/registration.html:5
+msgid "New User Registration"
+msgstr ""
+
+#: kallithea/templates/email_templates/registration.html:23
 msgid "Full Name"
 msgstr ""
 
+#: kallithea/templates/email_templates/registration.html:42
+msgid "View User Profile"
+msgstr ""
+
 #: kallithea/templates/files/diff_2way.html:15
 #, python-format
 msgid "%s File side-by-side diff"
@@ -5266,45 +5267,45 @@
 msgid "Stats gathered: "
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:87
-#: kallithea/templates/summary/summary.html:371
+#: kallithea/templates/summary/statistics.html:85
+#: kallithea/templates/summary/summary.html:369
 msgid "files"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:111
-#: kallithea/templates/summary/summary.html:401
+#: kallithea/templates/summary/statistics.html:109
+#: kallithea/templates/summary/summary.html:399
 msgid "Show more"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:405
+#: kallithea/templates/summary/statistics.html:395
 msgid "commits"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:406
+#: kallithea/templates/summary/statistics.html:396
 msgid "files added"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:407
+#: kallithea/templates/summary/statistics.html:397
 msgid "files changed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:408
+#: kallithea/templates/summary/statistics.html:398
 msgid "files removed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:410
+#: kallithea/templates/summary/statistics.html:400
 msgid "commit"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:411
+#: kallithea/templates/summary/statistics.html:401
 msgid "file added"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:412
+#: kallithea/templates/summary/statistics.html:402
 msgid "file changed"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:413
+#: kallithea/templates/summary/statistics.html:403
 msgid "file removed"
 msgstr ""
 
--- a/kallithea/i18n/uk/LC_MESSAGES/kallithea.po	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/i18n/uk/LC_MESSAGES/kallithea.po	Mon May 04 19:24:04 2020 +0200
@@ -4,7 +4,7 @@
 msgstr ""
 "Project-Id-Version: Kallithea 0.3.2\n"
 "Report-Msgid-Bugs-To: translations@kallithea-scm.org\n"
-"POT-Creation-Date: 2020-01-05 04:10+0100\n"
+"POT-Creation-Date: 2020-05-04 19:12+0200\n"
 "PO-Revision-Date: 2019-11-13 10:04+0000\n"
 "Last-Translator: Oleksandr Shtalinberg <o.shtalinberg@gmail.com>\n"
 "Language-Team: Ukrainian <https://hosted.weblate.org/projects/kallithea/"
@@ -18,14 +18,14 @@
 "X-Generator: Weblate 3.10-dev\n"
 
 #: kallithea/controllers/changelog.py:67
-#: kallithea/controllers/pullrequests.py:250 kallithea/lib/base.py:602
+#: kallithea/controllers/pullrequests.py:247 kallithea/lib/base.py:602
 msgid "There are no changesets yet"
 msgstr "Наборів змін немає"
 
 #: kallithea/controllers/admin/permissions.py:64
 #: kallithea/controllers/admin/permissions.py:68
 #: kallithea/controllers/admin/permissions.py:72
-#: kallithea/controllers/changelog.py:137
+#: kallithea/controllers/changelog.py:136
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:7
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:88
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:7
@@ -34,36 +34,36 @@
 msgid "None"
 msgstr "Нічого"
 
-#: kallithea/controllers/changelog.py:140 kallithea/controllers/files.py:189
+#: kallithea/controllers/changelog.py:139 kallithea/controllers/files.py:189
 msgid "(closed)"
 msgstr "(закрито)"
 
-#: kallithea/controllers/changeset.py:81
+#: kallithea/controllers/changeset.py:82
 msgid "Show whitespace"
 msgstr "Відображати пробіли"
 
-#: kallithea/controllers/changeset.py:88
-#: kallithea/controllers/changeset.py:95
+#: kallithea/controllers/changeset.py:89
+#: kallithea/controllers/changeset.py:96
 #: kallithea/templates/files/diff_2way.html:55
 msgid "Ignore whitespace"
 msgstr "Ігнорувати пробіли"
 
-#: kallithea/controllers/changeset.py:161
+#: kallithea/controllers/changeset.py:162
 #, python-format
 msgid "Increase diff context to %(num)s lines"
 msgstr "Збільшити відмінність контексту для %(num)s рядків"
 
-#: kallithea/controllers/changeset.py:201
+#: kallithea/controllers/changeset.py:202
 msgid "No permission to change status"
 msgstr "У вас немає дозволу змінювати статус"
 
-#: kallithea/controllers/changeset.py:212
+#: kallithea/controllers/changeset.py:213
 #, python-format
 msgid "Successfully deleted pull request %s"
 msgstr "Успішно вилучено pull request %s"
 
 #: kallithea/controllers/changeset.py:319 kallithea/controllers/files.py:89
-#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:700
+#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:697
 msgid "Such revision does not exist for this repository"
 msgstr "Така редакція не існує для цього репозиторію"
 
@@ -76,62 +76,62 @@
 msgid "Cannot compare repositories of different types"
 msgstr "Не вдається порівняти репозиторії різних типів"
 
-#: kallithea/controllers/compare.py:246
+#: kallithea/controllers/compare.py:247
 msgid "Cannot show empty diff"
 msgstr "Не вдалося відобразити пусті відмінності"
 
-#: kallithea/controllers/compare.py:248
+#: kallithea/controllers/compare.py:249
 msgid "No ancestor found for merge diff"
 msgstr "Не знайдено предка для злиття відмінностей"
 
-#: kallithea/controllers/compare.py:252
+#: kallithea/controllers/compare.py:253
 msgid "Multiple merge ancestors found for merge compare"
 msgstr "Множинні злиття предків знайдено для злиття порівняти"
 
-#: kallithea/controllers/compare.py:268
+#: kallithea/controllers/compare.py:269
 msgid "Cannot compare repositories without using common ancestor"
 msgstr "Не вдається порівняти репозиторії без використання спільного предка"
 
-#: kallithea/controllers/error.py:71
+#: kallithea/controllers/error.py:67
 msgid "No response"
 msgstr "Немає відповіді"
 
-#: kallithea/controllers/error.py:72
+#: kallithea/controllers/error.py:68
 msgid "Unknown error"
 msgstr "Невідома помилка"
 
-#: kallithea/controllers/error.py:85
+#: kallithea/controllers/error.py:81
 msgid ""
 "The request could not be understood by the server due to malformed syntax."
 msgstr "Запит не може бути зрозумілий сервером через синтаксичні помилки."
 
-#: kallithea/controllers/error.py:88
+#: kallithea/controllers/error.py:84
 msgid "Unauthorized access to resource"
 msgstr "Несанкціонований доступ до ресурсів"
 
-#: kallithea/controllers/error.py:90
+#: kallithea/controllers/error.py:86
 msgid "You don't have permission to view this page"
 msgstr "Ви не маєте дозволу на перегляд цієї сторінки"
 
-#: kallithea/controllers/error.py:92
+#: kallithea/controllers/error.py:88
 msgid "The resource could not be found"
 msgstr "Ресурс не може бути знайдений"
 
-#: kallithea/controllers/error.py:94
+#: kallithea/controllers/error.py:90
 msgid ""
 "The server encountered an unexpected condition which prevented it from "
 "fulfilling the request."
 msgstr ""
 "На сервері виявлено неочікувану умову, яка перешкоджала виконанню запиту."
 
-#: kallithea/controllers/feed.py:63
+#: kallithea/controllers/feed.py:59
 #, python-format
 msgid "%s committed on %s"
 msgstr "%s зафіксовано на %s"
 
-#: kallithea/controllers/feed.py:88
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/controllers/feed.py:84
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/compare/compare_diff.html:95
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
@@ -139,12 +139,12 @@
 msgid "Changeset was too big and was cut off..."
 msgstr "Changeset був занадто великий і був відрізаний..."
 
-#: kallithea/controllers/feed.py:111 kallithea/controllers/feed.py:140
+#: kallithea/controllers/feed.py:107
 #, python-format
 msgid "%s %s feed"
 msgstr "%s %s канал"
 
-#: kallithea/controllers/feed.py:113 kallithea/controllers/feed.py:142
+#: kallithea/controllers/feed.py:109
 #, python-format
 msgid "Changes on %s repository"
 msgstr "Зміни в репозиторії  %s"
@@ -162,104 +162,104 @@
 msgid "%s at %s"
 msgstr "%s у  %s"
 
-#: kallithea/controllers/files.py:296
+#: kallithea/controllers/files.py:295
 msgid "You can only delete files with revision being a valid branch"
 msgstr "Видаляти файли можна лише з ревізії припустимого бренчу"
 
-#: kallithea/controllers/files.py:307
+#: kallithea/controllers/files.py:306
 #, python-format
 msgid "Deleted file %s via Kallithea"
 msgstr "Видалений файл %s через Kallithea"
 
-#: kallithea/controllers/files.py:331
+#: kallithea/controllers/files.py:330
 #, python-format
 msgid "Successfully deleted file %s"
 msgstr "Успішно видалений файл %s"
 
-#: kallithea/controllers/files.py:335 kallithea/controllers/files.py:394
-#: kallithea/controllers/files.py:469
+#: kallithea/controllers/files.py:334 kallithea/controllers/files.py:392
+#: kallithea/controllers/files.py:467
 msgid "Error occurred during commit"
 msgstr "Під час фіксації сталася помилка"
 
-#: kallithea/controllers/files.py:350
+#: kallithea/controllers/files.py:349
 msgid "You can only edit files with revision being a valid branch"
 msgstr "Редагувати файли можна лише з ревізії, що належить валідному бренчу"
 
-#: kallithea/controllers/files.py:364
+#: kallithea/controllers/files.py:363
 #, python-format
 msgid "Edited file %s via Kallithea"
 msgstr "Відредагований файл %s через Kallithea"
 
-#: kallithea/controllers/files.py:380
+#: kallithea/controllers/files.py:378
 msgid "No changes"
 msgstr "Нема змін"
 
-#: kallithea/controllers/files.py:390 kallithea/controllers/files.py:458
+#: kallithea/controllers/files.py:388 kallithea/controllers/files.py:456
 #, python-format
 msgid "Successfully committed to %s"
 msgstr "Успішно зафіксовано в  %s"
 
-#: kallithea/controllers/files.py:409
+#: kallithea/controllers/files.py:407
 msgid "Added file via Kallithea"
 msgstr "Доданий файл через Kallithea"
 
-#: kallithea/controllers/files.py:430
+#: kallithea/controllers/files.py:428
 msgid "No content"
 msgstr "Немає вмісту"
 
-#: kallithea/controllers/files.py:434
+#: kallithea/controllers/files.py:432
 msgid "No filename"
 msgstr "Не вказано назви файлу"
 
-#: kallithea/controllers/files.py:461
+#: kallithea/controllers/files.py:459
 msgid "Location must be relative path and must not contain .. in path"
 msgstr ""
 "Розташування має бути відносним шляхом і не повинен містити .. в шляху"
 
-#: kallithea/controllers/files.py:493
+#: kallithea/controllers/files.py:491
 msgid "Downloads disabled"
 msgstr "Завантаження вимкнено"
 
-#: kallithea/controllers/files.py:504
+#: kallithea/controllers/files.py:502
 #, python-format
 msgid "Unknown revision %s"
 msgstr "Невідома редакція %s"
 
-#: kallithea/controllers/files.py:506
+#: kallithea/controllers/files.py:504
 msgid "Empty repository"
 msgstr "Порожній репозиторій"
 
-#: kallithea/controllers/files.py:508
+#: kallithea/controllers/files.py:506
 msgid "Unknown archive type"
 msgstr "Невідомий тип архіву"
 
-#: kallithea/controllers/files.py:729
+#: kallithea/controllers/files.py:726
 #: kallithea/templates/changeset/changeset_range.html:9
 #: kallithea/templates/email_templates/pull_request.html:64
 #: kallithea/templates/pullrequests/pullrequest.html:84
 msgid "Changesets"
 msgstr "Набори змін"
 
-#: kallithea/controllers/files.py:730
-#: kallithea/controllers/pullrequests.py:182 kallithea/model/scm.py:676
+#: kallithea/controllers/files.py:727
+#: kallithea/controllers/pullrequests.py:174 kallithea/model/scm.py:663
 msgid "Branches"
 msgstr "Гілки"
 
-#: kallithea/controllers/files.py:731
-#: kallithea/controllers/pullrequests.py:183 kallithea/model/scm.py:687
+#: kallithea/controllers/files.py:728
+#: kallithea/controllers/pullrequests.py:175 kallithea/model/scm.py:674
 msgid "Tags"
 msgstr "Теги"
 
-#: kallithea/controllers/forks.py:174
+#: kallithea/controllers/forks.py:175
 #, python-format
 msgid "An error occurred during repository forking %s"
 msgstr "Під час forking репозиторію %s сталася помилка"
 
-#: kallithea/controllers/home.py:79
+#: kallithea/controllers/home.py:77
 msgid "Groups"
 msgstr "Групи"
 
-#: kallithea/controllers/home.py:89
+#: kallithea/controllers/home.py:87
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:90
 #: kallithea/templates/admin/repos/repo_add.html:12
 #: kallithea/templates/admin/repos/repo_add.html:16
@@ -271,48 +271,54 @@
 msgid "Repositories"
 msgstr "Репозиторії"
 
-#: kallithea/controllers/home.py:122
+#: kallithea/controllers/home.py:119
 #: kallithea/templates/files/files_add.html:32
 #: kallithea/templates/files/files_delete.html:23
 #: kallithea/templates/files/files_edit.html:32
 msgid "Branch"
 msgstr "Гілка"
 
-#: kallithea/controllers/home.py:128
+#: kallithea/controllers/home.py:125
 msgid "Closed Branches"
 msgstr "Закриті Гілки"
 
-#: kallithea/controllers/home.py:134
+#: kallithea/controllers/home.py:131
 msgid "Tag"
 msgstr "Тег"
 
-#: kallithea/controllers/home.py:140
+#: kallithea/controllers/home.py:137
 msgid "Bookmark"
 msgstr "Закладка"
 
-#: kallithea/controllers/journal.py:112 kallithea/controllers/journal.py:154
+#: kallithea/controllers/journal.py:146 kallithea/controllers/journal.py:157
 #: kallithea/templates/journal/public_journal.html:4
 #: kallithea/templates/journal/public_journal.html:18
 msgid "Public Journal"
 msgstr "Публічний журнал"
 
-#: kallithea/controllers/journal.py:116 kallithea/controllers/journal.py:158
+#: kallithea/controllers/journal.py:150 kallithea/controllers/journal.py:161
 #: kallithea/templates/base/base.html:290
 #: kallithea/templates/journal/journal.html:5
 #: kallithea/templates/journal/journal.html:13
 msgid "Journal"
 msgstr "Журнал"
 
-#: kallithea/controllers/login.py:140 kallithea/controllers/login.py:185
+#: kallithea/controllers/login.py:109
+#, fuzzy
+#| msgid "HTTP authentication realm"
+msgid "Authentication failed."
+msgstr "Область автентифікації HTTP"
+
+#: kallithea/controllers/login.py:142 kallithea/controllers/login.py:187
 msgid "Bad captcha"
 msgstr "Погана капча"
 
-#: kallithea/controllers/login.py:146
+#: kallithea/controllers/login.py:148
 #, python-format
 msgid "You have successfully registered with %s"
 msgstr "Ви успішно зареєстровані з  %s"
 
-#: kallithea/controllers/login.py:190
+#: kallithea/controllers/login.py:192
 msgid "A password reset confirmation code has been sent"
 msgstr "Надісланий код підтвердження скидання пароля"
 
@@ -325,114 +331,114 @@
 msgid "Successfully updated password"
 msgstr "Пароль успішно оновлений"
 
-#: kallithea/controllers/pullrequests.py:68
+#: kallithea/controllers/pullrequests.py:67
 #, python-format
 msgid "Invalid reviewer \"%s\" specified"
 msgstr "Вказаний недійсний рецензент \"%s\""
 
-#: kallithea/controllers/pullrequests.py:131
+#: kallithea/controllers/pullrequests.py:123
 #, python-format
 msgid "%s (closed)"
 msgstr "%s (закрито)"
 
-#: kallithea/controllers/pullrequests.py:158
+#: kallithea/controllers/pullrequests.py:150
 #: kallithea/templates/changeset/changeset.html:12
 msgid "Changeset"
 msgstr "Набір змін"
 
-#: kallithea/controllers/pullrequests.py:179
+#: kallithea/controllers/pullrequests.py:171
 msgid "Special"
 msgstr "Спеціальний"
 
-#: kallithea/controllers/pullrequests.py:180
+#: kallithea/controllers/pullrequests.py:172
 msgid "Peer branches"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:181 kallithea/model/scm.py:682
+#: kallithea/controllers/pullrequests.py:173 kallithea/model/scm.py:669
 msgid "Bookmarks"
 msgstr "Закладки"
 
-#: kallithea/controllers/pullrequests.py:318
+#: kallithea/controllers/pullrequests.py:315
 #, python-format
 msgid "Error creating pull request: %s"
 msgstr "Помилка створення pull request: %s"
 
-#: kallithea/controllers/pullrequests.py:345
-#: kallithea/controllers/pullrequests.py:368
+#: kallithea/controllers/pullrequests.py:342
+#: kallithea/controllers/pullrequests.py:365
 msgid "Error occurred while creating pull request"
 msgstr "Сталася помилка при створенні pull request"
 
-#: kallithea/controllers/pullrequests.py:350
+#: kallithea/controllers/pullrequests.py:347
 msgid "Successfully opened new pull request"
 msgstr "Новий pull request успішно відкритий"
 
-#: kallithea/controllers/pullrequests.py:373
+#: kallithea/controllers/pullrequests.py:370
 msgid "New pull request iteration created"
 msgstr "Створено нову ітерацію запиту на pull request"
 
-#: kallithea/controllers/pullrequests.py:401
+#: kallithea/controllers/pullrequests.py:398
 #, python-format
 msgid "Meanwhile, the following reviewers have been added: %s"
 msgstr "Тим часом додано наступних рецензентів: %s"
 
-#: kallithea/controllers/pullrequests.py:405
+#: kallithea/controllers/pullrequests.py:402
 #, python-format
 msgid "Meanwhile, the following reviewers have been removed: %s"
 msgstr "Тим часом було видалено наступних рецензентів: %s"
 
-#: kallithea/controllers/pullrequests.py:421
-#: kallithea/model/pull_request.py:232
+#: kallithea/controllers/pullrequests.py:418
+#: kallithea/model/pull_request.py:230
 msgid "No description"
 msgstr "Без опису"
 
-#: kallithea/controllers/pullrequests.py:430
+#: kallithea/controllers/pullrequests.py:427
 msgid "Pull request updated"
 msgstr "Pull request оновлено"
 
-#: kallithea/controllers/pullrequests.py:443
+#: kallithea/controllers/pullrequests.py:440
 msgid "Successfully deleted pull request"
 msgstr "Успішно вилучено pull request"
 
-#: kallithea/controllers/pullrequests.py:479
+#: kallithea/controllers/pullrequests.py:476
 #, python-format
 msgid "Revision %s not found in %s"
 msgstr "Ревізія %s не знайдена в %s"
 
-#: kallithea/controllers/pullrequests.py:506
+#: kallithea/controllers/pullrequests.py:504
 #, python-format
 msgid "Error: changesets not found when displaying pull request from %s."
 msgstr ""
 "Помилка: changesets не знайдено під час відображення pull request з %s."
 
-#: kallithea/controllers/pullrequests.py:520
+#: kallithea/controllers/pullrequests.py:518
 #, python-format
 msgid "This pull request has already been merged to %s."
 msgstr "Цей pull request уже об'єднано з  %s."
 
-#: kallithea/controllers/pullrequests.py:522
+#: kallithea/controllers/pullrequests.py:520
 msgid "This pull request has been closed and can not be updated."
 msgstr "Цей pull request закрито, його не можна оновити."
 
-#: kallithea/controllers/pullrequests.py:546
+#: kallithea/controllers/pullrequests.py:539
 #, python-format
 msgid "The following additional changes are available on %s:"
 msgstr "Наступні додаткові зміни доступні на %s:"
 
-#: kallithea/controllers/pullrequests.py:548
-#: kallithea/controllers/pullrequests.py:552
+#: kallithea/controllers/pullrequests.py:541
+#: kallithea/controllers/pullrequests.py:545
 msgid "No additional changesets found for iterating on this pull request."
 msgstr "Немає додаткових змін для ітератування на  pull request."
 
-#: kallithea/controllers/pullrequests.py:560
+#: kallithea/controllers/pullrequests.py:553
 #, python-format
 msgid "Note: Branch %s has another head: %s."
 msgstr "Примітка: гілка %s має іншу голову: %s."
 
-#: kallithea/controllers/pullrequests.py:567
+#: kallithea/controllers/pullrequests.py:560
 msgid "Git pull requests don't support iterating yet."
 msgstr "Git pull requests не підтримують ітерацію."
 
-#: kallithea/controllers/pullrequests.py:569
+#: kallithea/controllers/pullrequests.py:562
 #, python-format
 msgid ""
 "Error: some changesets not found when displaying pull request from %s."
@@ -440,114 +446,114 @@
 "Помилка: деякі changesets  не знайдені під час відображення pull request "
 "з %s."
 
-#: kallithea/controllers/pullrequests.py:593
+#: kallithea/controllers/pullrequests.py:586
 msgid "The diff can't be shown - the PR revisions could not be found."
 msgstr "Різниця не може бути показана - версії PR не вдалося знайти."
 
+#: kallithea/controllers/search.py:132
+msgid "Invalid search query. Try quoting it."
+msgstr "Неприпустимий пошуковий запит. Спробуйте цитувати його."
+
 #: kallithea/controllers/search.py:136
-msgid "Invalid search query. Try quoting it."
-msgstr "Неприпустимий пошуковий запит. Спробуйте цитувати його."
-
-#: kallithea/controllers/search.py:140
 msgid "The server has no search index."
 msgstr "Сервер не має індексу пошуку."
 
-#: kallithea/controllers/search.py:143
+#: kallithea/controllers/search.py:139
 msgid "An error occurred during search operation."
 msgstr "Сталася помилка під час операції пошуку."
 
-#: kallithea/controllers/summary.py:168
-#: kallithea/templates/summary/summary.html:412
+#: kallithea/controllers/summary.py:169
+#: kallithea/templates/summary/summary.html:410
 msgid "No data ready yet"
 msgstr "Дані ще не готові"
 
-#: kallithea/controllers/summary.py:171
+#: kallithea/controllers/summary.py:172
 #: kallithea/templates/summary/summary.html:97
 msgid "Statistics are disabled for this repository"
 msgstr "Статистичні дані для цього репозиторію вимкнено"
 
-#: kallithea/controllers/admin/auth_settings.py:137
+#: kallithea/controllers/admin/auth_settings.py:136
 msgid "Auth settings updated successfully"
 msgstr "Параметри автентифікації успішно оновлено"
 
-#: kallithea/controllers/admin/auth_settings.py:148
+#: kallithea/controllers/admin/auth_settings.py:147
 msgid "error occurred during update of auth settings"
 msgstr "під час оновлення параметрів автентифікації сталася помилка"
 
-#: kallithea/controllers/admin/defaults.py:75
+#: kallithea/controllers/admin/defaults.py:74
 msgid "Default settings updated successfully"
 msgstr "Параметри за промовчанням оновлено успішно"
 
-#: kallithea/controllers/admin/defaults.py:90
+#: kallithea/controllers/admin/defaults.py:89
 msgid "Error occurred during update of defaults"
 msgstr "Сталася помилка під час оновлення за промовчанням"
 
 #: kallithea/controllers/admin/gists.py:59
 #: kallithea/controllers/admin/my_account.py:232
-#: kallithea/controllers/admin/users.py:248
+#: kallithea/controllers/admin/users.py:246
 msgid "Forever"
 msgstr "Назавжди"
 
 #: kallithea/controllers/admin/gists.py:60
 #: kallithea/controllers/admin/my_account.py:233
-#: kallithea/controllers/admin/users.py:249
+#: kallithea/controllers/admin/users.py:247
 msgid "5 minutes"
 msgstr "5 хвилин"
 
 #: kallithea/controllers/admin/gists.py:61
 #: kallithea/controllers/admin/my_account.py:234
-#: kallithea/controllers/admin/users.py:250
+#: kallithea/controllers/admin/users.py:248
 msgid "1 hour"
 msgstr "1 година"
 
 #: kallithea/controllers/admin/gists.py:62
 #: kallithea/controllers/admin/my_account.py:235
-#: kallithea/controllers/admin/users.py:251
+#: kallithea/controllers/admin/users.py:249
 msgid "1 day"
 msgstr "1 день"
 
 #: kallithea/controllers/admin/gists.py:63
 #: kallithea/controllers/admin/my_account.py:236
-#: kallithea/controllers/admin/users.py:252
+#: kallithea/controllers/admin/users.py:250
 msgid "1 month"
 msgstr "1 місяць"
 
 #: kallithea/controllers/admin/gists.py:67
 #: kallithea/controllers/admin/my_account.py:238
-#: kallithea/controllers/admin/users.py:254
+#: kallithea/controllers/admin/users.py:252
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:65
 #: kallithea/templates/admin/users/user_edit_api_keys.html:65
 msgid "Lifetime"
 msgstr "Постійно"
 
-#: kallithea/controllers/admin/gists.py:142
+#: kallithea/controllers/admin/gists.py:148
 msgid "Error occurred during gist creation"
 msgstr "Сталася помилка під час створення GIST"
 
-#: kallithea/controllers/admin/gists.py:158
+#: kallithea/controllers/admin/gists.py:164
 #, python-format
 msgid "Deleted gist %s"
 msgstr "Видалено gist %s"
 
-#: kallithea/controllers/admin/gists.py:198
+#: kallithea/controllers/admin/gists.py:207
 msgid "Unmodified"
 msgstr "Незмінений"
 
-#: kallithea/controllers/admin/gists.py:228
+#: kallithea/controllers/admin/gists.py:237
 msgid "Successfully updated gist content"
 msgstr "Зміст gist успішно оновлено"
 
-#: kallithea/controllers/admin/gists.py:233
+#: kallithea/controllers/admin/gists.py:242
 msgid "Successfully updated gist data"
 msgstr "Дані gist успішно оновлені"
 
-#: kallithea/controllers/admin/gists.py:236
+#: kallithea/controllers/admin/gists.py:245
 #, python-format
 msgid "Error occurred during update of gist %s"
 msgstr "Сталася помилка під час оновлення gist %s"
 
-#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:211
-#: kallithea/model/user.py:232
+#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:205
+#: kallithea/model/user.py:226
 msgid "You can't edit this user since it's crucial for entire application"
 msgstr ""
 "Ви не можете редагувати цього користувача, оскільки це важливо для всієї "
@@ -558,7 +564,7 @@
 msgstr "Ваш обліковий запис успішно оновлено"
 
 #: kallithea/controllers/admin/my_account.py:134
-#: kallithea/controllers/admin/users.py:181
+#: kallithea/controllers/admin/users.py:179
 #, python-format
 msgid "Error occurred during update of user %s"
 msgstr "Сталася помилка під час оновлення користувача %s"
@@ -568,44 +574,44 @@
 msgstr "Сталася помилка під час оновлення пароля користувача"
 
 #: kallithea/controllers/admin/my_account.py:209
-#: kallithea/controllers/admin/users.py:367
+#: kallithea/controllers/admin/users.py:365
 #, python-format
 msgid "Added email %s to user"
 msgstr "Додано email %s користувачу"
 
 #: kallithea/controllers/admin/my_account.py:215
-#: kallithea/controllers/admin/users.py:373
+#: kallithea/controllers/admin/users.py:371
 msgid "An error occurred during email saving"
 msgstr "Сталася помилка під час збереження електронної пошти"
 
 #: kallithea/controllers/admin/my_account.py:224
-#: kallithea/controllers/admin/users.py:383
+#: kallithea/controllers/admin/users.py:381
 msgid "Removed email from user"
 msgstr "Видалено email користувача"
 
 #: kallithea/controllers/admin/my_account.py:248
-#: kallithea/controllers/admin/users.py:271
+#: kallithea/controllers/admin/users.py:269
 msgid "API key successfully created"
 msgstr "API ключ успішно створений"
 
 #: kallithea/controllers/admin/my_account.py:257
-#: kallithea/controllers/admin/users.py:281
+#: kallithea/controllers/admin/users.py:279
 msgid "API key successfully reset"
 msgstr "Ключ API успішно скинуто"
 
 #: kallithea/controllers/admin/my_account.py:261
-#: kallithea/controllers/admin/users.py:285
+#: kallithea/controllers/admin/users.py:283
 msgid "API key successfully deleted"
 msgstr "API ключ успішно видалений"
 
 #: kallithea/controllers/admin/my_account.py:281
-#: kallithea/controllers/admin/users.py:456
+#: kallithea/controllers/admin/users.py:454
 #, python-format
 msgid "SSH key %s successfully added"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:293
-#: kallithea/controllers/admin/users.py:470
+#: kallithea/controllers/admin/users.py:468
 msgid "SSH key successfully deleted"
 msgstr ""
 
@@ -681,11 +687,11 @@
 msgid "Allowed with automatic account activation"
 msgstr "Дозволено з автоматичною активацію облікового запису"
 
-#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1673
+#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1578
 msgid "Manual activation of external account"
 msgstr "Ручна Активація зовнішнього акаунту"
 
-#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1674
+#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1579
 msgid "Automatic activation of external account"
 msgstr "Автоматична Активація зовнішнього акаунту"
 
@@ -707,186 +713,178 @@
 msgid "Error occurred during update of permissions"
 msgstr "Сталася помилка під час оновлення прав"
 
-#: kallithea/controllers/admin/repo_groups.py:172
+#: kallithea/controllers/admin/repo_groups.py:165
 #, python-format
 msgid "Error occurred during creation of repository group %s"
 msgstr "Сталася помилка при створенні repository group %s"
 
-#: kallithea/controllers/admin/repo_groups.py:177
+#: kallithea/controllers/admin/repo_groups.py:172
 #, python-format
 msgid "Created repository group %s"
 msgstr "Створена група репозиторіїв %s"
 
-#: kallithea/controllers/admin/repo_groups.py:224
+#: kallithea/controllers/admin/repo_groups.py:219
 #, python-format
 msgid "Updated repository group %s"
 msgstr "Оновлено групу репозиторіїв %s"
 
-#: kallithea/controllers/admin/repo_groups.py:240
+#: kallithea/controllers/admin/repo_groups.py:235
 #, python-format
 msgid "Error occurred during update of repository group %s"
 msgstr "Сталася помилка при оновленні repository group %s"
 
-#: kallithea/controllers/admin/repo_groups.py:250
+#: kallithea/controllers/admin/repo_groups.py:245
 #, python-format
 msgid "This group contains %s repositories and cannot be deleted"
 msgstr "Ця група містить %s репозиторії, і їх неможливо видалити"
 
-#: kallithea/controllers/admin/repo_groups.py:257
+#: kallithea/controllers/admin/repo_groups.py:252
 #, python-format
 msgid "This group contains %s subgroups and cannot be deleted"
 msgstr "Ця група містить %s підгрупи і не може бути видалена"
 
-#: kallithea/controllers/admin/repo_groups.py:263
+#: kallithea/controllers/admin/repo_groups.py:258
 #, python-format
 msgid "Removed repository group %s"
 msgstr "Видалена група репозиторіїв %s"
 
-#: kallithea/controllers/admin/repo_groups.py:268
+#: kallithea/controllers/admin/repo_groups.py:263
 #, python-format
 msgid "Error occurred during deletion of repository group %s"
 msgstr "Сталася помилка під час видалення групи репохиторіїв %s"
 
-#: kallithea/controllers/admin/repo_groups.py:352
-#: kallithea/controllers/admin/repo_groups.py:382
-#: kallithea/controllers/admin/user_groups.py:294
+#: kallithea/controllers/admin/repo_groups.py:347
+#: kallithea/controllers/admin/repo_groups.py:377
+#: kallithea/controllers/admin/user_groups.py:290
 msgid "Cannot revoke permission for yourself as admin"
 msgstr "Неможливо відкликати дозвіл для себе як адміністратора"
 
-#: kallithea/controllers/admin/repo_groups.py:367
+#: kallithea/controllers/admin/repo_groups.py:362
 msgid "Repository group permissions updated"
 msgstr "Оновлено дозволи групи репозиторіїв"
 
-#: kallithea/controllers/admin/repo_groups.py:399
-#: kallithea/controllers/admin/repos.py:358
-#: kallithea/controllers/admin/user_groups.py:306
+#: kallithea/controllers/admin/repo_groups.py:394
+#: kallithea/controllers/admin/repos.py:357
+#: kallithea/controllers/admin/user_groups.py:302
 msgid "An error occurred during revoking of permission"
 msgstr "Сталася помилка під час відкликання прав"
 
-#: kallithea/controllers/admin/repos.py:136
+#: kallithea/controllers/admin/repos.py:137
 #, python-format
 msgid "Error creating repository %s"
 msgstr "Помилка створення репозиторію  %s"
 
-#: kallithea/controllers/admin/repos.py:194
+#: kallithea/controllers/admin/repos.py:193
 #, python-format
 msgid "Created repository %s from %s"
 msgstr "Створено репозиторій  %s з %s"
 
-#: kallithea/controllers/admin/repos.py:203
+#: kallithea/controllers/admin/repos.py:202
 #, python-format
 msgid "Forked repository %s as %s"
 msgstr "Роздвоєно репозиторій %s як %s"
 
-#: kallithea/controllers/admin/repos.py:206
+#: kallithea/controllers/admin/repos.py:205
 #, python-format
 msgid "Created repository %s"
 msgstr "Створено репозиторій %s"
 
-#: kallithea/controllers/admin/repos.py:235
+#: kallithea/controllers/admin/repos.py:234
 #, python-format
 msgid "Repository %s updated successfully"
 msgstr "Репозиторій %s успішно оновлений"
 
-#: kallithea/controllers/admin/repos.py:255
+#: kallithea/controllers/admin/repos.py:254
 #, python-format
 msgid "Error occurred during update of repository %s"
 msgstr "Сталася помилка при оновленні репозиторію %s"
 
-#: kallithea/controllers/admin/repos.py:273
+#: kallithea/controllers/admin/repos.py:272
 #, python-format
 msgid "Detached %s forks"
 msgstr "Від'єднано %s forks"
 
-#: kallithea/controllers/admin/repos.py:276
+#: kallithea/controllers/admin/repos.py:275
 #, python-format
 msgid "Deleted %s forks"
 msgstr "Видалено %s forks"
 
-#: kallithea/controllers/admin/repos.py:281
+#: kallithea/controllers/admin/repos.py:280
 #, python-format
 msgid "Deleted repository %s"
 msgstr "Видалений репозиторій %s"
 
-#: kallithea/controllers/admin/repos.py:284
+#: kallithea/controllers/admin/repos.py:283
 #, python-format
 msgid "Cannot delete repository %s which still has forks"
 msgstr "Неможливо видалити репозиторій %s, що ще має forks"
 
-#: kallithea/controllers/admin/repos.py:289
+#: kallithea/controllers/admin/repos.py:288
 #, python-format
 msgid "An error occurred during deletion of %s"
 msgstr "Сталася помилка під час видалення %s"
 
-#: kallithea/controllers/admin/repos.py:329
+#: kallithea/controllers/admin/repos.py:328
 msgid "Repository permissions updated"
 msgstr "Права доступів до репозиторіїв оновлено"
 
-#: kallithea/controllers/admin/repos.py:388
+#: kallithea/controllers/admin/repos.py:387
 #, python-format
 msgid "Field validation error: %s"
 msgstr "Помилка перевірки поля: %s"
 
-#: kallithea/controllers/admin/repos.py:391
+#: kallithea/controllers/admin/repos.py:390
 #, python-format
 msgid "An error occurred during creation of field: %r"
 msgstr "Сталася помилка під час створення поля: %r"
 
-#: kallithea/controllers/admin/repos.py:402
+#: kallithea/controllers/admin/repos.py:401
 msgid "An error occurred during removal of field"
 msgstr "Під час видалення поля виникла помилка"
 
-#: kallithea/controllers/admin/repos.py:416
+#: kallithea/controllers/admin/repos.py:415
 msgid "-- Not a fork --"
 msgstr "-- Не fork --"
 
-#: kallithea/controllers/admin/repos.py:448
+#: kallithea/controllers/admin/repos.py:447
 msgid "Updated repository visibility in public journal"
 msgstr "Оновлено видимість репозиторія в публічному журналі"
 
-#: kallithea/controllers/admin/repos.py:452
+#: kallithea/controllers/admin/repos.py:451
 msgid "An error occurred during setting this repository in public journal"
 msgstr ""
 "Сталася помилка під час налаштувань цього репозиторію в публічному журналі"
 
-#: kallithea/controllers/admin/repos.py:468
+#: kallithea/controllers/admin/repos.py:467
 msgid "Nothing"
 msgstr "Нічого"
 
-#: kallithea/controllers/admin/repos.py:470
+#: kallithea/controllers/admin/repos.py:469
 #, python-format
 msgid "Marked repository %s as fork of %s"
 msgstr "Позначено репозиторій %s як відгалуження від %s"
 
-#: kallithea/controllers/admin/repos.py:477
+#: kallithea/controllers/admin/repos.py:476
 msgid "An error occurred during this operation"
 msgstr "Сталася помилка під час виконання цієї операції"
 
-#: kallithea/controllers/admin/repos.py:490
-msgid "Cache invalidation successful"
-msgstr "Інвалідація кешу успішна"
-
-#: kallithea/controllers/admin/repos.py:494
-msgid "An error occurred during cache invalidation"
-msgstr "Сталася помилка під час Анулювання кеша"
-
-#: kallithea/controllers/admin/repos.py:507
+#: kallithea/controllers/admin/repos.py:488
 msgid "Pulled from remote location"
 msgstr "Витягнуто з віддаленого місця"
 
-#: kallithea/controllers/admin/repos.py:510
+#: kallithea/controllers/admin/repos.py:491
 msgid "An error occurred during pull from remote location"
 msgstr "Сталася помилка під час витягування з віддаленого розташування"
 
-#: kallithea/controllers/admin/repos.py:541
+#: kallithea/controllers/admin/repos.py:522
 msgid "An error occurred during deletion of repository stats"
 msgstr "Під час видалення статистики репозиторію сталася помилка"
 
-#: kallithea/controllers/admin/settings.py:131
+#: kallithea/controllers/admin/settings.py:132
 msgid "Updated VCS settings"
 msgstr "Оновлені налаштування VCS"
 
-#: kallithea/controllers/admin/settings.py:135 kallithea/lib/utils.py:237
+#: kallithea/controllers/admin/settings.py:136
 msgid ""
 "Unable to activate hgsubversion support. The \"hgsubversion\" library is "
 "missing"
@@ -894,157 +892,157 @@
 "Не вдається активувати підтримку hgsubversion. Бібліотека \"hgsubversion"
 "\" відсутня"
 
-#: kallithea/controllers/admin/settings.py:141
-#: kallithea/controllers/admin/settings.py:233
+#: kallithea/controllers/admin/settings.py:142
+#: kallithea/controllers/admin/settings.py:234
 msgid "Error occurred while updating application settings"
 msgstr "Під час оновлення параметрів застосунку сталася помилка"
 
-#: kallithea/controllers/admin/settings.py:176
+#: kallithea/controllers/admin/settings.py:177
 #, python-format
 msgid "Repositories successfully rescanned. Added: %s. Removed: %s."
 msgstr "Репозиторії успішно перескановано. Додано: %s. Видалено: %s."
 
-#: kallithea/controllers/admin/settings.py:188
+#: kallithea/controllers/admin/settings.py:189
 #, python-format
 msgid "Invalidated %s repositories"
 msgstr "До оновлення %s репозиторіїв"
 
-#: kallithea/controllers/admin/settings.py:229
+#: kallithea/controllers/admin/settings.py:230
 msgid "Updated application settings"
 msgstr "Оновлені параметри застосунку"
 
-#: kallithea/controllers/admin/settings.py:283
+#: kallithea/controllers/admin/settings.py:284
 msgid "Updated visualisation settings"
 msgstr "Оновлені параметри візуалізації"
 
-#: kallithea/controllers/admin/settings.py:288
+#: kallithea/controllers/admin/settings.py:289
 msgid "Error occurred during updating visualisation settings"
 msgstr "Під час оновлення параметрів візуалізації сталася помилка"
 
-#: kallithea/controllers/admin/settings.py:312
+#: kallithea/controllers/admin/settings.py:313
 msgid "Please enter email address"
 msgstr "Будь ласка, введіть адресу електронної пошти"
 
-#: kallithea/controllers/admin/settings.py:327
+#: kallithea/controllers/admin/settings.py:328
 msgid "Send email task created"
 msgstr "Надіслати електронною поштою завдання створено"
 
-#: kallithea/controllers/admin/settings.py:355
+#: kallithea/controllers/admin/settings.py:356
 msgid "Hook already exists"
 msgstr "Hook вже існує"
 
-#: kallithea/controllers/admin/settings.py:357
+#: kallithea/controllers/admin/settings.py:358
 msgid "Builtin hooks are read-only. Please use another hook name."
 msgstr ""
 "Вбудовані hooks доступні лише для читання. Будь ласка, використовуйте "
 "інше ім'я hook."
 
-#: kallithea/controllers/admin/settings.py:360
+#: kallithea/controllers/admin/settings.py:361
 msgid "Added new hook"
 msgstr "Додано новий hook"
 
-#: kallithea/controllers/admin/settings.py:376
+#: kallithea/controllers/admin/settings.py:377
 msgid "Updated hooks"
 msgstr "Оновлено hooks"
 
-#: kallithea/controllers/admin/settings.py:380
+#: kallithea/controllers/admin/settings.py:381
 msgid "Error occurred during hook creation"
 msgstr "Сталася помилка під час створення hook"
 
-#: kallithea/controllers/admin/settings.py:404
+#: kallithea/controllers/admin/settings.py:405
 msgid "Whoosh reindex task scheduled"
 msgstr "Завдання реіндекса Whoosh заплановано"
 
-#: kallithea/controllers/admin/user_groups.py:138
+#: kallithea/controllers/admin/user_groups.py:134
 #, python-format
 msgid "Created user group %s"
 msgstr "Створена Група користувачів %s"
 
-#: kallithea/controllers/admin/user_groups.py:151
+#: kallithea/controllers/admin/user_groups.py:147
 #, python-format
 msgid "Error occurred during creation of user group %s"
 msgstr "Під час створення групи користувачів  %s сталася помилка"
 
-#: kallithea/controllers/admin/user_groups.py:179
+#: kallithea/controllers/admin/user_groups.py:175
 #, python-format
 msgid "Updated user group %s"
 msgstr "Оновлена група користувачів %s"
 
-#: kallithea/controllers/admin/user_groups.py:201
+#: kallithea/controllers/admin/user_groups.py:197
 #, python-format
 msgid "Error occurred during update of user group %s"
 msgstr "Сталася помилка під час оновлення групи користувачів %s"
 
-#: kallithea/controllers/admin/user_groups.py:212
+#: kallithea/controllers/admin/user_groups.py:208
 msgid "Successfully deleted user group"
 msgstr "Група користувачів успішно видалена"
 
-#: kallithea/controllers/admin/user_groups.py:217
+#: kallithea/controllers/admin/user_groups.py:213
 msgid "An error occurred during deletion of user group"
 msgstr "Під час видалення групи користувачів сталася помилка"
 
-#: kallithea/controllers/admin/user_groups.py:273
+#: kallithea/controllers/admin/user_groups.py:269
 msgid "Target group cannot be the same"
 msgstr "Цільова група не може бути однаковою"
 
-#: kallithea/controllers/admin/user_groups.py:279
+#: kallithea/controllers/admin/user_groups.py:275
 msgid "User group permissions updated"
 msgstr "Права на групи користувачів оновлені"
 
-#: kallithea/controllers/admin/user_groups.py:388
-#: kallithea/controllers/admin/users.py:338
+#: kallithea/controllers/admin/user_groups.py:384
+#: kallithea/controllers/admin/users.py:336
 msgid "Updated permissions"
 msgstr "Оновлені дозволи"
 
-#: kallithea/controllers/admin/user_groups.py:392
-#: kallithea/controllers/admin/users.py:342
+#: kallithea/controllers/admin/user_groups.py:388
+#: kallithea/controllers/admin/users.py:340
 msgid "An error occurred during permissions saving"
 msgstr "Сталася помилка під час збереження дозволів"
 
-#: kallithea/controllers/admin/users.py:123
+#: kallithea/controllers/admin/users.py:121
 #, python-format
 msgid "Created user %s"
 msgstr "Створено користувача %s"
 
-#: kallithea/controllers/admin/users.py:138
+#: kallithea/controllers/admin/users.py:136
 #, python-format
 msgid "Error occurred during creation of user %s"
 msgstr "Під час створення користувача %s сталася помилка"
 
-#: kallithea/controllers/admin/users.py:162
+#: kallithea/controllers/admin/users.py:160
 msgid "User updated successfully"
 msgstr "Користувач успішно оновлений"
 
-#: kallithea/controllers/admin/users.py:190
+#: kallithea/controllers/admin/users.py:188
 msgid "Successfully deleted user"
 msgstr "Користувач успішно видалений"
 
-#: kallithea/controllers/admin/users.py:195
+#: kallithea/controllers/admin/users.py:193
 msgid "An error occurred during deletion of user"
 msgstr "Сталася помилка під час видалення користувача"
 
-#: kallithea/controllers/admin/users.py:203
+#: kallithea/controllers/admin/users.py:201
 msgid "The default user cannot be edited"
 msgstr "Користувача за промовчанням не можна редагувати"
 
-#: kallithea/controllers/admin/users.py:409
+#: kallithea/controllers/admin/users.py:407
 #, python-format
 msgid "Added IP address %s to user whitelist"
 msgstr "Додана IP-адреса %s в білий список користувача"
 
-#: kallithea/controllers/admin/users.py:415
+#: kallithea/controllers/admin/users.py:413
 msgid "An error occurred while adding IP address"
 msgstr "Сталася помилка під час додавання IP-адреси"
 
-#: kallithea/controllers/admin/users.py:427
+#: kallithea/controllers/admin/users.py:425
 msgid "Removed IP address from user whitelist"
 msgstr "Вилучено IP-адресу з білого списку користувачів"
 
-#: kallithea/lib/auth.py:684
+#: kallithea/lib/auth.py:634
 msgid "You need to be a registered user to perform this action"
 msgstr "Для виконання цієї дії потрібно бути зареєстрованим користувачем"
 
-#: kallithea/lib/auth.py:712
+#: kallithea/lib/auth.py:662
 msgid "You need to be signed in to view this page"
 msgstr "Ви повинні бути зареєстровані для перегляду цієї сторінки"
 
@@ -1077,170 +1075,170 @@
 "Набір змін був занадто великий і було відрізано, використовуйте меню діфф "
 "для показу цього порівняння"
 
-#: kallithea/lib/diffs.py:224
+#: kallithea/lib/diffs.py:223
 msgid "No changes detected"
 msgstr "Не виявлено змін"
 
-#: kallithea/lib/helpers.py:653
+#: kallithea/lib/helpers.py:670
 #, python-format
 msgid "Deleted branch: %s"
 msgstr "Видалено гілку: %s"
 
-#: kallithea/lib/helpers.py:655
+#: kallithea/lib/helpers.py:672
 #, python-format
 msgid "Created tag: %s"
 msgstr "Створено тег: %s"
 
-#: kallithea/lib/helpers.py:666
+#: kallithea/lib/helpers.py:683
 #, python-format
 msgid "Changeset %s not found"
 msgstr "Набір змін %s не знайдено"
 
-#: kallithea/lib/helpers.py:715
+#: kallithea/lib/helpers.py:732
 #, python-format
 msgid "Show all combined changesets %s->%s"
 msgstr "Показати всі комбіновані набори змін %s- >%s"
 
-#: kallithea/lib/helpers.py:721
+#: kallithea/lib/helpers.py:738
 msgid "Compare view"
 msgstr "Порівняйте вигляд"
 
-#: kallithea/lib/helpers.py:740
+#: kallithea/lib/helpers.py:757
 msgid "and"
 msgstr "і"
 
-#: kallithea/lib/helpers.py:741
+#: kallithea/lib/helpers.py:758
 #, python-format
 msgid "%s more"
 msgstr "%s більше"
 
-#: kallithea/lib/helpers.py:742
+#: kallithea/lib/helpers.py:759
 #: kallithea/templates/changelog/changelog.html:43
 msgid "revisions"
 msgstr "редакції"
 
-#: kallithea/lib/helpers.py:766
+#: kallithea/lib/helpers.py:783
 #, python-format
 msgid "Fork name %s"
 msgstr "Ім'я розгалуження %s"
 
-#: kallithea/lib/helpers.py:787
+#: kallithea/lib/helpers.py:804
 #, python-format
 msgid "Pull request %s"
 msgstr "Pull request %s"
 
-#: kallithea/lib/helpers.py:797
+#: kallithea/lib/helpers.py:814
 msgid "[deleted] repository"
 msgstr "[видалений] репозиторій"
 
-#: kallithea/lib/helpers.py:799 kallithea/lib/helpers.py:811
+#: kallithea/lib/helpers.py:816 kallithea/lib/helpers.py:828
 msgid "[created] repository"
 msgstr "[створено] репозиторій"
 
-#: kallithea/lib/helpers.py:801
+#: kallithea/lib/helpers.py:818
 msgid "[created] repository as fork"
 msgstr "[створено] репозиторій як fork"
 
-#: kallithea/lib/helpers.py:803 kallithea/lib/helpers.py:813
+#: kallithea/lib/helpers.py:820 kallithea/lib/helpers.py:830
 msgid "[forked] repository"
 msgstr "[forked] репозиторій"
 
-#: kallithea/lib/helpers.py:805 kallithea/lib/helpers.py:815
+#: kallithea/lib/helpers.py:822 kallithea/lib/helpers.py:832
 msgid "[updated] repository"
 msgstr "[оновлено] репозиторій"
 
-#: kallithea/lib/helpers.py:807
+#: kallithea/lib/helpers.py:824
 msgid "[downloaded] archive from repository"
 msgstr "[завантажити] архів з репозиторію"
 
-#: kallithea/lib/helpers.py:809
+#: kallithea/lib/helpers.py:826
 msgid "[delete] repository"
 msgstr "[видалити] репозиторій"
 
-#: kallithea/lib/helpers.py:817
+#: kallithea/lib/helpers.py:834
 msgid "[created] user"
 msgstr "[створено] користувач"
 
-#: kallithea/lib/helpers.py:819
+#: kallithea/lib/helpers.py:836
 msgid "[updated] user"
 msgstr "[оновлений] користувач"
 
-#: kallithea/lib/helpers.py:821
+#: kallithea/lib/helpers.py:838
 msgid "[created] user group"
 msgstr "[створено] групу користувачів"
 
-#: kallithea/lib/helpers.py:823
+#: kallithea/lib/helpers.py:840
 msgid "[updated] user group"
 msgstr "[оновлено] група користувачів"
 
-#: kallithea/lib/helpers.py:825
+#: kallithea/lib/helpers.py:842
 msgid "[commented] on revision in repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:827
+#: kallithea/lib/helpers.py:844
 msgid "[commented] on pull request for"
 msgstr ""
 
-#: kallithea/lib/helpers.py:829
+#: kallithea/lib/helpers.py:846
 msgid "[closed] pull request for"
 msgstr ""
 
-#: kallithea/lib/helpers.py:831
+#: kallithea/lib/helpers.py:848
 msgid "[pushed] into"
 msgstr ""
 
-#: kallithea/lib/helpers.py:833
+#: kallithea/lib/helpers.py:850
 msgid "[committed via Kallithea] into repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:835
+#: kallithea/lib/helpers.py:852
 msgid "[pulled from remote] into repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:837
+#: kallithea/lib/helpers.py:854
 msgid "[pulled] from"
 msgstr ""
 
-#: kallithea/lib/helpers.py:839
+#: kallithea/lib/helpers.py:856
 msgid "[started following] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:841
+#: kallithea/lib/helpers.py:858
 msgid "[stopped following] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:961
+#: kallithea/lib/helpers.py:975
 #, python-format
 msgid " and %s more"
 msgstr ""
 
-#: kallithea/lib/helpers.py:965
+#: kallithea/lib/helpers.py:979
 #: kallithea/templates/compare/compare_diff.html:69
 #: kallithea/templates/pullrequests/pullrequest_show.html:297
 msgid "No files"
 msgstr "Файлів немає"
 
-#: kallithea/lib/helpers.py:990
+#: kallithea/lib/helpers.py:1004
 msgid "new file"
 msgstr "новий файл"
 
-#: kallithea/lib/helpers.py:993
+#: kallithea/lib/helpers.py:1007
 msgid "mod"
 msgstr ""
 
-#: kallithea/lib/helpers.py:996
+#: kallithea/lib/helpers.py:1010
 msgid "del"
 msgstr ""
 
-#: kallithea/lib/helpers.py:999
+#: kallithea/lib/helpers.py:1013
 msgid "rename"
 msgstr "перейменувати"
 
-#: kallithea/lib/helpers.py:1004
+#: kallithea/lib/helpers.py:1018
 msgid "chmod"
 msgstr "chmod"
 
-#: kallithea/lib/helpers.py:1297
+#: kallithea/lib/helpers.py:1323
 #, python-format
 msgid ""
 "%s repository is not mapped to db perhaps it was created or renamed from "
@@ -1248,34 +1246,36 @@
 "repositories"
 msgstr ""
 
-#: kallithea/lib/ssh.py:73
+#: kallithea/lib/ssh.py:75
 msgid "SSH key is missing"
 msgstr ""
 
-#: kallithea/lib/ssh.py:77
-msgid "Incorrect SSH key - it must have both a key type and a base64 part"
-msgstr ""
-
-#: kallithea/lib/ssh.py:81
+#: kallithea/lib/ssh.py:79
+msgid ""
+"Incorrect SSH key - it must have both a key type and a base64 part, like "
+"'ssh-rsa ASRNeaZu4FA...xlJp='"
+msgstr ""
+
+#: kallithea/lib/ssh.py:83
 msgid "Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'"
 msgstr ""
 
-#: kallithea/lib/ssh.py:84
+#: kallithea/lib/ssh.py:86
 #, python-format
 msgid "Incorrect SSH key - unexpected characters in base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:89
+#: kallithea/lib/ssh.py:91
 #, python-format
 msgid "Incorrect SSH key - failed to decode base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:92
+#: kallithea/lib/ssh.py:94
 #, python-format
 msgid "Incorrect SSH key - base64 part is not %r as claimed but %r"
 msgstr ""
 
-#: kallithea/lib/utils2.py:334
+#: kallithea/lib/utils2.py:253
 #, python-format
 msgid "%d year"
 msgid_plural "%d years"
@@ -1283,7 +1283,7 @@
 msgstr[1] ""
 msgstr[2] ""
 
-#: kallithea/lib/utils2.py:335
+#: kallithea/lib/utils2.py:254
 #, python-format
 msgid "%d month"
 msgid_plural "%d months"
@@ -1291,7 +1291,7 @@
 msgstr[1] ""
 msgstr[2] ""
 
-#: kallithea/lib/utils2.py:336
+#: kallithea/lib/utils2.py:255
 #, python-format
 msgid "%d day"
 msgid_plural "%d days"
@@ -1299,7 +1299,7 @@
 msgstr[1] "%d днів"
 msgstr[2] "%d дня"
 
-#: kallithea/lib/utils2.py:337
+#: kallithea/lib/utils2.py:256
 #, python-format
 msgid "%d hour"
 msgid_plural "%d hours"
@@ -1307,7 +1307,7 @@
 msgstr[1] "%d годин"
 msgstr[2] "%d години"
 
-#: kallithea/lib/utils2.py:338
+#: kallithea/lib/utils2.py:257
 #, python-format
 msgid "%d minute"
 msgid_plural "%d minutes"
@@ -1315,7 +1315,7 @@
 msgstr[1] "%d хвилин"
 msgstr[2] "%d хвилини"
 
-#: kallithea/lib/utils2.py:339
+#: kallithea/lib/utils2.py:258
 #, python-format
 msgid "%d second"
 msgid_plural "%d seconds"
@@ -1323,27 +1323,27 @@
 msgstr[1] "%d секунд"
 msgstr[2] "%d секунди"
 
-#: kallithea/lib/utils2.py:355
+#: kallithea/lib/utils2.py:274
 #, python-format
 msgid "in %s"
 msgstr "в %s"
 
-#: kallithea/lib/utils2.py:357
+#: kallithea/lib/utils2.py:276
 #, python-format
 msgid "%s ago"
 msgstr "%s тому"
 
-#: kallithea/lib/utils2.py:359
+#: kallithea/lib/utils2.py:278
 #, python-format
 msgid "in %s and %s"
 msgstr "у %s і %s"
 
-#: kallithea/lib/utils2.py:362
+#: kallithea/lib/utils2.py:281
 #, python-format
 msgid "%s and %s ago"
 msgstr "%s і %s тому"
 
-#: kallithea/lib/utils2.py:365
+#: kallithea/lib/utils2.py:284
 msgid "just now"
 msgstr "прямо зараз"
 
@@ -1352,137 +1352,137 @@
 msgid "on line %s"
 msgstr "в рядку %s"
 
-#: kallithea/model/comment.py:221 kallithea/model/pull_request.py:114
+#: kallithea/model/comment.py:219 kallithea/model/pull_request.py:112
 msgid "[Mention]"
 msgstr "[Згадування]"
 
-#: kallithea/model/db.py:1496
+#: kallithea/model/db.py:1411
 msgid "top level"
 msgstr "верхній рівень"
 
-#: kallithea/model/db.py:1637
+#: kallithea/model/db.py:1542
 msgid "Kallithea Administrator"
 msgstr "Kallithea Адміністратор"
 
-#: kallithea/model/db.py:1639
+#: kallithea/model/db.py:1544
 msgid "Default user has no access to new repositories"
 msgstr "Користувач за промовчанням не має доступу до нових репозиторіїв"
 
-#: kallithea/model/db.py:1640
+#: kallithea/model/db.py:1545
 msgid "Default user has read access to new repositories"
 msgstr ""
 "Користувач за замовчанням має доступ на перегляд  нових репозиторіїв"
 
-#: kallithea/model/db.py:1641
+#: kallithea/model/db.py:1546
 msgid "Default user has write access to new repositories"
 msgstr ""
 "Користувач за замовчуванням має доступ до запису до нових репозиторіїв"
 
-#: kallithea/model/db.py:1642
+#: kallithea/model/db.py:1547
 msgid "Default user has admin access to new repositories"
 msgstr ""
 "Користувач за промовчанням має доступ адміністратора до нових репозиторіїв"
 
-#: kallithea/model/db.py:1644
+#: kallithea/model/db.py:1549
 msgid "Default user has no access to new repository groups"
 msgstr ""
 "Користувач за замовчуванням не має доступу до нових груп репозиторіїв"
 
-#: kallithea/model/db.py:1645
+#: kallithea/model/db.py:1550
 msgid "Default user has read access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1646
+#: kallithea/model/db.py:1551
 msgid "Default user has write access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1647
+#: kallithea/model/db.py:1552
 msgid "Default user has admin access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1649
+#: kallithea/model/db.py:1554
 msgid "Default user has no access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1650
+#: kallithea/model/db.py:1555
 msgid "Default user has read access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1651
+#: kallithea/model/db.py:1556
 msgid "Default user has write access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1652
+#: kallithea/model/db.py:1557
 msgid "Default user has admin access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1654
+#: kallithea/model/db.py:1559
 msgid "Only admins can create repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1655
+#: kallithea/model/db.py:1560
 msgid "Non-admins can create repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1657
+#: kallithea/model/db.py:1562
 msgid "Only admins can create user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1658
+#: kallithea/model/db.py:1563
 msgid "Non-admins can create user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1660
+#: kallithea/model/db.py:1565
 msgid "Only admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1661
+#: kallithea/model/db.py:1566
 msgid "Non-admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1663
+#: kallithea/model/db.py:1568
 msgid ""
 "Repository creation enabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1664
+#: kallithea/model/db.py:1569
 msgid ""
 "Repository creation disabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1666
+#: kallithea/model/db.py:1571
 msgid "Only admins can fork repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1667
+#: kallithea/model/db.py:1572
 msgid "Non-admins can fork repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1669
+#: kallithea/model/db.py:1574
 msgid "Registration disabled"
 msgstr ""
 
-#: kallithea/model/db.py:1670
+#: kallithea/model/db.py:1575
 msgid "User registration with manual account activation"
 msgstr ""
 
-#: kallithea/model/db.py:1671
+#: kallithea/model/db.py:1576
 msgid "User registration with automatic account activation"
 msgstr ""
 
-#: kallithea/model/db.py:2206
+#: kallithea/model/db.py:1992
 msgid "Not reviewed"
 msgstr ""
 
-#: kallithea/model/db.py:2207
+#: kallithea/model/db.py:1993
 msgid "Under review"
 msgstr ""
 
-#: kallithea/model/db.py:2208
+#: kallithea/model/db.py:1994
 msgid "Not approved"
 msgstr ""
 
-#: kallithea/model/db.py:2209
+#: kallithea/model/db.py:1995
 msgid "Approved"
 msgstr ""
 
@@ -1508,313 +1508,313 @@
 msgid "Name must not contain only digits"
 msgstr ""
 
-#: kallithea/model/notification.py:164
+#: kallithea/model/notification.py:162
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s changeset %(short_id)s \"%(message_short)s\" on "
 "%(branch)s"
 msgstr ""
 
-#: kallithea/model/notification.py:167
+#: kallithea/model/notification.py:165
 #, python-format
 msgid "New user %(new_username)s registered"
 msgstr ""
 
-#: kallithea/model/notification.py:169
+#: kallithea/model/notification.py:167
 #, python-format
 msgid ""
 "[Review] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:170
+#: kallithea/model/notification.py:168
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:183
+#: kallithea/model/notification.py:188
 msgid "Closing"
 msgstr ""
 
-#: kallithea/model/pull_request.py:73
+#: kallithea/model/pull_request.py:72
 #, python-format
 msgid ""
 "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:209
+#: kallithea/model/pull_request.py:207
 msgid "Cannot create empty pull request"
 msgstr ""
 
-#: kallithea/model/pull_request.py:217
+#: kallithea/model/pull_request.py:215
 #, python-format
 msgid ""
 "Cannot create pull request - criss cross merge detected, please merge a "
 "later %s revision to %s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:245 kallithea/model/pull_request.py:380
+#: kallithea/model/pull_request.py:243 kallithea/model/pull_request.py:378
 msgid "You are not authorized to create the pull request"
 msgstr ""
 
-#: kallithea/model/pull_request.py:339
+#: kallithea/model/pull_request.py:337
 msgid "Missing changesets since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:346
+#: kallithea/model/pull_request.py:344
 #, python-format
 msgid "New changesets on %s %s since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:353
+#: kallithea/model/pull_request.py:351
 msgid "Ancestor didn't change - diff since previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:360
+#: kallithea/model/pull_request.py:358
 #, python-format
 msgid ""
 "This iteration is based on another %s revision and there is no simple "
 "diff."
 msgstr ""
 
-#: kallithea/model/pull_request.py:362
+#: kallithea/model/pull_request.py:360
 #, python-format
 msgid "No changes found on %s %s since previous iteration."
 msgstr ""
 
-#: kallithea/model/pull_request.py:388
+#: kallithea/model/pull_request.py:386
 #, python-format
 msgid "Closed, next iteration: %s ."
 msgstr ""
 
-#: kallithea/model/scm.py:668
+#: kallithea/model/scm.py:655
 msgid "latest tip"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:56
+#: kallithea/model/ssh_key.py:57
 #, python-format
 msgid "SSH key %r is invalid: %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:68
+#: kallithea/model/ssh_key.py:69
 #, python-format
 msgid "SSH key %s is already used by %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:89
-#, python-format
-msgid "SSH key %r not found"
-msgstr ""
-
-#: kallithea/model/user.py:186
+#: kallithea/model/ssh_key.py:88
+#, python-format
+msgid "SSH key with fingerprint %r found"
+msgstr ""
+
+#: kallithea/model/user.py:180
 msgid "New user registration"
 msgstr ""
 
-#: kallithea/model/user.py:250
+#: kallithea/model/user.py:244
 msgid ""
 "You can't remove this user since it is crucial for the entire application"
 msgstr ""
 
-#: kallithea/model/user.py:255
+#: kallithea/model/user.py:249
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repositories and cannot be removed. Switch "
 "owners or remove those repositories: %s"
 msgstr ""
 
-#: kallithea/model/user.py:260
+#: kallithea/model/user.py:254
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repository groups and cannot be removed. Switch "
 "owners or remove those repository groups: %s"
 msgstr ""
 
-#: kallithea/model/user.py:267
+#: kallithea/model/user.py:261
 #, python-format
 msgid ""
 "User \"%s\" still owns %s user groups and cannot be removed. Switch "
 "owners or remove those user groups: %s"
 msgstr ""
 
-#: kallithea/model/user.py:361
+#: kallithea/model/user.py:355
 msgid "Password reset link"
 msgstr ""
 
-#: kallithea/model/user.py:408
+#: kallithea/model/user.py:402
 msgid "Password reset notification"
 msgstr ""
 
-#: kallithea/model/user.py:409
+#: kallithea/model/user.py:403
 #, python-format
 msgid ""
 "The password to your account %s has been changed using password reset "
 "form."
 msgstr ""
 
-#: kallithea/model/validators.py:52 kallithea/model/validators.py:53
+#: kallithea/model/validators.py:53 kallithea/model/validators.py:54
 msgid "Value cannot be an empty list"
 msgstr ""
 
-#: kallithea/model/validators.py:72
+#: kallithea/model/validators.py:73
 #, python-format
 msgid "Username \"%(username)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:74
+#: kallithea/model/validators.py:75
 #, python-format
 msgid "Username \"%(username)s\" cannot be used"
 msgstr ""
 
-#: kallithea/model/validators.py:76
+#: kallithea/model/validators.py:77
 msgid ""
 "Username may only contain alphanumeric characters underscores, periods or "
 "dashes and must begin with an alphanumeric character or underscore"
 msgstr ""
 
-#: kallithea/model/validators.py:103
+#: kallithea/model/validators.py:104
 msgid "The input is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:110
+#: kallithea/model/validators.py:111
 #, python-format
 msgid "Username %(username)s is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:131
-msgid "Invalid user group name"
-msgstr ""
-
 #: kallithea/model/validators.py:132
+msgid "Invalid user group name"
+msgstr ""
+
+#: kallithea/model/validators.py:133
 #, python-format
 msgid "User group \"%(usergroup)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:134
+#: kallithea/model/validators.py:135
 msgid ""
 "user group name may only contain alphanumeric characters underscores, "
 "periods or dashes and must begin with alphanumeric character"
 msgstr ""
 
-#: kallithea/model/validators.py:174
-msgid "Cannot assign this group as parent"
-msgstr ""
-
 #: kallithea/model/validators.py:175
+msgid "Cannot assign this group as parent"
+msgstr ""
+
+#: kallithea/model/validators.py:176
 #, python-format
 msgid "Group \"%(group_name)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:177
+#: kallithea/model/validators.py:178
 #, python-format
 msgid "Repository with name \"%(group_name)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:233
+#: kallithea/model/validators.py:230
 msgid "Invalid characters (non-ascii) in password"
 msgstr ""
 
-#: kallithea/model/validators.py:248
+#: kallithea/model/validators.py:245
 msgid "Invalid old password"
 msgstr ""
 
-#: kallithea/model/validators.py:264
+#: kallithea/model/validators.py:261
 msgid "Passwords do not match"
 msgstr ""
 
-#: kallithea/model/validators.py:279
+#: kallithea/model/validators.py:276
 msgid "Invalid username or password"
 msgstr ""
 
+#: kallithea/model/validators.py:310
+#, python-format
+msgid "Repository name %(repo)s is not allowed"
+msgstr ""
+
+#: kallithea/model/validators.py:312
+#, python-format
+msgid "Repository named %(repo)s already exists"
+msgstr ""
+
 #: kallithea/model/validators.py:313
 #, python-format
-msgid "Repository name %(repo)s is not allowed"
+msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
 msgstr ""
 
 #: kallithea/model/validators.py:315
 #, python-format
-msgid "Repository named %(repo)s already exists"
-msgstr ""
-
-#: kallithea/model/validators.py:316
-#, python-format
-msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
-msgstr ""
-
-#: kallithea/model/validators.py:318
-#, python-format
 msgid "Repository group with name \"%(repo)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:404
+#: kallithea/model/validators.py:401
 msgid "Invalid repository URL"
 msgstr ""
 
-#: kallithea/model/validators.py:405
+#: kallithea/model/validators.py:402
 msgid ""
 "Invalid repository URL. It must be a valid http, https, ssh, svn+http or "
 "svn+https URL"
 msgstr ""
 
-#: kallithea/model/validators.py:430
+#: kallithea/model/validators.py:427
 msgid "Fork has to be the same type as parent"
 msgstr ""
 
-#: kallithea/model/validators.py:445
+#: kallithea/model/validators.py:442
 msgid "You don't have permissions to create repository in this group"
 msgstr ""
 
-#: kallithea/model/validators.py:447
+#: kallithea/model/validators.py:444
 msgid "no permission to create repository in root location"
 msgstr ""
 
-#: kallithea/model/validators.py:497
+#: kallithea/model/validators.py:494
 msgid "You don't have permissions to create a group in this location"
 msgstr ""
 
-#: kallithea/model/validators.py:537
+#: kallithea/model/validators.py:534
 msgid "This username or user group name is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:630
+#: kallithea/model/validators.py:627
 msgid "This is not a valid path"
 msgstr ""
 
-#: kallithea/model/validators.py:647
+#: kallithea/model/validators.py:644
 msgid "This email address is already in use"
 msgstr ""
 
-#: kallithea/model/validators.py:667
+#: kallithea/model/validators.py:664
 #, python-format
 msgid "Email address \"%(email)s\" not found"
 msgstr ""
 
-#: kallithea/model/validators.py:704
+#: kallithea/model/validators.py:701
 msgid ""
 "The LDAP Login attribute of the CN must be specified - this is the name "
 "of the attribute that is equivalent to \"username\""
 msgstr ""
 
-#: kallithea/model/validators.py:716
+#: kallithea/model/validators.py:713
 msgid "Please enter a valid IPv4 or IPv6 address"
 msgstr ""
 
-#: kallithea/model/validators.py:717
+#: kallithea/model/validators.py:714
 #, python-format
 msgid ""
 "The network size (bits) must be within the range of 0-32 (not %(bits)r)"
 msgstr ""
 
-#: kallithea/model/validators.py:750
+#: kallithea/model/validators.py:747
 msgid "Key name can only consist of letters, underscore, dash or numbers"
 msgstr ""
 
-#: kallithea/model/validators.py:764
+#: kallithea/model/validators.py:761
 msgid "Filename cannot be inside a directory"
 msgstr ""
 
-#: kallithea/model/validators.py:780
+#: kallithea/model/validators.py:777
 #, python-format
 msgid "Plugins %(loaded)s and %(next_to_load)s both export the same name"
 msgstr ""
@@ -1874,7 +1874,7 @@
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:60
 #: kallithea/templates/email_templates/pull_request.html:37
 #: kallithea/templates/forks/fork.html:34
-#: kallithea/templates/index_base.html:58
+#: kallithea/templates/index_base.html:59
 #: kallithea/templates/pullrequests/pullrequest.html:33
 #: kallithea/templates/pullrequests/pullrequest_show.html:38
 #: kallithea/templates/pullrequests/pullrequest_show.html:59
@@ -1882,14 +1882,14 @@
 msgid "Description"
 msgstr ""
 
-#: kallithea/templates/index_base.html:60
+#: kallithea/templates/index_base.html:61
 msgid "Last Change"
 msgstr ""
 
 #: kallithea/templates/admin/my_account/my_account_repos.html:15
 #: kallithea/templates/admin/my_account/my_account_watched.html:15
 #: kallithea/templates/admin/repos/repos.html:41
-#: kallithea/templates/index_base.html:62
+#: kallithea/templates/index_base.html:63
 msgid "Tip"
 msgstr ""
 
@@ -1899,7 +1899,7 @@
 #: kallithea/templates/admin/repos/repos.html:42
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:8
 #: kallithea/templates/admin/user_groups/user_groups.html:42
-#: kallithea/templates/index_base.html:63
+#: kallithea/templates/index_base.html:64
 #: kallithea/templates/pullrequests/pullrequest_data.html:16
 #: kallithea/templates/pullrequests/pullrequest_show.html:124
 #: kallithea/templates/pullrequests/pullrequest_show.html:219
@@ -1923,7 +1923,7 @@
 #: kallithea/templates/admin/users/user_edit_profile.html:18
 #: kallithea/templates/admin/users/users.html:37
 #: kallithea/templates/base/base.html:364
-#: kallithea/templates/email_templates/registration.html:11
+#: kallithea/templates/email_templates/registration.html:12
 #: kallithea/templates/login.html:28 kallithea/templates/register.html:31
 msgid "Username"
 msgstr ""
@@ -2047,7 +2047,7 @@
 #: kallithea/templates/admin/settings/settings.html:31
 #: kallithea/templates/admin/users/user_add.html:62
 #: kallithea/templates/admin/users/user_edit_profile.html:25
-#: kallithea/templates/email_templates/registration.html:33
+#: kallithea/templates/email_templates/registration.html:34
 #: kallithea/templates/register.html:66
 msgid "Email"
 msgstr ""
@@ -2295,7 +2295,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/gists/index.html:51
-#: kallithea/templates/data_table/_dt_elements.html:78
+#: kallithea/templates/data_table/_dt_elements.html:84
 msgid "Created"
 msgstr ""
 
@@ -2379,13 +2379,13 @@
 #: kallithea/templates/admin/users/user_edit_ips.html:21
 #: kallithea/templates/changeset/changeset_file_comment.html:30
 #: kallithea/templates/changeset/changeset_file_comment.html:121
-#: kallithea/templates/data_table/_dt_elements.html:69
-#: kallithea/templates/data_table/_dt_elements.html:89
-#: kallithea/templates/data_table/_dt_elements.html:91
-#: kallithea/templates/data_table/_dt_elements.html:101
-#: kallithea/templates/data_table/_dt_elements.html:103
-#: kallithea/templates/data_table/_dt_elements.html:120
-#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:75
+#: kallithea/templates/data_table/_dt_elements.html:95
+#: kallithea/templates/data_table/_dt_elements.html:97
+#: kallithea/templates/data_table/_dt_elements.html:107
+#: kallithea/templates/data_table/_dt_elements.html:109
+#: kallithea/templates/data_table/_dt_elements.html:126
+#: kallithea/templates/data_table/_dt_elements.html:128
 #: kallithea/templates/files/files_source.html:35
 #: kallithea/templates/files/files_source.html:38
 #: kallithea/templates/files/files_source.html:41
@@ -2401,14 +2401,14 @@
 #: kallithea/templates/base/perms_summary.html:44
 #: kallithea/templates/base/perms_summary.html:81
 #: kallithea/templates/base/perms_summary.html:83
-#: kallithea/templates/data_table/_dt_elements.html:63
-#: kallithea/templates/data_table/_dt_elements.html:64
-#: kallithea/templates/data_table/_dt_elements.html:85
-#: kallithea/templates/data_table/_dt_elements.html:86
-#: kallithea/templates/data_table/_dt_elements.html:97
-#: kallithea/templates/data_table/_dt_elements.html:98
-#: kallithea/templates/data_table/_dt_elements.html:116
-#: kallithea/templates/data_table/_dt_elements.html:117
+#: kallithea/templates/data_table/_dt_elements.html:69
+#: kallithea/templates/data_table/_dt_elements.html:70
+#: kallithea/templates/data_table/_dt_elements.html:91
+#: kallithea/templates/data_table/_dt_elements.html:92
+#: kallithea/templates/data_table/_dt_elements.html:103
+#: kallithea/templates/data_table/_dt_elements.html:104
+#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:123
 #: kallithea/templates/files/diff_2way.html:56
 #: kallithea/templates/files/files_source.html:37
 #: kallithea/templates/files/files_source.html:40
@@ -2709,7 +2709,7 @@
 #: kallithea/templates/admin/permissions/permissions_globals.html:27
 #: kallithea/templates/admin/repos/repo_add_base.html:28
 #: kallithea/templates/admin/repos/repo_edit_settings.html:33
-#: kallithea/templates/data_table/_dt_elements.html:134
+#: kallithea/templates/data_table/_dt_elements.html:140
 #: kallithea/templates/forks/fork.html:42
 msgid "Repository group"
 msgstr "Група репозиторіїв"
@@ -2730,7 +2730,7 @@
 msgstr "Дозволи для користувача за промовчанням на нові групи репозиторіів."
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:40
-#: kallithea/templates/data_table/_dt_elements.html:141
+#: kallithea/templates/data_table/_dt_elements.html:147
 msgid "User group"
 msgstr "Група користувачів"
 
@@ -2908,7 +2908,7 @@
 msgstr "Створено о"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:121
+#: kallithea/templates/data_table/_dt_elements.html:127
 #, python-format
 msgid "Confirm to delete this group: %s with %s repository"
 msgid_plural "Confirm to delete this group: %s with %s repositories"
@@ -3080,14 +3080,10 @@
 msgstr "Додаткові поля"
 
 #: kallithea/templates/admin/repos/repo_edit.html:37
-msgid "Caches"
-msgstr "Кеши"
+msgid "Remote"
+msgstr "Віддалений"
 
 #: kallithea/templates/admin/repos/repo_edit.html:40
-msgid "Remote"
-msgstr "Віддалений"
-
-#: kallithea/templates/admin/repos/repo_edit.html:43
 #: kallithea/templates/summary/statistics.html:8
 #: kallithea/templates/summary/summary.html:169
 #: kallithea/templates/summary/summary.html:170
@@ -3127,7 +3123,7 @@
 "публічному журналі."
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:46
-#: kallithea/templates/data_table/_dt_elements.html:68
+#: kallithea/templates/data_table/_dt_elements.html:74
 #, python-format
 msgid "Confirm to delete this repository: %s"
 msgstr "Підтвердити видалення цього сховища: %s"
@@ -3159,45 +3155,14 @@
 "it or restore it."
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:4
-msgid "Invalidate Repository Cache"
-msgstr "Скинути Кеш Репозиторію"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:6
-msgid ""
-"Manually invalidate cache for this repository. On first access, the "
-"repository will be cached again."
-msgstr ""
-"Вручну скинути кеш для цього репозиторію. При першому доступі, сховище "
-"буде знову кешовано."
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:9
-msgid "List of Cached Values"
-msgstr "Список кешованих значень"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:12
-msgid "Prefix"
-msgstr "Префікс"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:13
+#: kallithea/templates/admin/repos/repo_edit_fields.html:6
+msgid "Label"
+msgstr "Мітка"
+
 #: kallithea/templates/admin/repos/repo_edit_fields.html:7
 msgid "Key"
 msgstr "Ключ"
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:14
-#: kallithea/templates/admin/user_groups/user_group_add.html:40
-#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
-#: kallithea/templates/admin/user_groups/user_groups.html:41
-#: kallithea/templates/admin/users/user_add.html:69
-#: kallithea/templates/admin/users/user_edit_profile.html:74
-#: kallithea/templates/admin/users/users.html:42
-msgid "Active"
-msgstr "Активний"
-
-#: kallithea/templates/admin/repos/repo_edit_fields.html:6
-msgid "Label"
-msgstr "Мітка"
-
 #: kallithea/templates/admin/repos/repo_edit_fields.html:20
 #, python-format
 msgid "Confirm to delete this field: %s"
@@ -3736,6 +3701,15 @@
 msgid "Short, optional description for this user group."
 msgstr ""
 
+#: kallithea/templates/admin/user_groups/user_group_add.html:40
+#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
+#: kallithea/templates/admin/user_groups/user_groups.html:41
+#: kallithea/templates/admin/users/user_add.html:69
+#: kallithea/templates/admin/users/user_edit_profile.html:74
+#: kallithea/templates/admin/users/users.html:42
+msgid "Active"
+msgstr "Активний"
+
 #: kallithea/templates/admin/user_groups/user_group_edit.html:5
 #, python-format
 msgid "%s user group settings"
@@ -3757,7 +3731,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:19
-#: kallithea/templates/data_table/_dt_elements.html:102
+#: kallithea/templates/data_table/_dt_elements.html:108
 #, python-format
 msgid "Confirm to delete this user group: %s"
 msgstr ""
@@ -3831,7 +3805,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:90
+#: kallithea/templates/data_table/_dt_elements.html:96
 #, python-format
 msgid "Confirm to delete this user: %s"
 msgstr ""
@@ -3931,10 +3905,12 @@
 msgstr "Пошук"
 
 #: kallithea/templates/base/base.html:167
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Follow"
 msgstr "Слідувати"
 
 #: kallithea/templates/base/base.html:168
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Unfollow"
 msgstr "Не слідкувати"
 
@@ -4339,23 +4315,23 @@
 msgid "Merge"
 msgstr "Злити"
 
-#: kallithea/templates/changeset/changeset.html:96
+#: kallithea/templates/changeset/changeset.html:95
 msgid "Grafted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:102
+#: kallithea/templates/changeset/changeset.html:100
 msgid "Transplanted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:108
+#: kallithea/templates/changeset/changeset.html:106
 msgid "Replaced by:"
 msgstr "Замінено на:"
 
-#: kallithea/templates/changeset/changeset.html:122
+#: kallithea/templates/changeset/changeset.html:120
 msgid "Preceded by:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:139
+#: kallithea/templates/changeset/changeset.html:137
 #: kallithea/templates/compare/compare_diff.html:59
 #: kallithea/templates/pullrequests/pullrequest_show.html:290
 #, python-format
@@ -4365,7 +4341,7 @@
 msgstr[1] ""
 msgstr[2] ""
 
-#: kallithea/templates/changeset/changeset.html:141
+#: kallithea/templates/changeset/changeset.html:139
 #: kallithea/templates/compare/compare_diff.html:61
 #: kallithea/templates/pullrequests/pullrequest_show.html:292
 #, python-format
@@ -4375,8 +4351,8 @@
 msgstr[1] ""
 msgstr[2] ""
 
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
 #: kallithea/templates/pullrequests/pullrequest_show.html:333
@@ -4610,23 +4586,23 @@
 msgid "Repository creation in progress..."
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:42
+#: kallithea/templates/data_table/_dt_elements.html:48
 msgid "No changesets yet"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:48
-#: kallithea/templates/data_table/_dt_elements.html:50
+#: kallithea/templates/data_table/_dt_elements.html:54
+#: kallithea/templates/data_table/_dt_elements.html:56
 #, python-format
 msgid "Subscribe to %s rss feed"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:56
-#: kallithea/templates/data_table/_dt_elements.html:58
+#: kallithea/templates/data_table/_dt_elements.html:62
+#: kallithea/templates/data_table/_dt_elements.html:64
 #, python-format
 msgid "Subscribe to %s atom feed"
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:76
+#: kallithea/templates/data_table/_dt_elements.html:82
 msgid "Creating"
 msgstr ""
 
@@ -4658,6 +4634,13 @@
 msgid "by"
 msgstr ""
 
+#: kallithea/templates/email_templates/changeset_comment.html:36
+#: kallithea/templates/email_templates/pull_request_comment.html:43
+#, fuzzy
+#| msgid "comment"
+msgid "View Comment"
+msgstr "коментар"
+
 #: kallithea/templates/email_templates/comment.html:27
 msgid "Status change:"
 msgstr "Зміна статусу:"
@@ -4666,32 +4649,40 @@
 msgid "The pull request has been closed."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:9
+#: kallithea/templates/email_templates/default.html:4
+msgid "Message"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:4
+msgid "Password Reset Request"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:10
 #, python-format
 msgid "Hello %s"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:16
+#: kallithea/templates/email_templates/password_reset.html:17
 msgid "We have received a request to reset the password for your account."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:25
+#: kallithea/templates/email_templates/password_reset.html:26
 msgid ""
 "This account is however managed outside this system and the password "
 "cannot be changed here."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:28
+#: kallithea/templates/email_templates/password_reset.html:29
 msgid "To set a new password, click the following link"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:33
+#: kallithea/templates/email_templates/password_reset.html:34
 msgid ""
 "Should you not be able to use the link above, please type the following "
 "code into the password reset form"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:44
+#: kallithea/templates/email_templates/password_reset.html:45
 msgid ""
 "If it weren't you who requested the password reset, just disregard this "
 "message."
@@ -4722,6 +4713,12 @@
 msgid "to"
 msgstr ""
 
+#: kallithea/templates/email_templates/pull_request.html:85
+#, fuzzy
+#| msgid "Pull Requests"
+msgid "View Pull Request"
+msgstr "Запити Pull Requests"
+
 #: kallithea/templates/email_templates/pull_request_comment.html:4
 #, python-format
 msgid "Mention in Comment on Pull Request %s \"%s\""
@@ -4737,10 +4734,20 @@
 msgid "Comment on Pull Request %s \"%s\""
 msgstr ""
 
-#: kallithea/templates/email_templates/registration.html:22
+#: kallithea/templates/email_templates/registration.html:5
+#, fuzzy
+#| msgid "Registration"
+msgid "New User Registration"
+msgstr "Реєстрація"
+
+#: kallithea/templates/email_templates/registration.html:23
 msgid "Full Name"
 msgstr ""
 
+#: kallithea/templates/email_templates/registration.html:42
+msgid "View User Profile"
+msgstr ""
+
 #: kallithea/templates/files/diff_2way.html:15
 #, python-format
 msgid "%s File side-by-side diff"
@@ -5342,45 +5349,45 @@
 msgid "Stats gathered: "
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:87
-#: kallithea/templates/summary/summary.html:371
+#: kallithea/templates/summary/statistics.html:85
+#: kallithea/templates/summary/summary.html:369
 msgid "files"
 msgstr "файли"
 
-#: kallithea/templates/summary/statistics.html:111
-#: kallithea/templates/summary/summary.html:401
+#: kallithea/templates/summary/statistics.html:109
+#: kallithea/templates/summary/summary.html:399
 msgid "Show more"
 msgstr "Показати більше"
 
-#: kallithea/templates/summary/statistics.html:405
+#: kallithea/templates/summary/statistics.html:395
 msgid "commits"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:406
+#: kallithea/templates/summary/statistics.html:396
 msgid "files added"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:407
+#: kallithea/templates/summary/statistics.html:397
 msgid "files changed"
 msgstr "файли змінено"
 
-#: kallithea/templates/summary/statistics.html:408
+#: kallithea/templates/summary/statistics.html:398
 msgid "files removed"
 msgstr "вилучені файли"
 
-#: kallithea/templates/summary/statistics.html:410
+#: kallithea/templates/summary/statistics.html:400
 msgid "commit"
 msgstr "Фіксація"
 
-#: kallithea/templates/summary/statistics.html:411
+#: kallithea/templates/summary/statistics.html:401
 msgid "file added"
 msgstr "файл додано"
 
-#: kallithea/templates/summary/statistics.html:412
+#: kallithea/templates/summary/statistics.html:402
 msgid "file changed"
 msgstr "файл змінено"
 
-#: kallithea/templates/summary/statistics.html:413
+#: kallithea/templates/summary/statistics.html:403
 msgid "file removed"
 msgstr "файл видалено"
 
@@ -5480,3 +5487,28 @@
 #, python-format
 msgid "Download %s as %s"
 msgstr ""
+
+#~ msgid "Cache invalidation successful"
+#~ msgstr "Інвалідація кешу успішна"
+
+#~ msgid "An error occurred during cache invalidation"
+#~ msgstr "Сталася помилка під час Анулювання кеша"
+
+#~ msgid "Caches"
+#~ msgstr "Кеши"
+
+#~ msgid "Invalidate Repository Cache"
+#~ msgstr "Скинути Кеш Репозиторію"
+
+#~ msgid ""
+#~ "Manually invalidate cache for this repository. On first access, the "
+#~ "repository will be cached again."
+#~ msgstr ""
+#~ "Вручну скинути кеш для цього репозиторію. При першому доступі, сховище "
+#~ "буде знову кешовано."
+
+#~ msgid "List of Cached Values"
+#~ msgstr "Список кешованих значень"
+
+#~ msgid "Prefix"
+#~ msgstr "Префікс"
--- a/kallithea/i18n/zh_CN/LC_MESSAGES/kallithea.po	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/i18n/zh_CN/LC_MESSAGES/kallithea.po	Mon May 04 19:24:04 2020 +0200
@@ -4,7 +4,7 @@
 msgstr ""
 "Project-Id-Version: Kallithea 0.3\n"
 "Report-Msgid-Bugs-To: translations@kallithea-scm.org\n"
-"POT-Creation-Date: 2020-01-05 04:10+0100\n"
+"POT-Creation-Date: 2020-05-04 19:12+0200\n"
 "PO-Revision-Date: 2019-08-14 19:00+0000\n"
 "Last-Translator: Elizabeth Sherrock <lizzyd710@gmail.com>\n"
 "Language-Team: Chinese (Simplified) <https://hosted.weblate.org/projects/"
@@ -18,14 +18,14 @@
 "Generated-By: Babel 1.3\n"
 
 #: kallithea/controllers/changelog.py:67
-#: kallithea/controllers/pullrequests.py:250 kallithea/lib/base.py:602
+#: kallithea/controllers/pullrequests.py:247 kallithea/lib/base.py:602
 msgid "There are no changesets yet"
 msgstr "还没有修订集"
 
 #: kallithea/controllers/admin/permissions.py:64
 #: kallithea/controllers/admin/permissions.py:68
 #: kallithea/controllers/admin/permissions.py:72
-#: kallithea/controllers/changelog.py:137
+#: kallithea/controllers/changelog.py:136
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:7
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:88
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:7
@@ -34,38 +34,38 @@
 msgid "None"
 msgstr "无"
 
-#: kallithea/controllers/changelog.py:140 kallithea/controllers/files.py:189
+#: kallithea/controllers/changelog.py:139 kallithea/controllers/files.py:189
 msgid "(closed)"
 msgstr "(已关闭)"
 
-#: kallithea/controllers/changeset.py:81
+#: kallithea/controllers/changeset.py:82
 msgid "Show whitespace"
 msgstr "显示空白"
 
-#: kallithea/controllers/changeset.py:88
-#: kallithea/controllers/changeset.py:95
+#: kallithea/controllers/changeset.py:89
+#: kallithea/controllers/changeset.py:96
 #: kallithea/templates/files/diff_2way.html:55
 msgid "Ignore whitespace"
 msgstr "忽略空白"
 
-#: kallithea/controllers/changeset.py:161
+#: kallithea/controllers/changeset.py:162
 #, python-format
 msgid "Increase diff context to %(num)s lines"
 msgstr "增加差异上下文到 %(num)s 行"
 
-#: kallithea/controllers/changeset.py:201
+#: kallithea/controllers/changeset.py:202
 #, fuzzy
 #| msgid "Set changeset status"
 msgid "No permission to change status"
 msgstr "设置修订集状态"
 
-#: kallithea/controllers/changeset.py:212
+#: kallithea/controllers/changeset.py:213
 #, fuzzy, python-format
 msgid "Successfully deleted pull request %s"
 msgstr "成功删除拉取请求"
 
 #: kallithea/controllers/changeset.py:319 kallithea/controllers/files.py:89
-#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:700
+#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:697
 msgid "Such revision does not exist for this repository"
 msgstr "在此代码库内,此修改并不存在"
 
@@ -78,61 +78,61 @@
 msgid "Cannot compare repositories of different types"
 msgstr ""
 
-#: kallithea/controllers/compare.py:246
+#: kallithea/controllers/compare.py:247
 msgid "Cannot show empty diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:248
+#: kallithea/controllers/compare.py:249
 msgid "No ancestor found for merge diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:252
+#: kallithea/controllers/compare.py:253
 msgid "Multiple merge ancestors found for merge compare"
 msgstr ""
 
-#: kallithea/controllers/compare.py:268
+#: kallithea/controllers/compare.py:269
 msgid "Cannot compare repositories without using common ancestor"
 msgstr ""
 
-#: kallithea/controllers/error.py:71
+#: kallithea/controllers/error.py:67
 msgid "No response"
 msgstr "无响应"
 
-#: kallithea/controllers/error.py:72
+#: kallithea/controllers/error.py:68
 msgid "Unknown error"
 msgstr "未知错误"
 
-#: kallithea/controllers/error.py:85
+#: kallithea/controllers/error.py:81
 msgid ""
 "The request could not be understood by the server due to malformed syntax."
 msgstr "由于错误的语法,服务器无法对请求进行响应。"
 
-#: kallithea/controllers/error.py:88
+#: kallithea/controllers/error.py:84
 msgid "Unauthorized access to resource"
 msgstr "未授权的资源访问"
 
-#: kallithea/controllers/error.py:90
+#: kallithea/controllers/error.py:86
 msgid "You don't have permission to view this page"
 msgstr "无权访问该页面"
 
-#: kallithea/controllers/error.py:92
+#: kallithea/controllers/error.py:88
 msgid "The resource could not be found"
 msgstr "资源未找到"
 
-#: kallithea/controllers/error.py:94
+#: kallithea/controllers/error.py:90
 msgid ""
 "The server encountered an unexpected condition which prevented it from "
 "fulfilling the request."
 msgstr "服务进入非预期的混乱状态,这会阻止它对请求进行响应。"
 
-#: kallithea/controllers/feed.py:63
+#: kallithea/controllers/feed.py:59
 #, python-format
 msgid "%s committed on %s"
 msgstr ""
 
-#: kallithea/controllers/feed.py:88
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/controllers/feed.py:84
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/compare/compare_diff.html:95
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
@@ -140,12 +140,12 @@
 msgid "Changeset was too big and was cut off..."
 msgstr "修订集太大并已被截断..."
 
-#: kallithea/controllers/feed.py:111 kallithea/controllers/feed.py:140
+#: kallithea/controllers/feed.py:107
 #, python-format
 msgid "%s %s feed"
 msgstr "%s %s订阅"
 
-#: kallithea/controllers/feed.py:113 kallithea/controllers/feed.py:142
+#: kallithea/controllers/feed.py:109
 #, python-format
 msgid "Changes on %s repository"
 msgstr "%s库的修改"
@@ -165,103 +165,103 @@
 msgid "%s at %s"
 msgstr "%s 在 %s"
 
-#: kallithea/controllers/files.py:296
+#: kallithea/controllers/files.py:295
 msgid "You can only delete files with revision being a valid branch"
 msgstr "您只能删除有效分支的修订中的文件"
 
-#: kallithea/controllers/files.py:307
+#: kallithea/controllers/files.py:306
 #, python-format
 msgid "Deleted file %s via Kallithea"
 msgstr "删除文件 %s 通过 Kallithea"
 
-#: kallithea/controllers/files.py:331
+#: kallithea/controllers/files.py:330
 #, python-format
 msgid "Successfully deleted file %s"
 msgstr "成功删除文件 %s"
 
-#: kallithea/controllers/files.py:335 kallithea/controllers/files.py:394
-#: kallithea/controllers/files.py:469
+#: kallithea/controllers/files.py:334 kallithea/controllers/files.py:392
+#: kallithea/controllers/files.py:467
 msgid "Error occurred during commit"
 msgstr "提交时发生错误"
 
-#: kallithea/controllers/files.py:350
+#: kallithea/controllers/files.py:349
 msgid "You can only edit files with revision being a valid branch"
 msgstr "您只能编辑有效分支的修订中的文件"
 
-#: kallithea/controllers/files.py:364
+#: kallithea/controllers/files.py:363
 #, python-format
 msgid "Edited file %s via Kallithea"
 msgstr "已编辑文件 %s 通过 Kallithea"
 
-#: kallithea/controllers/files.py:380
+#: kallithea/controllers/files.py:378
 msgid "No changes"
 msgstr "无变更"
 
-#: kallithea/controllers/files.py:390 kallithea/controllers/files.py:458
+#: kallithea/controllers/files.py:388 kallithea/controllers/files.py:456
 #, python-format
 msgid "Successfully committed to %s"
 msgstr "成功提交到%s"
 
-#: kallithea/controllers/files.py:409
+#: kallithea/controllers/files.py:407
 msgid "Added file via Kallithea"
 msgstr "已添加文件通过 Kallithea"
 
-#: kallithea/controllers/files.py:430
+#: kallithea/controllers/files.py:428
 msgid "No content"
 msgstr "无内容"
 
-#: kallithea/controllers/files.py:434
+#: kallithea/controllers/files.py:432
 msgid "No filename"
 msgstr "无文件名"
 
-#: kallithea/controllers/files.py:461
+#: kallithea/controllers/files.py:459
 msgid "Location must be relative path and must not contain .. in path"
 msgstr ""
 
-#: kallithea/controllers/files.py:493
+#: kallithea/controllers/files.py:491
 msgid "Downloads disabled"
 msgstr "下载已禁用"
 
-#: kallithea/controllers/files.py:504
+#: kallithea/controllers/files.py:502
 #, python-format
 msgid "Unknown revision %s"
 msgstr "未知版本%s"
 
-#: kallithea/controllers/files.py:506
+#: kallithea/controllers/files.py:504
 msgid "Empty repository"
 msgstr "空版本库"
 
-#: kallithea/controllers/files.py:508
+#: kallithea/controllers/files.py:506
 msgid "Unknown archive type"
 msgstr "未知包类型"
 
-#: kallithea/controllers/files.py:729
+#: kallithea/controllers/files.py:726
 #: kallithea/templates/changeset/changeset_range.html:9
 #: kallithea/templates/email_templates/pull_request.html:64
 #: kallithea/templates/pullrequests/pullrequest.html:84
 msgid "Changesets"
 msgstr "修订集"
 
-#: kallithea/controllers/files.py:730
-#: kallithea/controllers/pullrequests.py:182 kallithea/model/scm.py:676
+#: kallithea/controllers/files.py:727
+#: kallithea/controllers/pullrequests.py:174 kallithea/model/scm.py:663
 msgid "Branches"
 msgstr "分支"
 
-#: kallithea/controllers/files.py:731
-#: kallithea/controllers/pullrequests.py:183 kallithea/model/scm.py:687
+#: kallithea/controllers/files.py:728
+#: kallithea/controllers/pullrequests.py:175 kallithea/model/scm.py:674
 msgid "Tags"
 msgstr "标签"
 
-#: kallithea/controllers/forks.py:174
+#: kallithea/controllers/forks.py:175
 #, python-format
 msgid "An error occurred during repository forking %s"
 msgstr "在复刻版本库%s的时候发生错误"
 
-#: kallithea/controllers/home.py:79
+#: kallithea/controllers/home.py:77
 msgid "Groups"
 msgstr "组"
 
-#: kallithea/controllers/home.py:89
+#: kallithea/controllers/home.py:87
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:90
 #: kallithea/templates/admin/repos/repo_add.html:12
 #: kallithea/templates/admin/repos/repo_add.html:16
@@ -273,48 +273,52 @@
 msgid "Repositories"
 msgstr "版本库"
 
-#: kallithea/controllers/home.py:122
+#: kallithea/controllers/home.py:119
 #: kallithea/templates/files/files_add.html:32
 #: kallithea/templates/files/files_delete.html:23
 #: kallithea/templates/files/files_edit.html:32
 msgid "Branch"
 msgstr "分支"
 
-#: kallithea/controllers/home.py:128
+#: kallithea/controllers/home.py:125
 msgid "Closed Branches"
 msgstr "已关闭分支"
 
-#: kallithea/controllers/home.py:134
+#: kallithea/controllers/home.py:131
 msgid "Tag"
 msgstr "标签"
 
-#: kallithea/controllers/home.py:140
+#: kallithea/controllers/home.py:137
 msgid "Bookmark"
 msgstr "书签"
 
-#: kallithea/controllers/journal.py:112 kallithea/controllers/journal.py:154
+#: kallithea/controllers/journal.py:146 kallithea/controllers/journal.py:157
 #: kallithea/templates/journal/public_journal.html:4
 #: kallithea/templates/journal/public_journal.html:18
 msgid "Public Journal"
 msgstr "公共日志"
 
-#: kallithea/controllers/journal.py:116 kallithea/controllers/journal.py:158
+#: kallithea/controllers/journal.py:150 kallithea/controllers/journal.py:161
 #: kallithea/templates/base/base.html:290
 #: kallithea/templates/journal/journal.html:5
 #: kallithea/templates/journal/journal.html:13
 msgid "Journal"
 msgstr "日志"
 
-#: kallithea/controllers/login.py:140 kallithea/controllers/login.py:185
+#: kallithea/controllers/login.py:109
+msgid "Authentication failed."
+msgstr ""
+
+#: kallithea/controllers/login.py:142 kallithea/controllers/login.py:187
 msgid "Bad captcha"
 msgstr "验证码错误"
 
-#: kallithea/controllers/login.py:146
+#: kallithea/controllers/login.py:148
 #, python-format
 msgid "You have successfully registered with %s"
 msgstr "您已成功注册 %s"
 
-#: kallithea/controllers/login.py:190
+#: kallithea/controllers/login.py:192
 msgid "A password reset confirmation code has been sent"
 msgstr "密码重置确认码已经发送"
 
@@ -327,234 +331,234 @@
 msgid "Successfully updated password"
 msgstr "成功更新密码"
 
-#: kallithea/controllers/pullrequests.py:68
+#: kallithea/controllers/pullrequests.py:67
 #, python-format
 msgid "Invalid reviewer \"%s\" specified"
 msgstr "指定的审核者 \"%s\" 无效"
 
-#: kallithea/controllers/pullrequests.py:131
+#: kallithea/controllers/pullrequests.py:123
 #, python-format
 msgid "%s (closed)"
 msgstr "%s (已关闭)"
 
-#: kallithea/controllers/pullrequests.py:158
+#: kallithea/controllers/pullrequests.py:150
 #: kallithea/templates/changeset/changeset.html:12
 msgid "Changeset"
 msgstr "修订集"
 
-#: kallithea/controllers/pullrequests.py:179
+#: kallithea/controllers/pullrequests.py:171
 msgid "Special"
 msgstr "特殊"
 
-#: kallithea/controllers/pullrequests.py:180
+#: kallithea/controllers/pullrequests.py:172
 msgid "Peer branches"
 msgstr "同等分支"
 
-#: kallithea/controllers/pullrequests.py:181 kallithea/model/scm.py:682
+#: kallithea/controllers/pullrequests.py:173 kallithea/model/scm.py:669
 msgid "Bookmarks"
 msgstr "书签"
 
-#: kallithea/controllers/pullrequests.py:318
+#: kallithea/controllers/pullrequests.py:315
 #, python-format
 msgid "Error creating pull request: %s"
 msgstr "创建拉取请求出错:%s"
 
-#: kallithea/controllers/pullrequests.py:345
-#: kallithea/controllers/pullrequests.py:368
+#: kallithea/controllers/pullrequests.py:342
+#: kallithea/controllers/pullrequests.py:365
 msgid "Error occurred while creating pull request"
 msgstr "创建拉取请求时发生错误"
 
-#: kallithea/controllers/pullrequests.py:350
+#: kallithea/controllers/pullrequests.py:347
 msgid "Successfully opened new pull request"
 msgstr "成功提交拉取请求"
 
-#: kallithea/controllers/pullrequests.py:373
+#: kallithea/controllers/pullrequests.py:370
 #, fuzzy
 #| msgid "Pull request update created"
 msgid "New pull request iteration created"
 msgstr "拉取请求更新已创建"
 
-#: kallithea/controllers/pullrequests.py:401
+#: kallithea/controllers/pullrequests.py:398
 #, python-format
 msgid "Meanwhile, the following reviewers have been added: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:405
+#: kallithea/controllers/pullrequests.py:402
 #, python-format
 msgid "Meanwhile, the following reviewers have been removed: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:421
-#: kallithea/model/pull_request.py:232
+#: kallithea/controllers/pullrequests.py:418
+#: kallithea/model/pull_request.py:230
 msgid "No description"
 msgstr "无描述"
 
-#: kallithea/controllers/pullrequests.py:430
+#: kallithea/controllers/pullrequests.py:427
 msgid "Pull request updated"
 msgstr "拉取请求已更新"
 
-#: kallithea/controllers/pullrequests.py:443
+#: kallithea/controllers/pullrequests.py:440
 msgid "Successfully deleted pull request"
 msgstr "成功删除拉取请求"
 
-#: kallithea/controllers/pullrequests.py:479
+#: kallithea/controllers/pullrequests.py:476
 #, fuzzy, python-format
 #| msgid "Changeset for %s %s not found in %s"
 msgid "Revision %s not found in %s"
 msgstr "未找到修订集"
 
-#: kallithea/controllers/pullrequests.py:506
+#: kallithea/controllers/pullrequests.py:504
 #, fuzzy, python-format
 #| msgid "No changesets found for updating this pull request."
 msgid "Error: changesets not found when displaying pull request from %s."
 msgstr "没有找到更新此拉取请求的修订集。"
 
-#: kallithea/controllers/pullrequests.py:520
+#: kallithea/controllers/pullrequests.py:518
 #, python-format
 msgid "This pull request has already been merged to %s."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:522
+#: kallithea/controllers/pullrequests.py:520
 msgid "This pull request has been closed and can not be updated."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:546
+#: kallithea/controllers/pullrequests.py:539
 #, python-format
 msgid "The following additional changes are available on %s:"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:548
-#: kallithea/controllers/pullrequests.py:552
+#: kallithea/controllers/pullrequests.py:541
+#: kallithea/controllers/pullrequests.py:545
 #, fuzzy
 #| msgid "No changesets found for updating this pull request."
 msgid "No additional changesets found for iterating on this pull request."
 msgstr "没有找到更新此拉取请求的修订集。"
 
-#: kallithea/controllers/pullrequests.py:560
+#: kallithea/controllers/pullrequests.py:553
 #, python-format
 msgid "Note: Branch %s has another head: %s."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:567
+#: kallithea/controllers/pullrequests.py:560
 msgid "Git pull requests don't support iterating yet."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:569
+#: kallithea/controllers/pullrequests.py:562
 #, fuzzy, python-format
 #| msgid "No changesets found for updating this pull request."
 msgid ""
 "Error: some changesets not found when displaying pull request from %s."
 msgstr "没有找到更新此拉取请求的修订集。"
 
-#: kallithea/controllers/pullrequests.py:593
+#: kallithea/controllers/pullrequests.py:586
 msgid "The diff can't be shown - the PR revisions could not be found."
 msgstr ""
 
-#: kallithea/controllers/search.py:136
+#: kallithea/controllers/search.py:132
 msgid "Invalid search query. Try quoting it."
 msgstr "错误的搜索。请尝试用引号包含它。"
 
-#: kallithea/controllers/search.py:140
+#: kallithea/controllers/search.py:136
 msgid "The server has no search index."
 msgstr ""
 
-#: kallithea/controllers/search.py:143
+#: kallithea/controllers/search.py:139
 msgid "An error occurred during search operation."
 msgstr "搜索操作期间发生错误。"
 
-#: kallithea/controllers/summary.py:168
-#: kallithea/templates/summary/summary.html:412
+#: kallithea/controllers/summary.py:169
+#: kallithea/templates/summary/summary.html:410
 msgid "No data ready yet"
 msgstr "数据尚未就绪"
 
-#: kallithea/controllers/summary.py:171
+#: kallithea/controllers/summary.py:172
 #: kallithea/templates/summary/summary.html:97
 msgid "Statistics are disabled for this repository"
 msgstr "该版本库统计功能已经禁用"
 
-#: kallithea/controllers/admin/auth_settings.py:137
+#: kallithea/controllers/admin/auth_settings.py:136
 msgid "Auth settings updated successfully"
 msgstr "验证设置更新成功"
 
-#: kallithea/controllers/admin/auth_settings.py:148
+#: kallithea/controllers/admin/auth_settings.py:147
 msgid "error occurred during update of auth settings"
 msgstr "验证设置更新时发生错误"
 
-#: kallithea/controllers/admin/defaults.py:75
+#: kallithea/controllers/admin/defaults.py:74
 msgid "Default settings updated successfully"
 msgstr "默认设置已经成功更新"
 
-#: kallithea/controllers/admin/defaults.py:90
+#: kallithea/controllers/admin/defaults.py:89
 msgid "Error occurred during update of defaults"
 msgstr "默认值更新时发生错误"
 
 #: kallithea/controllers/admin/gists.py:59
 #: kallithea/controllers/admin/my_account.py:232
-#: kallithea/controllers/admin/users.py:248
+#: kallithea/controllers/admin/users.py:246
 #, fuzzy
 msgid "Forever"
 msgstr "检视者"
 
 #: kallithea/controllers/admin/gists.py:60
 #: kallithea/controllers/admin/my_account.py:233
-#: kallithea/controllers/admin/users.py:249
+#: kallithea/controllers/admin/users.py:247
 msgid "5 minutes"
 msgstr "5 分钟"
 
 #: kallithea/controllers/admin/gists.py:61
 #: kallithea/controllers/admin/my_account.py:234
-#: kallithea/controllers/admin/users.py:250
+#: kallithea/controllers/admin/users.py:248
 msgid "1 hour"
 msgstr "1 小时"
 
 #: kallithea/controllers/admin/gists.py:62
 #: kallithea/controllers/admin/my_account.py:235
-#: kallithea/controllers/admin/users.py:251
+#: kallithea/controllers/admin/users.py:249
 msgid "1 day"
 msgstr "1 天"
 
 #: kallithea/controllers/admin/gists.py:63
 #: kallithea/controllers/admin/my_account.py:236
-#: kallithea/controllers/admin/users.py:252
+#: kallithea/controllers/admin/users.py:250
 msgid "1 month"
 msgstr "1 个月"
 
 #: kallithea/controllers/admin/gists.py:67
 #: kallithea/controllers/admin/my_account.py:238
-#: kallithea/controllers/admin/users.py:254
+#: kallithea/controllers/admin/users.py:252
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:65
 #: kallithea/templates/admin/users/user_edit_api_keys.html:65
 msgid "Lifetime"
 msgstr "终身"
 
-#: kallithea/controllers/admin/gists.py:142
+#: kallithea/controllers/admin/gists.py:148
 msgid "Error occurred during gist creation"
 msgstr "gist 创建时发生错误"
 
-#: kallithea/controllers/admin/gists.py:158
+#: kallithea/controllers/admin/gists.py:164
 #, python-format
 msgid "Deleted gist %s"
 msgstr "已删除 gist %s"
 
-#: kallithea/controllers/admin/gists.py:198
+#: kallithea/controllers/admin/gists.py:207
 msgid "Unmodified"
 msgstr "未修改"
 
-#: kallithea/controllers/admin/gists.py:228
+#: kallithea/controllers/admin/gists.py:237
 msgid "Successfully updated gist content"
 msgstr "成功更新 gist 内容"
 
-#: kallithea/controllers/admin/gists.py:233
+#: kallithea/controllers/admin/gists.py:242
 msgid "Successfully updated gist data"
 msgstr "成功更新 gist 数据"
 
-#: kallithea/controllers/admin/gists.py:236
+#: kallithea/controllers/admin/gists.py:245
 #, python-format
 msgid "Error occurred during update of gist %s"
 msgstr "gist %s 更新时发生错误"
 
-#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:211
-#: kallithea/model/user.py:232
+#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:205
+#: kallithea/model/user.py:226
 msgid "You can't edit this user since it's crucial for entire application"
 msgstr "由于是系统帐号,无法编辑该用户"
 
@@ -563,7 +567,7 @@
 msgstr "你的帐号已经更新完成"
 
 #: kallithea/controllers/admin/my_account.py:134
-#: kallithea/controllers/admin/users.py:181
+#: kallithea/controllers/admin/users.py:179
 #, python-format
 msgid "Error occurred during update of user %s"
 msgstr "用户 %s 更新时发生错误"
@@ -573,45 +577,45 @@
 msgstr "用户密码更新时发生错误"
 
 #: kallithea/controllers/admin/my_account.py:209
-#: kallithea/controllers/admin/users.py:367
+#: kallithea/controllers/admin/users.py:365
 #, python-format
 msgid "Added email %s to user"
 msgstr "已为用户添加电子邮件 %s"
 
 #: kallithea/controllers/admin/my_account.py:215
-#: kallithea/controllers/admin/users.py:373
+#: kallithea/controllers/admin/users.py:371
 msgid "An error occurred during email saving"
 msgstr "保存电子邮件时发生错误"
 
 #: kallithea/controllers/admin/my_account.py:224
-#: kallithea/controllers/admin/users.py:383
+#: kallithea/controllers/admin/users.py:381
 msgid "Removed email from user"
 msgstr "成功删除用户电子邮件"
 
 #: kallithea/controllers/admin/my_account.py:248
-#: kallithea/controllers/admin/users.py:271
+#: kallithea/controllers/admin/users.py:269
 msgid "API key successfully created"
 msgstr "API 密钥创建成功"
 
 #: kallithea/controllers/admin/my_account.py:257
-#: kallithea/controllers/admin/users.py:281
+#: kallithea/controllers/admin/users.py:279
 msgid "API key successfully reset"
 msgstr "API 密钥重置成功"
 
 #: kallithea/controllers/admin/my_account.py:261
-#: kallithea/controllers/admin/users.py:285
+#: kallithea/controllers/admin/users.py:283
 msgid "API key successfully deleted"
 msgstr "API 密钥删除成功"
 
 #: kallithea/controllers/admin/my_account.py:281
-#: kallithea/controllers/admin/users.py:456
+#: kallithea/controllers/admin/users.py:454
 #, fuzzy, python-format
 #| msgid "API key successfully created"
 msgid "SSH key %s successfully added"
 msgstr "API 密钥创建成功"
 
 #: kallithea/controllers/admin/my_account.py:293
-#: kallithea/controllers/admin/users.py:470
+#: kallithea/controllers/admin/users.py:468
 #, fuzzy
 #| msgid "API key successfully deleted"
 msgid "SSH key successfully deleted"
@@ -689,11 +693,11 @@
 msgid "Allowed with automatic account activation"
 msgstr "已允许自动激活账号"
 
-#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1673
+#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1578
 msgid "Manual activation of external account"
 msgstr "外部账号手动激活"
 
-#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1674
+#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1579
 msgid "Automatic activation of external account"
 msgstr "外部账号自动激活"
 
@@ -715,342 +719,334 @@
 msgid "Error occurred during update of permissions"
 msgstr "权限更新时发生错误"
 
-#: kallithea/controllers/admin/repo_groups.py:172
+#: kallithea/controllers/admin/repo_groups.py:165
 #, python-format
 msgid "Error occurred during creation of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:177
+#: kallithea/controllers/admin/repo_groups.py:172
 #, python-format
 msgid "Created repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:224
+#: kallithea/controllers/admin/repo_groups.py:219
 #, python-format
 msgid "Updated repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:240
+#: kallithea/controllers/admin/repo_groups.py:235
 #, python-format
 msgid "Error occurred during update of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:250
+#: kallithea/controllers/admin/repo_groups.py:245
 #, python-format
 msgid "This group contains %s repositories and cannot be deleted"
 msgstr "这个组内有%s个版本库因而无法删除"
 
-#: kallithea/controllers/admin/repo_groups.py:257
+#: kallithea/controllers/admin/repo_groups.py:252
 #, python-format
 msgid "This group contains %s subgroups and cannot be deleted"
 msgstr ""
 
+#: kallithea/controllers/admin/repo_groups.py:258
+#, python-format
+msgid "Removed repository group %s"
+msgstr ""
+
 #: kallithea/controllers/admin/repo_groups.py:263
 #, python-format
-msgid "Removed repository group %s"
-msgstr ""
-
-#: kallithea/controllers/admin/repo_groups.py:268
-#, python-format
 msgid "Error occurred during deletion of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:352
-#: kallithea/controllers/admin/repo_groups.py:382
-#: kallithea/controllers/admin/user_groups.py:294
+#: kallithea/controllers/admin/repo_groups.py:347
+#: kallithea/controllers/admin/repo_groups.py:377
+#: kallithea/controllers/admin/user_groups.py:290
 msgid "Cannot revoke permission for yourself as admin"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:367
+#: kallithea/controllers/admin/repo_groups.py:362
 msgid "Repository group permissions updated"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:399
-#: kallithea/controllers/admin/repos.py:358
-#: kallithea/controllers/admin/user_groups.py:306
+#: kallithea/controllers/admin/repo_groups.py:394
+#: kallithea/controllers/admin/repos.py:357
+#: kallithea/controllers/admin/user_groups.py:302
 msgid "An error occurred during revoking of permission"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:136
+#: kallithea/controllers/admin/repos.py:137
 #, python-format
 msgid "Error creating repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:194
+#: kallithea/controllers/admin/repos.py:193
 #, python-format
 msgid "Created repository %s from %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:203
+#: kallithea/controllers/admin/repos.py:202
 #, python-format
 msgid "Forked repository %s as %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:206
+#: kallithea/controllers/admin/repos.py:205
 #, python-format
 msgid "Created repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:235
+#: kallithea/controllers/admin/repos.py:234
 #, python-format
 msgid "Repository %s updated successfully"
 msgstr "版本库%s成功更新"
 
-#: kallithea/controllers/admin/repos.py:255
+#: kallithea/controllers/admin/repos.py:254
 #, python-format
 msgid "Error occurred during update of repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:273
+#: kallithea/controllers/admin/repos.py:272
 #, python-format
 msgid "Detached %s forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:276
+#: kallithea/controllers/admin/repos.py:275
 #, python-format
 msgid "Deleted %s forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:281
+#: kallithea/controllers/admin/repos.py:280
 #, python-format
 msgid "Deleted repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:284
+#: kallithea/controllers/admin/repos.py:283
 #, fuzzy, python-format
 msgid "Cannot delete repository %s which still has forks"
 msgstr "无法删除%s因为它还有其他分复刻本库"
 
-#: kallithea/controllers/admin/repos.py:289
+#: kallithea/controllers/admin/repos.py:288
 #, python-format
 msgid "An error occurred during deletion of %s"
 msgstr "在删除%s的时候发生错误"
 
-#: kallithea/controllers/admin/repos.py:329
+#: kallithea/controllers/admin/repos.py:328
 msgid "Repository permissions updated"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:388
+#: kallithea/controllers/admin/repos.py:387
 #, python-format
 msgid "Field validation error: %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:391
+#: kallithea/controllers/admin/repos.py:390
 #, fuzzy, python-format
 #| msgid "An error occurred during deletion of user"
 msgid "An error occurred during creation of field: %r"
 msgstr "删除用户时发生错误"
 
-#: kallithea/controllers/admin/repos.py:402
+#: kallithea/controllers/admin/repos.py:401
 msgid "An error occurred during removal of field"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:416
+#: kallithea/controllers/admin/repos.py:415
 msgid "-- Not a fork --"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:448
+#: kallithea/controllers/admin/repos.py:447
 msgid "Updated repository visibility in public journal"
 msgstr "成功更新在公共日志中的可见性"
 
-#: kallithea/controllers/admin/repos.py:452
+#: kallithea/controllers/admin/repos.py:451
 msgid "An error occurred during setting this repository in public journal"
 msgstr "设置版本库到公共日志时发生错误"
 
-#: kallithea/controllers/admin/repos.py:468
+#: kallithea/controllers/admin/repos.py:467
 msgid "Nothing"
 msgstr "无"
 
-#: kallithea/controllers/admin/repos.py:470
+#: kallithea/controllers/admin/repos.py:469
 #, python-format
 msgid "Marked repository %s as fork of %s"
 msgstr "成功将版本库%s标记为复刻自%s"
 
-#: kallithea/controllers/admin/repos.py:477
+#: kallithea/controllers/admin/repos.py:476
 msgid "An error occurred during this operation"
 msgstr "在搜索操作中发生错误"
 
-#: kallithea/controllers/admin/repos.py:490
-msgid "Cache invalidation successful"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:494
-msgid "An error occurred during cache invalidation"
-msgstr "清除缓存时发生错误"
-
-#: kallithea/controllers/admin/repos.py:507
+#: kallithea/controllers/admin/repos.py:488
 msgid "Pulled from remote location"
 msgstr "成功拉取自远程路径"
 
-#: kallithea/controllers/admin/repos.py:510
+#: kallithea/controllers/admin/repos.py:491
 msgid "An error occurred during pull from remote location"
 msgstr "从远程路径拉取时发生错误"
 
-#: kallithea/controllers/admin/repos.py:541
+#: kallithea/controllers/admin/repos.py:522
 msgid "An error occurred during deletion of repository stats"
 msgstr "删除版本库统计时发生错误"
 
-#: kallithea/controllers/admin/settings.py:131
+#: kallithea/controllers/admin/settings.py:132
 msgid "Updated VCS settings"
 msgstr "成功更新版本控制系统设置"
 
-#: kallithea/controllers/admin/settings.py:135 kallithea/lib/utils.py:237
+#: kallithea/controllers/admin/settings.py:136
 msgid ""
 "Unable to activate hgsubversion support. The \"hgsubversion\" library is "
 "missing"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:141
-#: kallithea/controllers/admin/settings.py:233
+#: kallithea/controllers/admin/settings.py:142
+#: kallithea/controllers/admin/settings.py:234
 msgid "Error occurred while updating application settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:176
+#: kallithea/controllers/admin/settings.py:177
 #, python-format
 msgid "Repositories successfully rescanned. Added: %s. Removed: %s."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:188
+#: kallithea/controllers/admin/settings.py:189
 #, fuzzy, python-format
 msgid "Invalidated %s repositories"
 msgstr "清除版本库缓存"
 
-#: kallithea/controllers/admin/settings.py:229
+#: kallithea/controllers/admin/settings.py:230
 msgid "Updated application settings"
 msgstr "更新应用设置"
 
-#: kallithea/controllers/admin/settings.py:283
+#: kallithea/controllers/admin/settings.py:284
 msgid "Updated visualisation settings"
 msgstr "成功更新可视化设置"
 
-#: kallithea/controllers/admin/settings.py:288
+#: kallithea/controllers/admin/settings.py:289
 msgid "Error occurred during updating visualisation settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:312
+#: kallithea/controllers/admin/settings.py:313
 msgid "Please enter email address"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:327
+#: kallithea/controllers/admin/settings.py:328
 msgid "Send email task created"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:355
+#: kallithea/controllers/admin/settings.py:356
 #, fuzzy
 #| msgid "No data ready yet"
 msgid "Hook already exists"
 msgstr "数据尚未就绪"
 
-#: kallithea/controllers/admin/settings.py:357
+#: kallithea/controllers/admin/settings.py:358
 msgid "Builtin hooks are read-only. Please use another hook name."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:360
+#: kallithea/controllers/admin/settings.py:361
 msgid "Added new hook"
 msgstr "新建钩子"
 
-#: kallithea/controllers/admin/settings.py:376
+#: kallithea/controllers/admin/settings.py:377
 msgid "Updated hooks"
 msgstr "更新钩子"
 
-#: kallithea/controllers/admin/settings.py:380
+#: kallithea/controllers/admin/settings.py:381
 msgid "Error occurred during hook creation"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:404
+#: kallithea/controllers/admin/settings.py:405
 msgid "Whoosh reindex task scheduled"
 msgstr "Whoosh重新索引任务调度"
 
-#: kallithea/controllers/admin/user_groups.py:138
+#: kallithea/controllers/admin/user_groups.py:134
 #, python-format
 msgid "Created user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:151
+#: kallithea/controllers/admin/user_groups.py:147
 #, python-format
 msgid "Error occurred during creation of user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:179
+#: kallithea/controllers/admin/user_groups.py:175
 #, python-format
 msgid "Updated user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:201
+#: kallithea/controllers/admin/user_groups.py:197
 #, python-format
 msgid "Error occurred during update of user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:212
+#: kallithea/controllers/admin/user_groups.py:208
 msgid "Successfully deleted user group"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:217
+#: kallithea/controllers/admin/user_groups.py:213
 msgid "An error occurred during deletion of user group"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:273
+#: kallithea/controllers/admin/user_groups.py:269
 msgid "Target group cannot be the same"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:279
+#: kallithea/controllers/admin/user_groups.py:275
 msgid "User group permissions updated"
 msgstr ""
 
+#: kallithea/controllers/admin/user_groups.py:384
+#: kallithea/controllers/admin/users.py:336
+msgid "Updated permissions"
+msgstr ""
+
 #: kallithea/controllers/admin/user_groups.py:388
-#: kallithea/controllers/admin/users.py:338
-msgid "Updated permissions"
-msgstr ""
-
-#: kallithea/controllers/admin/user_groups.py:392
-#: kallithea/controllers/admin/users.py:342
+#: kallithea/controllers/admin/users.py:340
 msgid "An error occurred during permissions saving"
 msgstr "保存权限时发生错误"
 
-#: kallithea/controllers/admin/users.py:123
+#: kallithea/controllers/admin/users.py:121
 #, python-format
 msgid "Created user %s"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:138
+#: kallithea/controllers/admin/users.py:136
 #, python-format
 msgid "Error occurred during creation of user %s"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:162
+#: kallithea/controllers/admin/users.py:160
 msgid "User updated successfully"
 msgstr "用户更新成功"
 
-#: kallithea/controllers/admin/users.py:190
+#: kallithea/controllers/admin/users.py:188
 msgid "Successfully deleted user"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:195
+#: kallithea/controllers/admin/users.py:193
 msgid "An error occurred during deletion of user"
 msgstr "删除用户时发生错误"
 
-#: kallithea/controllers/admin/users.py:203
+#: kallithea/controllers/admin/users.py:201
 msgid "The default user cannot be edited"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:409
+#: kallithea/controllers/admin/users.py:407
 #, python-format
 msgid "Added IP address %s to user whitelist"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:415
+#: kallithea/controllers/admin/users.py:413
 msgid "An error occurred while adding IP address"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:427
+#: kallithea/controllers/admin/users.py:425
 msgid "Removed IP address from user whitelist"
 msgstr ""
 
-#: kallithea/lib/auth.py:684
+#: kallithea/lib/auth.py:634
 msgid "You need to be a registered user to perform this action"
 msgstr "必须是注册用户才能进行此操作"
 
-#: kallithea/lib/auth.py:712
+#: kallithea/lib/auth.py:662
 msgid "You need to be signed in to view this page"
 msgstr "必须登录才能访问该页面"
 
@@ -1083,171 +1079,171 @@
 "Changeset was too big and was cut off, use diff menu to display this diff"
 msgstr "修订集过大并已被截断,使用差异菜单查看此差异"
 
-#: kallithea/lib/diffs.py:224
+#: kallithea/lib/diffs.py:223
 msgid "No changes detected"
 msgstr "未发现差异"
 
-#: kallithea/lib/helpers.py:653
+#: kallithea/lib/helpers.py:670
 #, python-format
 msgid "Deleted branch: %s"
 msgstr "已经删除分支%s"
 
-#: kallithea/lib/helpers.py:655
+#: kallithea/lib/helpers.py:672
 #, python-format
 msgid "Created tag: %s"
 msgstr "创建标签%s"
 
-#: kallithea/lib/helpers.py:666
+#: kallithea/lib/helpers.py:683
 #, fuzzy, python-format
 msgid "Changeset %s not found"
 msgstr "未找到修订集"
 
-#: kallithea/lib/helpers.py:715
+#: kallithea/lib/helpers.py:732
 #, python-format
 msgid "Show all combined changesets %s->%s"
 msgstr "显示所有合并的修订集 %s->%s"
 
-#: kallithea/lib/helpers.py:721
+#: kallithea/lib/helpers.py:738
 #, fuzzy
 msgid "Compare view"
 msgstr "比较显示"
 
-#: kallithea/lib/helpers.py:740
+#: kallithea/lib/helpers.py:757
 msgid "and"
 msgstr "还有"
 
-#: kallithea/lib/helpers.py:741
+#: kallithea/lib/helpers.py:758
 #, python-format
 msgid "%s more"
 msgstr "%s个"
 
-#: kallithea/lib/helpers.py:742
+#: kallithea/lib/helpers.py:759
 #: kallithea/templates/changelog/changelog.html:43
 msgid "revisions"
 msgstr "修订"
 
-#: kallithea/lib/helpers.py:766
+#: kallithea/lib/helpers.py:783
 #, fuzzy, python-format
 msgid "Fork name %s"
 msgstr "复刻名称%s"
 
-#: kallithea/lib/helpers.py:787
+#: kallithea/lib/helpers.py:804
 #, fuzzy, python-format
 msgid "Pull request %s"
 msgstr "拉取请求#%s"
 
-#: kallithea/lib/helpers.py:797
+#: kallithea/lib/helpers.py:814
 msgid "[deleted] repository"
 msgstr "[删除]版本库"
 
-#: kallithea/lib/helpers.py:799 kallithea/lib/helpers.py:811
+#: kallithea/lib/helpers.py:816 kallithea/lib/helpers.py:828
 msgid "[created] repository"
 msgstr "[创建]版本库"
 
-#: kallithea/lib/helpers.py:801
+#: kallithea/lib/helpers.py:818
 msgid "[created] repository as fork"
 msgstr "[创建]复刻版本库"
 
-#: kallithea/lib/helpers.py:803 kallithea/lib/helpers.py:813
+#: kallithea/lib/helpers.py:820 kallithea/lib/helpers.py:830
 msgid "[forked] repository"
 msgstr "[复刻]版本库"
 
-#: kallithea/lib/helpers.py:805 kallithea/lib/helpers.py:815
+#: kallithea/lib/helpers.py:822 kallithea/lib/helpers.py:832
 msgid "[updated] repository"
 msgstr "[更新]版本库"
 
-#: kallithea/lib/helpers.py:807
+#: kallithea/lib/helpers.py:824
 msgid "[downloaded] archive from repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:809
+#: kallithea/lib/helpers.py:826
 msgid "[delete] repository"
 msgstr "[删除]版本库"
 
-#: kallithea/lib/helpers.py:817
+#: kallithea/lib/helpers.py:834
 msgid "[created] user"
 msgstr "[创建]用户"
 
-#: kallithea/lib/helpers.py:819
+#: kallithea/lib/helpers.py:836
 msgid "[updated] user"
 msgstr "[更新]用户"
 
-#: kallithea/lib/helpers.py:821
+#: kallithea/lib/helpers.py:838
 msgid "[created] user group"
 msgstr ""
 
-#: kallithea/lib/helpers.py:823
+#: kallithea/lib/helpers.py:840
 msgid "[updated] user group"
 msgstr ""
 
-#: kallithea/lib/helpers.py:825
+#: kallithea/lib/helpers.py:842
 msgid "[commented] on revision in repository"
 msgstr "[评论]了版本库中的修订"
 
-#: kallithea/lib/helpers.py:827
+#: kallithea/lib/helpers.py:844
 msgid "[commented] on pull request for"
 msgstr "[评论]拉取请求"
 
-#: kallithea/lib/helpers.py:829
+#: kallithea/lib/helpers.py:846
 msgid "[closed] pull request for"
 msgstr "[关闭] 拉取请求"
 
-#: kallithea/lib/helpers.py:831
+#: kallithea/lib/helpers.py:848
 msgid "[pushed] into"
 msgstr "[推送]到"
 
-#: kallithea/lib/helpers.py:833
+#: kallithea/lib/helpers.py:850
 msgid "[committed via Kallithea] into repository"
 msgstr "[通过Kallithea提交]到版本库"
 
-#: kallithea/lib/helpers.py:835
+#: kallithea/lib/helpers.py:852
 msgid "[pulled from remote] into repository"
 msgstr "[远程拉取]到版本库"
 
-#: kallithea/lib/helpers.py:837
+#: kallithea/lib/helpers.py:854
 msgid "[pulled] from"
 msgstr "[拉取]自"
 
-#: kallithea/lib/helpers.py:839
+#: kallithea/lib/helpers.py:856
 msgid "[started following] repository"
 msgstr "[开始关注]版本库"
 
-#: kallithea/lib/helpers.py:841
+#: kallithea/lib/helpers.py:858
 msgid "[stopped following] repository"
 msgstr "[停止关注]版本库"
 
-#: kallithea/lib/helpers.py:961
+#: kallithea/lib/helpers.py:975
 #, python-format
 msgid " and %s more"
 msgstr " 还有%s个"
 
-#: kallithea/lib/helpers.py:965
+#: kallithea/lib/helpers.py:979
 #: kallithea/templates/compare/compare_diff.html:69
 #: kallithea/templates/pullrequests/pullrequest_show.html:297
 msgid "No files"
 msgstr "无文件"
 
-#: kallithea/lib/helpers.py:990
+#: kallithea/lib/helpers.py:1004
 msgid "new file"
 msgstr ""
 
-#: kallithea/lib/helpers.py:993
+#: kallithea/lib/helpers.py:1007
 msgid "mod"
 msgstr ""
 
-#: kallithea/lib/helpers.py:996
+#: kallithea/lib/helpers.py:1010
 msgid "del"
 msgstr ""
 
-#: kallithea/lib/helpers.py:999
+#: kallithea/lib/helpers.py:1013
 msgid "rename"
 msgstr ""
 
-#: kallithea/lib/helpers.py:1004
+#: kallithea/lib/helpers.py:1018
 msgid "chmod"
 msgstr ""
 
-#: kallithea/lib/helpers.py:1297
+#: kallithea/lib/helpers.py:1323
 #, python-format
 msgid ""
 "%s repository is not mapped to db perhaps it was created or renamed from "
@@ -1257,90 +1253,92 @@
 "版本库%s没有映射到数据库,可能是从文件系统创建或者重命名,请重启Kallithea"
 "以重新扫描版本库"
 
-#: kallithea/lib/ssh.py:73
+#: kallithea/lib/ssh.py:75
 msgid "SSH key is missing"
 msgstr ""
 
-#: kallithea/lib/ssh.py:77
-msgid "Incorrect SSH key - it must have both a key type and a base64 part"
-msgstr ""
-
-#: kallithea/lib/ssh.py:81
+#: kallithea/lib/ssh.py:79
+msgid ""
+"Incorrect SSH key - it must have both a key type and a base64 part, like "
+"'ssh-rsa ASRNeaZu4FA...xlJp='"
+msgstr ""
+
+#: kallithea/lib/ssh.py:83
 msgid "Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'"
 msgstr ""
 
-#: kallithea/lib/ssh.py:84
+#: kallithea/lib/ssh.py:86
 #, python-format
 msgid "Incorrect SSH key - unexpected characters in base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:89
+#: kallithea/lib/ssh.py:91
 #, python-format
 msgid "Incorrect SSH key - failed to decode base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:92
+#: kallithea/lib/ssh.py:94
 #, python-format
 msgid "Incorrect SSH key - base64 part is not %r as claimed but %r"
 msgstr ""
 
-#: kallithea/lib/utils2.py:334
+#: kallithea/lib/utils2.py:253
 #, python-format
 msgid "%d year"
 msgid_plural "%d years"
 msgstr[0] "%d年"
 
-#: kallithea/lib/utils2.py:335
+#: kallithea/lib/utils2.py:254
 #, python-format
 msgid "%d month"
 msgid_plural "%d months"
 msgstr[0] "%d月"
 
-#: kallithea/lib/utils2.py:336
+#: kallithea/lib/utils2.py:255
 #, python-format
 msgid "%d day"
 msgid_plural "%d days"
 msgstr[0] "%d天"
 
-#: kallithea/lib/utils2.py:337
+#: kallithea/lib/utils2.py:256
 #, python-format
 msgid "%d hour"
 msgid_plural "%d hours"
 msgstr[0] "%d时"
 
-#: kallithea/lib/utils2.py:338
+#: kallithea/lib/utils2.py:257
 #, python-format
 msgid "%d minute"
 msgid_plural "%d minutes"
 msgstr[0] "%d分"
 
-#: kallithea/lib/utils2.py:339
+#: kallithea/lib/utils2.py:258
 #, python-format
 msgid "%d second"
 msgid_plural "%d seconds"
 msgstr[0] "%d秒"
 
-#: kallithea/lib/utils2.py:355
+#: kallithea/lib/utils2.py:274
 #, python-format
 msgid "in %s"
 msgstr "%s"
 
-#: kallithea/lib/utils2.py:357
+#: kallithea/lib/utils2.py:276
 #, python-format
 msgid "%s ago"
 msgstr "%s前"
 
-#: kallithea/lib/utils2.py:359
+#: kallithea/lib/utils2.py:278
 #, python-format
 msgid "in %s and %s"
 msgstr "%s零%s"
 
-#: kallithea/lib/utils2.py:362
+#: kallithea/lib/utils2.py:281
 #, python-format
 msgid "%s and %s ago"
 msgstr "%s零%s前"
 
-#: kallithea/lib/utils2.py:365
+#: kallithea/lib/utils2.py:284
 msgid "just now"
 msgstr "刚才"
 
@@ -1349,143 +1347,143 @@
 msgid "on line %s"
 msgstr "在%s行"
 
-#: kallithea/model/comment.py:221 kallithea/model/pull_request.py:114
+#: kallithea/model/comment.py:219 kallithea/model/pull_request.py:112
 msgid "[Mention]"
 msgstr "[提及]"
 
-#: kallithea/model/db.py:1496
+#: kallithea/model/db.py:1411
 msgid "top level"
 msgstr ""
 
-#: kallithea/model/db.py:1637
+#: kallithea/model/db.py:1542
 msgid "Kallithea Administrator"
 msgstr "Kallithea 管理员"
 
-#: kallithea/model/db.py:1639
+#: kallithea/model/db.py:1544
 msgid "Default user has no access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1640
+#: kallithea/model/db.py:1545
 #, fuzzy
 msgid "Default user has read access to new repositories"
 msgstr "未授权的资源访问"
 
-#: kallithea/model/db.py:1641
+#: kallithea/model/db.py:1546
 #, fuzzy
 msgid "Default user has write access to new repositories"
 msgstr "未授权的资源访问"
 
-#: kallithea/model/db.py:1642
+#: kallithea/model/db.py:1547
 msgid "Default user has admin access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1644
+#: kallithea/model/db.py:1549
 msgid "Default user has no access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1645
+#: kallithea/model/db.py:1550
 msgid "Default user has read access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1646
+#: kallithea/model/db.py:1551
 msgid "Default user has write access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1647
+#: kallithea/model/db.py:1552
 msgid "Default user has admin access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1649
+#: kallithea/model/db.py:1554
 msgid "Default user has no access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1650
+#: kallithea/model/db.py:1555
 msgid "Default user has read access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1651
+#: kallithea/model/db.py:1556
 msgid "Default user has write access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1652
+#: kallithea/model/db.py:1557
 msgid "Default user has admin access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1654
+#: kallithea/model/db.py:1559
 #, fuzzy
 msgid "Only admins can create repository groups"
 msgstr "没有在该版本库组中创建版本库的权限"
 
-#: kallithea/model/db.py:1655
+#: kallithea/model/db.py:1560
 #, fuzzy
 msgid "Non-admins can create repository groups"
 msgstr "没有在该版本库组中创建版本库的权限"
 
-#: kallithea/model/db.py:1657
+#: kallithea/model/db.py:1562
 msgid "Only admins can create user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1658
+#: kallithea/model/db.py:1563
 msgid "Non-admins can create user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1660
+#: kallithea/model/db.py:1565
 msgid "Only admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1661
+#: kallithea/model/db.py:1566
 msgid "Non-admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1663
+#: kallithea/model/db.py:1568
 msgid ""
 "Repository creation enabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1664
+#: kallithea/model/db.py:1569
 msgid ""
 "Repository creation disabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1666
+#: kallithea/model/db.py:1571
 #, fuzzy
 msgid "Only admins can fork repositories"
 msgstr "创建版本库"
 
-#: kallithea/model/db.py:1667
+#: kallithea/model/db.py:1572
 #, fuzzy
 msgid "Non-admins can fork repositories"
 msgstr "创建版本库"
 
-#: kallithea/model/db.py:1669
+#: kallithea/model/db.py:1574
 msgid "Registration disabled"
 msgstr ""
 
-#: kallithea/model/db.py:1670
+#: kallithea/model/db.py:1575
 msgid "User registration with manual account activation"
 msgstr ""
 
-#: kallithea/model/db.py:1671
+#: kallithea/model/db.py:1576
 msgid "User registration with automatic account activation"
 msgstr ""
 
-#: kallithea/model/db.py:2206
+#: kallithea/model/db.py:1992
 #, fuzzy
 msgid "Not reviewed"
 msgstr "未检视"
 
-#: kallithea/model/db.py:2207
+#: kallithea/model/db.py:1993
 #, fuzzy
 msgid "Under review"
 msgstr "检视中"
 
-#: kallithea/model/db.py:2208
+#: kallithea/model/db.py:1994
 #, fuzzy
 #| msgid "Approved"
 msgid "Not approved"
 msgstr "已批准"
 
-#: kallithea/model/db.py:2209
+#: kallithea/model/db.py:1995
 msgid "Approved"
 msgstr "已批准"
 
@@ -1511,7 +1509,7 @@
 msgid "Name must not contain only digits"
 msgstr ""
 
-#: kallithea/model/notification.py:164
+#: kallithea/model/notification.py:162
 #, fuzzy, python-format
 #| msgid "[Comment] %(repo_name)s changeset %(short_id)s on %(branch)s"
 msgid ""
@@ -1519,119 +1517,119 @@
 "%(branch)s"
 msgstr "[评论] %(repo_name)s 修订集 %(short_id)s 在 %(branch)s"
 
-#: kallithea/model/notification.py:167
+#: kallithea/model/notification.py:165
 #, fuzzy, python-format
 msgid "New user %(new_username)s registered"
 msgstr "用户名称 %(new_username)s 无效"
 
-#: kallithea/model/notification.py:169
+#: kallithea/model/notification.py:167
 #, python-format
 msgid ""
 "[Review] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:170
+#: kallithea/model/notification.py:168
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:183
+#: kallithea/model/notification.py:188
 #, fuzzy
 msgid "Closing"
 msgstr "使用中"
 
-#: kallithea/model/pull_request.py:73
+#: kallithea/model/pull_request.py:72
 #, python-format
 msgid ""
 "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:209
+#: kallithea/model/pull_request.py:207
 #, fuzzy
 #| msgid "Error creating pull request: %s"
 msgid "Cannot create empty pull request"
 msgstr "创建拉取请求出错:%s"
 
-#: kallithea/model/pull_request.py:217
+#: kallithea/model/pull_request.py:215
 #, python-format
 msgid ""
 "Cannot create pull request - criss cross merge detected, please merge a "
 "later %s revision to %s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:245 kallithea/model/pull_request.py:380
+#: kallithea/model/pull_request.py:243 kallithea/model/pull_request.py:378
 #, fuzzy
 #| msgid "Confirm to delete this pull request"
 msgid "You are not authorized to create the pull request"
 msgstr "确认删除拉取请求"
 
-#: kallithea/model/pull_request.py:339
+#: kallithea/model/pull_request.py:337
 #, fuzzy
 #| msgid "Missing changesets since the previous pull request:"
 msgid "Missing changesets since the previous iteration:"
 msgstr "缺少上次拉取请求之后的修订集:"
 
-#: kallithea/model/pull_request.py:346
+#: kallithea/model/pull_request.py:344
 #, fuzzy, python-format
 #| msgid "New changesets on %s %s since the previous pull request:"
 msgid "New changesets on %s %s since the previous iteration:"
 msgstr "在上次拉取请求之后,在 %s %s 上的新修订集:"
 
-#: kallithea/model/pull_request.py:353
+#: kallithea/model/pull_request.py:351
 msgid "Ancestor didn't change - diff since previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:360
+#: kallithea/model/pull_request.py:358
 #, python-format
 msgid ""
 "This iteration is based on another %s revision and there is no simple "
 "diff."
 msgstr ""
 
-#: kallithea/model/pull_request.py:362
+#: kallithea/model/pull_request.py:360
 #, fuzzy, python-format
 #| msgid "New changesets on %s %s since the previous pull request:"
 msgid "No changes found on %s %s since previous iteration."
 msgstr "在上次拉取请求之后,在 %s %s 上的新修订集:"
 
-#: kallithea/model/pull_request.py:388
+#: kallithea/model/pull_request.py:386
 #, python-format
 msgid "Closed, next iteration: %s ."
 msgstr ""
 
-#: kallithea/model/scm.py:668
+#: kallithea/model/scm.py:655
 msgid "latest tip"
 msgstr "最新tip版本"
 
-#: kallithea/model/ssh_key.py:56
+#: kallithea/model/ssh_key.py:57
 #, python-format
 msgid "SSH key %r is invalid: %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:68
+#: kallithea/model/ssh_key.py:69
 #, python-format
 msgid "SSH key %s is already used by %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:89
+#: kallithea/model/ssh_key.py:88
 #, fuzzy, python-format
-msgid "SSH key %r not found"
+msgid "SSH key with fingerprint %r found"
 msgstr "未找到修订集"
 
-#: kallithea/model/user.py:186
+#: kallithea/model/user.py:180
 msgid "New user registration"
 msgstr ""
 
-#: kallithea/model/user.py:250
+#: kallithea/model/user.py:244
 #, fuzzy
 msgid ""
 "You can't remove this user since it is crucial for the entire application"
 msgstr "由于是系统帐号,无法删除该用户"
 
-#: kallithea/model/user.py:255
+#: kallithea/model/user.py:249
 #, fuzzy, python-format
 msgid ""
 "User \"%s\" still owns %s repositories and cannot be removed. Switch "
@@ -1640,7 +1638,7 @@
 "由于用户 \"%s\" 拥有版本库%s因而无法删除,请修改版本库所有者或删除版本"
 "库。%s"
 
-#: kallithea/model/user.py:260
+#: kallithea/model/user.py:254
 #, fuzzy, python-format
 msgid ""
 "User \"%s\" still owns %s repository groups and cannot be removed. Switch "
@@ -1649,7 +1647,7 @@
 "由于用户 \"%s\" 拥有版本库%s因而无法删除,请修改版本库所有者或删除版本"
 "库。%s"
 
-#: kallithea/model/user.py:267
+#: kallithea/model/user.py:261
 #, fuzzy, python-format
 msgid ""
 "User \"%s\" still owns %s user groups and cannot be removed. Switch "
@@ -1658,187 +1656,187 @@
 "由于用户 \"%s\" 拥有版本库%s因而无法删除,请修改版本库所有者或删除版本"
 "库。%s"
 
-#: kallithea/model/user.py:361
+#: kallithea/model/user.py:355
 msgid "Password reset link"
 msgstr ""
 
-#: kallithea/model/user.py:408
+#: kallithea/model/user.py:402
 #, fuzzy
 msgid "Password reset notification"
 msgstr "确认密码"
 
-#: kallithea/model/user.py:409
+#: kallithea/model/user.py:403
 #, python-format
 msgid ""
 "The password to your account %s has been changed using password reset "
 "form."
 msgstr ""
 
-#: kallithea/model/validators.py:52 kallithea/model/validators.py:53
+#: kallithea/model/validators.py:53 kallithea/model/validators.py:54
 msgid "Value cannot be an empty list"
 msgstr "值不能为空"
 
-#: kallithea/model/validators.py:72
+#: kallithea/model/validators.py:73
 #, python-format
 msgid "Username \"%(username)s\" already exists"
 msgstr "用户名称%(username)s已经存在"
 
-#: kallithea/model/validators.py:74
+#: kallithea/model/validators.py:75
 #, fuzzy, python-format
 msgid "Username \"%(username)s\" cannot be used"
 msgstr "用户名称 %(username)s 无效"
 
-#: kallithea/model/validators.py:76
+#: kallithea/model/validators.py:77
 msgid ""
 "Username may only contain alphanumeric characters underscores, periods or "
 "dashes and must begin with an alphanumeric character or underscore"
 msgstr ""
 
-#: kallithea/model/validators.py:103
+#: kallithea/model/validators.py:104
 msgid "The input is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:110
+#: kallithea/model/validators.py:111
 #, python-format
 msgid "Username %(username)s is not valid"
 msgstr "用户名称 %(username)s 无效"
 
-#: kallithea/model/validators.py:131
+#: kallithea/model/validators.py:132
 msgid "Invalid user group name"
 msgstr ""
 
-#: kallithea/model/validators.py:132
+#: kallithea/model/validators.py:133
 #, python-format
 msgid "User group \"%(usergroup)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:134
+#: kallithea/model/validators.py:135
 msgid ""
 "user group name may only contain alphanumeric characters underscores, "
 "periods or dashes and must begin with alphanumeric character"
 msgstr ""
 
-#: kallithea/model/validators.py:174
+#: kallithea/model/validators.py:175
 msgid "Cannot assign this group as parent"
 msgstr "不能将这个组作为parent"
 
-#: kallithea/model/validators.py:175
+#: kallithea/model/validators.py:176
 #, python-format
 msgid "Group \"%(group_name)s\" already exists"
 msgstr "组 \"%(group_name)s\" 已经存在"
 
-#: kallithea/model/validators.py:177
+#: kallithea/model/validators.py:178
 #, python-format
 msgid "Repository with name \"%(group_name)s\" already exists"
 msgstr "已经存在名为 \"%(group_name)s\" 的版本库"
 
-#: kallithea/model/validators.py:233
+#: kallithea/model/validators.py:230
 msgid "Invalid characters (non-ascii) in password"
 msgstr "密码含有无效(非ASCII)字符"
 
-#: kallithea/model/validators.py:248
+#: kallithea/model/validators.py:245
 msgid "Invalid old password"
 msgstr ""
 
-#: kallithea/model/validators.py:264
+#: kallithea/model/validators.py:261
 msgid "Passwords do not match"
 msgstr "密码不符"
 
-#: kallithea/model/validators.py:279
+#: kallithea/model/validators.py:276
 #, fuzzy
 msgid "Invalid username or password"
 msgstr "无效密码"
 
-#: kallithea/model/validators.py:313
+#: kallithea/model/validators.py:310
 #, fuzzy, python-format
 msgid "Repository name %(repo)s is not allowed"
 msgstr "版本库名称不能为%(repo)s"
 
-#: kallithea/model/validators.py:315
+#: kallithea/model/validators.py:312
 #, python-format
 msgid "Repository named %(repo)s already exists"
 msgstr "已经存在版本库%(repo)s"
 
-#: kallithea/model/validators.py:316
+#: kallithea/model/validators.py:313
 #, python-format
 msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
 msgstr "版本库组 \"%(group)s\" 中已经存在版本库 \"%(repo)s\""
 
-#: kallithea/model/validators.py:318
+#: kallithea/model/validators.py:315
 #, python-format
 msgid "Repository group with name \"%(repo)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:404
+#: kallithea/model/validators.py:401
 #, fuzzy
 msgid "Invalid repository URL"
 msgstr "私有版本库"
 
-#: kallithea/model/validators.py:405
+#: kallithea/model/validators.py:402
 msgid ""
 "Invalid repository URL. It must be a valid http, https, ssh, svn+http or "
 "svn+https URL"
 msgstr ""
 
-#: kallithea/model/validators.py:430
+#: kallithea/model/validators.py:427
 msgid "Fork has to be the same type as parent"
 msgstr "复刻版本库必须和父版本库类型相同"
 
-#: kallithea/model/validators.py:445
+#: kallithea/model/validators.py:442
 msgid "You don't have permissions to create repository in this group"
 msgstr "没有在该版本库组中创建版本库的权限"
 
-#: kallithea/model/validators.py:447
+#: kallithea/model/validators.py:444
 msgid "no permission to create repository in root location"
 msgstr ""
 
-#: kallithea/model/validators.py:497
+#: kallithea/model/validators.py:494
 msgid "You don't have permissions to create a group in this location"
 msgstr ""
 
-#: kallithea/model/validators.py:537
+#: kallithea/model/validators.py:534
 msgid "This username or user group name is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:630
+#: kallithea/model/validators.py:627
 msgid "This is not a valid path"
 msgstr "不是一个合法的路径"
 
-#: kallithea/model/validators.py:647
+#: kallithea/model/validators.py:644
 #, fuzzy
 msgid "This email address is already in use"
 msgstr "该邮件地址已被使用"
 
-#: kallithea/model/validators.py:667
+#: kallithea/model/validators.py:664
 #, fuzzy, python-format
 msgid "Email address \"%(email)s\" not found"
 msgstr "邮件地址\"%(email)s\"不存在"
 
-#: kallithea/model/validators.py:704
+#: kallithea/model/validators.py:701
 msgid ""
 "The LDAP Login attribute of the CN must be specified - this is the name "
 "of the attribute that is equivalent to \"username\""
 msgstr "LDAP 登陆属性的 CN 必须指定 - 这个名字作为用户名"
 
-#: kallithea/model/validators.py:716
+#: kallithea/model/validators.py:713
 msgid "Please enter a valid IPv4 or IPv6 address"
 msgstr ""
 
-#: kallithea/model/validators.py:717
+#: kallithea/model/validators.py:714
 #, python-format
 msgid ""
 "The network size (bits) must be within the range of 0-32 (not %(bits)r)"
 msgstr ""
 
-#: kallithea/model/validators.py:750
+#: kallithea/model/validators.py:747
 msgid "Key name can only consist of letters, underscore, dash or numbers"
 msgstr ""
 
-#: kallithea/model/validators.py:764
+#: kallithea/model/validators.py:761
 msgid "Filename cannot be inside a directory"
 msgstr ""
 
-#: kallithea/model/validators.py:780
+#: kallithea/model/validators.py:777
 #, python-format
 msgid "Plugins %(loaded)s and %(next_to_load)s both export the same name"
 msgstr ""
@@ -1898,7 +1896,7 @@
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:60
 #: kallithea/templates/email_templates/pull_request.html:37
 #: kallithea/templates/forks/fork.html:34
-#: kallithea/templates/index_base.html:58
+#: kallithea/templates/index_base.html:59
 #: kallithea/templates/pullrequests/pullrequest.html:33
 #: kallithea/templates/pullrequests/pullrequest_show.html:38
 #: kallithea/templates/pullrequests/pullrequest_show.html:59
@@ -1906,14 +1904,14 @@
 msgid "Description"
 msgstr "描述"
 
-#: kallithea/templates/index_base.html:60
+#: kallithea/templates/index_base.html:61
 msgid "Last Change"
 msgstr "最后修改"
 
 #: kallithea/templates/admin/my_account/my_account_repos.html:15
 #: kallithea/templates/admin/my_account/my_account_watched.html:15
 #: kallithea/templates/admin/repos/repos.html:41
-#: kallithea/templates/index_base.html:62
+#: kallithea/templates/index_base.html:63
 msgid "Tip"
 msgstr "Tip"
 
@@ -1923,7 +1921,7 @@
 #: kallithea/templates/admin/repos/repos.html:42
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:8
 #: kallithea/templates/admin/user_groups/user_groups.html:42
-#: kallithea/templates/index_base.html:63
+#: kallithea/templates/index_base.html:64
 #: kallithea/templates/pullrequests/pullrequest_data.html:16
 #: kallithea/templates/pullrequests/pullrequest_show.html:124
 #: kallithea/templates/pullrequests/pullrequest_show.html:219
@@ -1947,7 +1945,7 @@
 #: kallithea/templates/admin/users/user_edit_profile.html:18
 #: kallithea/templates/admin/users/users.html:37
 #: kallithea/templates/base/base.html:364
-#: kallithea/templates/email_templates/registration.html:11
+#: kallithea/templates/email_templates/registration.html:12
 #: kallithea/templates/login.html:28 kallithea/templates/register.html:31
 msgid "Username"
 msgstr "帐号"
@@ -2076,7 +2074,7 @@
 #: kallithea/templates/admin/settings/settings.html:31
 #: kallithea/templates/admin/users/user_add.html:62
 #: kallithea/templates/admin/users/user_edit_profile.html:25
-#: kallithea/templates/email_templates/registration.html:33
+#: kallithea/templates/email_templates/registration.html:34
 #: kallithea/templates/register.html:66
 msgid "Email"
 msgstr "电子邮件"
@@ -2326,7 +2324,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/gists/index.html:51
-#: kallithea/templates/data_table/_dt_elements.html:78
+#: kallithea/templates/data_table/_dt_elements.html:84
 msgid "Created"
 msgstr ""
 
@@ -2410,13 +2408,13 @@
 #: kallithea/templates/admin/users/user_edit_ips.html:21
 #: kallithea/templates/changeset/changeset_file_comment.html:30
 #: kallithea/templates/changeset/changeset_file_comment.html:121
-#: kallithea/templates/data_table/_dt_elements.html:69
-#: kallithea/templates/data_table/_dt_elements.html:89
-#: kallithea/templates/data_table/_dt_elements.html:91
-#: kallithea/templates/data_table/_dt_elements.html:101
-#: kallithea/templates/data_table/_dt_elements.html:103
-#: kallithea/templates/data_table/_dt_elements.html:120
-#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:75
+#: kallithea/templates/data_table/_dt_elements.html:95
+#: kallithea/templates/data_table/_dt_elements.html:97
+#: kallithea/templates/data_table/_dt_elements.html:107
+#: kallithea/templates/data_table/_dt_elements.html:109
+#: kallithea/templates/data_table/_dt_elements.html:126
+#: kallithea/templates/data_table/_dt_elements.html:128
 #: kallithea/templates/files/files_source.html:35
 #: kallithea/templates/files/files_source.html:38
 #: kallithea/templates/files/files_source.html:41
@@ -2432,14 +2430,14 @@
 #: kallithea/templates/base/perms_summary.html:44
 #: kallithea/templates/base/perms_summary.html:81
 #: kallithea/templates/base/perms_summary.html:83
-#: kallithea/templates/data_table/_dt_elements.html:63
-#: kallithea/templates/data_table/_dt_elements.html:64
-#: kallithea/templates/data_table/_dt_elements.html:85
-#: kallithea/templates/data_table/_dt_elements.html:86
-#: kallithea/templates/data_table/_dt_elements.html:97
-#: kallithea/templates/data_table/_dt_elements.html:98
-#: kallithea/templates/data_table/_dt_elements.html:116
-#: kallithea/templates/data_table/_dt_elements.html:117
+#: kallithea/templates/data_table/_dt_elements.html:69
+#: kallithea/templates/data_table/_dt_elements.html:70
+#: kallithea/templates/data_table/_dt_elements.html:91
+#: kallithea/templates/data_table/_dt_elements.html:92
+#: kallithea/templates/data_table/_dt_elements.html:103
+#: kallithea/templates/data_table/_dt_elements.html:104
+#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:123
 #: kallithea/templates/files/diff_2way.html:56
 #: kallithea/templates/files/files_source.html:37
 #: kallithea/templates/files/files_source.html:40
@@ -2757,7 +2755,7 @@
 #: kallithea/templates/admin/permissions/permissions_globals.html:27
 #: kallithea/templates/admin/repos/repo_add_base.html:28
 #: kallithea/templates/admin/repos/repo_edit_settings.html:33
-#: kallithea/templates/data_table/_dt_elements.html:134
+#: kallithea/templates/data_table/_dt_elements.html:140
 #: kallithea/templates/forks/fork.html:42
 msgid "Repository group"
 msgstr "版本库组"
@@ -2779,7 +2777,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:40
-#: kallithea/templates/data_table/_dt_elements.html:141
+#: kallithea/templates/data_table/_dt_elements.html:147
 msgid "User group"
 msgstr ""
 
@@ -2953,7 +2951,7 @@
 msgstr "创建于"
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:121
+#: kallithea/templates/data_table/_dt_elements.html:127
 #, python-format
 msgid "Confirm to delete this group: %s with %s repository"
 msgid_plural "Confirm to delete this group: %s with %s repositories"
@@ -3127,14 +3125,10 @@
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit.html:37
-msgid "Caches"
-msgstr ""
+msgid "Remote"
+msgstr "远程"
 
 #: kallithea/templates/admin/repos/repo_edit.html:40
-msgid "Remote"
-msgstr "远程"
-
-#: kallithea/templates/admin/repos/repo_edit.html:43
 #: kallithea/templates/summary/statistics.html:8
 #: kallithea/templates/summary/summary.html:169
 #: kallithea/templates/summary/summary.html:170
@@ -3177,7 +3171,7 @@
 msgstr "任何人都可以在公共日志上看到这个版本库上的所有动作"
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:46
-#: kallithea/templates/data_table/_dt_elements.html:68
+#: kallithea/templates/data_table/_dt_elements.html:74
 #, python-format
 msgid "Confirm to delete this repository: %s"
 msgstr "确认删除版本库:%s"
@@ -3208,46 +3202,14 @@
 "it or restore it."
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:4
-#, fuzzy
-msgid "Invalidate Repository Cache"
-msgstr "清除版本库缓存"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:6
-#, fuzzy
-msgid ""
-"Manually invalidate cache for this repository. On first access, the "
-"repository will be cached again."
-msgstr "手动清除版本库缓存。之后第一次访问的时候将重建缓存"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:9
-#, fuzzy
-msgid "List of Cached Values"
-msgstr "缓存值列表"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:12
-msgid "Prefix"
-msgstr "前缀"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:13
+#: kallithea/templates/admin/repos/repo_edit_fields.html:6
+msgid "Label"
+msgstr ""
+
 #: kallithea/templates/admin/repos/repo_edit_fields.html:7
 msgid "Key"
 msgstr "键"
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:14
-#: kallithea/templates/admin/user_groups/user_group_add.html:40
-#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
-#: kallithea/templates/admin/user_groups/user_groups.html:41
-#: kallithea/templates/admin/users/user_add.html:69
-#: kallithea/templates/admin/users/user_edit_profile.html:74
-#: kallithea/templates/admin/users/users.html:42
-msgid "Active"
-msgstr "启用"
-
-#: kallithea/templates/admin/repos/repo_edit_fields.html:6
-msgid "Label"
-msgstr ""
-
 #: kallithea/templates/admin/repos/repo_edit_fields.html:20
 #, python-format
 msgid "Confirm to delete this field: %s"
@@ -3787,6 +3749,15 @@
 msgid "Short, optional description for this user group."
 msgstr ""
 
+#: kallithea/templates/admin/user_groups/user_group_add.html:40
+#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
+#: kallithea/templates/admin/user_groups/user_groups.html:41
+#: kallithea/templates/admin/users/user_add.html:69
+#: kallithea/templates/admin/users/user_edit_profile.html:74
+#: kallithea/templates/admin/users/users.html:42
+msgid "Active"
+msgstr "启用"
+
 #: kallithea/templates/admin/user_groups/user_group_edit.html:5
 #, python-format
 msgid "%s user group settings"
@@ -3809,7 +3780,7 @@
 msgstr "成员"
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:19
-#: kallithea/templates/data_table/_dt_elements.html:102
+#: kallithea/templates/data_table/_dt_elements.html:108
 #, python-format
 msgid "Confirm to delete this user group: %s"
 msgstr ""
@@ -3884,7 +3855,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:90
+#: kallithea/templates/data_table/_dt_elements.html:96
 #, python-format
 msgid "Confirm to delete this user: %s"
 msgstr "确认删除用户:%s"
@@ -3987,10 +3958,12 @@
 msgstr "搜索"
 
 #: kallithea/templates/base/base.html:167
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Follow"
 msgstr ""
 
 #: kallithea/templates/base/base.html:168
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Unfollow"
 msgstr ""
 
@@ -4415,26 +4388,26 @@
 msgid "Merge"
 msgstr "合并"
 
-#: kallithea/templates/changeset/changeset.html:96
+#: kallithea/templates/changeset/changeset.html:95
 #, fuzzy
 msgid "Grafted from:"
 msgstr "创建于"
 
-#: kallithea/templates/changeset/changeset.html:102
+#: kallithea/templates/changeset/changeset.html:100
 msgid "Transplanted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:108
+#: kallithea/templates/changeset/changeset.html:106
 #, fuzzy
 msgid "Replaced by:"
 msgstr "创建于"
 
-#: kallithea/templates/changeset/changeset.html:122
+#: kallithea/templates/changeset/changeset.html:120
 #, fuzzy
 msgid "Preceded by:"
 msgstr "创建于"
 
-#: kallithea/templates/changeset/changeset.html:139
+#: kallithea/templates/changeset/changeset.html:137
 #: kallithea/templates/compare/compare_diff.html:59
 #: kallithea/templates/pullrequests/pullrequest_show.html:290
 #, python-format
@@ -4442,7 +4415,7 @@
 msgid_plural "%s files changed"
 msgstr[0] "修改%s个文件"
 
-#: kallithea/templates/changeset/changeset.html:141
+#: kallithea/templates/changeset/changeset.html:139
 #: kallithea/templates/compare/compare_diff.html:61
 #: kallithea/templates/pullrequests/pullrequest_show.html:292
 #, python-format
@@ -4450,8 +4423,8 @@
 msgid_plural "%s files changed with %s insertions and %s deletions"
 msgstr[0] "修改%s个文件包括%s行插入和%s行删除"
 
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
 #: kallithea/templates/pullrequests/pullrequest_show.html:333
@@ -4698,23 +4671,23 @@
 msgid "Repository creation in progress..."
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:42
+#: kallithea/templates/data_table/_dt_elements.html:48
 msgid "No changesets yet"
 msgstr "尚无任何修订集"
 
-#: kallithea/templates/data_table/_dt_elements.html:48
-#: kallithea/templates/data_table/_dt_elements.html:50
+#: kallithea/templates/data_table/_dt_elements.html:54
+#: kallithea/templates/data_table/_dt_elements.html:56
 #, python-format
 msgid "Subscribe to %s rss feed"
 msgstr "订阅%s的RSS"
 
-#: kallithea/templates/data_table/_dt_elements.html:56
-#: kallithea/templates/data_table/_dt_elements.html:58
+#: kallithea/templates/data_table/_dt_elements.html:62
+#: kallithea/templates/data_table/_dt_elements.html:64
 #, python-format
 msgid "Subscribe to %s atom feed"
 msgstr "订阅%s的Atom"
 
-#: kallithea/templates/data_table/_dt_elements.html:76
+#: kallithea/templates/data_table/_dt_elements.html:82
 msgid "Creating"
 msgstr ""
 
@@ -4752,6 +4725,13 @@
 msgid "by"
 msgstr ""
 
+#: kallithea/templates/email_templates/changeset_comment.html:36
+#: kallithea/templates/email_templates/pull_request_comment.html:43
+#, fuzzy
+#| msgid "Comment"
+msgid "View Comment"
+msgstr "评论"
+
 #: kallithea/templates/email_templates/comment.html:27
 #, fuzzy
 #| msgid "Status change"
@@ -4764,33 +4744,42 @@
 msgid "The pull request has been closed."
 msgstr "版本库未锁定"
 
-#: kallithea/templates/email_templates/password_reset.html:9
+#: kallithea/templates/email_templates/default.html:4
+msgid "Message"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:4
+#, fuzzy
+msgid "Password Reset Request"
+msgstr "确认新密码"
+
+#: kallithea/templates/email_templates/password_reset.html:10
 #, python-format
 msgid "Hello %s"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:16
+#: kallithea/templates/email_templates/password_reset.html:17
 #, fuzzy
 msgid "We have received a request to reset the password for your account."
 msgstr "我们收到重置你用户密码的请求。"
 
-#: kallithea/templates/email_templates/password_reset.html:25
+#: kallithea/templates/email_templates/password_reset.html:26
 msgid ""
 "This account is however managed outside this system and the password "
 "cannot be changed here."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:28
+#: kallithea/templates/email_templates/password_reset.html:29
 msgid "To set a new password, click the following link"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:33
+#: kallithea/templates/email_templates/password_reset.html:34
 msgid ""
 "Should you not be able to use the link above, please type the following "
 "code into the password reset form"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:44
+#: kallithea/templates/email_templates/password_reset.html:45
 msgid ""
 "If it weren't you who requested the password reset, just disregard this "
 "message."
@@ -4823,6 +4812,11 @@
 msgid "to"
 msgstr ""
 
+#: kallithea/templates/email_templates/pull_request.html:85
+#, fuzzy
+msgid "View Pull Request"
+msgstr "新建拉取请求"
+
 #: kallithea/templates/email_templates/pull_request_comment.html:4
 #, fuzzy, python-format
 #| msgid "%(user)s commented on pull request %(age)s"
@@ -4841,12 +4835,24 @@
 msgid "Comment on Pull Request %s \"%s\""
 msgstr "[评论]拉取请求"
 
-#: kallithea/templates/email_templates/registration.html:22
+#: kallithea/templates/email_templates/registration.html:5
+#, fuzzy
+#| msgid "Registration"
+msgid "New User Registration"
+msgstr "注册"
+
+#: kallithea/templates/email_templates/registration.html:23
 #, fuzzy
 #| msgid "Group name"
 msgid "Full Name"
 msgstr "组名"
 
+#: kallithea/templates/email_templates/registration.html:42
+#, fuzzy
+#| msgid "View this user here"
+msgid "View User Profile"
+msgstr "查看用户"
+
 #: kallithea/templates/files/diff_2way.html:15
 #, python-format
 msgid "%s File side-by-side diff"
@@ -5474,45 +5480,45 @@
 msgid "Stats gathered: "
 msgstr "已收集的统计: "
 
-#: kallithea/templates/summary/statistics.html:87
-#: kallithea/templates/summary/summary.html:371
+#: kallithea/templates/summary/statistics.html:85
+#: kallithea/templates/summary/summary.html:369
 msgid "files"
 msgstr "文件"
 
-#: kallithea/templates/summary/statistics.html:111
-#: kallithea/templates/summary/summary.html:401
+#: kallithea/templates/summary/statistics.html:109
+#: kallithea/templates/summary/summary.html:399
 msgid "Show more"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:405
+#: kallithea/templates/summary/statistics.html:395
 msgid "commits"
 msgstr "提交"
 
-#: kallithea/templates/summary/statistics.html:406
+#: kallithea/templates/summary/statistics.html:396
 msgid "files added"
 msgstr "文件已添加"
 
-#: kallithea/templates/summary/statistics.html:407
+#: kallithea/templates/summary/statistics.html:397
 msgid "files changed"
 msgstr "文件已更改"
 
-#: kallithea/templates/summary/statistics.html:408
+#: kallithea/templates/summary/statistics.html:398
 msgid "files removed"
 msgstr "文件已删除"
 
-#: kallithea/templates/summary/statistics.html:410
+#: kallithea/templates/summary/statistics.html:400
 msgid "commit"
 msgstr "提交"
 
-#: kallithea/templates/summary/statistics.html:411
+#: kallithea/templates/summary/statistics.html:401
 msgid "file added"
 msgstr "文件已添加"
 
-#: kallithea/templates/summary/statistics.html:412
+#: kallithea/templates/summary/statistics.html:402
 msgid "file changed"
 msgstr "文件已更改"
 
-#: kallithea/templates/summary/statistics.html:413
+#: kallithea/templates/summary/statistics.html:403
 msgid "file removed"
 msgstr "文件已删除"
 
@@ -5620,6 +5626,26 @@
 msgid "Download %s as %s"
 msgstr "下载%s为%s包"
 
+#~ msgid "An error occurred during cache invalidation"
+#~ msgstr "清除缓存时发生错误"
+
+#, fuzzy
+#~ msgid "Invalidate Repository Cache"
+#~ msgstr "清除版本库缓存"
+
+#, fuzzy
+#~ msgid ""
+#~ "Manually invalidate cache for this repository. On first access, the "
+#~ "repository will be cached again."
+#~ msgstr "手动清除版本库缓存。之后第一次访问的时候将重建缓存"
+
+#, fuzzy
+#~ msgid "List of Cached Values"
+#~ msgstr "缓存值列表"
+
+#~ msgid "Prefix"
+#~ msgstr "前缀"
+
 #~ msgid "This repository has been locked by %s on %s"
 #~ msgstr "版本库由%s于%s锁定"
 
@@ -5813,9 +5839,6 @@
 #~ msgid "The comment closed the pull request with status"
 #~ msgstr "[评论]拉取请求"
 
-#~ msgid "View this user here"
-#~ msgstr "查看用户"
-
 #~ msgid "No comments."
 #~ msgstr "%d条评论"
 
--- a/kallithea/i18n/zh_TW/LC_MESSAGES/kallithea.po	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/i18n/zh_TW/LC_MESSAGES/kallithea.po	Mon May 04 19:24:04 2020 +0200
@@ -4,7 +4,7 @@
 msgstr ""
 "Project-Id-Version: Kallithea 0.3\n"
 "Report-Msgid-Bugs-To: translations@kallithea-scm.org\n"
-"POT-Creation-Date: 2020-01-05 04:10+0100\n"
+"POT-Creation-Date: 2020-05-04 19:12+0200\n"
 "PO-Revision-Date: 2017-03-10 18:26+0000\n"
 "Last-Translator: mao <mao@lins.fju.edu.tw>\n"
 "Language-Team: Chinese (Traditional) <https://hosted.weblate.org/projects/"
@@ -18,14 +18,14 @@
 "Generated-By: Babel 1.3\n"
 
 #: kallithea/controllers/changelog.py:67
-#: kallithea/controllers/pullrequests.py:250 kallithea/lib/base.py:602
+#: kallithea/controllers/pullrequests.py:247 kallithea/lib/base.py:602
 msgid "There are no changesets yet"
 msgstr ""
 
 #: kallithea/controllers/admin/permissions.py:64
 #: kallithea/controllers/admin/permissions.py:68
 #: kallithea/controllers/admin/permissions.py:72
-#: kallithea/controllers/changelog.py:137
+#: kallithea/controllers/changelog.py:136
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:7
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:88
 #: kallithea/templates/admin/repos/repo_edit_permissions.html:7
@@ -34,37 +34,37 @@
 msgid "None"
 msgstr "無"
 
-#: kallithea/controllers/changelog.py:140 kallithea/controllers/files.py:189
+#: kallithea/controllers/changelog.py:139 kallithea/controllers/files.py:189
 msgid "(closed)"
 msgstr "(已關閉)"
 
-#: kallithea/controllers/changeset.py:81
+#: kallithea/controllers/changeset.py:82
 msgid "Show whitespace"
 msgstr "顯示空格"
 
-#: kallithea/controllers/changeset.py:88
-#: kallithea/controllers/changeset.py:95
+#: kallithea/controllers/changeset.py:89
+#: kallithea/controllers/changeset.py:96
 #: kallithea/templates/files/diff_2way.html:55
 msgid "Ignore whitespace"
 msgstr "忽略空格"
 
-#: kallithea/controllers/changeset.py:161
+#: kallithea/controllers/changeset.py:162
 #, python-format
 msgid "Increase diff context to %(num)s lines"
 msgstr "增加 diff 上下文至 %(num)s 行"
 
-#: kallithea/controllers/changeset.py:201
+#: kallithea/controllers/changeset.py:202
 #, fuzzy
 msgid "No permission to change status"
 msgstr "尚未有任何變更"
 
-#: kallithea/controllers/changeset.py:212
+#: kallithea/controllers/changeset.py:213
 #, fuzzy, python-format
 msgid "Successfully deleted pull request %s"
 msgstr "成功遞交至 %s"
 
 #: kallithea/controllers/changeset.py:319 kallithea/controllers/files.py:89
-#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:700
+#: kallithea/controllers/files.py:109 kallithea/controllers/files.py:697
 msgid "Such revision does not exist for this repository"
 msgstr ""
 
@@ -78,61 +78,61 @@
 msgid "Cannot compare repositories of different types"
 msgstr ""
 
-#: kallithea/controllers/compare.py:246
+#: kallithea/controllers/compare.py:247
 msgid "Cannot show empty diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:248
+#: kallithea/controllers/compare.py:249
 msgid "No ancestor found for merge diff"
 msgstr ""
 
-#: kallithea/controllers/compare.py:252
+#: kallithea/controllers/compare.py:253
 msgid "Multiple merge ancestors found for merge compare"
 msgstr ""
 
-#: kallithea/controllers/compare.py:268
+#: kallithea/controllers/compare.py:269
 msgid "Cannot compare repositories without using common ancestor"
 msgstr ""
 
-#: kallithea/controllers/error.py:71
+#: kallithea/controllers/error.py:67
 msgid "No response"
 msgstr "未回應"
 
-#: kallithea/controllers/error.py:72
+#: kallithea/controllers/error.py:68
 msgid "Unknown error"
 msgstr ""
 
-#: kallithea/controllers/error.py:85
+#: kallithea/controllers/error.py:81
 msgid ""
 "The request could not be understood by the server due to malformed syntax."
 msgstr ""
 
+#: kallithea/controllers/error.py:84
+msgid "Unauthorized access to resource"
+msgstr ""
+
+#: kallithea/controllers/error.py:86
+msgid "You don't have permission to view this page"
+msgstr "您沒有權限瀏覽這個頁面"
+
 #: kallithea/controllers/error.py:88
-msgid "Unauthorized access to resource"
-msgstr ""
+msgid "The resource could not be found"
+msgstr "找不到這個資源"
 
 #: kallithea/controllers/error.py:90
-msgid "You don't have permission to view this page"
-msgstr "您沒有權限瀏覽這個頁面"
-
-#: kallithea/controllers/error.py:92
-msgid "The resource could not be found"
-msgstr "找不到這個資源"
-
-#: kallithea/controllers/error.py:94
 msgid ""
 "The server encountered an unexpected condition which prevented it from "
 "fulfilling the request."
 msgstr ""
 
-#: kallithea/controllers/feed.py:63
+#: kallithea/controllers/feed.py:59
 #, python-format
 msgid "%s committed on %s"
 msgstr "%s 評論於 %s"
 
-#: kallithea/controllers/feed.py:88
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/controllers/feed.py:84
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/compare/compare_diff.html:95
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
@@ -140,12 +140,12 @@
 msgid "Changeset was too big and was cut off..."
 msgstr ""
 
-#: kallithea/controllers/feed.py:111 kallithea/controllers/feed.py:140
+#: kallithea/controllers/feed.py:107
 #, python-format
 msgid "%s %s feed"
 msgstr ""
 
-#: kallithea/controllers/feed.py:113 kallithea/controllers/feed.py:142
+#: kallithea/controllers/feed.py:109
 #, python-format
 msgid "Changes on %s repository"
 msgstr "修改於版本庫 %s"
@@ -165,103 +165,103 @@
 msgid "%s at %s"
 msgstr ""
 
-#: kallithea/controllers/files.py:296
+#: kallithea/controllers/files.py:295
 msgid "You can only delete files with revision being a valid branch"
 msgstr ""
 
-#: kallithea/controllers/files.py:307
+#: kallithea/controllers/files.py:306
 #, python-format
 msgid "Deleted file %s via Kallithea"
 msgstr ""
 
-#: kallithea/controllers/files.py:331
+#: kallithea/controllers/files.py:330
 #, python-format
 msgid "Successfully deleted file %s"
 msgstr ""
 
-#: kallithea/controllers/files.py:335 kallithea/controllers/files.py:394
-#: kallithea/controllers/files.py:469
+#: kallithea/controllers/files.py:334 kallithea/controllers/files.py:392
+#: kallithea/controllers/files.py:467
 msgid "Error occurred during commit"
 msgstr ""
 
-#: kallithea/controllers/files.py:350
+#: kallithea/controllers/files.py:349
 msgid "You can only edit files with revision being a valid branch"
 msgstr ""
 
-#: kallithea/controllers/files.py:364
+#: kallithea/controllers/files.py:363
 #, python-format
 msgid "Edited file %s via Kallithea"
 msgstr ""
 
-#: kallithea/controllers/files.py:380
+#: kallithea/controllers/files.py:378
 msgid "No changes"
 msgstr "沒有修改"
 
-#: kallithea/controllers/files.py:390 kallithea/controllers/files.py:458
+#: kallithea/controllers/files.py:388 kallithea/controllers/files.py:456
 #, python-format
 msgid "Successfully committed to %s"
 msgstr "成功遞交至 %s"
 
-#: kallithea/controllers/files.py:409
+#: kallithea/controllers/files.py:407
 msgid "Added file via Kallithea"
 msgstr ""
 
-#: kallithea/controllers/files.py:430
+#: kallithea/controllers/files.py:428
 msgid "No content"
 msgstr ""
 
-#: kallithea/controllers/files.py:434
+#: kallithea/controllers/files.py:432
 msgid "No filename"
 msgstr ""
 
-#: kallithea/controllers/files.py:461
+#: kallithea/controllers/files.py:459
 msgid "Location must be relative path and must not contain .. in path"
 msgstr ""
 
-#: kallithea/controllers/files.py:493
+#: kallithea/controllers/files.py:491
 msgid "Downloads disabled"
 msgstr ""
 
-#: kallithea/controllers/files.py:504
+#: kallithea/controllers/files.py:502
 #, python-format
 msgid "Unknown revision %s"
 msgstr "未知修訂 %s"
 
-#: kallithea/controllers/files.py:506
+#: kallithea/controllers/files.py:504
 msgid "Empty repository"
 msgstr "空的版本庫"
 
-#: kallithea/controllers/files.py:508
+#: kallithea/controllers/files.py:506
 msgid "Unknown archive type"
 msgstr "未知的存檔類型"
 
-#: kallithea/controllers/files.py:729
+#: kallithea/controllers/files.py:726
 #: kallithea/templates/changeset/changeset_range.html:9
 #: kallithea/templates/email_templates/pull_request.html:64
 #: kallithea/templates/pullrequests/pullrequest.html:84
 msgid "Changesets"
 msgstr "變更"
 
-#: kallithea/controllers/files.py:730
-#: kallithea/controllers/pullrequests.py:182 kallithea/model/scm.py:676
+#: kallithea/controllers/files.py:727
+#: kallithea/controllers/pullrequests.py:174 kallithea/model/scm.py:663
 msgid "Branches"
 msgstr "分支"
 
-#: kallithea/controllers/files.py:731
-#: kallithea/controllers/pullrequests.py:183 kallithea/model/scm.py:687
+#: kallithea/controllers/files.py:728
+#: kallithea/controllers/pullrequests.py:175 kallithea/model/scm.py:674
 msgid "Tags"
 msgstr "標籤"
 
-#: kallithea/controllers/forks.py:174
+#: kallithea/controllers/forks.py:175
 #, python-format
 msgid "An error occurred during repository forking %s"
 msgstr ""
 
-#: kallithea/controllers/home.py:79
+#: kallithea/controllers/home.py:77
 msgid "Groups"
 msgstr ""
 
-#: kallithea/controllers/home.py:89
+#: kallithea/controllers/home.py:87
 #: kallithea/templates/admin/repo_groups/repo_group_edit_perms.html:90
 #: kallithea/templates/admin/repos/repo_add.html:12
 #: kallithea/templates/admin/repos/repo_add.html:16
@@ -273,48 +273,52 @@
 msgid "Repositories"
 msgstr "版本庫"
 
-#: kallithea/controllers/home.py:122
+#: kallithea/controllers/home.py:119
 #: kallithea/templates/files/files_add.html:32
 #: kallithea/templates/files/files_delete.html:23
 #: kallithea/templates/files/files_edit.html:32
 msgid "Branch"
 msgstr ""
 
-#: kallithea/controllers/home.py:128
+#: kallithea/controllers/home.py:125
 msgid "Closed Branches"
 msgstr ""
 
-#: kallithea/controllers/home.py:134
+#: kallithea/controllers/home.py:131
 msgid "Tag"
 msgstr ""
 
-#: kallithea/controllers/home.py:140
+#: kallithea/controllers/home.py:137
 msgid "Bookmark"
 msgstr ""
 
-#: kallithea/controllers/journal.py:112 kallithea/controllers/journal.py:154
+#: kallithea/controllers/journal.py:146 kallithea/controllers/journal.py:157
 #: kallithea/templates/journal/public_journal.html:4
 #: kallithea/templates/journal/public_journal.html:18
 msgid "Public Journal"
 msgstr "開放日誌"
 
-#: kallithea/controllers/journal.py:116 kallithea/controllers/journal.py:158
+#: kallithea/controllers/journal.py:150 kallithea/controllers/journal.py:161
 #: kallithea/templates/base/base.html:290
 #: kallithea/templates/journal/journal.html:5
 #: kallithea/templates/journal/journal.html:13
 msgid "Journal"
 msgstr "日誌"
 
-#: kallithea/controllers/login.py:140 kallithea/controllers/login.py:185
+#: kallithea/controllers/login.py:109
+msgid "Authentication failed."
+msgstr ""
+
+#: kallithea/controllers/login.py:142 kallithea/controllers/login.py:187
 msgid "Bad captcha"
 msgstr ""
 
-#: kallithea/controllers/login.py:146
+#: kallithea/controllers/login.py:148
 #, python-format
 msgid "You have successfully registered with %s"
 msgstr ""
 
-#: kallithea/controllers/login.py:190
+#: kallithea/controllers/login.py:192
 msgid "A password reset confirmation code has been sent"
 msgstr "密碼重設的確認碼已寄出"
 
@@ -327,226 +331,226 @@
 msgid "Successfully updated password"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:68
+#: kallithea/controllers/pullrequests.py:67
 #, python-format
 msgid "Invalid reviewer \"%s\" specified"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:131
+#: kallithea/controllers/pullrequests.py:123
 #, python-format
 msgid "%s (closed)"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:158
+#: kallithea/controllers/pullrequests.py:150
 #: kallithea/templates/changeset/changeset.html:12
 msgid "Changeset"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:179
+#: kallithea/controllers/pullrequests.py:171
 msgid "Special"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:180
+#: kallithea/controllers/pullrequests.py:172
 msgid "Peer branches"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:181 kallithea/model/scm.py:682
+#: kallithea/controllers/pullrequests.py:173 kallithea/model/scm.py:669
 msgid "Bookmarks"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:318
+#: kallithea/controllers/pullrequests.py:315
 #, python-format
 msgid "Error creating pull request: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:345
-#: kallithea/controllers/pullrequests.py:368
+#: kallithea/controllers/pullrequests.py:342
+#: kallithea/controllers/pullrequests.py:365
 msgid "Error occurred while creating pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:350
+#: kallithea/controllers/pullrequests.py:347
 msgid "Successfully opened new pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:373
+#: kallithea/controllers/pullrequests.py:370
 msgid "New pull request iteration created"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:401
+#: kallithea/controllers/pullrequests.py:398
 #, python-format
 msgid "Meanwhile, the following reviewers have been added: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:405
+#: kallithea/controllers/pullrequests.py:402
 #, python-format
 msgid "Meanwhile, the following reviewers have been removed: %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:421
-#: kallithea/model/pull_request.py:232
+#: kallithea/controllers/pullrequests.py:418
+#: kallithea/model/pull_request.py:230
 msgid "No description"
 msgstr "無描述"
 
-#: kallithea/controllers/pullrequests.py:430
+#: kallithea/controllers/pullrequests.py:427
 msgid "Pull request updated"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:443
+#: kallithea/controllers/pullrequests.py:440
 msgid "Successfully deleted pull request"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:479
+#: kallithea/controllers/pullrequests.py:476
 #, python-format
 msgid "Revision %s not found in %s"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:506
+#: kallithea/controllers/pullrequests.py:504
 #, python-format
 msgid "Error: changesets not found when displaying pull request from %s."
 msgstr ""
 
+#: kallithea/controllers/pullrequests.py:518
+#, python-format
+msgid "This pull request has already been merged to %s."
+msgstr ""
+
 #: kallithea/controllers/pullrequests.py:520
-#, python-format
-msgid "This pull request has already been merged to %s."
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:522
 msgid "This pull request has been closed and can not be updated."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:546
+#: kallithea/controllers/pullrequests.py:539
 #, python-format
 msgid "The following additional changes are available on %s:"
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:548
-#: kallithea/controllers/pullrequests.py:552
+#: kallithea/controllers/pullrequests.py:541
+#: kallithea/controllers/pullrequests.py:545
 msgid "No additional changesets found for iterating on this pull request."
 msgstr ""
 
+#: kallithea/controllers/pullrequests.py:553
+#, python-format
+msgid "Note: Branch %s has another head: %s."
+msgstr ""
+
 #: kallithea/controllers/pullrequests.py:560
-#, python-format
-msgid "Note: Branch %s has another head: %s."
-msgstr ""
-
-#: kallithea/controllers/pullrequests.py:567
 msgid "Git pull requests don't support iterating yet."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:569
+#: kallithea/controllers/pullrequests.py:562
 #, python-format
 msgid ""
 "Error: some changesets not found when displaying pull request from %s."
 msgstr ""
 
-#: kallithea/controllers/pullrequests.py:593
+#: kallithea/controllers/pullrequests.py:586
 msgid "The diff can't be shown - the PR revisions could not be found."
 msgstr ""
 
-#: kallithea/controllers/search.py:136
+#: kallithea/controllers/search.py:132
 msgid "Invalid search query. Try quoting it."
 msgstr "無效的查詢。請使用跳脫字元。"
 
-#: kallithea/controllers/search.py:140
+#: kallithea/controllers/search.py:136
 msgid "The server has no search index."
 msgstr ""
 
-#: kallithea/controllers/search.py:143
+#: kallithea/controllers/search.py:139
 msgid "An error occurred during search operation."
 msgstr ""
 
-#: kallithea/controllers/summary.py:168
-#: kallithea/templates/summary/summary.html:412
+#: kallithea/controllers/summary.py:169
+#: kallithea/templates/summary/summary.html:410
 msgid "No data ready yet"
 msgstr ""
 
-#: kallithea/controllers/summary.py:171
+#: kallithea/controllers/summary.py:172
 #: kallithea/templates/summary/summary.html:97
 msgid "Statistics are disabled for this repository"
 msgstr "這個版本庫的統計功能已停用"
 
-#: kallithea/controllers/admin/auth_settings.py:137
+#: kallithea/controllers/admin/auth_settings.py:136
 msgid "Auth settings updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/auth_settings.py:148
+#: kallithea/controllers/admin/auth_settings.py:147
 msgid "error occurred during update of auth settings"
 msgstr ""
 
-#: kallithea/controllers/admin/defaults.py:75
+#: kallithea/controllers/admin/defaults.py:74
 msgid "Default settings updated successfully"
 msgstr ""
 
-#: kallithea/controllers/admin/defaults.py:90
+#: kallithea/controllers/admin/defaults.py:89
 msgid "Error occurred during update of defaults"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:59
 #: kallithea/controllers/admin/my_account.py:232
-#: kallithea/controllers/admin/users.py:248
+#: kallithea/controllers/admin/users.py:246
 msgid "Forever"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:60
 #: kallithea/controllers/admin/my_account.py:233
-#: kallithea/controllers/admin/users.py:249
+#: kallithea/controllers/admin/users.py:247
 msgid "5 minutes"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:61
 #: kallithea/controllers/admin/my_account.py:234
-#: kallithea/controllers/admin/users.py:250
+#: kallithea/controllers/admin/users.py:248
 msgid "1 hour"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:62
 #: kallithea/controllers/admin/my_account.py:235
-#: kallithea/controllers/admin/users.py:251
+#: kallithea/controllers/admin/users.py:249
 msgid "1 day"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:63
 #: kallithea/controllers/admin/my_account.py:236
-#: kallithea/controllers/admin/users.py:252
+#: kallithea/controllers/admin/users.py:250
 msgid "1 month"
 msgstr ""
 
 #: kallithea/controllers/admin/gists.py:67
 #: kallithea/controllers/admin/my_account.py:238
-#: kallithea/controllers/admin/users.py:254
+#: kallithea/controllers/admin/users.py:252
 #: kallithea/templates/admin/my_account/my_account_api_keys.html:65
 #: kallithea/templates/admin/users/user_edit_api_keys.html:65
 msgid "Lifetime"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:142
+#: kallithea/controllers/admin/gists.py:148
 msgid "Error occurred during gist creation"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:158
+#: kallithea/controllers/admin/gists.py:164
 #, python-format
 msgid "Deleted gist %s"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:198
+#: kallithea/controllers/admin/gists.py:207
 msgid "Unmodified"
 msgstr "未修改"
 
-#: kallithea/controllers/admin/gists.py:228
+#: kallithea/controllers/admin/gists.py:237
 msgid "Successfully updated gist content"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:233
+#: kallithea/controllers/admin/gists.py:242
 msgid "Successfully updated gist data"
 msgstr ""
 
-#: kallithea/controllers/admin/gists.py:236
+#: kallithea/controllers/admin/gists.py:245
 #, python-format
 msgid "Error occurred during update of gist %s"
 msgstr ""
 
-#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:211
-#: kallithea/model/user.py:232
+#: kallithea/controllers/admin/my_account.py:70 kallithea/model/user.py:205
+#: kallithea/model/user.py:226
 msgid "You can't edit this user since it's crucial for entire application"
 msgstr ""
 
@@ -555,7 +559,7 @@
 msgstr "您的帳號已更新完成"
 
 #: kallithea/controllers/admin/my_account.py:134
-#: kallithea/controllers/admin/users.py:181
+#: kallithea/controllers/admin/users.py:179
 #, python-format
 msgid "Error occurred during update of user %s"
 msgstr ""
@@ -565,44 +569,44 @@
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:209
-#: kallithea/controllers/admin/users.py:367
+#: kallithea/controllers/admin/users.py:365
 #, python-format
 msgid "Added email %s to user"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:215
-#: kallithea/controllers/admin/users.py:373
+#: kallithea/controllers/admin/users.py:371
 msgid "An error occurred during email saving"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:224
-#: kallithea/controllers/admin/users.py:383
+#: kallithea/controllers/admin/users.py:381
 msgid "Removed email from user"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:248
-#: kallithea/controllers/admin/users.py:271
+#: kallithea/controllers/admin/users.py:269
 msgid "API key successfully created"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:257
-#: kallithea/controllers/admin/users.py:281
+#: kallithea/controllers/admin/users.py:279
 msgid "API key successfully reset"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:261
-#: kallithea/controllers/admin/users.py:285
+#: kallithea/controllers/admin/users.py:283
 msgid "API key successfully deleted"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:281
-#: kallithea/controllers/admin/users.py:456
+#: kallithea/controllers/admin/users.py:454
 #, python-format
 msgid "SSH key %s successfully added"
 msgstr ""
 
 #: kallithea/controllers/admin/my_account.py:293
-#: kallithea/controllers/admin/users.py:470
+#: kallithea/controllers/admin/users.py:468
 #, fuzzy
 msgid "SSH key successfully deleted"
 msgstr "成功遞交至 %s"
@@ -679,11 +683,11 @@
 msgid "Allowed with automatic account activation"
 msgstr ""
 
-#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1673
+#: kallithea/controllers/admin/permissions.py:85 kallithea/model/db.py:1578
 msgid "Manual activation of external account"
 msgstr ""
 
-#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1674
+#: kallithea/controllers/admin/permissions.py:86 kallithea/model/db.py:1579
 msgid "Automatic activation of external account"
 msgstr ""
 
@@ -705,340 +709,332 @@
 msgid "Error occurred during update of permissions"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:172
+#: kallithea/controllers/admin/repo_groups.py:165
 #, python-format
 msgid "Error occurred during creation of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:177
+#: kallithea/controllers/admin/repo_groups.py:172
 #, python-format
 msgid "Created repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:224
+#: kallithea/controllers/admin/repo_groups.py:219
 #, python-format
 msgid "Updated repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:240
+#: kallithea/controllers/admin/repo_groups.py:235
 #, python-format
 msgid "Error occurred during update of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:250
+#: kallithea/controllers/admin/repo_groups.py:245
 #, python-format
 msgid "This group contains %s repositories and cannot be deleted"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:257
+#: kallithea/controllers/admin/repo_groups.py:252
 #, python-format
 msgid "This group contains %s subgroups and cannot be deleted"
 msgstr ""
 
+#: kallithea/controllers/admin/repo_groups.py:258
+#, python-format
+msgid "Removed repository group %s"
+msgstr ""
+
 #: kallithea/controllers/admin/repo_groups.py:263
 #, python-format
-msgid "Removed repository group %s"
-msgstr ""
-
-#: kallithea/controllers/admin/repo_groups.py:268
-#, python-format
 msgid "Error occurred during deletion of repository group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:352
-#: kallithea/controllers/admin/repo_groups.py:382
-#: kallithea/controllers/admin/user_groups.py:294
+#: kallithea/controllers/admin/repo_groups.py:347
+#: kallithea/controllers/admin/repo_groups.py:377
+#: kallithea/controllers/admin/user_groups.py:290
 msgid "Cannot revoke permission for yourself as admin"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:367
+#: kallithea/controllers/admin/repo_groups.py:362
 msgid "Repository group permissions updated"
 msgstr ""
 
-#: kallithea/controllers/admin/repo_groups.py:399
-#: kallithea/controllers/admin/repos.py:358
-#: kallithea/controllers/admin/user_groups.py:306
+#: kallithea/controllers/admin/repo_groups.py:394
+#: kallithea/controllers/admin/repos.py:357
+#: kallithea/controllers/admin/user_groups.py:302
 msgid "An error occurred during revoking of permission"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:136
+#: kallithea/controllers/admin/repos.py:137
 #, python-format
 msgid "Error creating repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:194
+#: kallithea/controllers/admin/repos.py:193
 #, python-format
 msgid "Created repository %s from %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:203
+#: kallithea/controllers/admin/repos.py:202
 #, python-format
 msgid "Forked repository %s as %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:206
+#: kallithea/controllers/admin/repos.py:205
 #, python-format
 msgid "Created repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:235
+#: kallithea/controllers/admin/repos.py:234
 #, python-format
 msgid "Repository %s updated successfully"
 msgstr "版本庫 %s 更新完成"
 
-#: kallithea/controllers/admin/repos.py:255
+#: kallithea/controllers/admin/repos.py:254
 #, python-format
 msgid "Error occurred during update of repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:273
+#: kallithea/controllers/admin/repos.py:272
 #, python-format
 msgid "Detached %s forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:276
+#: kallithea/controllers/admin/repos.py:275
 #, python-format
 msgid "Deleted %s forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:281
+#: kallithea/controllers/admin/repos.py:280
 #, python-format
 msgid "Deleted repository %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:284
+#: kallithea/controllers/admin/repos.py:283
 #, python-format
 msgid "Cannot delete repository %s which still has forks"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:289
+#: kallithea/controllers/admin/repos.py:288
 #, python-format
 msgid "An error occurred during deletion of %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:329
+#: kallithea/controllers/admin/repos.py:328
 msgid "Repository permissions updated"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:388
+#: kallithea/controllers/admin/repos.py:387
 #, python-format
 msgid "Field validation error: %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:391
+#: kallithea/controllers/admin/repos.py:390
 #, python-format
 msgid "An error occurred during creation of field: %r"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:402
+#: kallithea/controllers/admin/repos.py:401
 msgid "An error occurred during removal of field"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:416
+#: kallithea/controllers/admin/repos.py:415
 msgid "-- Not a fork --"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:448
+#: kallithea/controllers/admin/repos.py:447
 msgid "Updated repository visibility in public journal"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:452
+#: kallithea/controllers/admin/repos.py:451
 msgid "An error occurred during setting this repository in public journal"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:468
+#: kallithea/controllers/admin/repos.py:467
 msgid "Nothing"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:470
+#: kallithea/controllers/admin/repos.py:469
 #, python-format
 msgid "Marked repository %s as fork of %s"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:477
+#: kallithea/controllers/admin/repos.py:476
 msgid "An error occurred during this operation"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:490
-msgid "Cache invalidation successful"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:494
-msgid "An error occurred during cache invalidation"
-msgstr ""
-
-#: kallithea/controllers/admin/repos.py:507
+#: kallithea/controllers/admin/repos.py:488
 msgid "Pulled from remote location"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:510
+#: kallithea/controllers/admin/repos.py:491
 msgid "An error occurred during pull from remote location"
 msgstr ""
 
-#: kallithea/controllers/admin/repos.py:541
+#: kallithea/controllers/admin/repos.py:522
 msgid "An error occurred during deletion of repository stats"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:131
+#: kallithea/controllers/admin/settings.py:132
 msgid "Updated VCS settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:135 kallithea/lib/utils.py:237
+#: kallithea/controllers/admin/settings.py:136
 msgid ""
 "Unable to activate hgsubversion support. The \"hgsubversion\" library is "
 "missing"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:141
-#: kallithea/controllers/admin/settings.py:233
+#: kallithea/controllers/admin/settings.py:142
+#: kallithea/controllers/admin/settings.py:234
 msgid "Error occurred while updating application settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:176
+#: kallithea/controllers/admin/settings.py:177
 #, python-format
 msgid "Repositories successfully rescanned. Added: %s. Removed: %s."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:188
+#: kallithea/controllers/admin/settings.py:189
 #, fuzzy, python-format
 #| msgid "Invalidate Repository Cache"
 msgid "Invalidated %s repositories"
 msgstr "確認廢止版本庫快取"
 
-#: kallithea/controllers/admin/settings.py:229
+#: kallithea/controllers/admin/settings.py:230
 msgid "Updated application settings"
 msgstr "更新應用設定"
 
-#: kallithea/controllers/admin/settings.py:283
+#: kallithea/controllers/admin/settings.py:284
 msgid "Updated visualisation settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:288
+#: kallithea/controllers/admin/settings.py:289
 msgid "Error occurred during updating visualisation settings"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:312
+#: kallithea/controllers/admin/settings.py:313
 msgid "Please enter email address"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:327
+#: kallithea/controllers/admin/settings.py:328
 msgid "Send email task created"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:355
+#: kallithea/controllers/admin/settings.py:356
 msgid "Hook already exists"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:357
+#: kallithea/controllers/admin/settings.py:358
 msgid "Builtin hooks are read-only. Please use another hook name."
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:360
+#: kallithea/controllers/admin/settings.py:361
 msgid "Added new hook"
 msgstr "新增hook"
 
-#: kallithea/controllers/admin/settings.py:376
+#: kallithea/controllers/admin/settings.py:377
 msgid "Updated hooks"
 msgstr "更新hook"
 
-#: kallithea/controllers/admin/settings.py:380
+#: kallithea/controllers/admin/settings.py:381
 msgid "Error occurred during hook creation"
 msgstr ""
 
-#: kallithea/controllers/admin/settings.py:404
+#: kallithea/controllers/admin/settings.py:405
 msgid "Whoosh reindex task scheduled"
 msgstr "Whoosh 重新索引工作排程"
 
-#: kallithea/controllers/admin/user_groups.py:138
+#: kallithea/controllers/admin/user_groups.py:134
 #, python-format
 msgid "Created user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:151
+#: kallithea/controllers/admin/user_groups.py:147
 #, python-format
 msgid "Error occurred during creation of user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:179
+#: kallithea/controllers/admin/user_groups.py:175
 #, python-format
 msgid "Updated user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:201
+#: kallithea/controllers/admin/user_groups.py:197
 #, python-format
 msgid "Error occurred during update of user group %s"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:212
+#: kallithea/controllers/admin/user_groups.py:208
 msgid "Successfully deleted user group"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:217
+#: kallithea/controllers/admin/user_groups.py:213
 msgid "An error occurred during deletion of user group"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:273
+#: kallithea/controllers/admin/user_groups.py:269
 msgid "Target group cannot be the same"
 msgstr ""
 
-#: kallithea/controllers/admin/user_groups.py:279
+#: kallithea/controllers/admin/user_groups.py:275
 msgid "User group permissions updated"
 msgstr ""
 
+#: kallithea/controllers/admin/user_groups.py:384
+#: kallithea/controllers/admin/users.py:336
+msgid "Updated permissions"
+msgstr ""
+
 #: kallithea/controllers/admin/user_groups.py:388
-#: kallithea/controllers/admin/users.py:338
-msgid "Updated permissions"
-msgstr ""
-
-#: kallithea/controllers/admin/user_groups.py:392
-#: kallithea/controllers/admin/users.py:342
+#: kallithea/controllers/admin/users.py:340
 msgid "An error occurred during permissions saving"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:123
+#: kallithea/controllers/admin/users.py:121
 #, python-format
 msgid "Created user %s"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:138
+#: kallithea/controllers/admin/users.py:136
 #, python-format
 msgid "Error occurred during creation of user %s"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:162
+#: kallithea/controllers/admin/users.py:160
 msgid "User updated successfully"
 msgstr "使用者更新完成"
 
-#: kallithea/controllers/admin/users.py:190
+#: kallithea/controllers/admin/users.py:188
 msgid "Successfully deleted user"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:195
+#: kallithea/controllers/admin/users.py:193
 msgid "An error occurred during deletion of user"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:203
+#: kallithea/controllers/admin/users.py:201
 msgid "The default user cannot be edited"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:409
+#: kallithea/controllers/admin/users.py:407
 #, python-format
 msgid "Added IP address %s to user whitelist"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:415
+#: kallithea/controllers/admin/users.py:413
 msgid "An error occurred while adding IP address"
 msgstr ""
 
-#: kallithea/controllers/admin/users.py:427
+#: kallithea/controllers/admin/users.py:425
 msgid "Removed IP address from user whitelist"
 msgstr ""
 
-#: kallithea/lib/auth.py:684
+#: kallithea/lib/auth.py:634
 msgid "You need to be a registered user to perform this action"
 msgstr "您必須是註冊使用者才能執行這個動作"
 
-#: kallithea/lib/auth.py:712
+#: kallithea/lib/auth.py:662
 msgid "You need to be signed in to view this page"
 msgstr "您必須登入後才能瀏覽這個頁面"
 
@@ -1071,171 +1067,171 @@
 "Changeset was too big and was cut off, use diff menu to display this diff"
 msgstr ""
 
-#: kallithea/lib/diffs.py:224
+#: kallithea/lib/diffs.py:223
 msgid "No changes detected"
 msgstr "尚未有任何變更"
 
-#: kallithea/lib/helpers.py:653
+#: kallithea/lib/helpers.py:670
 #, python-format
 msgid "Deleted branch: %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:655
+#: kallithea/lib/helpers.py:672
 #, python-format
 msgid "Created tag: %s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:666
+#: kallithea/lib/helpers.py:683
 #, fuzzy, python-format
 #| msgid "Set changeset status"
 msgid "Changeset %s not found"
 msgstr "尚未有任何變更"
 
-#: kallithea/lib/helpers.py:715
+#: kallithea/lib/helpers.py:732
 #, python-format
 msgid "Show all combined changesets %s->%s"
 msgstr ""
 
-#: kallithea/lib/helpers.py:721
+#: kallithea/lib/helpers.py:738
 msgid "Compare view"
 msgstr ""
 
-#: kallithea/lib/helpers.py:740
+#: kallithea/lib/helpers.py:757
 msgid "and"
 msgstr "和"
 
-#: kallithea/lib/helpers.py:741
+#: kallithea/lib/helpers.py:758
 #, python-format
 msgid "%s more"
 msgstr ""
 
-#: kallithea/lib/helpers.py:742
+#: kallithea/lib/helpers.py:759
 #: kallithea/templates/changelog/changelog.html:43
 msgid "revisions"
 msgstr "修訂"
 
-#: kallithea/lib/helpers.py:766
+#: kallithea/lib/helpers.py:783
 #, python-format
 msgid "Fork name %s"
 msgstr "分支名稱 %s"
 
-#: kallithea/lib/helpers.py:787
+#: kallithea/lib/helpers.py:804
 #, python-format
 msgid "Pull request %s"
 msgstr "提取要求 %s"
 
-#: kallithea/lib/helpers.py:797
+#: kallithea/lib/helpers.py:814
 msgid "[deleted] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:799 kallithea/lib/helpers.py:811
+#: kallithea/lib/helpers.py:816 kallithea/lib/helpers.py:828
 msgid "[created] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:801
+#: kallithea/lib/helpers.py:818
 msgid "[created] repository as fork"
 msgstr ""
 
-#: kallithea/lib/helpers.py:803 kallithea/lib/helpers.py:813
+#: kallithea/lib/helpers.py:820 kallithea/lib/helpers.py:830
 msgid "[forked] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:805 kallithea/lib/helpers.py:815
+#: kallithea/lib/helpers.py:822 kallithea/lib/helpers.py:832
 msgid "[updated] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:807
+#: kallithea/lib/helpers.py:824
 msgid "[downloaded] archive from repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:809
+#: kallithea/lib/helpers.py:826
 msgid "[delete] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:817
+#: kallithea/lib/helpers.py:834
 msgid "[created] user"
 msgstr ""
 
-#: kallithea/lib/helpers.py:819
+#: kallithea/lib/helpers.py:836
 msgid "[updated] user"
 msgstr ""
 
-#: kallithea/lib/helpers.py:821
+#: kallithea/lib/helpers.py:838
 msgid "[created] user group"
 msgstr ""
 
-#: kallithea/lib/helpers.py:823
+#: kallithea/lib/helpers.py:840
 msgid "[updated] user group"
 msgstr ""
 
-#: kallithea/lib/helpers.py:825
+#: kallithea/lib/helpers.py:842
 msgid "[commented] on revision in repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:827
+#: kallithea/lib/helpers.py:844
 msgid "[commented] on pull request for"
 msgstr ""
 
-#: kallithea/lib/helpers.py:829
+#: kallithea/lib/helpers.py:846
 msgid "[closed] pull request for"
 msgstr ""
 
-#: kallithea/lib/helpers.py:831
+#: kallithea/lib/helpers.py:848
 msgid "[pushed] into"
 msgstr ""
 
-#: kallithea/lib/helpers.py:833
+#: kallithea/lib/helpers.py:850
 msgid "[committed via Kallithea] into repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:835
+#: kallithea/lib/helpers.py:852
 msgid "[pulled from remote] into repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:837
+#: kallithea/lib/helpers.py:854
 msgid "[pulled] from"
 msgstr ""
 
-#: kallithea/lib/helpers.py:839
+#: kallithea/lib/helpers.py:856
 msgid "[started following] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:841
+#: kallithea/lib/helpers.py:858
 msgid "[stopped following] repository"
 msgstr ""
 
-#: kallithea/lib/helpers.py:961
+#: kallithea/lib/helpers.py:975
 #, python-format
 msgid " and %s more"
 msgstr ""
 
-#: kallithea/lib/helpers.py:965
+#: kallithea/lib/helpers.py:979
 #: kallithea/templates/compare/compare_diff.html:69
 #: kallithea/templates/pullrequests/pullrequest_show.html:297
 msgid "No files"
 msgstr ""
 
-#: kallithea/lib/helpers.py:990
+#: kallithea/lib/helpers.py:1004
 msgid "new file"
 msgstr ""
 
-#: kallithea/lib/helpers.py:993
+#: kallithea/lib/helpers.py:1007
 msgid "mod"
 msgstr ""
 
-#: kallithea/lib/helpers.py:996
+#: kallithea/lib/helpers.py:1010
 msgid "del"
 msgstr ""
 
-#: kallithea/lib/helpers.py:999
+#: kallithea/lib/helpers.py:1013
 msgid "rename"
 msgstr ""
 
-#: kallithea/lib/helpers.py:1004
+#: kallithea/lib/helpers.py:1018
 msgid "chmod"
 msgstr ""
 
-#: kallithea/lib/helpers.py:1297
+#: kallithea/lib/helpers.py:1323
 #, python-format
 msgid ""
 "%s repository is not mapped to db perhaps it was created or renamed from "
@@ -1243,90 +1239,92 @@
 "repositories"
 msgstr ""
 
-#: kallithea/lib/ssh.py:73
+#: kallithea/lib/ssh.py:75
 msgid "SSH key is missing"
 msgstr ""
 
-#: kallithea/lib/ssh.py:77
-msgid "Incorrect SSH key - it must have both a key type and a base64 part"
-msgstr ""
-
-#: kallithea/lib/ssh.py:81
+#: kallithea/lib/ssh.py:79
+msgid ""
+"Incorrect SSH key - it must have both a key type and a base64 part, like "
+"'ssh-rsa ASRNeaZu4FA...xlJp='"
+msgstr ""
+
+#: kallithea/lib/ssh.py:83
 msgid "Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'"
 msgstr ""
 
-#: kallithea/lib/ssh.py:84
+#: kallithea/lib/ssh.py:86
 #, python-format
 msgid "Incorrect SSH key - unexpected characters in base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:89
+#: kallithea/lib/ssh.py:91
 #, python-format
 msgid "Incorrect SSH key - failed to decode base64 part %r"
 msgstr ""
 
-#: kallithea/lib/ssh.py:92
+#: kallithea/lib/ssh.py:94
 #, python-format
 msgid "Incorrect SSH key - base64 part is not %r as claimed but %r"
 msgstr ""
 
-#: kallithea/lib/utils2.py:334
+#: kallithea/lib/utils2.py:253
 #, python-format
 msgid "%d year"
 msgid_plural "%d years"
 msgstr[0] ""
 
-#: kallithea/lib/utils2.py:335
+#: kallithea/lib/utils2.py:254
 #, python-format
 msgid "%d month"
 msgid_plural "%d months"
 msgstr[0] ""
 
-#: kallithea/lib/utils2.py:336
+#: kallithea/lib/utils2.py:255
 #, python-format
 msgid "%d day"
 msgid_plural "%d days"
 msgstr[0] ""
 
-#: kallithea/lib/utils2.py:337
+#: kallithea/lib/utils2.py:256
 #, python-format
 msgid "%d hour"
 msgid_plural "%d hours"
 msgstr[0] ""
 
-#: kallithea/lib/utils2.py:338
+#: kallithea/lib/utils2.py:257
 #, python-format
 msgid "%d minute"
 msgid_plural "%d minutes"
 msgstr[0] ""
 
-#: kallithea/lib/utils2.py:339
+#: kallithea/lib/utils2.py:258
 #, python-format
 msgid "%d second"
 msgid_plural "%d seconds"
 msgstr[0] ""
 
-#: kallithea/lib/utils2.py:355
+#: kallithea/lib/utils2.py:274
 #, python-format
 msgid "in %s"
 msgstr ""
 
-#: kallithea/lib/utils2.py:357
+#: kallithea/lib/utils2.py:276
 #, python-format
 msgid "%s ago"
 msgstr ""
 
-#: kallithea/lib/utils2.py:359
+#: kallithea/lib/utils2.py:278
 #, python-format
 msgid "in %s and %s"
 msgstr ""
 
-#: kallithea/lib/utils2.py:362
+#: kallithea/lib/utils2.py:281
 #, python-format
 msgid "%s and %s ago"
 msgstr ""
 
-#: kallithea/lib/utils2.py:365
+#: kallithea/lib/utils2.py:284
 msgid "just now"
 msgstr "現在"
 
@@ -1335,134 +1333,134 @@
 msgid "on line %s"
 msgstr ""
 
-#: kallithea/model/comment.py:221 kallithea/model/pull_request.py:114
+#: kallithea/model/comment.py:219 kallithea/model/pull_request.py:112
 msgid "[Mention]"
 msgstr ""
 
-#: kallithea/model/db.py:1496
+#: kallithea/model/db.py:1411
 msgid "top level"
 msgstr ""
 
-#: kallithea/model/db.py:1637
+#: kallithea/model/db.py:1542
 msgid "Kallithea Administrator"
 msgstr ""
 
-#: kallithea/model/db.py:1639
+#: kallithea/model/db.py:1544
 msgid "Default user has no access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1640
+#: kallithea/model/db.py:1545
 msgid "Default user has read access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1641
+#: kallithea/model/db.py:1546
 msgid "Default user has write access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1642
+#: kallithea/model/db.py:1547
 msgid "Default user has admin access to new repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1644
+#: kallithea/model/db.py:1549
 msgid "Default user has no access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1645
+#: kallithea/model/db.py:1550
 msgid "Default user has read access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1646
+#: kallithea/model/db.py:1551
 msgid "Default user has write access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1647
+#: kallithea/model/db.py:1552
 msgid "Default user has admin access to new repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1649
+#: kallithea/model/db.py:1554
 msgid "Default user has no access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1650
+#: kallithea/model/db.py:1555
 msgid "Default user has read access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1651
+#: kallithea/model/db.py:1556
 msgid "Default user has write access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1652
+#: kallithea/model/db.py:1557
 msgid "Default user has admin access to new user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1654
+#: kallithea/model/db.py:1559
 msgid "Only admins can create repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1655
+#: kallithea/model/db.py:1560
 msgid "Non-admins can create repository groups"
 msgstr ""
 
-#: kallithea/model/db.py:1657
+#: kallithea/model/db.py:1562
 msgid "Only admins can create user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1658
+#: kallithea/model/db.py:1563
 msgid "Non-admins can create user groups"
 msgstr ""
 
-#: kallithea/model/db.py:1660
+#: kallithea/model/db.py:1565
 msgid "Only admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1661
+#: kallithea/model/db.py:1566
 msgid "Non-admins can create top level repositories"
 msgstr ""
 
-#: kallithea/model/db.py:1663
+#: kallithea/model/db.py:1568
 msgid ""
 "Repository creation enabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1664
+#: kallithea/model/db.py:1569
 msgid ""
 "Repository creation disabled with write permission to a repository group"
 msgstr ""
 
-#: kallithea/model/db.py:1666
+#: kallithea/model/db.py:1571
 msgid "Only admins can fork repositories"
 msgstr "祗有管理者才能分歧版本庫"
 
-#: kallithea/model/db.py:1667
+#: kallithea/model/db.py:1572
 #, fuzzy
 msgid "Non-admins can fork repositories"
 msgstr "建立版本庫"
 
-#: kallithea/model/db.py:1669
+#: kallithea/model/db.py:1574
 msgid "Registration disabled"
 msgstr ""
 
-#: kallithea/model/db.py:1670
+#: kallithea/model/db.py:1575
 msgid "User registration with manual account activation"
 msgstr ""
 
-#: kallithea/model/db.py:1671
+#: kallithea/model/db.py:1576
 msgid "User registration with automatic account activation"
 msgstr ""
 
-#: kallithea/model/db.py:2206
+#: kallithea/model/db.py:1992
 msgid "Not reviewed"
 msgstr "未審核"
 
-#: kallithea/model/db.py:2207
+#: kallithea/model/db.py:1993
 msgid "Under review"
 msgstr "審核中"
 
-#: kallithea/model/db.py:2208
+#: kallithea/model/db.py:1994
 msgid "Not approved"
 msgstr ""
 
-#: kallithea/model/db.py:2209
+#: kallithea/model/db.py:1995
 msgid "Approved"
 msgstr ""
 
@@ -1488,316 +1486,316 @@
 msgid "Name must not contain only digits"
 msgstr ""
 
-#: kallithea/model/notification.py:164
+#: kallithea/model/notification.py:162
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s changeset %(short_id)s \"%(message_short)s\" on "
 "%(branch)s"
 msgstr ""
 
-#: kallithea/model/notification.py:167
+#: kallithea/model/notification.py:165
 #, python-format
 msgid "New user %(new_username)s registered"
 msgstr ""
 
-#: kallithea/model/notification.py:169
+#: kallithea/model/notification.py:167
 #, python-format
 msgid ""
 "[Review] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:170
+#: kallithea/model/notification.py:168
 #, python-format
 msgid ""
 "[Comment] %(repo_name)s PR %(pr_nice_id)s \"%(pr_title_short)s\" from "
 "%(pr_source_branch)s by %(pr_owner_username)s"
 msgstr ""
 
-#: kallithea/model/notification.py:183
+#: kallithea/model/notification.py:188
 msgid "Closing"
 msgstr "關閉中"
 
-#: kallithea/model/pull_request.py:73
+#: kallithea/model/pull_request.py:72
 #, python-format
 msgid ""
 "%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:209
+#: kallithea/model/pull_request.py:207
 msgid "Cannot create empty pull request"
 msgstr ""
 
-#: kallithea/model/pull_request.py:217
+#: kallithea/model/pull_request.py:215
 #, python-format
 msgid ""
 "Cannot create pull request - criss cross merge detected, please merge a "
 "later %s revision to %s"
 msgstr ""
 
-#: kallithea/model/pull_request.py:245 kallithea/model/pull_request.py:380
+#: kallithea/model/pull_request.py:243 kallithea/model/pull_request.py:378
 msgid "You are not authorized to create the pull request"
 msgstr ""
 
-#: kallithea/model/pull_request.py:339
+#: kallithea/model/pull_request.py:337
 msgid "Missing changesets since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:346
+#: kallithea/model/pull_request.py:344
 #, python-format
 msgid "New changesets on %s %s since the previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:353
+#: kallithea/model/pull_request.py:351
 msgid "Ancestor didn't change - diff since previous iteration:"
 msgstr ""
 
-#: kallithea/model/pull_request.py:360
+#: kallithea/model/pull_request.py:358
 #, python-format
 msgid ""
 "This iteration is based on another %s revision and there is no simple "
 "diff."
 msgstr ""
 
-#: kallithea/model/pull_request.py:362
+#: kallithea/model/pull_request.py:360
 #, python-format
 msgid "No changes found on %s %s since previous iteration."
 msgstr ""
 
-#: kallithea/model/pull_request.py:388
+#: kallithea/model/pull_request.py:386
 #, python-format
 msgid "Closed, next iteration: %s ."
 msgstr ""
 
-#: kallithea/model/scm.py:668
+#: kallithea/model/scm.py:655
 msgid "latest tip"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:56
+#: kallithea/model/ssh_key.py:57
 #, python-format
 msgid "SSH key %r is invalid: %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:68
+#: kallithea/model/ssh_key.py:69
 #, python-format
 msgid "SSH key %s is already used by %s"
 msgstr ""
 
-#: kallithea/model/ssh_key.py:89
+#: kallithea/model/ssh_key.py:88
 #, fuzzy, python-format
 #| msgid "Set changeset status"
-msgid "SSH key %r not found"
+msgid "SSH key with fingerprint %r found"
 msgstr "尚未有任何變更"
 
-#: kallithea/model/user.py:186
+#: kallithea/model/user.py:180
 msgid "New user registration"
 msgstr ""
 
-#: kallithea/model/user.py:250
+#: kallithea/model/user.py:244
 msgid ""
 "You can't remove this user since it is crucial for the entire application"
 msgstr "您無法移除這個使用者,因為係供整個應用使用"
 
-#: kallithea/model/user.py:255
+#: kallithea/model/user.py:249
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repositories and cannot be removed. Switch "
 "owners or remove those repositories: %s"
 msgstr ""
 
-#: kallithea/model/user.py:260
+#: kallithea/model/user.py:254
 #, python-format
 msgid ""
 "User \"%s\" still owns %s repository groups and cannot be removed. Switch "
 "owners or remove those repository groups: %s"
 msgstr ""
 
-#: kallithea/model/user.py:267
+#: kallithea/model/user.py:261
 #, python-format
 msgid ""
 "User \"%s\" still owns %s user groups and cannot be removed. Switch "
 "owners or remove those user groups: %s"
 msgstr ""
 
-#: kallithea/model/user.py:361
+#: kallithea/model/user.py:355
 msgid "Password reset link"
 msgstr ""
 
-#: kallithea/model/user.py:408
+#: kallithea/model/user.py:402
 msgid "Password reset notification"
 msgstr ""
 
-#: kallithea/model/user.py:409
+#: kallithea/model/user.py:403
 #, python-format
 msgid ""
 "The password to your account %s has been changed using password reset "
 "form."
 msgstr ""
 
-#: kallithea/model/validators.py:52 kallithea/model/validators.py:53
+#: kallithea/model/validators.py:53 kallithea/model/validators.py:54
 msgid "Value cannot be an empty list"
 msgstr ""
 
-#: kallithea/model/validators.py:72
+#: kallithea/model/validators.py:73
 #, python-format
 msgid "Username \"%(username)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:74
+#: kallithea/model/validators.py:75
 #, python-format
 msgid "Username \"%(username)s\" cannot be used"
 msgstr ""
 
-#: kallithea/model/validators.py:76
+#: kallithea/model/validators.py:77
 msgid ""
 "Username may only contain alphanumeric characters underscores, periods or "
 "dashes and must begin with an alphanumeric character or underscore"
 msgstr ""
 
-#: kallithea/model/validators.py:103
+#: kallithea/model/validators.py:104
 msgid "The input is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:110
+#: kallithea/model/validators.py:111
 #, python-format
 msgid "Username %(username)s is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:131
-msgid "Invalid user group name"
-msgstr ""
-
 #: kallithea/model/validators.py:132
+msgid "Invalid user group name"
+msgstr ""
+
+#: kallithea/model/validators.py:133
 #, python-format
 msgid "User group \"%(usergroup)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:134
+#: kallithea/model/validators.py:135
 msgid ""
 "user group name may only contain alphanumeric characters underscores, "
 "periods or dashes and must begin with alphanumeric character"
 msgstr ""
 "使用者羣組名稱可以包括文數字字元、底線、句點或破折號,必須以文數字啟頭"
 
-#: kallithea/model/validators.py:174
+#: kallithea/model/validators.py:175
 msgid "Cannot assign this group as parent"
 msgstr ""
 
-#: kallithea/model/validators.py:175
+#: kallithea/model/validators.py:176
 #, python-format
 msgid "Group \"%(group_name)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:177
+#: kallithea/model/validators.py:178
 #, python-format
 msgid "Repository with name \"%(group_name)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:233
+#: kallithea/model/validators.py:230
 msgid "Invalid characters (non-ascii) in password"
 msgstr ""
 
-#: kallithea/model/validators.py:248
+#: kallithea/model/validators.py:245
 msgid "Invalid old password"
 msgstr ""
 
-#: kallithea/model/validators.py:264
+#: kallithea/model/validators.py:261
 msgid "Passwords do not match"
 msgstr "密碼不相符"
 
-#: kallithea/model/validators.py:279
+#: kallithea/model/validators.py:276
 msgid "Invalid username or password"
 msgstr "無效的用戶名稱或密碼"
 
-#: kallithea/model/validators.py:313
+#: kallithea/model/validators.py:310
 #, python-format
 msgid "Repository name %(repo)s is not allowed"
 msgstr ""
 
+#: kallithea/model/validators.py:312
+#, python-format
+msgid "Repository named %(repo)s already exists"
+msgstr ""
+
+#: kallithea/model/validators.py:313
+#, python-format
+msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
+msgstr ""
+
 #: kallithea/model/validators.py:315
 #, python-format
-msgid "Repository named %(repo)s already exists"
-msgstr ""
-
-#: kallithea/model/validators.py:316
-#, python-format
-msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
-msgstr ""
-
-#: kallithea/model/validators.py:318
-#, python-format
 msgid "Repository group with name \"%(repo)s\" already exists"
 msgstr ""
 
-#: kallithea/model/validators.py:404
+#: kallithea/model/validators.py:401
 msgid "Invalid repository URL"
 msgstr "無效的版本庫 URL"
 
-#: kallithea/model/validators.py:405
+#: kallithea/model/validators.py:402
 msgid ""
 "Invalid repository URL. It must be a valid http, https, ssh, svn+http or "
 "svn+https URL"
 msgstr ""
 
-#: kallithea/model/validators.py:430
+#: kallithea/model/validators.py:427
 msgid "Fork has to be the same type as parent"
 msgstr ""
 
-#: kallithea/model/validators.py:445
+#: kallithea/model/validators.py:442
 msgid "You don't have permissions to create repository in this group"
 msgstr ""
 
-#: kallithea/model/validators.py:447
+#: kallithea/model/validators.py:444
 msgid "no permission to create repository in root location"
 msgstr ""
 
-#: kallithea/model/validators.py:497
+#: kallithea/model/validators.py:494
 msgid "You don't have permissions to create a group in this location"
 msgstr ""
 
-#: kallithea/model/validators.py:537
+#: kallithea/model/validators.py:534
 msgid "This username or user group name is not valid"
 msgstr ""
 
-#: kallithea/model/validators.py:630
+#: kallithea/model/validators.py:627
 msgid "This is not a valid path"
 msgstr "不是一個有效的路徑"
 
-#: kallithea/model/validators.py:647
+#: kallithea/model/validators.py:644
 #, fuzzy
 msgid "This email address is already in use"
 msgstr "這個郵件位址已經使用了"
 
-#: kallithea/model/validators.py:667
+#: kallithea/model/validators.py:664
 #, python-format
 msgid "Email address \"%(email)s\" not found"
 msgstr ""
 
-#: kallithea/model/validators.py:704
+#: kallithea/model/validators.py:701
 msgid ""
 "The LDAP Login attribute of the CN must be specified - this is the name "
 "of the attribute that is equivalent to \"username\""
 msgstr ""
 
-#: kallithea/model/validators.py:716
+#: kallithea/model/validators.py:713
 msgid "Please enter a valid IPv4 or IPv6 address"
 msgstr ""
 
-#: kallithea/model/validators.py:717
+#: kallithea/model/validators.py:714
 #, python-format
 msgid ""
 "The network size (bits) must be within the range of 0-32 (not %(bits)r)"
 msgstr ""
 
-#: kallithea/model/validators.py:750
+#: kallithea/model/validators.py:747
 msgid "Key name can only consist of letters, underscore, dash or numbers"
 msgstr ""
 
-#: kallithea/model/validators.py:764
+#: kallithea/model/validators.py:761
 msgid "Filename cannot be inside a directory"
 msgstr ""
 
-#: kallithea/model/validators.py:780
+#: kallithea/model/validators.py:777
 #, python-format
 msgid "Plugins %(loaded)s and %(next_to_load)s both export the same name"
 msgstr ""
@@ -1857,7 +1855,7 @@
 #: kallithea/templates/admin/users/user_edit_ssh_keys.html:60
 #: kallithea/templates/email_templates/pull_request.html:37
 #: kallithea/templates/forks/fork.html:34
-#: kallithea/templates/index_base.html:58
+#: kallithea/templates/index_base.html:59
 #: kallithea/templates/pullrequests/pullrequest.html:33
 #: kallithea/templates/pullrequests/pullrequest_show.html:38
 #: kallithea/templates/pullrequests/pullrequest_show.html:59
@@ -1865,14 +1863,14 @@
 msgid "Description"
 msgstr "描述"
 
-#: kallithea/templates/index_base.html:60
+#: kallithea/templates/index_base.html:61
 msgid "Last Change"
 msgstr ""
 
 #: kallithea/templates/admin/my_account/my_account_repos.html:15
 #: kallithea/templates/admin/my_account/my_account_watched.html:15
 #: kallithea/templates/admin/repos/repos.html:41
-#: kallithea/templates/index_base.html:62
+#: kallithea/templates/index_base.html:63
 msgid "Tip"
 msgstr ""
 
@@ -1882,7 +1880,7 @@
 #: kallithea/templates/admin/repos/repos.html:42
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:8
 #: kallithea/templates/admin/user_groups/user_groups.html:42
-#: kallithea/templates/index_base.html:63
+#: kallithea/templates/index_base.html:64
 #: kallithea/templates/pullrequests/pullrequest_data.html:16
 #: kallithea/templates/pullrequests/pullrequest_show.html:124
 #: kallithea/templates/pullrequests/pullrequest_show.html:219
@@ -1906,7 +1904,7 @@
 #: kallithea/templates/admin/users/user_edit_profile.html:18
 #: kallithea/templates/admin/users/users.html:37
 #: kallithea/templates/base/base.html:364
-#: kallithea/templates/email_templates/registration.html:11
+#: kallithea/templates/email_templates/registration.html:12
 #: kallithea/templates/login.html:28 kallithea/templates/register.html:31
 msgid "Username"
 msgstr "帳號"
@@ -2034,7 +2032,7 @@
 #: kallithea/templates/admin/settings/settings.html:31
 #: kallithea/templates/admin/users/user_add.html:62
 #: kallithea/templates/admin/users/user_edit_profile.html:25
-#: kallithea/templates/email_templates/registration.html:33
+#: kallithea/templates/email_templates/registration.html:34
 #: kallithea/templates/register.html:66
 msgid "Email"
 msgstr "電子郵件"
@@ -2284,7 +2282,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/gists/index.html:51
-#: kallithea/templates/data_table/_dt_elements.html:78
+#: kallithea/templates/data_table/_dt_elements.html:84
 msgid "Created"
 msgstr ""
 
@@ -2368,13 +2366,13 @@
 #: kallithea/templates/admin/users/user_edit_ips.html:21
 #: kallithea/templates/changeset/changeset_file_comment.html:30
 #: kallithea/templates/changeset/changeset_file_comment.html:121
-#: kallithea/templates/data_table/_dt_elements.html:69
-#: kallithea/templates/data_table/_dt_elements.html:89
-#: kallithea/templates/data_table/_dt_elements.html:91
-#: kallithea/templates/data_table/_dt_elements.html:101
-#: kallithea/templates/data_table/_dt_elements.html:103
-#: kallithea/templates/data_table/_dt_elements.html:120
-#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:75
+#: kallithea/templates/data_table/_dt_elements.html:95
+#: kallithea/templates/data_table/_dt_elements.html:97
+#: kallithea/templates/data_table/_dt_elements.html:107
+#: kallithea/templates/data_table/_dt_elements.html:109
+#: kallithea/templates/data_table/_dt_elements.html:126
+#: kallithea/templates/data_table/_dt_elements.html:128
 #: kallithea/templates/files/files_source.html:35
 #: kallithea/templates/files/files_source.html:38
 #: kallithea/templates/files/files_source.html:41
@@ -2390,14 +2388,14 @@
 #: kallithea/templates/base/perms_summary.html:44
 #: kallithea/templates/base/perms_summary.html:81
 #: kallithea/templates/base/perms_summary.html:83
-#: kallithea/templates/data_table/_dt_elements.html:63
-#: kallithea/templates/data_table/_dt_elements.html:64
-#: kallithea/templates/data_table/_dt_elements.html:85
-#: kallithea/templates/data_table/_dt_elements.html:86
-#: kallithea/templates/data_table/_dt_elements.html:97
-#: kallithea/templates/data_table/_dt_elements.html:98
-#: kallithea/templates/data_table/_dt_elements.html:116
-#: kallithea/templates/data_table/_dt_elements.html:117
+#: kallithea/templates/data_table/_dt_elements.html:69
+#: kallithea/templates/data_table/_dt_elements.html:70
+#: kallithea/templates/data_table/_dt_elements.html:91
+#: kallithea/templates/data_table/_dt_elements.html:92
+#: kallithea/templates/data_table/_dt_elements.html:103
+#: kallithea/templates/data_table/_dt_elements.html:104
+#: kallithea/templates/data_table/_dt_elements.html:122
+#: kallithea/templates/data_table/_dt_elements.html:123
 #: kallithea/templates/files/diff_2way.html:56
 #: kallithea/templates/files/files_source.html:37
 #: kallithea/templates/files/files_source.html:40
@@ -2714,7 +2712,7 @@
 #: kallithea/templates/admin/permissions/permissions_globals.html:27
 #: kallithea/templates/admin/repos/repo_add_base.html:28
 #: kallithea/templates/admin/repos/repo_edit_settings.html:33
-#: kallithea/templates/data_table/_dt_elements.html:134
+#: kallithea/templates/data_table/_dt_elements.html:140
 #: kallithea/templates/forks/fork.html:42
 msgid "Repository group"
 msgstr "版本庫群組"
@@ -2735,7 +2733,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/permissions/permissions_globals.html:40
-#: kallithea/templates/data_table/_dt_elements.html:141
+#: kallithea/templates/data_table/_dt_elements.html:147
 msgid "User group"
 msgstr ""
 
@@ -2909,7 +2907,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/repo_groups/repo_group_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:121
+#: kallithea/templates/data_table/_dt_elements.html:127
 #, python-format
 msgid "Confirm to delete this group: %s with %s repository"
 msgid_plural "Confirm to delete this group: %s with %s repositories"
@@ -3080,14 +3078,10 @@
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit.html:37
-msgid "Caches"
-msgstr ""
+msgid "Remote"
+msgstr "遠端"
 
 #: kallithea/templates/admin/repos/repo_edit.html:40
-msgid "Remote"
-msgstr "遠端"
-
-#: kallithea/templates/admin/repos/repo_edit.html:43
 #: kallithea/templates/summary/statistics.html:8
 #: kallithea/templates/summary/summary.html:169
 #: kallithea/templates/summary/summary.html:170
@@ -3128,7 +3122,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/repos/repo_edit_advanced.html:46
-#: kallithea/templates/data_table/_dt_elements.html:68
+#: kallithea/templates/data_table/_dt_elements.html:74
 #, python-format
 msgid "Confirm to delete this repository: %s"
 msgstr ""
@@ -3159,44 +3153,14 @@
 "it or restore it."
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:4
-#, fuzzy
-msgid "Invalidate Repository Cache"
-msgstr "確認廢止版本庫快取"
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:6
-msgid ""
-"Manually invalidate cache for this repository. On first access, the "
-"repository will be cached again."
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:9
-msgid "List of Cached Values"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:12
-msgid "Prefix"
-msgstr ""
-
-#: kallithea/templates/admin/repos/repo_edit_caches.html:13
+#: kallithea/templates/admin/repos/repo_edit_fields.html:6
+msgid "Label"
+msgstr ""
+
 #: kallithea/templates/admin/repos/repo_edit_fields.html:7
 msgid "Key"
 msgstr ""
 
-#: kallithea/templates/admin/repos/repo_edit_caches.html:14
-#: kallithea/templates/admin/user_groups/user_group_add.html:40
-#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
-#: kallithea/templates/admin/user_groups/user_groups.html:41
-#: kallithea/templates/admin/users/user_add.html:69
-#: kallithea/templates/admin/users/user_edit_profile.html:74
-#: kallithea/templates/admin/users/users.html:42
-msgid "Active"
-msgstr "啟用"
-
-#: kallithea/templates/admin/repos/repo_edit_fields.html:6
-msgid "Label"
-msgstr ""
-
 #: kallithea/templates/admin/repos/repo_edit_fields.html:20
 #, python-format
 msgid "Confirm to delete this field: %s"
@@ -3733,6 +3697,15 @@
 msgid "Short, optional description for this user group."
 msgstr ""
 
+#: kallithea/templates/admin/user_groups/user_group_add.html:40
+#: kallithea/templates/admin/user_groups/user_group_edit_settings.html:17
+#: kallithea/templates/admin/user_groups/user_groups.html:41
+#: kallithea/templates/admin/users/user_add.html:69
+#: kallithea/templates/admin/users/user_edit_profile.html:74
+#: kallithea/templates/admin/users/users.html:42
+msgid "Active"
+msgstr "啟用"
+
 #: kallithea/templates/admin/user_groups/user_group_edit.html:5
 #, python-format
 msgid "%s user group settings"
@@ -3755,7 +3728,7 @@
 msgstr "成員"
 
 #: kallithea/templates/admin/user_groups/user_group_edit_advanced.html:19
-#: kallithea/templates/data_table/_dt_elements.html:102
+#: kallithea/templates/data_table/_dt_elements.html:108
 #, python-format
 msgid "Confirm to delete this user group: %s"
 msgstr ""
@@ -3830,7 +3803,7 @@
 msgstr ""
 
 #: kallithea/templates/admin/users/user_edit_advanced.html:21
-#: kallithea/templates/data_table/_dt_elements.html:90
+#: kallithea/templates/data_table/_dt_elements.html:96
 #, python-format
 msgid "Confirm to delete this user: %s"
 msgstr ""
@@ -3931,10 +3904,12 @@
 msgstr "搜尋"
 
 #: kallithea/templates/base/base.html:167
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Follow"
 msgstr ""
 
 #: kallithea/templates/base/base.html:168
+#: kallithea/templates/data_table/_dt_elements.html:36
 msgid "Unfollow"
 msgstr ""
 
@@ -4349,23 +4324,23 @@
 msgid "Merge"
 msgstr "合併"
 
-#: kallithea/templates/changeset/changeset.html:96
+#: kallithea/templates/changeset/changeset.html:95
 msgid "Grafted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:102
+#: kallithea/templates/changeset/changeset.html:100
 msgid "Transplanted from:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:108
+#: kallithea/templates/changeset/changeset.html:106
 msgid "Replaced by:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:122
+#: kallithea/templates/changeset/changeset.html:120
 msgid "Preceded by:"
 msgstr ""
 
-#: kallithea/templates/changeset/changeset.html:139
+#: kallithea/templates/changeset/changeset.html:137
 #: kallithea/templates/compare/compare_diff.html:59
 #: kallithea/templates/pullrequests/pullrequest_show.html:290
 #, python-format
@@ -4373,7 +4348,7 @@
 msgid_plural "%s files changed"
 msgstr[0] ""
 
-#: kallithea/templates/changeset/changeset.html:141
+#: kallithea/templates/changeset/changeset.html:139
 #: kallithea/templates/compare/compare_diff.html:61
 #: kallithea/templates/pullrequests/pullrequest_show.html:292
 #, python-format
@@ -4381,8 +4356,8 @@
 msgid_plural "%s files changed with %s insertions and %s deletions"
 msgstr[0] ""
 
-#: kallithea/templates/changeset/changeset.html:154
-#: kallithea/templates/changeset/changeset.html:173
+#: kallithea/templates/changeset/changeset.html:152
+#: kallithea/templates/changeset/changeset.html:171
 #: kallithea/templates/compare/compare_diff.html:81
 #: kallithea/templates/pullrequests/pullrequest_show.html:309
 #: kallithea/templates/pullrequests/pullrequest_show.html:333
@@ -4627,23 +4602,23 @@
 msgid "Repository creation in progress..."
 msgstr ""
 
-#: kallithea/templates/data_table/_dt_elements.html:42
+#: kallithea/templates/data_table/_dt_elements.html:48
 msgid "No changesets yet"
 msgstr "尚未有任何變更"
 
-#: kallithea/templates/data_table/_dt_elements.html:48
-#: kallithea/templates/data_table/_dt_elements.html:50
+#: kallithea/templates/data_table/_dt_elements.html:54
+#: kallithea/templates/data_table/_dt_elements.html:56
 #, python-format
 msgid "Subscribe to %s rss feed"
 msgstr "訂閱 %s rss"
 
-#: kallithea/templates/data_table/_dt_elements.html:56
-#: kallithea/templates/data_table/_dt_elements.html:58
+#: kallithea/templates/data_table/_dt_elements.html:62
+#: kallithea/templates/data_table/_dt_elements.html:64
 #, python-format
 msgid "Subscribe to %s atom feed"
 msgstr "訂閱 %s atom"
 
-#: kallithea/templates/data_table/_dt_elements.html:76
+#: kallithea/templates/data_table/_dt_elements.html:82
 msgid "Creating"
 msgstr ""
 
@@ -4680,6 +4655,13 @@
 msgid "by"
 msgstr ""
 
+#: kallithea/templates/email_templates/changeset_comment.html:36
+#: kallithea/templates/email_templates/pull_request_comment.html:43
+#, fuzzy
+#| msgid "%s committed on %s"
+msgid "View Comment"
+msgstr "%s 評論於 %s"
+
 #: kallithea/templates/email_templates/comment.html:27
 #, fuzzy
 #| msgid "Status change"
@@ -4692,32 +4674,40 @@
 msgid "The pull request has been closed."
 msgstr "儲存所已被鎖定"
 
-#: kallithea/templates/email_templates/password_reset.html:9
+#: kallithea/templates/email_templates/default.html:4
+msgid "Message"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:4
+msgid "Password Reset Request"
+msgstr ""
+
+#: kallithea/templates/email_templates/password_reset.html:10
 #, python-format
 msgid "Hello %s"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:16
+#: kallithea/templates/email_templates/password_reset.html:17
 msgid "We have received a request to reset the password for your account."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:25
+#: kallithea/templates/email_templates/password_reset.html:26
 msgid ""
 "This account is however managed outside this system and the password "
 "cannot be changed here."
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:28
+#: kallithea/templates/email_templates/password_reset.html:29
 msgid "To set a new password, click the following link"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:33
+#: kallithea/templates/email_templates/password_reset.html:34
 msgid ""
 "Should you not be able to use the link above, please type the following "
 "code into the password reset form"
 msgstr ""
 
-#: kallithea/templates/email_templates/password_reset.html:44
+#: kallithea/templates/email_templates/password_reset.html:45
 msgid ""
 "If it weren't you who requested the password reset, just disregard this "
 "message."
@@ -4750,6 +4740,12 @@
 msgid "to"
 msgstr ""
 
+#: kallithea/templates/email_templates/pull_request.html:85
+#, fuzzy
+#| msgid "Pull request %s"
+msgid "View Pull Request"
+msgstr "提取要求 %s"
+
 #: kallithea/templates/email_templates/pull_request_comment.html:4
 #, python-format
 msgid "Mention in Comment on Pull Request %s \"%s\""
@@ -4767,12 +4763,22 @@
 msgid "Comment on Pull Request %s \"%s\""
 msgstr "提取要求 %s"
 
-#: kallithea/templates/email_templates/registration.html:22
+#: kallithea/templates/email_templates/registration.html:5
+#, fuzzy
+#| msgid "Registration"
+msgid "New User Registration"
+msgstr "註冊"
+
+#: kallithea/templates/email_templates/registration.html:23
 #, fuzzy
 #| msgid "Group name"
 msgid "Full Name"
 msgstr "群組名稱"
 
+#: kallithea/templates/email_templates/registration.html:42
+msgid "View User Profile"
+msgstr ""
+
 #: kallithea/templates/files/diff_2way.html:15
 #, python-format
 msgid "%s File side-by-side diff"
@@ -5385,45 +5391,45 @@
 msgid "Stats gathered: "
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:87
-#: kallithea/templates/summary/summary.html:371
+#: kallithea/templates/summary/statistics.html:85
+#: kallithea/templates/summary/summary.html:369
 msgid "files"
 msgstr "檔案"
 
-#: kallithea/templates/summary/statistics.html:111
-#: kallithea/templates/summary/summary.html:401
+#: kallithea/templates/summary/statistics.html:109
+#: kallithea/templates/summary/summary.html:399
 msgid "Show more"
 msgstr ""
 
-#: kallithea/templates/summary/statistics.html:405
+#: kallithea/templates/summary/statistics.html:395
 msgid "commits"
 msgstr "遞交"
 
-#: kallithea/templates/summary/statistics.html:406
+#: kallithea/templates/summary/statistics.html:396
 msgid "files added"
 msgstr "多個檔案新增"
 
-#: kallithea/templates/summary/statistics.html:407
+#: kallithea/templates/summary/statistics.html:397
 msgid "files changed"
 msgstr "多個檔案修改"
 
-#: kallithea/templates/summary/statistics.html:408
+#: kallithea/templates/summary/statistics.html:398
 msgid "files removed"
 msgstr "移除多個檔案"
 
-#: kallithea/templates/summary/statistics.html:410
+#: kallithea/templates/summary/statistics.html:400
 msgid "commit"
 msgstr "遞交"
 
-#: kallithea/templates/summary/statistics.html:411
+#: kallithea/templates/summary/statistics.html:401
 msgid "file added"
 msgstr "檔案新增"
 
-#: kallithea/templates/summary/statistics.html:412
+#: kallithea/templates/summary/statistics.html:402
 msgid "file changed"
 msgstr "檔案修改"
 
-#: kallithea/templates/summary/statistics.html:413
+#: kallithea/templates/summary/statistics.html:403
 msgid "file removed"
 msgstr "移除檔案"
 
@@ -5530,6 +5536,10 @@
 msgid "Download %s as %s"
 msgstr "下載 %s 為 %s"
 
+#, fuzzy
+#~ msgid "Invalidate Repository Cache"
+#~ msgstr "確認廢止版本庫快取"
+
 #~ msgid "Repository has been locked"
 #~ msgstr "儲存所已被鎖定"
 
--- a/kallithea/lib/annotate.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/annotate.py	Mon May 04 19:24:04 2020 +0200
@@ -25,16 +25,15 @@
 :license: GPLv3, see LICENSE.md for more details.
 """
 
-import StringIO
-
 from pygments import highlight
 from pygments.formatters import HtmlFormatter
 
 from kallithea.lib.vcs.exceptions import VCSError
 from kallithea.lib.vcs.nodes import FileNode
+from kallithea.lib.vcs.utils import safe_str
 
 
-def annotate_highlight(filenode, annotate_from_changeset_func=None,
+def annotate_highlight(filenode, annotate_from_changeset_func,
         order=None, headers=None, **options):
     """
     Returns html portion containing annotated table with 3 columns: line
@@ -51,26 +50,26 @@
     """
     from kallithea.lib.pygmentsutils import get_custom_lexer
     options['linenos'] = True
-    formatter = AnnotateHtmlFormatter(filenode=filenode, order=order,
-        headers=headers,
-        annotate_from_changeset_func=annotate_from_changeset_func, **options)
+    formatter = AnnotateHtmlFormatter(filenode=filenode,
+        annotate_from_changeset_func=annotate_from_changeset_func, order=order,
+        headers=headers, **options)
     lexer = get_custom_lexer(filenode.extension) or filenode.lexer
-    highlighted = highlight(filenode.content, lexer, formatter)
+    highlighted = highlight(safe_str(filenode.content), lexer, formatter)
     return highlighted
 
 
 class AnnotateHtmlFormatter(HtmlFormatter):
 
-    def __init__(self, filenode, annotate_from_changeset_func=None,
+    def __init__(self, filenode, annotate_from_changeset_func,
             order=None, **options):
         """
-        If ``annotate_from_changeset_func`` is passed it should be a function
+        ``annotate_from_changeset_func`` must be a function
         which returns string from the given changeset. For example, we may pass
         following function as ``annotate_from_changeset_func``::
 
             def changeset_to_anchor(changeset):
                 return '<a href="/changesets/%s/">%s</a>\n' % \
-                       (changeset.id, changeset.id)
+                       (changeset.raw_id, changeset.raw_id)
 
         :param annotate_from_changeset_func: see above
         :param order: (default: ``['ls', 'annotate', 'code']``); order of
@@ -101,22 +100,13 @@
             raise VCSError("This formatter expect FileNode parameter, not %r"
                 % type(filenode))
 
-    def annotate_from_changeset(self, changeset):
-        """
-        Returns full html line for single changeset per annotated line.
-        """
-        if self.annotate_from_changeset_func:
-            return self.annotate_from_changeset_func(changeset)
-        else:
-            return ''.join((changeset.id, '\n'))
-
     def _wrap_tablelinenos(self, inner):
-        dummyoutfile = StringIO.StringIO()
+        inner_lines = []
         lncount = 0
         for t, line in inner:
             if t:
                 lncount += 1
-            dummyoutfile.write(line)
+            inner_lines.append(line)
 
         fl = self.linenostart
         mw = len(str(lncount + fl - 1))
@@ -166,7 +156,7 @@
 #        ln_ = len(ls.splitlines())
 #        if  ln_cs > ln_:
 #            annotate_changesets = annotate_changesets[:ln_ - ln_cs]
-        annotate = ''.join((self.annotate_from_changeset(el[2]())
+        annotate = ''.join((self.annotate_from_changeset_func(el[2]())
                             for el in self.filenode.annotate))
         # in case you wonder about the seemingly redundant <div> here:
         # since the content in the other cell also is wrapped in a div,
@@ -176,7 +166,7 @@
                   '<tr><td class="linenos"><div class="linenodiv"><pre>' +
                   ls + '</pre></div></td>' +
                   '<td class="code">')
-        yield 0, dummyoutfile.getvalue()
+        yield 0, ''.join(inner_lines)
         yield 0, '</td></tr></table>'
 
         '''
@@ -204,5 +194,5 @@
                   ''.join(headers_row) +
                   ''.join(body_row_start)
                   )
-        yield 0, dummyoutfile.getvalue()
+        yield 0, ''.join(inner_lines)
         yield 0, '</td></tr></table>'
--- a/kallithea/lib/app_globals.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/app_globals.py	Mon May 04 19:24:04 2020 +0200
@@ -39,9 +39,7 @@
         """One instance of Globals is created during application
         initialization and is available during requests via the
         'app_globals' variable
-
         """
-        self.available_permissions = None   # propagated after init_model
 
     @property
     def cache(self):
--- a/kallithea/lib/auth.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/auth.py	Mon May 04 19:24:04 2020 +0200
@@ -28,7 +28,9 @@
 import itertools
 import logging
 import os
+import string
 
+import bcrypt
 import ipaddr
 from decorator import decorator
 from sqlalchemy.orm import joinedload
@@ -37,14 +39,13 @@
 from tg.i18n import ugettext as _
 from webob.exc import HTTPForbidden, HTTPFound
 
-from kallithea import __platform__, is_unix, is_windows
+import kallithea
 from kallithea.config.routing import url
-from kallithea.lib.caching_query import FromCache
-from kallithea.lib.utils import conditional_cache, get_repo_group_slug, get_repo_slug, get_user_group_slug
-from kallithea.lib.utils2 import safe_str, safe_unicode
+from kallithea.lib.utils import get_repo_group_slug, get_repo_slug, get_user_group_slug
+from kallithea.lib.utils2 import ascii_bytes, ascii_str, safe_bytes
 from kallithea.lib.vcs.utils.lazy import LazyProperty
-from kallithea.model.db import (
-    Permission, RepoGroup, Repository, User, UserApiKeys, UserGroup, UserGroupMember, UserGroupRepoGroupToPerm, UserGroupRepoToPerm, UserGroupToPerm, UserGroupUserGroupToPerm, UserIpMap, UserToPerm)
+from kallithea.model.db import (Permission, UserApiKeys, UserGroup, UserGroupMember, UserGroupRepoGroupToPerm, UserGroupRepoToPerm, UserGroupToPerm,
+                                UserGroupUserGroupToPerm, UserIpMap, UserToPerm)
 from kallithea.model.meta import Session
 from kallithea.model.user import UserModel
 
@@ -86,43 +87,34 @@
 
 def get_crypt_password(password):
     """
-    Cryptographic function used for password hashing based on pybcrypt
-    or Python's own OpenSSL wrapper on windows
+    Cryptographic function used for bcrypt password hashing.
 
     :param password: password to hash
     """
-    if is_windows:
-        return hashlib.sha256(password).hexdigest()
-    elif is_unix:
-        import bcrypt
-        return bcrypt.hashpw(safe_str(password), bcrypt.gensalt(10))
-    else:
-        raise Exception('Unknown or unsupported platform %s'
-                        % __platform__)
+    return ascii_str(bcrypt.hashpw(safe_bytes(password), bcrypt.gensalt(10)))
 
 
 def check_password(password, hashed):
     """
-    Checks matching password with it's hashed value, runs different
-    implementation based on platform it runs on
+    Checks password match the hashed value using bcrypt.
+    Remains backwards compatible and accept plain sha256 hashes which used to
+    be used on Windows.
 
     :param password: password
     :param hashed: password in hashed form
     """
-
-    if is_windows:
+    # sha256 hashes will always be 64 hex chars
+    # bcrypt hashes will always contain $ (and be shorter)
+    if len(hashed) == 64 and all(x in string.hexdigits for x in hashed):
         return hashlib.sha256(password).hexdigest() == hashed
-    elif is_unix:
-        import bcrypt
-        try:
-            return bcrypt.checkpw(safe_str(password), safe_str(hashed))
-        except ValueError as e:
-            # bcrypt will throw ValueError 'Invalid hashed_password salt' on all password errors
-            log.error('error from bcrypt checking password: %s', e)
-            return False
-    else:
-        raise Exception('Unknown or unsupported platform %s'
-                        % __platform__)
+    try:
+        return bcrypt.checkpw(safe_bytes(password), ascii_bytes(hashed))
+    except ValueError as e:
+        # bcrypt will throw ValueError 'Invalid hashed_password salt' on all password errors
+        log.error('error from bcrypt checking password: %s', e)
+        return False
+    log.error('check_password failed - no method found for hash length %s', len(hashed))
+    return False
 
 
 def _cached_perms_data(user_id, user_is_admin):
@@ -147,12 +139,9 @@
     #======================================================================
     # fetch default permissions
     #======================================================================
-    default_user = User.get_by_username('default', cache=True)
-    default_user_id = default_user.user_id
-
-    default_repo_perms = Permission.get_default_perms(default_user_id)
-    default_repo_groups_perms = Permission.get_default_group_perms(default_user_id)
-    default_user_group_perms = Permission.get_default_user_group_perms(default_user_id)
+    default_repo_perms = Permission.get_default_perms(kallithea.DEFAULT_USER_ID)
+    default_repo_groups_perms = Permission.get_default_group_perms(kallithea.DEFAULT_USER_ID)
+    default_user_group_perms = Permission.get_default_user_group_perms(kallithea.DEFAULT_USER_ID)
 
     if user_is_admin:
         #==================================================================
@@ -164,19 +153,19 @@
 
         # repositories
         for perm in default_repo_perms:
-            r_k = perm.UserRepoToPerm.repository.repo_name
+            r_k = perm.repository.repo_name
             p = 'repository.admin'
             permissions[RK][r_k] = p
 
         # repository groups
         for perm in default_repo_groups_perms:
-            rg_k = perm.UserRepoGroupToPerm.group.group_name
+            rg_k = perm.group.group_name
             p = 'group.admin'
             permissions[GK][rg_k] = p
 
         # user groups
         for perm in default_user_group_perms:
-            u_k = perm.UserUserGroupToPerm.user_group.users_group_name
+            u_k = perm.user_group.users_group_name
             p = 'usergroup.admin'
             permissions[UK][u_k] = p
         return permissions
@@ -187,7 +176,7 @@
 
     # default global permissions taken from the default user
     default_global_perms = UserToPerm.query() \
-        .filter(UserToPerm.user_id == default_user_id) \
+        .filter(UserToPerm.user_id == kallithea.DEFAULT_USER_ID) \
         .options(joinedload(UserToPerm.permission))
 
     for perm in default_global_perms:
@@ -195,27 +184,27 @@
 
     # defaults for repositories, taken from default user
     for perm in default_repo_perms:
-        r_k = perm.UserRepoToPerm.repository.repo_name
-        if perm.Repository.owner_id == user_id:
+        r_k = perm.repository.repo_name
+        if perm.repository.owner_id == user_id:
             p = 'repository.admin'
-        elif perm.Repository.private:
+        elif perm.repository.private:
             p = 'repository.none'
         else:
-            p = perm.Permission.permission_name
+            p = perm.permission.permission_name
         permissions[RK][r_k] = p
 
     # defaults for repository groups taken from default user permission
     # on given group
     for perm in default_repo_groups_perms:
-        rg_k = perm.UserRepoGroupToPerm.group.group_name
-        p = perm.Permission.permission_name
+        rg_k = perm.group.group_name
+        p = perm.permission.permission_name
         permissions[GK][rg_k] = p
 
     # defaults for user groups taken from default user permission
     # on given user group
     for perm in default_user_group_perms:
-        u_k = perm.UserUserGroupToPerm.user_group.users_group_name
-        p = perm.Permission.permission_name
+        u_k = perm.user_group.users_group_name
+        p = perm.permission.permission_name
         permissions[UK][u_k] = p
 
     #======================================================================
@@ -269,30 +258,28 @@
 
     # user group for repositories permissions
     user_repo_perms_from_users_groups = \
-     Session().query(UserGroupRepoToPerm, Permission, Repository,) \
-        .join((Repository, UserGroupRepoToPerm.repository_id ==
-               Repository.repo_id)) \
-        .join((Permission, UserGroupRepoToPerm.permission_id ==
-               Permission.permission_id)) \
+     Session().query(UserGroupRepoToPerm) \
         .join((UserGroup, UserGroupRepoToPerm.users_group_id ==
                UserGroup.users_group_id)) \
         .filter(UserGroup.users_group_active == True) \
         .join((UserGroupMember, UserGroupRepoToPerm.users_group_id ==
                UserGroupMember.users_group_id)) \
         .filter(UserGroupMember.user_id == user_id) \
+        .options(joinedload(UserGroupRepoToPerm.repository)) \
+        .options(joinedload(UserGroupRepoToPerm.permission)) \
         .all()
 
     for perm in user_repo_perms_from_users_groups:
         bump_permission(RK,
-            perm.UserGroupRepoToPerm.repository.repo_name,
-            perm.Permission.permission_name)
+            perm.repository.repo_name,
+            perm.permission.permission_name)
 
     # user permissions for repositories
     user_repo_perms = Permission.get_default_perms(user_id)
     for perm in user_repo_perms:
         bump_permission(RK,
-            perm.UserRepoToPerm.repository.repo_name,
-            perm.Permission.permission_name)
+            perm.repository.repo_name,
+            perm.permission.permission_name)
 
     #======================================================================
     # !! PERMISSIONS FOR REPOSITORY GROUPS !!
@@ -303,59 +290,56 @@
     #======================================================================
     # user group for repo groups permissions
     user_repo_group_perms_from_users_groups = \
-     Session().query(UserGroupRepoGroupToPerm, Permission, RepoGroup) \
-     .join((RepoGroup, UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)) \
-     .join((Permission, UserGroupRepoGroupToPerm.permission_id
-            == Permission.permission_id)) \
+     Session().query(UserGroupRepoGroupToPerm) \
      .join((UserGroup, UserGroupRepoGroupToPerm.users_group_id ==
             UserGroup.users_group_id)) \
      .filter(UserGroup.users_group_active == True) \
      .join((UserGroupMember, UserGroupRepoGroupToPerm.users_group_id
             == UserGroupMember.users_group_id)) \
      .filter(UserGroupMember.user_id == user_id) \
+     .options(joinedload(UserGroupRepoGroupToPerm.permission)) \
      .all()
 
     for perm in user_repo_group_perms_from_users_groups:
         bump_permission(GK,
-            perm.UserGroupRepoGroupToPerm.group.group_name,
-            perm.Permission.permission_name)
+            perm.group.group_name,
+            perm.permission.permission_name)
 
     # user explicit permissions for repository groups
     user_repo_groups_perms = Permission.get_default_group_perms(user_id)
     for perm in user_repo_groups_perms:
         bump_permission(GK,
-            perm.UserRepoGroupToPerm.group.group_name,
-            perm.Permission.permission_name)
+            perm.group.group_name,
+            perm.permission.permission_name)
 
     #======================================================================
     # !! PERMISSIONS FOR USER GROUPS !!
     #======================================================================
     # user group for user group permissions
     user_group_user_groups_perms = \
-     Session().query(UserGroupUserGroupToPerm, Permission, UserGroup) \
+     Session().query(UserGroupUserGroupToPerm) \
      .join((UserGroup, UserGroupUserGroupToPerm.target_user_group_id
             == UserGroup.users_group_id)) \
-     .join((Permission, UserGroupUserGroupToPerm.permission_id
-            == Permission.permission_id)) \
      .join((UserGroupMember, UserGroupUserGroupToPerm.user_group_id
             == UserGroupMember.users_group_id)) \
      .filter(UserGroupMember.user_id == user_id) \
      .join((UserGroup, UserGroupMember.users_group_id ==
             UserGroup.users_group_id), aliased=True, from_joinpoint=True) \
      .filter(UserGroup.users_group_active == True) \
+     .options(joinedload(UserGroupUserGroupToPerm.permission)) \
      .all()
 
     for perm in user_group_user_groups_perms:
         bump_permission(UK,
-            perm.UserGroupUserGroupToPerm.target_user_group.users_group_name,
-            perm.Permission.permission_name)
+            perm.target_user_group.users_group_name,
+            perm.permission.permission_name)
 
     # user explicit permission for user groups
     user_user_groups_perms = Permission.get_default_user_group_perms(user_id)
     for perm in user_user_groups_perms:
         bump_permission(UK,
-            perm.UserUserGroupToPerm.user_group.users_group_name,
-            perm.Permission.permission_name)
+            perm.user_group.users_group_name,
+            perm.permission.permission_name)
 
     return permissions
 
@@ -403,7 +387,7 @@
         if not dbuser.active:
             log.info('Db user %s not active', dbuser.username)
             return None
-        allowed_ips = AuthUser.get_allowed_ips(dbuser.user_id, cache=True)
+        allowed_ips = AuthUser.get_allowed_ips(dbuser.user_id)
         if not check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
             log.info('Access for %s from %s forbidden - not in %s', dbuser.username, ip_addr, allowed_ips)
             return None
@@ -412,9 +396,8 @@
     def __init__(self, user_id=None, dbuser=None, is_external_auth=False):
         self.is_external_auth = is_external_auth # container auth - don't show logout option
 
-        # These attributes will be overridden by fill_data, below, unless the
-        # requested user cannot be found and the default anonymous user is
-        # not enabled.
+        # These attributes will be overridden below if the requested user is
+        # found or anonymous access (using the default user) is enabled.
         self.user_id = None
         self.username = None
         self.api_key = None
@@ -440,7 +423,7 @@
             self.is_default_user = False
         else:
             # copy non-confidential database fields from a `db.User` to this `AuthUser`.
-            for k, v in dbuser.get_dict().iteritems():
+            for k, v in dbuser.get_dict().items():
                 assert k not in ['api_keys', 'permissions']
                 setattr(self, k, v)
             self.is_default_user = dbuser.is_default_user
@@ -448,7 +431,15 @@
 
     @LazyProperty
     def permissions(self):
-        return self.__get_perms(user=self, cache=False)
+        """
+        Fills user permission attribute with permissions taken from database
+        works for permissions given for repositories, and for permissions that
+        are granted to groups
+
+        :param user: `AuthUser` instance
+        """
+        log.debug('Getting PERMISSION tree for %s', self)
+        return _cached_perms_data(self.user_id, self.is_admin)
 
     def has_repository_permission_level(self, repo_name, level, purpose=None):
         required_perms = {
@@ -490,22 +481,6 @@
     def api_keys(self):
         return self._get_api_keys()
 
-    def __get_perms(self, user, cache=False):
-        """
-        Fills user permission attribute with permissions taken from database
-        works for permissions given for repositories, and for permissions that
-        are granted to groups
-
-        :param user: `AuthUser` instance
-        """
-        user_id = user.user_id
-        user_is_admin = user.is_admin
-
-        log.debug('Getting PERMISSION tree')
-        compute = conditional_cache('short_term', 'cache_desc',
-                                    condition=cache, func=_cached_perms_data)
-        return compute(user_id, user_is_admin)
-
     def _get_api_keys(self):
         api_keys = [self.api_key]
         for api_key in UserApiKeys.query() \
@@ -523,7 +498,7 @@
         """
         Returns list of repositories you're an admin of
         """
-        return [x[0] for x in self.permissions['repositories'].iteritems()
+        return [x[0] for x in self.permissions['repositories'].items()
                 if x[1] == 'repository.admin']
 
     @property
@@ -531,7 +506,7 @@
         """
         Returns list of repository groups you're an admin of
         """
-        return [x[0] for x in self.permissions['repositories_groups'].iteritems()
+        return [x[0] for x in self.permissions['repositories_groups'].items()
                 if x[1] == 'group.admin']
 
     @property
@@ -539,11 +514,11 @@
         """
         Returns list of user groups you're an admin of
         """
-        return [x[0] for x in self.permissions['user_groups'].iteritems()
+        return [x[0] for x in self.permissions['user_groups'].items()
                 if x[1] == 'usergroup.admin']
 
     def __repr__(self):
-        return "<AuthUser('id:%s[%s]')>" % (self.user_id, self.username)
+        return "<%s %s: %r>" % (self.__class__.__name__, self.user_id, self.username)
 
     def to_cookie(self):
         """ Serializes this login session to a cookie `dict`. """
@@ -564,14 +539,10 @@
         )
 
     @classmethod
-    def get_allowed_ips(cls, user_id, cache=False):
+    def get_allowed_ips(cls, user_id):
         _set = set()
 
-        default_ips = UserIpMap.query().filter(UserIpMap.user_id ==
-                                        User.get_default_user(cache=True).user_id)
-        if cache:
-            default_ips = default_ips.options(FromCache("sql_cache_short",
-                                              "get_user_ips_default"))
+        default_ips = UserIpMap.query().filter(UserIpMap.user_id == kallithea.DEFAULT_USER_ID)
         for ip in default_ips:
             try:
                 _set.add(ip.ip_addr)
@@ -581,9 +552,6 @@
                 pass
 
         user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
-        if cache:
-            user_ips = user_ips.options(FromCache("sql_cache_short",
-                                                  "get_user_ips_%s" % user_id))
         for ip in user_ips:
             try:
                 _set.add(ip.ip_addr)
@@ -594,24 +562,6 @@
         return _set or set(['0.0.0.0/0', '::/0'])
 
 
-def set_available_permissions(config):
-    """
-    This function will propagate globals with all available defined
-    permission given in db. We don't want to check each time from db for new
-    permissions since adding a new permission also requires application restart
-    ie. to decorate new views with the newly created permission
-
-    :param config: current config instance
-
-    """
-    log.info('getting information about all available permissions')
-    try:
-        all_perms = Session().query(Permission).all()
-        config['available_permissions'] = [x.permission_name for x in all_perms]
-    finally:
-        Session.remove()
-
-
 #==============================================================================
 # CHECK DECORATORS
 #==============================================================================
@@ -776,7 +726,7 @@
     def __init__(self, *required_perms):
         self.required_perms = required_perms # usually very short - a list is thus fine
 
-    def __nonzero__(self):
+    def __bool__(self):
         """ Defend against accidentally forgetting to call the object
             and instead evaluating it directly in a boolean context,
             which could have security implications.
@@ -833,10 +783,6 @@
         self.required_perms = set(perms)
 
     def __call__(self, authuser, repo_name, purpose=None):
-        # repo_name MUST be unicode, since we handle keys in ok
-        # dict by unicode
-        repo_name = safe_unicode(repo_name)
-
         try:
             ok = authuser.permissions['repositories'][repo_name] in self.required_perms
         except KeyError:
--- a/kallithea/lib/auth_modules/__init__.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/auth_modules/__init__.py	Mon May 04 19:24:04 2020 +0200
@@ -20,7 +20,7 @@
 import traceback
 
 from kallithea.lib.auth import AuthUser, PasswordGenerator
-from kallithea.lib.compat import formatted_json, hybrid_property
+from kallithea.lib.compat import hybrid_property
 from kallithea.lib.utils2 import str2bool
 from kallithea.model.db import Setting, User
 from kallithea.model.meta import Session
@@ -136,9 +136,6 @@
                   username)
         if username:
             user = User.get_by_username_or_email(username)
-            if user is None:
-                log.debug('Fallback to fetch user in case insensitive mode')
-                user = User.get_by_username(username, case_insensitive=True)
         else:
             log.debug('provided username:`%s` is empty skipping...', username)
         return user
@@ -286,11 +283,11 @@
         ImportError -- if we couldn't import the plugin at all
     """
     log.debug("Importing %s", plugin)
-    if not plugin.startswith(u'kallithea.lib.auth_modules.auth_'):
-        parts = plugin.split(u'.lib.auth_modules.auth_', 1)
+    if not plugin.startswith('kallithea.lib.auth_modules.auth_'):
+        parts = plugin.split('.lib.auth_modules.auth_', 1)
         if len(parts) == 2:
             _module, pn = parts
-            plugin = u'kallithea.lib.auth_modules.auth_' + pn
+            plugin = 'kallithea.lib.auth_modules.auth_' + pn
     PLUGIN_CLASS_NAME = "KallitheaAuthPlugin"
     try:
         module = importlib.import_module(plugin)
@@ -309,7 +306,7 @@
                         "a subclass of %s" % (plugin, KallitheaAuthPluginBase))
 
     plugin = pluginclass()
-    if plugin.plugin_settings.im_func != KallitheaAuthPluginBase.plugin_settings.im_func:
+    if plugin.plugin_settings.__func__ != KallitheaAuthPluginBase.plugin_settings:
         raise TypeError("Authentication class %s.KallitheaAuthPluginBase "
                         "has overridden the plugin_settings method, which is "
                         "forbidden." % plugin)
@@ -351,7 +348,7 @@
             conf_key = "auth_%s_%s" % (plugin_name, v["name"])
             setting = Setting.get_by_name(conf_key)
             plugin_settings[v["name"]] = setting.app_settings_value if setting else None
-        log.debug('Settings for auth plugin %s:\n%s', plugin_name, formatted_json(plugin_settings))
+        log.debug('Settings for auth plugin %s: %s', plugin_name, plugin_settings)
 
         if not str2bool(plugin_settings["enabled"]):
             log.info("Authentication plugin %s is disabled, skipping for %s",
--- a/kallithea/lib/auth_modules/auth_container.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/auth_modules/auth_container.py	Mon May 04 19:24:04 2020 +0200
@@ -29,7 +29,7 @@
 
 from kallithea.lib import auth_modules
 from kallithea.lib.compat import hybrid_property
-from kallithea.lib.utils2 import safe_str, safe_unicode, str2bool
+from kallithea.lib.utils2 import str2bool
 from kallithea.model.db import Setting
 
 
@@ -180,7 +180,7 @@
         # only way to log in is using environ
         username = None
         if userobj:
-            username = safe_str(getattr(userobj, 'username'))
+            username = getattr(userobj, 'username')
 
         if not username:
             # we don't have any objects in DB, user doesn't exist, extract
@@ -199,8 +199,8 @@
 
         user_data = {
             'username': username,
-            'firstname': safe_unicode(firstname or username),
-            'lastname': safe_unicode(lastname or ''),
+            'firstname': firstname or username,
+            'lastname': lastname or '',
             'groups': [],
             'email': email or '',
             'admin': admin or False,
--- a/kallithea/lib/auth_modules/auth_crowd.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/auth_modules/auth_crowd.py	Mon May 04 19:24:04 2020 +0200
@@ -28,10 +28,12 @@
 
 import base64
 import logging
-import urllib2
+import urllib.parse
+import urllib.request
 
-from kallithea.lib import auth_modules
-from kallithea.lib.compat import formatted_json, hybrid_property, json
+from kallithea.lib import auth_modules, ext_json
+from kallithea.lib.compat import hybrid_property
+from kallithea.lib.utils2 import ascii_bytes, ascii_str, safe_bytes
 
 
 log = logging.getLogger(__name__)
@@ -71,10 +73,10 @@
         self._make_opener()
 
     def _make_opener(self):
-        mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
+        mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
         mgr.add_password(None, self._uri, self.user, self.passwd)
-        handler = urllib2.HTTPBasicAuthHandler(mgr)
-        self.opener = urllib2.build_opener(handler)
+        handler = urllib.request.HTTPBasicAuthHandler(mgr)
+        self.opener = urllib.request.build_opener(handler)
 
     def _request(self, url, body=None, headers=None,
                  method=None, noformat=False,
@@ -82,14 +84,12 @@
         _headers = {"Content-type": "application/json",
                     "Accept": "application/json"}
         if self.user and self.passwd:
-            authstring = base64.b64encode("%s:%s" % (self.user, self.passwd))
+            authstring = ascii_str(base64.b64encode(safe_bytes("%s:%s" % (self.user, self.passwd))))
             _headers["Authorization"] = "Basic %s" % authstring
         if headers:
             _headers.update(headers)
-        log.debug("Sent crowd: \n%s",
-                  formatted_json({"url": url, "body": body,
-                                           "headers": _headers}))
-        req = urllib2.Request(url, body, _headers)
+        log.debug("Sent to crowd at %s:\nHeaders: %s\nBody:\n%s", url, _headers, body)
+        req = urllib.request.Request(url, body, _headers)
         if method:
             req.get_method = lambda: method
 
@@ -103,7 +103,7 @@
                 rval["status"] = True
                 rval["error"] = "Response body was empty"
             elif not noformat:
-                rval = json.loads(msg)
+                rval = ext_json.loads(msg)
                 rval["status"] = True
             else:
                 rval = "".join(rdoc.readlines())
@@ -120,14 +120,14 @@
         """Authenticate a user against crowd. Returns brief information about
         the user."""
         url = ("%s/rest/usermanagement/%s/authentication?username=%s"
-               % (self._uri, self._version, urllib2.quote(username)))
-        body = json.dumps({"value": password})
+               % (self._uri, self._version, urllib.parse.quote(username)))
+        body = ascii_bytes(ext_json.dumps({"value": password}))
         return self._request(url, body)
 
     def user_groups(self, username):
         """Retrieve a list of groups to which this user belongs."""
         url = ("%s/rest/usermanagement/%s/user/group/nested?username=%s"
-               % (self._uri, self._version, urllib2.quote(username)))
+               % (self._uri, self._version, urllib.parse.quote(username)))
         return self._request(url)
 
 
@@ -209,11 +209,11 @@
             log.debug('Empty username or password skipping...')
             return None
 
-        log.debug("Crowd settings: \n%s", formatted_json(settings))
+        log.debug("Crowd settings: %s", settings)
         server = CrowdServer(**settings)
         server.set_credentials(settings["app_name"], settings["app_password"])
         crowd_user = server.user_auth(username, password)
-        log.debug("Crowd returned: \n%s", formatted_json(crowd_user))
+        log.debug("Crowd returned: %s", crowd_user)
         if not crowd_user["status"]:
             log.error('Crowd authentication as %s returned no status', username)
             return None
@@ -223,7 +223,7 @@
             return None
 
         res = server.user_groups(crowd_user["name"])
-        log.debug("Crowd groups: \n%s", formatted_json(res))
+        log.debug("Crowd groups: %s", res)
         crowd_user["groups"] = [x["name"] for x in res["groups"]]
 
         # old attrs fetched from Kallithea database
@@ -246,7 +246,7 @@
         for group in settings["admin_groups"].split(","):
             if group in user_data["groups"]:
                 user_data["admin"] = True
-        log.debug("Final crowd user object: \n%s", formatted_json(user_data))
+        log.debug("Final crowd user object: %s", user_data)
         log.info('user %s authenticated correctly', user_data['username'])
         return user_data
 
--- a/kallithea/lib/auth_modules/auth_internal.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/auth_modules/auth_internal.py	Mon May 04 19:24:04 2020 +0200
@@ -29,8 +29,7 @@
 import logging
 
 from kallithea.lib import auth_modules
-from kallithea.lib.compat import formatted_json, hybrid_property
-from kallithea.model.db import User
+from kallithea.lib.compat import hybrid_property
 
 
 log = logging.getLogger(__name__)
@@ -77,7 +76,7 @@
             "admin": userobj.admin,
             "extern_name": userobj.user_id,
         }
-        log.debug(formatted_json(user_data))
+        log.debug('user data: %s', user_data)
 
         from kallithea.lib import auth
         password_match = auth.check_password(password, userobj.password)
--- a/kallithea/lib/auth_modules/auth_ldap.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/auth_modules/auth_ldap.py	Mon May 04 19:24:04 2020 +0200
@@ -31,7 +31,7 @@
 from kallithea.lib import auth_modules
 from kallithea.lib.compat import hybrid_property
 from kallithea.lib.exceptions import LdapConnectionError, LdapImportError, LdapPasswordError, LdapUsernameError
-from kallithea.lib.utils2 import safe_str, safe_unicode
+from kallithea.lib.utils2 import safe_str
 
 
 log = logging.getLogger(__name__)
@@ -70,11 +70,11 @@
                             port)
             for host in server.split(',')))
 
-        self.LDAP_BIND_DN = safe_str(bind_dn)
-        self.LDAP_BIND_PASS = safe_str(bind_pass)
+        self.LDAP_BIND_DN = bind_dn
+        self.LDAP_BIND_PASS = bind_pass
 
-        self.BASE_DN = safe_str(base_dn)
-        self.LDAP_FILTER = safe_str(ldap_filter)
+        self.BASE_DN = base_dn
+        self.LDAP_FILTER = ldap_filter
         self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
         self.attr_login = attr_login
 
@@ -139,7 +139,7 @@
 
                 try:
                     log.debug('Trying simple bind with %s', dn)
-                    server.simple_bind_s(dn, safe_str(password))
+                    server.simple_bind_s(dn, password)
                     results = server.search_ext_s(dn, ldap.SCOPE_BASE,
                                                   '(objectClass=*)')
                     if len(results) == 1:
@@ -328,7 +328,8 @@
             (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
             log.debug('Got ldap DN response %s', user_dn)
 
-            get_ldap_attr = lambda k: ldap_attrs.get(settings.get(k), [''])[0]
+            def get_ldap_attr(k):
+                return safe_str(ldap_attrs.get(settings.get(k), [b''])[0])
 
             # old attrs fetched from Kallithea database
             admin = getattr(userobj, 'admin', False)
@@ -338,8 +339,8 @@
 
             user_data = {
                 'username': username,
-                'firstname': safe_unicode(get_ldap_attr('attr_firstname') or firstname),
-                'lastname': safe_unicode(get_ldap_attr('attr_lastname') or lastname),
+                'firstname': get_ldap_attr('attr_firstname') or firstname,
+                'lastname': get_ldap_attr('attr_lastname') or lastname,
                 'groups': [],
                 'email': get_ldap_attr('attr_email') or email,
                 'admin': admin,
--- a/kallithea/lib/auth_modules/auth_pam.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/auth_modules/auth_pam.py	Mon May 04 19:24:04 2020 +0200
@@ -32,7 +32,7 @@
 import time
 
 from kallithea.lib import auth_modules
-from kallithea.lib.compat import formatted_json, hybrid_property
+from kallithea.lib.compat import hybrid_property
 
 
 try:
@@ -142,7 +142,7 @@
             log.warning("Cannot extract additional info for PAM user %s", username)
             pass
 
-        log.debug("pamuser: \n%s", formatted_json(user_data))
+        log.debug("pamuser: %s", user_data)
         log.info('user %s authenticated correctly', user_data['username'])
         return user_data
 
--- a/kallithea/lib/base.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/base.py	Mon May 04 19:24:04 2020 +0200
@@ -28,9 +28,9 @@
 :license: GPLv3, see LICENSE.md for more details.
 """
 
+import base64
 import datetime
 import logging
-import time
 import traceback
 import warnings
 
@@ -45,12 +45,11 @@
 
 from kallithea import BACKENDS, __version__
 from kallithea.config.routing import url
-from kallithea.lib import auth_modules
+from kallithea.lib import auth_modules, ext_json
 from kallithea.lib.auth import AuthUser, HasPermissionAnyMiddleware
-from kallithea.lib.compat import json
 from kallithea.lib.exceptions import UserCreationError
 from kallithea.lib.utils import get_repo_slug, is_valid_repo
-from kallithea.lib.utils2 import AttributeDict, safe_int, safe_str, safe_unicode, set_hook_environment, str2bool
+from kallithea.lib.utils2 import AttributeDict, ascii_bytes, safe_int, safe_str, set_hook_environment, str2bool
 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError
 from kallithea.model import meta
 from kallithea.model.db import PullRequest, Repository, Setting, User
@@ -97,12 +96,18 @@
     return _filter_proxy(ip)
 
 
-def _get_access_path(environ):
-    """Return PATH_INFO from environ ... using tg.original_request if available."""
+def get_path_info(environ):
+    """Return PATH_INFO from environ ... using tg.original_request if available.
+
+    In Python 3 WSGI, PATH_INFO is a unicode str, but kind of contains encoded
+    bytes. The code points are guaranteed to only use the lower 8 bit bits, and
+    encoding the string with the 1:1 encoding latin1 will give the
+    corresponding byte string ... which then can be decoded to proper unicode.
+    """
     org_req = environ.get('tg.original_request')
     if org_req is not None:
         environ = org_req.environ
-    return environ.get('PATH_INFO')
+    return safe_str(environ['PATH_INFO'].encode('latin1'))
 
 
 def log_in_user(user, remember, is_external_auth, ip_addr):
@@ -172,7 +177,7 @@
         (authmeth, auth) = authorization.split(' ', 1)
         if 'basic' != authmeth.lower():
             return self.build_authentication(environ)
-        auth = auth.strip().decode('base64')
+        auth = safe_str(base64.b64decode(auth.strip()))
         _parts = auth.split(':', 1)
         if len(_parts) == 2:
             username, password = _parts
@@ -218,7 +223,7 @@
         Returns (None, wsgi_app) to send the wsgi_app response to the client.
         """
         # Use anonymous access if allowed for action on repo.
-        default_user = User.get_default_user(cache=True)
+        default_user = User.get_default_user()
         default_authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr)
         if default_authuser is None:
             log.debug('No anonymous access at all') # move on to proper user auth
@@ -242,7 +247,7 @@
 
         # If not authenticated by the container, running basic auth
         if not username:
-            self.authenticate.realm = safe_str(self.config['realm'])
+            self.authenticate.realm = self.config['realm']
             result = self.authenticate(environ)
             if isinstance(result, str):
                 paste.httpheaders.AUTH_TYPE.update(environ, 'basic')
@@ -273,11 +278,8 @@
 
     def _check_permission(self, action, authuser, repo_name):
         """
-        Checks permissions using action (push/pull) user and repository
-        name
-
-        :param action: 'push' or 'pull' action
-        :param user: `User` instance
+        :param action: 'push' or 'pull'
+        :param user: `AuthUser` instance
         :param repo_name: repository name
         """
         if action == 'push':
@@ -286,7 +288,7 @@
                                                                   repo_name):
                 return False
 
-        else:
+        elif action == 'pull':
             #any other action need at least read permission
             if not HasPermissionAnyMiddleware('repository.read',
                                               'repository.write',
@@ -294,13 +296,15 @@
                                                                   repo_name):
                 return False
 
+        else:
+            assert False, action
+
         return True
 
     def _get_ip_addr(self, environ):
         return _get_ip_addr(environ)
 
     def __call__(self, environ, start_response):
-        start = time.time()
         try:
             # try parsing a request for this VCS - if it fails, call the wrapped app
             parsed_request = self.parse_request(environ)
@@ -334,7 +338,7 @@
 
             try:
                 log.info('%s action on %s repo "%s" by "%s" from %s',
-                         parsed_request.action, self.scm_alias, parsed_request.repo_name, safe_str(user.username), ip_addr)
+                         parsed_request.action, self.scm_alias, parsed_request.repo_name, user.username, ip_addr)
                 app = self._make_app(parsed_request)
                 return app(environ, start_response)
             except Exception:
@@ -343,10 +347,6 @@
 
         except webob.exc.HTTPException as e:
             return e(environ, start_response)
-        finally:
-            log_ = logging.getLogger('kallithea.' + self.__class__.__name__)
-            log_.debug('Request time: %.3fs', time.time() - start)
-            meta.Session.remove()
 
 
 class BaseController(TGController):
@@ -413,7 +413,7 @@
         # END CONFIG VARS
 
         c.repo_name = get_repo_slug(request)  # can be empty
-        c.backends = BACKENDS.keys()
+        c.backends = list(BACKENDS)
 
         self.cut_off_limit = safe_int(config.get('cut_off_limit'))
 
@@ -454,7 +454,7 @@
                     return log_in_user(user, remember=False, is_external_auth=True, ip_addr=ip_addr)
 
         # User is default user (if active) or anonymous
-        default_user = User.get_default_user(cache=True)
+        default_user = User.get_default_user()
         authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr)
         if authuser is None: # fall back to anonymous
             authuser = AuthUser(dbuser=default_user) # TODO: somehow use .make?
@@ -529,9 +529,9 @@
             request.ip_addr = ip_addr
             request.needs_csrf_check = needs_csrf_check
 
-            log.info('IP: %s User: %s accessed %s',
+            log.info('IP: %s User: %s Request: %s',
                 request.ip_addr, request.authuser,
-                safe_unicode(_get_access_path(environ)),
+                get_path_info(environ),
             )
             return super(BaseController, self).__call__(environ, context)
         except webob.exc.HTTPException as e:
@@ -552,13 +552,13 @@
 
     def _before(self, *args, **kwargs):
         super(BaseRepoController, self)._before(*args, **kwargs)
-        if c.repo_name:  # extracted from routes
+        if c.repo_name:  # extracted from request by base-base BaseController._before
             _dbr = Repository.get_by_repo_name(c.repo_name)
             if not _dbr:
                 return
 
             log.debug('Found repository in database %s with state `%s`',
-                      safe_unicode(_dbr), safe_unicode(_dbr.repo_state))
+                      _dbr, _dbr.repo_state)
             route = getattr(request.environ.get('routes.route'), 'name', '')
 
             # allow to delete repos that are somehow damages in filesystem
@@ -608,7 +608,7 @@
             raise webob.exc.HTTPNotFound()
         except RepositoryError as e:
             log.error(traceback.format_exc())
-            h.flash(safe_str(e), category='error')
+            h.flash(e, category='error')
             raise webob.exc.HTTPBadRequest()
 
 
@@ -634,7 +634,7 @@
         warnings.warn(msg, Warning, 2)
         log.warning(msg)
     log.debug("Returning JSON wrapped action output")
-    return json.dumps(data, encoding='utf-8')
+    return ascii_bytes(ext_json.dumps(data))
 
 @decorator.decorator
 def IfSshEnabled(func, *args, **kwargs):
--- a/kallithea/lib/caching_query.py	Mon May 04 18:25:09 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,240 +0,0 @@
-"""caching_query.py
-
-Represent persistence structures which allow the usage of
-Beaker caching with SQLAlchemy.
-
-The three new concepts introduced here are:
-
- * CachingQuery - a Query subclass that caches and
-   retrieves results in/from Beaker.
- * FromCache - a query option that establishes caching
-   parameters on a Query
- * _params_from_query - extracts value parameters from
-   a Query.
-
-The rest of what's here are standard SQLAlchemy and
-Beaker constructs.
-
-"""
-import beaker
-from beaker.exceptions import BeakerException
-from sqlalchemy.orm.interfaces import MapperOption
-from sqlalchemy.orm.query import Query
-from sqlalchemy.sql import visitors
-
-from kallithea.lib.utils2 import safe_str
-
-
-class CachingQuery(Query):
-    """A Query subclass which optionally loads full results from a Beaker
-    cache region.
-
-    The CachingQuery stores additional state that allows it to consult
-    a Beaker cache before accessing the database:
-
-    * A "region", which is a cache region argument passed to a
-      Beaker CacheManager, specifies a particular cache configuration
-      (including backend implementation, expiration times, etc.)
-    * A "namespace", which is a qualifying name that identifies a
-      group of keys within the cache.  A query that filters on a name
-      might use the name "by_name", a query that filters on a date range
-      to a joined table might use the name "related_date_range".
-
-    When the above state is present, a Beaker cache is retrieved.
-
-    The "namespace" name is first concatenated with
-    a string composed of the individual entities and columns the Query
-    requests, i.e. such as ``Query(User.id, User.name)``.
-
-    The Beaker cache is then loaded from the cache manager based
-    on the region and composed namespace.  The key within the cache
-    itself is then constructed against the bind parameters specified
-    by this query, which are usually literals defined in the
-    WHERE clause.
-
-    The FromCache mapper option below represent
-    the "public" method of configuring this state upon the CachingQuery.
-
-    """
-
-    def __init__(self, manager, *args, **kw):
-        self.cache_manager = manager
-        Query.__init__(self, *args, **kw)
-
-    def __iter__(self):
-        """override __iter__ to pull results from Beaker
-           if particular attributes have been configured.
-
-           Note that this approach does *not* detach the loaded objects from
-           the current session. If the cache backend is an in-process cache
-           (like "memory") and lives beyond the scope of the current session's
-           transaction, those objects may be expired. The method here can be
-           modified to first expunge() each loaded item from the current
-           session before returning the list of items, so that the items
-           in the cache are not the same ones in the current Session.
-
-        """
-        if hasattr(self, '_cache_parameters'):
-            return self.get_value(createfunc=lambda:
-                                  list(Query.__iter__(self)))
-        else:
-            return Query.__iter__(self)
-
-    def invalidate(self):
-        """Invalidate the value represented by this Query."""
-
-        cache, cache_key = _get_cache_parameters(self)
-        cache.remove(cache_key)
-
-    def get_value(self, merge=True, createfunc=None):
-        """Return the value from the cache for this query.
-
-        Raise KeyError if no value present and no
-        createfunc specified.
-
-        """
-        cache, cache_key = _get_cache_parameters(self)
-        ret = cache.get_value(cache_key, createfunc=createfunc)
-        if merge:
-            ret = self.merge_result(ret, load=False)
-        return ret
-
-    def set_value(self, value):
-        """Set the value in the cache for this query."""
-
-        cache, cache_key = _get_cache_parameters(self)
-        cache.put(cache_key, value)
-
-
-def query_callable(manager, query_cls=CachingQuery):
-    def query(*arg, **kw):
-        return query_cls(manager, *arg, **kw)
-    return query
-
-
-def get_cache_region(name, region):
-    if region not in beaker.cache.cache_regions:
-        raise BeakerException('Cache region `%s` not configured '
-            'Check if proper cache settings are in the .ini files' % region)
-    kw = beaker.cache.cache_regions[region]
-    return beaker.cache.Cache._get_cache(name, kw)
-
-
-def _get_cache_parameters(query):
-    """For a query with cache_region and cache_namespace configured,
-    return the corresponding Cache instance and cache key, based
-    on this query's current criterion and parameter values.
-
-    """
-    if not hasattr(query, '_cache_parameters'):
-        raise ValueError("This Query does not have caching "
-                         "parameters configured.")
-
-    region, namespace, cache_key = query._cache_parameters
-
-    namespace = _namespace_from_query(namespace, query)
-
-    if cache_key is None:
-        # cache key - the value arguments from this query's parameters.
-        args = [safe_str(x) for x in _params_from_query(query)]
-        args.extend(filter(lambda k: k not in ['None', None, u'None'],
-                           [str(query._limit), str(query._offset)]))
-
-        cache_key = " ".join(args)
-
-    if cache_key is None:
-        raise Exception('Cache key cannot be None')
-
-    # get cache
-    #cache = query.cache_manager.get_cache_region(namespace, region)
-    cache = get_cache_region(namespace, region)
-    # optional - hash the cache_key too for consistent length
-    # import uuid
-    # cache_key= str(uuid.uuid5(uuid.NAMESPACE_DNS, cache_key))
-
-    return cache, cache_key
-
-
-def _namespace_from_query(namespace, query):
-    # cache namespace - the token handed in by the
-    # option + class we're querying against
-    namespace = " ".join([namespace] + [str(x) for x in query._entities])
-
-    # memcached wants this
-    namespace = namespace.replace(' ', '_')
-
-    return namespace
-
-
-def _set_cache_parameters(query, region, namespace, cache_key):
-
-    if hasattr(query, '_cache_parameters'):
-        region, namespace, cache_key = query._cache_parameters
-        raise ValueError("This query is already configured "
-                        "for region %r namespace %r" %
-                        (region, namespace)
-                    )
-    query._cache_parameters = region, safe_str(namespace), cache_key
-
-
-class FromCache(MapperOption):
-    """Specifies that a Query should load results from a cache."""
-
-    propagate_to_loaders = False
-
-    def __init__(self, region, namespace, cache_key=None):
-        """Construct a new FromCache.
-
-        :param region: the cache region.  Should be a
-        region configured in the Beaker CacheManager.
-
-        :param namespace: the cache namespace.  Should
-        be a name uniquely describing the target Query's
-        lexical structure.
-
-        :param cache_key: optional.  A string cache key
-        that will serve as the key to the query.   Use this
-        if your query has a huge amount of parameters (such
-        as when using in_()) which correspond more simply to
-        some other identifier.
-
-        """
-        self.region = region
-        self.namespace = namespace
-        self.cache_key = cache_key
-
-    def process_query(self, query):
-        """Process a Query during normal loading operation."""
-
-        _set_cache_parameters(query, self.region, self.namespace,
-                              self.cache_key)
-
-
-def _params_from_query(query):
-    """Pull the bind parameter values from a query.
-
-    This takes into account any scalar attribute bindparam set up.
-
-    E.g. params_from_query(query.filter(Cls.foo==5).filter(Cls.bar==7)))
-    would return [5, 7].
-
-    """
-    v = []
-
-    def visit_bindparam(bind):
-        if bind.key in query._params:
-            value = query._params[bind.key]
-        elif bind.callable:
-            # lazyloader may dig a callable in here, intended
-            # to late-evaluate params after autoflush is called.
-            # convert to a scalar value.
-            value = bind.callable()
-        else:
-            value = bind.value
-
-        v.append(value)
-    if query._criterion is not None:
-        visitors.traverse(query._criterion, {}, {'bindparam': visit_bindparam})
-    for f in query._from_obj:
-        visitors.traverse(f, {}, {'bindparam': visit_bindparam})
-    return v
--- a/kallithea/lib/celerylib/__init__.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/celerylib/__init__.py	Mon May 04 19:24:04 2020 +0200
@@ -33,9 +33,9 @@
 from decorator import decorator
 from tg import config
 
-from kallithea import CELERY_EAGER, CELERY_ON
+import kallithea
 from kallithea.lib.pidlock import DaemonLock, LockHeld
-from kallithea.lib.utils2 import safe_str
+from kallithea.lib.utils2 import safe_bytes
 from kallithea.model import meta
 
 
@@ -57,10 +57,10 @@
 
 
 def task(f_org):
-    """Wrapper of celery.task.task, running async if CELERY_ON
+    """Wrapper of celery.task.task, running async if CELERY_APP
     """
 
-    if CELERY_ON:
+    if kallithea.CELERY_APP:
         def f_async(*args, **kwargs):
             log.info('executing %s task', f_org.__name__)
             try:
@@ -68,8 +68,7 @@
             finally:
                 log.info('executed %s task', f_org.__name__)
         f_async.__name__ = f_org.__name__
-        from kallithea.lib import celerypylons
-        runner = celerypylons.task(ignore_result=True)(f_async)
+        runner = kallithea.CELERY_APP.task(ignore_result=True)(f_async)
 
         def f_wrapped(*args, **kwargs):
             t = runner.apply_async(args=args, kwargs=kwargs)
@@ -95,7 +94,7 @@
     func_name = str(func.__name__) if hasattr(func, '__name__') else str(func)
 
     lockkey = 'task_%s.lock' % \
-        md5(func_name + '-' + '-'.join(map(safe_str, params))).hexdigest()
+        md5(safe_bytes(func_name + '-' + '-'.join(str(x) for x in params))).hexdigest()
     return lockkey
 
 
@@ -128,7 +127,7 @@
             ret = func(*fargs, **fkwargs)
             return ret
         finally:
-            if CELERY_ON and not CELERY_EAGER:
+            if kallithea.CELERY_APP and not kallithea.CELERY_EAGER:
                 meta.Session.remove()
 
     return decorator(__wrapper, func)
--- a/kallithea/lib/celerylib/tasks.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/celerylib/tasks.py	Mon May 04 19:24:04 2020 +0200
@@ -26,24 +26,23 @@
 :license: GPLv3, see LICENSE.md for more details.
 """
 
-import logging
+import email.utils
 import os
-import rfc822
 import traceback
 from collections import OrderedDict
 from operator import itemgetter
 from time import mktime
 
+import celery.utils.log
 from tg import config
 
-from kallithea import CELERY_ON
-from kallithea.lib import celerylib
-from kallithea.lib.compat import json
+import kallithea
+from kallithea.lib import celerylib, ext_json
 from kallithea.lib.helpers import person
 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 str2bool
+from kallithea.lib.utils2 import ascii_bytes, str2bool
 from kallithea.lib.vcs.utils import author_email
 from kallithea.model.db import RepoGroup, Repository, Statistics, User
 
@@ -51,7 +50,7 @@
 __all__ = ['whoosh_index', 'get_commits_stats', 'send_email']
 
 
-log = logging.getLogger(__name__)
+log = celery.utils.log.get_task_logger(__name__)
 
 
 @celerylib.task
@@ -67,6 +66,11 @@
                          .run(full_index=full_index)
 
 
+# for js data compatibility cleans the key for person from '
+def akc(k):
+    return person(k).replace('"', '')
+
+
 @celerylib.task
 @celerylib.dbsession
 def get_commits_stats(repo_name, ts_min_y, ts_max_y, recurse_limit=100):
@@ -80,9 +84,6 @@
     try:
         lock = celerylib.DaemonLock(os.path.join(lockkey_path, lockkey))
 
-        # for js data compatibility cleans the key for person from '
-        akc = lambda k: person(k).replace('"', "")
-
         co_day_auth_aggr = {}
         commits_by_day_aggregate = {}
         repo = Repository.get_by_repo_name(repo_name)
@@ -118,22 +119,21 @@
             return True
 
         if cur_stats:
-            commits_by_day_aggregate = OrderedDict(json.loads(
+            commits_by_day_aggregate = OrderedDict(ext_json.loads(
                                         cur_stats.commit_activity_combined))
-            co_day_auth_aggr = json.loads(cur_stats.commit_activity)
+            co_day_auth_aggr = ext_json.loads(cur_stats.commit_activity)
 
         log.debug('starting parsing %s', parse_limit)
-        lmktime = mktime
 
-        last_rev = last_rev + 1 if last_rev >= 0 else 0
+        last_rev = last_rev + 1 if last_rev and last_rev >= 0 else 0
         log.debug('Getting revisions from %s to %s',
              last_rev, last_rev + parse_limit
         )
         for cs in repo[last_rev:last_rev + parse_limit]:
             log.debug('parsing %s', cs)
             last_cs = cs  # remember last parsed changeset
-            k = lmktime([cs.date.timetuple()[0], cs.date.timetuple()[1],
-                          cs.date.timetuple()[2], 0, 0, 0, 0, 0, 0])
+            tt = cs.date.timetuple()
+            k = mktime(tt[:3] + (0, 0, 0, 0, 0, 0))
 
             if akc(cs.author) in co_day_auth_aggr:
                 try:
@@ -143,8 +143,7 @@
                 except ValueError:
                     time_pos = None
 
-                if time_pos >= 0 and time_pos is not None:
-
+                if time_pos is not None and time_pos >= 0:
                     datadict = \
                         co_day_auth_aggr[akc(cs.author)]['data'][time_pos]
 
@@ -195,8 +194,8 @@
             }
 
         stats = cur_stats if cur_stats else Statistics()
-        stats.commit_activity = json.dumps(co_day_auth_aggr)
-        stats.commit_activity_combined = json.dumps(overview_data)
+        stats.commit_activity = ascii_bytes(ext_json.dumps(co_day_auth_aggr))
+        stats.commit_activity_combined = ascii_bytes(ext_json.dumps(overview_data))
 
         log.debug('last revision %s', last_rev)
         leftovers = len(repo.revisions[last_rev:])
@@ -204,7 +203,7 @@
 
         if last_rev == 0 or leftovers < parse_limit:
             log.debug('getting code trending stats')
-            stats.languages = json.dumps(__get_codes_stats(repo_name))
+            stats.languages = ascii_bytes(ext_json.dumps(__get_codes_stats(repo_name)))
 
         try:
             stats.repository = dbrepo
@@ -221,7 +220,7 @@
         lock.release()
 
         # execute another task if celery is enabled
-        if len(repo.revisions) > 1 and CELERY_ON and recurse_limit > 0:
+        if len(repo.revisions) > 1 and kallithea.CELERY_APP and recurse_limit > 0:
             get_commits_stats(repo_name, ts_min_y, ts_max_y, recurse_limit - 1)
         elif recurse_limit <= 0:
             log.debug('Not recursing - limit has been reached')
@@ -234,7 +233,7 @@
 
 @celerylib.task
 @celerylib.dbsession
-def send_email(recipients, subject, body='', html_body='', headers=None, author=None):
+def send_email(recipients, subject, body='', html_body='', headers=None, from_name=None):
     """
     Sends an email with defined parameters from the .ini files.
 
@@ -244,7 +243,8 @@
     :param body: body of the mail
     :param html_body: html version of body
     :param headers: dictionary of prepopulated e-mail headers
-    :param author: User object of the author of this mail, if known and relevant
+    :param from_name: full name to be used as sender of this mail - often a
+    .full_name_or_username value
     """
     assert isinstance(recipients, list), recipients
     if headers is None:
@@ -276,13 +276,13 @@
     # SMTP sender
     envelope_from = email_config.get('app_email_from', 'Kallithea')
     # 'From' header
-    if author is not None:
-        # set From header based on author but with a generic e-mail address
+    if from_name is not None:
+        # set From header based on from_name but with a generic e-mail address
         # In case app_email_from is in "Some Name <e-mail>" format, we first
         # extract the e-mail address.
         envelope_addr = author_email(envelope_from)
         headers['From'] = '"%s" <%s>' % (
-            rfc822.quote('%s (no-reply)' % author.full_name_or_username),
+            email.utils.quote('%s (no-reply)' % from_name),
             envelope_addr)
 
     user = email_config.get('smtp_username')
@@ -414,7 +414,7 @@
 
     DBS = celerylib.get_session()
 
-    base_path = Repository.base_path()
+    base_path = kallithea.CONFIG['base_path']
     cur_user = User.guess_instance(cur_user)
 
     repo_name = form_data['repo_name']  # fork in this case
@@ -489,7 +489,7 @@
     for _topnode, _dirnodes, filenodes in tip.walk('/'):
         for filenode in filenodes:
             ext = filenode.extension.lower()
-            if ext in LANGUAGES_EXTENSIONS_MAP.keys() and not filenode.is_binary:
+            if ext in LANGUAGES_EXTENSIONS_MAP and not filenode.is_binary:
                 if ext in code_stats:
                     code_stats[ext] += 1
                 else:
--- a/kallithea/lib/celerypylons/__init__.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/celerypylons/__init__.py	Mon May 04 19:24:04 2020 +0200
@@ -14,34 +14,64 @@
 mandatory settings.
 """
 
+import logging
+
 import celery
-import celery.result as result
 import tg
-from celery.bin import worker
-from celery.task import task
+
+import kallithea
 
 
-def celery_config(config):
-    """Return Celery config object populated from relevant settings in a config dict, such as tg.config"""
+class CeleryConfig(object):
+    imports = ['kallithea.lib.celerylib.tasks']
+    task_always_eager = False
 
-    # Verify .ini file configuration has been loaded
-    assert config['celery.imports'] == 'kallithea.lib.celerylib.tasks', 'Kallithea Celery configuration has not been loaded'
+# map from Kallithea .ini Celery 3 config names to Celery 4 config names
+celery3_compat = {
+    'broker.url': 'broker_url',
+    'celery.accept.content': 'accept_content',
+    'celery.always.eager': 'task_always_eager',
+    'celery.amqp.task.result.expires': 'result_expires',
+    'celeryd.concurrency': 'worker_concurrency',
+    'celeryd.max.tasks.per.child': 'worker_max_tasks_per_child',
+    #'celery.imports' ends up unchanged
+    'celery.result.backend': 'result_backend',
+    'celery.result.serializer': 'result_serializer',
+    'celery.task.serializer': 'task_serializer',
+}
 
-    class CeleryConfig(object):
-        pass
+list_config_names = """imports accept_content""".split()
+
+
+desupported = set([
+    'celery.result.dburi',
+    'celery.result.serialier',
+    'celery.send.task.error.emails',
+])
+
+
+log = logging.getLogger(__name__)
+
+
+def make_celery_config(config):
+    """Return Celery config object populated from relevant settings in a config dict, such as tg.config"""
 
     celery_config = CeleryConfig()
 
-    PREFIXES = """ADMINS BROKER CASSANDRA CELERYBEAT CELERYD CELERYMON CELERY EMAIL SERVER""".split()
-    LIST_PARAMS = """CELERY_IMPORTS ADMINS ROUTES CELERY_ACCEPT_CONTENT""".split()
-
     for config_key, config_value in sorted(config.items()):
-        celery_key = config_key.replace('.', '_').upper()
-        if celery_key.split('_', 1)[0] not in PREFIXES:
+        if config_key in desupported and config_value:
+            log.error('Celery configuration setting %r is no longer supported', config_key)
+        celery_key = celery3_compat.get(config_key)
+        parts = config_key.split('.', 1)
+        if celery_key:  # explicit Celery 3 backwards compatibility
+            pass
+        elif parts[0] == 'celery' and len(parts) == 2:  # Celery 4 config key
+            celery_key = parts[1]
+        else:
             continue
-        if not isinstance(config_value, basestring):
+        if not isinstance(config_value, str):
             continue
-        if celery_key in LIST_PARAMS:
+        if celery_key in list_config_names:
             celery_value = config_value.split()
         elif config_value.isdigit():
             celery_value = int(config_value)
@@ -53,6 +83,10 @@
     return celery_config
 
 
-# Create celery app from the TurboGears configuration file
-app = celery.Celery()
-app.config_from_object(celery_config(tg.config))
+def make_app():
+    """Create celery app from the TurboGears configuration file"""
+    app = celery.Celery()
+    celery_config = make_celery_config(tg.config)
+    kallithea.CELERY_EAGER = celery_config.task_always_eager
+    app.config_from_object(celery_config)
+    return app
--- a/kallithea/lib/colored_formatter.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/colored_formatter.py	Mon May 04 19:24:04 2020 +0200
@@ -15,7 +15,7 @@
 import logging
 
 
-BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = xrange(30, 38)
+BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(30, 38)
 
 # Sequences
 RESET_SEQ = "\033[0m"
--- a/kallithea/lib/compat.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/compat.py	Mon May 04 19:24:04 2020 +0200
@@ -29,7 +29,6 @@
 
 import functools
 import os
-import sys
 
 #==============================================================================
 # Hybrid property/method
@@ -43,15 +42,10 @@
 #==============================================================================
 # json
 #==============================================================================
-from kallithea.lib.ext_json import json
+from kallithea.lib import ext_json
 
 
-# alias for formatted json
-formatted_json = functools.partial(json.dumps, indent=4, sort_keys=True)
-
-
-
-
+formatted_json = functools.partial(ext_json.dumps, indent=4, sort_keys=True)
 
 
 #==============================================================================
@@ -68,3 +62,8 @@
 
 else:
     kill = os.kill
+
+
+# mute pyflakes "imported but unused"
+assert hybrid_property
+assert OrderedSet
--- a/kallithea/lib/db_manage.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/db_manage.py	Mon May 04 19:24:04 2020 +0200
@@ -26,8 +26,6 @@
 :license: GPLv3, see LICENSE.md for more details.
 """
 
-from __future__ import print_function
-
 import logging
 import os
 import sys
@@ -56,7 +54,6 @@
         self.tests = tests
         self.root = root
         self.dburi = dbconf
-        self.db_exists = False
         self.cli_args = cli_args or {}
         self.init_db(SESSION=SESSION)
 
@@ -189,7 +186,7 @@
 
                 return password
             if username is None:
-                username = raw_input('Specify admin username:')
+                username = input('Specify admin username:')
             if password is None:
                 password = get_password()
                 if not password:
@@ -198,7 +195,7 @@
                     if not password:
                         sys.exit()
             if email is None:
-                email = raw_input('Specify admin email:')
+                email = input('Specify admin email:')
             self.create_user(username, password, email, True)
         else:
             log.info('creating admin and regular test users')
@@ -294,7 +291,7 @@
         if _path is not None:
             path = _path
         elif not self.tests and not test_repo_path:
-            path = raw_input(
+            path = input(
                  'Enter a valid absolute path to store repositories. '
                  'All repositories in that path will be added automatically:'
             )
@@ -385,18 +382,18 @@
     def create_user(self, username, password, email='', admin=False):
         log.info('creating user %s', username)
         UserModel().create_or_update(username, password, email,
-                                     firstname=u'Kallithea', lastname=u'Admin',
+                                     firstname='Kallithea', lastname='Admin',
                                      active=True, admin=admin,
                                      extern_type=User.DEFAULT_AUTH_TYPE)
 
     def create_default_user(self):
         log.info('creating default user')
         # create default user for handling default permissions.
-        user = UserModel().create_or_update(username=User.DEFAULT_USER,
+        user = UserModel().create_or_update(username=User.DEFAULT_USER_NAME,
                                             password=str(uuid.uuid1())[:20],
                                             email='anonymous@kallithea-scm.org',
-                                            firstname=u'Anonymous',
-                                            lastname=u'User')
+                                            firstname='Anonymous',
+                                            lastname='User')
         # based on configuration options activate/deactivate this user which
         # controls anonymous access
         if self.cli_args.get('public_access') is False:
@@ -419,4 +416,4 @@
         permissions that are missing, and not alter already defined ones
         """
         log.info('creating default user permissions')
-        PermissionModel().create_default_permissions(user=User.DEFAULT_USER)
+        PermissionModel().create_default_permissions(user=User.DEFAULT_USER_NAME)
--- a/kallithea/lib/diffs.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/diffs.py	Mon May 04 19:24:04 2020 +0200
@@ -32,7 +32,7 @@
 from tg.i18n import ugettext as _
 
 from kallithea.lib import helpers as h
-from kallithea.lib.utils2 import safe_unicode
+from kallithea.lib.utils2 import safe_str
 from kallithea.lib.vcs.backends.base import EmptyChangeset
 from kallithea.lib.vcs.exceptions import VCSError
 from kallithea.lib.vcs.nodes import FileNode, SubModuleNode
@@ -216,8 +216,7 @@
         stats = (0, 0)
 
     if not html_diff:
-        submodules = filter(lambda o: isinstance(o, SubModuleNode),
-                            [filenode_new, filenode_old])
+        submodules = [o for o in [filenode_new, filenode_old] if isinstance(o, SubModuleNode)]
         if submodules:
             html_diff = wrap_to_table(h.escape('Submodule %r' % submodules[0]))
         else:
@@ -235,10 +234,9 @@
     """
     # make sure we pass in default context
     context = context or 3
-    submodules = filter(lambda o: isinstance(o, SubModuleNode),
-                        [filenode_new, filenode_old])
+    submodules = [o for o in [filenode_new, filenode_old] if isinstance(o, SubModuleNode)]
     if submodules:
-        return ''
+        return b''
 
     for filenode in (filenode_old, filenode_new):
         if not isinstance(filenode, FileNode):
@@ -263,7 +261,7 @@
                                      ignore_whitespace=ignore_whitespace, context=context)
     except MemoryError:
         h.flash('MemoryError: Diff is too big', category='error')
-        return ''
+        return b''
 
 
 NEW_FILENODE = 1
@@ -281,7 +279,7 @@
     mentioned in the diff together with a dict of meta information that
     can be used to render it in a HTML template.
     """
-    _diff_git_re = re.compile('^diff --git', re.MULTILINE)
+    _diff_git_re = re.compile(b'^diff --git', re.MULTILINE)
 
     def __init__(self, diff, vcs='hg', diff_limit=None, inline_diff=True):
         """
@@ -291,10 +289,10 @@
             based on that parameter cut off will be triggered, set to None
             to show full diff
         """
-        if not isinstance(diff, basestring):
-            raise Exception('Diff must be a basestring got %s instead' % type(diff))
+        if not isinstance(diff, bytes):
+            raise Exception('Diff must be bytes - got %s' % type(diff))
 
-        self._diff = diff
+        self._diff = memoryview(diff)
         self.adds = 0
         self.removes = 0
         self.diff_limit = diff_limit
@@ -317,7 +315,7 @@
                 self.limited_diff = True
                 continue
 
-            head, diff_lines = _get_header(self.vcs, buffer(self._diff, start, end - start))
+            head, diff_lines = _get_header(self.vcs, self._diff[start:end])
 
             op = None
             stats = {
@@ -399,7 +397,7 @@
                 'new_lineno': '',
                 'action':     'context',
                 'line':       msg,
-                } for _op, msg in stats['ops'].iteritems()
+                } for _op, msg in stats['ops'].items()
                   if _op not in [MOD_FILENODE]])
 
             _files.append({
@@ -420,22 +418,22 @@
             for chunk in diff_data['chunks']:
                 lineiter = iter(chunk)
                 try:
-                    peekline = lineiter.next()
+                    peekline = next(lineiter)
                     while True:
                         # find a first del line
                         while peekline['action'] != 'del':
-                            peekline = lineiter.next()
+                            peekline = next(lineiter)
                         delline = peekline
-                        peekline = lineiter.next()
+                        peekline = next(lineiter)
                         # if not followed by add, eat all following del lines
                         if peekline['action'] != 'add':
                             while peekline['action'] == 'del':
-                                peekline = lineiter.next()
+                                peekline = next(lineiter)
                             continue
                         # found an add - make sure it is the only one
                         addline = peekline
                         try:
-                            peekline = lineiter.next()
+                            peekline = next(lineiter)
                         except StopIteration:
                             # add was last line - ok
                             _highlight_inline_diff(delline, addline)
@@ -479,10 +477,10 @@
             return ' <i></i>'
         assert False
 
-    return _escape_re.sub(substitute, safe_unicode(string))
+    return _escape_re.sub(substitute, safe_str(string))
 
 
-_git_header_re = re.compile(r"""
+_git_header_re = re.compile(br"""
     ^diff[ ]--git[ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
     (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
        ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
@@ -499,7 +497,7 @@
 """, re.VERBOSE | re.MULTILINE)
 
 
-_hg_header_re = re.compile(r"""
+_hg_header_re = re.compile(br"""
     ^diff[ ]--git[ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
     (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
        ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
@@ -518,6 +516,9 @@
 """, re.VERBOSE | re.MULTILINE)
 
 
+_header_next_check = re.compile(br'''(?!@)(?!literal )(?!delta )''')
+
+
 def _get_header(vcs, diff_chunk):
     """
     Parses a Git diff for a single file (header and chunks) and returns a tuple with:
@@ -537,11 +538,11 @@
         match = _hg_header_re.match(diff_chunk)
     if match is None:
         raise Exception('diff not recognized as valid %s diff' % vcs)
-    meta_info = match.groupdict()
+    meta_info = {k: None if v is None else safe_str(v) for k, v in match.groupdict().items()}
     rest = diff_chunk[match.end():]
-    if rest and not rest.startswith('@') and not rest.startswith('literal ') and not rest.startswith('delta '):
-        raise Exception('cannot parse %s diff header: %r followed by %r' % (vcs, diff_chunk[:match.end()], rest[:1000]))
-    diff_lines = (_escaper(m.group(0)) for m in re.finditer(r'.*\n|.+$', rest)) # don't split on \r as str.splitlines do
+    if rest and _header_next_check.match(rest):
+        raise Exception('cannot parse %s diff header: %r followed by %r' % (vcs, safe_str(bytes(diff_chunk[:match.end()])), safe_str(bytes(rest[:1000]))))
+    diff_lines = (_escaper(m.group(0)) for m in re.finditer(br'.*\n|.+$', rest)) # don't split on \r as str.splitlines do
     return meta_info, diff_lines
 
 
@@ -557,9 +558,9 @@
     added = deleted = 0
     old_line = old_end = new_line = new_end = None
 
+    chunks = []
     try:
-        chunks = []
-        line = diff_lines.next()
+        line = next(diff_lines)
 
         while True:
             lines = []
@@ -590,7 +591,7 @@
                         'line':       line,
                     })
 
-            line = diff_lines.next()
+            line = next(diff_lines)
 
             while old_line < old_end or new_line < new_end:
                 if not line:
@@ -623,7 +624,7 @@
                         'line':         line[1:],
                     })
 
-                line = diff_lines.next()
+                line = next(diff_lines)
 
                 if _newline_marker.match(line):
                     # we need to append to lines, since this is not
@@ -634,7 +635,7 @@
                         'action':       'context',
                         'line':         line,
                     })
-                    line = diff_lines.next()
+                    line = next(diff_lines)
             if old_line > old_end:
                 raise Exception('error parsing diff - more than %s "-" lines at -%s+%s' % (old_end, old_line, new_line))
             if new_line > new_end:
--- a/kallithea/lib/exceptions.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/exceptions.py	Mon May 04 19:24:04 2020 +0200
@@ -74,9 +74,8 @@
     pass
 
 
-class RepositoryCreationError(Exception):
+class HgsubversionImportError(Exception):
     pass
 
-
-class HgsubversionImportError(Exception):
+class InvalidCloneUriException(Exception):
     pass
--- a/kallithea/lib/ext_json.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/ext_json.py	Mon May 04 19:24:04 2020 +0200
@@ -1,16 +1,16 @@
 """
-Extended JSON encoder for json
+Extended JSON encoder with support for more data types
 
-json.org does not specify how date time can be represented - monkeypatch it to do something.
+json.org does not specify how date time can be represented - just encode it somehow and ignore decoding ...
 """
 
 import datetime
 import decimal
 import functools
-import json  # is re-exported after monkey patching
+import json
 
 
-__all__ = ['json']
+__all__ = ['dumps', 'dump', 'load', 'loads']
 
 
 def _is_tz_aware(value):
@@ -70,10 +70,12 @@
         try:
             return _obj_dump(obj)
         except NotImplementedError:
-            pass
+            pass  # quiet skipping of unsupported types!
         raise TypeError("%r is not JSON serializable" % (obj,))
 
 
-# monkey-patch and export JSON encoder to use custom encoding method
-json.dumps = functools.partial(json.dumps, cls=ExtendedEncoder)
-json.dump = functools.partial(json.dump, cls=ExtendedEncoder)
+dumps = functools.partial(json.dumps, cls=ExtendedEncoder)
+dump = functools.partial(json.dump, cls=ExtendedEncoder)
+# No special support for loading these types back!!!
+load = json.load
+loads = json.loads
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/kallithea/lib/feeds.py	Mon May 04 19:24:04 2020 +0200
@@ -0,0 +1,152 @@
+# -*- 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/>.
+
+"""
+kallithea.lib.feeds
+~~~~~~~~~~~~~~~~~~~
+
+Shared code for providing RSS and ATOM feeds.
+"""
+
+import datetime
+import re
+
+import mako.template
+
+
+language = 'en-us'
+ttl = "5"
+
+
+# From ``django.utils.feedgenerator`` via webhelpers.feedgenerator
+def rfc2822_date(date):
+    # We do this ourselves to be timezone aware, email.Utils is not tz aware.
+    if getattr(date, "tzinfo", False):
+        time_str = date.strftime('%a, %d %b %Y %H:%M:%S ')
+        offset = date.tzinfo.utcoffset(date)
+        timezone = (offset.days * 24 * 60) + (offset.seconds / 60)
+        hour, minute = divmod(timezone, 60)
+        return time_str + "%+03d%02d" % (hour, minute)
+    else:
+        return date.strftime('%a, %d %b %Y %H:%M:%S -0000')
+
+# From ``django.utils.feedgenerator`` via webhelpers.feedgenerator
+def rfc3339_date(date):
+    if getattr(date, "tzinfo", False):
+        time_str = date.strftime('%Y-%m-%dT%H:%M:%S')
+        offset = date.tzinfo.utcoffset(date)
+        timezone = (offset.days * 24 * 60) + (offset.seconds / 60)
+        hour, minute = divmod(timezone, 60)
+        return time_str + "%+03d:%02d" % (hour, minute)
+    else:
+        return date.strftime('%Y-%m-%dT%H:%M:%SZ')
+
+# From ``django.utils.feedgenerator`` via webhelpers.feedgenerator
+def get_tag_uri(url, date):
+    "Creates a TagURI. See http://diveintomark.org/archives/2004/05/28/howto-atom-id"
+    tag = re.sub('^http://', '', url)
+    if date is not None:
+        tag = re.sub('/', ',%s:/' % date.strftime('%Y-%m-%d'), tag, 1)
+    tag = re.sub('#', '/', tag)
+    return 'tag:' + tag
+
+
+class Attributes(object):
+    """Simple namespace for attribute dict access in mako and elsewhere"""
+    def __init__(self, a_dict):
+        self.__dict__ = a_dict
+
+
+class _Feeder(object):
+
+    content_type = None
+    template = None  # subclass must provide a mako.template.Template
+
+    @classmethod
+    def render(cls, header, entries):
+        try:
+            latest_pubdate = max(
+                pubdate for pubdate in (e.get('pubdate') for e in entries)
+                if pubdate
+            )
+        except ValueError:  # max() arg is an empty sequence ... or worse
+            latest_pubdate = datetime.datetime.now()
+
+        return cls.template.render(
+            language=language,
+            ttl=ttl,  # rss only
+            latest_pubdate=latest_pubdate,
+            rfc2822_date=rfc2822_date,  # for RSS
+            rfc3339_date=rfc3339_date,  # for Atom
+            get_tag_uri=get_tag_uri,
+            entries=[Attributes(e) for e in entries],
+            **header
+        )
+
+
+class AtomFeed(_Feeder):
+
+    content_type = 'application/atom+xml'
+
+    template = mako.template.Template('''\
+<?xml version="1.0" encoding="utf-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="${language}">
+  <title>${title}</title>
+  <link href="${link}" rel="alternate"></link>
+  <id>${link}</id>
+  <updated>${rfc3339_date(latest_pubdate)}</updated>
+  % for entry in entries:
+  <entry>
+    <title>${entry.title}</title>
+    <link href="${entry.link}" rel="alternate"></link>
+    <updated>${rfc3339_date(entry.pubdate)}</updated>
+    <published>${rfc3339_date(entry.pubdate)}</published>
+    <author>
+      <name>${entry.author_name}</name>
+      <email>${entry.author_email}</email>
+    </author>
+    <id>${get_tag_uri(entry.link, entry.pubdate)}</id>
+    <summary type="html">${entry.description}</summary>
+  </entry>
+  % endfor
+</feed>
+''', default_filters=['x'], output_encoding='utf-8', encoding_errors='replace')
+
+
+class RssFeed(_Feeder):
+
+    content_type = 'application/rss+xml'
+
+    template = mako.template.Template('''\
+<?xml version="1.0" encoding="utf-8"?>
+<rss version="2.0">
+  <channel>
+    <title>${title}</title>
+    <link>${link}</link>
+    <description>${description}</description>
+    <language>${language}</language>
+    <lastBuildDate>${rfc2822_date(latest_pubdate)}</lastBuildDate>
+    <ttl>${ttl}</ttl>
+    % for entry in entries:
+    <item>
+      <title>${entry.title}</title>
+      <link>${entry.link}</link>
+      <description>${entry.description}</description>
+      <author>${entry.author_email} (${entry.author_name})</author>
+      <pubDate>${rfc2822_date(entry.pubdate)}</pubDate>
+    </item>
+    % endfor
+  </channel>
+</rss>
+''', default_filters=['x'], output_encoding='utf-8', encoding_errors='replace')
--- a/kallithea/lib/helpers.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/helpers.py	Mon May 04 19:24:04 2020 +0200
@@ -22,9 +22,8 @@
 import logging
 import random
 import re
-import StringIO
 import textwrap
-import urlparse
+import urllib.parse
 
 from beaker.cache import cache_region
 from pygments import highlight as code_highlight
@@ -49,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_int, safe_str, safe_unicode, str2bool, time_to_datetime
+from kallithea.lib.utils2 import credentials_filter, safe_bytes, safe_int, safe_str, str2bool, time_to_datetime
 from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
 #==============================================================================
@@ -58,6 +57,25 @@
 from kallithea.lib.vcs.utils import author_email, author_name
 
 
+# mute pyflakes "imported but unused"
+assert Option
+assert checkbox
+assert end_form
+assert password
+assert radio
+assert submit
+assert text
+assert textarea
+assert format_byte_size
+assert chop_at
+assert wrap_paragraphs
+assert HasPermissionAny
+assert HasRepoGroupPermissionLevel
+assert HasRepoPermissionLevel
+assert time_to_datetime
+assert EmptyChangeset
+
+
 log = logging.getLogger(__name__)
 
 
@@ -167,7 +185,7 @@
         for x in option_list:
             if isinstance(x, tuple) and len(x) == 2:
                 value, label = x
-            elif isinstance(x, basestring):
+            elif isinstance(x, str):
                 value = label = x
             else:
                 log.error('invalid select option %r', x)
@@ -177,7 +195,7 @@
                 for x in value:
                     if isinstance(x, tuple) and len(x) == 2:
                         group_value, group_label = x
-                    elif isinstance(x, basestring):
+                    elif isinstance(x, str):
                         group_value = group_label = x
                     else:
                         log.error('invalid select option %r', x)
@@ -200,14 +218,12 @@
     :param path:
     """
 
-    return 'C-%s-%s' % (short_id(raw_id), hashlib.md5(safe_str(path)).hexdigest()[:12])
+    return 'C-%s-%s' % (short_id(raw_id), hashlib.md5(safe_bytes(path)).hexdigest()[:12])
 
 
 class _FilesBreadCrumbs(object):
 
     def __call__(self, repo_name, rev, paths):
-        if isinstance(paths, str):
-            paths = safe_unicode(paths)
         url_l = [link_to(repo_name, url('files_home',
                                         repo_name=repo_name,
                                         revision=rev, f_path=''),
@@ -246,12 +262,12 @@
             yield i, t
 
     def _wrap_tablelinenos(self, inner):
-        dummyoutfile = StringIO.StringIO()
+        inner_lines = []
         lncount = 0
         for t, line in inner:
             if t:
                 lncount += 1
-            dummyoutfile.write(line)
+            inner_lines.append(line)
 
         fl = self.linenostart
         mw = len(str(lncount + fl - 1))
@@ -304,7 +320,7 @@
                       '<tr><td class="linenos"><div class="linenodiv">'
                       '<pre>' + ls + '</pre></div></td>'
                       '<td id="hlcode" class="code">')
-        yield 0, dummyoutfile.getvalue()
+        yield 0, ''.join(inner_lines)
         yield 0, '</td></tr></table>'
 
 
@@ -331,7 +347,48 @@
     """
     lexer = get_custom_lexer(filenode.extension) or filenode.lexer
     return literal(markup_whitespace(
-        code_highlight(filenode.content, lexer, CodeHtmlFormatter(**kwargs))))
+        code_highlight(safe_str(filenode.content), lexer, CodeHtmlFormatter(**kwargs))))
+
+
+def hsv_to_rgb(h, s, v):
+    if s == 0.0:
+        return v, v, v
+    i = int(h * 6.0)  # XXX assume int() truncates!
+    f = (h * 6.0) - i
+    p = v * (1.0 - s)
+    q = v * (1.0 - s * f)
+    t = v * (1.0 - s * (1.0 - f))
+    i = i % 6
+    if i == 0:
+        return v, t, p
+    if i == 1:
+        return q, v, p
+    if i == 2:
+        return p, v, t
+    if i == 3:
+        return p, q, v
+    if i == 4:
+        return t, p, v
+    if i == 5:
+        return v, p, q
+
+
+def gen_color(n=10000):
+    """generator for getting n of evenly distributed colors using
+    hsv color and golden ratio. It always return same order of colors
+
+    :returns: RGB tuple
+    """
+
+    golden_ratio = 0.618033988749895
+    h = 0.22717784590367374
+
+    for _unused in range(n):
+        h += golden_ratio
+        h %= 1
+        HSV_tuple = [h, 0.95, 0.95]
+        RGB_tuple = hsv_to_rgb(*HSV_tuple)
+        yield [str(int(x * 256)) for x in RGB_tuple]
 
 
 def pygmentize_annotation(repo_name, filenode, **kwargs):
@@ -340,82 +397,38 @@
 
     :param filenode:
     """
-
+    cgenerator = gen_color()
     color_dict = {}
 
-    def gen_color(n=10000):
-        """generator for getting n of evenly distributed colors using
-        hsv color and golden ratio. It always return same order of colors
-
-        :returns: RGB tuple
-        """
-
-        def hsv_to_rgb(h, s, v):
-            if s == 0.0:
-                return v, v, v
-            i = int(h * 6.0)  # XXX assume int() truncates!
-            f = (h * 6.0) - i
-            p = v * (1.0 - s)
-            q = v * (1.0 - s * f)
-            t = v * (1.0 - s * (1.0 - f))
-            i = i % 6
-            if i == 0:
-                return v, t, p
-            if i == 1:
-                return q, v, p
-            if i == 2:
-                return p, v, t
-            if i == 3:
-                return p, q, v
-            if i == 4:
-                return t, p, v
-            if i == 5:
-                return v, p, q
-
-        golden_ratio = 0.618033988749895
-        h = 0.22717784590367374
-
-        for _unused in xrange(n):
-            h += golden_ratio
-            h %= 1
-            HSV_tuple = [h, 0.95, 0.95]
-            RGB_tuple = hsv_to_rgb(*HSV_tuple)
-            yield map(lambda x: str(int(x * 256)), RGB_tuple)
-
-    cgenerator = gen_color()
-
     def get_color_string(cs):
         if cs in color_dict:
             col = color_dict[cs]
         else:
-            col = color_dict[cs] = cgenerator.next()
+            col = color_dict[cs] = next(cgenerator)
         return "color: rgb(%s)! important;" % (', '.join(col))
 
-    def url_func(repo_name):
-
-        def _url_func(changeset):
-            author = escape(changeset.author)
-            date = changeset.date
-            message = escape(changeset.message)
-            tooltip_html = ("<b>Author:</b> %s<br/>"
-                            "<b>Date:</b> %s</b><br/>"
-                            "<b>Message:</b> %s") % (author, date, message)
+    def url_func(changeset):
+        author = escape(changeset.author)
+        date = changeset.date
+        message = escape(changeset.message)
+        tooltip_html = ("<b>Author:</b> %s<br/>"
+                        "<b>Date:</b> %s</b><br/>"
+                        "<b>Message:</b> %s") % (author, date, message)
 
-            lnk_format = show_id(changeset)
-            uri = link_to(
-                    lnk_format,
-                    url('changeset_home', repo_name=repo_name,
-                        revision=changeset.raw_id),
-                    style=get_color_string(changeset.raw_id),
-                    **{'data-toggle': 'popover',
-                       'data-content': tooltip_html}
-                  )
+        lnk_format = show_id(changeset)
+        uri = link_to(
+                lnk_format,
+                url('changeset_home', repo_name=repo_name,
+                    revision=changeset.raw_id),
+                style=get_color_string(changeset.raw_id),
+                **{'data-toggle': 'popover',
+                   'data-content': tooltip_html}
+              )
 
-            uri += '\n'
-            return uri
-        return _url_func
+        uri += '\n'
+        return uri
 
-    return literal(markup_whitespace(annotate_highlight(filenode, url_func(repo_name), **kwargs)))
+    return literal(markup_whitespace(annotate_highlight(filenode, url_func, **kwargs)))
 
 
 class _Message(object):
@@ -424,22 +437,14 @@
     Converting the message to a string returns the message text. Instances
     also have the following attributes:
 
-    * ``message``: the message text.
     * ``category``: the category specified when the message was created.
+    * ``message``: the html-safe message text.
     """
 
     def __init__(self, category, message):
         self.category = category
         self.message = message
 
-    def __str__(self):
-        return self.message
-
-    __unicode__ = __str__
-
-    def __html__(self):
-        return escape(safe_unicode(self.message))
-
 
 def _session_flash_messages(append=None, clear=False):
     """Manage a message queue in tg.session: return the current message queue
@@ -461,7 +466,7 @@
     return flash_messages
 
 
-def flash(message, category=None, logf=None):
+def flash(message, category, logf=None):
     """
     Show a message to the user _and_ log it through the specified function
 
@@ -471,14 +476,22 @@
     logf defaults to log.info, unless category equals 'success', in which
     case logf defaults to log.debug.
     """
+    assert category in ('error', 'success', 'warning'), category
+    if hasattr(message, '__html__'):
+        # render to HTML for storing in cookie
+        safe_message = str(message)
+    else:
+        # Apply str - the message might be an exception with __str__
+        # Escape, so we can trust the result without further escaping, without any risk of injection
+        safe_message = html_escape(str(message))
     if logf is None:
         logf = log.info
         if category == 'success':
             logf = log.debug
 
-    logf('Flash %s: %s', category, message)
+    logf('Flash %s: %s', category, safe_message)
 
-    _session_flash_messages(append=(category, message))
+    _session_flash_messages(append=(category, safe_message))
 
 
 def pop_flash_messages():
@@ -486,14 +499,22 @@
 
     The return value is a list of ``Message`` objects.
     """
-    return [_Message(*m) for m in _session_flash_messages(clear=True)]
+    return [_Message(category, message) for category, message in _session_flash_messages(clear=True)]
 
 
-age = lambda x, y=False: _age(x, y)
-capitalize = lambda x: x.capitalize()
+def age(x, y=False):
+    return _age(x, y)
+
+def capitalize(x):
+    return x.capitalize()
+
 email = author_email
-short_id = lambda x: x[:12]
-hide_credentials = lambda x: ''.join(credentials_filter(x))
+
+def short_id(x):
+    return x[:12]
+
+def hide_credentials(x):
+    return ''.join(credentials_filter(x))
 
 
 def show_id(cs):
@@ -516,8 +537,7 @@
 
 def fmt_date(date):
     if date:
-        return date.strftime("%Y-%m-%d %H:%M:%S").decode('utf-8')
-
+        return date.strftime("%Y-%m-%d %H:%M:%S")
     return ""
 
 
@@ -548,7 +568,7 @@
     email = author_email(author)
     if email:
         from kallithea.model.db import User
-        user = User.get_by_email(email, cache=True) # cache will only use sql_cache_short
+        user = User.get_by_email(email)
         if user is not None:
             return getattr(user, show_attr)
     return None
@@ -590,15 +610,12 @@
 
 def person_by_id(id_, show_attr="username"):
     from kallithea.model.db import User
-    # attr to return from fetched user
-    person_getter = lambda usr: getattr(usr, show_attr)
-
     # maybe it's an ID ?
     if str(id_).isdigit() or isinstance(id_, int):
         id_ = int(id_)
         user = User.get(id_)
         if user is not None:
-            return person_getter(user)
+            return getattr(user, show_attr)
     return id_
 
 
@@ -677,7 +694,7 @@
             return _op, _name
 
         revs = []
-        if len(filter(lambda v: v != '', revs_ids)) > 0:
+        if len([v for v in revs_ids if v != '']) > 0:
             repo = None
             for rev in revs_ids[:revs_top_limit]:
                 _op, _name = _get_op(rev)
@@ -850,10 +867,7 @@
             .replace('[', '<b>') \
             .replace(']', '</b>')
 
-    action_params_func = lambda: ""
-
-    if callable(action_str[1]):
-        action_params_func = action_str[1]
+    action_params_func = action_str[1] if callable(action_str[1]) else (lambda: "")
 
     def action_parser_icon():
         action = user_log.action
@@ -937,13 +951,13 @@
     if email_address == _def:
         return default
 
-    parsed_url = urlparse.urlparse(url.current(qualified=True))
+    parsed_url = urllib.parse.urlparse(url.current(qualified=True))
     url = (c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL) \
                .replace('{email}', email_address) \
-               .replace('{md5email}', hashlib.md5(safe_str(email_address).lower()).hexdigest()) \
+               .replace('{md5email}', hashlib.md5(safe_bytes(email_address).lower()).hexdigest()) \
                .replace('{netloc}', parsed_url.netloc) \
                .replace('{scheme}', parsed_url.scheme) \
-               .replace('{size}', safe_str(size))
+               .replace('{size}', str(size))
     return url
 
 
@@ -959,7 +973,7 @@
         suf = ''
         if len(nodes) > 30:
             suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
-        return literal(pref + '<br/> '.join([safe_unicode(x.path)
+        return literal(pref + '<br/> '.join([x.path
                                              for x in nodes[:30]]) + suf)
     else:
         return ': ' + _('No files')
@@ -1069,6 +1083,8 @@
     URLs links to what they say.
     Issues are linked to given issue-server.
     If link_ is provided, all text not already linking somewhere will link there.
+    >>> urlify_text("Urlify http://example.com/ and 'https://example.com' *and* <b>markup/b>")
+    literal('Urlify <a href="http://example.com/">http://example.com/</a> and &#39;<a href="https://example.com&apos">https://example.com&apos</a>; <b>*and*</b> &lt;b&gt;markup/b&gt;')
     """
 
     def _replace(match_obj):
@@ -1162,10 +1178,11 @@
         assert CONFIG['sqlalchemy.url'] # make sure config has been loaded
 
         # Build chain of urlify functions, starting with not doing any transformation
-        tmp_urlify_issues_f = lambda s: s
+        def tmp_urlify_issues_f(s):
+            return s
 
         issue_pat_re = re.compile(r'issue_pat(.*)')
-        for k in CONFIG.keys():
+        for k in CONFIG:
             # Find all issue_pat* settings that also have corresponding server_link and prefix configuration
             m = issue_pat_re.match(k)
             if m is None:
@@ -1174,18 +1191,27 @@
             issue_pat = CONFIG.get(k)
             issue_server_link = CONFIG.get('issue_server_link%s' % suffix)
             issue_sub = CONFIG.get('issue_sub%s' % suffix)
-            if not issue_pat or not issue_server_link or issue_sub is None: # issue_sub can be empty but should be present
-                log.error('skipping incomplete issue pattern %r: %r -> %r %r', suffix, issue_pat, issue_server_link, issue_sub)
+            issue_prefix = CONFIG.get('issue_prefix%s' % suffix)
+            if issue_prefix:
+                log.error('found unsupported issue_prefix%s = %r - use issue_sub%s instead', suffix, issue_prefix, suffix)
+            if not issue_pat:
+                log.error('skipping incomplete issue pattern %r: it needs a regexp', k)
+                continue
+            if not issue_server_link:
+                log.error('skipping incomplete issue pattern %r: it needs issue_server_link%s', k, suffix)
+                continue
+            if issue_sub is None: # issue_sub can be empty but should be present
+                log.error('skipping incomplete issue pattern %r: it needs (a potentially empty) issue_sub%s', k, suffix)
                 continue
 
             # Wrap tmp_urlify_issues_f with substitution of this pattern, while making sure all loop variables (and compiled regexpes) are bound
             try:
                 issue_re = re.compile(issue_pat)
             except re.error as e:
-                log.error('skipping invalid issue pattern %r: %r -> %r %r. Error: %s', suffix, issue_pat, issue_server_link, issue_sub, str(e))
+                log.error('skipping invalid issue pattern %r: %r -> %r %r. Error: %s', k, issue_pat, issue_server_link, issue_sub, str(e))
                 continue
 
-            log.debug('issue pattern %r: %r -> %r %r', suffix, issue_pat, issue_server_link, issue_sub)
+            log.debug('issue pattern %r: %r -> %r %r', k, issue_pat, issue_server_link, issue_sub)
 
             def issues_replace(match_obj,
                                issue_server_link=issue_server_link, issue_sub=issue_sub):
@@ -1214,9 +1240,9 @@
                      'url': issue_url,
                      'text': issue_text,
                     }
-            tmp_urlify_issues_f = (lambda s,
-                                          issue_re=issue_re, issues_replace=issues_replace, chain_f=tmp_urlify_issues_f:
-                                   issue_re.sub(issues_replace, chain_f(s)))
+
+            def tmp_urlify_issues_f(s, issue_re=issue_re, issues_replace=issues_replace, chain_f=tmp_urlify_issues_f):
+                return issue_re.sub(issues_replace, chain_f(s))
 
         # Set tmp function globally - atomically
         _urlify_issues_f = tmp_urlify_issues_f
@@ -1229,7 +1255,7 @@
     Render plain text with revision hashes and issue references urlified
     and with @mention highlighting.
     """
-    s = safe_unicode(source)
+    s = safe_str(source)
     s = urlify_text(s, repo_name=repo_name)
     return literal('<div class="formatted-fixed">%s</div>' % s)
 
--- a/kallithea/lib/hooks.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/hooks.py	Mon May 04 19:24:04 2020 +0200
@@ -25,16 +25,17 @@
 :license: GPLv3, see LICENSE.md for more details.
 """
 
-import binascii
 import os
+import sys
 import time
 
+import mercurial.scmutil
+
 from kallithea.lib import helpers as h
 from kallithea.lib.exceptions import UserCreationError
-from kallithea.lib.utils import action_logger, make_ui, setup_cache_regions
-from kallithea.lib.utils2 import get_hook_environment, safe_str, safe_unicode
+from kallithea.lib.utils import action_logger, make_ui
+from kallithea.lib.utils2 import HookEnvironmentError, ascii_str, get_hook_environment, safe_bytes, safe_str
 from kallithea.lib.vcs.backends.base import EmptyChangeset
-from kallithea.lib.vcs.utils.hgcompat import revrange
 from kallithea.model.db import Repository, User
 
 
@@ -43,7 +44,7 @@
         alias += '.'
 
     size_scm, size_root = 0, 0
-    for path, dirs, files in os.walk(safe_str(root_path)):
+    for path, dirs, files in os.walk(root_path):
         if path.find(alias) != -1:
             for f in files:
                 try:
@@ -65,16 +66,19 @@
 
 
 def repo_size(ui, repo, hooktype=None, **kwargs):
-    """Presents size of repository after push"""
-    size_hg_f, size_root_f, size_total_f = _get_scm_size('.hg', repo.root)
+    """Show size of Mercurial repository.
+
+    Called as Mercurial hook changegroup.repo_size after push.
+    """
+    size_hg_f, size_root_f, size_total_f = _get_scm_size('.hg', safe_str(repo.root))
 
     last_cs = repo[len(repo) - 1]
 
     msg = ('Repository size .hg: %s Checkout: %s Total: %s\n'
            'Last revision is now r%s:%s\n') % (
-        size_hg_f, size_root_f, size_total_f, last_cs.rev(), last_cs.hex()[:12]
+        size_hg_f, size_root_f, size_total_f, last_cs.rev(), ascii_str(last_cs.hex())[:12]
     )
-    ui.status(msg)
+    ui.status(safe_bytes(msg))
 
 
 def log_pull_action(ui, repo, **kwargs):
@@ -102,15 +106,15 @@
 
 def log_push_action(ui, repo, node, node_last, **kwargs):
     """
-    Entry point for Mercurial hook changegroup.push_logger.
+    Register that changes have been added to the repo - log the action *and* invalidate caches.
+    Note: This hook is not only logging, but also the side effect invalidating
+    caches! The function should perhaps be renamed.
+
+    Called as Mercurial hook changegroup.kallithea_log_push_action .
 
     The pushed changesets is given by the revset 'node:node_last'.
-
-    Note: This hook is not only logging, but also the side effect invalidating
-    cahes! The function should perhaps be renamed.
     """
-    _h = binascii.hexlify
-    revs = [_h(repo[r].node()) for r in revrange(repo, [node + ':' + node_last])]
+    revs = [ascii_str(repo[r].hex()) for r in mercurial.scmutil.revrange(repo, [b'%s:%s' % (node, node_last)])]
     process_pushed_raw_ids(revs)
     return 0
 
@@ -119,7 +123,7 @@
     """
     Register that changes have been added to the repo - log the action *and* invalidate caches.
 
-    Called from  Mercurial changegroup.push_logger calling hook log_push_action,
+    Called from Mercurial changegroup.kallithea_log_push_action calling hook log_push_action,
     or from the Git post-receive hook calling handle_git_post_receive ...
     or from scm _handle_push.
     """
@@ -302,31 +306,23 @@
     they thus need enough info to be able to create an app environment and
     connect to the database.
     """
-    from paste.deploy import appconfig
-    from sqlalchemy import engine_from_config
-    from kallithea.config.environment import load_environment
-    from kallithea.model.base import init_model
+    import paste.deploy
+    import kallithea.config.middleware
 
     extras = get_hook_environment()
-    ini_file_path = extras['config']
-    #logging.config.fileConfig(ini_file_path) # Note: we are in a different process - don't use configured logging
-    app_conf = appconfig('config:%s' % ini_file_path)
-    conf = load_environment(app_conf.global_conf, app_conf.local_conf)
 
-    setup_cache_regions(conf)
+    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)
 
-    engine = engine_from_config(conf, 'sqlalchemy.')
-    init_model(engine)
-
-    repo_path = safe_unicode(repo_path)
     # fix if it's not a bare repo
     if repo_path.endswith(os.sep + '.git'):
         repo_path = repo_path[:-5]
 
     repo = Repository.get_by_full_path(repo_path)
     if not repo:
-        raise OSError('Repository %s not found in database'
-                      % (safe_str(repo_path)))
+        raise OSError('Repository %s not found in database' % repo_path)
 
     baseui = make_ui()
     return baseui, repo
@@ -340,7 +336,11 @@
 
 def handle_git_post_receive(repo_path, git_stdin_lines):
     """Called from Git post-receive hook"""
-    baseui, repo = _hook_environment(repo_path)
+    try:
+        baseui, repo = _hook_environment(repo_path)
+    except HookEnvironmentError as e:
+        sys.stderr.write("Skipping Kallithea Git post-recieve hook %r.\nGit was apparently not invoked by Kallithea: %s\n" % (sys.argv[0], e))
+        return 0
 
     # the post push hook should never use the cached instance
     scm_repo = repo.scm_instance_no_cache()
@@ -363,19 +363,20 @@
             if push_ref['old_rev'] == EmptyChangeset().raw_id:
                 # update the symbolic ref if we push new repo
                 if scm_repo.is_empty():
-                    scm_repo._repo.refs.set_symbolic_ref('HEAD',
-                                        'refs/heads/%s' % push_ref['name'])
+                    scm_repo._repo.refs.set_symbolic_ref(
+                        b'HEAD',
+                        b'refs/heads/%s' % safe_bytes(push_ref['name']))
 
                 # build exclude list without the ref
                 cmd = ['for-each-ref', '--format=%(refname)', 'refs/heads/*']
-                stdout, stderr = scm_repo.run_git_command(cmd)
+                stdout = scm_repo.run_git_command(cmd)
                 ref = push_ref['ref']
                 heads = [head for head in stdout.splitlines() if head != ref]
                 # now list the git revs while excluding from the list
                 cmd = ['log', push_ref['new_rev'], '--reverse', '--pretty=format:%H']
                 cmd.append('--not')
                 cmd.extend(heads) # empty list is ok
-                stdout, stderr = scm_repo.run_git_command(cmd)
+                stdout = scm_repo.run_git_command(cmd)
                 git_revs += stdout.splitlines()
 
             elif push_ref['new_rev'] == EmptyChangeset().raw_id:
@@ -384,7 +385,7 @@
             else:
                 cmd = ['log', '%(old_rev)s..%(new_rev)s' % push_ref,
                        '--reverse', '--pretty=format:%H']
-                stdout, stderr = scm_repo.run_git_command(cmd)
+                stdout = scm_repo.run_git_command(cmd)
                 git_revs += stdout.splitlines()
 
         elif _type == 'tags':
@@ -399,5 +400,5 @@
 def rejectpush(ui, **kwargs):
     """Mercurial hook to be installed as pretxnopen and prepushkey for read-only repos"""
     ex = get_hook_environment()
-    ui.warn((b"Push access to %r denied\n") % safe_str(ex.repository))
+    ui.warn(safe_bytes("Push access to %r denied\n" % ex.repository))
     return 1
--- a/kallithea/lib/indexers/__init__.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/indexers/__init__.py	Mon May 04 19:24:04 2020 +0200
@@ -146,7 +146,7 @@
             docnum = self.matcher.id()
             chunks = [offsets for offsets in self.get_chunks()]
             docs_id.append([docnum, chunks])
-            self.matcher.next()
+            self.matcher.next()  # this looks like a py2 iterator ... but it isn't
         return docs_id
 
     def __str__(self):
@@ -203,7 +203,7 @@
         return res
 
     def get_short_content(self, res, chunks):
-        return u''.join([res['content'][chunk[0]:chunk[1]] for chunk in chunks])
+        return ''.join([res['content'][chunk[0]:chunk[1]] for chunk in chunks])
 
     def get_chunks(self):
         """
--- a/kallithea/lib/indexers/daemon.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/indexers/daemon.py	Mon May 04 19:24:04 2020 +0200
@@ -39,8 +39,8 @@
 
 from kallithea.config.conf import INDEX_EXTENSIONS, INDEX_FILENAMES
 from kallithea.lib.indexers import CHGSET_IDX_NAME, CHGSETS_SCHEMA, IDX_NAME, SCHEMA
-from kallithea.lib.utils2 import safe_str, safe_unicode
-from kallithea.lib.vcs.exceptions import ChangesetError, NodeDoesNotExistError, RepositoryError
+from kallithea.lib.utils2 import safe_str
+from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, ChangesetError, NodeDoesNotExistError, RepositoryError
 from kallithea.model.db import Repository
 from kallithea.model.scm import ScmModel
 
@@ -77,8 +77,7 @@
 
         # filter repo list
         if repo_list:
-            # Fix non-ascii repo names to unicode
-            repo_list = map(safe_unicode, repo_list)
+            repo_list = set(repo_list)
             self.filtered_repo_paths = {}
             for repo_name, repo in self.repo_paths.items():
                 if repo_name in repo_list:
@@ -110,7 +109,7 @@
             self.initial = False
 
     def _get_index_revision(self, repo):
-        db_repo = Repository.get_by_repo_name(repo.name_unicode)
+        db_repo = Repository.get_by_repo_name(repo.name)
         landing_rev = 'tip'
         if db_repo:
             _rev_type, _rev = db_repo.landing_rev
@@ -133,7 +132,7 @@
             cs = self._get_index_changeset(repo)
             for _topnode, _dirs, files in cs.walk('/'):
                 for f in files:
-                    index_paths_.add(os.path.join(safe_str(repo.path), safe_str(f.path)))
+                    index_paths_.add(os.path.join(repo.path, f.path))
 
         except RepositoryError:
             log.debug(traceback.format_exc())
@@ -142,19 +141,16 @@
 
     def get_node(self, repo, path, index_rev=None):
         """
-        gets a filenode based on given full path. It operates on string for
-        hg git compatibility.
+        gets a filenode based on given full path.
 
         :param repo: scm repo instance
         :param path: full path including root location
         :return: FileNode
         """
         # FIXME: paths should be normalized ... or even better: don't include repo.path
-        path = safe_str(path)
-        repo_path = safe_str(repo.path)
-        assert path.startswith(repo_path)
-        assert path[len(repo_path)] in (os.path.sep, os.path.altsep)
-        node_path = path[len(repo_path) + 1:]
+        assert path.startswith(repo.path)
+        assert path[len(repo.path)] in (os.path.sep, os.path.altsep)
+        node_path = path[len(repo.path) + 1:]
         cs = self._get_index_changeset(repo, index_rev=index_rev)
         node = cs.get_node(node_path)
         return node
@@ -182,27 +178,27 @@
 
         indexed = indexed_w_content = 0
         if self.is_indexable_node(node):
-            u_content = node.content
-            if not isinstance(u_content, unicode):
+            bytes_content = node.content
+            if b'\0' in bytes_content:
                 log.warning('    >> %s - no text content', path)
-                u_content = u''
+                u_content = ''
             else:
                 log.debug('    >> %s', path)
+                u_content = safe_str(bytes_content)
                 indexed_w_content += 1
 
         else:
             log.debug('    >> %s - not indexable', path)
             # just index file name without it's content
-            u_content = u''
+            u_content = ''
             indexed += 1
 
-        p = safe_unicode(path)
         writer.add_document(
-            fileid=p,
-            owner=unicode(repo.contact),
-            repository_rawname=safe_unicode(repo_name),
-            repository=safe_unicode(repo_name),
-            path=p,
+            fileid=path,
+            owner=repo.contact,
+            repository_rawname=repo_name,
+            repository=repo_name,
+            path=path,
             content=u_content,
             modtime=self.get_node_mtime(node),
             extension=node.extension
@@ -237,18 +233,18 @@
             indexed += 1
             log.debug('    >> %s %s/%s', cs, indexed, total)
             writer.add_document(
-                raw_id=unicode(cs.raw_id),
-                owner=unicode(repo.contact),
+                raw_id=cs.raw_id,
+                owner=repo.contact,
                 date=cs._timestamp,
-                repository_rawname=safe_unicode(repo_name),
-                repository=safe_unicode(repo_name),
+                repository_rawname=repo_name,
+                repository=repo_name,
                 author=cs.author,
                 message=cs.message,
                 last=cs.last,
-                added=u' '.join([safe_unicode(node.path) for node in cs.added]).lower(),
-                removed=u' '.join([safe_unicode(node.path) for node in cs.removed]).lower(),
-                changed=u' '.join([safe_unicode(node.path) for node in cs.changed]).lower(),
-                parents=u' '.join([cs.raw_id for cs in cs.parents]),
+                added=' '.join(node.path for node in cs.added).lower(),
+                removed=' '.join(node.path for node in cs.removed).lower(),
+                changed=' '.join(node.path for node in cs.changed).lower(),
+                parents=' '.join(cs.raw_id for cs in cs.parents),
             )
 
         return indexed
@@ -291,7 +287,7 @@
                         continue
 
                     qp = QueryParser('repository', schema=CHGSETS_SCHEMA)
-                    q = qp.parse(u"last:t AND %s" % repo_name)
+                    q = qp.parse("last:t AND %s" % repo_name)
 
                     results = searcher.search(q)
 
@@ -303,14 +299,18 @@
                         # assuming that there is only one result, if not this
                         # may require a full re-index.
                         start_id = results[0]['raw_id']
-                        last_rev = repo.get_changeset(revision=start_id).revision
+                        try:
+                            last_rev = repo.get_changeset(revision=start_id).revision
+                        except ChangesetDoesNotExistError:
+                            log.error('previous last revision %s not found - indexing from scratch', start_id)
+                            start_id = None
 
                     # there are new changesets to index or a new repo to index
                     if last_rev == 0 or num_of_revs > last_rev + 1:
                         # delete the docs in the index for the previous
                         # last changeset(s)
                         for hit in results:
-                            q = qp.parse(u"last:t AND %s AND raw_id:%s" %
+                            q = qp.parse("last:t AND %s AND raw_id:%s" %
                                             (repo_name, hit['raw_id']))
                             writer.delete_by_query(q)
 
@@ -330,8 +330,8 @@
                     log.debug('>> NOTHING TO COMMIT TO CHANGESET INDEX<<')
 
     def update_file_index(self):
-        log.debug((u'STARTING INCREMENTAL INDEXING UPDATE FOR EXTENSIONS %s '
-                   'AND REPOS %s') % (INDEX_EXTENSIONS, self.repo_paths.keys()))
+        log.debug('STARTING INCREMENTAL INDEXING UPDATE FOR EXTENSIONS %s '
+                  'AND REPOS %s', INDEX_EXTENSIONS, ' and '.join(self.repo_paths))
 
         idx = open_dir(self.index_location, indexname=self.indexname)
         # The set of all paths in the index
@@ -390,9 +390,7 @@
                 ri_cnt = 0   # indexed
                 riwc_cnt = 0  # indexed with content
                 for path in self.get_paths(repo):
-                    path = safe_unicode(path)
                     if path in to_index or path not in indexed_paths:
-
                         # This is either a file that's changed, or a new file
                         # that wasn't indexed before. So index it!
                         i, iwc = self.add_doc(writer, path, repo, repo_name)
@@ -431,7 +429,7 @@
         file_idx = create_in(self.index_location, SCHEMA, indexname=IDX_NAME)
         file_idx_writer = file_idx.writer()
         log.debug('BUILDING INDEX FOR EXTENSIONS %s '
-                  'AND REPOS %s' % (INDEX_EXTENSIONS, self.repo_paths.keys()))
+                  'AND REPOS %s', INDEX_EXTENSIONS, ' and '.join(self.repo_paths))
 
         for repo_name, repo in sorted(self.repo_paths.items()):
             log.debug('Updating indices for repo %s', repo_name)
--- a/kallithea/lib/inifile.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/inifile.py	Mon May 04 19:24:04 2020 +0200
@@ -42,6 +42,10 @@
     'uuid': lambda: 'VERY-SECRET',
 }
 
+variable_options = {
+    'database_engine': ['sqlite', 'postgres', 'mysql'],
+    'http_server': ['waitress', 'gearbox', 'gevent', 'gunicorn', 'uwsgi'],
+}
 
 def expand(template, mako_variable_values, settings):
     """Expand mako template and tweak it.
@@ -63,16 +67,28 @@
     ... %elif conditional_options == 'option-b':
     ... some_variable = "never mind - option-b will not be used anyway ..."
     ... %endif
+    ...
+    ... [comment-section]
+    ... #variable3 = 3.0
+    ... #variable4 = 4.0
+    ... #variable5 = 5.0
+    ... variable5 = 5.1
+    ... #variable6 = 6.0
+    ... #variable6 = 6.1
+    ... #variable7 = 7.0
+    ... variable7 = 7.1
+    ... variable8 = 8.0
     ... '''
-    >>> selected_mako_conditionals = []
     >>> mako_variable_values = {'mako_variable': 'VALUE', 'mako_function': (lambda: 'FUNCTION RESULT'),
-    ...                         'conditional_options': 'option-a'}
+    ...                         'conditional_options': 'option-a', 'http_server': 'nc'}
     >>> settings = { # only partially used
-    ...     '[first-section]': {'variable2': 'VAL2', 'first_extra': 'EXTRA'},
+    ...     '[first-section]': {'variable2': 'VAL2', 'first_extra': 'EXTRA', 'spacey': ' '},
+    ...     '[comment-section]': {'variable3': '3.0', 'variable4': '4.1', 'variable5': '5.2', 'variable6': '6.2', 'variable7': '7.0', 'variable8': None, 'variable9': None},
     ...     '[third-section]': {'third_extra': ' 3'},
     ...     '[fourth-section]': {'fourth_extra': '4', 'fourth': '"four"'},
     ... }
-    >>> print expand(template, mako_variable_values, settings)
+    >>> print(expand(template, mako_variable_values, settings))
+    ERROR: http_server is 'nc' - it should be one of 'waitress', 'gearbox', 'gevent', 'gunicorn', 'uwsgi'
     <BLANKLINE>
     [first-section]
     <BLANKLINE>
@@ -81,12 +97,30 @@
     variable2 = VAL2
     <BLANKLINE>
     first_extra = EXTRA
+    spacey =
     <BLANKLINE>
     <BLANKLINE>
     # FUNCTION RESULT
     [second-section]
     # option a was chosen
     <BLANKLINE>
+    [comment-section]
+    variable3 = 3.0
+    #variable4 = 4.0
+    variable4 = 4.1
+    #variable5 = 5.0
+    #variable5 = 5.1
+    variable5 = 5.2
+    #variable6 = 6.0
+    #variable6 = 6.1
+    variable6 = 6.2
+    variable7 = 7.0
+    #variable7 = 7.1
+    #variable8 = 8.0
+    <BLANKLINE>
+    variable8 = None
+    variable9 = None
+    <BLANKLINE>
     [fourth-section]
     fourth = "four"
     fourth_extra = 4
@@ -99,6 +133,12 @@
     mako_variables.update(mako_variable_values or {})
     settings = dict((k, dict(v)) for k, v in settings.items()) # deep copy before mutating
 
+    for key, value in mako_variables.items():
+        if key in variable_options:
+            if value not in variable_options[key]:
+                print('ERROR: %s is %r - it should be one of %s' %
+                      (key, value, ', '.join(repr(x) for x in variable_options[key])))
+
     ini_lines = mako.template.Template(template).render(**mako_variables)
 
     def process_section(m):
@@ -106,25 +146,52 @@
         sectionname, lines = m.groups()
         if sectionname in settings:
             section_settings = settings.pop(sectionname)
+            add_after_key_value = {}  # map key to value it should be added after
 
-            def process_line(m):
-                """process a section line and update value if necessary"""
-                key, value = m.groups()
+            # 1st pass:
+            # comment out lines with keys that have new values
+            # find best line for keeping or un-commenting (because it has the right value) or adding after (because it is the last with other value)
+            def comment_out(m):
+                """process a section line if in section_settings and comment out and track in add_after_key_value"""
                 line = m.group(0)
-                if key in section_settings:
-                    new_line = '%s = %s' % (key, section_settings.pop(key))
-                    if new_line != line:
-                        # keep old entry as example - comments might refer to it
-                        line = '#%s\n%s' % (line, new_line)
-                return line.rstrip()
+                comment, key, line_value = m.groups()
+                if key not in section_settings:
+                    return line
+                new_value = section_settings[key]
+                if line_value == new_value or add_after_key_value.get(key) != new_value:
+                    add_after_key_value[key] = line_value
+                if comment:
+                    return line
+                return '#' + line
+
+            lines = re.sub(r'^(#)?([^#\n\s]*)[ \t]*=[ \t]*(.*)$', comment_out, lines, flags=re.MULTILINE)
 
-            # process lines that not are comments or empty and look like name=value
-            lines = re.sub(r'^([^#\n\s]*)[ \t]*=[ \t]*(.*)$', process_line, lines, flags=re.MULTILINE)
-            # add unused section settings
+            # 2nd pass:
+            # find the best comment line and un-comment or add after
+            def add_after_comment(m):
+                """process a section comment line and add new value"""
+                line = m.group(0)
+                key, line_value = m.groups()
+                if key not in section_settings:
+                    return line
+                if line_value != add_after_key_value.get(key):
+                    return line
+                new_value = section_settings[key]
+                if new_value == line_value:
+                    line = line.lstrip('#')
+                else:
+                    line += '\n%s = %s' % (key, new_value)
+                section_settings.pop(key)
+                return line
+
+            lines = re.sub(r'^#([^#\n\s]*)[ \t]*=[ \t]*(.*)$', add_after_comment, lines, flags=re.MULTILINE)
+
+            # 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()))
 
-        return sectionname + '\n' + lines
+        return sectionname + '\n' + re.sub('[ \t]+\n', '\n', lines)
 
     # process sections until comments before next section or end
     ini_lines = re.sub(r'''^
--- a/kallithea/lib/locale.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/locale.py	Mon May 04 19:24:04 2020 +0200
@@ -24,7 +24,7 @@
     Note: UTF-8 is preferred, but for example ISO-8859-1 or mbcs should also
     work under the right circumstances."""
     try:
-        u'\xe9'.encode(sys.getfilesystemencoding()) # Test using é (&eacute;)
+        '\xe9'.encode(sys.getfilesystemencoding()) # Test using é (&eacute;)
     except UnicodeEncodeError:
         log.error("Cannot encode Unicode paths to file system encoding %r", sys.getfilesystemencoding())
         for var in ['LC_ALL', 'LC_CTYPE', 'LANG']:
--- a/kallithea/lib/markup_renderer.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/markup_renderer.py	Mon May 04 19:24:04 2020 +0200
@@ -33,7 +33,7 @@
 import bleach
 import markdown as markdown_mod
 
-from kallithea.lib.utils2 import MENTIONS_REGEX, safe_unicode
+from kallithea.lib.utils2 import MENTIONS_REGEX, safe_str
 
 
 log = logging.getLogger(__name__)
@@ -119,17 +119,17 @@
         At last it will just do a simple html replacing new lines with <br/>
 
         >>> MarkupRenderer.render('''<img id="a" style="margin-top:-1000px;color:red" src="http://example.com/test.jpg">''', '.md')
-        u'<p><img id="a" src="http://example.com/test.jpg" style="color: red;"></p>'
+        '<p><img id="a" src="http://example.com/test.jpg" style="color: red;"></p>'
         >>> MarkupRenderer.render('''<img class="c d" src="file://localhost/test.jpg">''', 'b.mkd')
-        u'<p><img class="c d"></p>'
+        '<p><img class="c d"></p>'
         >>> MarkupRenderer.render('''<a href="foo">foo</a>''', 'c.mkdn')
-        u'<p><a href="foo">foo</a></p>'
+        '<p><a href="foo">foo</a></p>'
         >>> MarkupRenderer.render('''<script>alert(1)</script>''', 'd.mdown')
-        u'&lt;script&gt;alert(1)&lt;/script&gt;'
+        '&lt;script&gt;alert(1)&lt;/script&gt;'
         >>> MarkupRenderer.render('''<div onclick="alert(2)">yo</div>''', 'markdown')
-        u'<div>yo</div>'
+        '<div>yo</div>'
         >>> MarkupRenderer.render('''<a href="javascript:alert(3)">yo</a>''', 'md')
-        u'<p><a>yo</a></p>'
+        '<p><a>yo</a></p>'
         """
 
         renderer = cls._detect_renderer(source, filename)
@@ -150,7 +150,11 @@
 
     @classmethod
     def plain(cls, source, universal_newline=True):
-        source = safe_unicode(source)
+        """
+        >>> MarkupRenderer.plain('https://example.com/')
+        '<br /><a href="https://example.com/">https://example.com/</a>'
+        """
+        source = safe_str(source)
         if universal_newline:
             newline = '\n'
             source = newline.join(source.splitlines())
@@ -168,30 +172,30 @@
         with "safe" fall-back to plaintext. Output from this method should be sanitized before use.
 
         >>> MarkupRenderer.markdown('''<img id="a" style="margin-top:-1000px;color:red" src="http://example.com/test.jpg">''')
-        u'<p><img id="a" style="margin-top:-1000px;color:red" src="http://example.com/test.jpg"></p>'
+        '<p><img id="a" style="margin-top:-1000px;color:red" src="http://example.com/test.jpg"></p>'
         >>> MarkupRenderer.markdown('''<img class="c d" src="file://localhost/test.jpg">''')
-        u'<p><img class="c d" src="file://localhost/test.jpg"></p>'
+        '<p><img class="c d" src="file://localhost/test.jpg"></p>'
         >>> MarkupRenderer.markdown('''<a href="foo">foo</a>''')
-        u'<p><a href="foo">foo</a></p>'
+        '<p><a href="foo">foo</a></p>'
         >>> MarkupRenderer.markdown('''<script>alert(1)</script>''')
-        u'<script>alert(1)</script>'
+        '<script>alert(1)</script>'
         >>> MarkupRenderer.markdown('''<div onclick="alert(2)">yo</div>''')
-        u'<div onclick="alert(2)">yo</div>'
+        '<div onclick="alert(2)">yo</div>'
         >>> MarkupRenderer.markdown('''<a href="javascript:alert(3)">yo</a>''')
-        u'<p><a href="javascript:alert(3)">yo</a></p>'
+        '<p><a href="javascript:alert(3)">yo</a></p>'
         >>> MarkupRenderer.markdown('''## Foo''')
-        u'<h2>Foo</h2>'
-        >>> print MarkupRenderer.markdown('''
+        '<h2>Foo</h2>'
+        >>> print(MarkupRenderer.markdown('''
         ...     #!/bin/bash
         ...     echo "hello"
-        ... ''')
+        ... '''))
         <table class="code-highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1
         2</pre></div></td><td class="code"><div class="code-highlight"><pre><span></span><span class="ch">#!/bin/bash</span>
         <span class="nb">echo</span> <span class="s2">&quot;hello&quot;</span>
         </pre></div>
         </td></tr></table>
         """
-        source = safe_unicode(source)
+        source = safe_str(source)
         try:
             if flavored:
                 source = cls._flavored_markdown(source)
@@ -209,7 +213,7 @@
 
     @classmethod
     def rst(cls, source, safe=True):
-        source = safe_unicode(source)
+        source = safe_str(source)
         try:
             from docutils.core import publish_parts
             from docutils.parsers.rst import directives
@@ -219,7 +223,7 @@
             docutils_settings.update({'input_encoding': 'unicode',
                                       'report_level': 4})
 
-            for k, v in docutils_settings.iteritems():
+            for k, v in docutils_settings.items():
                 directives.register_directive(k, v)
 
             parts = publish_parts(source=source,
--- a/kallithea/lib/middleware/permanent_repo_url.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/middleware/permanent_repo_url.py	Mon May 04 19:24:04 2020 +0200
@@ -20,7 +20,8 @@
 """
 
 
-from kallithea.lib.utils import fix_repo_id_name, safe_str
+from kallithea.lib.utils import fix_repo_id_name
+from kallithea.lib.utils2 import safe_bytes, safe_str
 
 
 class PermanentRepoUrl(object):
@@ -30,9 +31,11 @@
         self.config = config
 
     def __call__(self, environ, start_response):
-        path_info = environ['PATH_INFO']
+        # Extract path_info as get_path_info does, but do it explicitly because
+        # we also have to do the reverse operation when patching it back in
+        path_info = safe_str(environ['PATH_INFO'].encode('latin1'))
         if path_info.startswith('/'): # it must
-            path_info = '/' + safe_str(fix_repo_id_name(path_info[1:]))
-            environ['PATH_INFO'] = path_info
+            path_info = '/' + fix_repo_id_name(path_info[1:])
+            environ['PATH_INFO'] = safe_bytes(path_info).decode('latin1')
 
         return self.application(environ, start_response)
--- a/kallithea/lib/middleware/pygrack.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/middleware/pygrack.py	Mon May 04 19:24:04 2020 +0200
@@ -33,7 +33,7 @@
 from webob import Request, Response, exc
 
 import kallithea
-from kallithea.lib.utils2 import safe_unicode
+from kallithea.lib.utils2 import ascii_bytes
 from kallithea.lib.vcs import subprocessio
 
 
@@ -87,7 +87,6 @@
 
         :param path:
         """
-        path = safe_unicode(path)
         assert path.startswith('/' + self.repo_name + '/')
         return path[len(self.repo_name) + 2:].strip('/')
 
@@ -113,14 +112,14 @@
         #                     ref_list
         #                     "0000"
         server_advert = '# service=%s\n' % git_command
-        packet_len = str(hex(len(server_advert) + 4)[2:].rjust(4, '0')).lower()
+        packet_len = hex(len(server_advert) + 4)[2:].rjust(4, '0').lower()
         _git_path = kallithea.CONFIG.get('git_path', 'git')
         cmd = [_git_path, git_command[4:],
                '--stateless-rpc', '--advertise-refs', self.content_path]
         log.debug('handling cmd %s', cmd)
         try:
             out = subprocessio.SubprocessIOChunker(cmd,
-                starting_values=[packet_len + server_advert + '0000']
+                starting_values=[ascii_bytes(packet_len + server_advert + '0000')]
             )
         except EnvironmentError as e:
             log.error(traceback.format_exc())
@@ -166,7 +165,7 @@
             log.error(traceback.format_exc())
             raise exc.HTTPExpectationFailed()
 
-        if git_command in [u'git-receive-pack']:
+        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 kallithea.lib.vcs import get_repo
@@ -186,7 +185,7 @@
         _path = self._get_fixedpath(req.path_info)
         if _path.startswith('info/refs'):
             app = self.inforefs
-        elif [a for a in self.valid_accepts if a in req.accept]:
+        elif req.accept.acceptable_offers(self.valid_accepts):
             app = self.backend
         try:
             resp = app(req, environ)
--- a/kallithea/lib/middleware/sessionmiddleware.py	Mon May 04 18:25:09 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,63 +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/>.
-"""
-kallithea.lib.middleware.sessionmiddleware
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-session management middleware
-
-This file overrides Beaker's built-in SessionMiddleware
-class to automagically use secure cookies over HTTPS.
-
-Original Beaker SessionMiddleware class written by Ben Bangert
-"""
-
-from beaker.middleware import SessionMiddleware
-from beaker.session import SessionObject
-
-
-class SecureSessionMiddleware(SessionMiddleware):
-    def __call__(self, environ, start_response):
-        """
-        This function's implementation is taken directly from Beaker,
-        with HTTPS detection added. When accessed over HTTPS, force
-        setting cookie's secure flag.
-
-        The only difference from that original code is that we switch
-        the secure option on and off depending on the URL scheme (first
-        two lines). To avoid concurrency issues, we use a local options
-        variable.
-        """
-        options = dict(self.options)
-        options["secure"] = environ['wsgi.url_scheme'] == 'https'
-
-        session = SessionObject(environ, **options)
-        if environ.get('paste.registry'):
-            if environ['paste.registry'].reglist:
-                environ['paste.registry'].register(self.session, session)
-        environ[self.environ_key] = session
-        environ['beaker.get_session'] = self._get_session
-
-        if 'paste.testing_variables' in environ and 'webtest_varname' in options:
-            environ['paste.testing_variables'][options['webtest_varname']] = session
-
-        def session_start_response(status, headers, exc_info=None):
-            if session.accessed():
-                session.persist()
-                if session.__dict__['_headers']['set_cookie']:
-                    cookie = session.__dict__['_headers']['cookie_out']
-                    if cookie:
-                        headers.append(('Set-cookie', cookie))
-            return start_response(status, headers, exc_info)
-        return self.wrap_app(environ, session_start_response)
--- a/kallithea/lib/middleware/simplegit.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/middleware/simplegit.py	Mon May 04 19:24:04 2020 +0200
@@ -31,11 +31,10 @@
 import logging
 import re
 
-from kallithea.lib.base import BaseVCSController
+from kallithea.lib.base import BaseVCSController, get_path_info
 from kallithea.lib.hooks import log_pull_action
 from kallithea.lib.middleware.pygrack import make_wsgi_app
 from kallithea.lib.utils import make_ui
-from kallithea.lib.utils2 import safe_unicode
 from kallithea.model.db import Repository
 
 
@@ -57,14 +56,14 @@
 
     @classmethod
     def parse_request(cls, environ):
-        path_info = environ.get('PATH_INFO', '')
+        path_info = get_path_info(environ)
         m = GIT_PROTO_PAT.match(path_info)
         if m is None:
             return None
 
         class parsed_request(object):
             # See https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols#_the_smart_protocol
-            repo_name = safe_unicode(m.group(1).rstrip('/'))
+            repo_name = m.group(1).rstrip('/')
             cmd = m.group(2)
 
             query_string = environ['QUERY_STRING']
--- a/kallithea/lib/middleware/simplehg.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/middleware/simplehg.py	Mon May 04 19:24:04 2020 +0200
@@ -30,12 +30,13 @@
 
 import logging
 import os
-import urllib
+import urllib.parse
+
+import mercurial.hgweb
 
-from kallithea.lib.base import BaseVCSController
+from kallithea.lib.base import BaseVCSController, get_path_info
 from kallithea.lib.utils import make_ui
-from kallithea.lib.utils2 import safe_str, safe_unicode
-from kallithea.lib.vcs.utils.hgcompat import hgweb_mod
+from kallithea.lib.utils2 import safe_bytes
 
 
 log = logging.getLogger(__name__)
@@ -99,12 +100,12 @@
         http_accept = environ.get('HTTP_ACCEPT', '')
         if not http_accept.startswith('application/mercurial'):
             return None
-        path_info = environ.get('PATH_INFO', '')
+        path_info = get_path_info(environ)
         if not path_info.startswith('/'): # it must!
             return None
 
         class parsed_request(object):
-            repo_name = safe_unicode(path_info[1:].rstrip('/'))
+            repo_name = path_info[1:].rstrip('/')
 
             query_string = environ['QUERY_STRING']
 
@@ -120,7 +121,7 @@
                             break
                         action = 'pull'
                         for cmd_arg in hgarg[5:].split(';'):
-                            cmd, _args = urllib.unquote_plus(cmd_arg).split(' ', 1)
+                            cmd, _args = urllib.parse.unquote_plus(cmd_arg).split(' ', 1)
                             op = cmd_mapping.get(cmd, 'push')
                             if op != 'pull':
                                 assert op == 'push'
@@ -136,13 +137,13 @@
         """
         Make an hgweb wsgi application.
         """
-        str_repo_name = safe_str(parsed_request.repo_name)
-        repo_path = os.path.join(safe_str(self.basepath), str_repo_name)
+        repo_name = parsed_request.repo_name
+        repo_path = os.path.join(self.basepath, repo_name)
         baseui = make_ui(repo_path=repo_path)
-        hgweb_app = hgweb_mod.hgweb(repo_path, name=str_repo_name, baseui=baseui)
+        hgweb_app = mercurial.hgweb.hgweb(safe_bytes(repo_path), name=safe_bytes(repo_name), baseui=baseui)
 
         def wrapper_app(environ, start_response):
-            environ['REPO_NAME'] = str_repo_name # used by hgweb_mod.hgweb
+            environ['REPO_NAME'] = repo_name # used by mercurial.hgweb.hgweb
             return hgweb_app(environ, start_response)
 
         return wrapper_app
--- a/kallithea/lib/middleware/wrapper.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/middleware/wrapper.py	Mon May 04 19:24:04 2020 +0200
@@ -29,8 +29,7 @@
 import logging
 import time
 
-from kallithea.lib.base import _get_access_path, _get_ip_addr
-from kallithea.lib.utils2 import safe_unicode
+from kallithea.lib.base import _get_ip_addr, get_path_info
 
 
 log = logging.getLogger(__name__)
@@ -41,12 +40,14 @@
     def __init__(self, start_response):
         self._start_response = start_response
         self._start = time.time()
+        self.status = None
         self._size = 0
 
     def duration(self):
         return time.time() - self._start
 
     def start_response(self, status, response_headers, exc_info=None):
+        self.status = status
         write = self._start_response(status, response_headers, exc_info)
         def metered_write(s):
             self.measure(s)
@@ -64,21 +65,21 @@
 
     def __init__(self, result, meter, description):
         self._result_close = getattr(result, 'close', None) or (lambda: None)
-        self._next = iter(result).next
+        self._next = iter(result).__next__
         self._meter = meter
         self._description = description
 
     def __iter__(self):
         return self
 
-    def next(self):
+    def __next__(self):
         chunk = self._next()
         self._meter.measure(chunk)
         return chunk
 
     def close(self):
         self._result_close()
-        log.info("%s responded after %.3fs with %s bytes", self._description, self._meter.duration(), self._meter.size())
+        log.info("%s responded %r after %.3fs with %s bytes", self._description, self._meter.status, self._meter.duration(), self._meter.size())
 
 
 class RequestWrapper(object):
@@ -91,10 +92,11 @@
         meter = Meter(start_response)
         description = "Request from %s for %s" % (
             _get_ip_addr(environ),
-            safe_unicode(_get_access_path(environ)),
+            get_path_info(environ),
         )
+        log.info("%s received", description)
         try:
             result = self.application(environ, meter.start_response)
         finally:
-            log.info("%s responding after %.3fs", description, meter.duration())
+            log.info("%s responding %r after %.3fs", description, meter.status, meter.duration())
         return ResultIter(result, meter, description)
--- a/kallithea/lib/page.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/page.py	Mon May 04 19:24:04 2020 +0200
@@ -15,11 +15,11 @@
 Custom paging classes
 """
 import logging
-import math
-import re
 
-from webhelpers2.html import HTML, literal
-from webhelpers.paginate import Page as _Page
+import paginate
+import paginate_sqlalchemy
+import sqlalchemy.orm
+from webhelpers2.html import literal
 
 from kallithea.config.routing import url
 
@@ -27,229 +27,36 @@
 log = logging.getLogger(__name__)
 
 
-class Page(_Page):
-    """
-    Custom pager emitting Bootstrap paginators
-    """
-
-    def __init__(self, *args, **kwargs):
-        kwargs.setdefault('url', url.current)
-        _Page.__init__(self, *args, **kwargs)
-
-    def _get_pos(self, cur_page, max_page, items):
-        edge = (items / 2) + 1
-        if (cur_page <= edge):
-            radius = max(items / 2, items - cur_page)
-        elif (max_page - cur_page) < edge:
-            radius = (items - 1) - (max_page - cur_page)
-        else:
-            radius = items / 2
-
-        left = max(1, (cur_page - (radius)))
-        right = min(max_page, cur_page + (radius))
-        return left, cur_page, right
-
-    def _range(self, regexp_match):
-        """
-        Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
-
-        Arguments:
-
-        regexp_match
-            A "re" (regular expressions) match object containing the
-            radius of linked pages around the current page in
-            regexp_match.group(1) as a string
-
-        This function is supposed to be called as a callable in
-        re.sub.
-
-        """
-        radius = int(regexp_match.group(1))
-
-        # Compute the first and last page number within the radius
-        # e.g. '1 .. 5 6 [7] 8 9 .. 12'
-        # -> leftmost_page  = 5
-        # -> rightmost_page = 9
-        leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
-                                                            self.last_page,
-                                                            (radius * 2) + 1)
-        nav_items = []
-
-        # Create a link to the first page (unless we are on the first page
-        # or there would be no need to insert '..' spacers)
-        if self.page != self.first_page and self.first_page < leftmost_page:
-            nav_items.append(HTML.li(self._pagerlink(self.first_page, self.first_page)))
+class Page(paginate.Page):
 
-        # Insert dots if there are pages between the first page
-        # and the currently displayed page range
-        if leftmost_page - self.first_page > 1:
-            # Wrap in a SPAN tag if nolink_attr is set
-            text_ = '..'
-            if self.dotdot_attr:
-                text_ = HTML.span(c=text_, **self.dotdot_attr)
-            nav_items.append(HTML.li(text_))
-
-        for thispage in xrange(leftmost_page, rightmost_page + 1):
-            # Highlight the current page number and do not use a link
-            text_ = str(thispage)
-            if thispage == self.page:
-                # Wrap in a SPAN tag if nolink_attr is set
-                if self.curpage_attr:
-                    text_ = HTML.li(HTML.span(c=text_), **self.curpage_attr)
-                nav_items.append(text_)
-            # Otherwise create just a link to that page
-            else:
-                nav_items.append(HTML.li(self._pagerlink(thispage, text_)))
-
-        # Insert dots if there are pages between the displayed
-        # page numbers and the end of the page range
-        if self.last_page - rightmost_page > 1:
-            text_ = '..'
-            # Wrap in a SPAN tag if nolink_attr is set
-            if self.dotdot_attr:
-                text_ = HTML.span(c=text_, **self.dotdot_attr)
-            nav_items.append(HTML.li(text_))
-
-        # Create a link to the very last page (unless we are on the last
-        # page or there would be no need to insert '..' spacers)
-        if self.page != self.last_page and rightmost_page < self.last_page:
-            nav_items.append(HTML.li(self._pagerlink(self.last_page, self.last_page)))
-
-        #_page_link = url.current()
-        #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
-        #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
-        return self.separator.join(nav_items)
-
-    def pager(self, format='<ul class="pagination">$link_previous ~2~ $link_next</ul>', page_param='page', partial_param='partial',
-        show_if_single_page=False, separator=' ', onclick=None,
-        symbol_first='<<', symbol_last='>>',
-        symbol_previous='<', symbol_next='>',
-        link_attr=None,
-        curpage_attr=None,
-        dotdot_attr=None, **kwargs
-    ):
-        self.curpage_attr = curpage_attr or {'class': 'active'}
-        self.separator = separator
-        self.pager_kwargs = kwargs
-        self.page_param = page_param
-        self.partial_param = partial_param
-        self.onclick = onclick
-        self.link_attr = link_attr or {'class': 'pager_link', 'rel': 'prerender'}
-        self.dotdot_attr = dotdot_attr or {'class': 'pager_dotdot'}
+    def __init__(self, collection,
+                 page=1, items_per_page=20, item_count=None,
+                 **kwargs):
+        if isinstance(collection, sqlalchemy.orm.query.Query):
+            collection = paginate_sqlalchemy.SqlalchemyOrmWrapper(collection)
+        paginate.Page.__init__(self, collection, page=page, items_per_page=items_per_page, item_count=item_count,
+                               url_maker=lambda page: url.current(page=page, **kwargs))
 
-        # Don't show navigator if there is no more than one page
-        if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
-            return ''
-
-        from string import Template
-        # Replace ~...~ in token format by range of pages
-        result = re.sub(r'~(\d+)~', self._range, format)
-
-        # Interpolate '%' variables
-        result = Template(result).safe_substitute({
-            'first_page': self.first_page,
-            'last_page': self.last_page,
-            'page': self.page,
-            'page_count': self.page_count,
-            'items_per_page': self.items_per_page,
-            'first_item': self.first_item,
-            'last_item': self.last_item,
-            'item_count': self.item_count,
-            'link_first': self.page > self.first_page and
-                    self._pagerlink(self.first_page, symbol_first) or '',
-            'link_last': self.page < self.last_page and
-                    self._pagerlink(self.last_page, symbol_last) or '',
-            'link_previous': HTML.li(self.previous_page and
-                    self._pagerlink(self.previous_page, symbol_previous)
-                    or HTML.a(symbol_previous)),
-            'link_next': HTML.li(self.next_page and
-                    self._pagerlink(self.next_page, symbol_next)
-                    or HTML.a(symbol_next)),
-        })
-
-        return literal(result)
-
-
-class RepoPage(Page):
-
-    def __init__(self, collection, page=1, items_per_page=20,
-                 item_count=None, **kwargs):
-
-        """Create a "RepoPage" instance. special pager for paging
-        repository
-        """
-        # TODO: call baseclass __init__
-        self._url_generator = kwargs.pop('url', url.current)
-
-        # Safe the kwargs class-wide so they can be used in the pager() method
-        self.kwargs = kwargs
-
-        # Save a reference to the collection
-        self.original_collection = collection
-
-        self.collection = collection
+    def pager(self):
+        return literal(
+            paginate.Page.pager(self,
+                format='<ul class="pagination">$link_previous\n~4~$link_next</ul>',
+                link_attr={'class': 'pager_link'},
+                dotdot_attr={'class': 'pager_dotdot'},
+                separator='\n',
+                ))
 
-        # The self.page is the number of the current page.
-        # The first page has the number 1!
-        try:
-            self.page = int(page)  # make it int() if we get it as a string
-        except (ValueError, TypeError):
-            log.error("Invalid page value: %r", page)
-            self.page = 1
-
-        self.items_per_page = items_per_page
-
-        # Unless the user tells us how many items the collections has
-        # we calculate that ourselves.
-        if item_count is not None:
-            self.item_count = item_count
-        else:
-            self.item_count = len(self.collection)
-
-        # Compute the number of the first and last available page
-        if self.item_count > 0:
-            self.first_page = 1
-            self.page_count = int(math.ceil(float(self.item_count) /
-                                            self.items_per_page))
-            self.last_page = self.first_page + self.page_count - 1
-
-            # Make sure that the requested page number is the range of
-            # valid pages
-            if self.page > self.last_page:
-                self.page = self.last_page
-            elif self.page < self.first_page:
-                self.page = self.first_page
+    @staticmethod
+    def default_link_tag(item):
+        # based on the base class implementation, but wrapping results in <li>, and with different handling of current_page
+        text = item['value']
+        if item['type'] == 'current_page':  # we need active on the li and can thus not use curpage_attr
+            return '''<li class="active"><span>%s</span></li>''' % text
 
-            # Note: the number of items on this page can be less than
-            #       items_per_page if the last page is not full
-            self.first_item = max(0, (self.item_count) - (self.page *
-                                                          items_per_page))
-            self.last_item = ((self.item_count - 1) - items_per_page *
-                              (self.page - 1))
-
-            self.items = list(self.collection[self.first_item:self.last_item + 1])
-
-            # Links to previous and next page
-            if self.page > self.first_page:
-                self.previous_page = self.page - 1
-            else:
-                self.previous_page = None
-
-            if self.page < self.last_page:
-                self.next_page = self.page + 1
-            else:
-                self.next_page = None
-
-        # No items available
+        if not item['href'] or item['type'] == 'span':
+            if item['attrs']:
+                text = paginate.make_html_tag('span', **item['attrs']) + text + '</span>'
         else:
-            self.first_page = None
-            self.page_count = 0
-            self.last_page = None
-            self.first_item = None
-            self.last_item = None
-            self.previous_page = None
-            self.next_page = None
-            self.items = []
-
-        # This is a subclass of the 'list' type. Initialise the list now.
-        list.__init__(self, reversed(self.items))
+            target_url = item['href']
+            text =  paginate.make_html_tag('a', text=text, href=target_url, **item['attrs'])
+        return '''<li>%s</li>''' % text
--- a/kallithea/lib/paster_commands/template.ini.mako	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/paster_commands/template.ini.mako	Mon May 04 19:24:04 2020 +0200
@@ -1,11 +1,11 @@
 ## -*- coding: utf-8 -*-
-<%text>################################################################################</%text>
-<%text>################################################################################</%text>
-# Kallithea - config file generated with kallithea-config                      #
-#                                                                              #
-# The %(here)s variable will be replaced with the parent directory of this file#
-<%text>################################################################################</%text>
-<%text>################################################################################</%text>
+<%text>###################################################################################</%text>
+<%text>###################################################################################</%text>
+<%text>## Kallithea config file generated with kallithea-config                         ##</%text>
+<%text>##                                                                               ##</%text>
+<%text>## The %(here)s variable will be replaced with the parent directory of this file ##</%text>
+<%text>###################################################################################</%text>
+<%text>###################################################################################</%text>
 
 [DEFAULT]
 
@@ -55,11 +55,11 @@
 <%text>## For "SSL", use smtp_use_ssl = true and smtp_port = 465.</%text>
 <%text>## For "STARTTLS", use smtp_use_tls = true and smtp_port = 587.</%text>
 smtp_server =
-#smtp_username =
-#smtp_password =
+smtp_username =
+smtp_password =
 smtp_port =
-#smtp_use_ssl = false
-#smtp_use_tls = false
+smtp_use_ssl = false
+smtp_use_tls = false
 
 %if http_server != 'uwsgi':
 <%text>## Entry point for 'gearbox serve'</%text>
@@ -111,64 +111,33 @@
 %endif
 %else:
 <%text>## UWSGI ##</%text>
-<%text>## run with uwsgi --ini-paste-logged <inifile.ini></%text>
 [uwsgi]
-socket = /tmp/uwsgi.sock
-master = true
-http = ${host}:${port}
-
-<%text>## set as daemon and redirect all output to file</%text>
-#daemonize = ./uwsgi_kallithea.log
-
-<%text>## master process PID</%text>
-pidfile = ./uwsgi_kallithea.pid
+<%text>## Note: this section is parsed by the uWSGI .ini parser when run as:</%text>
+<%text>## uwsgi --venv /srv/kallithea/venv --ini-paste-logged my.ini</%text>
+<%text>## Note: in uWSGI 2.0.18 or older, pastescript needs to be installed to</%text>
+<%text>## get correct application logging. In later versions this is not necessary.</%text>
+<%text>## pip install pastescript</%text>
 
-<%text>## stats server with workers statistics, use uwsgitop</%text>
-<%text>## for monitoring, `uwsgitop 127.0.0.1:1717`</%text>
-stats = 127.0.0.1:1717
-memory-report = true
-
-<%text>## log 5XX errors</%text>
-log-5xx = true
-
-<%text>## Set the socket listen queue size.</%text>
-listen = 128
-
-<%text>## Gracefully Reload workers after the specified amount of managed requests</%text>
-<%text>## (avoid memory leaks).</%text>
-max-requests = 1000
+<%text>## HTTP Basics:</%text>
+http-socket = ${host}:${port}
+buffer-size = 65535                    ; Mercurial will use huge GET headers for discovery
 
-<%text>## enable large buffers</%text>
-buffer-size = 65535
-
-<%text>## socket and http timeouts ##</%text>
-http-timeout = 3600
-socket-timeout = 3600
-
-<%text>## Log requests slower than the specified number of milliseconds.</%text>
-log-slow = 10
-
-<%text>## Exit if no app can be loaded.</%text>
-need-app = true
-
-<%text>## Set lazy mode (load apps in workers instead of master).</%text>
-lazy = true
+<%text>## Scaling:</%text>
+master = true                          ; Use separate master and worker processes
+auto-procname = true                   ; Name worker processes accordingly
+lazy = true                            ; App *must* be loaded in workers - db connections can't be shared
+workers = 4                            ; On demand scaling up to this many worker processes
+cheaper = 1                            ; Initial and on demand scaling down to this many worker processes
+max-requests = 1000                    ; Graceful reload of worker processes to avoid leaks
 
-<%text>## scaling ##</%text>
-<%text>## set cheaper algorithm to use, if not set default will be used</%text>
-cheaper-algo = spare
-
-<%text>## minimum number of workers to keep at all times</%text>
-cheaper = 1
-
-<%text>## number of workers to spawn at startup</%text>
-cheaper-initial = 1
-
-<%text>## maximum number of workers that can be spawned</%text>
-workers = 4
-
-<%text>## how many workers should be spawned at a time</%text>
-cheaper-step = 1
+<%text>## Tweak defaults:</%text>
+strict = true                          ; Fail on unknown config directives
+enable-threads = true                  ; Enable Python threads (not threaded workers)
+vacuum = true                          ; Delete sockets during shutdown
+single-interpreter = true
+die-on-term = true                     ; Shutdown when receiving SIGTERM (default is respawn)
+need-app = true                        ; Exit early if no app can be loaded.
+reload-on-exception = true             ; Don't assume that the application worker can process more requests after a severe error
 
 %endif
 <%text>## middleware for hosting the WSGI application under a URL prefix</%text>
@@ -185,10 +154,12 @@
 static_files = true
 
 <%text>## Internationalization (see setup documentation for details)</%text>
-<%text>## By default, the language requested by the browser is used if available.</%text>
-#i18n.enabled = false
-<%text>## Fallback language, empty for English (valid values are the names of subdirectories in kallithea/i18n):</%text>
-i18n.lang =
+<%text>## By default, the languages requested by the browser are used if available, with English as default.</%text>
+<%text>## Set i18n.enabled=false to disable automatic language choice.</%text>
+#i18n.enabled = true
+<%text>## To Force a language, set i18n.enabled=false and specify the language in i18n.lang.</%text>
+<%text>## Valid values are the names of subdirectories in kallithea/i18n with a LC_MESSAGES/kallithea.mo</%text>
+#i18n.lang = en
 
 cache_dir = %(here)s/data
 index_dir = %(here)s/data/index
@@ -218,7 +189,7 @@
 <%text>## used, which is correct in many cases but for example not when using uwsgi.</%text>
 <%text>## If you change this setting, you should reinstall the Git hooks via</%text>
 <%text>## Admin > Settings > Remap and Rescan.</%text>
-# git_hook_interpreter = /srv/kallithea/venv/bin/python2
+#git_hook_interpreter = /srv/kallithea/venv/bin/python3
 %if git_hook_interpreter:
 git_hook_interpreter = ${git_hook_interpreter}
 %endif
@@ -293,7 +264,7 @@
 <%text>## issue_pat, issue_server_link and issue_sub can have suffixes to specify</%text>
 <%text>## multiple patterns, to other issues server, wiki or others</%text>
 <%text>## below an example how to create a wiki pattern</%text>
-# wiki-some-id -> https://wiki.example.com/some-id
+<%text>## wiki-some-id -> https://wiki.example.com/some-id</%text>
 
 #issue_pat_wiki = wiki-(\S+)
 #issue_server_link_wiki = https://wiki.example.com/\1
@@ -311,12 +282,12 @@
 allow_custom_hooks_settings = True
 
 <%text>## extra extensions for indexing, space separated and without the leading '.'.</%text>
-# index.extensions =
+#index.extensions =
 #    gemfile
 #    lock
 
 <%text>## extra filenames for indexing, space separated</%text>
-# index.filenames =
+#index.filenames =
 #    .dockerignore
 #    .editorconfig
 #    INSTALL
@@ -354,25 +325,23 @@
 <%text>###        CELERY CONFIG        ####</%text>
 <%text>####################################</%text>
 
+<%text>## Note: Celery doesn't support Windows.</%text>
 use_celery = false
 
-<%text>## Example: connect to the virtual host 'rabbitmqhost' on localhost as rabbitmq:</%text>
-broker.url = amqp://rabbitmq:qewqew@localhost:5672/rabbitmqhost
+<%text>## Celery config settings from https://docs.celeryproject.org/en/4.4.0/userguide/configuration.html prefixed with 'celery.'.</%text>
 
-celery.imports = kallithea.lib.celerylib.tasks
-celery.accept.content = pickle
-celery.result.backend = amqp
-celery.result.dburi = amqp://
-celery.result.serialier = json
+<%text>## Example: use the message queue on the local virtual host 'kallitheavhost' as the RabbitMQ user 'kallithea':</%text>
+celery.broker_url = amqp://kallithea:thepassword@localhost:5672/kallitheavhost
 
-#celery.send.task.error.emails = true
+celery.result.backend = db+sqlite:///celery-results.db
+
 #celery.amqp.task.result.expires = 18000
 
-celeryd.concurrency = 2
-celeryd.max.tasks.per.child = 1
+celery.worker_concurrency = 2
+celery.worker_max_tasks_per_child = 1
 
 <%text>## If true, tasks will never be sent to the queue, but executed locally instead.</%text>
-celery.always.eager = false
+celery.task_always_eager = false
 
 <%text>####################################</%text>
 <%text>###         BEAKER CACHE        ####</%text>
@@ -381,19 +350,15 @@
 beaker.cache.data_dir = %(here)s/data/cache/data
 beaker.cache.lock_dir = %(here)s/data/cache/lock
 
-beaker.cache.regions = short_term,long_term,sql_cache_short
-
-beaker.cache.short_term.type = memory
-beaker.cache.short_term.expire = 60
-beaker.cache.short_term.key_length = 256
+beaker.cache.regions = long_term,long_term_file
 
 beaker.cache.long_term.type = memory
 beaker.cache.long_term.expire = 36000
 beaker.cache.long_term.key_length = 256
 
-beaker.cache.sql_cache_short.type = memory
-beaker.cache.sql_cache_short.expire = 10
-beaker.cache.sql_cache_short.key_length = 256
+beaker.cache.long_term_file.type = file
+beaker.cache.long_term_file.expire = 604800
+beaker.cache.long_term_file.key_length = 256
 
 <%text>####################################</%text>
 <%text>###       BEAKER SESSION        ####</%text>
@@ -427,74 +392,33 @@
 #session.sa.url = postgresql://postgres:qwe@localhost/kallithea
 #session.table_name = db_session
 
-<%text>############################</%text>
-<%text>## ERROR HANDLING SYSTEMS ##</%text>
-<%text>############################</%text>
+<%text>####################################</%text>
+<%text>###       ERROR HANDLING        ####</%text>
+<%text>####################################</%text>
+
+<%text>## Show a nice error page for application HTTP errors and exceptions (default true)</%text>
+#errorpage.enabled = true
 
-# Propagate email settings to ErrorReporter of TurboGears2
-# You do not normally need to change these lines
-get trace_errors.error_email = email_to
+<%text>## Enable Backlash client-side interactive debugger (default false)</%text>
+<%text>## WARNING: *THIS MUST BE false IN PRODUCTION ENVIRONMENTS!!!*</%text>
+<%text>## This debug mode will allow all visitors to execute malicious code.</%text>
+#debug = false
+
+<%text>## Enable Backlash server-side error reporting (unless debug mode handles it client-side) (default true)</%text>
+#trace_errors.enable = true
+<%text>## Errors will be reported by mail if trace_errors.error_email is set.</%text>
+
+<%text>## Propagate email settings to ErrorReporter of TurboGears2</%text>
+<%text>## You do not normally need to change these lines</%text>
 get trace_errors.smtp_server = smtp_server
 get trace_errors.smtp_port = smtp_port
 get trace_errors.from_address = error_email_from
-
-%if error_aggregation_service == 'appenlight':
-<%text>####################</%text>
-<%text>### [appenlight] ###</%text>
-<%text>####################</%text>
-
-<%text>## AppEnlight is tailored to work with Kallithea, see</%text>
-<%text>## http://appenlight.com for details how to obtain an account</%text>
-<%text>## you must install python package `appenlight_client` to make it work</%text>
-
-<%text>## appenlight enabled</%text>
-appenlight = false
-
-appenlight.server_url = https://api.appenlight.com
-appenlight.api_key = YOUR_API_KEY
-
-<%text>## TWEAK AMOUNT OF INFO SENT HERE</%text>
-
-<%text>## enables 404 error logging (default False)</%text>
-appenlight.report_404 = false
-
-<%text>## time in seconds after request is considered being slow (default 1)</%text>
-appenlight.slow_request_time = 1
-
-<%text>## record slow requests in application</%text>
-<%text>## (needs to be enabled for slow datastore recording and time tracking)</%text>
-appenlight.slow_requests = true
+get trace_errors.error_email = email_to
+get trace_errors.smtp_username = smtp_username
+get trace_errors.smtp_password = smtp_password
+get trace_errors.smtp_use_tls = smtp_use_tls
 
-<%text>## enable hooking to application loggers</%text>
-#appenlight.logging = true
-
-<%text>## minimum log level for log capture</%text>
-#appenlight.logging.level = WARNING
-
-<%text>## send logs only from erroneous/slow requests</%text>
-<%text>## (saves API quota for intensive logging)</%text>
-appenlight.logging_on_error = false
-
-<%text>## list of additional keywords that should be grabbed from environ object</%text>
-<%text>## can be string with comma separated list of words in lowercase</%text>
-<%text>## (by default client will always send following info:</%text>
-<%text>## 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that</%text>
-<%text>## start with HTTP* this list be extended with additional keywords here</%text>
-appenlight.environ_keys_whitelist =
-
-<%text>## list of keywords that should be blanked from request object</%text>
-<%text>## can be string with comma separated list of words in lowercase</%text>
-<%text>## (by default client will always blank keys that contain following words</%text>
-<%text>## 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf'</%text>
-<%text>## this list be extended with additional keywords set here</%text>
-appenlight.request_keys_blacklist =
-
-<%text>## list of namespaces that should be ignores when gathering log entries</%text>
-<%text>## can be string with comma separated list of namespaces</%text>
-<%text>## (by default the client ignores own entries: appenlight_client.client)</%text>
-appenlight.log_namespace_blacklist =
-
-%elif error_aggregation_service == 'sentry':
+%if error_aggregation_service == 'sentry':
 <%text>################</%text>
 <%text>### [sentry] ###</%text>
 <%text>################</%text>
@@ -514,12 +438,6 @@
 sentry.exclude_paths =
 
 %endif
-<%text>################################################################################</%text>
-<%text>## WARNING: *DEBUG MODE MUST BE OFF IN A PRODUCTION ENVIRONMENT*              ##</%text>
-<%text>## Debug mode will enable the interactive debugging tool, allowing ANYONE to  ##</%text>
-<%text>## execute malicious code after an exception is raised.                       ##</%text>
-<%text>################################################################################</%text>
-debug = false
 
 <%text>##################################</%text>
 <%text>###       LOGVIEW CONFIG       ###</%text>
@@ -534,19 +452,19 @@
 <%text>#########################################################</%text>
 
 %if database_engine == 'sqlite':
-# SQLITE [default]
+<%text>## SQLITE [default]</%text>
 sqlalchemy.url = sqlite:///%(here)s/kallithea.db?timeout=60
 
 %elif database_engine == 'postgres':
-# POSTGRESQL
+<%text>## POSTGRESQL</%text>
 sqlalchemy.url = postgresql://user:pass@localhost/kallithea
 
 %elif database_engine == 'mysql':
-# MySQL
+<%text>## MySQL</%text>
 sqlalchemy.url = mysql://user:pass@localhost/kallithea?charset=utf8
 
 %endif
-# see sqlalchemy docs for others
+<%text>## see sqlalchemy docs for other backends</%text>
 
 sqlalchemy.pool_recycle = 3600
 
@@ -577,8 +495,8 @@
 [logger_root]
 level = NOTSET
 handlers = console
-# For coloring based on log level:
-# handlers = console_color
+<%text>## For coloring based on log level:</%text>
+#handlers = console_color
 
 [logger_routes]
 level = WARN
@@ -615,10 +533,10 @@
 level = WARN
 handlers =
 qualname = sqlalchemy.engine
-# For coloring based on log level and pretty printing of SQL:
-# level = INFO
-# handlers = console_color_sql
-# propagate = 0
+<%text>## For coloring based on log level and pretty printing of SQL:</%text>
+#level = INFO
+#handlers = console_color_sql
+#propagate = 0
 
 [logger_whoosh_indexer]
 level = WARN
@@ -645,13 +563,13 @@
 formatter = generic
 
 [handler_console_color]
-# ANSI color coding based on log level
+<%text>## ANSI color coding based on log level</%text>
 class = StreamHandler
 args = (sys.stderr,)
 formatter = color_formatter
 
 [handler_console_color_sql]
-# ANSI color coding and pretty printing of SQL statements
+<%text>## ANSI color coding and pretty printing of SQL statements</%text>
 class = StreamHandler
 args = (sys.stderr,)
 formatter = color_formatter_sql
@@ -682,16 +600,16 @@
 <%text>## SSH LOGGING ##</%text>
 <%text>#################</%text>
 
-# The default loggers use 'handler_console' that uses StreamHandler with
-# destination 'sys.stderr'. In the context of the SSH server process, these log
-# messages would be sent to the client, which is normally not what you want.
-# By default, when running ssh-serve, just use NullHandler and disable logging
-# completely. For other logging options, see:
-# https://docs.python.org/2/library/logging.handlers.html
+<%text>## The default loggers use 'handler_console' that uses StreamHandler with</%text>
+<%text>## destination 'sys.stderr'. In the context of the SSH server process, these log</%text>
+<%text>## messages would be sent to the client, which is normally not what you want.</%text>
+<%text>## By default, when running ssh-serve, just use NullHandler and disable logging</%text>
+<%text>## completely. For other logging options, see:</%text>
+<%text>## https://docs.python.org/2/library/logging.handlers.html</%text>
 
 [ssh_serve:logger_root]
 level = CRITICAL
 handlers = null
 
-# Note: If logging is configured with other handlers, they might need similar
-# muting for ssh-serve too.
+<%text>## Note: If logging is configured with other handlers, they might need similar</%text>
+<%text>## muting for ssh-serve too.</%text>
--- a/kallithea/lib/pidlock.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/pidlock.py	Mon May 04 19:24:04 2020 +0200
@@ -12,8 +12,6 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-from __future__ import print_function
-
 import errno
 import os
 from multiprocessing.util import Finalize
@@ -137,6 +135,6 @@
         dir_, file_ = os.path.split(pidfile)
         if not os.path.isdir(dir_):
             os.makedirs(dir_)
-        with open(self.pidfile, 'wb') as f:
+        with open(self.pidfile, 'w') as f:
             f.write(lockname)
         self.held = True
--- a/kallithea/lib/pygmentsutils.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/pygmentsutils.py	Mon May 04 19:24:04 2020 +0200
@@ -26,7 +26,6 @@
 """
 
 from collections import defaultdict
-from itertools import ifilter
 
 from pygments import lexers
 
@@ -59,15 +58,11 @@
     """
     Get list of known indexable filenames from pygment lexer internals
     """
-
     filenames = []
-
-    def likely_filename(s):
-        return s.find('*') == -1 and s.find('[') == -1
-
     for lx, t in sorted(lexers.LEXERS.items()):
-        for f in ifilter(likely_filename, t[-2]):
-            filenames.append(f)
+        for f in t[-2]:
+            if '*' not in f and '[' not in f:
+                filenames.append(f)
 
     return filenames
 
--- a/kallithea/lib/rcmail/message.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/rcmail/message.py	Mon May 04 19:24:04 2020 +0200
@@ -2,35 +2,6 @@
 from kallithea.lib.rcmail.response import MailResponse
 
 
-class Attachment(object):
-    """
-    Encapsulates file attachment information.
-
-    :param filename: filename of attachment
-    :param content_type: file mimetype
-    :param data: the raw file data, either as string or file obj
-    :param disposition: content-disposition (if any)
-    """
-
-    def __init__(self,
-                 filename=None,
-                 content_type=None,
-                 data=None,
-                 disposition=None):
-
-        self.filename = filename
-        self.content_type = content_type
-        self.disposition = disposition or 'attachment'
-        self._data = data
-
-    @property
-    def data(self):
-        if isinstance(self._data, basestring):
-            return self._data
-        self._data = self._data.read()
-        return self._data
-
-
 class Message(object):
     """
     Encapsulates an email message.
--- a/kallithea/lib/rcmail/response.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/rcmail/response.py	Mon May 04 19:24:04 2020 +0200
@@ -44,7 +44,9 @@
 
 ADDRESS_HEADERS_WHITELIST = ['From', 'To', 'Delivered-To', 'Cc']
 DEFAULT_ENCODING = "utf-8"
-VALUE_IS_EMAIL_ADDRESS = lambda v: '@' in v
+
+def VALUE_IS_EMAIL_ADDRESS(v):
+    return '@' in v
 
 
 def normalize_header(header):
@@ -87,7 +89,7 @@
     def __delitem__(self, key):
         del self.headers[normalize_header(key)]
 
-    def __nonzero__(self):
+    def __bool__(self):
         return self.body is not None or len(self.headers) > 0 or len(self.parts) > 0
 
     def keys(self):
@@ -339,20 +341,20 @@
 
     try:
         out = MIMEPart(ctype, **params)
-    except TypeError as exc:  # pragma: no cover
+    except TypeError as e:  # pragma: no cover
         raise EncodingError("Content-Type malformed, not allowed: %r; "
-                            "%r (Python ERROR: %s" %
-                            (ctype, params, exc.message))
+                            "%r (Python ERROR: %s)" %
+                            (ctype, params, e.args[0]))
 
     for k in mail.keys():
         if k in ADDRESS_HEADERS_WHITELIST:
-            out[k.encode('ascii')] = header_to_mime_encoding(
+            out[k] = header_to_mime_encoding(
                                          mail[k],
                                          not_email=False,
                                          separator=separator
                                      )
         else:
-            out[k.encode('ascii')] = header_to_mime_encoding(
+            out[k] = header_to_mime_encoding(
                                          mail[k],
                                          not_email=True
                                     )
@@ -392,7 +394,7 @@
         if mail.body is None:
             return  # only None, '' is still ok
 
-        ctype, ctype_params = mail.content_encoding['Content-Type']
+        ctype, _ctype_params = mail.content_encoding['Content-Type']
         cdisp, cdisp_params = mail.content_encoding['Content-Disposition']
 
         assert ctype, ("Extract payload requires that mail.content_encoding "
@@ -422,7 +424,7 @@
         return ""
 
     encoder = Charset(DEFAULT_ENCODING)
-    if type(value) == list:
+    if isinstance(value, list):
         return separator.join(properly_encode_header(
             v, encoder, not_email) for v in value)
     else:
@@ -443,12 +445,12 @@
     check different, then change this.
     """
     try:
-        return value.encode("ascii")
-    except UnicodeEncodeError:
+        value.encode("ascii")
+        return value
+    except UnicodeError:
         if not not_email and VALUE_IS_EMAIL_ADDRESS(value):
             # this could have an email address, make sure we don't screw it up
             name, address = parseaddr(value)
-            return '"%s" <%s>' % (
-                encoder.header_encode(name.encode("utf-8")), address)
+            return '"%s" <%s>' % (encoder.header_encode(name), address)
 
-        return encoder.header_encode(value.encode("utf-8"))
+        return encoder.header_encode(value)
--- a/kallithea/lib/rcmail/smtp_mailer.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/rcmail/smtp_mailer.py	Mon May 04 19:24:04 2020 +0200
@@ -64,7 +64,7 @@
     def send(self, recipients=None, subject='', body='', html='',
              attachment_files=None, headers=None):
         recipients = recipients or []
-        if isinstance(recipients, basestring):
+        if isinstance(recipients, str):
             recipients = [recipients]
         if headers is None:
             headers = {}
--- a/kallithea/lib/recaptcha.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/recaptcha.py	Mon May 04 19:24:04 2020 +0200
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 import json
-import urllib
-import urllib2
+import urllib.parse
+import urllib.request
 
 
 class RecaptchaResponse(object):
@@ -26,17 +26,17 @@
         return RecaptchaResponse(is_valid=False, error_code='incorrect-captcha-sol')
 
     def encode_if_necessary(s):
-        if isinstance(s, unicode):
+        if isinstance(s, str):
             return s.encode('utf-8')
         return s
 
-    params = urllib.urlencode({
+    params = urllib.parse.urlencode({
         'secret': encode_if_necessary(private_key),
         'remoteip': encode_if_necessary(remoteip),
         'response': encode_if_necessary(g_recaptcha_response),
-    })
+    }).encode('ascii')
 
-    req = urllib2.Request(
+    req = urllib.request.Request(
         url="https://www.google.com/recaptcha/api/siteverify",
         data=params,
         headers={
@@ -45,7 +45,7 @@
         }
     )
 
-    httpresp = urllib2.urlopen(req)
+    httpresp = urllib.request.urlopen(req)
     return_values = json.loads(httpresp.read())
     httpresp.close()
 
--- a/kallithea/lib/ssh.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/ssh.py	Mon May 04 19:24:04 2020 +0200
@@ -21,12 +21,14 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-import binascii
+import base64
 import logging
 import re
 
 from tg.i18n import ugettext as _
 
+from kallithea.lib.utils2 import ascii_bytes, ascii_str
+
 
 log = logging.getLogger(__name__)
 
@@ -42,39 +44,39 @@
     >>> parse_pub_key('')
     Traceback (most recent call last):
     ...
-    SshKeyParseError: SSH key is missing
+    kallithea.lib.ssh.SshKeyParseError: SSH key is missing
     >>> parse_pub_key('''AAAAB3NzaC1yc2EAAAALVGhpcyBpcyBmYWtlIQ''')
     Traceback (most recent call last):
     ...
-    SshKeyParseError: Incorrect SSH key - it must have both a key type and a base64 part
+    kallithea.lib.ssh.SshKeyParseError: Incorrect SSH key - it must have both a key type and a base64 part, like 'ssh-rsa ASRNeaZu4FA...xlJp='
     >>> parse_pub_key('''abc AAAAB3NzaC1yc2EAAAALVGhpcyBpcyBmYWtlIQ''')
     Traceback (most recent call last):
     ...
-    SshKeyParseError: Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'
+    kallithea.lib.ssh.SshKeyParseError: Incorrect SSH key - it must start with 'ssh-(rsa|dss|ed25519)'
     >>> parse_pub_key('''ssh-rsa  AAAAB3NzaC1yc2EAAAALVGhpcyBpcyBmYWtlIQ''')
     Traceback (most recent call last):
     ...
-    SshKeyParseError: Incorrect SSH key - failed to decode base64 part 'AAAAB3NzaC1yc2EAAAALVGhpcyBpcyBmYWtlIQ'
+    kallithea.lib.ssh.SshKeyParseError: Incorrect SSH key - failed to decode base64 part 'AAAAB3NzaC1yc2EAAAALVGhpcyBpcyBmYWtlIQ'
     >>> parse_pub_key('''ssh-rsa  AAAAB2NzaC1yc2EAAAALVGhpcyBpcyBmYWtlIQ==''')
     Traceback (most recent call last):
     ...
-    SshKeyParseError: Incorrect SSH key - base64 part is not 'ssh-rsa' as claimed but 'csh-rsa'
+    kallithea.lib.ssh.SshKeyParseError: Incorrect SSH key - base64 part is not 'ssh-rsa' as claimed but 'csh-rsa'
     >>> parse_pub_key('''ssh-rsa  AAAAB3NzaC1yc2EAAAA'LVGhpcyBpcyBmYWtlIQ''')
     Traceback (most recent call last):
     ...
-    SshKeyParseError: Incorrect SSH key - unexpected characters in base64 part "AAAAB3NzaC1yc2EAAAA'LVGhpcyBpcyBmYWtlIQ"
+    kallithea.lib.ssh.SshKeyParseError: Incorrect SSH key - unexpected characters in base64 part "AAAAB3NzaC1yc2EAAAA'LVGhpcyBpcyBmYWtlIQ"
     >>> parse_pub_key(''' ssh-rsa  AAAAB3NzaC1yc2EAAAALVGhpcyBpcyBmYWtlIQ== and a comment
     ... ''')
-    ('ssh-rsa', '\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x0bThis is fake!', 'and a comment\n')
+    ('ssh-rsa', b'\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x0bThis is fake!', 'and a comment\n')
     >>> parse_pub_key('''ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIP1NA2kBQIKe74afUXmIWD9ByDYQJqUwW44Y4gJOBRuo''')
-    ('ssh-ed25519', '\x00\x00\x00\x0bssh-ed25519\x00\x00\x00 \xfdM\x03i\x01@\x82\x9e\xef\x86\x9fQy\x88X?A\xc86\x10&\xa50[\x8e\x18\xe2\x02N\x05\x1b\xa8', '')
+    ('ssh-ed25519', b'\x00\x00\x00\x0bssh-ed25519\x00\x00\x00 \xfdM\x03i\x01@\x82\x9e\xef\x86\x9fQy\x88X?A\xc86\x10&\xa50[\x8e\x18\xe2\x02N\x05\x1b\xa8', '')
     """
     if not ssh_key:
         raise SshKeyParseError(_("SSH key is missing"))
 
     parts = ssh_key.split(None, 2)
     if len(parts) < 2:
-        raise SshKeyParseError(_("Incorrect SSH key - it must have both a key type and a base64 part"))
+        raise SshKeyParseError(_("Incorrect SSH key - it must have both a key type and a base64 part, like 'ssh-rsa ASRNeaZu4FA...xlJp='"))
 
     keytype, keyvalue, comment = (parts + [''])[:3]
     if keytype not in ('ssh-rsa', 'ssh-dss', 'ssh-ed25519'):
@@ -84,19 +86,31 @@
         raise SshKeyParseError(_("Incorrect SSH key - unexpected characters in base64 part %r") % keyvalue)
 
     try:
-        decoded = keyvalue.decode('base64')
-    except binascii.Error:
+        key_bytes = base64.b64decode(keyvalue)
+    except base64.binascii.Error:
         raise SshKeyParseError(_("Incorrect SSH key - failed to decode base64 part %r") % keyvalue)
 
-    if not decoded.startswith('\x00\x00\x00' + chr(len(keytype)) + str(keytype) + '\x00'):
-        raise SshKeyParseError(_("Incorrect SSH key - base64 part is not %r as claimed but %r") % (str(keytype), str(decoded[4:].split('\0', 1)[0])))
+    if not key_bytes.startswith(b'\x00\x00\x00%c%s\x00' % (len(keytype), ascii_bytes(keytype))):
+        raise SshKeyParseError(_("Incorrect SSH key - base64 part is not %r as claimed but %r") % (keytype, ascii_str(key_bytes[4:].split(b'\0', 1)[0])))
 
-    return keytype, decoded, comment
+    return keytype, key_bytes, comment
 
 
 SSH_OPTIONS = 'no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding'
 
 
+def _safe_check(s, rec = re.compile('^[a-zA-Z0-9+/]+={0,2}$')):
+    """Return true if s really has the right content for base64 encoding and only contains safe characters
+    >>> _safe_check('asdf')
+    True
+    >>> _safe_check('as df')
+    False
+    >>> _safe_check('AAAAB3NzaC1yc2EAAAALVGhpcyBpcyBmYWtlIQ==')
+    True
+    """
+    return rec.match(s) is not None
+
+
 def authorized_keys_line(kallithea_cli_path, config_file, key):
     """
     Return a line as it would appear in .authorized_keys
@@ -109,11 +123,14 @@
     'no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,command="/srv/kallithea/venv/bin/kallithea-cli ssh-serve -c /srv/kallithea/my.ini 7 17" ssh-rsa AAAAB3NzaC1yc2EAAAALVGhpcyBpcyBmYWtlIQ==\\n'
     """
     try:
-        keytype, decoded, comment = parse_pub_key(key.public_key)
+        keytype, key_bytes, comment = parse_pub_key(key.public_key)
     except SshKeyParseError:
         return '# Invalid Kallithea SSH key: %s %s\n' % (key.user.user_id, key.user_ssh_key_id)
-    mimekey = decoded.encode('base64').replace('\n', '')
+    base64_key = ascii_str(base64.b64encode(key_bytes))
+    assert '\n' not in base64_key
+    if not _safe_check(base64_key):
+        return '# Invalid Kallithea SSH key - bad base64 encoding: %s %s\n' % (key.user.user_id, key.user_ssh_key_id)
     return '%s,command="%s ssh-serve -c %s %s %s" %s %s\n' % (
         SSH_OPTIONS, kallithea_cli_path, config_file,
         key.user.user_id, key.user_ssh_key_id,
-        keytype, mimekey)
+        keytype, base64_key)
--- a/kallithea/lib/timerproxy.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/timerproxy.py	Mon May 04 19:24:04 2020 +0200
@@ -20,7 +20,7 @@
 
 log = logging.getLogger('timerproxy')
 
-BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = xrange(30, 38)
+BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(30, 38)
 
 
 def color_sql(sql):
--- a/kallithea/lib/utils.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/utils.py	Mon May 04 19:24:04 2020 +0200
@@ -31,21 +31,24 @@
 import re
 import sys
 import traceback
+import urllib.error
 from distutils.version import StrictVersion
 
-import beaker
-from beaker.cache import _cache_decorate
-from tg.i18n import ugettext as _
+import mercurial.config
+import mercurial.error
+import mercurial.ui
 
-from kallithea.lib.exceptions import HgsubversionImportError
-from kallithea.lib.utils2 import get_current_authuser, safe_str, safe_unicode
-from kallithea.lib.vcs.exceptions import VCSError
+import kallithea.config.conf
+from kallithea.lib.exceptions import InvalidCloneUriException
+from kallithea.lib.utils2 import ascii_bytes, aslist, get_current_authuser, safe_bytes, safe_str
+from kallithea.lib.vcs.backends.git.repository import GitRepository
+from kallithea.lib.vcs.backends.hg.repository import MercurialRepository
+from kallithea.lib.vcs.conf import settings
+from kallithea.lib.vcs.exceptions import RepositoryError, VCSError
 from kallithea.lib.vcs.utils.fakemod import create_module
 from kallithea.lib.vcs.utils.helpers import get_scm
-from kallithea.lib.vcs.utils.hgcompat import config, ui
-from kallithea.model import meta
+from kallithea.model import db, meta
 from kallithea.model.db import RepoGroup, Repository, Setting, Ui, User, UserGroup, UserLog
-from kallithea.model.repo_group import RepoGroupModel
 
 
 log = logging.getLogger(__name__)
@@ -102,7 +105,6 @@
         rest = '/' + rest_
     repo_id = _get_permanent_id(first)
     if repo_id is not None:
-        from kallithea.model.db import Repository
         repo = Repository.get(repo_id)
         if repo is not None:
             return repo.repo_name + rest
@@ -130,7 +132,7 @@
 
     if getattr(user, 'user_id', None):
         user_obj = User.get(user.user_id)
-    elif isinstance(user, basestring):
+    elif isinstance(user, str):
         user_obj = User.get_by_username(user)
     else:
         raise Exception('You have to provide a user object or a username')
@@ -138,17 +140,17 @@
     if getattr(repo, 'repo_id', None):
         repo_obj = Repository.get(repo.repo_id)
         repo_name = repo_obj.repo_name
-    elif isinstance(repo, basestring):
+    elif isinstance(repo, str):
         repo_name = repo.lstrip('/')
         repo_obj = Repository.get_by_repo_name(repo_name)
     else:
         repo_obj = None
-        repo_name = u''
+        repo_name = ''
 
     user_log = UserLog()
     user_log.user_id = user_obj.user_id
     user_log.username = user_obj.username
-    user_log.action = safe_unicode(action)
+    user_log.action = action
 
     user_log.repository = repo_obj
     user_log.repository_name = repo_name
@@ -158,7 +160,7 @@
     meta.Session().add(user_log)
 
     log.info('Logging action:%s on %s by user:%s ip:%s',
-             action, safe_unicode(repo), user_obj, ipaddr)
+             action, repo, user_obj, ipaddr)
     if commit:
         meta.Session().commit()
 
@@ -172,7 +174,7 @@
     """
 
     # remove ending slash for better results
-    path = safe_str(path.rstrip(os.sep))
+    path = path.rstrip(os.sep)
     log.debug('now scanning in %s', path)
 
     def isdir(*n):
@@ -223,37 +225,43 @@
 
 
 def is_valid_repo_uri(repo_type, url, ui):
-    """Check if the url seems like a valid remote repo location - raise an Exception if any problems"""
+    """Check if the url seems like a valid remote repo location
+    Raise InvalidCloneUriException if any problems"""
     if repo_type == 'hg':
-        from kallithea.lib.vcs.backends.hg.repository import MercurialRepository
         if url.startswith('http') or url.startswith('ssh'):
             # initially check if it's at least the proper URL
             # or does it pass basic auth
-            MercurialRepository._check_url(url, ui)
+            try:
+                MercurialRepository._check_url(url, ui)
+            except urllib.error.URLError as e:
+                raise InvalidCloneUriException('URI %s URLError: %s' % (url, e))
+            except mercurial.error.RepoError as e:
+                raise InvalidCloneUriException('Mercurial %s: %s' % (type(e).__name__, safe_str(bytes(e))))
         elif url.startswith('svn+http'):
             try:
                 from hgsubversion.svnrepo import svnremoterepo
             except ImportError:
-                raise HgsubversionImportError(_('Unable to activate hgsubversion support. '
-                                                'The "hgsubversion" library is missing'))
+                raise InvalidCloneUriException('URI type %s not supported - hgsubversion is not available' % (url,))
             svnremoterepo(ui, url).svn.uuid
         elif url.startswith('git+http'):
-            raise NotImplementedError()
+            raise InvalidCloneUriException('URI type %s not implemented' % (url,))
         else:
-            raise Exception('URI %s not allowed' % (url,))
+            raise InvalidCloneUriException('URI %s not allowed' % (url,))
 
     elif repo_type == 'git':
-        from kallithea.lib.vcs.backends.git.repository import GitRepository
         if url.startswith('http') or url.startswith('git'):
             # initially check if it's at least the proper URL
             # or does it pass basic auth
-            GitRepository._check_url(url)
+            try:
+                GitRepository._check_url(url)
+            except urllib.error.URLError as e:
+                raise InvalidCloneUriException('URI %s URLError: %s' % (url, e))
         elif url.startswith('svn+http'):
-            raise NotImplementedError()
+            raise InvalidCloneUriException('URI type %s not implemented' % (url,))
         elif url.startswith('hg+http'):
-            raise NotImplementedError()
+            raise InvalidCloneUriException('URI type %s not implemented' % (url,))
         else:
-            raise Exception('URI %s not allowed' % (url))
+            raise InvalidCloneUriException('URI %s not allowed' % (url))
 
 
 def is_valid_repo(repo_name, base_path, scm=None):
@@ -269,7 +277,7 @@
     :return True: if given path is a valid repository
     """
     # TODO: paranoid security checks?
-    full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
+    full_path = os.path.join(base_path, repo_name)
 
     try:
         scm_ = get_scm(full_path)
@@ -287,7 +295,7 @@
     :param repo_name:
     :param base_path:
     """
-    full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
+    full_path = os.path.join(base_path, repo_group_name)
 
     # check if it's not a repo
     if is_valid_repo(repo_group_name, base_path):
@@ -309,65 +317,41 @@
     return False
 
 
-# propagated from mercurial documentation
-ui_sections = ['alias', 'auth',
-                'decode/encode', 'defaults',
-                'diff', 'email',
-                'extensions', 'format',
-                'merge-patterns', 'merge-tools',
-                'hooks', 'http_proxy',
-                'smtp', 'patch',
-                'paths', 'profiling',
-                'server', 'trusted',
-                'ui', 'web', ]
-
-
-def make_ui(repo_path=None, clear_session=True):
+def make_ui(repo_path=None):
     """
     Create an Mercurial 'ui' object based on database Ui settings, possibly
     augmenting with content from a hgrc file.
     """
-    baseui = ui.ui()
+    baseui = mercurial.ui.ui()
 
     # clean the baseui object
-    baseui._ocfg = config.config()
-    baseui._ucfg = config.config()
-    baseui._tcfg = config.config()
+    baseui._ocfg = mercurial.config.config()
+    baseui._ucfg = mercurial.config.config()
+    baseui._tcfg = mercurial.config.config()
 
     sa = meta.Session()
-    for ui_ in sa.query(Ui).all():
+    for ui_ in sa.query(Ui).order_by(Ui.ui_section, Ui.ui_key):
         if ui_.ui_active:
-            ui_val = '' if ui_.ui_value is None else safe_str(ui_.ui_value)
             log.debug('config from db: [%s] %s=%r', ui_.ui_section,
-                      ui_.ui_key, ui_val)
-            baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
-                             ui_val)
-    if clear_session:
-        meta.Session.remove()
+                      ui_.ui_key, ui_.ui_value)
+            baseui.setconfig(ascii_bytes(ui_.ui_section), ascii_bytes(ui_.ui_key),
+                             b'' if ui_.ui_value is None else safe_bytes(ui_.ui_value))
 
     # force set push_ssl requirement to False, Kallithea handles that
-    baseui.setconfig('web', 'push_ssl', False)
-    baseui.setconfig('web', 'allow_push', '*')
+    baseui.setconfig(b'web', b'push_ssl', False)
+    baseui.setconfig(b'web', b'allow_push', b'*')
     # prevent interactive questions for ssh password / passphrase
-    ssh = baseui.config('ui', 'ssh', default='ssh')
-    baseui.setconfig('ui', 'ssh', '%s -oBatchMode=yes -oIdentitiesOnly=yes' % ssh)
+    ssh = baseui.config(b'ui', b'ssh', default=b'ssh')
+    baseui.setconfig(b'ui', b'ssh', b'%s -oBatchMode=yes -oIdentitiesOnly=yes' % ssh)
     # push / pull hooks
-    baseui.setconfig('hooks', 'changegroup.kallithea_log_push_action', 'python:kallithea.lib.hooks.log_push_action')
-    baseui.setconfig('hooks', 'outgoing.kallithea_log_pull_action', 'python:kallithea.lib.hooks.log_pull_action')
+    baseui.setconfig(b'hooks', b'changegroup.kallithea_log_push_action', b'python:kallithea.lib.hooks.log_push_action')
+    baseui.setconfig(b'hooks', b'outgoing.kallithea_log_pull_action', b'python:kallithea.lib.hooks.log_pull_action')
 
     if repo_path is not None:
-        hgrc_path = os.path.join(repo_path, '.hg', 'hgrc')
-        if os.path.isfile(hgrc_path):
-            log.debug('reading hgrc from %s', hgrc_path)
-            cfg = config.config()
-            cfg.read(hgrc_path)
-            for section in ui_sections:
-                for k, v in cfg.items(section):
-                    log.debug('config from file: [%s] %s=%s', section, k, v)
-                    baseui.setconfig(safe_str(section), safe_str(k), safe_str(v))
-        else:
-            log.debug('hgrc file is not present at %s, skipping...', hgrc_path)
+        # Note: MercurialRepository / mercurial.localrepo.instance will do this too, so it will always be possible to override db settings or what is hardcoded above
+        baseui.readconfig(repo_path)
 
+    assert baseui.plain()  # set by hgcompat.monkey_do (invoked from import of vcs.backends.hg) to minimize potential impact of loading config files
     return baseui
 
 
@@ -377,12 +361,10 @@
 
     :param config:
     """
-    try:
-        hgsettings = Setting.get_app_settings()
-        for k, v in hgsettings.items():
-            config[k] = v
-    finally:
-        meta.Session.remove()
+    hgsettings = Setting.get_app_settings()
+    for k, v in hgsettings.items():
+        config[k] = v
+    config['base_path'] = Ui.get_repos_location()
 
 
 def set_vcs_config(config):
@@ -391,16 +373,14 @@
 
     :param config: kallithea.CONFIG
     """
-    from kallithea.lib.vcs import conf
-    from kallithea.lib.utils2 import aslist
-    conf.settings.BACKENDS = {
+    settings.BACKENDS = {
         'hg': 'kallithea.lib.vcs.backends.hg.MercurialRepository',
         'git': 'kallithea.lib.vcs.backends.git.GitRepository',
     }
 
-    conf.settings.GIT_EXECUTABLE_PATH = config.get('git_path', 'git')
-    conf.settings.GIT_REV_FILTER = config.get('git_rev_filter', '--all').strip()
-    conf.settings.DEFAULT_ENCODINGS = aslist(config.get('default_encoding',
+    settings.GIT_EXECUTABLE_PATH = config.get('git_path', 'git')
+    settings.GIT_REV_FILTER = config.get('git_rev_filter', '--all').strip()
+    settings.DEFAULT_ENCODINGS = aslist(config.get('default_encoding',
                                                         'utf-8'), sep=',')
 
 
@@ -410,13 +390,11 @@
 
     :param config: kallithea.CONFIG
     """
-    from kallithea.config import conf
-
     log.debug('adding extra into INDEX_EXTENSIONS')
-    conf.INDEX_EXTENSIONS.extend(re.split(r'\s+', config.get('index.extensions', '')))
+    kallithea.config.conf.INDEX_EXTENSIONS.extend(re.split(r'\s+', config.get('index.extensions', '')))
 
     log.debug('adding extra into INDEX_FILENAMES')
-    conf.INDEX_FILENAMES.extend(re.split(r'\s+', config.get('index.filenames', '')))
+    kallithea.config.conf.INDEX_FILENAMES.extend(re.split(r'\s+', config.get('index.filenames', '')))
 
 
 def map_groups(path):
@@ -427,8 +405,9 @@
 
     :param paths: full path to repository
     """
+    from kallithea.model.repo_group import RepoGroupModel
     sa = meta.Session()
-    groups = path.split(Repository.url_sep())
+    groups = path.split(db.URL_SEP)
     parent = None
     group = None
 
@@ -437,7 +416,7 @@
     rgm = RepoGroupModel()
     owner = User.get_first_admin()
     for lvl, group_name in enumerate(groups):
-        group_name = u'/'.join(groups[:lvl] + [group_name])
+        group_name = '/'.join(groups[:lvl] + [group_name])
         group = RepoGroup.get_by_group_name(group_name)
         desc = '%s group' % group_name
 
@@ -459,14 +438,14 @@
     return group
 
 
-def repo2db_mapper(initial_repo_list, remove_obsolete=False,
+def repo2db_mapper(initial_repo_dict, remove_obsolete=False,
                    install_git_hooks=False, user=None, overwrite_git_hooks=False):
     """
-    maps all repos given in initial_repo_list, non existing repositories
+    maps all repos given in initial_repo_dict, non existing repositories
     are created, if remove_obsolete is True it also check for db entries
-    that are not in initial_repo_list and removes them.
+    that are not in initial_repo_dict and removes them.
 
-    :param initial_repo_list: list of repositories found by scanning methods
+    :param initial_repo_dict: mapping with repositories found by scanning methods
     :param remove_obsolete: check for obsolete entries in database
     :param install_git_hooks: if this is True, also check and install git hook
         for a repo if missing
@@ -487,10 +466,9 @@
     enable_downloads = defs.get('repo_enable_downloads')
     private = defs.get('repo_private')
 
-    for name, repo in initial_repo_list.items():
+    for name, repo in initial_repo_dict.items():
         group = map_groups(name)
-        unicode_name = safe_unicode(name)
-        db_repo = repo_model.get_by_repo_name(unicode_name)
+        db_repo = repo_model.get_by_repo_name(name)
         # found repo that is on filesystem not in Kallithea database
         if not db_repo:
             log.info('repository %s not found, creating now', name)
@@ -526,9 +504,8 @@
 
     removed = []
     # remove from database those repositories that are not in the filesystem
-    unicode_initial_repo_list = set(safe_unicode(name) for name in initial_repo_list)
     for repo in sa.query(Repository).all():
-        if repo.repo_name not in unicode_initial_repo_list:
+        if repo.repo_name not in initial_repo_dict:
             if remove_obsolete:
                 log.debug("Removing non-existing repository found in db `%s`",
                           repo.repo_name)
@@ -544,9 +521,6 @@
 
 
 def load_rcextensions(root_path):
-    import kallithea
-    from kallithea.config import conf
-
     path = os.path.join(root_path, 'rcextensions', '__init__.py')
     if os.path.isfile(path):
         rcext = create_module('rc', path)
@@ -554,17 +528,17 @@
         log.debug('Found rcextensions now loading %s...', rcext)
 
         # Additional mappings that are not present in the pygments lexers
-        conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
+        kallithea.config.conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
 
         # OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present)
 
         if getattr(EXT, 'INDEX_EXTENSIONS', []):
             log.debug('settings custom INDEX_EXTENSIONS')
-            conf.INDEX_EXTENSIONS = getattr(EXT, 'INDEX_EXTENSIONS', [])
+            kallithea.config.conf.INDEX_EXTENSIONS = getattr(EXT, 'INDEX_EXTENSIONS', [])
 
         # ADDITIONAL MAPPINGS
         log.debug('adding extra into INDEX_EXTENSIONS')
-        conf.INDEX_EXTENSIONS.extend(getattr(EXT, 'EXTRA_INDEX_EXTENSIONS', []))
+        kallithea.config.conf.INDEX_EXTENSIONS.extend(getattr(EXT, 'EXTRA_INDEX_EXTENSIONS', []))
 
         # auto check if the module is not missing any data, set to default if is
         # this will help autoupdate new feature of rcext module
@@ -585,28 +559,33 @@
     Checks what version of git is installed on the system, and raise a system exit
     if it's too old for Kallithea to work properly.
     """
-    from kallithea import BACKENDS
-    from kallithea.lib.vcs.backends.git.repository import GitRepository
-    from kallithea.lib.vcs.conf import settings
-
-    if 'git' not in BACKENDS:
+    if 'git' not in kallithea.BACKENDS:
         return None
 
     if not settings.GIT_EXECUTABLE_PATH:
         log.warning('No git executable configured - check "git_path" in the ini file.')
         return None
 
-    stdout, stderr = GitRepository._run_git_command(['--version'], _bare=True,
-                                                    _safe=True)
+    try:
+        stdout, stderr = GitRepository._run_git_command(['--version'])
+    except RepositoryError as e:
+        # message will already have been logged as error
+        log.warning('No working git executable found - check "git_path" in the ini file.')
+        return None
 
     if stderr:
-        log.warning('Error/stderr from "%s --version": %r', settings.GIT_EXECUTABLE_PATH, stderr)
+        log.warning('Error/stderr from "%s --version":\n%s', settings.GIT_EXECUTABLE_PATH, safe_str(stderr))
 
-    m = re.search(r"\d+.\d+.\d+", stdout)
+    if not stdout:
+        log.warning('No working git executable found - check "git_path" in the ini file.')
+        return None
+
+    output = safe_str(stdout).strip()
+    m = re.search(r"\d+.\d+.\d+", output)
     if m:
         ver = StrictVersion(m.group(0))
         log.debug('Git executable: "%s", version %s (parsed from: "%s")',
-                  settings.GIT_EXECUTABLE_PATH, ver, stdout.strip())
+                  settings.GIT_EXECUTABLE_PATH, ver, output)
         if ver < git_req_ver:
             log.error('Kallithea detected %s version %s, which is too old '
                       'for the system to function properly. '
@@ -618,68 +597,7 @@
             sys.exit(1)
     else:
         ver = StrictVersion('0.0.0')
-        log.warning('Error finding version number in "%s --version" stdout: %r',
-                    settings.GIT_EXECUTABLE_PATH, stdout.strip())
+        log.warning('Error finding version number in "%s --version" stdout:\n%s',
+                    settings.GIT_EXECUTABLE_PATH, output)
 
     return ver
-
-
-#===============================================================================
-# CACHE RELATED METHODS
-#===============================================================================
-
-# set cache regions for beaker so celery can utilise it
-def setup_cache_regions(settings):
-    # Create dict with just beaker cache configs with prefix stripped
-    cache_settings = {'regions': None}
-    prefix = 'beaker.cache.'
-    for key in settings:
-        if key.startswith(prefix):
-            name = key[len(prefix):]
-            cache_settings[name] = settings[key]
-    # Find all regions, apply defaults, and apply to beaker
-    if cache_settings['regions']:
-        for region in cache_settings['regions'].split(','):
-            region = region.strip()
-            prefix = region + '.'
-            region_settings = {}
-            for key in cache_settings:
-                if key.startswith(prefix):
-                    name = key[len(prefix):]
-                    region_settings[name] = cache_settings[key]
-            region_settings.setdefault('expire',
-                                       cache_settings.get('expire', '60'))
-            region_settings.setdefault('lock_dir',
-                                       cache_settings.get('lock_dir'))
-            region_settings.setdefault('data_dir',
-                                       cache_settings.get('data_dir'))
-            region_settings.setdefault('type',
-                                       cache_settings.get('type', 'memory'))
-            beaker.cache.cache_regions[region] = region_settings
-
-
-def conditional_cache(region, prefix, condition, func):
-    """
-
-    Conditional caching function use like::
-        def _c(arg):
-            #heavy computation function
-            return data
-
-        # depending from condition the compute is wrapped in cache or not
-        compute = conditional_cache('short_term', 'cache_desc', condition=True, func=func)
-        return compute(arg)
-
-    :param region: name of cache region
-    :param prefix: cache region prefix
-    :param condition: condition for cache to be triggered, and return data cached
-    :param func: wrapped heavy function to compute
-
-    """
-    wrapped = func
-    if condition:
-        log.debug('conditional_cache: True, wrapping call of '
-                  'func: %s into %s region cache' % (region, func))
-        wrapped = _cache_decorate((prefix,), None, None, region)(func)
-
-    return wrapped
--- a/kallithea/lib/utils2.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/utils2.py	Mon May 04 19:24:04 2020 +0200
@@ -27,25 +27,37 @@
 :license: GPLv3, see LICENSE.md for more details.
 """
 
-from __future__ import print_function
-
 import binascii
 import datetime
+import json
 import os
-import pwd
 import re
 import time
-import urllib
+import urllib.parse
 
 import urlobject
 from tg.i18n import ugettext as _
 from tg.i18n import ungettext
 from webhelpers2.text import collapse, remove_formatting, strip_tags
 
-from kallithea.lib.compat import json
+from kallithea.lib.vcs.utils import ascii_bytes, ascii_str, safe_bytes, safe_str  # re-export
 from kallithea.lib.vcs.utils.lazy import LazyProperty
 
 
+try:
+    import pwd
+except ImportError:
+    pass
+
+
+# mute pyflakes "imported but unused"
+assert ascii_bytes
+assert ascii_str
+assert safe_bytes
+assert safe_str
+assert LazyProperty
+
+
 def str2bool(_str):
     """
     returns True/False value from given string, it tries to translate the
@@ -71,7 +83,7 @@
     :param sep:
     :param strip:
     """
-    if isinstance(obj, (basestring)):
+    if isinstance(obj, (str)):
         lst = obj.split(sep)
         if strip:
             lst = [v.strip() for v in lst]
@@ -98,14 +110,12 @@
     :rtype: str
     :return: converted line according to mode
     """
-    from string import replace
-
     if mode == 0:
-        line = replace(line, '\r\n', '\n')
-        line = replace(line, '\r', '\n')
+        line = line.replace('\r\n', '\n')
+        line = line.replace('\r', '\n')
     elif mode == 1:
-        line = replace(line, '\r\n', '\r')
-        line = replace(line, '\n', '\r')
+        line = line.replace('\r\n', '\r')
+        line = line.replace('\n', '\r')
     elif mode == 2:
         line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
     return line
@@ -142,7 +152,7 @@
         unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
     """
     # Hexadecimal certainly qualifies as URL-safe.
-    return binascii.hexlify(os.urandom(20))
+    return ascii_str(binascii.hexlify(os.urandom(20)))
 
 
 def safe_int(val, default=None):
@@ -153,104 +163,13 @@
     :param val:
     :param default:
     """
-
     try:
         val = int(val)
     except (ValueError, TypeError):
         val = default
-
     return val
 
 
-def safe_unicode(str_, from_encoding=None):
-    """
-    safe unicode function. Does few trick to turn str_ into unicode
-
-    In case of UnicodeDecode error we try to return it with encoding detected
-    by chardet library if it fails fallback to unicode with errors replaced
-
-    :param str_: string to decode
-    :rtype: unicode
-    :returns: unicode object
-    """
-    if isinstance(str_, unicode):
-        return str_
-
-    if not from_encoding:
-        import kallithea
-        DEFAULT_ENCODINGS = aslist(kallithea.CONFIG.get('default_encoding',
-                                                        'utf-8'), sep=',')
-        from_encoding = DEFAULT_ENCODINGS
-
-    if not isinstance(from_encoding, (list, tuple)):
-        from_encoding = [from_encoding]
-
-    try:
-        return unicode(str_)
-    except UnicodeDecodeError:
-        pass
-
-    for enc in from_encoding:
-        try:
-            return unicode(str_, enc)
-        except UnicodeDecodeError:
-            pass
-
-    try:
-        import chardet
-        encoding = chardet.detect(str_)['encoding']
-        if encoding is None:
-            raise Exception()
-        return str_.decode(encoding)
-    except (ImportError, UnicodeDecodeError, Exception):
-        return unicode(str_, from_encoding[0], 'replace')
-
-
-def safe_str(unicode_, to_encoding=None):
-    """
-    safe str function. Does few trick to turn unicode_ into string
-
-    In case of UnicodeEncodeError we try to return it with encoding detected
-    by chardet library if it fails fallback to string with errors replaced
-
-    :param unicode_: unicode to encode
-    :rtype: str
-    :returns: str object
-    """
-
-    # if it's not basestr cast to str
-    if not isinstance(unicode_, basestring):
-        return str(unicode_)
-
-    if isinstance(unicode_, str):
-        return unicode_
-
-    if not to_encoding:
-        import kallithea
-        DEFAULT_ENCODINGS = aslist(kallithea.CONFIG.get('default_encoding',
-                                                        'utf-8'), sep=',')
-        to_encoding = DEFAULT_ENCODINGS
-
-    if not isinstance(to_encoding, (list, tuple)):
-        to_encoding = [to_encoding]
-
-    for enc in to_encoding:
-        try:
-            return unicode_.encode(enc)
-        except UnicodeEncodeError:
-            pass
-
-    try:
-        import chardet
-        encoding = chardet.detect(unicode_)['encoding']
-        if encoding is None:
-            raise UnicodeEncodeError()
-
-        return unicode_.encode(encoding)
-    except (ImportError, UnicodeEncodeError):
-        return unicode_.encode(to_encoding[0], 'replace')
-
-
 def remove_suffix(s, suffix):
     if s.endswith(suffix):
         s = s[:-1 * len(suffix)]
@@ -271,8 +190,8 @@
 
     :param prevdate: datetime object
     :param show_short_version: if it should approximate the date and return a shorter string
-    :rtype: unicode
-    :returns: unicode words describing age
+    :rtype: str
+    :returns: str words describing age
     """
     now = now or datetime.datetime.now()
     order = ['year', 'month', 'day', 'hour', 'minute', 'second']
@@ -331,12 +250,12 @@
 
     # Format the result
     fmt_funcs = {
-        'year': lambda d: ungettext(u'%d year', '%d years', d) % d,
-        'month': lambda d: ungettext(u'%d month', '%d months', d) % d,
-        'day': lambda d: ungettext(u'%d day', '%d days', d) % d,
-        'hour': lambda d: ungettext(u'%d hour', '%d hours', d) % d,
-        'minute': lambda d: ungettext(u'%d minute', '%d minutes', d) % d,
-        'second': lambda d: ungettext(u'%d second', '%d seconds', d) % d,
+        'year': lambda d: ungettext('%d year', '%d years', d) % d,
+        'month': lambda d: ungettext('%d month', '%d months', d) % d,
+        'day': lambda d: ungettext('%d day', '%d days', d) % d,
+        'hour': lambda d: ungettext('%d hour', '%d hours', d) % d,
+        'minute': lambda d: ungettext('%d minute', '%d minutes', d) % d,
+        'second': lambda d: ungettext('%d second', '%d seconds', d) % d,
     }
 
     for i, part in enumerate(order):
@@ -370,11 +289,11 @@
     Removes user:password from given url string
 
     :param uri:
-    :rtype: unicode
+    :rtype: str
     :returns: filtered list of strings
     """
     if not uri:
-        return ''
+        return []
 
     proto = ''
 
@@ -394,7 +313,7 @@
     else:
         host, port = uri[:cred_pos], uri[cred_pos + 1:]
 
-    return filter(None, [proto, host, port])
+    return [_f for _f in [proto, host, port] if _f]
 
 
 def credentials_filter(uri):
@@ -414,19 +333,19 @@
 
 def get_clone_url(clone_uri_tmpl, prefix_url, repo_name, repo_id, username=None):
     parsed_url = urlobject.URLObject(prefix_url)
-    prefix = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
+    prefix = urllib.parse.unquote(parsed_url.path.rstrip('/'))
     try:
         system_user = pwd.getpwuid(os.getuid()).pw_name
-    except Exception: # TODO: support all systems - especially Windows
+    except NameError: # TODO: support all systems - especially Windows
         system_user = 'kallithea' # hardcoded default value ...
     args = {
         'scheme': parsed_url.scheme,
-        'user': safe_unicode(urllib.quote(safe_str(username or ''))),
+        'user': urllib.parse.quote(username or ''),
         'netloc': parsed_url.netloc + prefix,  # like "hostname:port/prefix" (with optional ":port" and "/prefix")
         'prefix': prefix, # undocumented, empty or starting with /
         'repo': repo_name,
         'repoid': str(repo_id),
-        'system_user': safe_unicode(system_user),
+        'system_user': system_user,
         'hostname': parsed_url.hostname,
     }
     url = re.sub('{([^{}]+)}', lambda m: args.get(m.group(1), m.group(0)), clone_uri_tmpl)
@@ -436,7 +355,7 @@
     if not url_obj.username:
         url_obj = url_obj.with_username(None)
 
-    return safe_unicode(url_obj)
+    return str(url_obj)
 
 
 def get_changeset_safe(repo, rev):
@@ -468,7 +387,7 @@
 
 def time_to_datetime(tm):
     if tm:
-        if isinstance(tm, basestring):
+        if isinstance(tm, str):
             try:
                 tm = float(tm)
             except ValueError:
@@ -522,6 +441,9 @@
     return str(_url)
 
 
+class HookEnvironmentError(Exception): pass
+
+
 def get_hook_environment():
     """
     Get hook context by deserializing the global KALLITHEA_EXTRAS environment
@@ -533,15 +455,16 @@
     """
 
     try:
-        extras = json.loads(os.environ['KALLITHEA_EXTRAS'])
+        kallithea_extras = os.environ['KALLITHEA_EXTRAS']
     except KeyError:
-        raise Exception("Environment variable KALLITHEA_EXTRAS not found")
+        raise HookEnvironmentError("Environment variable KALLITHEA_EXTRAS not found")
 
-    try:
-        for k in ['username', 'repository', 'scm', 'action', 'ip']:
+    extras = json.loads(kallithea_extras)
+    for k in ['username', 'repository', 'scm', 'action', 'ip', 'config']:
+        try:
             extras[k]
-    except KeyError:
-        raise Exception('Missing key %s in KALLITHEA_EXTRAS %s' % (k, extras))
+        except KeyError:
+            raise HookEnvironmentError('Missing key %s in KALLITHEA_EXTRAS %s' % (k, extras))
 
     return AttributeDict(extras)
 
@@ -573,10 +496,10 @@
     defined, else returns None.
     """
     from tg import tmpl_context
-    if hasattr(tmpl_context, 'authuser'):
-        return tmpl_context.authuser
-
-    return None
+    try:
+        return getattr(tmpl_context, 'authuser', None)
+    except TypeError:  # No object (name: context) has been registered for this thread
+        return None
 
 
 class OptionalAttr(object):
@@ -649,7 +572,7 @@
 
 
 def urlreadable(s, _cleanstringsub=re.compile('[^-a-zA-Z0-9./]+').sub):
-    return _cleanstringsub('_', safe_str(s)).rstrip('_')
+    return _cleanstringsub('_', s).rstrip('_')
 
 
 def recursive_replace(str_, replace=' '):
@@ -690,7 +613,7 @@
 
 def ask_ok(prompt, retries=4, complaint='Yes or no please!'):
     while True:
-        ok = raw_input(prompt)
+        ok = input(prompt)
         if ok in ('y', 'ye', 'yes'):
             return True
         if ok in ('n', 'no', 'nop', 'nope'):
--- a/kallithea/lib/vcs/backends/__init__.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/backends/__init__.py	Mon May 04 19:24:04 2020 +0200
@@ -9,7 +9,6 @@
     :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
 """
 import os
-from pprint import pformat
 
 from kallithea.lib.vcs.conf import settings
 from kallithea.lib.vcs.exceptions import VCSError
@@ -51,7 +50,7 @@
     """
     if alias not in settings.BACKENDS:
         raise VCSError("Given alias '%s' is not recognized! Allowed aliases:\n"
-            "%s" % (alias, pformat(settings.BACKENDS.keys())))
+            "%s" % (alias, '", "'.join(settings.BACKENDS)))
     backend_path = settings.BACKENDS[alias]
     klass = import_class(backend_path)
     return klass
--- a/kallithea/lib/vcs/backends/base.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/backends/base.py	Mon May 04 19:24:04 2020 +0200
@@ -13,9 +13,9 @@
 import itertools
 
 from kallithea.lib.vcs.conf import settings
-from kallithea.lib.vcs.exceptions import (
-    ChangesetError, EmptyRepositoryError, NodeAlreadyAddedError, NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError, NodeDoesNotExistError, NodeNotChangedError, RepositoryError)
-from kallithea.lib.vcs.utils import author_email, author_name, safe_unicode
+from kallithea.lib.vcs.exceptions import (ChangesetError, EmptyRepositoryError, NodeAlreadyAddedError, NodeAlreadyChangedError, NodeAlreadyExistsError,
+                                          NodeAlreadyRemovedError, NodeDoesNotExistError, NodeNotChangedError, RepositoryError)
+from kallithea.lib.vcs.utils import author_email, author_name
 from kallithea.lib.vcs.utils.helpers import get_dict_for_attrs
 from kallithea.lib.vcs.utils.lazy import LazyProperty
 
@@ -98,10 +98,6 @@
         """
         raise NotImplementedError
 
-    @property
-    def name_unicode(self):
-        return safe_unicode(self.name)
-
     @LazyProperty
     def owner(self):
         raise NotImplementedError
@@ -173,14 +169,9 @@
         """
         raise NotImplementedError
 
-    def __getslice__(self, i, j):
-        """
-        Returns a iterator of sliced repository
-        """
-        for rev in self.revisions[i:j]:
-            yield self.get_changeset(rev)
-
     def __getitem__(self, key):
+        if isinstance(key, slice):
+            return (self.get_changeset(rev) for rev in self.revisions[key])
         return self.get_changeset(key)
 
     def count(self):
@@ -267,8 +258,6 @@
         """
         Persists current changes made on this repository and returns newly
         created changeset.
-
-        :raises ``NothingChangedError``: if no changes has been made
         """
         raise NotImplementedError
 
@@ -329,9 +318,6 @@
         ``repository``
             repository object within which changeset exists
 
-        ``id``
-            may be ``raw_id`` or i.e. for mercurial's tip just ``tip``
-
         ``raw_id``
             raw changeset representation (i.e. full 40 length sha for git
             backend)
@@ -354,10 +340,10 @@
             combined list of ``Node`` objects
 
         ``author``
-            author of the changeset, as unicode
+            author of the changeset, as str
 
         ``message``
-            message of the changeset, as unicode
+            message of the changeset, as str
 
         ``parents``
             list of parent changesets
@@ -374,10 +360,9 @@
     def __repr__(self):
         return self.__str__()
 
-    def __unicode__(self):
-        return u'%s:%s' % (self.revision, self.short_id)
-
     def __eq__(self, other):
+        if type(self) is not type(other):
+            return False
         return self.raw_id == other.raw_id
 
     def __json__(self, with_file_list=False):
@@ -389,9 +374,9 @@
                 message=self.message,
                 date=self.date,
                 author=self.author,
-                added=[safe_unicode(el.path) for el in self.added],
-                changed=[safe_unicode(el.path) for el in self.changed],
-                removed=[safe_unicode(el.path) for el in self.removed],
+                added=[el.path for el in self.added],
+                changed=[el.path for el in self.changed],
+                removed=[el.path for el in self.removed],
             )
         else:
             return dict(
@@ -424,13 +409,6 @@
         raise NotImplementedError
 
     @LazyProperty
-    def id(self):
-        """
-        Returns string identifying this changeset.
-        """
-        raise NotImplementedError
-
-    @LazyProperty
     def raw_id(self):
         """
         Returns raw string identifying this changeset.
@@ -660,12 +638,12 @@
         """
         Returns dictionary with changeset's attributes and their values.
         """
-        data = get_dict_for_attrs(self, ['id', 'raw_id', 'short_id',
+        data = get_dict_for_attrs(self, ['raw_id', 'short_id',
             'revision', 'date', 'message'])
         data['author'] = {'name': self.author_name, 'email': self.author_email}
-        data['added'] = [safe_unicode(node.path) for node in self.added]
-        data['changed'] = [safe_unicode(node.path) for node in self.changed]
-        data['removed'] = [safe_unicode(node.path) for node in self.removed]
+        data['added'] = [node.path for node in self.added]
+        data['changed'] = [node.path for node in self.changed]
+        data['removed'] = [node.path for node in self.removed]
         return data
 
     @LazyProperty
@@ -936,18 +914,18 @@
                         "at %s" % (node.path, p))
 
         # Check nodes marked as changed
-        missing = set(self.changed)
-        not_changed = set(self.changed)
+        missing = set(node.path for node in self.changed)
+        not_changed = set(node.path for node in self.changed)
         if self.changed and not parents:
-            raise NodeDoesNotExistError(str(self.changed[0].path))
+            raise NodeDoesNotExistError(self.changed[0].path)
         for p in parents:
             for node in self.changed:
                 try:
                     old = p.get_node(node.path)
-                    missing.remove(node)
+                    missing.remove(node.path)
                     # if content actually changed, remove node from unchanged
                     if old.content != node.content:
-                        not_changed.remove(node)
+                        not_changed.remove(node.path)
                 except NodeDoesNotExistError:
                     pass
         if self.changed and missing:
@@ -956,7 +934,7 @@
 
         if self.changed and not_changed:
             raise NodeNotChangedError("Node at %s wasn't actually changed "
-                "since parents' changesets: %s" % (not_changed.pop().path,
+                "since parents' changesets: %s" % (not_changed.pop(),
                     parents)
             )
 
@@ -969,10 +947,10 @@
             for node in self.removed:
                 try:
                     p.get_node(node.path)
-                    really_removed.add(node)
+                    really_removed.add(node.path)
                 except ChangesetError:
                     pass
-        not_removed = set(self.removed) - really_removed
+        not_removed = list(set(node.path for node in self.removed) - really_removed)
         if not_removed:
             raise NodeDoesNotExistError("Cannot remove node at %s from "
                 "following parents: %s" % (not_removed[0], parents))
@@ -1046,7 +1024,7 @@
         return self
 
     def get_file_content(self, path):
-        return u''
+        return b''
 
     def get_file_size(self, path):
         return 0
--- a/kallithea/lib/vcs/backends/git/changeset.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/backends/git/changeset.py	Mon May 04 19:24:04 2020 +0200
@@ -9,38 +9,36 @@
 from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
 from kallithea.lib.vcs.conf import settings
 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, ChangesetError, ImproperArchiveTypeError, NodeDoesNotExistError, RepositoryError, VCSError
-from kallithea.lib.vcs.nodes import (
-    AddedFileNodesGenerator, ChangedFileNodesGenerator, DirNode, FileNode, NodeKind, RemovedFileNodesGenerator, RootNode, SubModuleNode)
-from kallithea.lib.vcs.utils import date_fromtimestamp, safe_int, safe_str, safe_unicode
+from kallithea.lib.vcs.nodes import (AddedFileNodesGenerator, ChangedFileNodesGenerator, DirNode, FileNode, NodeKind, RemovedFileNodesGenerator, RootNode,
+                                     SubModuleNode)
+from kallithea.lib.vcs.utils import ascii_bytes, ascii_str, date_fromtimestamp, safe_int, safe_str
 from kallithea.lib.vcs.utils.lazy import LazyProperty
 
 
 class GitChangeset(BaseChangeset):
     """
-    Represents state of the repository at single revision.
+    Represents state of the repository at a revision.
     """
 
     def __init__(self, repository, revision):
         self._stat_modes = {}
         self.repository = repository
-        revision = safe_str(revision)
         try:
-            commit = self.repository._repo[revision]
+            commit = self.repository._repo[ascii_bytes(revision)]
             if isinstance(commit, objects.Tag):
                 revision = safe_str(commit.object[1])
                 commit = self.repository._repo.get_object(commit.object[1])
         except KeyError:
             raise RepositoryError("Cannot get object with id %s" % revision)
-        self.raw_id = revision
-        self.id = self.raw_id
+        self.raw_id = ascii_str(commit.id)
         self.short_id = self.raw_id[:12]
-        self._commit = commit
+        self._commit = commit  # a Dulwich Commmit with .id
         self._tree_id = commit.tree
         self._committer_property = 'committer'
         self._author_property = 'author'
         self._date_property = 'commit_time'
         self._date_tz_property = 'commit_timezone'
-        self.revision = repository.revisions.index(revision)
+        self.revision = repository.revisions.index(self.raw_id)
 
         self.nodes = {}
         self._paths = {}
@@ -51,15 +49,15 @@
 
     @LazyProperty
     def message(self):
-        return safe_unicode(self._commit.message)
+        return safe_str(self._commit.message)
 
     @LazyProperty
     def committer(self):
-        return safe_unicode(getattr(self._commit, self._committer_property))
+        return safe_str(getattr(self._commit, self._committer_property))
 
     @LazyProperty
     def author(self):
-        return safe_unicode(getattr(self._commit, self._author_property))
+        return safe_str(getattr(self._commit, self._author_property))
 
     @LazyProperty
     def date(self):
@@ -80,7 +78,7 @@
     @LazyProperty
     def tags(self):
         _tags = []
-        for tname, tsha in self.repository.tags.iteritems():
+        for tname, tsha in self.repository.tags.items():
             if tsha == self.raw_id:
                 _tags.append(tname)
         return _tags
@@ -91,26 +89,16 @@
         # that might not make sense in Git where branches() is a better match
         # for the basic model
         heads = self.repository._heads(reverse=False)
-        ref = heads.get(self.raw_id)
+        ref = heads.get(self._commit.id)
         if ref:
-            return safe_unicode(ref)
+            return safe_str(ref)
 
     @LazyProperty
     def branches(self):
         heads = self.repository._heads(reverse=True)
-        return [b for b in heads if heads[b] == self.raw_id] # FIXME: Inefficient ... and returning None!
-
-    def _fix_path(self, path):
-        """
-        Paths are stored without trailing slash so we need to get rid off it if
-        needed.
-        """
-        if path.endswith('/'):
-            path = path.rstrip('/')
-        return path
+        return [safe_str(b) for b in heads if heads[b] == self._commit.id] # FIXME: Inefficient ... and returning None!
 
     def _get_id_for_path(self, path):
-        path = safe_str(path)
         # FIXME: Please, spare a couple of minutes and make those codes cleaner;
         if path not in self._paths:
             path = path.strip('/')
@@ -124,11 +112,10 @@
             curdir = ''
 
             # initially extract things from root dir
-            for item, stat, id in tree.iteritems():
+            for item, stat, id in tree.items():
+                name = safe_str(item)
                 if curdir:
-                    name = '/'.join((curdir, item))
-                else:
-                    name = item
+                    name = '/'.join((curdir, name))
                 self._paths[name] = id
                 self._stat_modes[name] = stat
 
@@ -138,8 +125,9 @@
                 else:
                     curdir = dir
                 dir_id = None
-                for item, stat, id in tree.iteritems():
-                    if dir == item:
+                for item, stat, id in tree.items():
+                    name = safe_str(item)
+                    if dir == name:
                         dir_id = id
                 if dir_id:
                     # Update tree
@@ -150,17 +138,16 @@
                     raise ChangesetError('%s have not been found' % curdir)
 
                 # cache all items from the given traversed tree
-                for item, stat, id in tree.iteritems():
+                for item, stat, id in tree.items():
+                    name = safe_str(item)
                     if curdir:
-                        name = '/'.join((curdir, item))
-                    else:
-                        name = item
+                        name = '/'.join((curdir, name))
                     self._paths[name] = id
                     self._stat_modes[name] = stat
             if path not in self._paths:
                 raise NodeDoesNotExistError("There is no file nor directory "
                     "at the given path '%s' at revision %s"
-                    % (path, safe_str(self.short_id)))
+                    % (path, self.short_id))
         return self._paths[path]
 
     def _get_kind(self, path):
@@ -171,7 +158,7 @@
             return NodeKind.DIR
 
     def _get_filectx(self, path):
-        path = self._fix_path(path)
+        path = path.rstrip('/')
         if self._get_kind(path) != NodeKind.FILE:
             raise ChangesetError("File does not exist for revision %s at "
                 " '%s'" % (self.raw_id, path))
@@ -185,8 +172,8 @@
         """
         Returns list of parents changesets.
         """
-        return [self.repository.get_changeset(parent)
-                for parent in self._commit.parents]
+        return [self.repository.get_changeset(ascii_str(parent_id))
+                for parent_id in self._commit.parents]
 
     @LazyProperty
     def children(self):
@@ -194,17 +181,15 @@
         Returns list of children changesets.
         """
         rev_filter = settings.GIT_REV_FILTER
-        so, se = self.repository.run_git_command(
+        so = self.repository.run_git_command(
             ['rev-list', rev_filter, '--children']
         )
-
-        children = []
-        pat = re.compile(r'^%s' % self.raw_id)
-        for l in so.splitlines():
-            if pat.match(l):
-                childs = l.split(' ')[1:]
-                children.extend(childs)
-        return [self.repository.get_changeset(cs) for cs in children]
+        return [
+            self.repository.get_changeset(cs)
+            for parts in (l.split(' ') for l in so.splitlines())
+            if parts[0] == self.raw_id
+            for cs in parts[1:]
+        ]
 
     def next(self, branch=None):
         if branch and self.branch != branch:
@@ -243,9 +228,10 @@
                 return cs
 
     def diff(self, ignore_whitespace=True, context=3):
+        # Only used to feed diffstat
         rev1 = self.parents[0] if self.parents else self.repository.EMPTY_CHANGESET
         rev2 = self
-        return ''.join(self.repository.get_diff(rev1, rev2,
+        return b''.join(self.repository.get_diff(rev1, rev2,
                                     ignore_whitespace=ignore_whitespace,
                                     context=context))
 
@@ -254,7 +240,6 @@
         Returns stat mode of the file at the given ``path``.
         """
         # ensure path is traversed
-        path = safe_str(path)
         self._get_id_for_path(path)
         return self._stat_modes[path]
 
@@ -290,17 +275,15 @@
         iterating commits.
         """
         self._get_filectx(path)
-        cs_id = safe_str(self.id)
-        f_path = safe_str(path)
 
         if limit is not None:
             cmd = ['log', '-n', str(safe_int(limit, 0)),
-                   '--pretty=format:%H', '-s', cs_id, '--', f_path]
+                   '--pretty=format:%H', '-s', self.raw_id, '--', path]
 
         else:
             cmd = ['log',
-                   '--pretty=format:%H', '-s', cs_id, '--', f_path]
-        so, se = self.repository.run_git_command(cmd)
+                   '--pretty=format:%H', '-s', self.raw_id, '--', path]
+        so = self.repository.run_git_command(cmd)
         ids = re.findall(r'[0-9a-fA-F]{40}', so)
         return [self.repository.get_changeset(sha) for sha in ids]
 
@@ -312,31 +295,29 @@
         """
         self._get_filectx(path)
         from dulwich.walk import Walker
-        include = [self.id]
+        include = [self.raw_id]
         walker = Walker(self.repository._repo.object_store, include,
                         paths=[path], max_entries=1)
-        return [self.repository.get_changeset(sha)
-                for sha in (x.commit.id for x in walker)]
+        return [self.repository.get_changeset(ascii_str(x.commit.id.decode))
+                for x in walker]
 
     def get_file_annotate(self, path):
         """
         Returns a generator of four element tuples with
             lineno, sha, changeset lazy loader and line
-
-        TODO: This function now uses os underlying 'git' command which is
-        generally not good. Should be replaced with algorithm iterating
-        commits.
         """
-        cmd = ['blame', '-l', '--root', '-r', self.id, '--', path]
+        # TODO: This function now uses os underlying 'git' command which is
+        # generally not good. Should be replaced with algorithm iterating
+        # commits.
+        cmd = ['blame', '-l', '--root', '-r', self.raw_id, '--', path]
         # -l     ==> outputs long shas (and we need all 40 characters)
         # --root ==> doesn't put '^' character for boundaries
         # -r sha ==> blames for the given revision
-        so, se = self.repository.run_git_command(cmd)
+        so = self.repository.run_git_command(cmd)
 
         for i, blame_line in enumerate(so.split('\n')[:-1]):
-            ln_no = i + 1
             sha, line = re.split(r' ', blame_line, 1)
-            yield (ln_no, sha, lambda: self.repository.get_changeset(sha), line)
+            yield (i + 1, sha, lambda sha=sha: self.repository.get_changeset(sha), line)
 
     def fill_archive(self, stream=None, kind='tgz', prefix=None,
                      subrepos=False):
@@ -353,12 +334,15 @@
 
         :raise ImproperArchiveTypeError: If given kind is wrong.
         :raise VcsError: If given stream is None
-
         """
-        allowed_kinds = settings.ARCHIVE_SPECS.keys()
+        allowed_kinds = settings.ARCHIVE_SPECS
         if kind not in allowed_kinds:
             raise ImproperArchiveTypeError('Archive kind not supported use one'
-                'of %s' % allowed_kinds)
+                'of %s' % ' '.join(allowed_kinds))
+
+        if stream is None:
+            raise VCSError('You need to pass in a valid stream for filling'
+                           ' with archival data')
 
         if prefix is None:
             prefix = '%s-%s' % (self.repository.name, self.short_id)
@@ -394,25 +378,30 @@
         popen.communicate()
 
     def get_nodes(self, path):
+        """
+        Returns combined ``DirNode`` and ``FileNode`` objects list representing
+        state of changeset at the given ``path``. If node at the given ``path``
+        is not instance of ``DirNode``, ChangesetError would be raised.
+        """
+
         if self._get_kind(path) != NodeKind.DIR:
             raise ChangesetError("Directory does not exist for revision %s at "
                 " '%s'" % (self.revision, path))
-        path = self._fix_path(path)
+        path = path.rstrip('/')
         id = self._get_id_for_path(path)
         tree = self.repository._repo[id]
         dirnodes = []
         filenodes = []
         als = self.repository.alias
-        for name, stat, id in tree.iteritems():
+        for name, stat, id in tree.items():
+            obj_path = safe_str(name)
             if path != '':
-                obj_path = '/'.join((path, name))
-            else:
-                obj_path = name
+                obj_path = '/'.join((path, obj_path))
             if objects.S_ISGITLINK(stat):
                 root_tree = self.repository._repo[self._tree_id]
-                cf = ConfigFile.from_file(BytesIO(self.repository._repo.get_object(root_tree['.gitmodules'][1]).data))
-                url = cf.get(('submodule', obj_path), 'url')
-                dirnodes.append(SubModuleNode(obj_path, url=url, changeset=id,
+                cf = ConfigFile.from_file(BytesIO(self.repository._repo.get_object(root_tree[b'.gitmodules'][1]).data))
+                url = ascii_str(cf.get(('submodule', obj_path), 'url'))
+                dirnodes.append(SubModuleNode(obj_path, url=url, changeset=ascii_str(id),
                                               alias=als))
                 continue
 
@@ -434,9 +423,11 @@
         return nodes
 
     def get_node(self, path):
-        if isinstance(path, unicode):
-            path = path.encode('utf-8')
-        path = self._fix_path(path)
+        """
+        Returns ``Node`` object from the given ``path``. If there is no node at
+        the given ``path``, ``ChangesetError`` would be raised.
+        """
+        path = path.rstrip('/')
         if path not in self.nodes:
             try:
                 id_ = self._get_id_for_path(path)
@@ -444,12 +435,12 @@
                 raise NodeDoesNotExistError("Cannot find one of parents' "
                     "directories for a given path: %s" % path)
 
-            _GL = lambda m: m and objects.S_ISGITLINK(m)
-            if _GL(self._stat_modes.get(path)):
+            stat = self._stat_modes.get(path)
+            if stat and objects.S_ISGITLINK(stat):
                 tree = self.repository._repo[self._tree_id]
-                cf = ConfigFile.from_file(BytesIO(self.repository._repo.get_object(tree['.gitmodules'][1]).data))
-                url = cf.get(('submodule', path), 'url')
-                node = SubModuleNode(path, url=url, changeset=id_,
+                cf = ConfigFile.from_file(BytesIO(self.repository._repo.get_object(tree[b'.gitmodules'][1]).data))
+                url = ascii_str(cf.get(('submodule', path), 'url'))
+                node = SubModuleNode(path, url=url, changeset=ascii_str(id_),
                                      alias=self.repository.alias)
             else:
                 obj = self.repository._repo.get_object(id_)
@@ -465,7 +456,7 @@
                     node._blob = obj
                 else:
                     raise NodeDoesNotExistError("There is no file nor directory "
-                        "at the given path '%s' at revision %s"
+                        "at the given path: '%s' at revision %s"
                         % (path, self.short_id))
             # cache node
             self.nodes[path] = node
@@ -480,16 +471,6 @@
         return list(added.union(modified).union(deleted))
 
     @LazyProperty
-    def _diff_name_status(self):
-        output = []
-        for parent in self.parents:
-            cmd = ['diff', '--name-status', parent.raw_id, self.raw_id,
-                   '--encoding=utf8']
-            so, se = self.repository.run_git_command(cmd)
-            output.append(so.strip())
-        return '\n'.join(output)
-
-    @LazyProperty
     def _changes_cache(self):
         added = set()
         modified = set()
@@ -503,15 +484,15 @@
             if isinstance(parent, EmptyChangeset):
                 oid = None
             else:
-                oid = _r[parent.raw_id].tree
-            changes = _r.object_store.tree_changes(oid, _r[self.raw_id].tree)
+                oid = _r[parent._commit.id].tree
+            changes = _r.object_store.tree_changes(oid, _r[self._commit.id].tree)
             for (oldpath, newpath), (_, _), (_, _) in changes:
                 if newpath and oldpath:
-                    modified.add(newpath)
+                    modified.add(safe_str(newpath))
                 elif newpath and not oldpath:
-                    added.add(newpath)
+                    added.add(safe_str(newpath))
                 elif not newpath and oldpath:
-                    deleted.add(oldpath)
+                    deleted.add(safe_str(oldpath))
         return added, modified, deleted
 
     def _get_paths_for_status(self, status):
--- a/kallithea/lib/vcs/backends/git/inmemory.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/backends/git/inmemory.py	Mon May 04 19:24:04 2020 +0200
@@ -7,7 +7,7 @@
 
 from kallithea.lib.vcs.backends.base import BaseInMemoryChangeset
 from kallithea.lib.vcs.exceptions import RepositoryError
-from kallithea.lib.vcs.utils import safe_str
+from kallithea.lib.vcs.utils import ascii_str, safe_bytes
 
 
 class GitInMemoryChangeset(BaseInMemoryChangeset):
@@ -39,7 +39,7 @@
         repo = self.repository._repo
         object_store = repo.object_store
 
-        ENCODING = "UTF-8"
+        ENCODING = b"UTF-8"  # TODO: should probably be kept in sync with safe_str/safe_bytes and vcs/conf/settings.py DEFAULT_ENCODINGS
 
         # Create tree and populates it with blobs
         commit_tree = self.parents[0] and repo[self.parents[0]._commit.tree] or \
@@ -47,7 +47,7 @@
         for node in self.added + self.changed:
             # Compute subdirs if needed
             dirpath, nodename = posixpath.split(node.path)
-            dirnames = map(safe_str, dirpath and dirpath.split('/') or [])
+            dirnames = safe_bytes(dirpath).split(b'/') if dirpath else []
             parent = commit_tree
             ancestors = [('', parent)]
 
@@ -68,13 +68,9 @@
             # for dirnames (in reverse order) [this only applies for nodes from added]
             new_trees = []
 
-            if not node.is_binary:
-                content = node.content.encode(ENCODING)
-            else:
-                content = node.content
-            blob = objects.Blob.from_string(content)
+            blob = objects.Blob.from_string(node.content)
 
-            node_path = node.name.encode(ENCODING)
+            node_path = safe_bytes(node.name)
             if dirnames:
                 # If there are trees which should be created we need to build
                 # them now (in reverse order)
@@ -104,7 +100,7 @@
             for tree in new_trees:
                 object_store.add_object(tree)
         for node in self.removed:
-            paths = node.path.split('/')
+            paths = safe_bytes(node.path).split(b'/')
             tree = commit_tree
             trees = [tree]
             # Traverse deep into the forest...
@@ -117,7 +113,7 @@
                 except KeyError:
                     break
             # Cut down the blob and all rotten trees on the way back...
-            for path, tree in reversed(zip(paths, trees)):
+            for path, tree in reversed(list(zip(paths, trees))):
                 del tree[path]
                 if tree:
                     # This tree still has elements - don't remove it or any
@@ -130,9 +126,9 @@
         commit = objects.Commit()
         commit.tree = commit_tree.id
         commit.parents = [p._commit.id for p in self.parents if p]
-        commit.author = commit.committer = safe_str(author)
+        commit.author = commit.committer = safe_bytes(author)
         commit.encoding = ENCODING
-        commit.message = safe_str(message)
+        commit.message = safe_bytes(message)
 
         # Compute date
         if date is None:
@@ -150,11 +146,10 @@
 
         object_store.add_object(commit)
 
-        ref = 'refs/heads/%s' % branch
+        # Update vcs repository object & recreate dulwich repo
+        ref = b'refs/heads/%s' % safe_bytes(branch)
         repo.refs[ref] = commit.id
-
-        # Update vcs repository object & recreate dulwich repo
-        self.repository.revisions.append(commit.id)
+        self.repository.revisions.append(ascii_str(commit.id))
         # invalidate parsed refs after commit
         self.repository._parsed_refs = self.repository._get_parsed_refs()
         tip = self.repository.get_changeset()
@@ -177,15 +172,15 @@
             return []
 
         def get_tree_for_dir(tree, dirname):
-            for name, mode, id in tree.iteritems():
+            for name, mode, id in tree.items():
                 if name == dirname:
                     obj = self.repository._repo[id]
                     if isinstance(obj, objects.Tree):
                         return obj
                     else:
                         raise RepositoryError("Cannot create directory %s "
-                        "at tree %s as path is occupied and is not a "
-                        "Tree" % (dirname, tree))
+                            "at tree %s as path is occupied and is not a "
+                            "Tree" % (dirname, tree))
             return None
 
         trees = []
--- a/kallithea/lib/vcs/backends/git/repository.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/backends/git/repository.py	Mon May 04 19:24:04 2020 +0200
@@ -12,13 +12,15 @@
 import errno
 import logging
 import os
-import posixpath
 import re
 import time
-import urllib
-import urllib2
+import urllib.error
+import urllib.parse
+import urllib.request
 from collections import OrderedDict
 
+import mercurial.url  # import httpbasicauthhandler, httpdigestauthhandler
+import mercurial.util  # import url as hg_url
 from dulwich.config import ConfigFile
 from dulwich.objects import Tag
 from dulwich.repo import NotGitRepository, Repo
@@ -26,10 +28,9 @@
 from kallithea.lib.vcs import subprocessio
 from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator
 from kallithea.lib.vcs.conf import settings
-from kallithea.lib.vcs.exceptions import (
-    BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, TagAlreadyExistError, TagDoesNotExistError)
-from kallithea.lib.vcs.utils import date_fromtimestamp, makedate, safe_str, safe_unicode
-from kallithea.lib.vcs.utils.hgcompat import hg_url, httpbasicauthhandler, httpdigestauthhandler
+from kallithea.lib.vcs.exceptions import (BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
+                                          TagDoesNotExistError)
+from kallithea.lib.vcs.utils import ascii_str, date_fromtimestamp, makedate, safe_bytes, safe_str
 from kallithea.lib.vcs.utils.lazy import LazyProperty
 from kallithea.lib.vcs.utils.paths import abspath, get_user_home
 
@@ -53,7 +54,7 @@
     def __init__(self, repo_path, create=False, src_url=None,
                  update_after_clone=False, bare=False):
 
-        self.path = safe_unicode(abspath(repo_path))
+        self.path = abspath(repo_path)
         self.repo = self._get_repo(create, src_url, update_after_clone, bare)
         self.bare = self.repo.bare
 
@@ -97,63 +98,54 @@
         return self._get_all_revisions()
 
     @classmethod
-    def _run_git_command(cls, cmd, **opts):
+    def _run_git_command(cls, cmd, cwd=None):
         """
-        Runs given ``cmd`` as git command and returns tuple
-        (stdout, stderr).
+        Runs given ``cmd`` as git command and returns output bytes in a tuple
+        (stdout, stderr) ... or raise RepositoryError.
 
         :param cmd: git command to be executed
-        :param opts: env options to pass into Subprocess command
+        :param cwd: passed directly to subprocess
         """
-
-        if '_bare' in opts:
-            _copts = []
-            del opts['_bare']
-        else:
-            _copts = ['-c', 'core.quotepath=false', ]
-        safe_call = False
-        if '_safe' in opts:
-            # no exc on failure
-            del opts['_safe']
-            safe_call = True
-
-        assert isinstance(cmd, list), cmd
-
-        gitenv = os.environ
         # need to clean fix GIT_DIR !
-        if 'GIT_DIR' in gitenv:
-            del gitenv['GIT_DIR']
+        gitenv = dict(os.environ)
+        gitenv.pop('GIT_DIR', None)
         gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
 
-        _git_path = settings.GIT_EXECUTABLE_PATH
-        cmd = [_git_path] + _copts + cmd
+        assert isinstance(cmd, list), cmd
+        cmd = [settings.GIT_EXECUTABLE_PATH, '-c', 'core.quotepath=false'] + cmd
+        try:
+            p = subprocessio.SubprocessIOChunker(cmd, cwd=cwd, env=gitenv, shell=False)
+        except (EnvironmentError, OSError) as err:
+            # output from the failing process is in str(EnvironmentError)
+            msg = ("Couldn't run git command %s.\n"
+                   "Subprocess failed with '%s': %s\n" %
+                   (cmd, type(err).__name__, err)
+            ).strip()
+            log.error(msg)
+            raise RepositoryError(msg)
 
         try:
-            _opts = dict(
-                env=gitenv,
-                shell=False,
-            )
-            _opts.update(opts)
-            p = subprocessio.SubprocessIOChunker(cmd, **_opts)
-        except (EnvironmentError, OSError) as err:
-            tb_err = ("Couldn't run git command (%s).\n"
-                      "Original error was:%s\n" % (cmd, err))
-            log.error(tb_err)
-            if safe_call:
-                return '', err
-            else:
-                raise RepositoryError(tb_err)
-
-        try:
-            return ''.join(p.output), ''.join(p.error)
+            stdout = b''.join(p.output)
+            stderr = b''.join(p.error)
         finally:
             p.close()
+        # TODO: introduce option to make commands fail if they have any stderr output?
+        if stderr:
+            log.debug('stderr from %s:\n%s', cmd, stderr)
+        else:
+            log.debug('stderr from %s: None', cmd)
+        return stdout, stderr
 
     def run_git_command(self, cmd):
-        opts = {}
+        """
+        Runs given ``cmd`` as git command with cwd set to current repo.
+        Returns stdout as unicode str ... or raise RepositoryError.
+        """
+        cwd = None
         if os.path.isdir(self.path):
-            opts['cwd'] = self.path
-        return self._run_git_command(cmd, **opts)
+            cwd = self.path
+        stdout, _stderr = self._run_git_command(cmd, cwd=cwd)
+        return safe_str(stdout)
 
     @classmethod
     def _check_url(cls, url):
@@ -166,7 +158,6 @@
         On failures it'll raise urllib2.HTTPError, exception is also thrown
         when the return code is non 200
         """
-
         # check first if it's not an local url
         if os.path.isdir(url) or url.startswith('file:'):
             return True
@@ -178,29 +169,30 @@
             url = url[url.find('+') + 1:]
 
         handlers = []
-        url_obj = hg_url(url)
+        url_obj = mercurial.util.url(safe_bytes(url))
         test_uri, authinfo = url_obj.authinfo()
-        url_obj.passwd = '*****'
+        if not test_uri.endswith(b'info/refs'):
+            test_uri = test_uri.rstrip(b'/') + b'/info/refs'
+
+        url_obj.passwd = b'*****'
         cleaned_uri = str(url_obj)
 
-        if not test_uri.endswith('info/refs'):
-            test_uri = test_uri.rstrip('/') + '/info/refs'
-
         if authinfo:
             # create a password manager
-            passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
+            passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
             passmgr.add_password(*authinfo)
 
-            handlers.extend((httpbasicauthhandler(passmgr),
-                             httpdigestauthhandler(passmgr)))
+            handlers.extend((mercurial.url.httpbasicauthhandler(passmgr),
+                             mercurial.url.httpdigestauthhandler(passmgr)))
 
-        o = urllib2.build_opener(*handlers)
+        o = urllib.request.build_opener(*handlers)
         o.addheaders = [('User-Agent', 'git/1.7.8.0')]  # fake some git
 
-        q = {"service": 'git-upload-pack'}
-        qs = '?%s' % urllib.urlencode(q)
-        cu = "%s%s" % (test_uri, qs)
-        req = urllib2.Request(cu, None, {})
+        req = urllib.request.Request(
+            "%s?%s" % (
+                safe_str(test_uri),
+                urllib.parse.urlencode({"service": 'git-upload-pack'})
+            ))
 
         try:
             resp = o.open(req)
@@ -208,13 +200,13 @@
                 raise Exception('Return Code is not 200')
         except Exception as e:
             # means it cannot be cloned
-            raise urllib2.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
+            raise urllib.error.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
 
         # now detect if it's proper git repo
         gitdata = resp.read()
-        if 'service=git-upload-pack' not in gitdata:
-            raise urllib2.URLError(
-                "url [%s] does not look like an git" % (cleaned_uri))
+        if b'service=git-upload-pack' not in gitdata:
+            raise urllib.error.URLError(
+                "url [%s] does not look like an git" % cleaned_uri)
 
         return True
 
@@ -253,7 +245,7 @@
         rev_filter = settings.GIT_REV_FILTER
         cmd = ['rev-list', rev_filter, '--reverse', '--date-order']
         try:
-            so, se = self.run_git_command(cmd)
+            so = self.run_git_command(cmd)
         except RepositoryError:
             # Can be raised for empty repositories
             return []
@@ -261,58 +253,56 @@
 
     def _get_all_revisions2(self):
         # alternate implementation using dulwich
-        includes = [x[1][0] for x in self._parsed_refs.iteritems()
-                    if x[1][1] != 'T']
+        includes = [ascii_str(sha) for key, (sha, type_) in self._parsed_refs.items()
+                    if type_ != b'T']
         return [c.commit.id for c in self._repo.get_walker(include=includes)]
 
     def _get_revision(self, revision):
         """
-        For git backend we always return integer here. This way we ensure
-        that changeset's revision attribute would become integer.
+        Given any revision identifier, returns a 40 char string with revision hash.
         """
-
-        is_null = lambda o: len(o) == revision.count('0')
-
         if self._empty:
             raise EmptyRepositoryError("There are no changesets yet")
 
         if revision in (None, '', 'tip', 'HEAD', 'head', -1):
-            return self.revisions[-1]
+            revision = -1
 
-        is_bstr = isinstance(revision, (str, unicode))
-        if ((is_bstr and revision.isdigit() and len(revision) < 12)
-            or isinstance(revision, int) or is_null(revision)
-        ):
+        if isinstance(revision, int):
             try:
-                revision = self.revisions[int(revision)]
+                return self.revisions[revision]
             except IndexError:
-                msg = ("Revision %s does not exist for %s" % (revision, self))
+                msg = "Revision %r does not exist for %s" % (revision, self.name)
                 raise ChangesetDoesNotExistError(msg)
 
-        elif is_bstr:
-            # get by branch/tag name
-            _ref_revision = self._parsed_refs.get(revision)
-            if _ref_revision:  # and _ref_revision[1] in ['H', 'RH', 'T']:
-                return _ref_revision[0]
+        if isinstance(revision, str):
+            if revision.isdigit() and (len(revision) < 12 or len(revision) == revision.count('0')):
+                try:
+                    return self.revisions[int(revision)]
+                except IndexError:
+                    msg = "Revision %r does not exist for %s" % (revision, self)
+                    raise ChangesetDoesNotExistError(msg)
 
-            _tags_shas = self.tags.values()
+            # get by branch/tag name
+            _ref_revision = self._parsed_refs.get(safe_bytes(revision))
+            if _ref_revision:  # and _ref_revision[1] in [b'H', b'RH', b'T']:
+                return ascii_str(_ref_revision[0])
+
+            if revision in self.revisions:
+                return revision
+
             # maybe it's a tag ? we don't have them in self.revisions
-            if revision in _tags_shas:
-                return _tags_shas[_tags_shas.index(revision)]
+            if revision in self.tags.values():
+                return revision
 
-            elif not SHA_PATTERN.match(revision) or revision not in self.revisions:
-                msg = ("Revision %s does not exist for %s" % (revision, self))
+            if SHA_PATTERN.match(revision):
+                msg = "Revision %r does not exist for %s" % (revision, self.name)
                 raise ChangesetDoesNotExistError(msg)
 
-        # Ensure we return full id
-        if not SHA_PATTERN.match(str(revision)):
-            raise ChangesetDoesNotExistError("Given revision %s not recognized"
-                % revision)
-        return revision
+        raise ChangesetDoesNotExistError("Given revision %r not recognized" % revision)
 
     def get_ref_revision(self, ref_type, ref_name):
         """
-        Returns ``MercurialChangeset`` object representing repository's
+        Returns ``GitChangeset`` object representing repository's
         changeset at the given ``revision``.
         """
         return self._get_revision(ref_name)
@@ -327,20 +317,10 @@
         Returns normalized url. If schema is not given, would fall to
         filesystem (``file:///``) schema.
         """
-        url = safe_str(url)
         if url != 'default' and '://' not in url:
             url = ':///'.join(('file', url))
         return url
 
-    def get_hook_location(self):
-        """
-        returns absolute path to location where hooks are stored
-        """
-        loc = os.path.join(self.path, 'hooks')
-        if not self.bare:
-            loc = os.path.join(self.path, '.git', 'hooks')
-        return loc
-
     @LazyProperty
     def name(self):
         return os.path.basename(self.path)
@@ -367,23 +347,20 @@
 
     @LazyProperty
     def description(self):
-        undefined_description = u'unknown'
-        _desc = self._repo.get_description()
-        return safe_unicode(_desc or undefined_description)
+        return safe_str(self._repo.get_description() or b'unknown')
 
     @LazyProperty
     def contact(self):
-        undefined_contact = u'Unknown'
+        undefined_contact = 'Unknown'
         return undefined_contact
 
     @property
     def branches(self):
         if not self.revisions:
             return {}
-        sortkey = lambda ctx: ctx[0]
-        _branches = [(x[0], x[1][0])
-                     for x in self._parsed_refs.iteritems() if x[1][1] == 'H']
-        return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
+        _branches = [(safe_str(key), ascii_str(sha))
+                     for key, (sha, type_) in self._parsed_refs.items() if type_ == b'H']
+        return OrderedDict(sorted(_branches, key=(lambda ctx: ctx[0]), reverse=False))
 
     @LazyProperty
     def closed_branches(self):
@@ -396,11 +373,9 @@
     def _get_tags(self):
         if not self.revisions:
             return {}
-
-        sortkey = lambda ctx: ctx[0]
-        _tags = [(x[0], x[1][0])
-                 for x in self._parsed_refs.iteritems() if x[1][1] == 'T']
-        return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
+        _tags = [(safe_str(key), ascii_str(sha))
+                 for key, (sha, type_) in self._parsed_refs.items() if type_ == b'T']
+        return OrderedDict(sorted(_tags, key=(lambda ctx: ctx[0]), reverse=True))
 
     def tag(self, name, user, revision=None, message=None, date=None,
             **kwargs):
@@ -420,7 +395,7 @@
         changeset = self.get_changeset(revision)
         message = message or "Added tag %s for commit %s" % (name,
             changeset.raw_id)
-        self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
+        self._repo.refs[b"refs/tags/%s" % safe_bytes(name)] = changeset._commit.id
 
         self._parsed_refs = self._get_parsed_refs()
         self.tags = self._get_tags()
@@ -439,7 +414,8 @@
         """
         if name not in self.tags:
             raise TagDoesNotExistError("Tag %s does not exist" % name)
-        tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
+        # self._repo.refs is a DiskRefsContainer, and .path gives the full absolute path of '.git'
+        tagpath = os.path.join(safe_str(self._repo.refs.path), 'refs', 'tags', name)
         try:
             os.remove(tagpath)
             self._parsed_refs = self._get_parsed_refs()
@@ -459,18 +435,20 @@
         return self._get_parsed_refs()
 
     def _get_parsed_refs(self):
-        # cache the property
+        """Return refs as a dict, like:
+        { b'v0.2.0': [b'599ba911aa24d2981225f3966eb659dfae9e9f30', b'T'] }
+        """
         _repo = self._repo
         refs = _repo.get_refs()
-        keys = [('refs/heads/', 'H'),
-                ('refs/remotes/origin/', 'RH'),
-                ('refs/tags/', 'T')]
+        keys = [(b'refs/heads/', b'H'),
+                (b'refs/remotes/origin/', b'RH'),
+                (b'refs/tags/', b'T')]
         _refs = {}
-        for ref, sha in refs.iteritems():
+        for ref, sha in refs.items():
             for k, type_ in keys:
                 if ref.startswith(k):
                     _key = ref[len(k):]
-                    if type_ == 'T':
+                    if type_ == b'T':
                         obj = _repo.get_object(sha)
                         if isinstance(obj, Tag):
                             sha = _repo.get_object(sha).object[1]
@@ -483,13 +461,13 @@
         heads = {}
 
         for key, val in refs.items():
-            for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
+            for ref_key in [b'refs/heads/', b'refs/remotes/origin/']:
                 if key.startswith(ref_key):
                     n = key[len(ref_key):]
-                    if n not in ['HEAD']:
+                    if n not in [b'HEAD']:
                         heads[n] = val
 
-        return heads if reverse else dict((y, x) for x, y in heads.iteritems())
+        return heads if reverse else dict((y, x) for x, y in heads.items())
 
     def get_changeset(self, revision=None):
         """
@@ -498,9 +476,7 @@
         """
         if isinstance(revision, GitChangeset):
             return revision
-        revision = self._get_revision(revision)
-        changeset = GitChangeset(repository=self, revision=revision)
-        return changeset
+        return GitChangeset(repository=self, revision=self._get_revision(revision))
 
     def get_changesets(self, start=None, end=None, start_date=None,
            end_date=None, branch_name=None, reverse=False, max_revisions=None):
@@ -547,7 +523,7 @@
         else:
             cmd.append(settings.GIT_REV_FILTER)
 
-        revs = self.run_git_command(cmd)[0].splitlines()
+        revs = self.run_git_command(cmd).splitlines()
         start_pos = 0
         end_pos = len(revs)
         if start:
@@ -572,14 +548,15 @@
 
         revs = revs[start_pos:end_pos]
         if reverse:
-            revs = reversed(revs)
+            revs.reverse()
+
         return CollectionGenerator(self, revs)
 
     def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
                  context=3):
         """
-        Returns (git like) *diff*, as plain text. Shows changes introduced by
-        ``rev2`` since ``rev1``.
+        Returns (git like) *diff*, as plain bytes text. Shows changes
+        introduced by ``rev2`` since ``rev1``.
 
         :param rev1: Entry point from which diff is shown. Can be
           ``self.EMPTY_CHANGESET`` - in this case, patch showing all
@@ -633,14 +610,13 @@
         if path:
             cmd += ['--', path]
 
-        stdout, stderr = self.run_git_command(cmd)
-        # TODO: don't ignore stderr
+        stdout, stderr = self._run_git_command(cmd, cwd=self.path)
         # If we used 'show' command, strip first few lines (until actual diff
         # starts)
         if rev1 == self.EMPTY_CHANGESET:
-            parts = stdout.split('\ndiff ', 1)
+            parts = stdout.split(b'\ndiff ', 1)
             if len(parts) > 1:
-                stdout = 'diff ' + parts[1]
+                stdout = b'diff ' + parts[1]
         return stdout
 
     @LazyProperty
@@ -683,7 +659,7 @@
         Tries to pull changes from external location.
         """
         url = self._get_url(url)
-        so, se = self.run_git_command(['ls-remote', '-h', url])
+        so = self.run_git_command(['ls-remote', '-h', url])
         cmd = ['fetch', url, '--']
         for line in (x for x in so.splitlines()):
             sha, ref = line.split('\t')
@@ -721,7 +697,7 @@
         """
         if config_file is None:
             config_file = []
-        elif isinstance(config_file, basestring):
+        elif isinstance(config_file, str):
             config_file = [config_file]
 
         def gen_configs():
@@ -733,9 +709,10 @@
 
         for config in gen_configs():
             try:
-                return config.get(section, name)
+                value = config.get(section, name)
             except KeyError:
                 continue
+            return None if value is None else safe_str(value)
         return None
 
     def get_user_name(self, config_file=None):
--- a/kallithea/lib/vcs/backends/git/ssh.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/backends/git/ssh.py	Mon May 04 19:24:04 2020 +0200
@@ -17,7 +17,6 @@
 
 from kallithea.lib.hooks import log_pull_action
 from kallithea.lib.utils import make_ui
-from kallithea.lib.utils2 import safe_str, safe_unicode
 from kallithea.lib.vcs.backends.ssh import BaseSshHandler
 
 
@@ -33,15 +32,15 @@
         >>> import shlex
 
         >>> GitSshHandler.make(shlex.split("git-upload-pack '/foo bar'")).repo_name
-        u'foo bar'
+        'foo bar'
         >>> GitSshHandler.make(shlex.split("git-upload-pack '/foo bar'")).verb
         'git-upload-pack'
         >>> GitSshHandler.make(shlex.split(" git-upload-pack /blåbærgrød ")).repo_name # might not be necessary to support no quoting ... but we can
-        u'bl\xe5b\xe6rgr\xf8d'
+        'bl\xe5b\xe6rgr\xf8d'
         >>> GitSshHandler.make(shlex.split('''git-upload-pack "/foo'bar"''')).repo_name
-        u"foo'bar"
+        "foo'bar"
         >>> GitSshHandler.make(shlex.split("git-receive-pack '/foo'")).repo_name
-        u'foo'
+        'foo'
         >>> GitSshHandler.make(shlex.split("git-receive-pack '/foo'")).verb
         'git-receive-pack'
 
@@ -56,12 +55,12 @@
             ssh_command_parts[0] in ['git-upload-pack', 'git-receive-pack'] and
             ssh_command_parts[1].startswith('/')
         ):
-            return cls(safe_unicode(ssh_command_parts[1][1:]), ssh_command_parts[0])
+            return cls(ssh_command_parts[1][1:], ssh_command_parts[0])
 
         return None
 
     def __init__(self, repo_name, verb):
-        self.repo_name = repo_name
+        BaseSshHandler.__init__(self, repo_name)
         self.verb = verb
 
     def _serve(self):
@@ -70,7 +69,7 @@
             log_pull_action(ui=make_ui(), repo=self.db_repo.scm_instance._repo)
         else: # probably verb 'git-receive-pack', action 'push'
             if not self.allow_push:
-                self.exit('Push access to %r denied' % safe_str(self.repo_name))
+                self.exit('Push access to %r denied' % self.repo_name)
             # Note: push logging is handled by Git post-receive hook
 
         # git shell is not a real shell but use shell inspired quoting *inside* the argument.
--- a/kallithea/lib/vcs/backends/git/workdir.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/backends/git/workdir.py	Mon May 04 19:24:04 2020 +0200
@@ -1,5 +1,6 @@
 import re
 
+from kallithea.lib.utils2 import ascii_str, safe_str
 from kallithea.lib.vcs.backends.base import BaseWorkdir
 from kallithea.lib.vcs.exceptions import BranchDoesNotExistError, RepositoryError
 
@@ -7,9 +8,9 @@
 class GitWorkdir(BaseWorkdir):
 
     def get_branch(self):
-        headpath = self.repository._repo.refs.refpath('HEAD')
+        headpath = self.repository._repo.refs.refpath(b'HEAD')
         try:
-            content = open(headpath).read()
+            content = safe_str(open(headpath, 'rb').read())
             match = re.match(r'^ref: refs/heads/(?P<branch>.+)\n$', content)
             if match:
                 return match.groupdict()['branch']
@@ -20,7 +21,7 @@
             raise RepositoryError("Couldn't compute workdir's branch")
 
     def get_changeset(self):
-        wk_dir_id = self.repository._repo.refs.as_dict().get('HEAD')
+        wk_dir_id = ascii_str(self.repository._repo.refs.as_dict().get(b'HEAD'))
         return self.repository.get_changeset(wk_dir_id)
 
     def checkout_branch(self, branch=None):
--- a/kallithea/lib/vcs/backends/hg/__init__.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/backends/hg/__init__.py	Mon May 04 19:24:04 2020 +0200
@@ -9,6 +9,8 @@
     :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
 """
 
+from kallithea.lib.vcs.utils import hgcompat
+
 from .changeset import MercurialChangeset
 from .inmemory import MercurialInMemoryChangeset
 from .repository import MercurialRepository
@@ -19,3 +21,5 @@
     'MercurialRepository', 'MercurialChangeset',
     'MercurialInMemoryChangeset', 'MercurialWorkdir',
 ]
+
+hgcompat.monkey_do()
--- a/kallithea/lib/vcs/backends/hg/changeset.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/backends/hg/changeset.py	Mon May 04 19:24:04 2020 +0200
@@ -1,41 +1,44 @@
 import os
 import posixpath
 
+import mercurial.archival
+import mercurial.node
+import mercurial.obsutil
+
 from kallithea.lib.vcs.backends.base import BaseChangeset
 from kallithea.lib.vcs.conf import settings
 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, ChangesetError, ImproperArchiveTypeError, NodeDoesNotExistError, VCSError
-from kallithea.lib.vcs.nodes import (
-    AddedFileNodesGenerator, ChangedFileNodesGenerator, DirNode, FileNode, NodeKind, RemovedFileNodesGenerator, RootNode, SubModuleNode)
-from kallithea.lib.vcs.utils import date_fromtimestamp, safe_str, safe_unicode
-from kallithea.lib.vcs.utils.hgcompat import archival, hex, obsutil
+from kallithea.lib.vcs.nodes import (AddedFileNodesGenerator, ChangedFileNodesGenerator, DirNode, FileNode, NodeKind, RemovedFileNodesGenerator, RootNode,
+                                     SubModuleNode)
+from kallithea.lib.vcs.utils import ascii_bytes, ascii_str, date_fromtimestamp, safe_bytes, safe_str
 from kallithea.lib.vcs.utils.lazy import LazyProperty
 from kallithea.lib.vcs.utils.paths import get_dirs_for_path
 
 
 class MercurialChangeset(BaseChangeset):
     """
-    Represents state of the repository at the single revision.
+    Represents state of the repository at a revision.
     """
 
     def __init__(self, repository, revision):
         self.repository = repository
-        assert isinstance(revision, basestring), repr(revision)
-        self.raw_id = revision
-        self._ctx = repository._repo[revision]
+        assert isinstance(revision, str), repr(revision)
+        self._ctx = repository._repo[ascii_bytes(revision)]
+        self.raw_id = ascii_str(self._ctx.hex())
         self.revision = self._ctx._rev
         self.nodes = {}
 
     @LazyProperty
     def tags(self):
-        return map(safe_unicode, self._ctx.tags())
+        return [safe_str(tag) for tag in self._ctx.tags()]
 
     @LazyProperty
     def branch(self):
-        return safe_unicode(self._ctx.branch())
+        return safe_str(self._ctx.branch())
 
     @LazyProperty
     def branches(self):
-        return [safe_unicode(self._ctx.branch())]
+        return [safe_str(self._ctx.branch())]
 
     @LazyProperty
     def closesbranch(self):
@@ -47,17 +50,11 @@
 
     @LazyProperty
     def bumped(self):
-        try:
-            return self._ctx.phasedivergent()
-        except AttributeError: # renamed in Mercurial 4.6 (9fa874fb34e1)
-            return self._ctx.bumped()
+        return self._ctx.phasedivergent()
 
     @LazyProperty
     def divergent(self):
-        try:
-            return self._ctx.contentdivergent()
-        except AttributeError: # renamed in Mercurial 4.6 (8b2d7684407b)
-            return self._ctx.divergent()
+        return self._ctx.contentdivergent()
 
     @LazyProperty
     def extinct(self):
@@ -65,10 +62,7 @@
 
     @LazyProperty
     def unstable(self):
-        try:
-            return self._ctx.orphan()
-        except AttributeError: # renamed in Mercurial 4.6 (03039ff3082b)
-            return self._ctx.unstable()
+        return self._ctx.orphan()
 
     @LazyProperty
     def phase(self):
@@ -81,33 +75,30 @@
 
     @LazyProperty
     def successors(self):
-        successors = obsutil.successorssets(self._ctx._repo, self._ctx.node(), closest=True)
-        if successors:
-            # flatten the list here handles both divergent (len > 1)
-            # and the usual case (len = 1)
-            successors = [hex(n)[:12] for sub in successors for n in sub if n != self._ctx.node()]
-
-        return successors
+        successors = mercurial.obsutil.successorssets(self._ctx._repo, self._ctx.node(), closest=True)
+        # flatten the list here handles both divergent (len > 1)
+        # and the usual case (len = 1)
+        return [safe_str(mercurial.node.hex(n)[:12]) for sub in successors for n in sub if n != self._ctx.node()]
 
     @LazyProperty
     def predecessors(self):
-        return [hex(n)[:12] for n in obsutil.closestpredecessors(self._ctx._repo, self._ctx.node())]
+        return [safe_str(mercurial.node.hex(n)[:12]) for n in mercurial.obsutil.closestpredecessors(self._ctx._repo, self._ctx.node())]
 
     @LazyProperty
     def bookmarks(self):
-        return map(safe_unicode, self._ctx.bookmarks())
+        return [safe_str(bookmark) for bookmark in self._ctx.bookmarks()]
 
     @LazyProperty
     def message(self):
-        return safe_unicode(self._ctx.description())
+        return safe_str(self._ctx.description())
 
     @LazyProperty
     def committer(self):
-        return safe_unicode(self.author)
+        return safe_str(self.author)
 
     @LazyProperty
     def author(self):
-        return safe_unicode(self._ctx.user())
+        return safe_str(self._ctx.user())
 
     @LazyProperty
     def date(self):
@@ -127,7 +118,7 @@
 
     @LazyProperty
     def _file_paths(self):
-        return list(self._ctx)
+        return list(safe_str(f) for f in self._ctx)
 
     @LazyProperty
     def _dir_paths(self):
@@ -140,12 +131,6 @@
         return self._dir_paths + self._file_paths
 
     @LazyProperty
-    def id(self):
-        if self.last:
-            return u'tip'
-        return self.short_id
-
-    @LazyProperty
     def short_id(self):
         return self.raw_id[:12]
 
@@ -202,22 +187,11 @@
                 return cs
 
     def diff(self):
-        # Only used for feed diffstat
-        return ''.join(self._ctx.diff())
-
-    def _fix_path(self, path):
-        """
-        Paths are stored without trailing slash so we need to get rid off it if
-        needed. Also mercurial keeps filenodes as str so we need to decode
-        from unicode to str
-        """
-        if path.endswith('/'):
-            path = path.rstrip('/')
-
-        return safe_str(path)
+        # Only used to feed diffstat
+        return b''.join(self._ctx.diff())
 
     def _get_kind(self, path):
-        path = self._fix_path(path)
+        path = path.rstrip('/')
         if path in self._file_paths:
             return NodeKind.FILE
         elif path in self._dir_paths:
@@ -227,11 +201,11 @@
                 % (path))
 
     def _get_filectx(self, path):
-        path = self._fix_path(path)
+        path = path.rstrip('/')
         if self._get_kind(path) != NodeKind.FILE:
             raise ChangesetError("File does not exist for revision %s at "
                 " '%s'" % (self.raw_id, path))
-        return self._ctx.filectx(path)
+        return self._ctx.filectx(safe_bytes(path))
 
     def _extract_submodules(self):
         """
@@ -245,10 +219,10 @@
         Returns stat mode of the file at the given ``path``.
         """
         fctx = self._get_filectx(path)
-        if 'x' in fctx.flags():
-            return 0100755
+        if b'x' in fctx.flags():
+            return 0o100755
         else:
-            return 0100644
+            return 0o100644
 
     def get_file_content(self, path):
         """
@@ -280,7 +254,7 @@
         cnt = 0
         for cs in reversed([x for x in fctx.filelog()]):
             cnt += 1
-            hist.append(hex(fctx.filectx(cs).node()))
+            hist.append(mercurial.node.hex(fctx.filectx(cs).node()))
             if limit is not None and cnt == limit:
                 break
 
@@ -292,13 +266,10 @@
             lineno, sha, changeset lazy loader and line
         """
         annotations = self._get_filectx(path).annotate()
-        try:
-            annotation_lines = [(annotateline.fctx, annotateline.text) for annotateline in annotations]
-        except AttributeError: # annotateline was introduced in Mercurial 4.6 (b33b91ca2ec2)
-            annotation_lines = [(aline.fctx, l) for aline, l in annotations]
-        for i, (fctx, l) in enumerate(annotation_lines):
-            sha = fctx.hex()
-            yield (i + 1, sha, lambda sha=sha, l=l: self.repository.get_changeset(sha), l)
+        annotation_lines = [(annotateline.fctx, annotateline.text) for annotateline in annotations]
+        for i, (fctx, line) in enumerate(annotation_lines):
+            sha = ascii_str(fctx.hex())
+            yield (i + 1, sha, lambda sha=sha: self.repository.get_changeset(sha), line)
 
     def fill_archive(self, stream=None, kind='tgz', prefix=None,
                      subrepos=False):
@@ -316,11 +287,10 @@
         :raise ImproperArchiveTypeError: If given kind is wrong.
         :raise VcsError: If given stream is None
         """
-
-        allowed_kinds = settings.ARCHIVE_SPECS.keys()
+        allowed_kinds = settings.ARCHIVE_SPECS
         if kind not in allowed_kinds:
             raise ImproperArchiveTypeError('Archive kind not supported use one'
-                'of %s' % allowed_kinds)
+                'of %s' % ' '.join(allowed_kinds))
 
         if stream is None:
             raise VCSError('You need to pass in a valid stream for filling'
@@ -333,8 +303,8 @@
         elif prefix.strip() == '':
             raise VCSError("Prefix cannot be empty")
 
-        archival.archive(self.repository._repo, stream, self.raw_id,
-                         kind, prefix=prefix, subrepos=subrepos)
+        mercurial.archival.archive(self.repository._repo, stream, ascii_bytes(self.raw_id),
+                         safe_bytes(kind), prefix=safe_bytes(prefix), subrepos=subrepos)
 
     def get_nodes(self, path):
         """
@@ -346,8 +316,7 @@
         if self._get_kind(path) != NodeKind.DIR:
             raise ChangesetError("Directory does not exist for revision %s at "
                 " '%s'" % (self.revision, path))
-        path = self._fix_path(path)
-
+        path = path.rstrip('/')
         filenodes = [FileNode(f, changeset=self) for f in self._file_paths
             if os.path.dirname(f) == path]
         dirs = path == '' and '' or [d for d in self._dir_paths
@@ -356,18 +325,16 @@
             if os.path.dirname(d) == path]
 
         als = self.repository.alias
-        for k, vals in self._extract_submodules().iteritems():
+        for k, vals in self._extract_submodules().items():
             #vals = url,rev,type
             loc = vals[0]
             cs = vals[1]
             dirnodes.append(SubModuleNode(k, url=loc, changeset=cs,
                                           alias=als))
         nodes = dirnodes + filenodes
-        # cache nodes
         for node in nodes:
             self.nodes[node.path] = node
         nodes.sort()
-
         return nodes
 
     def get_node(self, path):
@@ -375,9 +342,7 @@
         Returns ``Node`` object from the given ``path``. If there is no node at
         the given ``path``, ``ChangesetError`` would be raised.
         """
-
-        path = self._fix_path(path)
-
+        path = path.rstrip('/')
         if path not in self.nodes:
             if path in self._file_paths:
                 node = FileNode(path, changeset=self)
@@ -406,21 +371,21 @@
         """
         Returns list of added ``FileNode`` objects.
         """
-        return AddedFileNodesGenerator([n for n in self.status[1]], self)
+        return AddedFileNodesGenerator([safe_str(n) for n in self.status.added], self)
 
     @property
     def changed(self):
         """
         Returns list of modified ``FileNode`` objects.
         """
-        return ChangedFileNodesGenerator([n for n in self.status[0]], self)
+        return ChangedFileNodesGenerator([safe_str(n) for n in self.status.modified], self)
 
     @property
     def removed(self):
         """
         Returns list of removed ``FileNode`` objects.
         """
-        return RemovedFileNodesGenerator([n for n in self.status[2]], self)
+        return RemovedFileNodesGenerator([safe_str(n) for n in self.status.removed], self)
 
     @LazyProperty
     def extra(self):
--- a/kallithea/lib/vcs/backends/hg/inmemory.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/backends/hg/inmemory.py	Mon May 04 19:24:04 2020 +0200
@@ -1,14 +1,17 @@
 import datetime
 
+import mercurial.context
+import mercurial.node
+
 from kallithea.lib.vcs.backends.base import BaseInMemoryChangeset
 from kallithea.lib.vcs.exceptions import RepositoryError
-from kallithea.lib.vcs.utils.hgcompat import hex, memctx, memfilectx, tolocal
+from kallithea.lib.vcs.utils import ascii_str, safe_bytes, safe_str
 
 
 class MercurialInMemoryChangeset(BaseInMemoryChangeset):
 
     def commit(self, message, author, parents=None, branch=None, date=None,
-            **kwargs):
+               **kwargs):
         """
         Performs in-memory commit (doesn't check workdir in any way) and
         returns newly created ``Changeset``. Updates repository's
@@ -27,21 +30,22 @@
         """
         self.check_integrity(parents)
 
+        if not isinstance(message, str):
+            raise RepositoryError('message must be a str - got %r' % type(message))
+        if not isinstance(author, str):
+            raise RepositoryError('author must be a str - got %r' % type(author))
+
         from .repository import MercurialRepository
-        if not isinstance(message, unicode) or not isinstance(author, unicode):
-            raise RepositoryError('Given message and author needs to be '
-                                  'an <unicode> instance got %r & %r instead'
-                                  % (type(message), type(author)))
-
         if branch is None:
             branch = MercurialRepository.DEFAULT_BRANCH_NAME
-        kwargs['branch'] = branch
+        kwargs[b'branch'] = safe_bytes(branch)
 
-        def filectxfn(_repo, memctx, path):
+        def filectxfn(_repo, memctx, bytes_path):
             """
-            Marks given path as added/changed/removed in a given _repo. This is
-            for internal mercurial commit function.
+            Callback from Mercurial, returning ctx to commit for the given
+            path.
             """
+            path = safe_str(bytes_path)
 
             # check if this path is removed
             if path in (node.path for node in self.removed):
@@ -50,9 +54,8 @@
             # check if this path is added
             for node in self.added:
                 if node.path == path:
-                    return memfilectx(_repo, memctx, path=node.path,
-                        data=(node.content.encode('utf-8')
-                              if not node.is_binary else node.content),
+                    return mercurial.context.memfilectx(_repo, memctx, path=bytes_path,
+                        data=node.content,
                         islink=False,
                         isexec=node.is_executable,
                         copysource=False)
@@ -60,14 +63,13 @@
             # or changed
             for node in self.changed:
                 if node.path == path:
-                    return memfilectx(_repo, memctx, path=node.path,
-                        data=(node.content.encode('utf-8')
-                              if not node.is_binary else node.content),
+                    return mercurial.context.memfilectx(_repo, memctx, path=bytes_path,
+                        data=node.content,
                         islink=False,
                         isexec=node.is_executable,
                         copysource=False)
 
-            raise RepositoryError("Given path haven't been marked as added,"
+            raise RepositoryError("Given path haven't been marked as added, "
                                   "changed or removed (%s)" % path)
 
         parents = [None, None]
@@ -76,22 +78,21 @@
                 parents[i] = parent._ctx.node()
 
         if date and isinstance(date, datetime.datetime):
-            date = date.strftime('%a, %d %b %Y %H:%M:%S')
+            date = safe_bytes(date.strftime('%a, %d %b %Y %H:%M:%S'))
 
-        commit_ctx = memctx(repo=self.repository._repo,
+        commit_ctx = mercurial.context.memctx(
+            repo=self.repository._repo,
             parents=parents,
-            text='',
-            files=self.get_paths(),
+            text=b'',
+            files=[safe_bytes(x) for x in self.get_paths()],
             filectxfn=filectxfn,
-            user=author,
+            user=safe_bytes(author),
             date=date,
             extra=kwargs)
 
-        loc = lambda u: tolocal(u.encode('utf-8'))
-
         # injecting given _repo params
-        commit_ctx._text = loc(message)
-        commit_ctx._user = loc(author)
+        commit_ctx._text = safe_bytes(message)
+        commit_ctx._user = safe_bytes(author)
         commit_ctx._date = date
 
         # TODO: Catch exceptions!
@@ -100,9 +101,8 @@
         self._commit_ctx = commit_ctx  # For reference
         # Update vcs repository object & recreate mercurial _repo
         # new_ctx = self.repository._repo[node]
-        # new_tip = self.repository.get_changeset(new_ctx.hex())
-        new_id = hex(n)
-        self.repository.revisions.append(new_id)
+        # new_tip = ascii_str(self.repository.get_changeset(new_ctx.hex()))
+        self.repository.revisions.append(ascii_str(mercurial.node.hex(n)))
         self._repo = self.repository._get_repo(create=False)
         self.repository.branches = self.repository._get_branches()
         tip = self.repository.get_changeset()
--- a/kallithea/lib/vcs/backends/hg/repository.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/backends/hg/repository.py	Mon May 04 19:24:04 2020 +0200
@@ -13,16 +13,33 @@
 import logging
 import os
 import time
-import urllib
-import urllib2
+import urllib.error
+import urllib.parse
+import urllib.request
 from collections import OrderedDict
 
+import mercurial.commands
+import mercurial.error
+import mercurial.exchange
+import mercurial.hg
+import mercurial.hgweb
+import mercurial.httppeer
+import mercurial.localrepo
+import mercurial.match
+import mercurial.mdiff
+import mercurial.node
+import mercurial.patch
+import mercurial.scmutil
+import mercurial.sshpeer
+import mercurial.tags
+import mercurial.ui
+import mercurial.url
+import mercurial.util
+
 from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator
-from kallithea.lib.vcs.exceptions import (
-    BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, TagAlreadyExistError, TagDoesNotExistError, VCSError)
-from kallithea.lib.vcs.utils import author_email, author_name, date_fromtimestamp, makedate, safe_str, safe_unicode
-from kallithea.lib.vcs.utils.hgcompat import (
-    Abort, RepoError, RepoLookupError, clone, diffopts, get_contact, hex, hg_url, httpbasicauthhandler, httpdigestauthhandler, httppeer, localrepo, match_exact, nullid, patch, peer, scmutil, sshpeer, tag, ui)
+from kallithea.lib.vcs.exceptions import (BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
+                                          TagDoesNotExistError, VCSError)
+from kallithea.lib.vcs.utils import ascii_str, author_email, author_name, date_fromtimestamp, makedate, safe_bytes, safe_str
 from kallithea.lib.vcs.utils.lazy import LazyProperty
 from kallithea.lib.vcs.utils.paths import abspath
 
@@ -60,9 +77,8 @@
             raise VCSError('Mercurial backend requires repository path to '
                            'be instance of <str> got %s instead' %
                            type(repo_path))
-
         self.path = abspath(repo_path)
-        self.baseui = baseui or ui.ui()
+        self.baseui = baseui or mercurial.ui.ui()
         # We've set path and ui, now we can set _repo itself
         self._repo = self._get_repo(create, src_url, update_after_clone)
 
@@ -115,14 +131,13 @@
             return {}
 
         bt = OrderedDict()
-        for bn, _heads, tip, isclosed in sorted(self._repo.branchmap().iterbranches()):
+        for bn, _heads, node, isclosed in sorted(self._repo.branchmap().iterbranches()):
             if isclosed:
                 if closed:
-                    bt[safe_unicode(bn)] = hex(tip)
+                    bt[safe_str(bn)] = ascii_str(mercurial.node.hex(node))
             else:
                 if normal:
-                    bt[safe_unicode(bn)] = hex(tip)
-
+                    bt[safe_str(bn)] = ascii_str(mercurial.node.hex(node))
         return bt
 
     @LazyProperty
@@ -136,11 +151,11 @@
         if self._empty:
             return {}
 
-        sortkey = lambda ctx: ctx[0]  # sort by name
-        _tags = [(safe_unicode(n), hex(h),) for n, h in
-                 self._repo.tags().items()]
-
-        return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
+        return OrderedDict(sorted(
+            ((safe_str(n), ascii_str(mercurial.node.hex(h))) for n, h in self._repo.tags().items()),
+            reverse=True,
+            key=lambda x: x[0],  # sort by name
+        ))
 
     def tag(self, name, user, revision=None, message=None, date=None,
             **kwargs):
@@ -165,12 +180,12 @@
                 changeset.short_id)
 
         if date is None:
-            date = datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S')
+            date = safe_bytes(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S'))
 
         try:
-            tag(self._repo, name, changeset._ctx.node(), message, local, user, date)
-        except Abort as e:
-            raise RepositoryError(e.message)
+            mercurial.tags.tag(self._repo, safe_bytes(name), changeset._ctx.node(), safe_bytes(message), local, safe_bytes(user), date)
+        except mercurial.error.Abort as e:
+            raise RepositoryError(e.args[0])
 
         # Reinitialize tags
         self.tags = self._get_tags()
@@ -194,14 +209,14 @@
         if message is None:
             message = "Removed tag %s" % name
         if date is None:
-            date = datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S')
+            date = safe_bytes(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S'))
         local = False
 
         try:
-            tag(self._repo, name, nullid, message, local, user, date)
+            mercurial.tags.tag(self._repo, safe_bytes(name), mercurial.commands.nullid, safe_bytes(message), local, safe_bytes(user), date)
             self.tags = self._get_tags()
-        except Abort as e:
-            raise RepositoryError(e.message)
+        except mercurial.error.Abort as e:
+            raise RepositoryError(e.args[0])
 
     @LazyProperty
     def bookmarks(self):
@@ -214,14 +229,14 @@
         if self._empty:
             return {}
 
-        sortkey = lambda ctx: ctx[0]  # sort by name
-        _bookmarks = [(safe_unicode(n), hex(h),) for n, h in
-                 self._repo._bookmarks.items()]
-        return OrderedDict(sorted(_bookmarks, key=sortkey, reverse=True))
+        return OrderedDict(sorted(
+            ((safe_str(n), ascii_str(h)) for n, h in self._repo._bookmarks.items()),
+            reverse=True,
+            key=lambda x: x[0],  # sort by name
+        ))
 
     def _get_all_revisions(self):
-
-        return [self._repo[x].hex() for x in self._repo.filtered('visible').changelog.revs()]
+        return [ascii_str(self._repo[x].hex()) for x in self._repo.filtered(b'visible').changelog.revs()]
 
     def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
                   context=3):
@@ -257,12 +272,12 @@
             self.get_changeset(rev1)
         self.get_changeset(rev2)
         if path:
-            file_filter = match_exact(path)
+            file_filter = mercurial.match.exact(path)
         else:
             file_filter = None
 
-        return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter,
-                          opts=diffopts(git=True,
+        return b''.join(mercurial.patch.diff(self._repo, rev1, rev2, match=file_filter,
+                          opts=mercurial.mdiff.diffopts(git=True,
                                         showfunc=True,
                                         ignorews=ignore_whitespace,
                                         context=context)))
@@ -279,42 +294,46 @@
         when the return code is non 200
         """
         # check first if it's not an local url
-        if os.path.isdir(url) or url.startswith('file:'):
+        url = safe_bytes(url)
+        if os.path.isdir(url) or url.startswith(b'file:'):
             return True
 
-        if url.startswith('ssh:'):
+        if url.startswith(b'ssh:'):
             # in case of invalid uri or authentication issues, sshpeer will
             # throw an exception.
-            sshpeer.instance(repoui or ui.ui(), url, False).lookup('tip')
+            mercurial.sshpeer.instance(repoui or mercurial.ui.ui(), url, False).lookup(b'tip')
             return True
 
         url_prefix = None
-        if '+' in url[:url.find('://')]:
-            url_prefix, url = url.split('+', 1)
+        if b'+' in url[:url.find(b'://')]:
+            url_prefix, url = url.split(b'+', 1)
 
         handlers = []
-        url_obj = hg_url(url)
+        url_obj = mercurial.util.url(url)
         test_uri, authinfo = url_obj.authinfo()
-        url_obj.passwd = '*****'
+        url_obj.passwd = b'*****'
         cleaned_uri = str(url_obj)
 
         if authinfo:
             # create a password manager
-            passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
+            passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
             passmgr.add_password(*authinfo)
 
-            handlers.extend((httpbasicauthhandler(passmgr),
-                             httpdigestauthhandler(passmgr)))
+            handlers.extend((mercurial.url.httpbasicauthhandler(passmgr),
+                             mercurial.url.httpdigestauthhandler(passmgr)))
 
-        o = urllib2.build_opener(*handlers)
+        o = urllib.request.build_opener(*handlers)
         o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
                         ('Accept', 'application/mercurial-0.1')]
 
-        q = {"cmd": 'between'}
-        q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
-        qs = '?%s' % urllib.urlencode(q)
-        cu = "%s%s" % (test_uri, qs)
-        req = urllib2.Request(cu, None, {})
+        req = urllib.request.Request(
+            "%s?%s" % (
+                test_uri,
+                urllib.parse.urlencode({
+                    'cmd': 'between',
+                    'pairs': "%s-%s" % ('0' * 40, '0' * 40),
+                })
+            ))
 
         try:
             resp = o.open(req)
@@ -322,14 +341,14 @@
                 raise Exception('Return Code is not 200')
         except Exception as e:
             # means it cannot be cloned
-            raise urllib2.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
+            raise urllib.error.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
 
         if not url_prefix: # skip svn+http://... (and git+... too)
             # now check if it's a proper hg repo
             try:
-                httppeer.instance(repoui or ui.ui(), url, False).lookup('tip')
+                mercurial.httppeer.instance(repoui or mercurial.ui.ui(), url, False).lookup(b'tip')
             except Exception as e:
-                raise urllib2.URLError(
+                raise urllib.error.URLError(
                     "url [%s] does not look like an hg repo org_exc: %s"
                     % (cleaned_uri, e))
 
@@ -345,26 +364,25 @@
         location at given clone_point. Additionally it'll make update to
         working copy accordingly to ``update_after_clone`` flag
         """
-
         try:
             if src_url:
-                url = safe_str(self._get_url(src_url))
+                url = safe_bytes(self._get_url(src_url))
                 opts = {}
                 if not update_after_clone:
                     opts.update({'noupdate': True})
                 MercurialRepository._check_url(url, self.baseui)
-                clone(self.baseui, url, self.path, **opts)
+                mercurial.commands.clone(self.baseui, url, safe_bytes(self.path), **opts)
 
                 # Don't try to create if we've already cloned repo
                 create = False
-            return localrepo.instance(self.baseui, self.path, create=create)
-        except (Abort, RepoError) as err:
+            return mercurial.localrepo.instance(self.baseui, safe_bytes(self.path), create=create)
+        except (mercurial.error.Abort, mercurial.error.RepoError) as err:
             if create:
                 msg = "Cannot create repository at %s. Original error was %s" \
-                    % (self.path, err)
+                    % (self.name, err)
             else:
                 msg = "Not valid repository at %s. Original error was %s" \
-                    % (self.path, err)
+                    % (self.name, err)
             raise RepositoryError(msg)
 
     @LazyProperty
@@ -373,15 +391,13 @@
 
     @LazyProperty
     def description(self):
-        undefined_description = u'unknown'
-        _desc = self._repo.ui.config('web', 'description', None, untrusted=True)
-        return safe_unicode(_desc or undefined_description)
+        _desc = self._repo.ui.config(b'web', b'description', None, untrusted=True)
+        return safe_str(_desc or b'unknown')
 
     @LazyProperty
     def contact(self):
-        undefined_contact = u'Unknown'
-        return safe_unicode(get_contact(self._repo.ui.config)
-                            or undefined_contact)
+        return safe_str(mercurial.hgweb.common.get_contact(self._repo.ui.config)
+                            or b'Unknown')
 
     @LazyProperty
     def last_change(self):
@@ -404,39 +420,33 @@
 
     def _get_revision(self, revision):
         """
-        Gets an ID revision given as str. This will always return a full
-        40 char revision number
+        Given any revision identifier, returns a 40 char string with revision hash.
 
         :param revision: str or int or None
         """
-        if isinstance(revision, unicode):
-            revision = safe_str(revision)
-
         if self._empty:
             raise EmptyRepositoryError("There are no changesets yet")
 
         if revision in [-1, None]:
-            revision = 'tip'
+            revision = b'tip'
+        elif isinstance(revision, str):
+            revision = safe_bytes(revision)
 
         try:
             if isinstance(revision, int):
-                return self._repo[revision].hex()
-            try:
-                return scmutil.revsymbol(self._repo, revision).hex()
-            except AttributeError: # revsymbol was introduced in Mercurial 4.6
-                return self._repo[revision].hex()
-        except (IndexError, ValueError, RepoLookupError, TypeError):
-            msg = ("Revision %s does not exist for %s" % (revision, self))
+                return ascii_str(self._repo[revision].hex())
+            return ascii_str(mercurial.scmutil.revsymbol(self._repo, revision).hex())
+        except (IndexError, ValueError, mercurial.error.RepoLookupError, TypeError):
+            msg = "Revision %r does not exist for %s" % (safe_str(revision), self.name)
             raise ChangesetDoesNotExistError(msg)
         except (LookupError, ):
-            msg = ("Ambiguous identifier `%s` for %s" % (revision, self))
+            msg = "Ambiguous identifier `%s` for %s" % (safe_str(revision), self.name)
             raise ChangesetDoesNotExistError(msg)
 
     def get_ref_revision(self, ref_type, ref_name):
         """
         Returns revision number for the given reference.
         """
-        ref_name = safe_str(ref_name)
         if ref_type == 'rev' and not ref_name.strip('0'):
             return self.EMPTY_CHANGESET
         # lookup up the exact node id
@@ -451,17 +461,13 @@
         try:
             revs = self._repo.revs(rev_spec, ref_name, ref_name)
         except LookupError:
-            msg = ("Ambiguous identifier %s:%s for %s" % (ref_type, ref_name, self.name))
+            msg = "Ambiguous identifier %s:%s for %s" % (ref_type, ref_name, self.name)
             raise ChangesetDoesNotExistError(msg)
-        except RepoLookupError:
-            msg = ("Revision %s:%s does not exist for %s" % (ref_type, ref_name, self.name))
+        except mercurial.error.RepoLookupError:
+            msg = "Revision %s:%s does not exist for %s" % (ref_type, ref_name, self.name)
             raise ChangesetDoesNotExistError(msg)
         if revs:
-            try:
-                revision = revs.last()
-            except AttributeError:
-                # removed in hg 3.2
-                revision = revs[-1]
+            revision = revs.last()
         else:
             # TODO: just report 'not found'?
             revision = ref_name
@@ -469,39 +475,29 @@
         return self._get_revision(revision)
 
     def _get_archives(self, archive_name='tip'):
-        allowed = self.baseui.configlist("web", "allow_archive",
+        allowed = self.baseui.configlist(b"web", b"allow_archive",
                                          untrusted=True)
-        for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
-            if i[0] in allowed or self._repo.ui.configbool("web",
-                                                           "allow" + i[0],
+        for name, ext in [(b'zip', '.zip'), (b'gz', '.tar.gz'), (b'bz2', '.tar.bz2')]:
+            if name in allowed or self._repo.ui.configbool(b"web",
+                                                           b"allow" + name,
                                                            untrusted=True):
-                yield {"type": i[0], "extension": i[1], "node": archive_name}
+                yield {"type": safe_str(name), "extension": ext, "node": archive_name}
 
     def _get_url(self, url):
         """
-        Returns normalized url. If schema is not given, would fall
-        to filesystem
-        (``file:///``) schema.
+        Returns normalized url. If schema is not given, fall back to
+        filesystem (``file:///``) schema.
         """
-        url = safe_str(url)
         if url != 'default' and '://' not in url:
-            url = "file:" + urllib.pathname2url(url)
+            url = "file:" + urllib.request.pathname2url(url)
         return url
 
-    def get_hook_location(self):
-        """
-        returns absolute path to location where hooks are stored
-        """
-        return os.path.join(self.path, '.hg', '.hgrc')
-
     def get_changeset(self, revision=None):
         """
         Returns ``MercurialChangeset`` object representing repository's
         changeset at the given ``revision``.
         """
-        revision = self._get_revision(revision)
-        changeset = MercurialChangeset(repository=self, revision=revision)
-        return changeset
+        return MercurialChangeset(repository=self, revision=self._get_revision(revision))
 
     def get_changesets(self, start=None, end=None, start_date=None,
                        end_date=None, branch_name=None, reverse=False, max_revisions=None):
@@ -517,35 +513,35 @@
         :param reversed: return changesets in reversed order
         """
         start_raw_id = self._get_revision(start)
-        start_pos = self.revisions.index(start_raw_id) if start else None
+        start_pos = None if start is None else self.revisions.index(start_raw_id)
         end_raw_id = self._get_revision(end)
-        end_pos = self.revisions.index(end_raw_id) if end else None
+        end_pos = None if end is None else self.revisions.index(end_raw_id)
 
-        if None not in [start, end] and start_pos > end_pos:
+        if start_pos is not None and end_pos is not None and start_pos > end_pos:
             raise RepositoryError("Start revision '%s' cannot be "
                                   "after end revision '%s'" % (start, end))
 
-        if branch_name and branch_name not in self.allbranches.keys():
-            msg = ("Branch %s not found in %s" % (branch_name, self))
+        if branch_name and branch_name not in self.allbranches:
+            msg = "Branch %r not found in %s" % (branch_name, self.name)
             raise BranchDoesNotExistError(msg)
         if end_pos is not None:
             end_pos += 1
         # filter branches
         filter_ = []
         if branch_name:
-            filter_.append('branch("%s")' % safe_str(branch_name))
+            filter_.append(b'branch("%s")' % safe_bytes(branch_name))
         if start_date:
-            filter_.append('date(">%s")' % start_date)
+            filter_.append(b'date(">%s")' % safe_bytes(str(start_date)))
         if end_date:
-            filter_.append('date("<%s")' % end_date)
+            filter_.append(b'date("<%s")' % safe_bytes(str(end_date)))
         if filter_ or max_revisions:
             if filter_:
-                revspec = ' and '.join(filter_)
+                revspec = b' and '.join(filter_)
             else:
-                revspec = 'all()'
+                revspec = b'all()'
             if max_revisions:
-                revspec = 'limit(%s, %s)' % (revspec, max_revisions)
-            revisions = scmutil.revrange(self._repo, [revspec])
+                revspec = b'limit(%s, %d)' % (revspec, max_revisions)
+            revisions = mercurial.scmutil.revrange(self._repo, [revspec])
         else:
             revisions = self.revisions
 
@@ -553,7 +549,7 @@
         # would be to get rid of this function entirely and use revsets
         revs = list(revisions)[start_pos:end_pos]
         if reverse:
-            revs = reversed(revs)
+            revs.reverse()
 
         return CollectionGenerator(self, revs)
 
@@ -561,15 +557,10 @@
         """
         Tries to pull changes from external location.
         """
-        url = self._get_url(url)
-        other = peer(self._repo, {}, url)
+        other = mercurial.hg.peer(self._repo, {}, safe_bytes(self._get_url(url)))
         try:
-            # hg 3.2 moved push / pull to exchange module
-            from mercurial import exchange
-            exchange.pull(self._repo, other, heads=None, force=None)
-        except ImportError:
-            self._repo.pull(other, heads=None, force=None)
-        except Abort as err:
+            mercurial.exchange.pull(self._repo, other, heads=None, force=None)
+        except mercurial.error.Abort as err:
             # Propagate error but with vcs's type
             raise RepositoryError(str(err))
 
@@ -591,15 +582,16 @@
         """
         if config_file is None:
             config_file = []
-        elif isinstance(config_file, basestring):
+        elif isinstance(config_file, str):
             config_file = [config_file]
 
         config = self._repo.ui
         if config_file:
-            config = ui.ui()
+            config = mercurial.ui.ui()
             for path in config_file:
-                config.readconfig(path)
-        return config.config(section, name)
+                config.readconfig(safe_bytes(path))
+        value = config.config(safe_bytes(section), safe_bytes(name))
+        return value if value is None else safe_str(value)
 
     def get_user_name(self, config_file=None):
         """
--- a/kallithea/lib/vcs/backends/hg/ssh.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/backends/hg/ssh.py	Mon May 04 19:24:04 2020 +0200
@@ -14,18 +14,12 @@
 
 import logging
 
-from mercurial import hg
+import mercurial.hg
+import mercurial.wireprotoserver
 
 from kallithea.lib.utils import make_ui
-from kallithea.lib.utils2 import safe_str, safe_unicode
 from kallithea.lib.vcs.backends.ssh import BaseSshHandler
-
-
-try:
-    from mercurial.wireprotoserver import sshserver
-except ImportError:
-    from mercurial.sshserver import sshserver # moved in Mercurial 4.6 (1bf5263fe5cc)
-
+from kallithea.lib.vcs.utils import safe_bytes
 
 
 log = logging.getLogger(__name__)
@@ -40,11 +34,11 @@
         >>> import shlex
 
         >>> MercurialSshHandler.make(shlex.split('hg -R "foo bar" serve --stdio')).repo_name
-        u'foo bar'
+        'foo bar'
         >>> MercurialSshHandler.make(shlex.split(' hg -R blåbærgrød serve --stdio ')).repo_name
-        u'bl\xe5b\xe6rgr\xf8d'
+        'bl\xe5b\xe6rgr\xf8d'
         >>> MercurialSshHandler.make(shlex.split('''hg -R 'foo"bar' serve --stdio''')).repo_name
-        u'foo"bar'
+        'foo"bar'
 
         >>> MercurialSshHandler.make(shlex.split('/bin/hg -R "foo" serve --stdio'))
         >>> MercurialSshHandler.make(shlex.split('''hg -R "foo"bar" serve --stdio''')) # ssh-serve will report: Error parsing SSH command "...": invalid syntax
@@ -53,20 +47,17 @@
         >>> MercurialSshHandler.make(shlex.split('git-upload-pack "/foo"')) # not handled here
         """
         if ssh_command_parts[:2] == ['hg', '-R'] and ssh_command_parts[3:] == ['serve', '--stdio']:
-            return cls(safe_unicode(ssh_command_parts[2]))
+            return cls(ssh_command_parts[2])
 
         return None
 
-    def __init__(self, repo_name):
-        self.repo_name = repo_name
-
     def _serve(self):
         # Note: we want a repo with config based on .hg/hgrc and can thus not use self.db_repo.scm_instance._repo.ui
         baseui = make_ui(repo_path=self.db_repo.repo_full_path)
         if not self.allow_push:
-            baseui.setconfig('hooks', 'pretxnopen._ssh_reject', 'python:kallithea.lib.hooks.rejectpush')
-            baseui.setconfig('hooks', 'prepushkey._ssh_reject', 'python:kallithea.lib.hooks.rejectpush')
+            baseui.setconfig(b'hooks', b'pretxnopen._ssh_reject', b'python:kallithea.lib.hooks.rejectpush')
+            baseui.setconfig(b'hooks', b'prepushkey._ssh_reject', b'python:kallithea.lib.hooks.rejectpush')
 
-        repo = hg.repository(baseui, safe_str(self.db_repo.repo_full_path))
+        repo = mercurial.hg.repository(baseui, safe_bytes(self.db_repo.repo_full_path))
         log.debug("Starting Mercurial sshserver for %s", self.db_repo.repo_full_path)
-        sshserver(baseui, repo).serve_forever()
+        mercurial.wireprotoserver.sshserver(baseui, repo).serve_forever()
--- a/kallithea/lib/vcs/backends/hg/workdir.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/backends/hg/workdir.py	Mon May 04 19:24:04 2020 +0200
@@ -1,15 +1,17 @@
+import mercurial.merge
+
 from kallithea.lib.vcs.backends.base import BaseWorkdir
 from kallithea.lib.vcs.exceptions import BranchDoesNotExistError
-from kallithea.lib.vcs.utils.hgcompat import hg_merge
+from kallithea.lib.vcs.utils import ascii_bytes, ascii_str, safe_str
 
 
 class MercurialWorkdir(BaseWorkdir):
 
     def get_branch(self):
-        return self.repository._repo.dirstate.branch()
+        return safe_str(self.repository._repo.dirstate.branch())
 
     def get_changeset(self):
-        wk_dir_id = self.repository._repo[None].parents()[0].hex()
+        wk_dir_id = ascii_str(self.repository._repo[None].parents()[0].hex())
         return self.repository.get_changeset(wk_dir_id)
 
     def checkout_branch(self, branch=None):
@@ -19,4 +21,4 @@
             raise BranchDoesNotExistError
 
         raw_id = self.repository.branches[branch]
-        hg_merge.update(self.repository._repo, raw_id, False, False, None)
+        mercurial.merge.update(self.repository._repo, ascii_bytes(raw_id), False, False, None)
--- a/kallithea/lib/vcs/backends/ssh.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/backends/ssh.py	Mon May 04 19:24:04 2020 +0200
@@ -24,7 +24,7 @@
 import sys
 
 from kallithea.lib.auth import AuthUser, HasPermissionAnyMiddleware
-from kallithea.lib.utils2 import safe_str, set_hook_environment
+from kallithea.lib.utils2 import set_hook_environment
 from kallithea.model.db import Repository, User, UserSshKeys
 from kallithea.model.meta import Session
 
@@ -55,6 +55,9 @@
         """
         raise NotImplementedError
 
+    def __init__(self, repo_name):
+        self.repo_name = repo_name.rstrip('/')
+
     def serve(self, user_id, key_id, client_ip):
         """Verify basic sanity of the repository, and that the user is
         valid and has access - then serve the native VCS protocol for
@@ -79,7 +82,7 @@
         elif HasPermissionAnyMiddleware('repository.read')(self.authuser, self.repo_name):
             self.allow_push = False
         else:
-            self.exit('Access to %r denied' % safe_str(self.repo_name))
+            self.exit('Access to %r denied' % self.repo_name)
 
         self.db_repo = Repository.get_by_repo_name(self.repo_name)
         if self.db_repo is None:
--- a/kallithea/lib/vcs/conf/settings.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/conf/settings.py	Mon May 04 19:24:04 2020 +0200
@@ -1,24 +1,7 @@
-import os
-import tempfile
-
 from kallithea.lib.vcs.utils import aslist
-from kallithea.lib.vcs.utils.paths import get_user_home
 
 
-abspath = lambda * p: os.path.abspath(os.path.join(*p))
-
-VCSRC_PATH = os.environ.get('VCSRC_PATH')
-
-if not VCSRC_PATH:
-    HOME_ = get_user_home()
-    if not HOME_:
-        HOME_ = tempfile.gettempdir()
-
-VCSRC_PATH = VCSRC_PATH or abspath(HOME_, '.vcsrc')
-if os.path.isdir(VCSRC_PATH):
-    VCSRC_PATH = os.path.join(VCSRC_PATH, '__init__.py')
-
-# list of default encoding used in safe_unicode/safe_str methods
+# list of default encoding used in safe_str/safe_bytes methods
 DEFAULT_ENCODINGS = aslist('utf-8')
 
 # path to git executable run by run_git_command function
--- a/kallithea/lib/vcs/exceptions.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/exceptions.py	Mon May 04 19:24:04 2020 +0200
@@ -30,10 +30,6 @@
     pass
 
 
-class BranchAlreadyExistError(RepositoryError):
-    pass
-
-
 class BranchDoesNotExistError(RepositoryError):
     pass
 
@@ -50,10 +46,6 @@
     pass
 
 
-class NothingChangedError(CommitError):
-    pass
-
-
 class NodeError(VCSError):
     pass
 
@@ -88,7 +80,3 @@
 
 class ImproperArchiveTypeError(VCSError):
     pass
-
-
-class CommandError(VCSError):
-    pass
--- a/kallithea/lib/vcs/nodes.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/nodes.py	Mon May 04 19:24:04 2020 +0200
@@ -9,13 +9,14 @@
     :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
 """
 
+import functools
 import mimetypes
 import posixpath
 import stat
 
 from kallithea.lib.vcs.backends.base import EmptyChangeset
 from kallithea.lib.vcs.exceptions import NodeError, RemovedFileNodeError
-from kallithea.lib.vcs.utils import safe_str, safe_unicode
+from kallithea.lib.vcs.utils import safe_bytes, safe_str
 from kallithea.lib.vcs.utils.lazy import LazyProperty
 
 
@@ -26,10 +27,10 @@
 
 
 class NodeState:
-    ADDED = u'added'
-    CHANGED = u'changed'
-    NOT_CHANGED = u'not changed'
-    REMOVED = u'removed'
+    ADDED = 'added'
+    CHANGED = 'changed'
+    NOT_CHANGED = 'not changed'
+    REMOVED = 'removed'
 
 
 class NodeGeneratorBase(object):
@@ -44,11 +45,9 @@
         self.cs = cs
         self.current_paths = current_paths
 
-    def __call__(self):
-        return [n for n in self]
-
-    def __getslice__(self, i, j):
-        for p in self.current_paths[i:j]:
+    def __getitem__(self, key):
+        assert isinstance(key, slice), key
+        for p in self.current_paths[key]:
             yield self.cs.get_node(p)
 
     def __len__(self):
@@ -81,11 +80,13 @@
         for p in self.current_paths:
             yield RemovedFileNode(path=p)
 
-    def __getslice__(self, i, j):
-        for p in self.current_paths[i:j]:
+    def __getitem__(self, key):
+        assert isinstance(key, slice), key
+        for p in self.current_paths[key]:
             yield RemovedFileNode(path=p)
 
 
+@functools.total_ordering
 class Node(object):
     """
     Simplest class representing file or directory on repository.  SCM backends
@@ -101,7 +102,7 @@
         if path.startswith('/'):
             raise NodeError("Cannot initialize Node objects with slash at "
                             "the beginning as only relative paths are supported")
-        self.path = safe_str(path.rstrip('/'))  # we store paths as str
+        self.path = path.rstrip('/')
         if path == '' and kind != NodeKind.DIR:
             raise NodeError("Only DirNode and its subclasses may be "
                             "initialized with empty path")
@@ -120,67 +121,34 @@
         return None
 
     @LazyProperty
-    def unicode_path(self):
-        return safe_unicode(self.path)
-
-    @LazyProperty
     def name(self):
         """
         Returns name of the node so if its path
         then only last part is returned.
         """
-        return safe_unicode(self.path.rstrip('/').split('/')[-1])
-
-    def _get_kind(self):
-        return self._kind
-
-    def _set_kind(self, kind):
-        if hasattr(self, '_kind'):
-            raise NodeError("Cannot change node's kind")
-        else:
-            self._kind = kind
-            # Post setter check (path's trailing slash)
-            if self.path.endswith('/'):
-                raise NodeError("Node's path cannot end with slash")
-
-    kind = property(_get_kind, _set_kind)
-
-    def __cmp__(self, other):
-        """
-        Comparator using name of the node, needed for quick list sorting.
-        """
-        kind_cmp = cmp(self.kind, other.kind)
-        if kind_cmp:
-            return kind_cmp
-        return cmp(self.name, other.name)
+        return self.path.rstrip('/').split('/')[-1]
 
     def __eq__(self, other):
-        for attr in ['name', 'path', 'kind']:
-            if getattr(self, attr) != getattr(other, attr):
-                return False
-        if self.is_file():
-            if self.content != other.content:
-                return False
-        else:
-            # For DirNode's check without entering each dir
-            self_nodes_paths = list(sorted(n.path for n in self.nodes))
-            other_nodes_paths = list(sorted(n.path for n in self.nodes))
-            if self_nodes_paths != other_nodes_paths:
-                return False
-        return True
+        if type(self) is not type(other):
+            return False
+        if self.kind != other.kind:
+            return False
+        if self.path != other.path:
+            return False
 
-    def __nq__(self, other):
-        return not self.__eq__(other)
+    def __lt__(self, other):
+        if self.kind < other.kind:
+            return True
+        if self.kind > other.kind:
+            return False
+        if self.path < other.path:
+            return True
+        if self.path > other.path:
+            return False
 
     def __repr__(self):
         return '<%s %r>' % (self.__class__.__name__, self.path)
 
-    def __str__(self):
-        return self.__repr__()
-
-    def __unicode__(self):
-        return self.name
-
     def get_parent_path(self):
         """
         Returns node's parent path or empty string if node is root.
@@ -258,8 +226,24 @@
             raise NodeError("Cannot use both content and changeset")
         super(FileNode, self).__init__(path, kind=NodeKind.FILE)
         self.changeset = changeset
+        if not isinstance(content, bytes) and content is not None:
+            # File content is one thing that inherently must be bytes ... but
+            # VCS module tries to be "user friendly" and support unicode ...
+            content = safe_bytes(content)
         self._content = content
-        self._mode = mode or 0100644
+        self._mode = mode or 0o100644
+
+    def __eq__(self, other):
+        eq = super(FileNode, self).__eq__(other)
+        if eq is not None:
+            return eq
+        return self.content == other.content
+
+    def __lt__(self, other):
+        lt = super(FileNode, self).__lt__(other)
+        if lt is not None:
+            return lt
+        return self.content < other.content
 
     @LazyProperty
     def mode(self):
@@ -273,25 +257,17 @@
             mode = self._mode
         return mode
 
-    def _get_content(self):
+    @property
+    def content(self):
+        """
+        Returns lazily byte content of the FileNode.
+        """
         if self.changeset:
             content = self.changeset.get_file_content(self.path)
         else:
             content = self._content
         return content
 
-    @property
-    def content(self):
-        """
-        Returns lazily content of the FileNode. If possible, would try to
-        decode content from UTF-8.
-        """
-        content = self._get_content()
-
-        if bool(content and '\0' in content):
-            return content
-        return safe_unicode(content)
-
     @LazyProperty
     def size(self):
         if self.changeset:
@@ -329,8 +305,8 @@
                 encoding = None
 
                 # try with pygments
+                from pygments import lexers
                 try:
-                    from pygments import lexers
                     mt = lexers.get_lexer_for_filename(self.name).mimetypes
                 except lexers.ClassNotFound:
                     mt = None
@@ -361,7 +337,7 @@
         """
         from pygments import lexers
         try:
-            lexer = lexers.guess_lexer_for_filename(self.name, self.content, stripnl=False)
+            lexer = lexers.guess_lexer_for_filename(self.name, safe_str(self.content), stripnl=False)
         except lexers.ClassNotFound:
             lexer = lexers.TextLexer(stripnl=False)
         # returns first alias
@@ -409,8 +385,7 @@
         """
         Returns True if file has binary content.
         """
-        _bin = '\0' in self._get_content()
-        return _bin
+        return b'\0' in self.content
 
     def is_browser_compatible_image(self):
         return self.mimetype in [
@@ -488,10 +463,23 @@
         self.changeset = changeset
         self._nodes = nodes
 
-    @LazyProperty
-    def content(self):
-        raise NodeError("%s represents a dir and has no ``content`` attribute"
-            % self)
+    def __eq__(self, other):
+        eq = super(DirNode, self).__eq__(other)
+        if eq is not None:
+            return eq
+        # check without entering each dir
+        self_nodes_paths = list(sorted(n.path for n in self.nodes))
+        other_nodes_paths = list(sorted(n.path for n in self.nodes))
+        return self_nodes_paths == other_nodes_paths
+
+    def __lt__(self, other):
+        lt = super(DirNode, self).__lt__(other)
+        if lt is not None:
+            return lt
+        # check without entering each dir
+        self_nodes_paths = list(sorted(n.path for n in self.nodes))
+        other_nodes_paths = list(sorted(n.path for n in self.nodes))
+        return self_nodes_paths < other_nodes_paths
 
     @LazyProperty
     def nodes(self):
@@ -595,12 +583,13 @@
     size = 0
 
     def __init__(self, name, url, changeset=None, alias=None):
-        self.path = name
+        # Note: Doesn't call Node.__init__!
+        self.path = name.rstrip('/')
         self.kind = NodeKind.SUBMODULE
         self.alias = alias
         # we have to use emptyChangeset here since this can point to svn/git/hg
         # submodules we cannot get from repository
-        self.changeset = EmptyChangeset(str(changeset), alias=alias)
+        self.changeset = EmptyChangeset(changeset, alias=alias)
         self.url = url
 
     def __repr__(self):
@@ -613,5 +602,5 @@
         Returns name of the node so if its path
         then only last part is returned.
         """
-        org = safe_unicode(self.path.rstrip('/').split('/')[-1])
-        return u'%s @ %s' % (org, self.changeset.short_id)
+        org = self.path.rstrip('/').rsplit('/', 1)[-1]
+        return '%s @ %s' % (org, self.changeset.short_id)
--- a/kallithea/lib/vcs/subprocessio.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/subprocessio.py	Mon May 04 19:24:04 2020 +0200
@@ -44,7 +44,7 @@
         if type(source) in (type(''), bytes, bytearray):  # string-like
             self.bytes = bytes(source)
         else:  # can be either file pointer or file-like
-            if type(source) in (int, long):  # file pointer it is
+            if isinstance(source, int):  # file pointer it is
                 # converting file descriptor (int) stdin into file-like
                 source = os.fdopen(source, 'rb', 16384)
             # let's see if source is file-like by now
@@ -125,11 +125,7 @@
             if len(t) > ccm:
                 kr.clear()
                 kr.wait(2)
-                # # this only works on 2.7.x and up
-                # if not kr.wait(10):
-                #     raise Exception("Timed out while waiting for input to be read.")
-                # instead we'll use this
-                if len(t) > ccm + 3:
+                if not kr.wait(10):
                     raise IOError(
                         "Timed out while waiting for input from subprocess.")
             t.append(b)
@@ -178,7 +174,7 @@
     def __iter__(self):
         return self
 
-    def next(self):
+    def __next__(self):
         while not len(self.data) and not self.worker.EOF.is_set():
             self.worker.data_added.clear()
             self.worker.data_added.wait(0.2)
@@ -225,17 +221,6 @@
         return not self.worker.keep_reading.is_set()
 
     @property
-    def done_reading_event(self):
-        """
-        Done_reading does not mean that the iterator's buffer is empty.
-        Iterator might have done reading from underlying source, but the read
-        chunks might still be available for serving through .next() method.
-
-        :returns: An threading.Event class instance.
-        """
-        return self.worker.EOF
-
-    @property
     def done_reading(self):
         """
         Done_reading does not mean that the iterator's buffer is empty.
@@ -286,7 +271,7 @@
 
     - We are multithreaded. Writing in and reading out, err are all sep threads.
     - We support concurrent (in and out) stream processing.
-    - The output is not a stream. It's a queue of read string (bytes, not unicode)
+    - The output is not a stream. It's a queue of read string (bytes, not str)
       chunks. The object behaves as an iterable. You can "for chunk in obj:" us.
     - We are non-blocking in more respects than communicate()
       (reading from subprocess out pauses when internal buffer is full, but
@@ -367,18 +352,17 @@
             and returncode != 0
         ): # and it failed
             bg_out.stop()
-            out = ''.join(bg_out)
+            out = b''.join(bg_out)
             bg_err.stop()
-            err = ''.join(bg_err)
-            if (err.strip() == 'fatal: The remote end hung up unexpectedly' and
-                out.startswith('0034shallow ')
+            err = b''.join(bg_err)
+            if (err.strip() == b'fatal: The remote end hung up unexpectedly' and
+                out.startswith(b'0034shallow ')
             ):
                 # hack inspired by https://github.com/schacon/grack/pull/7
                 bg_out = iter([out])
                 _p = None
             elif err:
-                raise EnvironmentError(
-                    "Subprocess exited due to an error:\n" + err)
+                raise EnvironmentError("Subprocess exited due to an error: %s" % err)
             else:
                 raise EnvironmentError(
                     "Subprocess exited with non 0 ret code: %s" % returncode)
@@ -390,7 +374,7 @@
     def __iter__(self):
         return self
 
-    def next(self):
+    def __next__(self):
         if self.process:
             returncode = self.process.poll()
             if (returncode is not None # process has terminated
@@ -400,7 +384,7 @@
                 self.error.stop()
                 err = ''.join(self.error)
                 raise EnvironmentError("Subprocess exited due to an error:\n" + err)
-        return self.output.next()
+        return next(self.output)
 
     def throw(self, type, value=None, traceback=None):
         if self.output.length or not self.output.done_reading:
--- a/kallithea/lib/vcs/utils/__init__.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/utils/__init__.py	Mon May 04 19:24:04 2020 +0200
@@ -1,3 +1,5 @@
+# -*- coding: utf-8 -*-
+
 """
 This module provides some useful tools for ``vcs`` like annotate/diff html
 output. It also includes some internal helpers.
@@ -25,7 +27,7 @@
     :param sep:
     :param strip:
     """
-    if isinstance(obj, (basestring)):
+    if isinstance(obj, str):
         lst = obj.split(sep)
         if strip:
             lst = [v.strip() for v in lst]
@@ -66,89 +68,107 @@
     return val
 
 
-def safe_unicode(str_, from_encoding=None):
+def safe_str(s):
     """
-    safe unicode function. Does few trick to turn str_ into unicode
-
-    In case of UnicodeDecode error we try to return it with encoding detected
-    by chardet library if it fails fallback to unicode with errors replaced
-
-    :param str_: string to decode
-    :rtype: unicode
-    :returns: unicode object
+    Safe unicode str function. Use a few tricks to turn s into str:
+    In case of UnicodeDecodeError with configured default encodings, try to
+    detect encoding with chardet library, then fall back to first encoding with
+    errors replaced.
     """
-    if isinstance(str_, unicode):
-        return str_
+    if isinstance(s, str):
+        return s
 
-    if not from_encoding:
-        from kallithea.lib.vcs.conf import settings
-        from_encoding = settings.DEFAULT_ENCODINGS
-
-    if not isinstance(from_encoding, (list, tuple)):
-        from_encoding = [from_encoding]
+    if not isinstance(s, bytes):  # use __str__ and don't expect UnicodeDecodeError
+        return str(s)
 
-    try:
-        return unicode(str_)
-    except UnicodeDecodeError:
-        pass
-
-    for enc in from_encoding:
+    from kallithea.lib.vcs.conf import settings
+    for enc in settings.DEFAULT_ENCODINGS:
         try:
-            return unicode(str_, enc)
+            return str(s, enc)
         except UnicodeDecodeError:
             pass
 
     try:
         import chardet
-        encoding = chardet.detect(str_)['encoding']
-        if encoding is None:
-            raise Exception()
-        return str_.decode(encoding)
-    except (ImportError, UnicodeDecodeError, Exception):
-        return unicode(str_, from_encoding[0], 'replace')
+        encoding = chardet.detect(s)['encoding']
+        if encoding is not None:
+            return s.decode(encoding)
+    except (ImportError, UnicodeDecodeError):
+        pass
+
+    return str(s, settings.DEFAULT_ENCODINGS[0], 'replace')
 
 
-def safe_str(unicode_, to_encoding=None):
+def safe_bytes(s):
     """
-    safe str function. Does few trick to turn unicode_ into string
-
-    In case of UnicodeEncodeError we try to return it with encoding detected
-    by chardet library if it fails fallback to string with errors replaced
-
-    :param unicode_: unicode to encode
-    :rtype: str
-    :returns: str object
+    Safe bytes function. Use a few tricks to turn s into bytes string:
+    In case of UnicodeEncodeError with configured default encodings, fall back
+    to first configured encoding with errors replaced.
     """
+    if isinstance(s, bytes):
+        return s
 
-    # if it's not basestr cast to str
-    if not isinstance(unicode_, basestring):
-        return str(unicode_)
-
-    if isinstance(unicode_, str):
-        return unicode_
+    assert isinstance(s, str), repr(s)  # bytes cannot coerse with __str__ or handle None or int
 
-    if not to_encoding:
-        from kallithea.lib.vcs.conf import settings
-        to_encoding = settings.DEFAULT_ENCODINGS
-
-    if not isinstance(to_encoding, (list, tuple)):
-        to_encoding = [to_encoding]
-
-    for enc in to_encoding:
+    from kallithea.lib.vcs.conf import settings
+    for enc in settings.DEFAULT_ENCODINGS:
         try:
-            return unicode_.encode(enc)
+            return s.encode(enc)
         except UnicodeEncodeError:
             pass
 
-    try:
-        import chardet
-        encoding = chardet.detect(unicode_)['encoding']
-        if encoding is None:
-            raise UnicodeEncodeError()
+    return s.encode(settings.DEFAULT_ENCODINGS[0], 'replace')
+
+
+def ascii_bytes(s):
+    """
+    Simple conversion from str to bytes, *assuming* all codepoints are
+    7-bit and it thus is pure ASCII.
+    Will fail badly with UnicodeError on invalid input.
+    This should be used where enocding and "safe" ambiguity should be avoided.
+    Where strings already have been encoded in other ways but still are unicode
+    string - for example to hex, base64, json, urlencoding, or are known to be
+    identifiers.
 
-        return unicode_.encode(encoding)
-    except (ImportError, UnicodeEncodeError):
-        return unicode_.encode(to_encoding[0], 'replace')
+    >>> ascii_bytes('a')
+    b'a'
+    >>> ascii_bytes(u'a')
+    b'a'
+    >>> ascii_bytes('å')
+    Traceback (most recent call last):
+    UnicodeEncodeError: 'ascii' codec can't encode character '\xe5' in position 0: ordinal not in range(128)
+    >>> ascii_bytes('å'.encode('utf8'))
+    Traceback (most recent call last):
+    AssertionError: b'\xc3\xa5'
+    """
+    assert isinstance(s, str), repr(s)
+    return s.encode('ascii')
+
+
+def ascii_str(s):
+    r"""
+    Simple conversion from bytes to str, *assuming* all codepoints are
+    7-bit and it thus is pure ASCII.
+    Will fail badly with UnicodeError on invalid input.
+    This should be used where enocding and "safe" ambiguity should be avoided.
+    Where strings are encoded but also in other ways are known to be ASCII, and
+    where a unicode string is wanted without caring about encoding. For example
+    to hex, base64, urlencoding, or are known to be identifiers.
+
+    >>> ascii_str(b'a')
+    'a'
+    >>> ascii_str(u'a')
+    Traceback (most recent call last):
+    AssertionError: 'a'
+    >>> ascii_str('å'.encode('utf8'))
+    Traceback (most recent call last):
+    UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 0: ordinal not in range(128)
+    >>> ascii_str(u'å')
+    Traceback (most recent call last):
+    AssertionError: 'å'
+    """
+    assert isinstance(s, bytes), repr(s)
+    return s.decode('ascii')
 
 
 # Regex taken from http://www.regular-expressions.info/email.html
@@ -178,7 +198,7 @@
     m = email_re.search(author)
     if m is None:
         return ''
-    return safe_str(m.group(0))
+    return m.group(0)
 
 
 def author_name(author):
--- a/kallithea/lib/vcs/utils/annotate.py	Mon May 04 18:25:09 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,179 +0,0 @@
-import StringIO
-
-from pygments import highlight
-from pygments.formatters import HtmlFormatter
-
-from kallithea.lib.vcs.exceptions import VCSError
-from kallithea.lib.vcs.nodes import FileNode
-
-
-def annotate_highlight(filenode, annotate_from_changeset_func=None,
-        order=None, headers=None, **options):
-    """
-    Returns html portion containing annotated table with 3 columns: line
-    numbers, changeset information and pygmentized line of code.
-
-    :param filenode: FileNode object
-    :param annotate_from_changeset_func: function taking changeset and
-      returning single annotate cell; needs break line at the end
-    :param order: ordered sequence of ``ls`` (line numbers column),
-      ``annotate`` (annotate column), ``code`` (code column); Default is
-      ``['ls', 'annotate', 'code']``
-    :param headers: dictionary with headers (keys are whats in ``order``
-      parameter)
-    """
-    options['linenos'] = True
-    formatter = AnnotateHtmlFormatter(filenode=filenode, order=order,
-        headers=headers,
-        annotate_from_changeset_func=annotate_from_changeset_func, **options)
-    lexer = filenode.lexer
-    highlighted = highlight(filenode.content, lexer, formatter)
-    return highlighted
-
-
-class AnnotateHtmlFormatter(HtmlFormatter):
-
-    def __init__(self, filenode, annotate_from_changeset_func=None,
-            order=None, **options):
-        """
-        If ``annotate_from_changeset_func`` is passed it should be a function
-        which returns string from the given changeset. For example, we may pass
-        following function as ``annotate_from_changeset_func``::
-
-            def changeset_to_anchor(changeset):
-                return '<a href="/changesets/%s/">%s</a>\n' % \
-                       (changeset.id, changeset.id)
-
-        :param annotate_from_changeset_func: see above
-        :param order: (default: ``['ls', 'annotate', 'code']``); order of
-          columns;
-        :param options: standard pygment's HtmlFormatter options, there is
-          extra option tough, ``headers``. For instance we can pass::
-
-             formatter = AnnotateHtmlFormatter(filenode, headers={
-                'ls': '#',
-                'annotate': 'Annotate',
-                'code': 'Code',
-             })
-
-        """
-        super(AnnotateHtmlFormatter, self).__init__(**options)
-        self.annotate_from_changeset_func = annotate_from_changeset_func
-        self.order = order or ('ls', 'annotate', 'code')
-        headers = options.pop('headers', None)
-        if headers and not ('ls' in headers and 'annotate' in headers and
-            'code' in headers
-        ):
-            raise ValueError("If headers option dict is specified it must "
-                "all 'ls', 'annotate' and 'code' keys")
-        self.headers = headers
-        if isinstance(filenode, FileNode):
-            self.filenode = filenode
-        else:
-            raise VCSError("This formatter expect FileNode parameter, not %r"
-                % type(filenode))
-
-    def annotate_from_changeset(self, changeset):
-        """
-        Returns full html line for single changeset per annotated line.
-        """
-        if self.annotate_from_changeset_func:
-            return self.annotate_from_changeset_func(changeset)
-        else:
-            return ''.join((changeset.id, '\n'))
-
-    def _wrap_tablelinenos(self, inner):
-        dummyoutfile = StringIO.StringIO()
-        lncount = 0
-        for t, line in inner:
-            if t:
-                lncount += 1
-            dummyoutfile.write(line)
-
-        fl = self.linenostart
-        mw = len(str(lncount + fl - 1))
-        sp = self.linenospecial
-        st = self.linenostep
-        la = self.lineanchors
-        aln = self.anchorlinenos
-        if sp:
-            lines = []
-
-            for i in range(fl, fl + lncount):
-                if i % st == 0:
-                    if i % sp == 0:
-                        if aln:
-                            lines.append('<a href="#%s-%d" class="special">'
-                                         '%*d</a>' %
-                                         (la, i, mw, i))
-                        else:
-                            lines.append('<span class="special">'
-                                         '%*d</span>' % (mw, i))
-                    else:
-                        if aln:
-                            lines.append('<a href="#%s-%d">'
-                                         '%*d</a>' % (la, i, mw, i))
-                        else:
-                            lines.append('%*d' % (mw, i))
-                else:
-                    lines.append('')
-            ls = '\n'.join(lines)
-        else:
-            lines = []
-            for i in range(fl, fl + lncount):
-                if i % st == 0:
-                    if aln:
-                        lines.append('<a href="#%s-%d">%*d</a>'
-                                     % (la, i, mw, i))
-                    else:
-                        lines.append('%*d' % (mw, i))
-                else:
-                    lines.append('')
-            ls = '\n'.join(lines)
-
-        annotate_changesets = [tup[1] for tup in self.filenode.annotate]
-        # If pygments cropped last lines break we need do that too
-        ln_cs = len(annotate_changesets)
-        ln_ = len(ls.splitlines())
-        if ln_cs > ln_:
-            annotate_changesets = annotate_changesets[:ln_ - ln_cs]
-        annotate = ''.join((self.annotate_from_changeset(changeset)
-            for changeset in annotate_changesets))
-        # in case you wonder about the seemingly redundant <div> here:
-        # since the content in the other cell also is wrapped in a div,
-        # some browsers in some configurations seem to mess up the formatting.
-        '''
-        yield 0, ('<table class="%stable">' % self.cssclass +
-                  '<tr><td class="linenos"><div class="linenodiv"><pre>' +
-                  ls + '</pre></div></td>' +
-                  '<td class="code">')
-        yield 0, dummyoutfile.getvalue()
-        yield 0, '</td></tr></table>'
-
-        '''
-        headers_row = []
-        if self.headers:
-            headers_row = ['<tr class="annotate-header">']
-            for key in self.order:
-                td = ''.join(('<td>', self.headers[key], '</td>'))
-                headers_row.append(td)
-            headers_row.append('</tr>')
-
-        body_row_start = ['<tr>']
-        for key in self.order:
-            if key == 'ls':
-                body_row_start.append(
-                    '<td class="linenos"><div class="linenodiv"><pre>' +
-                    ls + '</pre></div></td>')
-            elif key == 'annotate':
-                body_row_start.append(
-                    '<td class="annotate"><div class="annotatediv"><pre>' +
-                    annotate + '</pre></div></td>')
-            elif key == 'code':
-                body_row_start.append('<td class="code">')
-        yield 0, ('<table class="%stable">' % self.cssclass +
-                  ''.join(headers_row) +
-                  ''.join(body_row_start)
-                  )
-        yield 0, dummyoutfile.getvalue()
-        yield 0, '</td></tr></table>'
--- a/kallithea/lib/vcs/utils/archivers.py	Mon May 04 18:25:09 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,67 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-    vcs.utils.archivers
-    ~~~~~~~~~~~~~~~~~~~
-
-    set of archiver functions for creating archives from repository content
-
-    :created_on: Jan 21, 2011
-    :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
-"""
-
-
-class BaseArchiver(object):
-
-    def __init__(self):
-        self.archive_file = self._get_archive_file()
-
-    def addfile(self):
-        """
-        Adds a file to archive container
-        """
-        pass
-
-    def close(self):
-        """
-        Closes and finalizes operation of archive container object
-        """
-        self.archive_file.close()
-
-    def _get_archive_file(self):
-        """
-        Returns container for specific archive
-        """
-        raise NotImplementedError()
-
-
-class TarArchiver(BaseArchiver):
-    pass
-
-
-class Tbz2Archiver(BaseArchiver):
-    pass
-
-
-class TgzArchiver(BaseArchiver):
-    pass
-
-
-class ZipArchiver(BaseArchiver):
-    pass
-
-
-def get_archiver(self, kind):
-    """
-    Returns instance of archiver class specific to given kind
-
-    :param kind: archive kind
-    """
-
-    archivers = {
-        'tar': TarArchiver,
-        'tbz2': Tbz2Archiver,
-        'tgz': TgzArchiver,
-        'zip': ZipArchiver,
-    }
-
-    return archivers[kind]()
--- a/kallithea/lib/vcs/utils/fakemod.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/utils/fakemod.py	Mon May 04 19:24:04 2020 +0200
@@ -9,5 +9,5 @@
     """
     module = imp.new_module(name)
     module.__file__ = path
-    execfile(path, module.__dict__)
+    exec(compile(open(path, "rb").read(), path, 'exec'), module.__dict__)
     return module
--- a/kallithea/lib/vcs/utils/helpers.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/utils/helpers.py	Mon May 04 19:24:04 2020 +0200
@@ -1,7 +1,6 @@
 """
 Utilities aimed to help achieve mostly basic tasks.
 """
-from __future__ import division
 
 import datetime
 import os
@@ -33,16 +32,14 @@
     if not os.path.isdir(path):
         raise VCSError("Given path %s is not a directory" % path)
 
-    def get_scms(path):
-        return [(scm, path) for scm in get_scms_for_path(path)]
-
-    found_scms = get_scms(path)
-    while not found_scms and search_up:
+    while True:
+        found_scms = [(scm, path) for scm in get_scms_for_path(path)]
+        if found_scms or not search_up:
+            break
         newpath = abspath(path, '..')
         if newpath == path:
             break
         path = newpath
-        found_scms = get_scms(path)
 
     if len(found_scms) > 1:
         for scm in found_scms:
@@ -133,7 +130,7 @@
         >>> parse_changesets('aaabbb')
         {'start': None, 'main': 'aaabbb', 'end': None}
         >>> parse_changesets('aaabbb..cccddd')
-        {'start': 'aaabbb', 'main': None, 'end': 'cccddd'}
+        {'start': 'aaabbb', 'end': 'cccddd', 'main': None}
 
     """
     text = text.strip()
--- a/kallithea/lib/vcs/utils/hgcompat.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/utils/hgcompat.py	Mon May 04 19:24:04 2020 +0200
@@ -2,46 +2,16 @@
 Mercurial libs compatibility
 """
 
-# Mercurial 5.0 550a172a603b renamed memfilectx argument `copied` to `copysource`
-import inspect
-
-import mercurial
-from mercurial import archival, config, demandimport, discovery, httppeer, localrepo
-from mercurial import merge as hg_merge
-from mercurial import obsutil, patch, scmutil, sshpeer, ui, unionrepo
-from mercurial.commands import clone, nullid, pull
-from mercurial.context import memctx, memfilectx
-from mercurial.discovery import findcommonoutgoing
-from mercurial.encoding import tolocal
-from mercurial.error import Abort, RepoError, RepoLookupError
-from mercurial.hg import peer
-from mercurial.hgweb import hgweb_mod
-from mercurial.hgweb.common import get_contact
-from mercurial.match import exact as match_exact
-from mercurial.match import match
-from mercurial.mdiff import diffopts
-from mercurial.node import hex, nullrev
-from mercurial.scmutil import revrange
-from mercurial.tags import tag
-from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
-from mercurial.util import url as hg_url
+import mercurial.encoding
+import mercurial.localrepo
 
 
-# patch demandimport, due to bug in mercurial when it always triggers demandimport.enable()
-demandimport.enable = lambda *args, **kwargs: 1
-
-
-# workaround for 3.3 94ac64bcf6fe and not calling largefiles reposetup correctly
-localrepo.localrepository._lfstatuswriters = [lambda *msg, **opts: None]
-# 3.5 7699d3212994 added the invariant that repo.lfstatus must exist before hitting overridearchive
-localrepo.localrepository.lfstatus = False
+def monkey_do():
+    """Apply some Mercurial monkey patching"""
+    # workaround for 3.3 94ac64bcf6fe and not calling largefiles reposetup correctly, and test_archival failing
+    mercurial.localrepo.localrepository._lfstatuswriters = [lambda *msg, **opts: None]
+    # 3.5 7699d3212994 added the invariant that repo.lfstatus must exist before hitting overridearchive
+    mercurial.localrepo.localrepository.lfstatus = False
 
-if inspect.getargspec(memfilectx.__init__).args[7] != 'copysource':
-    assert inspect.getargspec(memfilectx.__init__).args[7] == 'copied', inspect.getargspec(memfilectx.__init__).args
-    __org_memfilectx_ = memfilectx
-    memfilectx = lambda repo, changectx, path, data, islink=False, isexec=False, copysource=None: \
-        __org_memfilectx_(repo, changectx, path, data, islink=islink, isexec=isexec, copied=copysource)
-
-# Mercurial 5.0 dropped exact argument for match in 635a12c53ea6, and 0531dff73d0b made the exact function stable with a single parameter
-if inspect.getargspec(match_exact).args[0] != 'files':
-    match_exact = lambda path: match(None, '', [path], exact=True)
+    # Minimize potential impact from custom configuration
+    mercurial.encoding.environ[b'HGPLAIN'] = b'1'
--- a/kallithea/lib/vcs/utils/imports.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/utils/imports.py	Mon May 04 19:24:04 2020 +0200
@@ -1,6 +1,3 @@
-from kallithea.lib.vcs.exceptions import VCSError
-
-
 def import_class(class_path):
     """
     Returns class from the given path.
@@ -8,10 +5,7 @@
     For example, in order to get class located at
     ``vcs.backends.hg.MercurialRepository``:
 
-        try:
-            hgrepo = import_class('vcs.backends.hg.MercurialRepository')
-        except VCSError:
-            # handle error
+        hgrepo = import_class('vcs.backends.hg.MercurialRepository')
     """
     splitted = class_path.split('.')
     mod_path = '.'.join(splitted[:-1])
--- a/kallithea/lib/vcs/utils/lazy.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/utils/lazy.py	Mon May 04 19:24:04 2020 +0200
@@ -1,6 +1,3 @@
-import threading
-
-
 class _Missing(object):
 
     def __repr__(self):
@@ -44,21 +41,3 @@
             value = self._func(obj)
             obj.__dict__[self.__name__] = value
         return value
-
-
-class ThreadLocalLazyProperty(LazyProperty):
-    """
-    Same as above but uses thread local dict for cache storage.
-    """
-
-    def __get__(self, obj, klass=None):
-        if obj is None:
-            return self
-        if not hasattr(obj, '__tl_dict__'):
-            obj.__tl_dict__ = threading.local().__dict__
-
-        value = obj.__tl_dict__.get(self.__name__, _missing)
-        if value is _missing:
-            value = self._func(obj)
-            obj.__tl_dict__[self.__name__] = value
-        return value
--- a/kallithea/lib/vcs/utils/paths.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/utils/paths.py	Mon May 04 19:24:04 2020 +0200
@@ -1,7 +1,8 @@
 import os
 
 
-abspath = lambda * p: os.path.abspath(os.path.join(*p))
+def abspath(*p):
+    return os.path.abspath(os.path.join(*p))
 
 
 def get_dirs_for_path(*paths):
@@ -11,7 +12,7 @@
     for path in paths:
         head = path
         while head:
-            head, tail = os.path.split(head)
+            head, _tail = os.path.split(head)
             if head:
                 yield head
             else:
--- a/kallithea/lib/vcs/utils/progressbar.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/utils/progressbar.py	Mon May 04 19:24:04 2020 +0200
@@ -1,7 +1,5 @@
 # encoding: UTF-8
 
-from __future__ import print_function
-
 import datetime
 import string
 import sys
@@ -43,7 +41,7 @@
     def __iter__(self):
         start = self.step
         end = self.steps + 1
-        for x in xrange(start, end):
+        for x in range(start, end):
             self.render(x)
             yield x
 
@@ -215,7 +213,7 @@
     code_list = []
     if text == '' and len(opts) == 1 and opts[0] == 'reset':
         return '\x1b[%sm' % RESET
-    for k, v in kwargs.iteritems():
+    for k, v in kwargs.items():
         if k == 'fg':
             code_list.append(foreground[v])
         elif k == 'bg':
@@ -359,7 +357,7 @@
 
     print("Standard progress bar...")
     bar = ProgressBar(30)
-    for x in xrange(1, 31):
+    for x in range(1, 31):
         bar.render(x)
         time.sleep(0.02)
     bar.stream.write('\n')
@@ -410,7 +408,7 @@
     bar.width = 50
     bar.elements.remove('steps')
     bar.elements += ['transfer', 'time', 'eta', 'speed']
-    for x in xrange(0, bar.steps, 1024):
+    for x in range(0, bar.steps, 1024):
         bar.render(x)
         time.sleep(0.01)
         now = datetime.datetime.now()
--- a/kallithea/lib/vcs/utils/termcolors.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/lib/vcs/utils/termcolors.py	Mon May 04 19:24:04 2020 +0200
@@ -44,7 +44,7 @@
     code_list = []
     if text == '' and len(opts) == 1 and opts[0] == 'reset':
         return '\x1b[%sm' % RESET
-    for k, v in kwargs.iteritems():
+    for k, v in kwargs.items():
         if k == 'fg':
             code_list.append(foreground[v])
         elif k == 'bg':
@@ -188,7 +188,7 @@
                 definition['bg'] = colors[-1]
 
             # All remaining instructions are options
-            opts = tuple(s for s in styles if s in opt_dict.keys())
+            opts = tuple(s for s in styles if s in opt_dict)
             if opts:
                 definition['opts'] = opts
 
--- a/kallithea/model/comment.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/model/comment.py	Mon May 04 19:24:04 2020 +0200
@@ -31,7 +31,7 @@
 from tg.i18n import ugettext as _
 
 from kallithea.lib import helpers as h
-from kallithea.lib.utils2 import extract_mentioned_users, safe_unicode
+from kallithea.lib.utils2 import extract_mentioned_users
 from kallithea.model.db import ChangesetComment, PullRequest, Repository, User
 from kallithea.model.meta import Session
 from kallithea.model.notification import NotificationModel
@@ -81,11 +81,10 @@
                 repo_name=repo.repo_name,
                 revision=revision,
                 anchor='comment-%s' % comment.comment_id)
-            subj = safe_unicode(
-                h.link_to('Re changeset: %(desc)s %(line)s' %
+            subj = h.link_to(
+                'Re changeset: %(desc)s %(line)s' %
                           {'desc': desc, 'line': line},
-                          comment_url)
-            )
+                 comment_url)
             # get the current participants of this changeset
             recipients = _list_changeset_commenters(revision)
             # add changeset author if it's known locally
@@ -127,13 +126,12 @@
                                                           h.canonical_hostname()))
             comment_url = pull_request.url(canonical=True,
                 anchor='comment-%s' % comment.comment_id)
-            subj = safe_unicode(
-                h.link_to('Re pull request %(pr_nice_id)s: %(desc)s %(line)s' %
+            subj = h.link_to(
+                'Re pull request %(pr_nice_id)s: %(desc)s %(line)s' %
                           {'desc': desc,
                            'pr_nice_id': comment.pull_request.nice_id(),
                            'line': line},
-                          comment_url)
-            )
+                comment_url)
             # get the current participants of this pull request
             recipients = _list_pull_request_commenters(pull_request)
             recipients.append(pull_request.owner)
@@ -257,7 +255,7 @@
         paths = defaultdict(lambda: defaultdict(list))
         for co in comments:
             paths[co.f_path][co.line_no].append(co)
-        return paths.items()
+        return sorted(paths.items())
 
     def _get_comments(self, repo_id, revision=None, pull_request=None,
                 inline=False, f_path=None, line_no=None):
--- a/kallithea/model/db.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/model/db.py	Mon May 04 19:24:04 2020 +0200
@@ -25,6 +25,7 @@
 :license: GPLv3, see LICENSE.md for more details.
 """
 
+import base64
 import collections
 import datetime
 import functools
@@ -36,22 +37,20 @@
 
 import ipaddr
 import sqlalchemy
-from beaker.cache import cache_region, region_invalidate
-from sqlalchemy import *
+from sqlalchemy import Boolean, Column, DateTime, Float, ForeignKey, Index, Integer, LargeBinary, String, Unicode, UnicodeText, UniqueConstraint
 from sqlalchemy.ext.hybrid import hybrid_property
 from sqlalchemy.orm import class_mapper, joinedload, relationship, validates
 from tg.i18n import lazy_ugettext as _
 from webob.exc import HTTPNotFound
 
 import kallithea
-from kallithea.lib.caching_query import FromCache
-from kallithea.lib.compat import json
+from kallithea.lib import ext_json
 from kallithea.lib.exceptions import DefaultUserException
-from kallithea.lib.utils2 import Optional, aslist, get_changeset_safe, get_clone_url, remove_prefix, safe_int, safe_str, safe_unicode, str2bool, urlreadable
+from kallithea.lib.utils2 import (Optional, ascii_bytes, aslist, get_changeset_safe, get_clone_url, remove_prefix, safe_bytes, safe_int, safe_str, str2bool,
+                                  urlreadable)
 from kallithea.lib.vcs import get_backend
 from kallithea.lib.vcs.backends.base import EmptyChangeset
 from kallithea.lib.vcs.utils.helpers import get_scm
-from kallithea.lib.vcs.utils.lazy import LazyProperty
 from kallithea.model.meta import Base, Session
 
 
@@ -62,7 +61,8 @@
 # BASE CLASSES
 #==============================================================================
 
-_hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
+def _hash_key(k):
+    return hashlib.md5(safe_bytes(k)).hexdigest()
 
 
 class BaseDbModel(object):
@@ -73,6 +73,7 @@
     @classmethod
     def _get_keys(cls):
         """return column names for this model """
+        # Note: not a normal dict - iterator gives "users.firstname", but keys gives "firstname"
         return class_mapper(cls).c.keys()
 
     def get_dict(self):
@@ -90,7 +91,7 @@
             # update with attributes from __json__
             if callable(_json_attr):
                 _json_attr = _json_attr()
-            for k, val in _json_attr.iteritems():
+            for k, val in _json_attr.items():
                 d[k] = val
         return d
 
@@ -135,8 +136,10 @@
             return None
         if isinstance(value, cls):
             return value
-        if isinstance(value, (int, long)) or safe_str(value).isdigit():
+        if isinstance(value, int):
             return cls.get(value)
+        if isinstance(value, str) and value.isdigit():
+            return cls.get(int(value))
         if callback is not None:
             return callback(value)
 
@@ -163,12 +166,6 @@
         Session().delete(obj)
 
     def __repr__(self):
-        if hasattr(self, '__unicode__'):
-            # python repr needs to return str
-            try:
-                return safe_str(self.__unicode__())
-            except UnicodeDecodeError:
-                pass
         return '<DB:%s>' % (self.__class__.__name__)
 
 
@@ -185,9 +182,9 @@
     )
 
     SETTINGS_TYPES = {
-        'str': safe_str,
+        'str': safe_bytes,
         'int': safe_int,
-        'unicode': safe_unicode,
+        'unicode': safe_str,
         'bool': str2bool,
         'list': functools.partial(aslist, sep=',')
     }
@@ -205,7 +202,7 @@
 
     @validates('_app_settings_value')
     def validate_settings_value(self, key, val):
-        assert type(val) == unicode
+        assert isinstance(val, str)
         return val
 
     @hybrid_property
@@ -218,11 +215,9 @@
     @app_settings_value.setter
     def app_settings_value(self, val):
         """
-        Setter that will always make sure we use unicode in app_settings_value
-
-        :param val:
+        Setter that will always make sure we use str in app_settings_value
         """
-        self._app_settings_value = safe_unicode(val)
+        self._app_settings_value = safe_str(val)
 
     @hybrid_property
     def app_settings_type(self):
@@ -232,13 +227,13 @@
     def app_settings_type(self, val):
         if val not in self.SETTINGS_TYPES:
             raise Exception('type must be one of %s got %s'
-                            % (self.SETTINGS_TYPES.keys(), val))
+                            % (list(self.SETTINGS_TYPES), val))
         self._app_settings_type = val
 
-    def __unicode__(self):
-        return u"<%s('%s:%s[%s]')>" % (
+    def __repr__(self):
+        return "<%s %s.%s=%r>" % (
             self.__class__.__name__,
-            self.app_settings_name, self.app_settings_value, self.app_settings_type
+            self.app_settings_name, self.app_settings_type, self.app_settings_value
         )
 
     @classmethod
@@ -281,13 +276,9 @@
         return res
 
     @classmethod
-    def get_app_settings(cls, cache=False):
+    def get_app_settings(cls):
 
         ret = cls.query()
-
-        if cache:
-            ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
-
         if ret is None:
             raise Exception('Could not get application settings !')
         settings = {}
@@ -298,7 +289,7 @@
         return settings
 
     @classmethod
-    def get_auth_settings(cls, cache=False):
+    def get_auth_settings(cls):
         ret = cls.query() \
                 .filter(cls.app_settings_name.startswith('auth_')).all()
         fd = {}
@@ -307,7 +298,7 @@
         return fd
 
     @classmethod
-    def get_default_repo_settings(cls, cache=False, strip_prefix=False):
+    def get_default_repo_settings(cls, strip_prefix=False):
         ret = cls.query() \
                 .filter(cls.app_settings_name.startswith('default_')).all()
         fd = {}
@@ -328,9 +319,9 @@
         info = {
             'modules': sorted(mods, key=lambda k: k[0].lower()),
             'py_version': platform.python_version(),
-            'platform': safe_unicode(platform.platform()),
+            'platform': platform.platform(),
             'kallithea_version': kallithea.__version__,
-            'git_version': safe_unicode(check_git_version()),
+            'git_version': str(check_git_version()),
             'git_path': kallithea.CONFIG.get('git_path')
         }
         return info
@@ -339,9 +330,7 @@
 class Ui(Base, BaseDbModel):
     __tablename__ = 'ui'
     __table_args__ = (
-        # FIXME: ui_key as key is wrong and should be removed when the corresponding
-        # Ui.get_by_key has been replaced by the composite key
-        UniqueConstraint('ui_key'),
+        Index('ui_ui_section_ui_key_idx', 'ui_section', 'ui_key'),
         UniqueConstraint('ui_section', 'ui_key'),
         _table_args_default_dict,
     )
@@ -374,6 +363,7 @@
         q = cls.query()
         q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE]))
         q = q.filter(cls.ui_section == 'hooks')
+        q = q.order_by(cls.ui_section, cls.ui_key)
         return q.all()
 
     @classmethod
@@ -381,6 +371,7 @@
         q = cls.query()
         q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE]))
         q = q.filter(cls.ui_section == 'hooks')
+        q = q.order_by(cls.ui_section, cls.ui_key)
         return q.all()
 
     @classmethod
@@ -394,8 +385,9 @@
         new_ui.ui_value = val
 
     def __repr__(self):
-        return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
-                                    self.ui_key, self.ui_value)
+        return '<%s %s.%s=%r>' % (
+            self.__class__.__name__,
+            self.ui_section, self.ui_key, self.ui_value)
 
 
 class User(Base, BaseDbModel):
@@ -406,7 +398,7 @@
         _table_args_default_dict,
     )
 
-    DEFAULT_USER = 'default'
+    DEFAULT_USER_NAME = 'default'
     DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
     # The name of the default auth type in extern_type, 'internal' lives in auth_internal.py
     DEFAULT_AUTH_TYPE = 'internal'
@@ -512,7 +504,7 @@
 
     @hybrid_property
     def is_default_user(self):
-        return self.username == User.DEFAULT_USER
+        return self.username == User.DEFAULT_USER_NAME
 
     @hybrid_property
     def user_data(self):
@@ -520,20 +512,19 @@
             return {}
 
         try:
-            return json.loads(self._user_data)
+            return ext_json.loads(self._user_data)
         except TypeError:
             return {}
 
     @user_data.setter
     def user_data(self, val):
         try:
-            self._user_data = json.dumps(val)
+            self._user_data = ascii_bytes(ext_json.dumps(val))
         except Exception:
             log.error(traceback.format_exc())
 
-    def __unicode__(self):
-        return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
-                                      self.user_id, self.username)
+    def __repr__(self):
+        return "<%s %s: %r>" % (self.__class__.__name__, self.user_id, self.username)
 
     @classmethod
     def guess_instance(cls, value):
@@ -551,7 +542,7 @@
         return user
 
     @classmethod
-    def get_by_username_or_email(cls, username_or_email, case_insensitive=False, cache=False):
+    def get_by_username_or_email(cls, username_or_email, case_insensitive=True):
         """
         For anything that looks like an email address, look up by the email address (matching
         case insensitively).
@@ -560,35 +551,24 @@
         This assumes no normal username can have '@' symbol.
         """
         if '@' in username_or_email:
-            return User.get_by_email(username_or_email, cache=cache)
+            return User.get_by_email(username_or_email)
         else:
-            return User.get_by_username(username_or_email, case_insensitive=case_insensitive, cache=cache)
+            return User.get_by_username(username_or_email, case_insensitive=case_insensitive)
 
     @classmethod
-    def get_by_username(cls, username, case_insensitive=False, cache=False):
+    def get_by_username(cls, username, case_insensitive=False):
         if case_insensitive:
-            q = cls.query().filter(func.lower(cls.username) == func.lower(username))
+            q = cls.query().filter(sqlalchemy.func.lower(cls.username) == sqlalchemy.func.lower(username))
         else:
             q = cls.query().filter(cls.username == username)
-
-        if cache:
-            q = q.options(FromCache(
-                            "sql_cache_short",
-                            "get_user_%s" % _hash_key(username)
-                          )
-            )
         return q.scalar()
 
     @classmethod
-    def get_by_api_key(cls, api_key, cache=False, fallback=True):
+    def get_by_api_key(cls, api_key, fallback=True):
         if len(api_key) != 40 or not api_key.isalnum():
             return None
 
         q = cls.query().filter(cls.api_key == api_key)
-
-        if cache:
-            q = q.options(FromCache("sql_cache_short",
-                                    "get_api_key_%s" % api_key))
         res = q.scalar()
 
         if fallback and not res:
@@ -602,21 +582,13 @@
 
     @classmethod
     def get_by_email(cls, email, cache=False):
-        q = cls.query().filter(func.lower(cls.email) == func.lower(email))
-
-        if cache:
-            q = q.options(FromCache("sql_cache_short",
-                                    "get_email_key_%s" % email))
-
+        q = cls.query().filter(sqlalchemy.func.lower(cls.email) == sqlalchemy.func.lower(email))
         ret = q.scalar()
         if ret is None:
             q = UserEmailMap.query()
             # try fetching in alternate email map
-            q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
+            q = q.filter(sqlalchemy.func.lower(UserEmailMap.email) == sqlalchemy.func.lower(email))
             q = q.options(joinedload(UserEmailMap.user))
-            if cache:
-                q = q.options(FromCache("sql_cache_short",
-                                        "get_email_map_key_%s" % email))
             ret = getattr(q.scalar(), 'user', None)
 
         return ret
@@ -654,8 +626,8 @@
         return user
 
     @classmethod
-    def get_default_user(cls, cache=False):
-        user = User.get_by_username(User.DEFAULT_USER, cache=cache)
+    def get_default_user(cls):
+        user = User.get_by_username(User.DEFAULT_USER_NAME)
         if user is None:
             raise Exception('Missing default account!')
         return user
@@ -772,9 +744,8 @@
           ip_range=self._get_ip_range(self.ip_addr)
         )
 
-    def __unicode__(self):
-        return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
-                                            self.user_id, self.ip_addr)
+    def __repr__(self):
+        return "<%s %s: %s>" % (self.__class__.__name__, self.user_id, self.ip_addr)
 
 
 class UserLog(Base, BaseDbModel):
@@ -792,10 +763,10 @@
     action = Column(UnicodeText(), nullable=False)
     action_date = Column(DateTime(timezone=False), nullable=False)
 
-    def __unicode__(self):
-        return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
-                                      self.repository_name,
-                                      self.action)
+    def __repr__(self):
+        return "<%s %r: %r>" % (self.__class__.__name__,
+                                  self.repository_name,
+                                  self.action)
 
     @property
     def action_as_day(self):
@@ -823,8 +794,8 @@
     users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
     users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
     users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
-    user_user_group_to_perm = relationship('UserUserGroupToPerm ', cascade='all')
-    user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
+    user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
+    user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
 
     owner = relationship('User')
 
@@ -834,47 +805,37 @@
             return {}
 
         try:
-            return json.loads(self._group_data)
+            return ext_json.loads(self._group_data)
         except TypeError:
             return {}
 
     @group_data.setter
     def group_data(self, val):
         try:
-            self._group_data = json.dumps(val)
+            self._group_data = ascii_bytes(ext_json.dumps(val))
         except Exception:
             log.error(traceback.format_exc())
 
-    def __unicode__(self):
-        return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
-                                      self.users_group_id,
-                                      self.users_group_name)
+    def __repr__(self):
+        return "<%s %s: %r>" % (self.__class__.__name__,
+                                  self.users_group_id,
+                                  self.users_group_name)
 
     @classmethod
     def guess_instance(cls, value):
         return super(UserGroup, cls).guess_instance(value, UserGroup.get_by_group_name)
 
     @classmethod
-    def get_by_group_name(cls, group_name, cache=False,
-                          case_insensitive=False):
+    def get_by_group_name(cls, group_name, case_insensitive=False):
         if case_insensitive:
-            q = cls.query().filter(func.lower(cls.users_group_name) == func.lower(group_name))
+            q = cls.query().filter(sqlalchemy.func.lower(cls.users_group_name) == sqlalchemy.func.lower(group_name))
         else:
             q = cls.query().filter(cls.users_group_name == group_name)
-        if cache:
-            q = q.options(FromCache(
-                            "sql_cache_short",
-                            "get_group_%s" % _hash_key(group_name)
-                          )
-            )
         return q.scalar()
 
     @classmethod
-    def get(cls, user_group_id, cache=False):
+    def get(cls, user_group_id):
         user_group = cls.query()
-        if cache:
-            user_group = user_group.options(FromCache("sql_cache_short",
-                                    "get_users_group_%s" % user_group_id))
         return user_group.get(user_group_id)
 
     def get_api_data(self, with_members=True):
@@ -962,9 +923,9 @@
     DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
     DEFAULT_CLONE_SSH = 'ssh://{system_user}@{hostname}/{repo}'
 
-    STATE_CREATED = u'repo_state_created'
-    STATE_PENDING = u'repo_state_pending'
-    STATE_ERROR = u'repo_state_error'
+    STATE_CREATED = 'repo_state_created'
+    STATE_PENDING = 'repo_state_pending'
+    STATE_ERROR = 'repo_state_error'
 
     repo_id = Column(Integer(), primary_key=True)
     repo_name = Column(Unicode(255), nullable=False, unique=True)
@@ -1009,9 +970,9 @@
                     primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
                     cascade="all, delete-orphan")
 
-    def __unicode__(self):
-        return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
-                                   safe_unicode(self.repo_name))
+    def __repr__(self):
+        return "<%s %s: %r>" % (self.__class__.__name__,
+                                  self.repo_id, self.repo_name)
 
     @hybrid_property
     def landing_rev(self):
@@ -1033,7 +994,7 @@
     @hybrid_property
     def changeset_cache(self):
         try:
-            cs_cache = json.loads(self._changeset_cache) # might raise on bad data
+            cs_cache = ext_json.loads(self._changeset_cache) # might raise on bad data
             cs_cache['raw_id'] # verify data, raise exception on error
             return cs_cache
         except (TypeError, KeyError, ValueError):
@@ -1042,7 +1003,7 @@
     @changeset_cache.setter
     def changeset_cache(self, val):
         try:
-            self._changeset_cache = json.dumps(val)
+            self._changeset_cache = ascii_bytes(ext_json.dumps(val))
         except Exception:
             log.error(traceback.format_exc())
 
@@ -1055,15 +1016,11 @@
         q = super(Repository, cls).query()
 
         if sorted:
-            q = q.order_by(func.lower(Repository.repo_name))
+            q = q.order_by(sqlalchemy.func.lower(Repository.repo_name))
 
         return q
 
     @classmethod
-    def url_sep(cls):
-        return URL_SEP
-
-    @classmethod
     def normalize_repo_name(cls, repo_name):
         """
         Normalizes os specific repo_name to the format internally stored inside
@@ -1072,7 +1029,7 @@
         :param cls:
         :param repo_name:
         """
-        return cls.url_sep().join(repo_name.split(os.sep))
+        return URL_SEP.join(repo_name.split(os.sep))
 
     @classmethod
     def guess_instance(cls, value):
@@ -1083,7 +1040,7 @@
         """Get the repo, defaulting to database case sensitivity.
         case_insensitive will be slower and should only be specified if necessary."""
         if case_insensitive:
-            q = Session().query(cls).filter(func.lower(cls.repo_name) == func.lower(repo_name))
+            q = Session().query(cls).filter(sqlalchemy.func.lower(cls.repo_name) == sqlalchemy.func.lower(repo_name))
         else:
             q = Session().query(cls).filter(cls.repo_name == repo_name)
         q = q.options(joinedload(Repository.fork)) \
@@ -1093,7 +1050,7 @@
 
     @classmethod
     def get_by_full_path(cls, repo_full_path):
-        base_full_path = os.path.realpath(cls.base_path())
+        base_full_path = os.path.realpath(kallithea.CONFIG['base_path'])
         repo_full_path = os.path.realpath(repo_full_path)
         assert repo_full_path.startswith(base_full_path + os.path.sep)
         repo_name = repo_full_path[len(base_full_path) + 1:]
@@ -1104,18 +1061,6 @@
     def get_repo_forks(cls, repo_id):
         return cls.query().filter(Repository.fork_id == repo_id)
 
-    @classmethod
-    def base_path(cls):
-        """
-        Returns base path where all repos are stored
-
-        :param cls:
-        """
-        q = Session().query(Ui) \
-            .filter(Ui.ui_key == cls.url_sep())
-        q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
-        return q.one().ui_value
-
     @property
     def forks(self):
         """
@@ -1132,7 +1077,7 @@
 
     @property
     def just_name(self):
-        return self.repo_name.split(Repository.url_sep())[-1]
+        return self.repo_name.split(URL_SEP)[-1]
 
     @property
     def groups_with_parents(self):
@@ -1145,35 +1090,18 @@
         groups.reverse()
         return groups
 
-    @LazyProperty
-    def repo_path(self):
-        """
-        Returns base full path for that repository means where it actually
-        exists on a filesystem
-        """
-        q = Session().query(Ui).filter(Ui.ui_key ==
-                                              Repository.url_sep())
-        q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
-        return q.one().ui_value
-
     @property
     def repo_full_path(self):
-        p = [self.repo_path]
+        """
+        Returns base full path for the repository - where it actually
+        exists on a filesystem.
+        """
+        p = [kallithea.CONFIG['base_path']]
         # we need to split the name by / since this is how we store the
         # names in the database, but that eventually needs to be converted
         # into a valid system path
-        p += self.repo_name.split(Repository.url_sep())
-        return os.path.join(*map(safe_unicode, p))
-
-    @property
-    def cache_keys(self):
-        """
-        Returns associated cache keys for that repo
-        """
-        return CacheInvalidation.query() \
-            .filter(CacheInvalidation.cache_args == self.repo_name) \
-            .order_by(CacheInvalidation.cache_key) \
-            .all()
+        p += self.repo_name.split(URL_SEP)
+        return os.path.join(*p)
 
     def get_new_name(self, repo_name):
         """
@@ -1182,7 +1110,7 @@
         :param group_name:
         """
         path_prefix = self.group.full_path_splitted if self.group else []
-        return Repository.url_sep().join(path_prefix + [repo_name])
+        return URL_SEP.join(path_prefix + [repo_name])
 
     @property
     def _ui(self):
@@ -1190,7 +1118,7 @@
         Creates an db based ui object for this repository
         """
         from kallithea.lib.utils import make_ui
-        return make_ui(clear_session=False)
+        return make_ui()
 
     @classmethod
     def is_valid(cls, repo_name):
@@ -1202,7 +1130,7 @@
         """
         from kallithea.lib.utils import is_valid_repo
 
-        return is_valid_repo(repo_name, cls.base_path())
+        return is_valid_repo(repo_name, kallithea.CONFIG['base_path'])
 
     def get_api_data(self, with_revision_names=False,
                            with_pullrequests=False):
@@ -1397,47 +1325,34 @@
 
     def set_invalidate(self):
         """
-        Mark caches of this repo as invalid.
+        Flush SA session caches of instances of on disk repo.
         """
-        CacheInvalidation.set_invalidate(self.repo_name)
+        try:
+            del self._scm_instance
+        except AttributeError:
+            pass
 
-    _scm_instance = None
+    _scm_instance = None  # caching inside lifetime of SA session
 
     @property
     def scm_instance(self):
         if self._scm_instance is None:
-            self._scm_instance = self.scm_instance_cached()
+            return self.scm_instance_no_cache()  # will populate self._scm_instance
         return self._scm_instance
 
-    def scm_instance_cached(self, valid_cache_keys=None):
-        @cache_region('long_term', 'scm_instance_cached')
-        def _c(repo_name): # repo_name is just for the cache key
-            log.debug('Creating new %s scm_instance and populating cache', repo_name)
-            return self.scm_instance_no_cache()
-        rn = self.repo_name
-
-        valid = CacheInvalidation.test_and_set_valid(rn, None, valid_cache_keys=valid_cache_keys)
-        if not valid:
-            log.debug('Cache for %s invalidated, getting new object', rn)
-            region_invalidate(_c, None, 'scm_instance_cached', rn)
-        else:
-            log.debug('Trying to get scm_instance of %s from cache', rn)
-        return _c(rn)
-
     def scm_instance_no_cache(self):
-        repo_full_path = safe_str(self.repo_full_path)
+        repo_full_path = self.repo_full_path
         alias = get_scm(repo_full_path)[0]
         log.debug('Creating instance of %s repository from %s',
                   alias, self.repo_full_path)
         backend = get_backend(alias)
 
         if alias == 'hg':
-            repo = backend(repo_full_path, create=False,
-                           baseui=self._ui)
+            self._scm_instance = backend(repo_full_path, create=False, baseui=self._ui)
         else:
-            repo = backend(repo_full_path, create=False)
+            self._scm_instance = backend(repo_full_path, create=False)
 
-        return repo
+        return self._scm_instance
 
     def __json__(self):
         return dict(
@@ -1476,7 +1391,7 @@
         q = super(RepoGroup, cls).query()
 
         if sorted:
-            q = q.order_by(func.lower(RepoGroup.group_name))
+            q = q.order_by(sqlalchemy.func.lower(RepoGroup.group_name))
 
         return q
 
@@ -1484,16 +1399,16 @@
         self.group_name = group_name
         self.parent_group = parent_group
 
-    def __unicode__(self):
-        return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
-                                      self.group_name)
+    def __repr__(self):
+        return "<%s %s: %s>" % (self.__class__.__name__,
+                                self.group_id, self.group_name)
 
     @classmethod
     def _generate_choice(cls, repo_group):
         """Return tuple with group_id and name as html literal"""
         from webhelpers2.html import literal
         if repo_group is None:
-            return (-1, u'-- %s --' % _('top level'))
+            return (-1, '-- %s --' % _('top level'))
         return repo_group.group_id, literal(cls.SEP.join(repo_group.full_path_splitted))
 
     @classmethod
@@ -1503,28 +1418,18 @@
                       key=lambda c: c[1].split(cls.SEP))
 
     @classmethod
-    def url_sep(cls):
-        return URL_SEP
-
-    @classmethod
     def guess_instance(cls, value):
         return super(RepoGroup, cls).guess_instance(value, RepoGroup.get_by_group_name)
 
     @classmethod
-    def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
+    def get_by_group_name(cls, group_name, case_insensitive=False):
         group_name = group_name.rstrip('/')
         if case_insensitive:
             gr = cls.query() \
-                .filter(func.lower(cls.group_name) == func.lower(group_name))
+                .filter(sqlalchemy.func.lower(cls.group_name) == sqlalchemy.func.lower(group_name))
         else:
             gr = cls.query() \
                 .filter(cls.group_name == group_name)
-        if cache:
-            gr = gr.options(FromCache(
-                            "sql_cache_short",
-                            "get_group_%s" % _hash_key(group_name)
-                            )
-            )
         return gr.scalar()
 
     @property
@@ -1544,7 +1449,7 @@
 
     @property
     def name(self):
-        return self.group_name.split(RepoGroup.url_sep())[-1]
+        return self.group_name.split(URL_SEP)[-1]
 
     @property
     def full_path(self):
@@ -1552,7 +1457,7 @@
 
     @property
     def full_path_splitted(self):
-        return self.group_name.split(RepoGroup.url_sep())
+        return self.group_name.split(URL_SEP)
 
     @property
     def repositories(self):
@@ -1607,7 +1512,7 @@
         """
         path_prefix = (self.parent_group.full_path_splitted if
                        self.parent_group else [])
-        return RepoGroup.url_sep().join(path_prefix + [group_name])
+        return URL_SEP.join(path_prefix + [group_name])
 
     def get_api_data(self):
         """
@@ -1731,8 +1636,8 @@
     permission_id = Column(Integer(), primary_key=True)
     permission_name = Column(String(255), nullable=False)
 
-    def __unicode__(self):
-        return u"<%s('%s:%s')>" % (
+    def __repr__(self):
+        return "<%s %s: %r>" % (
             self.__class__.__name__, self.permission_id, self.permission_name
         )
 
@@ -1746,27 +1651,27 @@
 
     @classmethod
     def get_default_perms(cls, default_user_id):
-        q = Session().query(UserRepoToPerm, Repository, cls) \
-         .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id)) \
-         .join((cls, UserRepoToPerm.permission_id == cls.permission_id)) \
+        q = Session().query(UserRepoToPerm) \
+         .options(joinedload(UserRepoToPerm.repository)) \
+         .options(joinedload(UserRepoToPerm.permission)) \
          .filter(UserRepoToPerm.user_id == default_user_id)
 
         return q.all()
 
     @classmethod
     def get_default_group_perms(cls, default_user_id):
-        q = Session().query(UserRepoGroupToPerm, RepoGroup, cls) \
-         .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id)) \
-         .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id)) \
+        q = Session().query(UserRepoGroupToPerm) \
+         .options(joinedload(UserRepoGroupToPerm.group)) \
+         .options(joinedload(UserRepoGroupToPerm.permission)) \
          .filter(UserRepoGroupToPerm.user_id == default_user_id)
 
         return q.all()
 
     @classmethod
     def get_default_user_group_perms(cls, default_user_id):
-        q = Session().query(UserUserGroupToPerm, UserGroup, cls) \
-         .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id)) \
-         .join((cls, UserUserGroupToPerm.permission_id == cls.permission_id)) \
+        q = Session().query(UserUserGroupToPerm) \
+         .options(joinedload(UserUserGroupToPerm.user_group)) \
+         .options(joinedload(UserUserGroupToPerm.permission)) \
          .filter(UserUserGroupToPerm.user_id == default_user_id)
 
         return q.all()
@@ -1797,8 +1702,9 @@
         Session().add(n)
         return n
 
-    def __unicode__(self):
-        return u'<%s => %s >' % (self.user, self.repository)
+    def __repr__(self):
+        return '<%s %s at %s: %s>' % (
+            self.__class__.__name__, self.user, self.repository, self.permission)
 
 
 class UserUserGroupToPerm(Base, BaseDbModel):
@@ -1826,8 +1732,9 @@
         Session().add(n)
         return n
 
-    def __unicode__(self):
-        return u'<%s => %s >' % (self.user, self.user_group)
+    def __repr__(self):
+        return '<%s %s at %s: %s>' % (
+            self.__class__.__name__, self.user, self.user_group, self.permission)
 
 
 class UserToPerm(Base, BaseDbModel):
@@ -1844,8 +1751,9 @@
     user = relationship('User')
     permission = relationship('Permission')
 
-    def __unicode__(self):
-        return u'<%s => %s >' % (self.user, self.permission)
+    def __repr__(self):
+        return '<%s %s: %s>' % (
+            self.__class__.__name__, self.user, self.permission)
 
 
 class UserGroupRepoToPerm(Base, BaseDbModel):
@@ -1873,8 +1781,9 @@
         Session().add(n)
         return n
 
-    def __unicode__(self):
-        return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
+    def __repr__(self):
+        return '<%s %s at %s: %s>' % (
+            self.__class__.__name__, self.users_group, self.repository, self.permission)
 
 
 class UserGroupUserGroupToPerm(Base, BaseDbModel):
@@ -1902,8 +1811,9 @@
         Session().add(n)
         return n
 
-    def __unicode__(self):
-        return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
+    def __repr__(self):
+        return '<%s %s at %s: %s>' % (
+            self.__class__.__name__, self.user_group, self.target_user_group, self.permission)
 
 
 class UserGroupToPerm(Base, BaseDbModel):
@@ -2006,137 +1916,13 @@
     user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
 
     follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
-    follows_repository = relationship('Repository', order_by=lambda: func.lower(Repository.repo_name))
+    follows_repository = relationship('Repository', order_by=lambda: sqlalchemy.func.lower(Repository.repo_name))
 
     @classmethod
     def get_repo_followers(cls, repo_id):
         return cls.query().filter(cls.follows_repository_id == repo_id)
 
 
-class CacheInvalidation(Base, BaseDbModel):
-    __tablename__ = 'cache_invalidation'
-    __table_args__ = (
-        Index('key_idx', 'cache_key'),
-        _table_args_default_dict,
-    )
-
-    # cache_id, not used
-    cache_id = Column(Integer(), primary_key=True)
-    # cache_key as created by _get_cache_key
-    cache_key = Column(Unicode(255), nullable=False, unique=True)
-    # cache_args is a repo_name
-    cache_args = Column(Unicode(255), nullable=False)
-    # instance sets cache_active True when it is caching, other instances set
-    # cache_active to False to indicate that this cache is invalid
-    cache_active = Column(Boolean(), nullable=False, default=False)
-
-    def __init__(self, cache_key, repo_name=''):
-        self.cache_key = cache_key
-        self.cache_args = repo_name
-        self.cache_active = False
-
-    def __unicode__(self):
-        return u"<%s('%s:%s[%s]')>" % (
-            self.__class__.__name__,
-            self.cache_id, self.cache_key, self.cache_active)
-
-    def _cache_key_partition(self):
-        prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
-        return prefix, repo_name, suffix
-
-    def get_prefix(self):
-        """
-        get prefix that might have been used in _get_cache_key to
-        generate self.cache_key. Only used for informational purposes
-        in repo_edit.html.
-        """
-        # prefix, repo_name, suffix
-        return self._cache_key_partition()[0]
-
-    def get_suffix(self):
-        """
-        get suffix that might have been used in _get_cache_key to
-        generate self.cache_key. Only used for informational purposes
-        in repo_edit.html.
-        """
-        # prefix, repo_name, suffix
-        return self._cache_key_partition()[2]
-
-    @classmethod
-    def clear_cache(cls):
-        """
-        Delete all cache keys from database.
-        Should only be run when all instances are down and all entries thus stale.
-        """
-        cls.query().delete()
-        Session().commit()
-
-    @classmethod
-    def _get_cache_key(cls, key):
-        """
-        Wrapper for generating a unique cache key for this instance and "key".
-        key must / will start with a repo_name which will be stored in .cache_args .
-        """
-        prefix = kallithea.CONFIG.get('instance_id', '')
-        return "%s%s" % (prefix, key)
-
-    @classmethod
-    def set_invalidate(cls, repo_name):
-        """
-        Mark all caches of a repo as invalid in the database.
-        """
-        inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all()
-        log.debug('for repo %s got %s invalidation objects',
-                  safe_str(repo_name), inv_objs)
-
-        for inv_obj in inv_objs:
-            log.debug('marking %s key for invalidation based on repo_name=%s',
-                      inv_obj, safe_str(repo_name))
-            Session().delete(inv_obj)
-        Session().commit()
-
-    @classmethod
-    def test_and_set_valid(cls, repo_name, kind, valid_cache_keys=None):
-        """
-        Mark this cache key as active and currently cached.
-        Return True if the existing cache registration still was valid.
-        Return False to indicate that it had been invalidated and caches should be refreshed.
-        """
-
-        key = (repo_name + '_' + kind) if kind else repo_name
-        cache_key = cls._get_cache_key(key)
-
-        if valid_cache_keys and cache_key in valid_cache_keys:
-            return True
-
-        inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
-        if inv_obj is None:
-            inv_obj = cls(cache_key, repo_name)
-            Session().add(inv_obj)
-        elif inv_obj.cache_active:
-            return True
-        inv_obj.cache_active = True
-        try:
-            Session().commit()
-        except sqlalchemy.exc.IntegrityError:
-            log.error('commit of CacheInvalidation failed - retrying')
-            Session().rollback()
-            inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
-            if inv_obj is None:
-                log.error('failed to create CacheInvalidation entry')
-                # TODO: fail badly?
-            # else: TOCTOU - another thread added the key at the same time; no further action required
-        return False
-
-    @classmethod
-    def get_valid_cache_keys(cls):
-        """
-        Return opaque object with information of which caches still are valid
-        and can be used without checking for invalidation.
-        """
-        return set(inv_obj.cache_key for inv_obj in cls.query().filter(cls.cache_active).all())
-
-
 class ChangesetComment(Base, BaseDbModel):
     __tablename__ = 'changeset_comments'
     __table_args__ = (
@@ -2225,8 +2011,8 @@
     comment = relationship('ChangesetComment')
     pull_request = relationship('PullRequest')
 
-    def __unicode__(self):
-        return u"<%s('%s:%s')>" % (
+    def __repr__(self):
+        return "<%s %r by %r>" % (
             self.__class__.__name__,
             self.status, self.author
         )
@@ -2256,8 +2042,8 @@
     )
 
     # values for .status
-    STATUS_NEW = u'new'
-    STATUS_CLOSED = u'closed'
+    STATUS_NEW = 'new'
+    STATUS_CLOSED = 'closed'
 
     pull_request_id = Column(Integer(), primary_key=True)
     title = Column(Unicode(255), nullable=False)
@@ -2278,7 +2064,7 @@
 
     @revisions.setter
     def revisions(self, val):
-        self._revisions = safe_unicode(':'.join(val))
+        self._revisions = ':'.join(val)
 
     @property
     def org_ref_parts(self):
@@ -2426,9 +2212,9 @@
         _table_args_default_dict,
     )
 
-    GIST_PUBLIC = u'public'
-    GIST_PRIVATE = u'private'
-    DEFAULT_FILENAME = u'gistfile1.txt'
+    GIST_PUBLIC = 'public'
+    GIST_PRIVATE = 'private'
+    DEFAULT_FILENAME = 'gistfile1.txt'
 
     gist_id = Column(Integer(), primary_key=True)
     gist_access_id = Column(Unicode(250), nullable=False)
@@ -2446,7 +2232,9 @@
         return (self.gist_expires != -1) & (time.time() > self.gist_expires)
 
     def __repr__(self):
-        return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
+        return "<%s %s %s>" % (
+            self.__class__.__name__,
+            self.gist_type, self.gist_access_id)
 
     @classmethod
     def guess_instance(cls, value):
@@ -2471,19 +2259,6 @@
         import kallithea.lib.helpers as h
         return h.canonical_url('gist', gist_id=self.gist_access_id)
 
-    @classmethod
-    def base_path(cls):
-        """
-        Returns base path where all gists are stored
-
-        :param cls:
-        """
-        from kallithea.model.gist import GIST_STORE_LOC
-        q = Session().query(Ui) \
-            .filter(Ui.ui_key == URL_SEP)
-        q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
-        return os.path.join(q.one().ui_value, GIST_STORE_LOC)
-
     def get_api_data(self):
         """
         Common function for generating gist related data for API
@@ -2505,20 +2280,20 @@
         )
         data.update(self.get_api_data())
         return data
+
     ## SCM functions
 
     @property
     def scm_instance(self):
         from kallithea.lib.vcs import get_repo
-        base_path = self.base_path()
-        return get_repo(os.path.join(*map(safe_str,
-                                          [base_path, self.gist_access_id])))
+        from kallithea.model.gist import GIST_STORE_LOC
+        gist_base_path = os.path.join(kallithea.CONFIG['base_path'], GIST_STORE_LOC)
+        return get_repo(os.path.join(gist_base_path, self.gist_access_id))
 
 
 class UserSshKeys(Base, BaseDbModel):
     __tablename__ = 'user_ssh_keys'
     __table_args__ = (
-        Index('usk_public_key_idx', 'public_key'),
         Index('usk_fingerprint_idx', 'fingerprint'),
         UniqueConstraint('fingerprint'),
         _table_args_default_dict
@@ -2544,5 +2319,5 @@
         # the full public key is too long to be suitable as database key - instead,
         # use fingerprints similar to 'ssh-keygen -E sha256 -lf ~/.ssh/id_rsa.pub'
         self._public_key = full_key
-        enc_key = full_key.split(" ")[1]
-        self.fingerprint = hashlib.sha256(enc_key.decode('base64')).digest().encode('base64').replace('\n', '').rstrip('=')
+        enc_key = safe_bytes(full_key.split(" ")[1])
+        self.fingerprint = base64.b64encode(hashlib.sha256(base64.b64decode(enc_key)).digest()).replace(b'\n', b'').rstrip(b'=').decode()
--- a/kallithea/model/forms.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/model/forms.py	Mon May 04 19:24:04 2020 +0200
@@ -238,7 +238,7 @@
     return _PasswordResetConfirmationForm
 
 
-def RepoForm(edit=False, old_data=None, supported_backends=BACKENDS.keys(),
+def RepoForm(edit=False, old_data=None, supported_backends=BACKENDS,
              repo_groups=None, landing_revs=None):
     old_data = old_data or {}
     repo_groups = repo_groups or []
@@ -315,7 +315,7 @@
     return _RepoFieldForm
 
 
-def RepoForkForm(edit=False, old_data=None, supported_backends=BACKENDS.keys(),
+def RepoForkForm(edit=False, old_data=None, supported_backends=BACKENDS,
                  repo_groups=None, landing_revs=None):
     old_data = old_data or {}
     repo_groups = repo_groups or []
@@ -435,7 +435,7 @@
     return _CustomDefaultPermissionsForm
 
 
-def DefaultsForm(edit=False, old_data=None, supported_backends=BACKENDS.keys()):
+def DefaultsForm(edit=False, old_data=None, supported_backends=BACKENDS):
     class _DefaultsForm(formencode.Schema):
         allow_extra_fields = True
         filter_extra_fields = True
@@ -558,11 +558,11 @@
 
         filename = All(v.BasePath()(),
                        v.UnicodeString(strip=True, required=False))
-        description = v.UnicodeString(required=False, if_missing=u'')
+        description = v.UnicodeString(required=False, if_missing='')
         lifetime = v.OneOf(lifetime_options)
         mimetype = v.UnicodeString(required=False, if_missing=None)
         content = v.UnicodeString(required=True, not_empty=True)
-        public = v.UnicodeString(required=False, if_missing=u'')
-        private = v.UnicodeString(required=False, if_missing=u'')
+        public = v.UnicodeString(required=False, if_missing='')
+        private = v.UnicodeString(required=False, if_missing='')
 
     return _GistForm
--- a/kallithea/model/gist.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/model/gist.py	Mon May 04 19:24:04 2020 +0200
@@ -32,8 +32,8 @@
 import time
 import traceback
 
-from kallithea.lib.compat import json
-from kallithea.lib.utils2 import AttributeDict, safe_int, safe_unicode, time_to_datetime
+from kallithea.lib import ext_json
+from kallithea.lib.utils2 import AttributeDict, ascii_bytes, safe_int, time_to_datetime
 from kallithea.model.db import Gist, Session, User
 from kallithea.model.repo import RepoModel
 from kallithea.model.scm import ScmModel
@@ -45,12 +45,12 @@
 GIST_METADATA_FILE = '.rc_gist_metadata'
 
 
-def make_gist_id():
+def make_gist_access_id():
     """Generate a random, URL safe, almost certainly unique gist identifier."""
     rnd = random.SystemRandom() # use cryptographically secure system PRNG
     alphabet = '23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz'
     length = 20
-    return u''.join(rnd.choice(alphabet) for _ in xrange(length))
+    return ''.join(rnd.choice(alphabet) for _ in range(length))
 
 
 class GistModel(object):
@@ -82,7 +82,7 @@
             'gist_updated': time.time(),
         }
         with open(os.path.join(repo.path, '.hg', GIST_METADATA_FILE), 'wb') as f:
-            f.write(json.dumps(metadata))
+            f.write(ascii_bytes(ext_json.dumps(metadata)))
 
     def get_gist(self, gist):
         return Gist.guess_instance(gist)
@@ -108,7 +108,7 @@
         :param lifetime: in minutes, -1 == forever
         """
         owner = User.guess_instance(owner)
-        gist_id = make_gist_id()
+        gist_access_id = make_gist_access_id()
         lifetime = safe_int(lifetime, -1)
         gist_expires = time.time() + (lifetime * 60) if lifetime != -1 else -1
         log.debug('set GIST expiration date to: %s',
@@ -117,21 +117,19 @@
         # create the Database version
         gist = Gist()
         gist.gist_description = description
-        gist.gist_access_id = gist_id
+        gist.gist_access_id = gist_access_id
         gist.owner_id = owner.user_id
         gist.gist_expires = gist_expires
-        gist.gist_type = safe_unicode(gist_type)
+        gist.gist_type = gist_type
         Session().add(gist)
         Session().flush() # make database assign gist.gist_id
         if gist_type == Gist.GIST_PUBLIC:
             # use DB ID for easy to use GIST ID
-            gist_id = safe_unicode(gist.gist_id)
-            gist.gist_access_id = gist_id
+            gist.gist_access_id = str(gist.gist_id)
 
-        gist_repo_path = os.path.join(GIST_STORE_LOC, gist_id)
-        log.debug('Creating new %s GIST repo in %s', gist_type, gist_repo_path)
+        log.debug('Creating new %s GIST repo %s', gist_type, gist.gist_access_id)
         repo = RepoModel()._create_filesystem_repo(
-            repo_name=gist_id, repo_type='hg', repo_group=GIST_STORE_LOC)
+            repo_name=gist.gist_access_id, repo_type='hg', repo_group=GIST_STORE_LOC)
 
         processed_mapping = {}
         for filename in gist_mapping:
@@ -155,7 +153,7 @@
 
         # fake Kallithea Repository object
         fake_repo = AttributeDict(dict(
-            repo_name=gist_repo_path,
+            repo_name=os.path.join(GIST_STORE_LOC, gist.gist_access_id),
             scm_instance_no_cache=lambda: repo,
         ))
         ScmModel().create_nodes(
@@ -219,7 +217,7 @@
 
         # fake Kallithea Repository object
         fake_repo = AttributeDict(dict(
-            repo_name=gist_repo.path,
+            repo_name=os.path.join(GIST_STORE_LOC, gist.gist_access_id),
             scm_instance_no_cache=lambda: gist_repo,
         ))
 
--- a/kallithea/model/meta.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/model/meta.py	Mon May 04 19:24:04 2020 +0200
@@ -18,8 +18,6 @@
 from sqlalchemy.ext.declarative import declarative_base
 from sqlalchemy.orm import scoped_session, sessionmaker
 
-from kallithea.lib import caching_query
-
 
 # Beaker CacheManager.  A home base for cache configurations.
 cache_manager = cache.CacheManager()
@@ -29,18 +27,13 @@
 #
 # SQLAlchemy session manager.
 #
-session_factory = sessionmaker(
-    query_cls=caching_query.query_callable(cache_manager),
-    expire_on_commit=True)
+session_factory = sessionmaker(expire_on_commit=True)
 Session = scoped_session(session_factory)
 
 # The base class for declarative schemas in db.py
 # Engine is injected when model.__init__.init_model() sets meta.Base.metadata.bind
 Base = declarative_base()
 
-# to use cache use this in query:
-#   .options(FromCache("sqlalchemy_cache_type", "cachekey"))
-
 
 # Define naming conventions for foreign keys, primary keys, indexes,
 # check constraints, and unique constraints, respectively.
--- a/kallithea/model/notification.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/model/notification.py	Mon May 04 19:24:04 2020 +0200
@@ -33,9 +33,7 @@
 from tg import tmpl_context as c
 from tg.i18n import ugettext as _
 
-import kallithea
 from kallithea.lib import helpers as h
-from kallithea.lib.utils2 import safe_unicode
 from kallithea.model.db import User
 
 
@@ -44,12 +42,12 @@
 
 class NotificationModel(object):
 
-    TYPE_CHANGESET_COMMENT = u'cs_comment'
-    TYPE_MESSAGE = u'message'
-    TYPE_MENTION = u'mention' # not used
-    TYPE_REGISTRATION = u'registration'
-    TYPE_PULL_REQUEST = u'pull_request'
-    TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
+    TYPE_CHANGESET_COMMENT = 'cs_comment'
+    TYPE_MESSAGE = 'message'
+    TYPE_MENTION = 'mention' # not used
+    TYPE_REGISTRATION = 'registration'
+    TYPE_PULL_REQUEST = 'pull_request'
+    TYPE_PULL_REQUEST_COMMENT = 'pull_request_comment'
 
     def create(self, created_by, subject, body, recipients=None,
                type_=TYPE_MESSAGE, with_email=True,
@@ -134,7 +132,8 @@
         # send email with notification to all other participants
         for rec in rec_objs:
             tasks.send_email([rec.email], email_subject, email_txt_body,
-                     email_html_body, headers, author=created_by_obj)
+                     email_html_body, headers,
+                     from_name=created_by_obj.full_name_or_username)
 
 
 class EmailNotificationModel(object):
@@ -150,7 +149,6 @@
 
     def __init__(self):
         super(EmailNotificationModel, self).__init__()
-        self._template_root = kallithea.CONFIG['paths']['templates'][0]
         self._tmpl_lookup = app_globals.mako_lookup
         self.email_types = {
             self.TYPE_CHANGESET_COMMENT: 'changeset_comment',
@@ -178,14 +176,21 @@
         try:
             subj = tmpl % kwargs
         except KeyError as e:
-            log.error('error generating email subject for %r from %s: %s', type_, ','.join(self._subj_map.keys()), e)
+            log.error('error generating email subject for %r from %s: %s', type_, ', '.join(self._subj_map), e)
             raise
-        l = [safe_unicode(x) for x in [kwargs.get('status_change'), kwargs.get('closing_pr') and _('Closing')] if x]
-        if l:
+        # gmail doesn't do proper threading but will ignore leading square
+        # bracket content ... so that is where we put status info
+        bracket_tags = []
+        status_change = kwargs.get('status_change')
+        if status_change:
+            bracket_tags.append(str(status_change))  # apply str to evaluate LazyString before .join
+        if kwargs.get('closing_pr'):
+            bracket_tags.append(_('Closing'))
+        if bracket_tags:
             if subj.startswith('['):
-                subj = '[' + ', '.join(l) + ': ' + subj[1:]
+                subj = '[' + ', '.join(bracket_tags) + ': ' + subj[1:]
             else:
-                subj = '[' + ', '.join(l) + '] ' + subj
+                subj = '[' + ', '.join(bracket_tags) + '] ' + subj
         return subj
 
     def get_email_tmpl(self, type_, content_type, **kwargs):
--- a/kallithea/model/permission.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/model/permission.py	Mon May 04 19:24:04 2020 +0200
@@ -73,10 +73,8 @@
             return '.'.join(perm_name.split('.')[:1])
 
         perms = UserToPerm.query().filter(UserToPerm.user == user).all()
-        defined_perms_groups = map(_get_group,
-                                (x.permission.permission_name for x in perms))
+        defined_perms_groups = set(_get_group(x.permission.permission_name) for x in perms)
         log.debug('GOT ALREADY DEFINED:%s', perms)
-        DEFAULT_PERMS = Permission.DEFAULT_USER_PERMISSIONS
 
         if force:
             for perm in perms:
@@ -85,7 +83,7 @@
             defined_perms_groups = []
         # For every default permission that needs to be created, we check if
         # its group is already defined. If it's not, we create default permission.
-        for perm_name in DEFAULT_PERMS:
+        for perm_name in Permission.DEFAULT_USER_PERMISSIONS:
             gr = _get_group(perm_name)
             if gr not in defined_perms_groups:
                 log.debug('GR:%s not found, creating permission %s',
--- a/kallithea/model/pull_request.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/model/pull_request.py	Mon May 04 19:24:04 2020 +0200
@@ -33,7 +33,7 @@
 from tg.i18n import ugettext as _
 
 from kallithea.lib import helpers as h
-from kallithea.lib.utils2 import extract_mentioned_users, safe_str, safe_unicode
+from kallithea.lib.utils2 import ascii_bytes, extract_mentioned_users
 from kallithea.model.db import ChangesetStatus, PullRequest, PullRequestReviewer, User
 from kallithea.model.meta import Session
 from kallithea.model.notification import NotificationModel
@@ -68,14 +68,12 @@
         threading = ['%s-pr-%s@%s' % (pr.other_repo.repo_name,
                                       pr.pull_request_id,
                                       h.canonical_hostname())]
-        subject = safe_unicode(
-            h.link_to(
-              _('%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s') %
+        subject = h.link_to(
+            _('%(user)s wants you to review pull request %(pr_nice_id)s: %(pr_title)s') %
                 {'user': user.username,
                  'pr_title': pr.title,
                  'pr_nice_id': pr.nice_id()},
-                pr_url)
-            )
+            pr_url)
         body = pr.description
         _org_ref_type, org_ref_name, _org_rev = pr.org_ref.split(':')
         _other_ref_type, other_ref_name, _other_rev = pr.other_ref.split(':')
@@ -261,13 +259,13 @@
 
         if self.org_repo.scm_instance.alias == 'git':
             # create a ref under refs/pull/ so that commits don't get garbage-collected
-            self.org_repo.scm_instance._repo["refs/pull/%d/head" % pr.pull_request_id] = safe_str(self.org_rev)
+            self.org_repo.scm_instance._repo[b"refs/pull/%d/head" % pr.pull_request_id] = ascii_bytes(self.org_rev)
 
         # reset state to under-review
         from kallithea.model.changeset_status import ChangesetStatusModel
         from kallithea.model.comment import ChangesetCommentsModel
         comment = ChangesetCommentsModel().create(
-            text=u'',
+            text='',
             repo=self.org_repo,
             author=created_by,
             pull_request=pr,
@@ -362,11 +360,11 @@
             infos.append(_('No changes found on %s %s since previous iteration.') % (org_ref_type, org_ref_name))
             # TODO: fail?
 
-        try:
-            title, old_v = re.match(r'(.*)\(v(\d+)\)\s*$', title).groups()
-            v = int(old_v) + 1
-        except (AttributeError, ValueError):
-            v = 2
+        v = 2
+        m = re.match(r'(.*)\(v(\d+)\)\s*$', title)
+        if m is not None:
+            title = m.group(1)
+            v = int(m.group(2)) + 1
         self.create_action.title = '%s (v%s)' % (title.strip(), v)
 
         # using a mail-like separator, insert new iteration info in description with latest first
--- a/kallithea/model/repo.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/model/repo.py	Mon May 04 19:24:04 2020 +0200
@@ -35,14 +35,13 @@
 import kallithea.lib.utils2
 from kallithea.lib import helpers as h
 from kallithea.lib.auth import HasRepoPermissionLevel, HasUserGroupPermissionLevel
-from kallithea.lib.caching_query import FromCache
 from kallithea.lib.exceptions import AttachedForksError
 from kallithea.lib.hooks import log_delete_repository
 from kallithea.lib.utils import is_valid_repo_uri, make_ui
-from kallithea.lib.utils2 import LazyProperty, get_current_authuser, obfuscate_url_pw, remove_prefix, safe_str, safe_unicode
+from kallithea.lib.utils2 import LazyProperty, get_current_authuser, obfuscate_url_pw, remove_prefix
 from kallithea.lib.vcs.backends import get_backend
-from kallithea.model.db import (
-    Permission, RepoGroup, Repository, RepositoryField, Session, Statistics, Ui, User, UserGroup, UserGroupRepoGroupToPerm, UserGroupRepoToPerm, UserRepoGroupToPerm, UserRepoToPerm)
+from kallithea.model.db import (URL_SEP, Permission, RepoGroup, Repository, RepositoryField, Session, Statistics, Ui, User, UserGroup, UserGroupRepoGroupToPerm,
+                                UserGroupRepoToPerm, UserRepoGroupToPerm, UserRepoToPerm)
 
 
 log = logging.getLogger(__name__)
@@ -50,7 +49,7 @@
 
 class RepoModel(object):
 
-    URL_SEPARATOR = Repository.url_sep()
+    URL_SEPARATOR = URL_SEP
 
     def _create_default_perms(self, repository, private):
         # create default permission
@@ -81,25 +80,17 @@
         q = Ui.query().filter(Ui.ui_key == '/').one()
         return q.ui_value
 
-    def get(self, repo_id, cache=False):
+    def get(self, repo_id):
         repo = Repository.query() \
             .filter(Repository.repo_id == repo_id)
-
-        if cache:
-            repo = repo.options(FromCache("sql_cache_short",
-                                          "get_repo_%s" % repo_id))
         return repo.scalar()
 
     def get_repo(self, repository):
         return Repository.guess_instance(repository)
 
-    def get_by_repo_name(self, repo_name, cache=False):
+    def get_by_repo_name(self, repo_name):
         repo = Repository.query() \
             .filter(Repository.repo_name == repo_name)
-
-        if cache:
-            repo = repo.options(FromCache("sql_cache_short",
-                                          "get_repo_%s" % repo_name))
         return repo.scalar()
 
     def get_all_user_repos(self, user):
@@ -109,17 +100,15 @@
         :param user:
         """
         from kallithea.lib.auth import AuthUser
-        user = User.guess_instance(user)
-        repos = AuthUser(dbuser=user).permissions['repositories']
-        access_check = lambda r: r[1] in ['repository.read',
-                                          'repository.write',
-                                          'repository.admin']
-        repos = [x[0] for x in filter(access_check, repos.items())]
+        auth_user = AuthUser(dbuser=User.guess_instance(user))
+        repos = [repo_name
+            for repo_name, perm in auth_user.permissions['repositories'].items()
+            if perm in ['repository.read', 'repository.write', 'repository.admin']
+            ]
         return Repository.query().filter(Repository.repo_name.in_(repos))
 
     @classmethod
     def _render_datatable(cls, tmpl, *args, **kwargs):
-        import kallithea
         from tg import tmpl_context as c, request, app_globals
         from tg.i18n import ugettext as _
 
@@ -128,7 +117,7 @@
 
         tmpl = template.get_def(tmpl)
         kwargs.update(dict(_=_, h=h, c=c, request=request))
-        return tmpl.render(*args, **kwargs)
+        return tmpl.render_unicode(*args, **kwargs)
 
     def get_repos_as_dict(self, repos_list, repo_groups_list=None,
                           admin=False,
@@ -139,12 +128,16 @@
         admin: return data for action column.
         """
         _render = self._render_datatable
-        from tg import tmpl_context as c
+        from tg import tmpl_context as c, request
+        from kallithea.model.scm import ScmModel
 
         def repo_lnk(name, rtype, rstate, private, fork_of):
             return _render('repo_name', name, rtype, rstate, private, fork_of,
                            short_name=short_name)
 
+        def following(repo_id, is_following):
+            return _render('following', repo_id, is_following)
+
         def last_change(last_change):
             return _render("last_change", last_change)
 
@@ -189,6 +182,10 @@
                 "just_name": repo.just_name,
                 "name": repo_lnk(repo.repo_name, repo.repo_type,
                                  repo.repo_state, repo.private, repo.fork),
+                "following": following(
+                    repo.repo_id,
+                    ScmModel().is_following_repo(repo.repo_name, request.authuser.user_id),
+                ),
                 "last_change_iso": repo.last_db_change.isoformat(),
                 "last_change": last_change(repo.last_db_change),
                 "last_changeset": last_rev(repo.repo_name, cs_cache),
@@ -273,7 +270,7 @@
                 cur_repo.owner = User.get_by_username(kwargs['owner'])
 
             if 'repo_group' in kwargs:
-                assert kwargs['repo_group'] != u'-1', kwargs # RepoForm should have converted to None
+                assert kwargs['repo_group'] != '-1', kwargs # RepoForm should have converted to None
                 cur_repo.group = RepoGroup.get(kwargs['repo_group'])
                 cur_repo.repo_name = cur_repo.get_new_name(cur_repo.just_name)
             log.debug('Updating repo %s with params:%s', cur_repo, kwargs)
@@ -290,7 +287,7 @@
                 # clone_uri is modified - if given a value, check it is valid
                 if clone_uri != '':
                     # will raise exception on error
-                    is_valid_repo_uri(cur_repo.repo_type, clone_uri, make_ui(clear_session=False))
+                    is_valid_repo_uri(cur_repo.repo_type, clone_uri, make_ui())
                 cur_repo.clone_uri = clone_uri
 
             if 'repo_name' in kwargs:
@@ -306,8 +303,7 @@
                     repo=cur_repo, user='default', perm=EMPTY_PERM
                 )
                 # handle extra fields
-            for field in filter(lambda k: k.startswith(RepositoryField.PREFIX),
-                                kwargs):
+            for field in [k for k in kwargs if k.startswith(RepositoryField.PREFIX)]:
                 k = RepositoryField.un_prefix_key(field)
                 ex_field = RepositoryField.get_by_key_name(key=k, repo=cur_repo)
                 if ex_field:
@@ -339,13 +335,13 @@
         fork_of = Repository.guess_instance(fork_of)
         repo_group = RepoGroup.guess_instance(repo_group)
         try:
-            repo_name = safe_unicode(repo_name)
-            description = safe_unicode(description)
+            repo_name = repo_name
+            description = description
             # repo name is just a name of repository
             # while repo_name_full is a full qualified name that is combined
             # with name and path of group
             repo_name_full = repo_name
-            repo_name = repo_name.split(self.URL_SEPARATOR)[-1]
+            repo_name = repo_name.split(URL_SEP)[-1]
             if kallithea.lib.utils2.repo_name_slug(repo_name) != repo_name:
                 raise Exception('invalid repo name %s' % repo_name)
 
@@ -360,7 +356,7 @@
             new_repo.private = private
             if clone_uri:
                 # will raise exception on error
-                is_valid_repo_uri(repo_type, clone_uri, make_ui(clear_session=False))
+                is_valid_repo_uri(repo_type, clone_uri, make_ui())
             new_repo.clone_uri = clone_uri
             new_repo.landing_rev = landing_rev
 
@@ -643,8 +639,7 @@
             _paths = [repo_store_location]
         else:
             _paths = [self.repos_path, new_parent_path, repo_name]
-            # we need to make it str for mercurial
-        repo_path = os.path.join(*map(lambda x: safe_str(x), _paths))
+        repo_path = os.path.join(*_paths)
 
         # check if this path is not a repository
         if is_valid_repo(repo_path, self.repos_path):
@@ -655,13 +650,13 @@
             raise Exception('This path %s is a valid group' % repo_path)
 
         log.info('creating repo %s in %s from url: `%s`',
-            repo_name, safe_unicode(repo_path),
+            repo_name, repo_path,
             obfuscate_url_pw(clone_uri))
 
         backend = get_backend(repo_type)
 
         if repo_type == 'hg':
-            baseui = make_ui(clear_session=False)
+            baseui = make_ui()
             # patch and reset hooks section of UI config to not run any
             # hooks on creating remote repo
             for k, v in baseui.configitems('hooks'):
@@ -676,7 +671,7 @@
             raise Exception('Not supported repo_type %s expected hg/git' % repo_type)
 
         log.debug('Created repo %s with %s backend',
-                  safe_unicode(repo_name), safe_unicode(repo_type))
+                  repo_name, repo_type)
         return repo
 
     def _rename_filesystem_repo(self, old, new):
@@ -688,8 +683,8 @@
         """
         log.info('renaming repo from %s to %s', old, new)
 
-        old_path = safe_str(os.path.join(self.repos_path, old))
-        new_path = safe_str(os.path.join(self.repos_path, new))
+        old_path = os.path.join(self.repos_path, old)
+        new_path = os.path.join(self.repos_path, new)
         if os.path.isdir(new_path):
             raise Exception(
                 'Was trying to rename to already existing dir %s' % new_path
@@ -704,7 +699,7 @@
 
         :param repo: repo object
         """
-        rm_path = safe_str(os.path.join(self.repos_path, repo.repo_name))
+        rm_path = os.path.join(self.repos_path, repo.repo_name)
         log.info("Removing %s", rm_path)
 
         _now = datetime.now()
@@ -715,6 +710,6 @@
             args = repo.group.full_path_splitted + [_d]
             _d = os.path.join(*args)
         if os.path.exists(rm_path):
-            shutil.move(rm_path, safe_str(os.path.join(self.repos_path, _d)))
+            shutil.move(rm_path, os.path.join(self.repos_path, _d))
         else:
             log.error("Can't find repo to delete in %r", rm_path)
--- a/kallithea/model/repo_group.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/model/repo_group.py	Mon May 04 19:24:04 2020 +0200
@@ -34,6 +34,7 @@
 
 import kallithea.lib.utils2
 from kallithea.lib.utils2 import LazyProperty
+from kallithea.model import db
 from kallithea.model.db import Permission, RepoGroup, Repository, Session, Ui, User, UserGroup, UserGroupRepoGroupToPerm, UserRepoGroupToPerm
 
 
@@ -115,7 +116,7 @@
         :param group: instance of group from database
         :param force_delete: use shutil rmtree to remove all objects
         """
-        paths = group.full_path.split(RepoGroup.url_sep())
+        paths = group.full_path.split(db.URL_SEP)
         paths = os.sep.join(paths)
 
         rm_path = os.path.join(self.repos_path, paths)
@@ -288,7 +289,7 @@
                 repo_group.parent_group_id = repo_group_args['parent_group_id']
 
             if 'parent_group_id' in repo_group_args:
-                assert repo_group_args['parent_group_id'] != u'-1', repo_group_args  # RepoGroupForm should have converted to None
+                assert repo_group_args['parent_group_id'] != '-1', repo_group_args  # RepoGroupForm should have converted to None
                 repo_group.parent_group = RepoGroup.get(repo_group_args['parent_group_id'])
             if 'group_name' in repo_group_args:
                 group_name = repo_group_args['group_name']
--- a/kallithea/model/repo_permission.py	Mon May 04 18:25:09 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,100 +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/>.
-"""
-kallithea.model.repo_permission
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-repository permission model for Kallithea
-
-This file was forked by the Kallithea project in July 2014.
-Original author and date, and relevant copyright and licensing information is below:
-:created_on: Oct 1, 2011
-:author: nvinot, marcink
-"""
-
-import logging
-
-from kallithea.model.db import Permission, Repository, Session, User, UserGroupRepoToPerm, UserRepoToPerm
-
-
-log = logging.getLogger(__name__)
-
-
-class RepositoryPermissionModel(object):
-
-    def get_user_permission(self, repository, user):
-        repository = Repository.guess_instance(repository)
-        user = User.guess_instance(user)
-
-        return UserRepoToPerm.query() \
-                .filter(UserRepoToPerm.user == user) \
-                .filter(UserRepoToPerm.repository == repository) \
-                .scalar()
-
-    def update_user_permission(self, repository, user, permission):
-        permission = Permission.get_by_key(permission)
-        current = self.get_user_permission(repository, user)
-        if current:
-            if current.permission is not permission:
-                current.permission = permission
-        else:
-            p = UserRepoToPerm()
-            p.user = user
-            p.repository = repository
-            p.permission = permission
-            Session().add(p)
-
-    def delete_user_permission(self, repository, user):
-        current = self.get_user_permission(repository, user)
-        if current:
-            Session().delete(current)
-
-    def get_users_group_permission(self, repository, users_group):
-        return UserGroupRepoToPerm.query() \
-                .filter(UserGroupRepoToPerm.users_group == users_group) \
-                .filter(UserGroupRepoToPerm.repository == repository) \
-                .scalar()
-
-    def update_users_group_permission(self, repository, users_group,
-                                      permission):
-        permission = Permission.get_by_key(permission)
-        current = self.get_users_group_permission(repository, users_group)
-        if current:
-            if current.permission is not permission:
-                current.permission = permission
-        else:
-            p = UserGroupRepoToPerm()
-            p.users_group = users_group
-            p.repository = repository
-            p.permission = permission
-            Session().add(p)
-
-    def delete_users_group_permission(self, repository, users_group):
-        current = self.get_users_group_permission(repository, users_group)
-        if current:
-            Session().delete(current)
-
-    def update_or_delete_user_permission(self, repository, user, permission):
-        if permission:
-            self.update_user_permission(repository, user, permission)
-        else:
-            self.delete_user_permission(repository, user)
-
-    def update_or_delete_users_group_permission(self, repository, user_group,
-                                              permission):
-        if permission:
-            self.update_users_group_permission(repository, user_group,
-                                               permission)
-        else:
-            self.delete_users_group_permission(repository, user_group)
--- a/kallithea/model/scm.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/model/scm.py	Mon May 04 19:24:04 2020 +0200
@@ -25,7 +25,6 @@
 :license: GPLv3, see LICENSE.md for more details.
 """
 
-import cStringIO
 import logging
 import os
 import posixpath
@@ -42,7 +41,7 @@
 from kallithea.lib.exceptions import IMCCommitError, NonRelativePathError
 from kallithea.lib.hooks import process_pushed_raw_ids
 from kallithea.lib.utils import action_logger, get_filesystem_repos, make_ui
-from kallithea.lib.utils2 import safe_str, safe_unicode, set_hook_environment
+from kallithea.lib.utils2 import safe_bytes, set_hook_environment
 from kallithea.lib.vcs import get_backend
 from kallithea.lib.vcs.backends.base import EmptyChangeset
 from kallithea.lib.vcs.exceptions import RepositoryError
@@ -140,9 +139,11 @@
         cls = Repository
         if isinstance(instance, cls):
             return instance
-        elif isinstance(instance, int) or safe_str(instance).isdigit():
+        elif isinstance(instance, int):
             return cls.get(instance)
-        elif isinstance(instance, basestring):
+        elif isinstance(instance, str):
+            if instance.isdigit():
+                return cls.get(int(instance))
             return cls.get_by_repo_name(instance)
         elif instance is not None:
             raise Exception('given object must be int, basestr or Instance'
@@ -161,7 +162,8 @@
     def repo_scan(self, repos_path=None):
         """
         Listing of repositories in given path. This path should not be a
-        repository itself. Return a dictionary of repository objects
+        repository itself. Return a dictionary of repository objects mapping to
+        vcs instances.
 
         :param repos_path: path to directory containing repositories
         """
@@ -187,10 +189,10 @@
 
                     klass = get_backend(path[0])
 
-                    if path[0] == 'hg' and path[0] in BACKENDS.keys():
-                        repos[name] = klass(safe_str(path[1]), baseui=baseui)
+                    if path[0] == 'hg' and path[0] in BACKENDS:
+                        repos[name] = klass(path[1], baseui=baseui)
 
-                    if path[0] == 'git' and path[0] in BACKENDS.keys():
+                    if path[0] == 'git' and path[0] in BACKENDS:
                         repos[name] = klass(path[1])
             except OSError:
                 continue
@@ -394,17 +396,8 @@
         """
         user = User.guess_instance(user)
         IMC = self._get_IMC_module(repo.alias)
-
-        # decoding here will force that we have proper encoded values
-        # in any other case this will throw exceptions and deny commit
-        content = safe_str(content)
-        path = safe_str(f_path)
-        # message and author needs to be unicode
-        # proper backend should then translate that into required type
-        message = safe_unicode(message)
-        author = safe_unicode(author)
         imc = IMC(repo)
-        imc.change(FileNode(path, content, mode=cs.get_file_mode(f_path)))
+        imc.change(FileNode(f_path, content, mode=cs.get_file_mode(f_path)))
         try:
             tip = imc.commit(message=message, author=author,
                              parents=[cs], branch=cs.branch)
@@ -480,22 +473,14 @@
         for f_path in nodes:
             content = nodes[f_path]['content']
             f_path = self._sanitize_path(f_path)
-            f_path = safe_str(f_path)
-            # decoding here will force that we have proper encoded values
-            # in any other case this will throw exceptions and deny commit
-            if isinstance(content, (basestring,)):
-                content = safe_str(content)
-            elif isinstance(content, (file, cStringIO.OutputType,)):
+            if not isinstance(content, str) and not isinstance(content, bytes):
                 content = content.read()
-            else:
-                raise Exception('Content is of unrecognized type %s' % (
-                    type(content)
-                ))
             processed_nodes.append((f_path, content))
 
-        message = safe_unicode(message)
+        message = message
         committer = user.full_contact
-        author = safe_unicode(author) if author else committer
+        if not author:
+            author = committer
 
         IMC = self._get_IMC_module(scm_instance.alias)
         imc = IMC(scm_instance)
@@ -536,9 +521,10 @@
         user = User.guess_instance(user)
         scm_instance = repo.scm_instance_no_cache()
 
-        message = safe_unicode(message)
+        message = message
         committer = user.full_contact
-        author = safe_unicode(author) if author else committer
+        if not author:
+            author = committer
 
         imc_class = self._get_IMC_module(scm_instance.alias)
         imc = imc_class(scm_instance)
@@ -616,9 +602,10 @@
             content = nodes[f_path].get('content')
             processed_nodes.append((f_path, content))
 
-        message = safe_unicode(message)
+        message = message
         committer = user.full_contact
-        author = safe_unicode(author) if author else committer
+        if not author:
+            author = committer
 
         IMC = self._get_IMC_module(scm_instance.alias)
         imc = IMC(scm_instance)
@@ -672,19 +659,19 @@
 
         repo = repo.scm_instance
 
-        branches_group = ([(u'branch:%s' % k, k) for k, v in
-                           repo.branches.iteritems()], _("Branches"))
+        branches_group = ([('branch:%s' % k, k) for k, v in
+                           repo.branches.items()], _("Branches"))
         hist_l.append(branches_group)
         choices.extend([x[0] for x in branches_group[0]])
 
         if repo.alias == 'hg':
-            bookmarks_group = ([(u'book:%s' % k, k) for k, v in
-                                repo.bookmarks.iteritems()], _("Bookmarks"))
+            bookmarks_group = ([('book:%s' % k, k) for k, v in
+                                repo.bookmarks.items()], _("Bookmarks"))
             hist_l.append(bookmarks_group)
             choices.extend([x[0] for x in bookmarks_group[0]])
 
-        tags_group = ([(u'tag:%s' % k, k) for k, v in
-                       repo.tags.iteritems()], _("Tags"))
+        tags_group = ([('tag:%s' % k, k) for k, v in
+                       repo.tags.items()], _("Tags"))
         hist_l.append(tags_group)
         choices.extend([x[0] for x in tags_group[0]])
 
@@ -702,7 +689,7 @@
         # FIXME This may not work on Windows and may need a shell wrapper script.
         return (kallithea.CONFIG.get('git_hook_interpreter')
                 or sys.executable
-                or '/usr/bin/env python2')
+                or '/usr/bin/env python3')
 
     def install_git_hooks(self, repo, force_create=False):
         """
@@ -718,11 +705,11 @@
         if not os.path.isdir(loc):
             os.makedirs(loc)
 
-        tmpl_post = "#!%s\n" % self._get_git_hook_interpreter()
+        tmpl_post = b"#!%s\n" % safe_bytes(self._get_git_hook_interpreter())
         tmpl_post += pkg_resources.resource_string(
             'kallithea', os.path.join('config', 'post_receive_tmpl.py')
         )
-        tmpl_pre = "#!%s\n" % self._get_git_hook_interpreter()
+        tmpl_pre = b"#!%s\n" % safe_bytes(self._get_git_hook_interpreter())
         tmpl_pre += pkg_resources.resource_string(
             'kallithea', os.path.join('config', 'pre_receive_tmpl.py')
         )
@@ -736,12 +723,11 @@
                 log.debug('hook exists, checking if it is from kallithea')
                 with open(_hook_file, 'rb') as f:
                     data = f.read()
-                    matches = re.compile(r'(?:%s)\s*=\s*(.*)'
-                                         % 'KALLITHEA_HOOK_VER').search(data)
+                    matches = re.search(br'^KALLITHEA_HOOK_VER\s*=\s*(.*)$', data, flags=re.MULTILINE)
                     if matches:
                         try:
                             ver = matches.groups()[0]
-                            log.debug('got %s it is kallithea', ver)
+                            log.debug('Found Kallithea hook - it has KALLITHEA_HOOK_VER %r', ver)
                             has_hook = True
                         except Exception:
                             log.error(traceback.format_exc())
@@ -753,9 +739,9 @@
                 log.debug('writing %s hook file !', h_type)
                 try:
                     with open(_hook_file, 'wb') as f:
-                        tmpl = tmpl.replace('_TMPL_', kallithea.__version__)
+                        tmpl = tmpl.replace(b'_TMPL_', safe_bytes(kallithea.__version__))
                         f.write(tmpl)
-                    os.chmod(_hook_file, 0755)
+                    os.chmod(_hook_file, 0o755)
                 except IOError as e:
                     log.error('error writing %s: %s', _hook_file, e)
             else:
--- a/kallithea/model/ssh_key.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/model/ssh_key.py	Mon May 04 19:24:04 2020 +0200
@@ -29,7 +29,8 @@
 from tg.i18n import ugettext as _
 
 from kallithea.lib import ssh
-from kallithea.lib.utils2 import safe_str, str2bool
+from kallithea.lib.utils2 import str2bool
+from kallithea.lib.vcs.exceptions import RepositoryError
 from kallithea.model.db import User, UserSshKeys
 from kallithea.model.meta import Session
 
@@ -37,7 +38,7 @@
 log = logging.getLogger(__name__)
 
 
-class SshKeyModelException(Exception):
+class SshKeyModelException(RepositoryError):
     """Exception raised by SshKeyModel methods to report errors"""
 
 
@@ -51,9 +52,9 @@
         Will raise SshKeyModelException on errors
         """
         try:
-            keytype, pub, comment = ssh.parse_pub_key(public_key)
+            keytype, _pub, comment = ssh.parse_pub_key(public_key)
         except ssh.SshKeyParseError as e:
-            raise SshKeyModelException(_('SSH key %r is invalid: %s') % (safe_str(public_key), e.message))
+            raise SshKeyModelException(_('SSH key %r is invalid: %s') % (public_key, e.args[0]))
         if not description.strip():
             description = comment.strip()
 
@@ -72,21 +73,19 @@
 
         return new_ssh_key
 
-    def delete(self, public_key, user=None):
+    def delete(self, fingerprint, user):
         """
-        Deletes given public_key, if user is set it also filters the object for
-        deletion by given user.
+        Deletes ssh key with given fingerprint for the given user.
         Will raise SshKeyModelException on errors
         """
-        ssh_key = UserSshKeys.query().filter(UserSshKeys._public_key == public_key)
+        ssh_key = UserSshKeys.query().filter(UserSshKeys.fingerprint == fingerprint)
 
-        if user:
-            user = User.guess_instance(user)
-            ssh_key = ssh_key.filter(UserSshKeys.user_id == user.user_id)
+        user = User.guess_instance(user)
+        ssh_key = ssh_key.filter(UserSshKeys.user_id == user.user_id)
 
         ssh_key = ssh_key.scalar()
         if ssh_key is None:
-            raise SshKeyModelException(_('SSH key %r not found') % safe_str(public_key))
+            raise SshKeyModelException(_('SSH key with fingerprint %r found') % fingerprint)
         Session().delete(ssh_key)
 
     def get_ssh_keys(self, user):
@@ -116,7 +115,7 @@
         # Now, test that the directory is or was created in a readable way by previous.
         if not (os.path.isdir(authorized_keys_dir) and
                 os.access(authorized_keys_dir, os.W_OK)):
-            raise Exception("Directory of authorized_keys cannot be written to so authorized_keys file %s cannot be written" % (authorized_keys))
+            raise SshKeyModelException("Directory of authorized_keys cannot be written to so authorized_keys file %s cannot be written" % (authorized_keys))
 
         # Make sure we don't overwrite a key file with important content
         if os.path.exists(authorized_keys):
@@ -127,15 +126,13 @@
                     elif ssh.SSH_OPTIONS in l and ' ssh-serve ' in l:
                         pass # Kallithea entries are ok to overwrite
                     else:
-                        raise Exception("Safety check failed, found %r in %s - please review and remove it" % (l.strip(), authorized_keys))
+                        raise SshKeyModelException("Safety check failed, found %r line in %s - please remove it if Kallithea should manage the file" % (l.strip(), authorized_keys))
 
         fh, tmp_authorized_keys = tempfile.mkstemp('.authorized_keys', dir=os.path.dirname(authorized_keys))
         with os.fdopen(fh, 'w') as f:
+            f.write("# WARNING: This .ssh/authorized_keys file is managed by Kallithea. Manual editing or adding new entries will make Kallithea back off.\n")
             for key in UserSshKeys.query().join(UserSshKeys.user).filter(User.active == True):
                 f.write(ssh.authorized_keys_line(kallithea_cli_path, config['__file__'], key))
         os.chmod(tmp_authorized_keys, stat.S_IRUSR | stat.S_IWUSR)
-        # This preliminary remove is needed for Windows, not for Unix.
-        # TODO In Python 3, the remove+rename sequence below should become os.replace.
-        if os.path.exists(authorized_keys):
-            os.remove(authorized_keys)
-        os.rename(tmp_authorized_keys, authorized_keys)
+        # Note: simple overwrite / rename isn't enough to replace the file on Windows
+        os.replace(tmp_authorized_keys, authorized_keys)
--- a/kallithea/model/user.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/model/user.py	Mon May 04 19:24:04 2020 +0200
@@ -36,9 +36,8 @@
 from tg import config
 from tg.i18n import ugettext as _
 
-from kallithea.lib.caching_query import FromCache
 from kallithea.lib.exceptions import DefaultUserException, UserOwnsReposException
-from kallithea.lib.utils2 import generate_api_key, get_current_authuser, safe_unicode
+from kallithea.lib.utils2 import generate_api_key, get_current_authuser
 from kallithea.model.db import Permission, User, UserEmailMap, UserIpMap, UserToPerm
 from kallithea.model.meta import Session
 
@@ -49,11 +48,8 @@
 class UserModel(object):
     password_reset_token_lifetime = 86400 # 24 hours
 
-    def get(self, user_id, cache=False):
+    def get(self, user_id):
         user = User.query()
-        if cache:
-            user = user.options(FromCache("sql_cache_short",
-                                          "get_user_%s" % user_id))
         return user.get(user_id)
 
     def get_user(self, user):
@@ -94,8 +90,8 @@
         log_create_user(new_user.get_dict(), cur_user)
         return new_user
 
-    def create_or_update(self, username, password, email, firstname=u'',
-                         lastname=u'', active=True, admin=False,
+    def create_or_update(self, username, password, email, firstname='',
+                         lastname='', active=True, admin=False,
                          extern_type=None, extern_name=None, cur_user=None):
         """
         Creates a new instance if not found, or updates current one
@@ -142,10 +138,8 @@
             new_user.admin = admin
             new_user.email = email
             new_user.active = active
-            new_user.extern_name = safe_unicode(extern_name) \
-                if extern_name else None
-            new_user.extern_type = safe_unicode(extern_type) \
-                if extern_type else None
+            new_user.extern_name = extern_name
+            new_user.extern_type = extern_type
             new_user.name = firstname
             new_user.lastname = lastname
 
@@ -185,7 +179,7 @@
         # notification to admins
         subject = _('New user registration')
         body = (
-            u'New user registration\n'
+            'New user registration\n'
             '---------------------\n'
             '- Username: {user.username}\n'
             '- Full Name: {user.full_name}\n'
@@ -205,7 +199,7 @@
     def update(self, user_id, form_data, skip_attrs=None):
         from kallithea.lib.auth import get_crypt_password
         skip_attrs = skip_attrs or []
-        user = self.get(user_id, cache=False)
+        user = self.get(user_id)
         if user.is_default_user:
             raise DefaultUserException(
                             _("You can't edit this user since it's "
@@ -310,8 +304,8 @@
         """
         app_secret = config.get('app_instance_uuid')
         return hmac.HMAC(
-            key=u'\0'.join([app_secret, user.password]).encode('utf-8'),
-            msg=u'\0'.join([session_id, str(user.user_id), user.email, str(timestamp)]).encode('utf-8'),
+            key='\0'.join([app_secret, user.password]).encode('utf-8'),
+            msg='\0'.join([session_id, str(user.user_id), user.email, str(timestamp)]).encode('utf-8'),
             digestmod=hashlib.sha1,
         ).hexdigest()
 
--- a/kallithea/model/user_group.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/model/user_group.py	Mon May 04 19:24:04 2020 +0200
@@ -28,8 +28,8 @@
 import traceback
 
 from kallithea.lib.exceptions import RepoGroupAssignmentError, UserGroupsAssignedException
-from kallithea.model.db import (
-    Permission, Session, User, UserGroup, UserGroupMember, UserGroupRepoToPerm, UserGroupToPerm, UserGroupUserGroupToPerm, UserUserGroupToPerm)
+from kallithea.model.db import (Permission, Session, User, UserGroup, UserGroupMember, UserGroupRepoToPerm, UserGroupToPerm, UserGroupUserGroupToPerm,
+                                UserUserGroupToPerm)
 
 
 log = logging.getLogger(__name__)
@@ -94,8 +94,8 @@
     def get_group(self, user_group):
         return UserGroup.guess_instance(user_group)
 
-    def get_by_name(self, name, cache=False, case_insensitive=False):
-        return UserGroup.get_by_group_name(name, cache=cache, case_insensitive=case_insensitive)
+    def get_by_name(self, name, case_insensitive=False):
+        return UserGroup.get_by_group_name(name, case_insensitive=case_insensitive)
 
     def create(self, name, description, owner, active=True, group_data=None):
         try:
@@ -126,7 +126,7 @@
                 if k == 'users_group_members':
                     members_list = []
                     if v:
-                        v = [v] if isinstance(v, basestring) else v
+                        v = [v] if isinstance(v, str) else v
                         for u_id in set(v):
                             member = UserGroupMember(user_group.users_group_id, u_id)
                             members_list.append(member)
@@ -367,7 +367,7 @@
         for gr in set(groups):
             existing_group = UserGroup.get_by_group_name(gr)
             if not existing_group:
-                desc = u'Automatically created from plugin:%s' % extern_type
+                desc = 'Automatically created from plugin:%s' % extern_type
                 # we use first admin account to set the owner of the group
                 existing_group = UserGroupModel().create(gr, desc, owner,
                                         group_data={'extern_type': extern_type})
--- a/kallithea/model/validators.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/model/validators.py	Mon May 04 19:24:04 2020 +0200
@@ -30,9 +30,10 @@
 from kallithea.config.routing import ADMIN_PREFIX
 from kallithea.lib.auth import HasPermissionAny, HasRepoGroupPermissionLevel
 from kallithea.lib.compat import OrderedSet
-from kallithea.lib.exceptions import LdapImportError
+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.model import db
 from kallithea.model.db import RepoGroup, Repository, User, UserGroup
 
 
@@ -186,11 +187,7 @@
             slug = repo_name_slug(group_name)
 
             # check for parent of self
-            parent_of_self = lambda: (
-                old_data['group_id'] == parent_group_id
-                if parent_group_id else False
-            )
-            if edit and parent_of_self():
+            if edit and parent_group_id and old_data['group_id'] == parent_group_id:
                 msg = self.message('parent_group_id', state)
                 raise formencode.Invalid(msg, value, state,
                     error_dict=dict(parent_group_id=msg)
@@ -235,7 +232,7 @@
 
         def _validate_python(self, value, state):
             try:
-                (value or '').decode('ascii')
+                (value or '').encode('ascii')
             except UnicodeError:
                 msg = self.message('invalid_password', state)
                 raise formencode.Invalid(msg, value, state,)
@@ -276,7 +273,7 @@
 def ValidAuth():
     class _validator(formencode.validators.FancyValidator):
         messages = {
-            'invalid_auth': _(u'Invalid username or password'),
+            'invalid_auth': _('Invalid username or password'),
         }
 
         def _validate_python(self, value, state):
@@ -329,7 +326,7 @@
                 # value needs to be aware of group name in order to check
                 # db key This is an actual just the name to store in the
                 # database
-                repo_name_full = group_path + RepoGroup.url_sep() + repo_name
+                repo_name_full = group_path + db.URL_SEP + repo_name
             else:
                 group_name = group_path = ''
                 repo_name_full = repo_name
@@ -412,9 +409,9 @@
 
             if url and url != value.get('clone_uri_hidden'):
                 try:
-                    is_valid_repo_uri(repo_type, url, make_ui(clear_session=False))
-                except Exception:
-                    log.exception('URL validation failed')
+                    is_valid_repo_uri(repo_type, url, make_ui())
+                except InvalidCloneUriException as e:
+                    log.warning('validation of clone URL %r failed: %s', url, e)
                     msg = self.message('clone_uri', state)
                     raise formencode.Invalid(msg, value, state,
                         error_dict=dict(clone_uri=msg)
@@ -544,7 +541,7 @@
 
             # CLEAN OUT ORG VALUE FROM NEW MEMBERS, and group them using
             new_perms_group = defaultdict(dict)
-            for k, v in value.copy().iteritems():
+            for k, v in value.copy().items():
                 if k.startswith('perm_new_member'):
                     del value[k]
                     _type, part = k.split('perm_new_member_')
@@ -556,26 +553,26 @@
                         new_perms_group[pos][_key] = v
 
             # fill new permissions in order of how they were added
-            for k in sorted(map(int, new_perms_group.keys())):
-                perm_dict = new_perms_group[str(k)]
+            for k in sorted(new_perms_group, key=lambda k: int(k)):
+                perm_dict = new_perms_group[k]
                 new_member = perm_dict.get('name')
                 new_perm = perm_dict.get('perm')
                 new_type = perm_dict.get('type')
                 if new_member and new_perm and new_type:
                     perms_new.add((new_member, new_perm, new_type))
 
-            for k, v in value.iteritems():
+            for k, v in value.items():
                 if k.startswith('u_perm_') or k.startswith('g_perm_'):
-                    member = k[7:]
+                    member_name = k[7:]
                     t = {'u': 'user',
                          'g': 'users_group'
                     }[k[0]]
-                    if member == User.DEFAULT_USER:
+                    if member_name == User.DEFAULT_USER_NAME:
                         if str2bool(value.get('repo_private')):
                             # set none for default when updating to
                             # private repo protects against form manipulation
                             v = EMPTY_PERM
-                    perms_update.add((member, v, t))
+                    perms_update.add((member_name, v, t))
 
             value['perms_updates'] = list(perms_update)
             value['perms_new'] = list(perms_new)
@@ -584,16 +581,16 @@
             for k, v, t in perms_new:
                 try:
                     if t == 'user':
-                        self.user_db = User.query() \
+                        _user_db = User.query() \
                             .filter(User.active == True) \
                             .filter(User.username == k).one()
                     if t == 'users_group':
-                        self.user_db = UserGroup.query() \
+                        _user_db = UserGroup.query() \
                             .filter(UserGroup.users_group_active == True) \
                             .filter(UserGroup.users_group_name == k).one()
 
-                except Exception:
-                    log.exception('Updated permission failed')
+                except Exception as e:
+                    log.warning('Error validating %s permission %s', t, k)
                     msg = self.message('perm_new_member_type', state)
                     raise formencode.Invalid(msg, value, state,
                         error_dict=dict(perm_new_member_name=msg)
@@ -782,7 +779,7 @@
 
         def _convert_to_python(self, value, state):
             # filter empty values
-            return filter(lambda s: s not in [None, ''], value)
+            return [s for s in value if s not in [None, '']]
 
         def _validate_python(self, value, state):
             from kallithea.lib import auth_modules
--- a/kallithea/public/fontello/demo.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/public/fontello/demo.html	Mon May 04 19:24:04 2020 +0200
@@ -276,7 +276,7 @@
     }
      </style>
     <link rel="stylesheet" href="css/animation.css"><!--[if IE 7]><link rel="stylesheet" href="css/kallithea-ie7.css"><![endif]-->
-    <script>
+    <script>'use strict';
       function toggleCodes(on) {
         var obj = document.getElementById('icons');
       
--- a/kallithea/public/js/base.js	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/public/js/base.js	Mon May 04 19:24:04 2020 +0200
@@ -171,12 +171,12 @@
             return output.join('');
         }
 
-        var str_format = function() {
+        function str_format() {
             if (!str_format.cache.hasOwnProperty(arguments[0])) {
                 str_format.cache[arguments[0]] = str_format.parse(arguments[0]);
             }
             return str_format.format.call(null, str_format.cache[arguments[0]], arguments);
-        };
+        }
 
         str_format.format = function(parse_tree, argv) {
             var cursor = 1, tree_length = parse_tree.length, node_type = '', arg, output = [], i, k, match, pad, pad_character, pad_length;
@@ -239,7 +239,7 @@
                 else if ((match = /^\x25{2}/.exec(_fmt)) !== null) {
                     parse_tree.push('%');
                 }
-                else if ((match = /^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(_fmt)) !== null) {
+                else if ((match = /^\x25(?:([1-9]\d*)\$|\(([^)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(_fmt)) !== null) {
                     if (match[2]) {
                         arg_names |= 1;
                         var field_list = [], replacement_field = match[2], field_match = [];
@@ -281,10 +281,6 @@
         return str_format;
     })();
 
-    var vsprintf = function(fmt, argv) {
-        argv.unshift(fmt);
-        return sprintf.apply(null, argv);
-    };
     return {
         'url': function(route_name, params) {
             var result = route_name;
@@ -335,25 +331,10 @@
 })();
 
 
-/* Invoke all functions in callbacks */
-var _run_callbacks = function(callbacks){
-    if (callbacks !== undefined){
-        var _l = callbacks.length;
-        for (var i=0;i<_l;i++){
-            var func = callbacks[i];
-            if(typeof(func)=='function'){
-                try{
-                    func();
-                }catch (err){};
-            }
-        }
-    }
-}
-
 /**
  * turns objects into GET query string
  */
-var _toQueryString = function(o) {
+function _toQueryString(o) {
     if(typeof o !== 'object') {
         return false;
     }
@@ -362,7 +343,7 @@
         _qs.push(encodeURIComponent(_p) + '=' + encodeURIComponent(o[_p]));
     }
     return _qs.join('&');
-};
+}
 
 /**
  * Load HTML into DOM using Ajax
@@ -386,18 +367,18 @@
                     success();
                 }
             })
-        .fail(function(jqXHR, textStatus, errorThrown) {
+        .fail(function(jqXHR, textStatus) {
                 if (textStatus == "abort")
                     return;
                 $target.html('<span class="bg-danger">ERROR: {0}</span>'.format(textStatus));
                 $target.css('opacity','1.0');
             })
         ;
-};
+}
 
-var ajaxGET = function(url, success, failure) {
+function ajaxGET(url, success, failure) {
     if(failure === undefined) {
-        failure = function(jqXHR, textStatus, errorThrown) {
+        failure = function(jqXHR, textStatus) {
                 if (textStatus != "abort")
                     alert("Ajax GET error: " + textStatus);
             };
@@ -405,21 +386,20 @@
     return $.ajax({url: url, headers: {'X-PARTIAL-XHR': '1'}, cache: false})
         .done(success)
         .fail(failure);
-};
+}
 
-var ajaxPOST = function(url, postData, success, failure) {
+function ajaxPOST(url, postData, success, failure) {
     postData['_session_csrf_secret_token'] = _session_csrf_secret_token;
-    var postData = _toQueryString(postData);
     if(failure === undefined) {
-        failure = function(jqXHR, textStatus, errorThrown) {
+        failure = function(jqXHR, textStatus) {
                 if (textStatus != "abort")
                     alert("Error posting to server: " + textStatus);
             };
     }
-    return $.ajax({url: url, data: postData, type: 'POST', headers: {'X-PARTIAL-XHR': '1'}, cache: false})
+    return $.ajax({url: url, data: _toQueryString(postData), type: 'POST', headers: {'X-PARTIAL-XHR': '1'}, cache: false})
         .done(success)
         .fail(failure);
-};
+}
 
 
 /**
@@ -427,45 +407,47 @@
  * the .show_more must have an id that is the the id of an element to hide prefixed with _
  * the parentnode will be displayed
  */
-var show_more_event = function(){
+function show_more_event(){
     $('.show_more').click(function(e){
         var el = e.currentTarget;
         $('#' + el.id.substring(1)).hide();
         $(el.parentNode).show();
     });
-};
+}
 
 
-var _onSuccessFollow = function(target){
+function _onSuccessFollow(target){
     var $target = $(target);
     var $f_cnt = $('#current_followers_count');
     if ($target.hasClass('follow')) {
         $target.removeClass('follow').addClass('following');
         $target.prop('title', _TM['Stop following this repository']);
         if ($f_cnt.html()) {
-            var cnt = Number($f_cnt.html())+1;
+            const cnt = Number($f_cnt.html())+1;
             $f_cnt.html(cnt);
         }
     } else {
         $target.removeClass('following').addClass('follow');
         $target.prop('title', _TM['Start following this repository']);
         if ($f_cnt.html()) {
-            var cnt = Number($f_cnt.html())-1;
+            const cnt = Number($f_cnt.html())-1;
             $f_cnt.html(cnt);
         }
     }
 }
 
-var toggleFollowingRepo = function(target, follows_repository_id){
-    var args = 'follows_repository_id=' + follows_repository_id;
-    args += '&amp;_session_csrf_secret_token=' + _session_csrf_secret_token;
-    $.post(TOGGLE_FOLLOW_URL, args, function(data){
+function toggleFollowingRepo(target, follows_repository_id){
+    var args = {
+        'follows_repository_id': follows_repository_id,
+        '_session_csrf_secret_token': _session_csrf_secret_token
+    }
+    $.post(TOGGLE_FOLLOW_URL, args, function(){
             _onSuccessFollow(target);
         });
     return false;
-};
+}
 
-var showRepoSize = function(target, repo_name){
+function showRepoSize(target, repo_name){
     var args = '_session_csrf_secret_token=' + _session_csrf_secret_token;
 
     if(!$("#" + target).hasClass('loaded')){
@@ -477,12 +459,12 @@
         });
     }
     return false;
-};
+}
 
 /**
  * load tooltips dynamically based on data attributes, used for .lazy-cs changeset links
  */
-var get_changeset_tooltip = function() {
+function get_changeset_tooltip() {
     var $target = $(this);
     var tooltip = $target.data('tooltip');
     if (!tooltip) {
@@ -499,12 +481,12 @@
         $target.data('tooltip', tooltip);
     }
     return tooltip;
-};
+}
 
 /**
  * activate tooltips and popups
  */
-var tooltip_activate = function(){
+function tooltip_activate(){
     function placement(p, e){
         if(e.getBoundingClientRect().top > 2*$(window).height()/3){
             return 'top';
@@ -529,63 +511,7 @@
             placement: placement
         });
     });
-};
-
-
-/**
- * Quick filter widget
- *
- * @param target: filter input target
- * @param nodes: list of nodes in html we want to filter.
- * @param display_element function that takes current node from nodes and
- *    does hide or show based on the node
- */
-var q_filter = (function() {
-    var _namespace = {};
-    var namespace = function (target) {
-        if (!(target in _namespace)) {
-            _namespace[target] = {};
-        }
-        return _namespace[target];
-    };
-    return function (target, $nodes, display_element) {
-        var $nodes = $nodes;
-        var $q_filter_field = $('#' + target);
-        var F = namespace(target);
-
-        $q_filter_field.keyup(function (e) {
-            clearTimeout(F.filterTimeout);
-            F.filterTimeout = setTimeout(F.updateFilter, 600);
-        });
-
-        F.filterTimeout = null;
-
-        F.updateFilter = function () {
-            // Reset timeout
-            F.filterTimeout = null;
-
-            var obsolete = [];
-
-            var req = $q_filter_field.val().toLowerCase();
-
-            var showing = 0;
-            $nodes.each(function () {
-                var n = this;
-                var target_element = display_element(n);
-                if (req && n.innerHTML.toLowerCase().indexOf(req) == -1) {
-                    $(target_element).hide();
-                }
-                else {
-                    $(target_element).show();
-                    showing += 1;
-                }
-            });
-
-            $('#repo_count').html(showing);
-            /* FIXME: don't hardcode */
-        }
-    }
-})();
+}
 
 
 /**
@@ -661,12 +587,13 @@
     var addlabel = TRANSLATION_MAP['Add Another Comment'];
     var $add = $('<div class="add-button-row"><span class="btn btn-default btn-xs add-button">{0}</span></div>'.format(addlabel));
     $comment_div.append($add);
-    $add.children('.add-button').click(function(e) {
+    $add.children('.add-button').click(function() {
         comment_div_state($comment_div, f_path, line_no, true);
     });
 }
 
 // append a comment form to $comment_div
+// Note: var AJAX_COMMENT_URL must have been defined before invoking this function
 function _comment_div_append_form($comment_div, f_path, line_no) {
     var $form_div = $('#comment-inline-form-template').children()
         .clone()
@@ -725,7 +652,7 @@
             'save_close': pr_close,
             'save_delete': pr_delete
         };
-        var success = function(json_data) {
+        function success(json_data) {
             if (pr_delete) {
                 location = json_data['location'];
             } else {
@@ -738,8 +665,8 @@
                     location.reload(true);
                 }
             }
-        };
-        var failure = function(x, s, e) {
+        }
+        function failure(x, s, e) {
             $preview.removeClass('submitting').addClass('failed');
             var $status = $preview.find('.comment-submission-status');
             $('<span>', {
@@ -764,12 +691,12 @@
                     comment_div_state($comment_div, f_path, line_no);
                 })
             ).appendTo($status);
-        };
+        }
         ajaxPOST(AJAX_COMMENT_URL, postData, success, failure);
     });
 
     // add event handler for hide/cancel buttons
-    $form.find('.hide-inline-form').click(function(e) {
+    $form.find('.hide-inline-form').click(function() {
         comment_div_state($comment_div, f_path, line_no);
     });
 
@@ -783,10 +710,11 @@
 }
 
 
+// Note: var AJAX_COMMENT_URL must have been defined before invoking this function
 function deleteComment(comment_id) {
     var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
     var postData = {};
-    var success = function(o) {
+    function success() {
         $('#comment-'+comment_id).remove();
         // Ignore that this might leave a stray Add button (or have a pending form with another comment) ...
     }
@@ -797,7 +725,7 @@
 /**
  * Double link comments
  */
-var linkInlineComments = function($firstlinks, $comments){
+function linkInlineComments($firstlinks, $comments){
     if ($comments.length > 0) {
         $firstlinks.html('<a href="#{0}">First comment</a>'.format($comments.prop('id')));
     }
@@ -805,7 +733,7 @@
         return;
     }
 
-    $comments.each(function(i, e){
+    $comments.each(function(i){
             var prev = '';
             if (i > 0){
                 var prev_anchor = $($comments.get(i-1)).prop('id');
@@ -823,13 +751,13 @@
 }
 
 /* activate files.html stuff */
-var fileBrowserListeners = function(node_list_url, url_base){
+function fileBrowserListeners(node_list_url, url_base){
     var $node_filter = $('#node_filter');
 
     var filterTimeout = null;
     var nodes = null;
 
-    var initFilter = function(){
+    function initFilter(){
         $('#node_filter_box_loading').show();
         $('#search_activate_id').hide();
         $('#add_node_id').hide();
@@ -850,7 +778,7 @@
         ;
     }
 
-    var updateFilter = function(e) {
+    function updateFilter(e) {
         return function(){
             // Reset timeout
             filterTimeout = null;
@@ -897,7 +825,7 @@
                 $('#tbody_filtered').hide();
             }
         }
-    };
+    }
 
     $('#filter_activate').click(function(){
             initFilter();
@@ -912,10 +840,10 @@
             clearTimeout(filterTimeout);
             filterTimeout = setTimeout(updateFilter(e),600);
         });
-};
+}
 
 
-var initCodeMirror = function(textarea_id, baseUrl, resetUrl){
+function initCodeMirror(textarea_id, baseUrl, resetUrl){
     var myCodeMirror = CodeMirror.fromTextArea($('#' + textarea_id)[0], {
             mode: "null",
             lineNumbers: true,
@@ -924,7 +852,7 @@
         });
     CodeMirror.modeURL = baseUrl + "/codemirror/mode/%N/%N.js";
 
-    $('#reset').click(function(e){
+    $('#reset').click(function(){
             window.location=resetUrl;
         });
 
@@ -941,14 +869,14 @@
         });
 
     return myCodeMirror
-};
+}
 
-var setCodeMirrorMode = function(codeMirrorInstance, mode) {
+function setCodeMirrorMode(codeMirrorInstance, mode) {
     CodeMirror.autoLoadMode(codeMirrorInstance, mode);
 }
 
 
-var _getIdentNode = function(n){
+function _getIdentNode(n){
     //iterate thrugh nodes until matching interesting node
 
     if (typeof n == 'undefined'){
@@ -961,11 +889,11 @@
     else{
         return _getIdentNode(n.parentNode);
     }
-};
+}
 
 /* generate links for multi line selects that can be shown by files.html page_highlights.
  * This is a mouseup handler for hlcode from CodeHtmlFormatter and pygmentize */
-var getSelectionLink = function(e) {
+function getSelectionLink() {
     //get selection from start/to nodes
     if (typeof window.getSelection != "undefined") {
         var s = window.getSelection();
@@ -973,8 +901,8 @@
         var from = _getIdentNode(s.anchorNode);
         var till = _getIdentNode(s.focusNode);
 
-        var f_int = parseInt(from.id.replace('L',''));
-        var t_int = parseInt(till.id.replace('L',''));
+        //var f_int = parseInt(from.id.replace('L',''));
+        //var t_int = parseInt(till.id.replace('L',''));
 
         var yoffset = 35;
         var ranges = [parseInt(from.id.replace('L','')), parseInt(till.id.replace('L',''))];
@@ -1002,53 +930,15 @@
             $hl_div.hide();
         }
     }
-};
+}
 
 /**
  * Autocomplete functionality
  */
 
-// Custom search function for the DataSource of users
-var autocompleteMatchUsers = function (sQuery, myUsers) {
-    // Case insensitive matching
-    var query = sQuery.toLowerCase();
-    var i = 0;
-    var l = myUsers.length;
-    var matches = [];
-
-    // Match against each name of each contact
-    for (; i < l; i++) {
-        var contact = myUsers[i];
-        if (((contact.fname+"").toLowerCase().indexOf(query) > -1) ||
-             ((contact.lname+"").toLowerCase().indexOf(query) > -1) ||
-             ((contact.nname) && ((contact.nname).toLowerCase().indexOf(query) > -1))) {
-            matches[matches.length] = contact;
-        }
-    }
-    return matches;
-};
-
-// Custom search function for the DataSource of userGroups
-var autocompleteMatchGroups = function (sQuery, myGroups) {
-    // Case insensitive matching
-    var query = sQuery.toLowerCase();
-    var i = 0;
-    var l = myGroups.length;
-    var matches = [];
-
-    // Match against each name of each group
-    for (; i < l; i++) {
-        var matched_group = myGroups[i];
-        if (matched_group.grname.toLowerCase().indexOf(query) > -1) {
-            matches[matches.length] = matched_group;
-        }
-    }
-    return matches;
-};
-
 // Highlight the snippet if it is found in the full text, while escaping any existing markup.
 // Snippet must be lowercased already.
-var autocompleteHighlightMatch = function (full, snippet) {
+function autocompleteHighlightMatch(full, snippet) {
     var matchindex = full.toLowerCase().indexOf(snippet);
     if (matchindex <0)
         return full.html_escape();
@@ -1057,10 +947,10 @@
         + full.substr(matchindex, snippet.length).html_escape()
         + '</span>'
         + full.substring(matchindex + snippet.length).html_escape();
-};
+}
 
 // Return html snippet for showing the provided gravatar url
-var gravatar = function(gravatar_lnk, size, cssclass) {
+function gravatar(gravatar_lnk, size, cssclass) {
     if (!gravatar_lnk) {
         return '';
     }
@@ -1072,7 +962,7 @@
             '></i>').format(size, gravatar_lnk, cssclass);
 }
 
-var autocompleteGravatar = function(res, gravatar_lnk, size, group) {
+function autocompleteGravatar(res, gravatar_lnk, size, group) {
     var elem;
     if (group !== undefined) {
         elem = '<i class="perm-gravatar-ac icon-users"></i>';
@@ -1083,7 +973,7 @@
 }
 
 // Custom formatter to highlight the matching letters and do HTML escaping
-var autocompleteFormatter = function (oResultData, sQuery, sResultMatch) {
+function autocompleteFormatter(oResultData, sQuery, sResultMatch) {
     var query;
     if (sQuery && sQuery.toLowerCase) // YAHOO AutoComplete
         query = sQuery.toLowerCase();
@@ -1113,9 +1003,9 @@
     }
 
     return '';
-};
+}
 
-var SimpleUserAutoComplete = function ($inputElement) {
+function SimpleUserAutoComplete($inputElement) {
     $inputElement.select2({
         formatInputTooShort: $inputElement.attr('placeholder'),
         initSelection : function (element, callback) {
@@ -1134,12 +1024,12 @@
         ajax: {
             url: pyroutes.url('users_and_groups_data'),
             dataType: 'json',
-            data: function(term, page){
+            data: function(term){
               return {
                 query: term
               };
             },
-            results: function (data, page){
+            results: function (data){
               return data;
             },
             cache: true
@@ -1150,7 +1040,7 @@
     });
 }
 
-var MembersAutoComplete = function ($inputElement, $typeElement) {
+function MembersAutoComplete($inputElement, $typeElement) {
 
     $inputElement.select2({
         placeholder: $inputElement.attr('placeholder'),
@@ -1158,13 +1048,13 @@
         ajax: {
             url: pyroutes.url('users_and_groups_data'),
             dataType: 'json',
-            data: function(term, page){
+            data: function(term){
               return {
                 query: term,
                 types: 'users,groups'
               };
             },
-            results: function (data, page){
+            results: function (data){
               return data;
             },
             cache: true
@@ -1178,7 +1068,7 @@
     });
 }
 
-var MentionsAutoComplete = function ($inputElement) {
+function MentionsAutoComplete($inputElement) {
   $inputElement.atwho({
     at: "@",
     callbacks: {
@@ -1194,7 +1084,7 @@
           }
         );
       },
-      sorter: function(query, items, searchKey) {
+      sorter: function(query, items) {
         return items;
       }
     },
@@ -1207,28 +1097,10 @@
     },
     insertTpl: "${atwho-at}${nname}"
   });
-};
-
-
-// Set caret at the given position in the input element
-function _setCaretPosition($inputElement, caretPos) {
-    $inputElement.each(function(){
-        if(this.createTextRange) { // IE
-            var range = this.createTextRange();
-            range.move('character', caretPos);
-            range.select();
-        }
-        else if(this.selectionStart) { // other recent browsers
-            this.focus();
-            this.setSelectionRange(caretPos, caretPos);
-        }
-        else // last resort - very old browser
-            this.focus();
-    });
 }
 
 
-var addReviewMember = function(id,fname,lname,nname,gravatar_link,gravatar_size){
+function addReviewMember(id,fname,lname,nname,gravatar_link,gravatar_size){
     var displayname = nname;
     if ((fname != "") && (lname != "")) {
         displayname = "{0} {1} ({2})".format(fname, lname, nname);
@@ -1265,7 +1137,7 @@
     }
 }
 
-var removeReviewMember = function(reviewer_id, repo_name, pull_request_id){
+function removeReviewMember(reviewer_id){
     var $li = $('#reviewer_{0}'.format(reviewer_id));
     $li.find('div div').css("text-decoration", "line-through");
     $li.find('input').prop('name', 'review_members_removed');
@@ -1273,7 +1145,7 @@
 }
 
 /* activate auto completion of users as PR reviewers */
-var PullRequestAutoComplete = function ($inputElement) {
+function PullRequestAutoComplete($inputElement) {
     $inputElement.select2(
     {
         placeholder: $inputElement.attr('placeholder'),
@@ -1281,12 +1153,12 @@
         ajax: {
             url: pyroutes.url('users_and_groups_data'),
             dataType: 'json',
-            data: function(term, page){
+            data: function(term){
               return {
                 query: term
               };
             },
-            results: function (data, page){
+            results: function (data){
               return data;
             },
             cache: true
@@ -1320,12 +1192,12 @@
 }
 
 function ajaxActionRevokePermission(url, obj_id, obj_type, field_id, extra_data) {
-    var success = function (o) {
+    function success() {
             $('#' + field_id).remove();
-        };
-    var failure = function (o) {
+        }
+    function failure(o) {
             alert(_TM['Failed to revoke permission'] + ": " + o.status);
-        };
+        }
     var query_params = {};
     // put extra data into POST
     if (extra_data !== undefined && (typeof extra_data === 'object')){
@@ -1344,11 +1216,11 @@
     }
 
     ajaxPOST(url, query_params, success, failure);
-};
+}
 
 /* Multi selectors */
 
-var MultiSelectWidget = function(selected_id, available_id, form_id){
+function MultiSelectWidget(selected_id, available_id, form_id){
     var $availableselect = $('#' + available_id);
     var $selectedselect = $('#' + selected_id);
 
@@ -1363,10 +1235,10 @@
             return false;
         }).remove();
 
-    $('#add_element').click(function(e){
+    $('#add_element').click(function(){
             $selectedselect.append($availableselect.children('option:selected'));
         });
-    $('#remove_element').click(function(e){
+    $('#remove_element').click(function(){
             $availableselect.append($selectedselect.children('option:selected'));
         });
 
@@ -1382,7 +1254,7 @@
  Branch Sorting callback for select2, modifying the filtered result so prefix
  matches come before matches in the line.
  **/
-var branchSort = function(results, container, query) {
+function branchSort(results, container, query) {
     if (query.term) {
         return results.sort(function (a, b) {
             // Put closed branches after open ones (a bit of a hack ...)
@@ -1416,9 +1288,9 @@
         });
     }
     return results;
-};
+}
 
-var prefixFirstSort = function(results, container, query) {
+function prefixFirstSort(results, container, query) {
     if (query.term) {
         return results.sort(function (a, b) {
             // if parent node, no sorting
@@ -1447,23 +1319,23 @@
         });
     }
     return results;
-};
+}
 
 /* Helper for jQuery DataTables */
 
-var updateRowCountCallback = function updateRowCountCallback($elem, onlyDisplayed) {
+function updateRowCountCallback($elem, onlyDisplayed) {
     return function drawCallback() {
         var info = this.api().page.info(),
             count = onlyDisplayed === true ? info.recordsDisplay : info.recordsTotal;
         $elem.html(count);
     }
-};
+}
 
 
 /**
  * activate changeset parent/child navigation links
  */
-var activate_parent_child_links = function(){
+function activate_parent_child_links(){
 
     $('.parent-child-link').on('click', function(e){
         var $this = $(this);
--- a/kallithea/public/js/codemirror_loadmode.js	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/public/js/codemirror_loadmode.js	Mon May 04 19:24:04 2020 +0200
@@ -1,3 +1,5 @@
+'use strict';
+
 (function() {
   var loading = {};
   function splitCallback(cont, n) {
@@ -8,13 +10,13 @@
     var deps = CodeMirror.modes[mode].dependencies;
     if (!deps) return cont();
     var missing = [];
-    for (var i = 0; i < deps.length; ++i) {
+    for (let i = 0; i < deps.length; ++i) {
       if (!CodeMirror.modes.hasOwnProperty(deps[i]))
         missing.push(deps[i]);
     }
     if (!missing.length) return cont();
     var split = splitCallback(cont, missing.length);
-    for (var i = 0; i < missing.length; ++i)
+    for (let i = 0; i < missing.length; ++i)
       CodeMirror.requireMode(missing[i], split);
   }
 
@@ -60,10 +62,11 @@
 
   CodeMirror.getFilenameAndExt = function(filename){
     var parts = filename.split('.');
+    var ext;
 
     if (parts.length > 1){
-        var ext = parts.pop();
-        var filename = parts.join("");
+        ext = parts.pop();
+        filename = parts.join("");
     }
     return {"filename": filename, "ext": ext};
   };
--- a/kallithea/public/js/graph.js	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/public/js/graph.js	Mon May 04 19:24:04 2020 +0200
@@ -1,3 +1,5 @@
+'use strict';
+
 // branch_renderer.js - Rendering of branch DAGs on the client side
 //
 // Copyright 2010 Marcin Kuzminski <marcin AT python-works DOT com>
@@ -32,7 +34,7 @@
 	if (!document.createElement("canvas").getContext)
 		this.canvas = window.G_vmlCanvasManager.initElement(this.canvas);
 	if (!this.canvas) { // canvas creation did for some reason fail - fail silently
-		this.render = function(data) {};
+		this.render = function() {};
 		return;
 	}
 	this.ctx = this.canvas.getContext('2d');
@@ -80,7 +82,7 @@
 		}
 
 		var lineCount = 1;
-		for (var i=0;i<data.length;i++) {
+		for (let i=0;i<data.length;i++) {
 			var in_l = data[i][1];
 			for (var j in in_l) {
 				var m = in_l[j][0];
@@ -93,7 +95,7 @@
 		var box_size = Math.min(18, (canvasWidth - edge_pad * 2) / lineCount);
 		var base_x = canvasWidth - edge_pad;
 
-		for (var i=0; i < data.length; ++i) {
+		for (let i=0; i < data.length; ++i) {
 			var row = document.getElementById(row_id_prefix+idx);
 			if (row == null) {
 				console.log("error: row "+row_id_prefix+idx+" not found");
@@ -102,15 +104,15 @@
 			var next = document.getElementById(row_id_prefix+(idx+1));
 			var extra = 0;
 
-			cur = data[i];
-			node = cur[0];
-			in_l = cur[1];
-			closing = cur[2];
-			obsolete_node = cur[3];
-			bumped_node = cur[4];
-			divergent_node = cur[5];
-			extinct_node = cur[6];
-			unstable_node = cur[7];
+			const cur = data[i];
+			const node = cur[0];
+			const in_l = cur[1];
+			const closing = cur[2];
+			const obsolete_node = cur[3];
+			//const bumped_node = cur[4];
+			//const divergent_node = cur[5];
+			//const extinct_node = cur[6];
+			const unstable_node = cur[7];
 
 			// center dots on the first element in a td (not necessarily the first one, but there must be one)
 			var firstincell = $(row).find('td>*:visible')[0];
@@ -118,21 +120,21 @@
 			var rowY = Math.floor(row.offsetTop + firstincell.offsetTop + firstincell.offsetHeight/2);
 			var nextY = Math.floor((next == null) ? rowY + row.offsetHeight/2 : next.offsetTop + nextFirstincell.offsetTop + nextFirstincell.offsetHeight/2);
 
-			for (var j in in_l) {
-				line = in_l[j];
-				start = line[0];
-				end = line[1];
-				color = line[2];
-				obsolete_line = line[3];
+			for (let j in in_l) {
+				const line = in_l[j];
+				const start = line[0];
+				const end = line[1];
+				const color = line[2];
+				const obsolete_line = line[3];
 
-				x = Math.floor(base_x - box_size * start);
+				const x = Math.floor(base_x - box_size * start);
 
 				// figure out if this is a dead-end;
 				// we want to fade away this line
 				var dead_end = true;
 				if (next != null) {
-					nextdata = data[i+1];
-					next_l = nextdata[1];
+					const nextdata = data[i+1];
+					const next_l = nextdata[1];
 					for (var k=0; k < next_l.length; ++k) {
 						if (next_l[k][0] == end) {
 							dead_end = false;
@@ -144,7 +146,7 @@
 				}
 
 				if (dead_end) {
-					var gradient = this.ctx.createLinearGradient(x,rowY,x,nextY);
+					let gradient = this.ctx.createLinearGradient(x,rowY,x,nextY);
 					gradient.addColorStop(0,this.calcColor(color, 0.0, 0.65));
 					gradient.addColorStop(1,this.calcColor(color, 1.0, 0.0));
 					this.ctx.strokeStyle = gradient;
@@ -155,7 +157,7 @@
 				// the merged color
 				else if (color != node[1] && start == node[0])
 				{
-					var gradient = this.ctx.createLinearGradient(x,rowY,x,nextY);
+					let gradient = this.ctx.createLinearGradient(x,rowY,x,nextY);
 					gradient.addColorStop(0,this.calcColor(node[1], 0.0, 0.65));
 					gradient.addColorStop(1,this.calcColor(color, 0.0, 0.65));
 					this.ctx.strokeStyle = gradient;
@@ -192,10 +194,10 @@
 				this.ctx.setLineDash([]); // reset the dashed line, if any
 			}
 
-			column = node[0];
-			color = node[1];
+			const column = node[0];
+			const color = node[1];
 
-			x = Math.floor(base_x - box_size * column);
+			const x = Math.floor(base_x - box_size * column);
 
 			this.setColor(color, 0.25, 0.75);
 			if(unstable_node)
@@ -203,7 +205,7 @@
 				this.ctx.fillStyle = 'rgb(255, 0, 0)';
 			}
 
-			r = this.dot_radius
+			let r = this.dot_radius
 			if (obsolete_node)
 			{
 				this.ctx.beginPath();
--- a/kallithea/public/js/mergely.js	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/public/js/mergely.js	Mon May 04 19:24:04 2020 +0200
@@ -699,7 +699,7 @@
 		// resize
 		if (this.settings.autoresize) {
 			var sz_timeout1 = null;
-			var sz = function(init) {
+			function sz(init) {
 				//self.em_height = null; //recalculate
 				if (self.settings.resize) self.settings.resize(init);
 				self.editor[self.id + '-lhs'].refresh();
@@ -854,7 +854,7 @@
 	_clear: function() {
 		var self = this, name, editor, fns, timer, i, change, l;
 
-		var clear_changes = function() {
+		function clear_changes() {
 			timer = new Mgly.Timer();
 			for (i = 0, l = editor.lineCount(); i < l; ++i) {
 				editor.removeLineClass(i, 'background');
--- a/kallithea/templates/about.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/about.html	Mon May 04 19:24:04 2020 +0200
@@ -24,12 +24,14 @@
   necessarily limited to the following:</p>
   <ul>
 
-  <li>Copyright &copy; 2012&ndash;2019, Mads Kiilerich</li>
+  <li>Copyright &copy; 2012&ndash;2020, Mads Kiilerich</li>
+  <li>Copyright &copy; 2014&ndash;2020, Thomas De Schampheleire</li>
+  <li>Copyright &copy; 2020, Dennis Fink</li>
   <li>Copyright &copy; 2012, 2014&ndash;2017, 2019, Andrej Shadura</li>
-  <li>Copyright &copy; 2014&ndash;2019, Thomas De Schampheleire</li>
   <li>Copyright &copy; 2015&ndash;2017, 2019, Étienne Gilli</li>
   <li>Copyright &copy; 2017&ndash;2019, Allan Nordhøy</li>
   <li>Copyright &copy; 2018&ndash;2019, ssantos</li>
+  <li>Copyright &copy; 2019, Adi Kriegisch</li>
   <li>Copyright &copy; 2019, Danni Randeris</li>
   <li>Copyright &copy; 2019, Edmund Wong</li>
   <li>Copyright &copy; 2019, Elizabeth Sherrock</li>
@@ -39,6 +41,7 @@
   <li>Copyright &copy; 2019, Mateusz Mendel</li>
   <li>Copyright &copy; 2019, Nathan</li>
   <li>Copyright &copy; 2019, Oleksandr Shtalinberg</li>
+  <li>Copyright &copy; 2019, Private</li>
   <li>Copyright &copy; 2019, THANOS SIOURDAKIS</li>
   <li>Copyright &copy; 2019, Wolfgang Scherer</li>
   <li>Copyright &copy; 2019, Христо Станев</li>
--- a/kallithea/templates/admin/admin.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/admin.html	Mon May 04 19:24:04 2020 +0200
@@ -27,7 +27,7 @@
     </div>
 </div>
 
-<script>
+<script>'use strict';
 $(document).ready(function() {
   $('#j_filter').click(function(){
     var $jfilter = $('#j_filter');
@@ -35,9 +35,9 @@
         $jfilter.val('');
     }
   });
-  var fix_j_filter_width = function(len){
+  function fix_j_filter_width(len){
       $('#j_filter').css('width', Math.max(80, len*6.50)+'px');
-  };
+  }
   $('#j_filter').keyup(function () {
     fix_j_filter_width($('#j_filter').val().length);
   });
--- a/kallithea/templates/admin/admin_log.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/admin_log.html	Mon May 04 19:24:04 2020 +0200
@@ -37,7 +37,7 @@
     %endfor
 </table>
 
-<script type="text/javascript">
+<script>'use strict';
   $(document).ready(function(){
     var $user_log = $('#user_log');
     $user_log.on('click','.pager_link',function(e){
--- a/kallithea/templates/admin/auth/auth_settings.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/auth/auth_settings.html	Mon May 04 19:24:04 2020 +0200
@@ -105,10 +105,10 @@
     </div>
 </div>
 
-<script>
+<script>'use strict';
     $('.toggle-plugin').click(function(e){
         var $auth_plugins_input = $('#auth_plugins');
-        var notEmpty = function(element, index, array) {
+        function notEmpty(element) {
             return (element != "");
         }
         var elems = $auth_plugins_input.val().split(',').filter(notEmpty);
--- a/kallithea/templates/admin/gists/edit.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/gists/edit.html	Mon May 04 19:24:04 2020 +0200
@@ -6,9 +6,9 @@
 </%block>
 
 <%block name="js_extra">
-  <script type="text/javascript" src="${h.url('/codemirror/lib/codemirror.js')}"></script>
-  <script type="text/javascript" src="${h.url('/js/codemirror_loadmode.js')}"></script>
-  <script type="text/javascript" src="${h.url('/codemirror/mode/meta.js')}"></script>
+  <script src="${h.url('/codemirror/lib/codemirror.js')}"></script>
+  <script src="${h.url('/js/codemirror_loadmode.js')}"></script>
+  <script src="${h.url('/codemirror/mode/meta.js')}"></script>
 </%block>
 <%block name="css_extra">
   <link rel="stylesheet" type="text/css" href="${h.url('/codemirror/lib/codemirror.css')}"/>
@@ -35,7 +35,7 @@
               ${(h.HTML(_('Gist was updated since you started editing. Copy your changes and click %(here)s to reload new version.'))
                              % {'here': h.link_to(_('here'),h.url('edit_gist', gist_id=c.gist.gist_access_id))})}
             </div>
-            <script>
+            <script>'use strict';
             if (typeof jQuery != 'undefined') {
                 $(".alert").alert();
             }
@@ -67,19 +67,19 @@
             % for cnt, file in enumerate(c.files):
                 <div id="body" class="panel panel-default form-inline">
                     <div class="panel-heading">
-                        <input type="hidden" value="${h.safe_unicode(file.path)}" name="org_files">
-                        <input class="form-control" id="filename_${h.FID('f',file.path)}" name="files" size="30" type="text" value="${h.safe_unicode(file.path)}">
+                        <input type="hidden" value="${file.path}" name="org_files">
+                        <input class="form-control" id="filename_${h.FID('f',file.path)}" name="files" size="30" type="text" value="${file.path}">
                         <select class="form-control" id="mimetype_${h.FID('f',file.path)}" name="mimetypes"></select>
                     </div>
                     <div class="panel-body no-padding">
                         <div id="editor_container">
-                            <textarea id="editor_${h.FID('f',file.path)}" name="contents" style="display:none">${file.content}</textarea>
+                            <textarea id="editor_${h.FID('f',file.path)}" name="contents" style="display:none">${safe_str(file.content)}</textarea>
                         </div>
                     </div>
                 </div>
 
                 ## dynamic edit box.
-                <script type="text/javascript">
+                <script>'use strict';
                     $(document).ready(function(){
                         var myCodeMirror = initCodeMirror(${h.js('editor_' + h.FID('f',file.path))}, ${h.jshtml(request.script_name)}, '');
 
@@ -117,7 +117,7 @@
                         });
 
                         // on type the new filename set mode
-                        $filename_input.keyup(function(e){
+                        $filename_input.keyup(function(){
                             var file_data = CodeMirror.getFilenameAndExt(this.value);
                             if(file_data['ext'] != null){
                                 var detected_mode = CodeMirror.findModeByExtension(file_data['ext']) || CodeMirror.findModeByMIME('text/plain');
@@ -146,7 +146,7 @@
             <a class="btn btn-default" href="${h.url('gist', gist_id=c.gist.gist_access_id)}">${_('Cancel')}</a>
             </div>
           ${h.end_form()}
-          <script>
+          <script>'use strict';
               $('#update').on('click', function(e){
                   e.preventDefault();
 
--- a/kallithea/templates/admin/gists/index.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/gists/index.html	Mon May 04 19:24:04 2020 +0200
@@ -61,7 +61,7 @@
             <div class="text-muted">${gist.gist_description}</div>
           </div>
         % endfor
-        ${c.gists_pager.pager(**request.GET.mixed())}
+        ${c.gists_pager.pager()}
       %else:
         <div>${_('There are no gists yet')}</div>
       %endif
--- a/kallithea/templates/admin/gists/new.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/gists/new.html	Mon May 04 19:24:04 2020 +0200
@@ -6,9 +6,9 @@
 </%block>
 
 <%block name="js_extra">
-  <script type="text/javascript" src="${h.url('/codemirror/lib/codemirror.js')}"></script>
-  <script type="text/javascript" src="${h.url('/js/codemirror_loadmode.js')}"></script>
-  <script type="text/javascript" src="${h.url('/codemirror/mode/meta.js')}"></script>
+  <script src="${h.url('/codemirror/lib/codemirror.js')}"></script>
+  <script src="${h.url('/js/codemirror_loadmode.js')}"></script>
+  <script src="${h.url('/codemirror/mode/meta.js')}"></script>
 </%block>
 <%block name="css_extra">
   <link rel="stylesheet" type="text/css" href="${h.url('/codemirror/lib/codemirror.css')}"/>
@@ -55,7 +55,7 @@
             ${h.reset('reset',_('Reset'),class_="btn btn-default btn-xs")}
             </div>
           ${h.end_form()}
-          <script type="text/javascript">
+          <script>'use strict';
             $(document).ready(function(){
                 var myCodeMirror = initCodeMirror('editor', ${h.jshtml(request.script_name)}, '');
 
@@ -93,7 +93,7 @@
                 });
 
                 // on type the new filename set mode
-                $filename_input.keyup(function(e){
+                $filename_input.keyup(function(){
                     var file_data = CodeMirror.getFilenameAndExt(this.value);
                     if(file_data['ext'] != null){
                         var detected_mode = CodeMirror.findModeByExtension(file_data['ext']) || CodeMirror.findModeByMIME('text/plain');
--- a/kallithea/templates/admin/gists/show.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/gists/show.html	Mon May 04 19:24:04 2020 +0200
@@ -76,10 +76,10 @@
               <div class="panel panel-default">
                 <div id="${h.FID('G', file.path)}" class="panel-heading clearfix">
                     <div class="pull-left">
-                      <b>${h.safe_unicode(file.path)}</b>
+                      <b>${file.path}</b>
                     </div>
                     <div class="pull-right">
-                      ${h.link_to(_('Show as raw'),h.url('formatted_gist_file', gist_id=c.gist.gist_access_id, format='raw', revision=file.changeset.raw_id, f_path=h.safe_unicode(file.path)),class_="btn btn-default btn-xs")}
+                      ${h.link_to(_('Show as raw'),h.url('formatted_gist_file', gist_id=c.gist.gist_access_id, format='raw', revision=file.changeset.raw_id, f_path=file.path),class_="btn btn-default btn-xs")}
                     </div>
                 </div>
                 <div class="panel-body no-padding">
--- a/kallithea/templates/admin/my_account/my_account_api_keys.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/my_account/my_account_api_keys.html	Mon May 04 19:24:04 2020 +0200
@@ -90,7 +90,7 @@
 ''')}</p>
 </div>
 
-<script>
+<script>'use strict';
     $(document).ready(function(){
         $("#lifetime").select2({
             'dropdownAutoWidth': true
--- a/kallithea/templates/admin/my_account/my_account_repos.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/my_account/my_account_repos.html	Mon May 04 19:24:04 2020 +0200
@@ -4,9 +4,9 @@
     <table class="table" id="datatable_list_wrap" width="100%"></table>
 </div>
 
-<script>
+<script>'use strict';
   var data = ${h.js(c.data)};
-  var myDataTable = $("#datatable_list_wrap").DataTable({
+  $("#datatable_list_wrap").DataTable({
         data: data.records,
         columns: [
             {data: "raw_name", "visible": false, searchable: false},
--- a/kallithea/templates/admin/my_account/my_account_ssh_keys.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/my_account/my_account_ssh_keys.html	Mon May 04 19:24:04 2020 +0200
@@ -23,7 +23,7 @@
             </td>
             <td>
                 ${h.form(url('my_account_ssh_keys_delete'))}
-                    ${h.hidden('del_public_key', ssh_key.public_key)}
+                    ${h.hidden('del_public_key_fingerprint', ssh_key.fingerprint)}
                     <button class="btn btn-danger btn-xs" type="submit"
                             onclick="return confirm('${_('Confirm to remove this SSH key: %s') % ssh_key.fingerprint}');">
                         <i class="icon-trashcan"></i>
--- a/kallithea/templates/admin/my_account/my_account_watched.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/my_account/my_account_watched.html	Mon May 04 19:24:04 2020 +0200
@@ -4,9 +4,9 @@
     <table class="table" id="datatable_list_wrap" width="100%"></table>
 </div>
 
-<script>
+<script>'use strict';
   var data = ${h.js(c.data)};
-  var myDataTable = $("#datatable_list_wrap").DataTable({
+  $("#datatable_list_wrap").DataTable({
         data: data.records,
         columns: [
             {data: "raw_name", "visible": false, searchable: false},
--- a/kallithea/templates/admin/repo_groups/repo_group_add.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/repo_groups/repo_group_add.html	Mon May 04 19:24:04 2020 +0200
@@ -61,9 +61,9 @@
     </div>
     ${h.end_form()}
 </div>
-<script>
+<script>'use strict';
     $(document).ready(function(){
-        var setCopyPermsOption = function(group_val){
+        function setCopyPermsOption(group_val){
             if(group_val != "-1"){
                 $('#copy_perms').show();
             }
--- a/kallithea/templates/admin/repo_groups/repo_group_edit_perms.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/repo_groups/repo_group_edit_perms.html	Mon May 04 19:24:04 2020 +0200
@@ -102,15 +102,15 @@
 </div>
 ${h.end_form()}
 
-<script type="text/javascript">
+<script>'use strict';
     function ajaxActionRevoke(obj_id, obj_type, field_id, obj_name) {
-        url = ${h.jshtml(h.url('edit_repo_group_perms_delete', group_name=c.repo_group.group_name))};
+        let url = ${h.jshtml(h.url('edit_repo_group_perms_delete', group_name=c.repo_group.group_name))};
         var revoke_msg = _TM['Confirm to revoke permission for {0}: {1} ?'].format(obj_type.replace('_', ' '), obj_name);
         if (confirm(revoke_msg)){
             var recursive = $('input[name=recursive]:checked').val();
             ajaxActionRevokePermission(url, obj_id, obj_type, field_id, {recursive:recursive});
         }
-    };
+    }
 
     $(document).ready(function () {
         if (!$('#perm_new_member_name').hasClass('error')) {
--- a/kallithea/templates/admin/repo_groups/repo_group_edit_settings.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/repo_groups/repo_group_edit_settings.html	Mon May 04 19:24:04 2020 +0200
@@ -41,7 +41,7 @@
 </div>
 ${h.end_form()}
 
-<script>
+<script>'use strict';
     $(document).ready(function(){
         $("#parent_group_id").select2({
             'dropdownAutoWidth': true
--- a/kallithea/templates/admin/repo_groups/repo_groups.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/repo_groups/repo_groups.html	Mon May 04 19:24:04 2020 +0200
@@ -30,9 +30,9 @@
         <table class="table" id="datatable_list_wrap" width="100%"></table>
     </div>
 </div>
-<script>
+<script>'use strict';
   var data = ${h.js(c.data)};
-  var myDataTable = $("#datatable_list_wrap").DataTable({
+  $("#datatable_list_wrap").DataTable({
         data: data.records,
         columns: [
             {data: "raw_name", visible: false, searchable: false},
--- a/kallithea/templates/admin/repos/repo_add_base.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/repos/repo_add_base.html	Mon May 04 19:24:04 2020 +0200
@@ -65,9 +65,9 @@
             </div>
         </div>
 </div>
-<script>
+<script>'use strict';
     $(document).ready(function(){
-        var setCopyPermsOption = function(group_val){
+        function setCopyPermsOption(group_val){
             if(group_val != "-1"){
                 $('#copy_perms').show();
             }
--- a/kallithea/templates/admin/repos/repo_creating.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/repos/repo_creating.html	Mon May 04 19:24:04 2020 +0200
@@ -42,7 +42,7 @@
     </div>
 </div>
 
-<script>
+<script>'use strict';
 (function worker() {
   $.ajax({
     url: ${h.js(h.url('repo_check_home', repo_name=c.repo_name, repo=c.repo, task_id=c.task_id))},
@@ -52,7 +52,7 @@
           window.location = ${h.js(h.url('summary_home', repo_name = c.repo))};
       }
     },
-    complete: function(resp, status) {
+    complete: function(resp) {
       if (resp.status == 200){
           // Schedule the next request when the current one's complete
           setTimeout(worker, 1000);
--- a/kallithea/templates/admin/repos/repo_edit.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/repos/repo_edit.html	Mon May 04 19:24:04 2020 +0200
@@ -33,9 +33,6 @@
           <li class="${'active' if c.active=='fields' else ''}">
               <a href="${h.url('edit_repo_fields', repo_name=c.repo_name)}">${_('Extra Fields')}</a>
           </li>
-          <li class="${'active' if c.active=='caches' else ''}">
-              <a href="${h.url('edit_repo_caches', repo_name=c.repo_name)}">${_('Caches')}</a>
-          </li>
           <li class="${'active' if c.active=='remote' else ''}">
               <a href="${h.url('edit_repo_remote', repo_name=c.repo_name)}">${_('Remote')}</a>
           </li>
--- a/kallithea/templates/admin/repos/repo_edit_advanced.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/repos/repo_edit_advanced.html	Mon May 04 19:24:04 2020 +0200
@@ -9,7 +9,7 @@
 </div>
 ${h.end_form()}
 
-<script>
+<script>'use strict';
     $(document).ready(function(){
         $("#id_fork_of").select2({
             'dropdownAutoWidth': true
--- a/kallithea/templates/admin/repos/repo_edit_caches.html	Mon May 04 18:25:09 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,27 +0,0 @@
-${h.form(url('update_repo_caches', repo_name=c.repo_name))}
-<div class="form">
-   <div>
-       ${h.submit('reset_cache_%s' % c.repo_info.repo_name,_('Invalidate Repository Cache'),class_="btn btn-default btn-sm")}
-      <div class="text-muted">
-        ${_('Manually invalidate cache for this repository. On first access, the repository will be cached again.')}
-      </div>
-      <div>
-        <h5>${_('List of Cached Values')}</h5>
-        <table class="table">
-          <tr>
-            <th>${_('Prefix')}</th>
-            <th>${_('Key')}</th>
-            <th>${_('Active')}</th>
-          </tr>
-          %for cache in c.repo_info.cache_keys:
-              <tr>
-                <td>${cache.get_prefix() or '-'}</td>
-                <td>${cache.cache_key}</td>
-                <td>${h.boolicon(cache.cache_active)}</td>
-              </tr>
-          %endfor
-        </table>
-      </div>
-   </div>
-</div>
-${h.end_form()}
--- a/kallithea/templates/admin/repos/repo_edit_permissions.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/repos/repo_edit_permissions.html	Mon May 04 19:24:04 2020 +0200
@@ -12,7 +12,7 @@
                     <td></td>
                 </tr>
                 ## USERS
-                %for r2p in sorted(c.repo_info.repo_to_perm, key=lambda x: x.user.username != 'default' and x.user.username):
+                %for r2p in sorted(c.repo_info.repo_to_perm, key=lambda x: '' if x.user.username == 'default' else x.user.username):
                     %if r2p.user.username =='default' and c.repo_info.private:
                         <tr>
                             <td colspan="4">
@@ -87,14 +87,14 @@
 </div>
 ${h.end_form()}
 
-<script type="text/javascript">
+<script>'use strict';
     function ajaxActionRevoke(obj_id, obj_type, field_id, obj_name) {
-        url = ${h.js(h.url('edit_repo_perms_revoke',repo_name=c.repo_name))};
+        let url = ${h.js(h.url('edit_repo_perms_revoke',repo_name=c.repo_name))};
         var revoke_msg = _TM['Confirm to revoke permission for {0}: {1} ?'].format(obj_type.replace('_', ' '), obj_name);
         if (confirm(revoke_msg)){
             ajaxActionRevokePermission(url, obj_id, obj_type, field_id);
         }
-    };
+    }
 
     $(document).ready(function () {
         if (!$('#perm_new_member_name').hasClass('error')) {
--- a/kallithea/templates/admin/repos/repo_edit_settings.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/repos/repo_edit_settings.html	Mon May 04 19:24:04 2020 +0200
@@ -103,7 +103,7 @@
     </div>
     ${h.end_form()}
 
-<script>
+<script>'use strict';
     $(document).ready(function(){
         $('#repo_landing_rev').select2({
             'dropdownAutoWidth': true
--- a/kallithea/templates/admin/repos/repos.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/repos/repos.html	Mon May 04 19:24:04 2020 +0200
@@ -29,9 +29,9 @@
     </div>
 
 </div>
-<script>
+<script>'use strict';
   var data = ${h.js(c.data)};
-  var myDataTable = $("#datatable_list_wrap").DataTable({
+  $("#datatable_list_wrap").DataTable({
         data: data.records,
         columns: [
             {data: "raw_name", visible: false, searchable: false},
--- a/kallithea/templates/admin/settings/settings_hooks.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/settings/settings_hooks.html	Mon May 04 19:24:04 2020 +0200
@@ -50,16 +50,16 @@
 ${h.end_form()}
 % endif
 
-<script type="text/javascript">
+<script>'use strict';
 function delete_hook(hook_id, field_id) {
     var sUrl = ${h.js(h.url('admin_settings_hooks_delete'))};
-    var success = function (o) {
+    function success() {
             $('#' + field_id).remove();
-        };
-    var failure = function (o) {
+        }
+    function failure() {
             alert(${h.js(_('Failed to remove hook'))});
-        };
+        }
     var postData = {'hook_id': hook_id};
     ajaxPOST(sUrl, postData, success, failure);
-};
+}
 </script>
--- a/kallithea/templates/admin/settings/settings_vcs.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/settings/settings_vcs.html	Mon May 04 19:24:04 2020 +0200
@@ -69,9 +69,9 @@
     </div>
     ${h.end_form()}
 
-    <script type="text/javascript">
+    <script>'use strict';
         $(document).ready(function(){
-            $('#path_unlock').on('click', function(e){
+            $('#path_unlock').on('click', function(){
                 $('#path_unlock_icon').removeClass('icon-lock');
                 $('#path_unlock_icon').addClass('icon-lock-open-alt');
                 $('#paths_root_path').removeAttr('readonly');
--- a/kallithea/templates/admin/user_groups/user_group_add.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/user_groups/user_group_add.html	Mon May 04 19:24:04 2020 +0200
@@ -52,7 +52,7 @@
     ${h.end_form()}
 </div>
 
-<script>
+<script>'use strict';
     $(document).ready(function(){
         $('#users_group_name').focus();
     });
--- a/kallithea/templates/admin/user_groups/user_group_edit_perms.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/user_groups/user_group_edit_perms.html	Mon May 04 19:24:04 2020 +0200
@@ -92,14 +92,14 @@
 </div>
 ${h.end_form()}
 
-<script type="text/javascript">
+<script>'use strict';
     function ajaxActionRevoke(obj_id, obj_type, field_id, obj_name) {
-        url = ${h.js(h.url('edit_user_group_perms_delete', id=c.user_group.users_group_id))};
+        let url = ${h.js(h.url('edit_user_group_perms_delete', id=c.user_group.users_group_id))};
         var revoke_msg = _TM['Confirm to revoke permission for {0}: {1} ?'].format(obj_type.replace('_', ' '), obj_name);
         if (confirm(revoke_msg)){
             ajaxActionRevokePermission(url, obj_id, obj_type, field_id);
         }
-    };
+    }
 
     $(document).ready(function () {
         if (!$('#perm_new_member_name').hasClass('error')) {
--- a/kallithea/templates/admin/user_groups/user_group_edit_settings.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/user_groups/user_group_edit_settings.html	Mon May 04 19:24:04 2020 +0200
@@ -48,6 +48,6 @@
                 </div>
     </div>
 ${h.end_form()}
-<script type="text/javascript">
+<script>'use strict';
   MultiSelectWidget('users_group_members','available_members','edit_users_group');
 </script>
--- a/kallithea/templates/admin/user_groups/user_groups.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/user_groups/user_groups.html	Mon May 04 19:24:04 2020 +0200
@@ -29,9 +29,9 @@
         <table class="table" id="datatable_list_wrap" width="100%"></table>
     </div>
 </div>
-<script>
+<script>'use strict';
     var data = ${h.js(c.data)};
-    var $dataTable = $("#datatable_list_wrap").DataTable({
+    $("#datatable_list_wrap").DataTable({
         data: data.records,
         columns: [
             {data: "raw_name", visible: false, searchable: false},
--- a/kallithea/templates/admin/users/user_add.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/users/user_add.html	Mon May 04 19:24:04 2020 +0200
@@ -84,7 +84,7 @@
     ${h.end_form()}
 </div>
 
-<script>
+<script>'use strict';
     $(document).ready(function(){
         $('#username').focus();
     });
--- a/kallithea/templates/admin/users/user_edit_api_keys.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/users/user_edit_api_keys.html	Mon May 04 19:24:04 2020 +0200
@@ -77,7 +77,7 @@
     ${h.end_form()}
 </div>
 
-<script>
+<script>'use strict';
     $(document).ready(function(){
         $("#lifetime").select2({
             'dropdownAutoWidth': true
--- a/kallithea/templates/admin/users/user_edit_ssh_keys.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/users/user_edit_ssh_keys.html	Mon May 04 19:24:04 2020 +0200
@@ -23,7 +23,7 @@
             </td>
             <td>
                 ${h.form(url('edit_user_ssh_keys_delete', id=c.user.user_id))}
-                    ${h.hidden('del_public_key', ssh_key.public_key)}
+                    ${h.hidden('del_public_key_fingerprint', ssh_key.fingerprint)}
                     <button class="btn btn-danger btn-xs" type="submit"
                             onclick="return confirm('${_('Confirm to remove this SSH key: %s') % ssh_key.fingerprint}');">
                         <i class="icon-trashcan"></i>
--- a/kallithea/templates/admin/users/users.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/admin/users/users.html	Mon May 04 19:24:04 2020 +0200
@@ -28,9 +28,9 @@
     </div>
 </div>
 
-<script>
+<script>'use strict';
     var data = ${h.js(c.data)};
-    var $dataTable = $("#datatable_list_wrap").DataTable({
+    $("#datatable_list_wrap").DataTable({
         data: data.records,
         columns: [
             {data: "gravatar", sortable: false, searchable: false},
--- a/kallithea/templates/base/base.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/base/base.html	Mon May 04 19:24:04 2020 +0200
@@ -23,7 +23,7 @@
             <a class="navbar-link" href="${h.url('kallithea_project_url')}" target="_blank">Kallithea</a>,
         %endif
         which is
-        <a class="navbar-link" href="${h.canonical_url('about')}#copyright">&copy; 2010&ndash;2019 by various authors &amp; licensed under GPLv3</a>.
+        <a class="navbar-link" href="${h.canonical_url('about')}#copyright">&copy; 2010&ndash;2020 by various authors &amp; licensed under GPLv3</a>.
         %if c.issues_url:
             &ndash; <a class="navbar-link" href="${c.issues_url}" target="_blank">${_('Support')}</a>
         %endif
@@ -163,12 +163,12 @@
               ## also it feels like a job for the controller
               %if request.authuser.username != 'default':
                   <li>
-                   <a href="#" class="${'following' if c.repository_following else 'follow'}" onclick="toggleFollowingRepo(this, ${c.db_repo.repo_id});">
+                   <a href="#" class="${'following' if c.repository_following else 'follow'}" onclick="return toggleFollowingRepo(this, ${c.db_repo.repo_id});">
                     <span class="show-follow"><i class="icon-heart-empty"></i>${_('Follow')}</span>
                     <span class="show-following"><i class="icon-heart"></i>${_('Unfollow')}</span>
                    </a>
                   </li>
-                  <li><a href="${h.url('repo_fork_home',repo_name=c.repo_name)}"><i class="icon-git-pull-request"></i>${_('Fork')}</a></li>
+                  <li><a href="${h.url('repo_fork_home',repo_name=c.repo_name)}"><i class="icon-fork"></i>${_('Fork')}</a></li>
                   <li><a href="${h.url('pullrequest_home',repo_name=c.repo_name)}"><i class="icon-git-pull-request"></i>${_('Create Pull Request')}</a></li>
               %endif
              </ul>
@@ -177,7 +177,7 @@
     </div>
     </div>
   </nav>
-  <script type="text/javascript">
+  <script>'use strict';
     $(document).ready(function() {
       var bcache = {};
 
@@ -192,7 +192,7 @@
           formatSelection: function(obj) {
               return obj.text.html_escape();
           },
-          formatNoMatches: function(term) {
+          formatNoMatches: function() {
               return ${h.jshtml(_('No matches found'))};
           },
           escapeMarkup: function(m) {
@@ -399,12 +399,12 @@
     </li>
   </ul>
 
-    <script type="text/javascript">
+    <script>'use strict';
         $(document).ready(function(){
             var visual_show_public_icon = ${h.js(c.visual.show_public_icon)};
             var cache = {}
             /*format the look of items in the list*/
-            var format = function(state){
+            function format(state){
                 if (!state.id){
                   return state.text.html_escape(); // optgroup
                 }
@@ -441,7 +441,7 @@
                 sortResults: prefixFirstSort,
                 formatResult: format,
                 formatSelection: format,
-                formatNoMatches: function(term){
+                formatNoMatches: function(){
                     return ${h.jshtml(_('No matches found'))};
                 },
                 containerCssClass: "repo-switcher",
@@ -527,7 +527,7 @@
         </div>
     </div>
 
-    <script type="text/javascript">
+    <script>'use strict';
       $(document).ready(function(){
           activate_parent_child_links();
       });
--- a/kallithea/templates/base/flash_msg.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/base/flash_msg.html	Mon May 04 19:24:04 2020 +0200
@@ -5,11 +5,11 @@
         % for message in messages:
             <div class="alert alert-dismissable ${alert_categories[message.category]}" role="alert">
               <button type="button" class="close" data-dismiss="alert" aria-hidden="true"><i class="icon-cancel-circled"></i></button>
-              ${message}
+              ${message.message|n}
             </div>
         % endfor
     % endif
-    <script>
+    <script>'use strict';
     if (typeof jQuery != 'undefined') {
         $(".alert").alert();
     }
--- a/kallithea/templates/base/perms_summary.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/base/perms_summary.html	Mon May 04 19:24:04 2020 +0200
@@ -5,7 +5,7 @@
 
 <%def name="perms_summary(permissions, show_all=False, actions=True)">
 <div id="perms">
-     %for section in sorted(permissions.keys()):
+     %for section in sorted(permissions):
         <div class="perms_section_head">
             <h4>${section.replace("_"," ").capitalize()}</h4>
             %if section != 'global':
@@ -97,9 +97,9 @@
         %endif
      %endfor
 </div>
-<script>
+<script>'use strict';
     $(document).ready(function(){
-        var show_empty = function(section){
+        function show_empty(section){
             var visible = $('.section_{0} tr.perm_row:visible'.format(section)).length;
             if(visible == 0){
                 $('#empty_{0}'.format(section)).show();
@@ -108,10 +108,10 @@
                 $('#empty_{0}'.format(section)).hide();
             }
         }
-        var update_show = function($checkbox){
+        function update_show($checkbox){
             var section = $checkbox.data('section');
 
-            var elems = $('.filter_' + section).each(function(el){
+            $('.filter_' + section).each(function(){
                 var perm_type = $checkbox.data('perm_type');
                 var checked = $checkbox.prop('checked');
                 if(checked){
--- a/kallithea/templates/base/root.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/base/root.html	Mon May 04 19:24:04 2020 +0200
@@ -21,7 +21,7 @@
         <%block name="css_extra"/>
 
         ## JAVASCRIPT ##
-        <script type="text/javascript">
+        <script>'use strict';
             ## JS translations map
             var TRANSLATION_MAP = {
                 'Cancel': ${h.jshtml(_("Cancel"))},
@@ -58,7 +58,7 @@
             };
             var _TM = TRANSLATION_MAP;
 
-            var TOGGLE_FOLLOW_URL  = ${h.js(h.url('toggle_following'))};
+            var TOGGLE_FOLLOW_URL = ${h.js(h.url('toggle_following'))};
 
             var REPO_NAME = "";
             %if hasattr(c, 'repo_name'):
@@ -67,17 +67,17 @@
 
             var _session_csrf_secret_token = ${h.js(h.session_csrf_secret_token())};
         </script>
-        <script type="text/javascript" src="${h.url('/js/jquery.min.js', ver=c.kallithea_version)}"></script>
-        <script type="text/javascript" src="${h.url('/js/jquery.dataTables.js', ver=c.kallithea_version)}"></script>
-        <script type="text/javascript" src="${h.url('/js/dataTables.bootstrap.js', ver=c.kallithea_version)}"></script>
-        <script type="text/javascript" src="${h.url('/js/bootstrap.js', ver=c.kallithea_version)}"></script>
-        <script type="text/javascript" src="${h.url('/js/select2.js', ver=c.kallithea_version)}"></script>
-        <script type="text/javascript" src="${h.url('/js/jquery.caret.min.js', ver=c.kallithea_version)}"></script>
-        <script type="text/javascript" src="${h.url('/js/jquery.atwho.min.js', ver=c.kallithea_version)}"></script>
-        <script type="text/javascript" src="${h.url('/js/base.js', ver=c.kallithea_version)}"></script>
+        <script src="${h.url('/js/jquery.min.js', ver=c.kallithea_version)}"></script>
+        <script src="${h.url('/js/jquery.dataTables.js', ver=c.kallithea_version)}"></script>
+        <script src="${h.url('/js/dataTables.bootstrap.js', ver=c.kallithea_version)}"></script>
+        <script src="${h.url('/js/bootstrap.js', ver=c.kallithea_version)}"></script>
+        <script src="${h.url('/js/select2.js', ver=c.kallithea_version)}"></script>
+        <script src="${h.url('/js/jquery.caret.min.js', ver=c.kallithea_version)}"></script>
+        <script src="${h.url('/js/jquery.atwho.min.js', ver=c.kallithea_version)}"></script>
+        <script src="${h.url('/js/base.js', ver=c.kallithea_version)}"></script>
         ## EXTRA FOR JS
         <%block name="js_extra"/>
-        <script type="text/javascript">
+        <script>'use strict';
             $(document).ready(function(){
               tooltip_activate();
               show_more_event();
--- a/kallithea/templates/changelog/changelog.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/changelog/changelog.html	Mon May 04 19:24:04 2020 +0200
@@ -80,8 +80,8 @@
 
                 ${c.cs_pagination.pager()}
 
-        <script type="text/javascript" src="${h.url('/js/graph.js', ver=c.kallithea_version)}"></script>
-        <script type="text/javascript">
+        <script src="${h.url('/js/graph.js', ver=c.kallithea_version)}"></script>
+        <script>'use strict';
             var jsdata = ${h.js(c.jsdata)};
             var graph = new BranchRenderer('graph_canvas', 'graph_content', 'chg_');
 
@@ -90,7 +90,7 @@
 
                 pyroutes.register('changeset_home', ${h.js(h.url('changeset_home', repo_name='%(repo_name)s', revision='%(revision)s'))}, ['repo_name', 'revision']);
 
-                var checkbox_checker = function(e) {
+                function checkbox_checker() {
                     var $checked_checkboxes = $checkboxes.filter(':checked');
                     var $singlerange = $('#singlerange');
 
@@ -163,7 +163,7 @@
                         $('#compare_fork').show();
                         $checkboxes.closest('tr').removeClass('out-of-range');
                     }
-                };
+                }
                 checkbox_checker();
                 $checkboxes.click(function() {
                     checkbox_checker();
@@ -171,7 +171,7 @@
                 });
                 $('#singlerange').click(checkbox_checker);
 
-                $('#rev_range_clear').click(function(e){
+                $('#rev_range_clear').click(function(){
                     $checkboxes.prop('checked', false);
                     checkbox_checker();
                     graph.render(jsdata);
--- a/kallithea/templates/changelog/changelog_table.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/changelog/changelog_table.html	Mon May 04 19:24:04 2020 +0200
@@ -110,9 +110,9 @@
     </tbody>
     </table>
 
-<script type="text/javascript">
+<script>'use strict';
   $(document).ready(function() {
-    $('#changesets .expand_commit').on('click',function(e){
+    $('#changesets .expand_commit').on('click',function(){
       $(this).next('.mid').find('.message > div').toggleClass('hidden');
       ${resize_js};
     });
--- a/kallithea/templates/changeset/changeset.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/changeset/changeset.html	Mon May 04 19:24:04 2020 +0200
@@ -22,9 +22,9 @@
   <div class="panel-heading clearfix">
     ${self.breadcrumbs()}
   </div>
-  <script>
-    AJAX_COMMENT_URL = ${h.js(url('changeset_comment',repo_name=c.repo_name,revision=c.changeset.raw_id))};
-    AJAX_COMMENT_DELETE_URL = ${h.js(url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__'))};
+  <script>'use strict';
+    var AJAX_COMMENT_URL = ${h.js(url('changeset_comment',repo_name=c.repo_name,revision=c.changeset.raw_id))};
+    var AJAX_COMMENT_DELETE_URL = ${h.js(url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__'))};
   </script>
   <div class="panel-body">
     <div class="panel panel-default">
@@ -90,16 +90,14 @@
                          <span><b>${h.person(c.changeset.author,'full_name_and_username')}</b> - ${h.age(c.changeset.date,True)} ${h.fmt_date(c.changeset.date)}</span><br/>
                          <span>${h.email_or_none(c.changeset.author)}</span><br/>
                      </div>
-                     <% rev = c.changeset.extra.get('source') %>
-                     %if rev:
+                     %if c.changeset_graft_source_hash:
                      <div>
-                       ${_('Grafted from:')} ${h.link_to(h.short_id(rev),h.url('changeset_home',repo_name=c.repo_name,revision=rev), class_="changeset_hash")}
+                       ${_('Grafted from:')} ${h.link_to(h.short_id(c.changeset_graft_source_hash),h.url('changeset_home',repo_name=c.repo_name,revision=c.changeset_graft_source_hash), class_="changeset_hash")}
                      </div>
                      %endif
-                     <% rev = c.changeset.extra.get('transplant_source', '').encode('hex') %>
-                     %if rev:
+                     %if c.changeset_transplant_source_hash:
                      <div>
-                       ${_('Transplanted from:')} ${h.link_to(h.short_id(rev),h.url('changeset_home',repo_name=c.repo_name,revision=rev), class_="changeset_hash")}
+                       ${_('Transplanted from:')} ${h.link_to(h.short_id(c.changeset_transplant_source_hash),h.url('changeset_home',repo_name=c.repo_name,revision=c.changeset_transplant_source_hash), class_="changeset_hash")}
                      </div>
                      %endif
 
@@ -145,7 +143,7 @@
                 %for fid, url_fid, op, a_path, path, diff, stats in file_diff_data:
                     <div class="cs_${op} clearfix">
                       <span class="node">
-                          <i class="icon-diff-${op}"></i>${h.link_to(h.safe_unicode(path), '#%s' % fid)}
+                          <i class="icon-diff-${op}"></i>${h.link_to(path, '#%s' % fid)}
                       </span>
                       <div class="changes">${h.fancy_file_stats(stats)}</div>
                     </div>
@@ -186,9 +184,9 @@
     </div>
 
     ## FORM FOR MAKING JS ACTION AS CHANGESET COMMENTS
-    <script type="text/javascript">
+    <script>'use strict';
       $(document).ready(function(){
-          $('.code-difftable').on('click', '.add-bubble', function(e){
+          $('.code-difftable').on('click', '.add-bubble', function(){
               show_comment_form($(this));
           });
 
--- a/kallithea/templates/changeset/changeset_file_comment.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/changeset/changeset_file_comment.html	Mon May 04 19:24:04 2020 +0200
@@ -163,8 +163,8 @@
 ## original location of comments ... but the ones outside diff context remains here
 <div class="comments inline-comments">
   %for f_path, lines in c.inline_comments:
-    %for line_no, comments in lines.iteritems():
-      <div class="comments-list-chunk" data-f_path="${f_path}" data-line_no="${line_no}" data-target-id="${h.safeid(h.safe_unicode(f_path))}_${line_no}">
+    %for line_no, comments in lines.items():
+      <div class="comments-list-chunk" data-f_path="${f_path}" data-line_no="${line_no}" data-target-id="${h.safeid(f_path)}_${line_no}">
         %for co in comments:
             ${comment_block(co)}
         %endfor
@@ -192,7 +192,7 @@
   </div>
 </div>
 
-<script>
+<script>'use strict';
 
 $(document).ready(function () {
 
--- a/kallithea/templates/changeset/changeset_range.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/changeset/changeset_range.html	Mon May 04 19:24:04 2020 +0200
@@ -56,7 +56,7 @@
                             <div class="cs_${op} clearfix">
                                 <span class="node">
                                     <i class="icon-diff-${op}"></i>
-                                    ${h.link_to(h.safe_unicode(path), '#%s' % fid)}
+                                    ${h.link_to(path, '#%s' % fid)}
                                 </span>
                                 <div class="changes">${h.fancy_file_stats(stats)}</div>
                             </div>
--- a/kallithea/templates/changeset/diff_block.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/changeset/diff_block.html	Mon May 04 19:24:04 2020 +0200
@@ -22,7 +22,7 @@
     <div id="${id_fid}" class="panel panel-default ${cls}">
         <div class="panel-heading clearfix">
                 <div class="pull-left">
-                    ${h.safe_unicode(cs_filename)}
+                    ${cs_filename}
                 </div>
                 <div class="pull-left diff-actions">
                   <span>
@@ -57,13 +57,13 @@
                     %endif
                   </span>
 
-                  <a href="${h.url('files_diff_home',repo_name=cs_repo_name,f_path=h.safe_unicode(cs_filename),diff2=cs_rev,diff1=a_rev,diff='diff',fulldiff=1)}" data-toggle="tooltip" title="${_('Show full diff for this file')}">
+                  <a href="${h.url('files_diff_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 diff for this file')}">
                       <i class="icon-file-code"></i></a>
-                  <a href="${h.url('files_diff_2way_home',repo_name=cs_repo_name,f_path=h.safe_unicode(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')}">
+                  <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=h.safe_unicode(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')}">
                       <i class="icon-diff"></i></a>
-                  <a href="${h.url('files_diff_home',repo_name=cs_repo_name,f_path=h.safe_unicode(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')}">
                       <i class="icon-floppy"></i></a>
                   ${c.ignorews_url(request.GET, url_fid)}
                   ${c.context_url(request.GET, url_fid)}
@@ -73,7 +73,7 @@
                     ${h.checkbox('checkbox-show-inline-' + id_fid, checked="checked",class_="show-inline-comments",**{'data-id_for':id_fid})}
                 </div>
         </div>
-        <div class="no-padding panel-body" data-f_path="${h.safe_unicode(cs_filename)}">
+        <div class="no-padding panel-body" data-f_path="${cs_filename}">
             ${diff|n}
             %if op and cs_filename.rsplit('.')[-1] in ['png', 'gif', 'jpg', 'bmp']:
               <div class="btn btn-image-diff-show">Show images</div>
@@ -96,9 +96,9 @@
 </%def>
 
 <%def name="diff_block_js()">
-<script type="text/javascript">
+<script>'use strict';
 $(document).ready(function(){
-    $('.btn-image-diff-show').click(function(e){
+    $('.btn-image-diff-show').click(function(){
         $('.btn-image-diff-show').hide();
         $('.btn-image-diff-swap').show();
         $('.img-diff-swapable')
@@ -112,10 +112,10 @@
         $('#'+e.currentTarget.id+'-img-a.img-diff-swapable')
           .before($('#'+e.currentTarget.id+'-img-b.img-diff-swapable'));
     });
-    var reset = function(e){
+    function reset(e){
         $('#'+e.currentTarget.id+'-img-a.img-diff-swapable')
           .after($('#'+e.currentTarget.id+'-img-b.img-diff-swapable'));
-    };
+    }
     $('.btn-image-diff-swap').mouseup(reset);
     $('.btn-image-diff-swap').mouseleave(reset);
 
--- a/kallithea/templates/compare/compare_cs.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/compare/compare_cs.html	Mon May 04 19:24:04 2020 +0200
@@ -63,17 +63,17 @@
 %if c.is_ajax_preview:
 <div id="jsdata" style="display:none">${h.js(c.jsdata)}</div>
 %else:
-<script type="text/javascript" src="${h.url('/js/graph.js', ver=c.kallithea_version)}"></script>
+<script src="${h.url('/js/graph.js', ver=c.kallithea_version)}"></script>
 %endif
 
-<script type="text/javascript">
+<script>'use strict';
     var jsdata = ${h.js(c.jsdata)};
     var graph = new BranchRenderer('graph_canvas', 'graph_content_pr', 'chg_');
 
     $(document).ready(function(){
         graph.render(jsdata);
 
-        $('.expand_commit').click(function(e){
+        $('.expand_commit').click(function(){
             $(this).next('.mid').find('.message').toggleClass('expanded');
             graph.render(jsdata);
         });
--- a/kallithea/templates/compare/compare_diff.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/compare/compare_diff.html	Mon May 04 19:24:04 2020 +0200
@@ -72,7 +72,7 @@
                     <div class="cs_${op} clearfix">
                       <span class="node">
                           <i class="icon-diff-${op}"></i>
-                          ${h.link_to(h.safe_unicode(path), '#%s' % fid)}
+                          ${h.link_to(path, '#%s' % fid)}
                       </span>
                       <div class="changes">${h.fancy_file_stats(stats)}</div>
                     </div>
@@ -98,7 +98,7 @@
     </div>
 
 </div>
-    <script type="text/javascript">
+    <script>'use strict';
 
    $(document).ready(function(){
     var cache = {};
@@ -154,7 +154,7 @@
     make_revision_dropdown("#compare_org",   ${h.js(c.a_repo.repo_name)},  ${h.js(c.a_ref_name)},  'cache');
     make_revision_dropdown("#compare_other", ${h.js(c.cs_repo.repo_name)}, ${h.js(c.cs_ref_name)}, 'cache2');
 
-    var values_changed = function() {
+    function values_changed() {
         var values = $('#compare_org').select2('data') && $('#compare_other').select2('data');
         if (values) {
              $('#compare_revs').removeClass("disabled");
@@ -167,7 +167,7 @@
     values_changed();
     $('#compare_org').change(values_changed);
     $('#compare_other').change(values_changed);
-    $('#compare_revs').on('click', function(e){
+    $('#compare_revs').on('click', function(){
         var org = $('#compare_org').select2('data');
         var other = $('#compare_other').select2('data');
         if (!org || !other) {
--- a/kallithea/templates/data_table/_dt_elements.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/data_table/_dt_elements.html	Mon May 04 19:24:04 2020 +0200
@@ -31,6 +31,12 @@
   </div>
 </%def>
 
+<%def name="following(repo_id, repo_following)">
+  %if request.authuser.username != 'default':
+    <a href="#" class="${'following' if repo_following else 'follow'}" onclick="return toggleFollowingRepo(this, ${repo_id});"><i class="list-extra icon-heart-empty show-follow" title="${_('Follow')}"></i><i class="list-extra icon-heart show-following" title="${_('Unfollow')}"></i></a>
+  %endif
+</%def>
+
 <%def name="last_change(last_change)">
   <span data-toggle="tooltip" title="${h.fmt_date(last_change)}" date="${last_change}">${h.age(last_change)}</span>
 </%def>
--- a/kallithea/templates/email_templates/changeset_comment.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/email_templates/changeset_comment.html	Mon May 04 19:24:04 2020 +0200
@@ -33,7 +33,8 @@
     </tr>
     <tr>
         <td>
-<%include file="button.html" args="url=cs_comment_url,title=_('View Comment'),padding_bottom=False"/>\
+<% title = _('View Comment') %>\
+<%include file="button.html" args="url=cs_comment_url,title=title,padding_bottom=False"/>\
         </td>
     </tr>
 </table>
--- a/kallithea/templates/email_templates/changeset_comment.txt	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/email_templates/changeset_comment.txt	Mon May 04 19:24:04 2020 +0200
@@ -13,4 +13,5 @@
 ${_('by')|n,unicode} \
 ${cs_author.full_name_and_username|n,unicode}.
 
-<%include file="button.txt" args="url=cs_comment_url,title=_('View Comment')"/>\
+<% title = _('View Comment') %>\
+<%include file="button.txt" args="url=cs_comment_url,title=title"/>\
--- a/kallithea/templates/email_templates/default.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/email_templates/default.html	Mon May 04 19:24:04 2020 +0200
@@ -1,7 +1,8 @@
 <%inherit file="main.html"/>\
 \
 <%block name="header">\
-<%include file="header.html" args="title=_('Message'),link=None"/>\
+<% title = _('Message') %>\
+<%include file="header.html" args="title=title,link=None"/>\
 </%block>\
 \
 <table cellpadding="0" cellspacing="0" border="0" width="100%">
--- a/kallithea/templates/email_templates/password_reset.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/email_templates/password_reset.html	Mon May 04 19:24:04 2020 +0200
@@ -1,7 +1,8 @@
 <%inherit file="main.html"/>\
 \
 <%block name="header">\
-<%include file="header.html" args="title=_('Password Reset Request'),link=None"/>\
+<% title = _('Password Reset Request') %>\
+<%include file="header.html" args="title=title,link=None"/>\
 </%block>\
 \
 <table cellpadding="0" cellspacing="0" border="0" width="100%" style="table-layout:fixed;word-wrap:break-word;">
--- a/kallithea/templates/email_templates/password_reset.txt	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/email_templates/password_reset.txt	Mon May 04 19:24:04 2020 +0200
@@ -1,5 +1,6 @@
 <%block name="header">\
-<%include file="header.txt" args="title=_('Password Reset Request'),link=None"/>\
+<% title = _('Password Reset Request') %>\
+<%include file="header.txt" args="title=title,link=None"/>\
 </%block>\
 \
 ${_('Hello %s') % user|n,unicode},
--- a/kallithea/templates/email_templates/pull_request.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/email_templates/pull_request.html	Mon May 04 19:24:04 2020 +0200
@@ -82,7 +82,8 @@
     </tr>
     <tr>
         <td>
-<%include file="button.html" args="url=pr_url,title=_('View Pull Request'),padding_bottom=False"/>\
+<% title = _('View Pull Request') %>\
+<%include file="button.html" args="url=pr_url,title=title,padding_bottom=False"/>\
         </td>
     </tr>
 </table>
--- a/kallithea/templates/email_templates/pull_request.txt	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/email_templates/pull_request.txt	Mon May 04 19:24:04 2020 +0200
@@ -29,4 +29,5 @@
 ${h.shorter(desc, 80, firstline=True)|n,unicode}
 %endfor
 
-<%include file="button.txt" args="url=pr_url,title='View Pull Request'"/>\
+<% title = _('View Pull Request') %>\
+<%include file="button.txt" args="url=pr_url,title=title"/>\
--- a/kallithea/templates/email_templates/pull_request_comment.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/email_templates/pull_request_comment.html	Mon May 04 19:24:04 2020 +0200
@@ -40,7 +40,8 @@
     </tr>
     <tr>
         <td>
-<%include file="button.html" args="url=pr_comment_url,title=_('View Comment'),padding_bottom=False"/>\
+<% title = _('View Comment') %>\
+<%include file="button.html" args="url=pr_comment_url,title=title,padding_bottom=False"/>\
         </td>
     </tr>
 </table>
--- a/kallithea/templates/email_templates/pull_request_comment.txt	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/email_templates/pull_request_comment.txt	Mon May 04 19:24:04 2020 +0200
@@ -19,4 +19,5 @@
 ${_('branch')|n,unicode} \
 ${pr_target_branch|n,unicode}
 
-<%include file="button.txt" args="url=pr_comment_url,title=_('View Comment')"/>\
+<% title = _('View Comment') %>\
+<%include file="button.txt" args="url=pr_comment_url,title=title"/>\
--- a/kallithea/templates/email_templates/registration.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/email_templates/registration.html	Mon May 04 19:24:04 2020 +0200
@@ -2,7 +2,8 @@
 <%inherit file="main.html"/>\
 \
 <%block name="header">\
-<%include file="header.html" args="title=_('New User Registration'),link=registered_user_url"/>\
+<% title = _('New User Registration') %>\
+<%include file="header.html" args="title=title,link=registered_user_url"/>\
 </%block>\
 \
 <table cellpadding="0" cellspacing="0" border="0" width="100%">
@@ -38,7 +39,8 @@
     </tr>
     <tr>
         <td colspan="2">
-<%include file="button.html" args="url=registered_user_url,title=_('View User Profile'),padding_bottom=False"/>\
+<% title = _('View User Profile') %>\
+<%include file="button.html" args="url=registered_user_url,title=title,padding_bottom=False"/>\
         </td>
     </tr>
 </table>
--- a/kallithea/templates/email_templates/registration.txt	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/email_templates/registration.txt	Mon May 04 19:24:04 2020 +0200
@@ -1,5 +1,6 @@
 <%block name="header">\
-<%include file="header.txt" args="title=_('New User Registration'),link=registered_user_url"/>\
+<% title = _('New User Registration') %>\
+<%include file="header.txt" args="title=title,link=registered_user_url"/>\
 </%block>\
 
 ${_('Username')|n,unicode}: ${new_username|n,unicode}
@@ -8,4 +9,5 @@
 
 ${_('Email')|n,unicode}: ${new_email|n,unicode}
 
-<%include file="button.txt" args="url=registered_user_url,title='View User Profile'"/>\
+<% title = _('View User Profile') %>\
+<%include file="button.txt" args="url=registered_user_url,title=title"/>\
--- a/kallithea/templates/files/diff_2way.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/files/diff_2way.html	Mon May 04 19:24:04 2020 +0200
@@ -3,8 +3,8 @@
 <%inherit file="/base/base.html"/>
 
 <%block name="js_extra">
-  <script type="text/javascript" src="${h.url('/codemirror/lib/codemirror.js')}"></script>
-  <script type="text/javascript" src="${h.url('/js/mergely.js')}"></script>
+  <script src="${h.url('/codemirror/lib/codemirror.js')}"></script>
+  <script src="${h.url('/js/mergely.js')}"></script>
 </%block>
 <%block name="css_extra">
   <link rel="stylesheet" type="text/css" href="${h.url('/codemirror/lib/codemirror.css')}"/>
@@ -34,22 +34,22 @@
         <div class="panel panel-default">
             <div class="panel-heading clearfix">
                     <div class="pull-left">
-                        ${h.link_to(h.safe_unicode(c.node1.path),h.url('files_home',repo_name=c.repo_name,
-                        revision=c.cs2.raw_id,f_path=h.safe_unicode(c.node1.path)))}
+                        ${h.link_to(c.node1.path,h.url('files_home',repo_name=c.repo_name,
+                        revision=c.cs2.raw_id,f_path=c.node1.path))}
                     </div>
                     <div class="pull-left diff-actions">
-                      <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(c.node1.path),diff2=c.cs2.raw_id,diff1=c.cs1.raw_id,diff='diff',fulldiff=1)}"
+                      <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='diff',fulldiff=1)}"
                          data-toggle="tooltip"
                          title="${_('Show full diff for this file')}">
                           <i class="icon-file-code"></i></a>
-                      <a href="${h.url('files_diff_2way_home',repo_name=c.repo_name,f_path=h.safe_unicode(c.node1.path),diff2=c.cs2.raw_id,diff1=c.cs1.raw_id,diff='diff',fulldiff=1)}"
+                      <a href="${h.url('files_diff_2way_home',repo_name=c.repo_name,f_path=c.node1.path,diff2=c.cs2.raw_id,diff1=c.cs1.raw_id,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=c.repo_name,f_path=h.safe_unicode(c.node1.path),diff2=c.cs2.raw_id,diff1=c.cs1.raw_id,diff='raw')}"
+                      <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>
-                      <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(c.node1.path),diff2=c.cs2.raw_id,diff1=c.cs1.raw_id,diff='download')}"
+                      <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>
                       ${h.checkbox('ignorews', label=_('Ignore whitespace'))}
@@ -60,9 +60,9 @@
         </div>
     </div>
 
-<script>
-var orig1_url = ${h.jshtml(h.url('files_raw_home',repo_name=c.repo_name,f_path=h.safe_unicode(c.node1.path),revision=c.cs1.raw_id))};
-var orig2_url = ${h.jshtml(h.url('files_raw_home',repo_name=c.repo_name,f_path=h.safe_unicode(c.node2.path),revision=c.cs2.raw_id))};
+<script>'use strict';
+var orig1_url = ${h.jshtml(h.url('files_raw_home',repo_name=c.repo_name,f_path=c.node1.path,revision=c.cs1.raw_id))};
+var orig2_url = ${h.jshtml(h.url('files_raw_home',repo_name=c.repo_name,f_path=c.node2.path,revision=c.cs2.raw_id))};
 
 $(document).ready(function () {
     $('#compare').mergely({
--- a/kallithea/templates/files/files.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/files/files.html	Mon May 04 19:24:04 2020 +0200
@@ -3,7 +3,7 @@
 <%block name="title">
     ${_('%s Files') % c.repo_name}
     %if hasattr(c,'file'):
-        &middot; ${h.safe_unicode(c.file.path) or '/'}
+        &middot; ${c.file.path or '/'}
     %endif
 </%block>
 
@@ -36,7 +36,7 @@
     </div>
 </div>
 
-<script type="text/javascript">
+<script>'use strict';
 var CACHE = {};
 var CACHE_EXPIRE = 5*60*1000; //cache for 5*60s
 //used to construct links from the search list
@@ -50,7 +50,7 @@
 pyroutes.register('files_history_home', ${h.js(h.url('files_history_home', repo_name=c.repo_name,revision='%(revision)s',f_path='%(f_path)s'))}, ['revision', 'f_path']);
 pyroutes.register('files_authors_home', ${h.js(h.url('files_authors_home', repo_name=c.repo_name,revision='%(revision)s',f_path='%(f_path)s'))}, ['revision', 'f_path']);
 
-var ypjax_links = function(){
+function ypjax_links(){
     $('.ypjax-link').click(function(e){
 
         //don't do ypjax on middle click
@@ -88,7 +88,7 @@
     });
 }
 
-var load_state = function(state) {
+function load_state(state) {
     var $files_data = $('#files_data');
     var cache_key = state.url;
     var _cache_obj = CACHE[cache_key];
@@ -106,7 +106,7 @@
     }
 }
 
-var post_load_state = function(state) {
+function post_load_state(state) {
     ypjax_links();
     tooltip_activate();
 
@@ -125,16 +125,16 @@
     }
 
     function highlight_lines(lines){
-        for(pos in lines){
+        for(let pos in lines){
           $('#L'+lines[pos]).css('background-color','#FFFFBE');
         }
     }
-    page_highlights = location.href.substring(location.href.indexOf('#')+1).split('L');
+    let page_highlights = location.href.substring(location.href.indexOf('#')+1).split('L');
     if (page_highlights.length == 2){
-       highlight_ranges  = page_highlights[1].split(",");
+       let highlight_ranges  = page_highlights[1].split(",");
 
        var h_lines = [];
-       for (pos in highlight_ranges){
+       for (let pos in highlight_ranges){
             var _range = highlight_ranges[pos].split('-');
             if(_range.length == 2){
                 var start = parseInt(_range[0]);
@@ -217,12 +217,12 @@
     });
 
     // init the search filter
-    var _node_list_url = node_list_url.replace('__REV__', ${h.js(c.changeset.raw_id)}).replace('__FPATH__', ${h.js(h.safe_unicode(c.file.path))});
+    var _node_list_url = node_list_url.replace('__REV__', ${h.js(c.changeset.raw_id)}).replace('__FPATH__', ${h.js(c.file.path)});
     var _url_base = url_base.replace('__REV__', ${h.js(c.changeset.raw_id)});
     fileBrowserListeners(_node_list_url, _url_base);
 
     var initial_state = {url:window.location.href, title:document.title, url_base:_url_base,
-         node_list_url:_node_list_url, rev:${h.js(c.changeset.raw_id)}, f_path:${h.js(h.safe_unicode(c.file.path))}};
+         node_list_url:_node_list_url, rev:${h.js(c.changeset.raw_id)}, f_path:${h.js(c.file.path)}};
 
     // change branch filter
     $("#branch_selector").select2({
@@ -234,7 +234,7 @@
     $("#branch_selector").change(function(e){
         var selected = e.currentTarget.options[e.currentTarget.selectedIndex].value;
         if(selected && selected != ${h.js(c.changeset.raw_id)}){
-            window.location = pyroutes.url('files_home', {'repo_name': ${h.js(h.safe_unicode(c.repo_name))}, 'revision': selected, 'f_path': ${h.js(h.safe_unicode(c.file.path))}});
+            window.location = pyroutes.url('files_home', {'repo_name': ${h.js(c.repo_name)}, 'revision': selected, 'f_path': ${h.js(c.file.path)}});
             $("#body").hide();
         } else {
             $("#branch_selector").val(${h.js(c.changeset.raw_id)});
@@ -242,7 +242,7 @@
     });
     $('#show_authors').on('click', function(){
         $.ajax({
-            url: pyroutes.url('files_authors_home', {'revision': ${h.js(c.changeset.raw_id)}, 'f_path': ${h.js(h.safe_unicode(c.file.path))}}),
+            url: pyroutes.url('files_authors_home', {'revision': ${h.js(c.changeset.raw_id)}, 'f_path': ${h.js(c.file.path)}}),
             success: function(data) {
                 $('#file_authors').html(data);
                 $('#file_authors').show();
--- a/kallithea/templates/files/files_add.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/files/files_add.html	Mon May 04 19:24:04 2020 +0200
@@ -5,9 +5,9 @@
 </%block>
 
 <%block name="js_extra">
-  <script type="text/javascript" src="${h.url('/codemirror/lib/codemirror.js')}"></script>
-  <script type="text/javascript" src="${h.url('/js/codemirror_loadmode.js')}"></script>
-  <script type="text/javascript" src="${h.url('/codemirror/mode/meta.js')}"></script>
+  <script src="${h.url('/codemirror/lib/codemirror.js')}"></script>
+  <script src="${h.url('/js/codemirror_loadmode.js')}"></script>
+  <script src="${h.url('/codemirror/mode/meta.js')}"></script>
 </%block>
 <%block name="css_extra">
   <link rel="stylesheet" type="text/css" href="${h.url('/codemirror/lib/codemirror.css')}"/>
@@ -70,7 +70,7 @@
               </div>
             </div>
             ${h.end_form()}
-            <script type="text/javascript">
+            <script>'use strict';
                 $(document).ready(function(){
                     var reset_url = ${h.jshtml(h.url('files_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path))};
                     var myCodeMirror = initCodeMirror('editor', ${h.jshtml(request.script_name)}, reset_url);
@@ -107,7 +107,7 @@
                     });
 
                     // on type the new filename set mode
-                    $filename_input.keyup(function(e){
+                    $filename_input.keyup(function(){
                         var file_data = CodeMirror.getFilenameAndExt(this.value);
                         if(file_data['ext'] != null){
                             var detected_mode = CodeMirror.findModeByExtension(file_data['ext']) || CodeMirror.findModeByMIME('text/plain');
--- a/kallithea/templates/files/files_browser.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/files/files_browser.html	Mon May 04 19:24:04 2020 +0200
@@ -13,7 +13,7 @@
     %if node.is_submodule():
         <%return node.url or '#'%>
     %else:
-        <%return h.url('files_home', repo_name=c.repo_name, revision=c.changeset.raw_id, f_path=h.safe_unicode(node.path))%>
+        <%return h.url('files_home', repo_name=c.repo_name, revision=c.changeset.raw_id, f_path=node.path)%>
     %endif
 </%def>
 <%def name="_file_name(iconclass, name)">
@@ -109,7 +109,7 @@
     </div>
 </div>
 
-<script>
+<script>'use strict';
     $(document).ready(function(){
         // init node filter if we pass GET param ?search=1
         var search_GET = ${h.js(request.GET.get('search',''))};
--- a/kallithea/templates/files/files_edit.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/files/files_edit.html	Mon May 04 19:24:04 2020 +0200
@@ -5,9 +5,9 @@
 </%block>
 
 <%block name="js_extra">
-  <script type="text/javascript" src="${h.url('/codemirror/lib/codemirror.js')}"></script>
-  <script type="text/javascript" src="${h.url('/js/codemirror_loadmode.js')}"></script>
-  <script type="text/javascript" src="${h.url('/codemirror/mode/meta.js')}"></script>
+  <script src="${h.url('/codemirror/lib/codemirror.js')}"></script>
+  <script src="${h.url('/js/codemirror_loadmode.js')}"></script>
+  <script src="${h.url('/codemirror/mode/meta.js')}"></script>
 </%block>
 <%block name="css_extra">
   <link rel="stylesheet" type="text/css" href="${h.url('/codemirror/lib/codemirror.css')}"/>
@@ -59,7 +59,7 @@
                     </span>
               </div>
               <div class="panel-body no-padding">
-                <textarea id="editor" name="content" style="display:none">${h.escape(c.file.content)|n}</textarea>
+                <textarea id="editor" name="content" style="display:none">${h.escape(h.safe_str(c.file.content))|n}</textarea>
               </div>
             </div>
             <div>
@@ -77,7 +77,7 @@
     </div>
 </div>
 
-<script type="text/javascript">
+<script>'use strict';
     $(document).ready(function(){
         var reset_url = ${h.jshtml(h.url('files_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.file.path))};
         var myCodeMirror = initCodeMirror('editor', ${h.jshtml(request.script_name)}, reset_url);
--- a/kallithea/templates/followers/followers.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/followers/followers.html	Mon May 04 19:24:04 2020 +0200
@@ -25,7 +25,7 @@
         </div>
     </div>
 </div>
-<script type="text/javascript">
+<script>'use strict';
   $(document).ready(function(){
     var $followers = $('#followers');
     $followers.on('click','.pager_link',function(e){
--- a/kallithea/templates/forks/fork.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/forks/fork.html	Mon May 04 19:24:04 2020 +0200
@@ -88,7 +88,7 @@
     </div>
     ${h.end_form()}
 </div>
-<script>
+<script>'use strict';
     $(document).ready(function(){
         $("#repo_group").select2({
             'dropdownAutoWidth': true
--- a/kallithea/templates/forks/forks.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/forks/forks.html	Mon May 04 19:24:04 2020 +0200
@@ -25,7 +25,7 @@
         </div>
     </div>
 </div>
-<script type="text/javascript">
+<script>'use strict';
   $(document).ready(function(){
       var $forks = $('#forks');
       $forks.on('click','.pager_link',function(e){
--- a/kallithea/templates/index_base.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/index_base.html	Mon May 04 19:24:04 2020 +0200
@@ -44,22 +44,23 @@
         </div>
     </div>
 
-      <script>
-        var data = ${h.js(c.data)},
-            $dataTable = $("#repos_list_wrap").DataTable({
+      <script>'use strict';
+        var data = ${h.js(c.data)};
+        $("#repos_list_wrap").DataTable({
                 data: data.records,
                 columns: [
                     {data: "raw_name", visible: false, searchable: false},
                     {title: ${h.jshtml(_('Repository'))}, data: "name", orderData: [0,], render: {
-                        filter: function(data, type, row, meta) {
+                        filter: function(data, type, row) {
                             return row.just_name;
                         }
                     }},
+                    {data: "following", defaultContent: '', sortable: false},
                     {data: "desc", title: ${h.jshtml(_('Description'))}, searchable: false},
                     {data: "last_change_iso", defaultContent: '', visible: false, searchable: false},
-                    {data: "last_change", defaultContent: '', title: ${h.jshtml(_('Last Change'))}, orderData: [3,], searchable: false},
+                    {data: "last_change", defaultContent: '', title: ${h.jshtml(_('Last Change'))}, orderData: [4,], searchable: false},
                     {data: "last_rev_raw", defaultContent: '', visible: false, searchable: false},
-                    {data: "last_changeset", defaultContent: '', title: ${h.jshtml(_('Tip'))}, orderData: [5,], searchable: false},
+                    {data: "last_changeset", defaultContent: '', title: ${h.jshtml(_('Tip'))}, orderData: [6,], searchable: false},
                     {data: "owner", defaultContent: '', title: ${h.jshtml(_('Owner'))}, searchable: false},
                     {data: "atom", defaultContent: '', sortable: false}
                 ],
--- a/kallithea/templates/journal/journal.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/journal/journal.html	Mon May 04 19:24:04 2020 +0200
@@ -41,7 +41,7 @@
         </div>
     </div>
 
-<script type="text/javascript">
+<script>'use strict';
 
     $('#j_filter').click(function(){
         var $jfilter = $('#j_filter');
@@ -49,9 +49,9 @@
             $jfilter.val('');
         }
     });
-    var fix_j_filter_width = function(len){
+    function fix_j_filter_width(len){
         $('#j_filter').css('width', Math.max(80, len*6.50)+'px');
-    };
+    }
     $('#j_filter').keyup(function(){
         fix_j_filter_width($('#j_filter').val().length);
     });
@@ -72,7 +72,7 @@
 
 </script>
 
-<script type="text/javascript">
+<script>'use strict';
     $(document).ready(function(){
         var $journal = $('#journal');
         $journal.on('click','.pager_link',function(e){
--- a/kallithea/templates/login.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/login.html	Mon May 04 19:24:04 2020 +0200
@@ -64,7 +64,7 @@
             </div>
         </div>
         ${h.end_form()}
-        <script type="text/javascript">
+        <script>'use strict';
         $(document).ready(function(){
             $('#username').focus();
         });
--- a/kallithea/templates/password_reset.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/password_reset.html	Mon May 04 19:24:04 2020 +0200
@@ -7,7 +7,7 @@
 
 <%block name="js_extra">
     %if c.captcha_active:
-        <script type="text/javascript" src="https://www.google.com/recaptcha/api.js"></script>
+        <script src="https://www.google.com/recaptcha/api.js"></script>
     %endif
 </%block>
 
@@ -53,7 +53,7 @@
                 </div>
         </div>
         ${h.end_form()}
-        <script type="text/javascript">
+        <script>'use strict';
          $(document).ready(function(){
             $('#email').focus();
          });
--- a/kallithea/templates/password_reset_confirmation.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/password_reset_confirmation.html	Mon May 04 19:24:04 2020 +0200
@@ -22,13 +22,13 @@
         ${h.form(h.url('reset_password_confirmation'), method='post')}
         <p>${_('You are about to set a new password for the email address %s.') % c.email}</p>
         <p>${_('Note that you must use the same browser session for this as the one used to request the password reset.')}</p>
-        ${h.hidden('email')}
-        ${h.hidden('timestamp')}
+        ${h.hidden('email', value=c.email)}
+        ${h.hidden('timestamp', value=c.timestamp)}
         <div class="form">
                 <div class="form-group">
                     <label class="control-label" for="token">${_('Code you received in the email')}:</label>
                     <div>
-                        ${h.text('token', class_='form-control')}
+                        ${h.text('token', value=c.token, class_='form-control')}
                     </div>
                 </div>
 
--- a/kallithea/templates/pullrequests/pullrequest.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/pullrequests/pullrequest.html	Mon May 04 19:24:04 2020 +0200
@@ -91,12 +91,12 @@
 
 </div>
 
-<script type="text/javascript" src="${h.url('/js/graph.js', ver=c.kallithea_version)}"></script>
-<script type="text/javascript">
+<script src="${h.url('/js/graph.js', ver=c.kallithea_version)}"></script>
+<script>'use strict';
   pyroutes.register('pullrequest_repo_info', ${h.js(url('pullrequest_repo_info',repo_name='%(repo_name)s'))}, ['repo_name']);
 
   var pendingajax = undefined;
-  var otherrepoChanged = function(){
+  function otherrepoChanged(){
       var $other_ref = $('#other_ref');
       $other_ref.prop('disabled', true);
       var repo_name = $('#other_repo').val();
@@ -132,9 +132,9 @@
               $other_ref.prop('disabled', false);
               loadPreview();
           });
-  };
+  }
 
-  var loadPreview = function(){
+  function loadPreview(){
       //url template
       var url = ${h.js(h.url('compare_url',
                          repo_name='__other_repo__',
@@ -162,7 +162,7 @@
           '__other_ref_name__': other_ref[2]
       }; // gather the org/other ref and repo here
 
-      for (k in rev_data){
+      for (let k in rev_data){
           url = url.replace(k,rev_data[k]);
       }
 
@@ -170,7 +170,7 @@
           pendingajax.abort();
           pendingajax = undefined;
       }
-      pendingajax = asynchtml(url, $('#pull_request_overview'), function(o){
+      pendingajax = asynchtml(url, $('#pull_request_overview'), function(){
           pendingajax = undefined;
       });
   }
@@ -186,14 +186,14 @@
           maxResults: 50,
           sortResults: branchSort
       });
-      $("#org_ref").on("change", function(e){
+      $("#org_ref").on("change", function(){
           loadPreview();
       });
 
       $("#other_repo").select2({
           dropdownAutoWidth: true
       });
-      $("#other_repo").on("change", function(e){
+      $("#other_repo").on("change", function(){
           otherrepoChanged();
       });
 
@@ -202,7 +202,7 @@
           maxResults: 50,
           sortResults: branchSort
       });
-      $("#other_ref").on("change", function(e){
+      $("#other_ref").on("change", function(){
           loadPreview();
       });
 
--- a/kallithea/templates/pullrequests/pullrequest_data.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/pullrequests/pullrequest_data.html	Mon May 04 19:24:04 2020 +0200
@@ -80,7 +80,7 @@
 </div>
 
 %if hasattr(pullrequests, 'pager'):
-    ${pullrequests.pager(**request.GET.mixed())}
+    ${pullrequests.pager()}
 %endif
 
 </%def>
--- a/kallithea/templates/pullrequests/pullrequest_show.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/pullrequests/pullrequest_show.html	Mon May 04 19:24:04 2020 +0200
@@ -300,7 +300,7 @@
                     <div class="cs_${op} clearfix">
                       <span class="node">
                           <i class="icon-diff-${op}"></i>
-                          ${h.link_to(h.safe_unicode(path), '#%s' % fid)}
+                          ${h.link_to(path, '#%s' % fid)}
                       </span>
                       <div class="changes">${h.fancy_file_stats(stats)}</div>
                     </div>
@@ -312,10 +312,10 @@
             </div>
         </div>
     </div>
-    <script>
+    <script>'use strict';
     // TODO: switch this to pyroutes
-    AJAX_COMMENT_URL = ${h.js(url('pullrequest_comment',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id))};
-    AJAX_COMMENT_DELETE_URL = ${h.js(url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__'))};
+    var AJAX_COMMENT_URL = ${h.js(url('pullrequest_comment',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id))};
+    var AJAX_COMMENT_DELETE_URL = ${h.js(url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__'))};
 
     pyroutes.register('pullrequest_comment', ${h.js(url('pullrequest_comment',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s'))}, ['repo_name', 'pull_request_id']);
     pyroutes.register('pullrequest_comment_delete', ${h.js(url('pullrequest_comment_delete',repo_name='%(repo_name)s',comment_id='%(comment_id)s'))}, ['repo_name', 'comment_id']);
@@ -343,12 +343,12 @@
     ## main comment form and it status
     ${comment.comments(change_status=c.allowed_to_change_status)}
 
-    <script type="text/javascript">
+    <script>'use strict';
       $(document).ready(function(){
           PullRequestAutoComplete($('#user'));
           SimpleUserAutoComplete($('#owner'));
 
-          $('.code-difftable').on('click', '.add-bubble', function(e){
+          $('.code-difftable').on('click', '.add-bubble', function(){
               show_comment_form($(this));
           });
 
@@ -368,7 +368,7 @@
               $('#pr-form-clone').prop('disabled',!update);
           });
           var $org_review_members = $('#review_members').clone();
-          $('#pr-form-reset').click(function(e){
+          $('#pr-form-reset').click(function(){
               $('.pr-do-edit').hide();
               $('.pr-not-edit').show();
               $('#pr-form-save').prop('disabled',false);
--- a/kallithea/templates/register.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/register.html	Mon May 04 19:24:04 2020 +0200
@@ -7,7 +7,7 @@
 
 <%block name="js_extra">
     %if c.captcha_active:
-        <script type="text/javascript" src="https://www.google.com/recaptcha/api.js"></script>
+        <script src="https://www.google.com/recaptcha/api.js"></script>
     %endif
 </%block>
 
@@ -90,7 +90,7 @@
                 </div>
         </div>
         ${h.end_form()}
-        <script type="text/javascript">
+        <script>'use strict';
         $(document).ready(function(){
             $('#username').focus();
         });
--- a/kallithea/templates/summary/statistics.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/summary/statistics.html	Mon May 04 19:24:04 2020 +0200
@@ -15,9 +15,9 @@
 <%block name="head_extra">
   <link href="${h.url('atom_feed_home',repo_name=c.db_repo.repo_name,api_key=request.authuser.api_key)}" rel="alternate" title="${_('%s ATOM feed') % c.repo_name}" type="application/atom+xml" />
   <link href="${h.url('rss_feed_home',repo_name=c.db_repo.repo_name,api_key=request.authuser.api_key)}" rel="alternate" title="${_('%s RSS feed') % c.repo_name}" type="application/rss+xml" />
-  <script type="text/javascript" src="${h.url('/js/jquery.flot.js', ver=c.kallithea_version)}"></script>
-  <script type="text/javascript" src="${h.url('/js/jquery.flot.selection.js', ver=c.kallithea_version)}"></script>
-  <script type="text/javascript" src="${h.url('/js/jquery.flot.time.js', ver=c.kallithea_version)}"></script>
+  <script src="${h.url('/js/jquery.flot.js', ver=c.kallithea_version)}"></script>
+  <script src="${h.url('/js/jquery.flot.selection.js', ver=c.kallithea_version)}"></script>
+  <script src="${h.url('/js/jquery.flot.time.js', ver=c.kallithea_version)}"></script>
 </%block>
 
 <%def name="main()">
@@ -28,8 +28,8 @@
     </div>
 
     <div class="graph panel-body">
-         <div>
-         %if c.no_data:
+        <div>
+        %if not c.stats_percentage:
            ${c.no_data_msg}
            %if h.HasPermissionAny('hg.admin')('enable stats on from summary'):
                 ${h.link_to(_('Enable'),h.url('edit_repo',repo_name=c.repo_name),class_="btn btn-default btn-xs")}
@@ -51,19 +51,17 @@
     </div>
 </div>
 
-<script type="text/javascript">
+<script>'use strict';
 var data = ${h.js(c.trending_languages)};
 var total = 0;
-var no_data = true;
 var tbl = document.createElement('table');
 tbl.setAttribute('class','trending_language_tbl');
 var cnt = 0;
-for (var i=0;i<data.length;i++){
+for (let i=0;i<data.length;i++){
     total+= data[i][1].count;
 }
-for (var i=0;i<data.length;i++){
+for (let i=0;i<data.length;i++){
     cnt += 1;
-    no_data = false;
 
     var hide = cnt>2;
     var tr = document.createElement('tr');
@@ -105,7 +103,7 @@
     if(cnt == 3){
         var show_more = document.createElement('tr');
         var td = document.createElement('td');
-        lnk = document.createElement('a');
+        let lnk = document.createElement('a');
 
         lnk.href='#';
         lnk.innerHTML = ${h.jshtml(_('Show more'))};
@@ -120,7 +118,7 @@
 }
 
 </script>
-<script type="text/javascript">
+<script>'use strict';
 
 /**
  * Plots summary graph
@@ -138,17 +136,15 @@
             "to":to
         }
     };
-    for(var key in dataset){
-      var data = dataset[key].data;
+    for(let key in dataset){
+      let data = dataset[key].data;
       for(var d in data){
         data[d].time *= 1000;
       }
     }
-    for(var key in overview_dataset){
+    for(let key in overview_dataset){
       overview_dataset[key][0] *= 1000;
     }
-    var dataset = dataset;
-    var overview_dataset = [overview_dataset];
     var choiceContainer = $("#legend_choices")[0];
     var choiceContainerTable = $("#legend_choices_tables")[0];
     var $plotContainer = $('#commit_history');
@@ -160,8 +156,7 @@
         bars: {show:true, align: 'center', lineWidth: 4},
         legend: {show:true,
                 container: "#legend_container",
-                labelFormatter: function(label, series) {
-                        // series is the series object for the label
+                labelFormatter: function(label) {
                         return '<a href="javascript:void(0)"> ' + label + '</a>';
                     }
         },
@@ -209,8 +204,6 @@
 
     /**
      * generate checkboxes accordingly to data
-     * @param keys
-     * @returns
      */
     function generateCheckboxes(data) {
         //append checkboxes
@@ -259,12 +252,9 @@
 
         var data = [];
         var new_dataset = {};
-        var keys = [];
-        var max_commits = 0;
         for(var key in dataset){
-
             for(var ds in dataset[key].data){
-                commit_data = dataset[key].data[ds];
+                let commit_data = dataset[key].data[ds];
                 if (commit_data.time >= ranges.xaxis.from && commit_data.time <= ranges.xaxis.to){
                     if(new_dataset[key] === undefined){
                         new_dataset[key] = {data:[],label:key};
@@ -292,7 +282,7 @@
     * redraw using new checkbox data
     */
     function plotchoiced(e){
-        args = e.data;
+        let args = e.data;
         var cur_data = args[0];
         var cur_ranges = args[1];
 
@@ -384,7 +374,7 @@
             if (previousPoint != item.datapoint) {
                 previousPoint = item.datapoint;
 
-                var tooltip = $("#tooltip")[0];
+                let tooltip = $("#tooltip")[0];
                 if(tooltip) {
                       tooltip.parentNode.removeChild(tooltip);
                 }
@@ -421,7 +411,7 @@
             }
         }
         else {
-              var tooltip = $("#tooltip")[0];
+              let tooltip = $("#tooltip")[0];
 
               if(tooltip) {
                     tooltip.parentNode.removeChild(tooltip);
@@ -434,14 +424,14 @@
      * MAIN EXECUTION
      */
 
-    var data = getDataAccordingToRanges(initial_ranges);
+    let data = getDataAccordingToRanges(initial_ranges);
     generateCheckboxes(data);
 
     //main plot
     var plot = $.plot(plotContainer,data,plot_options);
 
     //overview
-    var overview = $.plot(overviewContainer, overview_dataset, overview_options);
+    var overview = $.plot(overviewContainer, [overview_dataset], overview_options);
 
     //show initial selection on overview
     overview.setSelection(initial_ranges);
--- a/kallithea/templates/summary/summary.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/templates/summary/summary.html	Mon May 04 19:24:04 2020 +0200
@@ -27,8 +27,8 @@
   <link href="${h.url('atom_feed_home',repo_name=c.db_repo.repo_name,api_key=request.authuser.api_key)}" rel="alternate" title="${_('%s ATOM feed') % c.repo_name}" type="application/atom+xml" />
   <link href="${h.url('rss_feed_home',repo_name=c.db_repo.repo_name,api_key=request.authuser.api_key)}" rel="alternate" title="${_('%s RSS feed') % c.repo_name}" type="application/rss+xml" />
 
-  <script>
-  redirect_hash_branch = function(){
+  <script>'use strict';
+  function redirect_hash_branch(){
     var branch = window.location.hash.replace(/^#(.*)/, '$1');
     if (branch){
       window.location = ${h.js(h.url('changelog_home',repo_name=c.repo_name,branch='__BRANCH__'))}
@@ -238,9 +238,9 @@
 </div>
 %endif
 
-<script type="text/javascript">
+<script>'use strict';
 $(document).ready(function(){
-    $('#clone-url input').click(function(e){
+    $('#clone-url input').click(function(){
         if($(this).hasClass('selected')){
             $(this).removeClass('selected');
             return ;
@@ -254,17 +254,17 @@
     var $clone_by_name = $('#clone_by_name');
     var $clone_by_id = $('#clone_by_id');
     var $clone_ssh = $('#clone_ssh');
-    $clone_url.on('click', '.btn.use-name', function(e){
+    $clone_url.on('click', '.btn.use-name', function(){
         $clone_by_name.show();
         $clone_by_id.hide();
         $clone_ssh.hide();
     });
-    $clone_url.on('click', '.btn.use-id', function(e){
+    $clone_url.on('click', '.btn.use-id', function(){
         $clone_by_id.show();
         $clone_by_name.hide();
         $clone_ssh.hide();
     });
-    $clone_url.on('click', '.btn.use-ssh', function(e){
+    $clone_url.on('click', '.btn.use-ssh', function(){
         $clone_by_id.hide();
         $clone_by_name.hide();
         $clone_ssh.show();
@@ -309,7 +309,7 @@
     $('#download_options').change(function(e){
        var new_cs = e.added
 
-       for(k in tmpl_links){
+       for(let k in tmpl_links){
            var s = $('#'+k+'_link');
            if(s){
              var title_tmpl = ${h.jshtml(_('Download %s as %s') % ('__CS_NAME__','__CS_EXT__'))};
@@ -334,20 +334,18 @@
 </script>
 
 %if c.show_stats:
-<script type="text/javascript">
+<script>'use strict';
 $(document).ready(function(){
     var data = ${h.js(c.trending_languages)};
     var total = 0;
-    var no_data = true;
     var tbl = document.createElement('table');
     tbl.setAttribute('class','table');
     var cnt = 0;
-    for (var i=0;i<data.length;i++){
+    for (let i=0;i<data.length;i++){
         total+= data[i][1].count;
     }
-    for (var i=0;i<data.length;i++){
+    for (let i=0;i<data.length;i++){
         cnt += 1;
-        no_data = false;
 
         var hide = cnt>2;
         var tr = document.createElement('tr');
@@ -395,7 +393,7 @@
         if(cnt == 3){
             var show_more = document.createElement('tr');
             var td = document.createElement('td');
-            lnk = document.createElement('a');
+            let lnk = document.createElement('a');
 
             lnk.href='#';
             lnk.innerHTML = ${h.jshtml(_('Show more'))};
--- a/kallithea/tests/__init__.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/__init__.py	Mon May 04 19:24:04 2020 +0200
@@ -17,13 +17,3 @@
 
 Refer to docs/contributing.rst for details on running the test suite.
 """
-
-import pytest
-
-
-if getattr(pytest, 'register_assert_rewrite', None):
-    # make sure that all asserts under kallithea/tests benefit from advanced
-    # assert reporting with pytest-3.0.0+, including api/api_base.py,
-    # models/common.py etc.
-    # See also: https://docs.pytest.org/en/latest/assert.html#advanced-assertion-introspection
-    pytest.register_assert_rewrite('kallithea.tests')
--- a/kallithea/tests/api/api_base.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/api/api_base.py	Mon May 04 19:24:04 2020 +0200
@@ -23,8 +23,9 @@
 import mock
 import pytest
 
+from kallithea.lib import ext_json
 from kallithea.lib.auth import AuthUser
-from kallithea.lib.compat import json
+from kallithea.lib.utils2 import ascii_bytes
 from kallithea.model.changeset_status import ChangesetStatusModel
 from kallithea.model.db import ChangesetStatus, PullRequest, RepoGroup, Repository, Setting, Ui, User
 from kallithea.model.gist import GistModel
@@ -34,13 +35,13 @@
 from kallithea.model.scm import ScmModel
 from kallithea.model.user import UserModel
 from kallithea.model.user_group import UserGroupModel
-from kallithea.tests.base import *
+from kallithea.tests import base
 from kallithea.tests.fixture import Fixture
 
 
 API_URL = '/_admin/api'
-TEST_USER_GROUP = u'test_user_group'
-TEST_REPO_GROUP = u'test_repo_group'
+TEST_USER_GROUP = 'test_user_group'
+TEST_REPO_GROUP = 'test_repo_group'
 
 fixture = Fixture()
 
@@ -48,11 +49,10 @@
 def _build_data(apikey, method, **kw):
     """
     Builds API data with given random ID
-
-    :param random_id:
+    For convenience, the json is returned as str
     """
     random_id = random.randrange(1, 9999)
-    return random_id, json.dumps({
+    return random_id, ext_json.dumps({
         "id": random_id,
         "api_key": apikey,
         "method": method,
@@ -60,7 +60,7 @@
     })
 
 
-jsonify = lambda obj: json.loads(json.dumps(obj))
+jsonify = lambda obj: ext_json.loads(ext_json.dumps(obj))
 
 
 def crash(*args, **kwargs):
@@ -75,15 +75,15 @@
 
 ## helpers
 def make_user_group(name=TEST_USER_GROUP):
-    gr = fixture.create_user_group(name, cur_user=TEST_USER_ADMIN_LOGIN)
+    gr = fixture.create_user_group(name, cur_user=base.TEST_USER_ADMIN_LOGIN)
     UserGroupModel().add_user_to_group(user_group=gr,
-                                       user=TEST_USER_ADMIN_LOGIN)
+                                       user=base.TEST_USER_ADMIN_LOGIN)
     Session().commit()
     return gr
 
 
 def make_repo_group(name=TEST_REPO_GROUP):
-    gr = fixture.create_repo_group(name, cur_user=TEST_USER_ADMIN_LOGIN)
+    gr = fixture.create_repo_group(name, cur_user=base.TEST_USER_ADMIN_LOGIN)
     Session().commit()
     return gr
 
@@ -94,19 +94,18 @@
 
     @classmethod
     def setup_class(cls):
-        cls.usr = User.get_by_username(TEST_USER_ADMIN_LOGIN)
+        cls.usr = User.get_by_username(base.TEST_USER_ADMIN_LOGIN)
         cls.apikey = cls.usr.api_key
         cls.test_user = UserModel().create_or_update(
             username='test-api',
             password='test',
             email='test@example.com',
-            firstname=u'first',
-            lastname=u'last'
+            firstname='first',
+            lastname='last'
         )
         Session().commit()
         cls.TEST_USER_LOGIN = cls.test_user.username
         cls.apikey_regular = cls.test_user.api_key
-        cls.default_user_username = User.get_default_user().username
 
     @classmethod
     def teardown_class(cls):
@@ -127,7 +126,7 @@
             'error': None,
             'result': expected
         })
-        given = json.loads(given)
+        given = ext_json.loads(given)
         assert expected == given, (expected, given)
 
     def _compare_error(self, id_, expected, given):
@@ -136,7 +135,7 @@
             'error': expected,
             'result': None
         })
-        given = json.loads(given)
+        given = ext_json.loads(given)
         assert expected == given, (expected, given)
 
     def test_Optional_object(self):
@@ -230,10 +229,10 @@
 
     def test_api_get_user(self):
         id_, params = _build_data(self.apikey, 'get_user',
-                                  userid=TEST_USER_ADMIN_LOGIN)
+                                  userid=base.TEST_USER_ADMIN_LOGIN)
         response = api_call(self, params)
 
-        usr = User.get_by_username(TEST_USER_ADMIN_LOGIN)
+        usr = User.get_by_username(base.TEST_USER_ADMIN_LOGIN)
         ret = usr.get_api_data()
         ret['permissions'] = AuthUser(dbuser=usr).permissions
 
@@ -252,7 +251,7 @@
         id_, params = _build_data(self.apikey, 'get_user')
         response = api_call(self, params)
 
-        usr = User.get_by_username(TEST_USER_ADMIN_LOGIN)
+        usr = User.get_by_username(base.TEST_USER_ADMIN_LOGIN)
         ret = usr.get_api_data()
         ret['permissions'] = AuthUser(dbuser=usr).permissions
 
@@ -281,7 +280,7 @@
     def test_api_pull_remote(self):
         # Note: pulling from local repos is a mis-feature - it will bypass access control
         # ... but ok, if the path already has been set in the database
-        repo_name = u'test_pull'
+        repo_name = 'test_pull'
         r = fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
         # hack around that clone_uri can't be set to to a local path
         # (as shown by test_api_create_repo_clone_uri_local)
@@ -305,7 +304,7 @@
         assert pre_cached_tip != post_cached_tip
 
     def test_api_pull_fork(self):
-        fork_name = u'fork'
+        fork_name = 'fork'
         fixture.create_fork(self.REPO, fork_name)
         id_, params = _build_data(self.apikey, 'pull',
                                   repoid=fork_name,)
@@ -327,7 +326,7 @@
         self._compare_error(id_, expected, given=response.body)
 
     def test_api_pull_custom_remote(self):
-        repo_name = u'test_pull_custom_remote'
+        repo_name = 'test_pull_custom_remote'
         fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
 
         custom_remote_path = os.path.join(Ui.get_by_key('paths', '/').ui_value, self.REPO)
@@ -358,58 +357,24 @@
         expected = 'Error occurred during rescan repositories action'
         self._compare_error(id_, expected, given=response.body)
 
-    def test_api_invalidate_cache(self):
-        repo = RepoModel().get_by_repo_name(self.REPO)
-        repo.scm_instance_cached()  # seed cache
-
-        id_, params = _build_data(self.apikey, 'invalidate_cache',
-                                  repoid=self.REPO)
-        response = api_call(self, params)
-
-        expected = {
-            'msg': "Cache for repository `%s` was invalidated" % (self.REPO,),
-            'repository': self.REPO
-        }
-        self._compare_ok(id_, expected, given=response.body)
-
-    @mock.patch.object(ScmModel, 'mark_for_invalidation', crash)
-    def test_api_invalidate_cache_error(self):
-        id_, params = _build_data(self.apikey, 'invalidate_cache',
-                                  repoid=self.REPO)
-        response = api_call(self, params)
-
-        expected = 'Error occurred during cache invalidation action'
-        self._compare_error(id_, expected, given=response.body)
-
-    def test_api_invalidate_cache_regular_user_no_permission(self):
-        repo = RepoModel().get_by_repo_name(self.REPO)
-        repo.scm_instance_cached() # seed cache
-
-        id_, params = _build_data(self.apikey_regular, 'invalidate_cache',
-                                  repoid=self.REPO)
-        response = api_call(self, params)
-
-        expected = "repository `%s` does not exist" % (self.REPO,)
-        self._compare_error(id_, expected, given=response.body)
-
     def test_api_create_existing_user(self):
         id_, params = _build_data(self.apikey, 'create_user',
-                                  username=TEST_USER_ADMIN_LOGIN,
+                                  username=base.TEST_USER_ADMIN_LOGIN,
                                   email='test@example.com',
                                   password='trololo')
         response = api_call(self, params)
 
-        expected = "user `%s` already exist" % TEST_USER_ADMIN_LOGIN
+        expected = "user `%s` already exist" % base.TEST_USER_ADMIN_LOGIN
         self._compare_error(id_, expected, given=response.body)
 
     def test_api_create_user_with_existing_email(self):
         id_, params = _build_data(self.apikey, 'create_user',
-                                  username=TEST_USER_ADMIN_LOGIN + 'new',
-                                  email=TEST_USER_REGULAR_EMAIL,
+                                  username=base.TEST_USER_ADMIN_LOGIN + 'new',
+                                  email=base.TEST_USER_REGULAR_EMAIL,
                                   password='trololo')
         response = api_call(self, params)
 
-        expected = "email `%s` already exist" % TEST_USER_REGULAR_EMAIL
+        expected = "email `%s` already exist" % base.TEST_USER_REGULAR_EMAIL
         self._compare_error(id_, expected, given=response.body)
 
     def test_api_create_user(self):
@@ -489,10 +454,10 @@
         self._compare_error(id_, expected, given=response.body)
 
     def test_api_delete_user(self):
-        usr = UserModel().create_or_update(username=u'test_user',
-                                           password=u'qweqwe',
-                                           email=u'u232@example.com',
-                                           firstname=u'u1', lastname=u'u1')
+        usr = UserModel().create_or_update(username='test_user',
+                                           password='qweqwe',
+                                           email='u232@example.com',
+                                           firstname='u1', lastname='u1')
         Session().commit()
         username = usr.username
         email = usr.email
@@ -510,10 +475,10 @@
 
     @mock.patch.object(UserModel, 'delete', crash)
     def test_api_delete_user_when_exception_happened(self):
-        usr = UserModel().create_or_update(username=u'test_user',
-                                           password=u'qweqwe',
-                                           email=u'u232@example.com',
-                                           firstname=u'u1', lastname=u'u1')
+        usr = UserModel().create_or_update(username='test_user',
+                                           password='qweqwe',
+                                           email='u232@example.com',
+                                           firstname='u1', lastname='u1')
         Session().commit()
         username = usr.username
 
@@ -525,7 +490,7 @@
         expected = ret
         self._compare_error(id_, expected, given=response.body)
 
-    @parametrize('name,expected', [
+    @base.parametrize('name,expected', [
         ('firstname', 'new_username'),
         ('lastname', 'new_username'),
         ('email', 'new_username'),
@@ -558,22 +523,22 @@
         self._compare_ok(id_, expected, given=response.body)
 
     def test_api_update_user_no_changed_params(self):
-        usr = User.get_by_username(TEST_USER_ADMIN_LOGIN)
+        usr = User.get_by_username(base.TEST_USER_ADMIN_LOGIN)
         ret = jsonify(usr.get_api_data())
         id_, params = _build_data(self.apikey, 'update_user',
-                                  userid=TEST_USER_ADMIN_LOGIN)
+                                  userid=base.TEST_USER_ADMIN_LOGIN)
 
         response = api_call(self, params)
         ret = {
             'msg': 'updated user ID:%s %s' % (
-                usr.user_id, TEST_USER_ADMIN_LOGIN),
+                usr.user_id, base.TEST_USER_ADMIN_LOGIN),
             'user': ret
         }
         expected = ret
         self._compare_ok(id_, expected, given=response.body)
 
     def test_api_update_user_by_user_id(self):
-        usr = User.get_by_username(TEST_USER_ADMIN_LOGIN)
+        usr = User.get_by_username(base.TEST_USER_ADMIN_LOGIN)
         ret = jsonify(usr.get_api_data())
         id_, params = _build_data(self.apikey, 'update_user',
                                   userid=usr.user_id)
@@ -581,7 +546,7 @@
         response = api_call(self, params)
         ret = {
             'msg': 'updated user ID:%s %s' % (
-                usr.user_id, TEST_USER_ADMIN_LOGIN),
+                usr.user_id, base.TEST_USER_ADMIN_LOGIN),
             'user': ret
         }
         expected = ret
@@ -598,7 +563,7 @@
 
     @mock.patch.object(UserModel, 'update_user', crash)
     def test_api_update_user_when_exception_happens(self):
-        usr = User.get_by_username(TEST_USER_ADMIN_LOGIN)
+        usr = User.get_by_username(base.TEST_USER_ADMIN_LOGIN)
         ret = jsonify(usr.get_api_data())
         id_, params = _build_data(self.apikey, 'update_user',
                                   userid=usr.user_id)
@@ -610,7 +575,7 @@
         self._compare_error(id_, expected, given=response.body)
 
     def test_api_get_repo(self):
-        new_group = u'some_new_group'
+        new_group = 'some_new_group'
         make_user_group(new_group)
         RepoModel().grant_user_group_permission(repo=self.REPO,
                                                 group_name=new_group,
@@ -619,8 +584,8 @@
         id_, params = _build_data(self.apikey, 'get_repo',
                                   repoid=self.REPO)
         response = api_call(self, params)
-        assert u"tags" not in response.json[u'result']
-        assert u'pull_requests' not in response.json[u'result']
+        assert "tags" not in response.json['result']
+        assert 'pull_requests' not in response.json['result']
 
         repo = RepoModel().get_by_repo_name(self.REPO)
         ret = repo.get_api_data()
@@ -655,10 +620,10 @@
                                   with_revision_names=True,
                                   with_pullrequests=True)
         response = api_call(self, params)
-        assert u"v0.2.0" in response.json[u'result'][u'tags']
-        assert u'pull_requests' in response.json[u'result']
+        assert "v0.2.0" in response.json['result']['tags']
+        assert 'pull_requests' in response.json['result']
 
-    @parametrize('grant_perm', [
+    @base.parametrize('grant_perm', [
         ('repository.admin'),
         ('repository.write'),
         ('repository.read'),
@@ -673,32 +638,32 @@
         response = api_call(self, params)
 
         repo = RepoModel().get_by_repo_name(self.REPO)
-        ret = repo.get_api_data()
+        assert len(repo.repo_to_perm) >= 2  # make sure we actually are testing something - probably the default 2 permissions, possibly more
+
+        expected = repo.get_api_data()
 
         members = []
-        followers = []
-        assert 2 == len(repo.repo_to_perm)
         for user in repo.repo_to_perm:
             perm = user.permission.permission_name
             user_obj = user.user
             user_data = {'name': user_obj.username, 'type': "user",
                          'permission': perm}
             members.append(user_data)
-
         for user_group in repo.users_group_to_perm:
             perm = user_group.permission.permission_name
             user_group_obj = user_group.users_group
             user_group_data = {'name': user_group_obj.users_group_name,
                                'type': "user_group", 'permission': perm}
             members.append(user_group_data)
+        expected['members'] = members
+
+        followers = []
 
         for user in repo.followers:
             followers.append(user.user.get_api_data())
 
-        ret['members'] = members
-        ret['followers'] = followers
+        expected['followers'] = followers
 
-        expected = ret
         try:
             self._compare_ok(id_, expected, given=response.body)
         finally:
@@ -706,7 +671,7 @@
 
     def test_api_get_repo_by_non_admin_no_permission_to_repo(self):
         RepoModel().grant_user_permission(repo=self.REPO,
-                                          user=self.default_user_username,
+                                          user=User.DEFAULT_USER_NAME,
                                           perm='repository.none')
         try:
             RepoModel().grant_user_permission(repo=self.REPO,
@@ -721,7 +686,7 @@
             self._compare_error(id_, expected, given=response.body)
         finally:
             RepoModel().grant_user_permission(repo=self.REPO,
-                                              user=self.default_user_username,
+                                              user=User.DEFAULT_USER_NAME,
                                               perm='repository.read')
 
     def test_api_get_repo_that_doesn_not_exist(self):
@@ -755,7 +720,7 @@
 
         self._compare_ok(id_, expected, given=response.body)
 
-    @parametrize('name,ret_type', [
+    @base.parametrize('name,ret_type', [
         ('all', 'all'),
         ('dirs', 'dirs'),
         ('files', 'files'),
@@ -807,10 +772,10 @@
         response = api_call(self, params)
 
         expected = ('ret_type must be one of %s'
-                    % (','.join(['files', 'dirs', 'all'])))
+                    % (','.join(sorted(['files', 'dirs', 'all']))))
         self._compare_error(id_, expected, given=response.body)
 
-    @parametrize('name,ret_type,grant_perm', [
+    @base.parametrize('name,ret_type,grant_perm', [
         ('all', 'all', 'repository.write'),
         ('dirs', 'dirs', 'repository.admin'),
         ('files', 'files', 'repository.read'),
@@ -838,10 +803,10 @@
             RepoModel().revoke_user_permission(self.REPO, self.TEST_USER_LOGIN)
 
     def test_api_create_repo(self):
-        repo_name = u'api-repo'
+        repo_name = 'api-repo'
         id_, params = _build_data(self.apikey, 'create_repo',
                                   repo_name=repo_name,
-                                  owner=TEST_USER_ADMIN_LOGIN,
+                                  owner=base.TEST_USER_ADMIN_LOGIN,
                                   repo_type=self.REPO_TYPE,
         )
         response = api_call(self, params)
@@ -857,18 +822,18 @@
         self._compare_ok(id_, expected, given=response.body)
         fixture.destroy_repo(repo_name)
 
-    @parametrize('repo_name', [
-        u'',
-        u'.',
-        u'..',
-        u':',
-        u'/',
-        u'<test>',
+    @base.parametrize('repo_name', [
+        '',
+        '.',
+        '..',
+        ':',
+        '/',
+        '<test>',
     ])
     def test_api_create_repo_bad_names(self, repo_name):
         id_, params = _build_data(self.apikey, 'create_repo',
                                   repo_name=repo_name,
-                                  owner=TEST_USER_ADMIN_LOGIN,
+                                  owner=base.TEST_USER_ADMIN_LOGIN,
                                   repo_type=self.REPO_TYPE,
         )
         response = api_call(self, params)
@@ -883,11 +848,11 @@
     def test_api_create_repo_clone_uri_local(self):
         # cloning from local repos was a mis-feature - it would bypass access control
         # TODO: introduce other test coverage of actual remote cloning
-        clone_uri = os.path.join(TESTS_TMP_PATH, self.REPO)
-        repo_name = u'api-repo'
+        clone_uri = os.path.join(base.TESTS_TMP_PATH, self.REPO)
+        repo_name = 'api-repo'
         id_, params = _build_data(self.apikey, 'create_repo',
                                   repo_name=repo_name,
-                                  owner=TEST_USER_ADMIN_LOGIN,
+                                  owner=base.TEST_USER_ADMIN_LOGIN,
                                   repo_type=self.REPO_TYPE,
                                   clone_uri=clone_uri,
         )
@@ -897,16 +862,16 @@
         fixture.destroy_repo(repo_name)
 
     def test_api_create_repo_and_repo_group(self):
-        repo_group_name = u'my_gr'
-        repo_name = u'%s/api-repo' % repo_group_name
+        repo_group_name = 'my_gr'
+        repo_name = '%s/api-repo' % repo_group_name
 
         # repo creation can no longer also create repo group
         id_, params = _build_data(self.apikey, 'create_repo',
                                   repo_name=repo_name,
-                                  owner=TEST_USER_ADMIN_LOGIN,
+                                  owner=base.TEST_USER_ADMIN_LOGIN,
                                   repo_type=self.REPO_TYPE,)
         response = api_call(self, params)
-        expected = u'repo group `%s` not found' % repo_group_name
+        expected = 'repo group `%s` not found' % repo_group_name
         self._compare_error(id_, expected, given=response.body)
         assert RepoModel().get_by_repo_name(repo_name) is None
 
@@ -916,7 +881,7 @@
 
         id_, params = _build_data(self.apikey, 'create_repo',
                                   repo_name=repo_name,
-                                  owner=TEST_USER_ADMIN_LOGIN,
+                                  owner=base.TEST_USER_ADMIN_LOGIN,
                                   repo_type=self.REPO_TYPE,)
         response = api_call(self, params)
         expected = {
@@ -932,9 +897,9 @@
         fixture.destroy_repo_group(repo_group_name)
 
     def test_api_create_repo_in_repo_group_without_permission(self):
-        repo_group_basename = u'api-repo-repo'
-        repo_group_name = u'%s/%s' % (TEST_REPO_GROUP, repo_group_basename)
-        repo_name = u'%s/api-repo' % repo_group_name
+        repo_group_basename = 'api-repo-repo'
+        repo_group_name = '%s/%s' % (TEST_REPO_GROUP, repo_group_basename)
+        repo_name = '%s/api-repo' % repo_group_name
 
         top_group = RepoGroup.get_by_group_name(TEST_REPO_GROUP)
         assert top_group
@@ -968,7 +933,7 @@
         fixture.destroy_repo_group(repo_group_name)
 
     def test_api_create_repo_unknown_owner(self):
-        repo_name = u'api-repo'
+        repo_name = 'api-repo'
         owner = 'i-dont-exist'
         id_, params = _build_data(self.apikey, 'create_repo',
                                   repo_name=repo_name,
@@ -980,7 +945,7 @@
         self._compare_error(id_, expected, given=response.body)
 
     def test_api_create_repo_dont_specify_owner(self):
-        repo_name = u'api-repo'
+        repo_name = 'api-repo'
         owner = 'i-dont-exist'
         id_, params = _build_data(self.apikey, 'create_repo',
                                   repo_name=repo_name,
@@ -1000,7 +965,7 @@
         fixture.destroy_repo(repo_name)
 
     def test_api_create_repo_by_non_admin(self):
-        repo_name = u'api-repo'
+        repo_name = 'api-repo'
         owner = 'i-dont-exist'
         id_, params = _build_data(self.apikey_regular, 'create_repo',
                                   repo_name=repo_name,
@@ -1020,7 +985,7 @@
         fixture.destroy_repo(repo_name)
 
     def test_api_create_repo_by_non_admin_specify_owner(self):
-        repo_name = u'api-repo'
+        repo_name = 'api-repo'
         owner = 'i-dont-exist'
         id_, params = _build_data(self.apikey_regular, 'create_repo',
                                   repo_name=repo_name,
@@ -1036,7 +1001,7 @@
         repo_name = self.REPO
         id_, params = _build_data(self.apikey, 'create_repo',
                                   repo_name=repo_name,
-                                  owner=TEST_USER_ADMIN_LOGIN,
+                                  owner=base.TEST_USER_ADMIN_LOGIN,
                                   repo_type=self.REPO_TYPE,)
         response = api_call(self, params)
         expected = "repo `%s` already exist" % repo_name
@@ -1048,38 +1013,38 @@
         repo_name = '%s/%s' % (group_name, 'could-be-outside')
         id_, params = _build_data(self.apikey, 'create_repo',
                                   repo_name=repo_name,
-                                  owner=TEST_USER_ADMIN_LOGIN,
+                                  owner=base.TEST_USER_ADMIN_LOGIN,
                                   repo_type=self.REPO_TYPE,)
         response = api_call(self, params)
-        expected = u'repo group `%s` not found' % group_name
+        expected = 'repo group `%s` not found' % group_name
         self._compare_error(id_, expected, given=response.body)
         fixture.destroy_repo(repo_name)
 
     @mock.patch.object(RepoModel, 'create', crash)
     def test_api_create_repo_exception_occurred(self):
-        repo_name = u'api-repo'
+        repo_name = 'api-repo'
         id_, params = _build_data(self.apikey, 'create_repo',
                                   repo_name=repo_name,
-                                  owner=TEST_USER_ADMIN_LOGIN,
+                                  owner=base.TEST_USER_ADMIN_LOGIN,
                                   repo_type=self.REPO_TYPE,)
         response = api_call(self, params)
         expected = 'failed to create repository `%s`' % repo_name
         self._compare_error(id_, expected, given=response.body)
 
-    @parametrize('changing_attr,updates', [
-        ('owner', {'owner': TEST_USER_REGULAR_LOGIN}),
-        ('description', {'description': u'new description'}),
+    @base.parametrize('changing_attr,updates', [
+        ('owner', {'owner': base.TEST_USER_REGULAR_LOGIN}),
+        ('description', {'description': 'new description'}),
         ('clone_uri', {'clone_uri': 'http://example.com/repo'}), # will fail - pulling from non-existing repo should fail
         ('clone_uri', {'clone_uri': '/repo'}), # will fail - pulling from local repo was a mis-feature - it would bypass access control
         ('clone_uri', {'clone_uri': None}),
         ('landing_rev', {'landing_rev': 'branch:master'}),
         ('enable_statistics', {'enable_statistics': True}),
         ('enable_downloads', {'enable_downloads': True}),
-        ('name', {'name': u'new_repo_name'}),
-        ('repo_group', {'group': u'test_group_for_update'}),
+        ('name', {'name': 'new_repo_name'}),
+        ('repo_group', {'group': 'test_group_for_update'}),
     ])
     def test_api_update_repo(self, changing_attr, updates):
-        repo_name = u'api_update_me'
+        repo_name = 'api_update_me'
         repo = fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
         if changing_attr == 'repo_group':
             fixture.create_repo_group(updates['group'])
@@ -1090,10 +1055,10 @@
         if changing_attr == 'name':
             repo_name = updates['name']
         if changing_attr == 'repo_group':
-            repo_name = u'/'.join([updates['group'], repo_name])
+            repo_name = '/'.join([updates['group'], repo_name])
         try:
             if changing_attr == 'clone_uri' and updates['clone_uri']:
-                expected = u'failed to update repo `%s`' % repo_name
+                expected = 'failed to update repo `%s`' % repo_name
                 self._compare_error(id_, expected, given=response.body)
             else:
                 expected = {
@@ -1106,22 +1071,22 @@
             if changing_attr == 'repo_group':
                 fixture.destroy_repo_group(updates['group'])
 
-    @parametrize('changing_attr,updates', [
-        ('owner', {'owner': TEST_USER_REGULAR_LOGIN}),
-        ('description', {'description': u'new description'}),
+    @base.parametrize('changing_attr,updates', [
+        ('owner', {'owner': base.TEST_USER_REGULAR_LOGIN}),
+        ('description', {'description': 'new description'}),
         ('clone_uri', {'clone_uri': 'http://example.com/repo'}), # will fail - pulling from non-existing repo should fail
         ('clone_uri', {'clone_uri': '/repo'}), # will fail - pulling from local repo was a mis-feature - it would bypass access control
         ('clone_uri', {'clone_uri': None}),
         ('landing_rev', {'landing_rev': 'branch:master'}),
         ('enable_statistics', {'enable_statistics': True}),
         ('enable_downloads', {'enable_downloads': True}),
-        ('name', {'name': u'new_repo_name'}),
-        ('repo_group', {'group': u'test_group_for_update'}),
+        ('name', {'name': 'new_repo_name'}),
+        ('repo_group', {'group': 'test_group_for_update'}),
     ])
     def test_api_update_group_repo(self, changing_attr, updates):
-        group_name = u'lololo'
+        group_name = 'lololo'
         fixture.create_repo_group(group_name)
-        repo_name = u'%s/api_update_me' % group_name
+        repo_name = '%s/api_update_me' % group_name
         repo = fixture.create_repo(repo_name, repo_group=group_name, repo_type=self.REPO_TYPE)
         if changing_attr == 'repo_group':
             fixture.create_repo_group(updates['group'])
@@ -1130,12 +1095,12 @@
                                   repoid=repo_name, **updates)
         response = api_call(self, params)
         if changing_attr == 'name':
-            repo_name = u'%s/%s' % (group_name, updates['name'])
+            repo_name = '%s/%s' % (group_name, updates['name'])
         if changing_attr == 'repo_group':
-            repo_name = u'/'.join([updates['group'], repo_name.rsplit('/', 1)[-1]])
+            repo_name = '/'.join([updates['group'], repo_name.rsplit('/', 1)[-1]])
         try:
             if changing_attr == 'clone_uri' and updates['clone_uri']:
-                expected = u'failed to update repo `%s`' % repo_name
+                expected = 'failed to update repo `%s`' % repo_name
                 self._compare_error(id_, expected, given=response.body)
             else:
                 expected = {
@@ -1150,7 +1115,7 @@
         fixture.destroy_repo_group(group_name)
 
     def test_api_update_repo_repo_group_does_not_exist(self):
-        repo_name = u'admin_owned'
+        repo_name = 'admin_owned'
         fixture.create_repo(repo_name)
         updates = {'group': 'test_group_for_update'}
         id_, params = _build_data(self.apikey, 'update_repo',
@@ -1163,7 +1128,7 @@
             fixture.destroy_repo(repo_name)
 
     def test_api_update_repo_regular_user_not_allowed(self):
-        repo_name = u'admin_owned'
+        repo_name = 'admin_owned'
         fixture.create_repo(repo_name)
         updates = {'description': 'something else'}
         id_, params = _build_data(self.apikey_regular, 'update_repo',
@@ -1177,10 +1142,10 @@
 
     @mock.patch.object(RepoModel, 'update', crash)
     def test_api_update_repo_exception_occurred(self):
-        repo_name = u'api_update_me'
+        repo_name = 'api_update_me'
         fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
         id_, params = _build_data(self.apikey, 'update_repo',
-                                  repoid=repo_name, owner=TEST_USER_ADMIN_LOGIN,)
+                                  repoid=repo_name, owner=base.TEST_USER_ADMIN_LOGIN,)
         response = api_call(self, params)
         try:
             expected = 'failed to update repo `%s`' % repo_name
@@ -1189,8 +1154,8 @@
             fixture.destroy_repo(repo_name)
 
     def test_api_update_repo_regular_user_change_repo_name(self):
-        repo_name = u'admin_owned'
-        new_repo_name = u'new_repo_name'
+        repo_name = 'admin_owned'
+        new_repo_name = 'new_repo_name'
         fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
         RepoModel().grant_user_permission(repo=repo_name,
                                           user=self.TEST_USER_LOGIN,
@@ -1209,8 +1174,8 @@
             fixture.destroy_repo(new_repo_name)
 
     def test_api_update_repo_regular_user_change_repo_name_allowed(self):
-        repo_name = u'admin_owned'
-        new_repo_name = u'new_repo_name'
+        repo_name = 'admin_owned'
+        new_repo_name = 'new_repo_name'
         repo = fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
         RepoModel().grant_user_permission(repo=repo_name,
                                           user=self.TEST_USER_LOGIN,
@@ -1232,12 +1197,12 @@
             fixture.destroy_repo(new_repo_name)
 
     def test_api_update_repo_regular_user_change_owner(self):
-        repo_name = u'admin_owned'
+        repo_name = 'admin_owned'
         fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
         RepoModel().grant_user_permission(repo=repo_name,
                                           user=self.TEST_USER_LOGIN,
                                           perm='repository.admin')
-        updates = {'owner': TEST_USER_ADMIN_LOGIN}
+        updates = {'owner': base.TEST_USER_ADMIN_LOGIN}
         id_, params = _build_data(self.apikey_regular, 'update_repo',
                                   repoid=repo_name, **updates)
         response = api_call(self, params)
@@ -1248,7 +1213,7 @@
             fixture.destroy_repo(repo_name)
 
     def test_api_delete_repo(self):
-        repo_name = u'api_delete_me'
+        repo_name = 'api_delete_me'
         fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
 
         id_, params = _build_data(self.apikey, 'delete_repo',
@@ -1266,7 +1231,7 @@
             fixture.destroy_repo(repo_name)
 
     def test_api_delete_repo_by_non_admin(self):
-        repo_name = u'api_delete_me'
+        repo_name = 'api_delete_me'
         fixture.create_repo(repo_name, repo_type=self.REPO_TYPE,
                             cur_user=self.TEST_USER_LOGIN)
         id_, params = _build_data(self.apikey_regular, 'delete_repo',
@@ -1284,7 +1249,7 @@
             fixture.destroy_repo(repo_name)
 
     def test_api_delete_repo_by_non_admin_no_permission(self):
-        repo_name = u'api_delete_me'
+        repo_name = 'api_delete_me'
         fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
         try:
             id_, params = _build_data(self.apikey_regular, 'delete_repo',
@@ -1296,7 +1261,7 @@
             fixture.destroy_repo(repo_name)
 
     def test_api_delete_repo_exception_occurred(self):
-        repo_name = u'api_delete_me'
+        repo_name = 'api_delete_me'
         fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
         try:
             with mock.patch.object(RepoModel, 'delete', crash):
@@ -1310,11 +1275,11 @@
             fixture.destroy_repo(repo_name)
 
     def test_api_fork_repo(self):
-        fork_name = u'api-repo-fork'
+        fork_name = 'api-repo-fork'
         id_, params = _build_data(self.apikey, 'fork_repo',
                                   repoid=self.REPO,
                                   fork_name=fork_name,
-                                  owner=TEST_USER_ADMIN_LOGIN,
+                                  owner=base.TEST_USER_ADMIN_LOGIN,
         )
         response = api_call(self, params)
 
@@ -1328,9 +1293,9 @@
         self._compare_ok(id_, expected, given=response.body)
         fixture.destroy_repo(fork_name)
 
-    @parametrize('fork_name', [
-        u'api-repo-fork',
-        u'%s/api-repo-fork' % TEST_REPO_GROUP,
+    @base.parametrize('fork_name', [
+        'api-repo-fork',
+        '%s/api-repo-fork' % TEST_REPO_GROUP,
     ])
     def test_api_fork_repo_non_admin(self, fork_name):
         id_, params = _build_data(self.apikey_regular, 'fork_repo',
@@ -1350,11 +1315,11 @@
         fixture.destroy_repo(fork_name)
 
     def test_api_fork_repo_non_admin_specify_owner(self):
-        fork_name = u'api-repo-fork'
+        fork_name = 'api-repo-fork'
         id_, params = _build_data(self.apikey_regular, 'fork_repo',
                                   repoid=self.REPO,
                                   fork_name=fork_name,
-                                  owner=TEST_USER_ADMIN_LOGIN,
+                                  owner=base.TEST_USER_ADMIN_LOGIN,
         )
         response = api_call(self, params)
         expected = 'Only Kallithea admin can specify `owner` param'
@@ -1363,10 +1328,10 @@
 
     def test_api_fork_repo_non_admin_no_permission_to_fork(self):
         RepoModel().grant_user_permission(repo=self.REPO,
-                                          user=self.default_user_username,
+                                          user=User.DEFAULT_USER_NAME,
                                           perm='repository.none')
         try:
-            fork_name = u'api-repo-fork'
+            fork_name = 'api-repo-fork'
             id_, params = _build_data(self.apikey_regular, 'fork_repo',
                                       repoid=self.REPO,
                                       fork_name=fork_name,
@@ -1376,17 +1341,17 @@
             self._compare_error(id_, expected, given=response.body)
         finally:
             RepoModel().grant_user_permission(repo=self.REPO,
-                                              user=self.default_user_username,
+                                              user=User.DEFAULT_USER_NAME,
                                               perm='repository.read')
             fixture.destroy_repo(fork_name)
 
-    @parametrize('name,perm', [
+    @base.parametrize('name,perm', [
         ('read', 'repository.read'),
         ('write', 'repository.write'),
         ('admin', 'repository.admin'),
     ])
     def test_api_fork_repo_non_admin_no_create_repo_permission(self, name, perm):
-        fork_name = u'api-repo-fork'
+        fork_name = 'api-repo-fork'
         # regardless of base repository permission, forking is disallowed
         # when repository creation is disabled
         RepoModel().grant_user_permission(repo=self.REPO,
@@ -1404,7 +1369,7 @@
         fixture.destroy_repo(fork_name)
 
     def test_api_fork_repo_unknown_owner(self):
-        fork_name = u'api-repo-fork'
+        fork_name = 'api-repo-fork'
         owner = 'i-dont-exist'
         id_, params = _build_data(self.apikey, 'fork_repo',
                                   repoid=self.REPO,
@@ -1416,16 +1381,16 @@
         self._compare_error(id_, expected, given=response.body)
 
     def test_api_fork_repo_fork_exists(self):
-        fork_name = u'api-repo-fork'
+        fork_name = 'api-repo-fork'
         fixture.create_fork(self.REPO, fork_name)
 
         try:
-            fork_name = u'api-repo-fork'
+            fork_name = 'api-repo-fork'
 
             id_, params = _build_data(self.apikey, 'fork_repo',
                                       repoid=self.REPO,
                                       fork_name=fork_name,
-                                      owner=TEST_USER_ADMIN_LOGIN,
+                                      owner=base.TEST_USER_ADMIN_LOGIN,
             )
             response = api_call(self, params)
 
@@ -1440,7 +1405,7 @@
         id_, params = _build_data(self.apikey, 'fork_repo',
                                   repoid=self.REPO,
                                   fork_name=fork_name,
-                                  owner=TEST_USER_ADMIN_LOGIN,
+                                  owner=base.TEST_USER_ADMIN_LOGIN,
         )
         response = api_call(self, params)
 
@@ -1449,11 +1414,11 @@
 
     @mock.patch.object(RepoModel, 'create_fork', crash)
     def test_api_fork_repo_exception_occurred(self):
-        fork_name = u'api-repo-fork'
+        fork_name = 'api-repo-fork'
         id_, params = _build_data(self.apikey, 'fork_repo',
                                   repoid=self.REPO,
                                   fork_name=fork_name,
-                                  owner=TEST_USER_ADMIN_LOGIN,
+                                  owner=base.TEST_USER_ADMIN_LOGIN,
         )
         response = api_call(self, params)
 
@@ -1478,7 +1443,7 @@
         self._compare_ok(id_, expected, given=response.body)
 
     def test_api_get_user_groups(self):
-        gr_name = u'test_user_group2'
+        gr_name = 'test_user_group2'
         make_user_group(gr_name)
 
         try:
@@ -1486,7 +1451,7 @@
             response = api_call(self, params)
 
             expected = []
-            for gr_name in [TEST_USER_GROUP, u'test_user_group2']:
+            for gr_name in [TEST_USER_GROUP, 'test_user_group2']:
                 user_group = UserGroupModel().get_group(gr_name)
                 ret = user_group.get_api_data()
                 expected.append(ret)
@@ -1495,7 +1460,7 @@
             fixture.destroy_user_group(gr_name)
 
     def test_api_create_user_group(self):
-        group_name = u'some_new_group'
+        group_name = 'some_new_group'
         id_, params = _build_data(self.apikey, 'create_user_group',
                                   group_name=group_name)
         response = api_call(self, params)
@@ -1521,7 +1486,7 @@
 
     @mock.patch.object(UserGroupModel, 'create', crash)
     def test_api_get_user_group_exception_occurred(self):
-        group_name = u'exception_happens'
+        group_name = 'exception_happens'
         id_, params = _build_data(self.apikey, 'create_user_group',
                                   group_name=group_name)
         response = api_call(self, params)
@@ -1529,15 +1494,15 @@
         expected = 'failed to create group `%s`' % group_name
         self._compare_error(id_, expected, given=response.body)
 
-    @parametrize('changing_attr,updates', [
-        ('group_name', {'group_name': u'new_group_name'}),
-        ('group_name', {'group_name': u'test_group_for_update'}),
-        ('owner', {'owner': TEST_USER_REGULAR_LOGIN}),
+    @base.parametrize('changing_attr,updates', [
+        ('group_name', {'group_name': 'new_group_name'}),
+        ('group_name', {'group_name': 'test_group_for_update'}),
+        ('owner', {'owner': base.TEST_USER_REGULAR_LOGIN}),
         ('active', {'active': False}),
         ('active', {'active': True}),
     ])
     def test_api_update_user_group(self, changing_attr, updates):
-        gr_name = u'test_group_for_update'
+        gr_name = 'test_group_for_update'
         user_group = fixture.create_user_group(gr_name)
         try:
             id_, params = _build_data(self.apikey, 'update_user_group',
@@ -1557,7 +1522,7 @@
 
     @mock.patch.object(UserGroupModel, 'update', crash)
     def test_api_update_user_group_exception_occurred(self):
-        gr_name = u'test_group'
+        gr_name = 'test_group'
         fixture.create_user_group(gr_name)
         try:
             id_, params = _build_data(self.apikey, 'update_user_group',
@@ -1569,16 +1534,16 @@
             fixture.destroy_user_group(gr_name)
 
     def test_api_add_user_to_user_group(self):
-        gr_name = u'test_group'
+        gr_name = 'test_group'
         fixture.create_user_group(gr_name)
         try:
             id_, params = _build_data(self.apikey, 'add_user_to_user_group',
                                       usergroupid=gr_name,
-                                      userid=TEST_USER_ADMIN_LOGIN)
+                                      userid=base.TEST_USER_ADMIN_LOGIN)
             response = api_call(self, params)
             expected = {
             'msg': 'added member `%s` to user group `%s`' % (
-                    TEST_USER_ADMIN_LOGIN, gr_name),
+                    base.TEST_USER_ADMIN_LOGIN, gr_name),
             'success': True
             }
             self._compare_ok(id_, expected, given=response.body)
@@ -1588,7 +1553,7 @@
     def test_api_add_user_to_user_group_that_doesnt_exist(self):
         id_, params = _build_data(self.apikey, 'add_user_to_user_group',
                                   usergroupid='false-group',
-                                  userid=TEST_USER_ADMIN_LOGIN)
+                                  userid=base.TEST_USER_ADMIN_LOGIN)
         response = api_call(self, params)
 
         expected = 'user group `%s` does not exist' % 'false-group'
@@ -1596,12 +1561,12 @@
 
     @mock.patch.object(UserGroupModel, 'add_user_to_group', crash)
     def test_api_add_user_to_user_group_exception_occurred(self):
-        gr_name = u'test_group'
+        gr_name = 'test_group'
         fixture.create_user_group(gr_name)
         try:
             id_, params = _build_data(self.apikey, 'add_user_to_user_group',
                                       usergroupid=gr_name,
-                                      userid=TEST_USER_ADMIN_LOGIN)
+                                      userid=base.TEST_USER_ADMIN_LOGIN)
             response = api_call(self, params)
             expected = 'failed to add member to user group `%s`' % gr_name
             self._compare_error(id_, expected, given=response.body)
@@ -1609,18 +1574,18 @@
             fixture.destroy_user_group(gr_name)
 
     def test_api_remove_user_from_user_group(self):
-        gr_name = u'test_group_3'
+        gr_name = 'test_group_3'
         gr = fixture.create_user_group(gr_name)
-        UserGroupModel().add_user_to_group(gr, user=TEST_USER_ADMIN_LOGIN)
+        UserGroupModel().add_user_to_group(gr, user=base.TEST_USER_ADMIN_LOGIN)
         Session().commit()
         try:
             id_, params = _build_data(self.apikey, 'remove_user_from_user_group',
                                       usergroupid=gr_name,
-                                      userid=TEST_USER_ADMIN_LOGIN)
+                                      userid=base.TEST_USER_ADMIN_LOGIN)
             response = api_call(self, params)
             expected = {
                 'msg': 'removed member `%s` from user group `%s`' % (
-                    TEST_USER_ADMIN_LOGIN, gr_name
+                    base.TEST_USER_ADMIN_LOGIN, gr_name
                 ),
                 'success': True}
             self._compare_ok(id_, expected, given=response.body)
@@ -1629,14 +1594,14 @@
 
     @mock.patch.object(UserGroupModel, 'remove_user_from_group', crash)
     def test_api_remove_user_from_user_group_exception_occurred(self):
-        gr_name = u'test_group_3'
+        gr_name = 'test_group_3'
         gr = fixture.create_user_group(gr_name)
-        UserGroupModel().add_user_to_group(gr, user=TEST_USER_ADMIN_LOGIN)
+        UserGroupModel().add_user_to_group(gr, user=base.TEST_USER_ADMIN_LOGIN)
         Session().commit()
         try:
             id_, params = _build_data(self.apikey, 'remove_user_from_user_group',
                                       usergroupid=gr_name,
-                                      userid=TEST_USER_ADMIN_LOGIN)
+                                      userid=base.TEST_USER_ADMIN_LOGIN)
             response = api_call(self, params)
             expected = 'failed to remove member from user group `%s`' % gr_name
             self._compare_error(id_, expected, given=response.body)
@@ -1644,7 +1609,7 @@
             fixture.destroy_user_group(gr_name)
 
     def test_api_delete_user_group(self):
-        gr_name = u'test_group'
+        gr_name = 'test_group'
         ugroup = fixture.create_user_group(gr_name)
         gr_id = ugroup.users_group_id
         try:
@@ -1661,7 +1626,7 @@
                 fixture.destroy_user_group(gr_name)
 
     def test_api_delete_user_group_that_is_assigned(self):
-        gr_name = u'test_group'
+        gr_name = 'test_group'
         ugroup = fixture.create_user_group(gr_name)
         gr_id = ugroup.users_group_id
 
@@ -1679,7 +1644,7 @@
                 fixture.destroy_user_group(gr_name)
 
     def test_api_delete_user_group_exception_occurred(self):
-        gr_name = u'test_group'
+        gr_name = 'test_group'
         ugroup = fixture.create_user_group(gr_name)
         gr_id = ugroup.users_group_id
         id_, params = _build_data(self.apikey, 'delete_user_group',
@@ -1693,7 +1658,7 @@
         finally:
             fixture.destroy_user_group(gr_name)
 
-    @parametrize('name,perm', [
+    @base.parametrize('name,perm', [
         ('none', 'repository.none'),
         ('read', 'repository.read'),
         ('write', 'repository.write'),
@@ -1703,13 +1668,13 @@
         id_, params = _build_data(self.apikey,
                                   'grant_user_permission',
                                   repoid=self.REPO,
-                                  userid=TEST_USER_ADMIN_LOGIN,
+                                  userid=base.TEST_USER_ADMIN_LOGIN,
                                   perm=perm)
         response = api_call(self, params)
 
         ret = {
             'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
-                perm, TEST_USER_ADMIN_LOGIN, self.REPO
+                perm, base.TEST_USER_ADMIN_LOGIN, self.REPO
             ),
             'success': True
         }
@@ -1721,7 +1686,7 @@
         id_, params = _build_data(self.apikey,
                                   'grant_user_permission',
                                   repoid=self.REPO,
-                                  userid=TEST_USER_ADMIN_LOGIN,
+                                  userid=base.TEST_USER_ADMIN_LOGIN,
                                   perm=perm)
         response = api_call(self, params)
 
@@ -1734,12 +1699,12 @@
         id_, params = _build_data(self.apikey,
                                   'grant_user_permission',
                                   repoid=self.REPO,
-                                  userid=TEST_USER_ADMIN_LOGIN,
+                                  userid=base.TEST_USER_ADMIN_LOGIN,
                                   perm=perm)
         response = api_call(self, params)
 
         expected = 'failed to edit permission for user: `%s` in repo: `%s`' % (
-            TEST_USER_ADMIN_LOGIN, self.REPO
+            base.TEST_USER_ADMIN_LOGIN, self.REPO
         )
         self._compare_error(id_, expected, given=response.body)
 
@@ -1747,12 +1712,12 @@
         id_, params = _build_data(self.apikey,
                                   'revoke_user_permission',
                                   repoid=self.REPO,
-                                  userid=TEST_USER_ADMIN_LOGIN, )
+                                  userid=base.TEST_USER_ADMIN_LOGIN, )
         response = api_call(self, params)
 
         expected = {
             'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
-                TEST_USER_ADMIN_LOGIN, self.REPO
+                base.TEST_USER_ADMIN_LOGIN, self.REPO
             ),
             'success': True
         }
@@ -1763,15 +1728,15 @@
         id_, params = _build_data(self.apikey,
                                   'revoke_user_permission',
                                   repoid=self.REPO,
-                                  userid=TEST_USER_ADMIN_LOGIN, )
+                                  userid=base.TEST_USER_ADMIN_LOGIN, )
         response = api_call(self, params)
 
         expected = 'failed to edit permission for user: `%s` in repo: `%s`' % (
-            TEST_USER_ADMIN_LOGIN, self.REPO
+            base.TEST_USER_ADMIN_LOGIN, self.REPO
         )
         self._compare_error(id_, expected, given=response.body)
 
-    @parametrize('name,perm', [
+    @base.parametrize('name,perm', [
         ('none', 'repository.none'),
         ('read', 'repository.read'),
         ('write', 'repository.write'),
@@ -1853,7 +1818,7 @@
         )
         self._compare_error(id_, expected, given=response.body)
 
-    @parametrize('name,perm,apply_to_children', [
+    @base.parametrize('name,perm,apply_to_children', [
         ('none', 'group.none', 'none'),
         ('read', 'group.read', 'none'),
         ('write', 'group.write', 'none'),
@@ -1878,20 +1843,20 @@
         id_, params = _build_data(self.apikey,
                                   'grant_user_permission_to_repo_group',
                                   repogroupid=TEST_REPO_GROUP,
-                                  userid=TEST_USER_ADMIN_LOGIN,
+                                  userid=base.TEST_USER_ADMIN_LOGIN,
                                   perm=perm, apply_to_children=apply_to_children)
         response = api_call(self, params)
 
         ret = {
             'msg': 'Granted perm: `%s` (recursive:%s) for user: `%s` in repo group: `%s`' % (
-                perm, apply_to_children, TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
+                perm, apply_to_children, base.TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
             ),
             'success': True
         }
         expected = ret
         self._compare_ok(id_, expected, given=response.body)
 
-    @parametrize('name,perm,apply_to_children,grant_admin,access_ok', [
+    @base.parametrize('name,perm,apply_to_children,grant_admin,access_ok', [
         ('none_fails', 'group.none', 'none', False, False),
         ('read_fails', 'group.read', 'none', False, False),
         ('write_fails', 'group.write', 'none', False, False),
@@ -1914,13 +1879,13 @@
         id_, params = _build_data(self.apikey_regular,
                                   'grant_user_permission_to_repo_group',
                                   repogroupid=TEST_REPO_GROUP,
-                                  userid=TEST_USER_ADMIN_LOGIN,
+                                  userid=base.TEST_USER_ADMIN_LOGIN,
                                   perm=perm, apply_to_children=apply_to_children)
         response = api_call(self, params)
         if access_ok:
             ret = {
                 'msg': 'Granted perm: `%s` (recursive:%s) for user: `%s` in repo group: `%s`' % (
-                    perm, apply_to_children, TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
+                    perm, apply_to_children, base.TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
                 ),
                 'success': True
             }
@@ -1935,7 +1900,7 @@
         id_, params = _build_data(self.apikey,
                                   'grant_user_permission_to_repo_group',
                                   repogroupid=TEST_REPO_GROUP,
-                                  userid=TEST_USER_ADMIN_LOGIN,
+                                  userid=base.TEST_USER_ADMIN_LOGIN,
                                   perm=perm)
         response = api_call(self, params)
 
@@ -1948,16 +1913,16 @@
         id_, params = _build_data(self.apikey,
                                   'grant_user_permission_to_repo_group',
                                   repogroupid=TEST_REPO_GROUP,
-                                  userid=TEST_USER_ADMIN_LOGIN,
+                                  userid=base.TEST_USER_ADMIN_LOGIN,
                                   perm=perm)
         response = api_call(self, params)
 
         expected = 'failed to edit permission for user: `%s` in repo group: `%s`' % (
-            TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
+            base.TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
         )
         self._compare_error(id_, expected, given=response.body)
 
-    @parametrize('name,apply_to_children', [
+    @base.parametrize('name,apply_to_children', [
         ('none', 'none'),
         ('all', 'all'),
         ('repos', 'repos'),
@@ -1965,26 +1930,26 @@
     ])
     def test_api_revoke_user_permission_from_repo_group(self, name, apply_to_children):
         RepoGroupModel().grant_user_permission(repo_group=TEST_REPO_GROUP,
-                                               user=TEST_USER_ADMIN_LOGIN,
+                                               user=base.TEST_USER_ADMIN_LOGIN,
                                                perm='group.read',)
         Session().commit()
 
         id_, params = _build_data(self.apikey,
                                   'revoke_user_permission_from_repo_group',
                                   repogroupid=TEST_REPO_GROUP,
-                                  userid=TEST_USER_ADMIN_LOGIN,
+                                  userid=base.TEST_USER_ADMIN_LOGIN,
                                   apply_to_children=apply_to_children,)
         response = api_call(self, params)
 
         expected = {
             'msg': 'Revoked perm (recursive:%s) for user: `%s` in repo group: `%s`' % (
-                apply_to_children, TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
+                apply_to_children, base.TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
             ),
             'success': True
         }
         self._compare_ok(id_, expected, given=response.body)
 
-    @parametrize('name,apply_to_children,grant_admin,access_ok', [
+    @base.parametrize('name,apply_to_children,grant_admin,access_ok', [
         ('none', 'none', False, False),
         ('all', 'all', False, False),
         ('repos', 'repos', False, False),
@@ -1999,7 +1964,7 @@
     def test_api_revoke_user_permission_from_repo_group_by_regular_user(
             self, name, apply_to_children, grant_admin, access_ok):
         RepoGroupModel().grant_user_permission(repo_group=TEST_REPO_GROUP,
-                                               user=TEST_USER_ADMIN_LOGIN,
+                                               user=base.TEST_USER_ADMIN_LOGIN,
                                                perm='group.read',)
         Session().commit()
 
@@ -2012,13 +1977,13 @@
         id_, params = _build_data(self.apikey_regular,
                                   'revoke_user_permission_from_repo_group',
                                   repogroupid=TEST_REPO_GROUP,
-                                  userid=TEST_USER_ADMIN_LOGIN,
+                                  userid=base.TEST_USER_ADMIN_LOGIN,
                                   apply_to_children=apply_to_children,)
         response = api_call(self, params)
         if access_ok:
             expected = {
                 'msg': 'Revoked perm (recursive:%s) for user: `%s` in repo group: `%s`' % (
-                    apply_to_children, TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
+                    apply_to_children, base.TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
                 ),
                 'success': True
             }
@@ -2032,15 +1997,15 @@
         id_, params = _build_data(self.apikey,
                                   'revoke_user_permission_from_repo_group',
                                   repogroupid=TEST_REPO_GROUP,
-                                  userid=TEST_USER_ADMIN_LOGIN, )
+                                  userid=base.TEST_USER_ADMIN_LOGIN, )
         response = api_call(self, params)
 
         expected = 'failed to edit permission for user: `%s` in repo group: `%s`' % (
-            TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
+            base.TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
         )
         self._compare_error(id_, expected, given=response.body)
 
-    @parametrize('name,perm,apply_to_children', [
+    @base.parametrize('name,perm,apply_to_children', [
         ('none', 'group.none', 'none'),
         ('read', 'group.read', 'none'),
         ('write', 'group.write', 'none'),
@@ -2079,7 +2044,7 @@
         expected = ret
         self._compare_ok(id_, expected, given=response.body)
 
-    @parametrize('name,perm,apply_to_children,grant_admin,access_ok', [
+    @base.parametrize('name,perm,apply_to_children,grant_admin,access_ok', [
         ('none_fails', 'group.none', 'none', False, False),
         ('read_fails', 'group.read', 'none', False, False),
         ('write_fails', 'group.write', 'none', False, False),
@@ -2146,7 +2111,7 @@
         )
         self._compare_error(id_, expected, given=response.body)
 
-    @parametrize('name,apply_to_children', [
+    @base.parametrize('name,apply_to_children', [
         ('none', 'none'),
         ('all', 'all'),
         ('repos', 'repos'),
@@ -2172,7 +2137,7 @@
         }
         self._compare_ok(id_, expected, given=response.body)
 
-    @parametrize('name,apply_to_children,grant_admin,access_ok', [
+    @base.parametrize('name,apply_to_children,grant_admin,access_ok', [
         ('none', 'none', False, False),
         ('all', 'all', False, False),
         ('repos', 'repos', False, False),
@@ -2187,7 +2152,7 @@
     def test_api_revoke_user_group_permission_from_repo_group_by_regular_user(
             self, name, apply_to_children, grant_admin, access_ok):
         RepoGroupModel().grant_user_permission(repo_group=TEST_REPO_GROUP,
-                                               user=TEST_USER_ADMIN_LOGIN,
+                                               user=base.TEST_USER_ADMIN_LOGIN,
                                                perm='group.read',)
         Session().commit()
 
@@ -2206,7 +2171,7 @@
         if access_ok:
             expected = {
                 'msg': 'Revoked perm (recursive:%s) for user group: `%s` in repo group: `%s`' % (
-                    apply_to_children, TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
+                    apply_to_children, base.TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
                 ),
                 'success': True
             }
@@ -2310,7 +2275,7 @@
 
     def test_api_get_gists_regular_user_with_different_userid(self):
         id_, params = _build_data(self.apikey_regular, 'get_gists',
-                                  userid=TEST_USER_ADMIN_LOGIN)
+                                  userid=base.TEST_USER_ADMIN_LOGIN)
         response = api_call(self, params)
         expected = 'userid is not the same as your user'
         self._compare_error(id_, expected, given=response.body)
@@ -2322,16 +2287,15 @@
                                   gist_type='public',
                                   files={'foobar': {'content': 'foo'}})
         response = api_call(self, params)
-        response_json = response.json
         expected = {
             'gist': {
-                'access_id': response_json['result']['gist']['access_id'],
-                'created_on': response_json['result']['gist']['created_on'],
+                'access_id': response.json['result']['gist']['access_id'],
+                'created_on': response.json['result']['gist']['created_on'],
                 'description': 'foobar-gist',
-                'expires': response_json['result']['gist']['expires'],
-                'gist_id': response_json['result']['gist']['gist_id'],
+                'expires': response.json['result']['gist']['expires'],
+                'gist_id': response.json['result']['gist']['gist_id'],
                 'type': 'public',
-                'url': response_json['result']['gist']['url']
+                'url': response.json['result']['gist']['url']
             },
             'msg': 'created new gist'
         }
@@ -2397,7 +2361,7 @@
         id_, params = _build_data(self.apikey, 'get_changesets',
                                   repoid=self.REPO, start=0, end=2)
         response = api_call(self, params)
-        result = json.loads(response.body)["result"]
+        result = ext_json.loads(response.body)["result"]
         assert len(result) == 3
         assert 'message' in result[0]
         assert 'added' not in result[0]
@@ -2406,7 +2370,7 @@
         id_, params = _build_data(self.apikey, 'get_changesets',
                                   repoid=self.REPO, start_date="2011-02-24T00:00:00", max_revisions=10)
         response = api_call(self, params)
-        result = json.loads(response.body)["result"]
+        result = ext_json.loads(response.body)["result"]
         assert len(result) == 10
         assert 'message' in result[0]
         assert 'added' not in result[0]
@@ -2419,7 +2383,7 @@
         id_, params = _build_data(self.apikey, 'get_changesets',
                                   repoid=self.REPO, branch_name=branch, start_date="2011-02-24T00:00:00")
         response = api_call(self, params)
-        result = json.loads(response.body)["result"]
+        result = ext_json.loads(response.body)["result"]
         assert len(result) == 5
         assert 'message' in result[0]
         assert 'added' not in result[0]
@@ -2428,7 +2392,7 @@
         id_, params = _build_data(self.apikey, 'get_changesets',
                                   repoid=self.REPO, start_date="2010-04-07T23:30:30", end_date="2010-04-08T00:31:14", with_file_list=True)
         response = api_call(self, params)
-        result = json.loads(response.body)["result"]
+        result = ext_json.loads(response.body)["result"]
         assert len(result) == 3
         assert 'message' in result[0]
         assert 'added' in result[0]
@@ -2438,7 +2402,7 @@
         id_, params = _build_data(self.apikey, 'get_changeset',
                                   repoid=self.REPO, raw_id=self.TEST_REVISION)
         response = api_call(self, params)
-        result = json.loads(response.body)["result"]
+        result = ext_json.loads(response.body)["result"]
         assert result["raw_id"] == self.TEST_REVISION
         assert "reviews" not in result
 
@@ -2448,7 +2412,7 @@
                                   repoid=self.REPO, raw_id=self.TEST_REVISION,
                                   with_reviews=True)
         response = api_call(self, params)
-        result = json.loads(response.body)["result"]
+        result = ext_json.loads(response.body)["result"]
         assert result["raw_id"] == self.TEST_REVISION
         assert "reviews" in result
         assert len(result["reviews"]) == 1
@@ -2468,7 +2432,7 @@
         id_, params = _build_data(self.apikey, 'get_changeset',
                                   repoid=self.REPO, raw_id = '7ab37bc680b4aa72c34d07b230c866c28e9fcfff')
         response = api_call(self, params)
-        expected = u'Changeset %s does not exist' % ('7ab37bc680b4aa72c34d07b230c866c28e9fcfff',)
+        expected = 'Changeset %s does not exist' % ('7ab37bc680b4aa72c34d07b230c866c28e9fcfff',)
         self._compare_error(id_, expected, given=response.body)
 
     def test_api_get_changeset_without_permission(self):
@@ -2478,18 +2442,18 @@
         id_, params = _build_data(self.apikey_regular, 'get_changeset',
                                   repoid=self.REPO, raw_id=self.TEST_REVISION)
         response = api_call(self, params)
-        expected = u'Access denied to repo %s' % self.REPO
+        expected = 'Access denied to repo %s' % self.REPO
         self._compare_error(id_, expected, given=response.body)
 
     def test_api_get_pullrequest(self):
-        pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, u'get test')
+        pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'get test')
         random_id = random.randrange(1, 9999)
-        params = json.dumps({
+        params = ascii_bytes(ext_json.dumps({
             "id": random_id,
             "api_key": self.apikey,
             "method": 'get_pullrequest',
             "args": {"pullrequest_id": pull_request_id},
-        })
+        }))
         response = api_call(self, params)
         pullrequest = PullRequest().get(pull_request_id)
         expected = {
@@ -2501,26 +2465,26 @@
             "org_repo_url": "http://localhost:80/%s" % self.REPO,
             "org_ref_parts": ["branch", "stable", self.TEST_PR_SRC],
             "other_ref_parts": ["branch", "default", self.TEST_PR_DST],
-            "comments": [{"username": TEST_USER_ADMIN_LOGIN, "text": "",
+            "comments": [{"username": base.TEST_USER_ADMIN_LOGIN, "text": "",
                          "comment_id": pullrequest.comments[0].comment_id}],
-            "owner": TEST_USER_ADMIN_LOGIN,
-            "statuses": [{"status": "under_review", "reviewer": TEST_USER_ADMIN_LOGIN, "modified_at": "2000-01-01T00:00:00"} for i in range(0, len(self.TEST_PR_REVISIONS))],
+            "owner": base.TEST_USER_ADMIN_LOGIN,
+            "statuses": [{"status": "under_review", "reviewer": base.TEST_USER_ADMIN_LOGIN, "modified_at": "2000-01-01T00:00:00"} for i in range(0, len(self.TEST_PR_REVISIONS))],
             "title": "get test",
             "revisions": self.TEST_PR_REVISIONS,
         }
         self._compare_ok(random_id, expected,
-                         given=re.sub("\d\d\d\d\-\d\d\-\d\dT\d\d\:\d\d\:\d\d",
-                                      "2000-01-01T00:00:00", response.body))
+                         given=re.sub(br"\d\d\d\d\-\d\d\-\d\dT\d\d\:\d\d\:\d\d",
+                                      b"2000-01-01T00:00:00", response.body))
 
     def test_api_close_pullrequest(self):
-        pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, u'close test')
+        pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'close test')
         random_id = random.randrange(1, 9999)
-        params = json.dumps({
+        params = ascii_bytes(ext_json.dumps({
             "id": random_id,
             "api_key": self.apikey,
             "method": "comment_pullrequest",
             "args": {"pull_request_id": pull_request_id, "close_pr": True},
-        })
+        }))
         response = api_call(self, params)
         self._compare_ok(random_id, True, given=response.body)
         pullrequest = PullRequest().get(pull_request_id)
@@ -2529,40 +2493,40 @@
         assert pullrequest.is_closed() == True
 
     def test_api_status_pullrequest(self):
-        pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, u"status test")
+        pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, "status test")
 
         random_id = random.randrange(1, 9999)
-        params = json.dumps({
+        params = ascii_bytes(ext_json.dumps({
             "id": random_id,
-            "api_key": User.get_by_username(TEST_USER_REGULAR2_LOGIN).api_key,
+            "api_key": User.get_by_username(base.TEST_USER_REGULAR2_LOGIN).api_key,
             "method": "comment_pullrequest",
             "args": {"pull_request_id": pull_request_id, "status": ChangesetStatus.STATUS_APPROVED},
-        })
+        }))
         response = api_call(self, params)
         pullrequest = PullRequest().get(pull_request_id)
         self._compare_error(random_id, "No permission to change pull request status. User needs to be admin, owner or reviewer.", given=response.body)
         assert ChangesetStatus.STATUS_UNDER_REVIEW == ChangesetStatusModel().calculate_pull_request_result(pullrequest)[2]
-        params = json.dumps({
+        params = ascii_bytes(ext_json.dumps({
             "id": random_id,
-            "api_key": User.get_by_username(TEST_USER_REGULAR_LOGIN).api_key,
+            "api_key": User.get_by_username(base.TEST_USER_REGULAR_LOGIN).api_key,
             "method": "comment_pullrequest",
             "args": {"pull_request_id": pull_request_id, "status": ChangesetStatus.STATUS_APPROVED},
-        })
+        }))
         response = api_call(self, params)
         self._compare_ok(random_id, True, given=response.body)
         pullrequest = PullRequest().get(pull_request_id)
         assert ChangesetStatus.STATUS_APPROVED == ChangesetStatusModel().calculate_pull_request_result(pullrequest)[2]
 
     def test_api_comment_pullrequest(self):
-        pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, u"comment test")
+        pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, "comment test")
         random_id = random.randrange(1, 9999)
-        params = json.dumps({
+        params = ascii_bytes(ext_json.dumps({
             "id": random_id,
             "api_key": self.apikey,
             "method": "comment_pullrequest",
             "args": {"pull_request_id": pull_request_id, "comment_msg": "Looks good to me"},
-        })
+        }))
         response = api_call(self, params)
         self._compare_ok(random_id, True, given=response.body)
         pullrequest = PullRequest().get(pull_request_id)
-        assert pullrequest.comments[-1].text == u'Looks good to me'
+        assert pullrequest.comments[-1].text == 'Looks good to me'
--- a/kallithea/tests/api/test_api_git.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/api/test_api_git.py	Mon May 04 19:24:04 2020 +0200
@@ -20,9 +20,9 @@
     REPO = GIT_REPO
     REPO_TYPE = 'git'
     TEST_REVISION = GIT_TEST_REVISION
-    TEST_PR_SRC = u'c60f01b77c42dce653d6b1d3b04689862c261929'
-    TEST_PR_DST = u'10cddef6b794696066fb346434014f0a56810218'
-    TEST_PR_REVISIONS = [u'1bead5880d2dbe831762bf7fb439ba2919b75fdd',
-                         u'9bcd3ecfc8832a8cd881c1c1bbe2d13ffa9d94c7',
-                         u'283de4dfca8479875a1befb8d4059f3bbb725145',
-                         u'c60f01b77c42dce653d6b1d3b04689862c261929']
+    TEST_PR_SRC = 'c60f01b77c42dce653d6b1d3b04689862c261929'
+    TEST_PR_DST = '10cddef6b794696066fb346434014f0a56810218'
+    TEST_PR_REVISIONS = ['1bead5880d2dbe831762bf7fb439ba2919b75fdd',
+                         '9bcd3ecfc8832a8cd881c1c1bbe2d13ffa9d94c7',
+                         '283de4dfca8479875a1befb8d4059f3bbb725145',
+                         'c60f01b77c42dce653d6b1d3b04689862c261929']
--- a/kallithea/tests/api/test_api_hg.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/api/test_api_hg.py	Mon May 04 19:24:04 2020 +0200
@@ -20,10 +20,10 @@
     REPO = HG_REPO
     REPO_TYPE = 'hg'
     TEST_REVISION = HG_TEST_REVISION
-    TEST_PR_SRC = u'4f7e2131323e0749a740c0a56ab68ae9269c562a'
-    TEST_PR_DST = u'92831aebf2f8dd4879e897024b89d09af214df1c'
-    TEST_PR_REVISIONS = [u'720bbdb27665d6262b313e8a541b654d0cbd5b27',
-                         u'f41649565a9e89919a588a163e717b4084f8a3b1',
-                         u'94f45ed825a113e61af7e141f44ca578374abef0',
-                         u'fef5bfe1dc17611d5fb59a7f6f95c55c3606f933',
-                         u'4f7e2131323e0749a740c0a56ab68ae9269c562a']
+    TEST_PR_SRC = '4f7e2131323e0749a740c0a56ab68ae9269c562a'
+    TEST_PR_DST = '92831aebf2f8dd4879e897024b89d09af214df1c'
+    TEST_PR_REVISIONS = ['720bbdb27665d6262b313e8a541b654d0cbd5b27',
+                         'f41649565a9e89919a588a163e717b4084f8a3b1',
+                         '94f45ed825a113e61af7e141f44ca578374abef0',
+                         'fef5bfe1dc17611d5fb59a7f6f95c55c3606f933',
+                         '4f7e2131323e0749a740c0a56ab68ae9269c562a']
--- a/kallithea/tests/base.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/base.py	Mon May 04 19:24:04 2020 +0200
@@ -22,7 +22,7 @@
 import pytest
 from webtest import TestApp
 
-from kallithea.lib.utils2 import safe_str
+from kallithea.lib.utils2 import ascii_str
 from kallithea.model.db import User
 
 
@@ -66,17 +66,17 @@
 
 IP_ADDR = '127.0.0.127'
 
-HG_REPO = u'vcs_test_hg'
-GIT_REPO = u'vcs_test_git'
+HG_REPO = 'vcs_test_hg'
+GIT_REPO = 'vcs_test_git'
 
-NEW_HG_REPO = u'vcs_test_hg_new'
-NEW_GIT_REPO = u'vcs_test_git_new'
+NEW_HG_REPO = 'vcs_test_hg_new'
+NEW_GIT_REPO = 'vcs_test_git_new'
 
-HG_FORK = u'vcs_test_hg_fork'
-GIT_FORK = u'vcs_test_git_fork'
+HG_FORK = 'vcs_test_hg_fork'
+GIT_FORK = 'vcs_test_git_fork'
 
-HG_TEST_REVISION = u"a53d9201d4bc278910d416d94941b7ea007ecd52"
-GIT_TEST_REVISION = u"7ab37bc680b4aa72c34d07b230c866c28e9fc204"
+HG_TEST_REVISION = "a53d9201d4bc278910d416d94941b7ea007ecd52"
+GIT_TEST_REVISION = "7ab37bc680b4aa72c34d07b230c866c28e9fc204"
 
 
 ## VCS
@@ -156,7 +156,7 @@
                                   'password': password,
                                   '_session_csrf_secret_token': self.session_csrf_secret_token()})
 
-        if 'Invalid username or password' in response.body:
+        if b'Invalid username or password' in response.body:
             pytest.fail('could not login using %s %s' % (username, password))
 
         assert response.status == '302 Found'
@@ -176,20 +176,19 @@
         assert user == expected_username
 
     def session_csrf_secret_token(self):
-        return self.app.get(url('session_csrf_secret_token')).body
+        return ascii_str(self.app.get(url('session_csrf_secret_token')).body)
 
     def checkSessionFlash(self, response, msg=None, skip=0, _matcher=lambda msg, m: msg in m):
         if 'flash' not in response.session:
-            pytest.fail(safe_str(u'msg `%s` not found - session has no flash:\n%s' % (msg, response)))
+            pytest.fail('msg `%s` not found - session has no flash:\n%s' % (msg, response))
         try:
             level, m = response.session['flash'][-1 - skip]
             if _matcher(msg, m):
                 return
         except IndexError:
             pass
-        pytest.fail(safe_str(u'msg `%s` not found in session flash (skipping %s): %s' %
-                           (msg, skip,
-                            ', '.join('`%s`' % m for level, m in response.session['flash']))))
+        pytest.fail('msg `%s` not found in session flash (skipping %s): %s' %
+                    (msg, skip, ', '.join('`%s`' % m for level, m in response.session['flash'])))
 
     def checkSessionFlashRegex(self, response, regex, skip=0):
         self.checkSessionFlash(response, regex, skip=skip, _matcher=re.search)
--- a/kallithea/tests/conftest.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/conftest.py	Mon May 04 19:24:04 2020 +0200
@@ -45,7 +45,6 @@
             #'ssh_locale': 'C',
             'app_instance_uuid': 'test',
             'show_revision_number': 'true',
-            'beaker.cache.sql_cache_short.expire': '1',
             'session.secret': '{74e0cd75-b339-478b-b129-07dd221def1f}',
             #'i18n.lang': '',
         },
@@ -146,7 +145,7 @@
     user_model = UserModel()
 
     user_ids = []
-    user_ids.append(User.get_default_user().user_id)
+    user_ids.append(kallithea.DEFAULT_USER_ID)
     user_ids.append(User.get_by_username(TEST_USER_REGULAR_LOGIN).user_id)
 
     for user_id in user_ids:
--- a/kallithea/tests/fixture.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/fixture.py	Mon May 04 19:24:04 2020 +0200
@@ -41,8 +41,8 @@
 from kallithea.model.scm import ScmModel
 from kallithea.model.user import UserModel
 from kallithea.model.user_group import UserGroupModel
-from kallithea.tests.base import (
-    GIT_REPO, HG_REPO, IP_ADDR, TEST_USER_ADMIN_EMAIL, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH, invalidate_all_caches)
+from kallithea.tests.base import (GIT_REPO, HG_REPO, IP_ADDR, TEST_USER_ADMIN_EMAIL, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH,
+                                  invalidate_all_caches)
 
 
 log = logging.getLogger(__name__)
@@ -92,8 +92,8 @@
             repo_name=None,
             repo_type='hg',
             clone_uri='',
-            repo_group=u'-1',
-            repo_description=u'DESC',
+            repo_group='-1',
+            repo_description='DESC',
             repo_private=False,
             repo_landing_rev='rev:tip',
             repo_copy_permissions=False,
@@ -113,8 +113,8 @@
         """Return form values to be validated through RepoGroupForm"""
         defs = dict(
             group_name=None,
-            group_description=u'DESC',
-            parent_group_id=u'-1',
+            group_description='DESC',
+            parent_group_id='-1',
             perms_updates=[],
             perms_new=[],
             recursive=False
@@ -128,8 +128,8 @@
             username=name,
             password='qweqwe',
             email='%s+test@example.com' % name,
-            firstname=u'TestUser',
-            lastname=u'Test',
+            firstname='TestUser',
+            lastname='Test',
             active=True,
             admin=False,
             extern_type='internal',
@@ -142,7 +142,7 @@
     def _get_user_group_create_params(self, name, **custom):
         defs = dict(
             users_group_name=name,
-            user_group_description=u'DESC',
+            user_group_description='DESC',
             users_group_active=True,
             user_group_data={},
         )
@@ -253,7 +253,7 @@
 
     def create_gist(self, **kwargs):
         form_data = {
-            'description': u'new-gist',
+            'description': 'new-gist',
             'owner': TEST_USER_ADMIN_LOGIN,
             'gist_type': Gist.GIST_PUBLIC,
             'lifetime': -1,
@@ -324,12 +324,12 @@
         return cs
 
     def review_changeset(self, repo, revision, status, author=TEST_USER_ADMIN_LOGIN):
-        comment = ChangesetCommentsModel().create(u"review comment", repo, author, revision=revision, send_email=False)
+        comment = ChangesetCommentsModel().create("review comment", repo, author, revision=revision, send_email=False)
         csm = ChangesetStatusModel().set_status(repo, ChangesetStatus.STATUS_APPROVED, author, comment, revision=revision)
         Session().commit()
         return csm
 
-    def create_pullrequest(self, testcontroller, repo_name, pr_src_rev, pr_dst_rev, title=u'title'):
+    def create_pullrequest(self, testcontroller, repo_name, pr_src_rev, pr_dst_rev, title='title'):
         org_ref = 'branch:stable:%s' % pr_src_rev
         other_ref = 'branch:default:%s' % pr_dst_rev
         with test_context(testcontroller.app): # needed to be able to mock request user
@@ -339,7 +339,7 @@
             request.authuser = AuthUser(dbuser=owner_user)
             # creating a PR sends a message with an absolute URL - without routing that requires mocking
             with mock.patch.object(helpers, 'url', (lambda arg, qualified=False, **kwargs: ('https://localhost' if qualified else '') + '/fake/' + arg)):
-                cmd = CreatePullRequestAction(org_repo, other_repo, org_ref, other_ref, title, u'No description', owner_user, reviewers)
+                cmd = CreatePullRequestAction(org_repo, other_repo, org_ref, other_ref, title, 'No description', owner_user, reviewers)
                 pull_request = cmd.execute()
             Session().commit()
         return pull_request.pull_request_id
@@ -423,7 +423,7 @@
 
 
 def failing_test_hook(ui, repo, **kwargs):
-    ui.write("failing_test_hook failed\n")
+    ui.write(b"failing_test_hook failed\n")
     return 1
 
 
@@ -432,5 +432,5 @@
 
 
 def passing_test_hook(ui, repo, **kwargs):
-    ui.write("passing_test_hook succeeded\n")
+    ui.write(b"passing_test_hook succeeded\n")
     return 0
--- a/kallithea/tests/functional/test_admin.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_admin.py	Mon May 04 19:24:04 2020 +0200
@@ -3,16 +3,15 @@
 import os
 from os.path import dirname
 
-from kallithea.lib.utils2 import safe_unicode
 from kallithea.model.db import UserLog
 from kallithea.model.meta import Session
-from kallithea.tests.base import *
+from kallithea.tests import base
 
 
 FIXTURES = os.path.join(dirname(dirname(os.path.abspath(__file__))), 'fixtures')
 
 
-class TestAdminController(TestController):
+class TestAdminController(base.TestController):
 
     @classmethod
     def setup_class(cls):
@@ -34,8 +33,7 @@
         with open(os.path.join(FIXTURES, 'journal_dump.csv')) as f:
             for row in csv.DictReader(f):
                 ul = UserLog()
-                for k, v in row.iteritems():
-                    v = safe_unicode(v)
+                for k, v in row.items():
                     if k == 'action_date':
                         v = strptime(v)
                     if k in ['user_id', 'repository_id']:
@@ -52,105 +50,105 @@
 
     def test_index(self):
         self.log_user()
-        response = self.app.get(url(controller='admin/admin', action='index'))
+        response = self.app.get(base.url(controller='admin/admin', action='index'))
         response.mustcontain('Admin Journal')
 
     def test_filter_all_entries(self):
         self.log_user()
-        response = self.app.get(url(controller='admin/admin', action='index',))
+        response = self.app.get(base.url(controller='admin/admin', action='index',))
         response.mustcontain(' 2036 Entries')
 
     def test_filter_journal_filter_exact_match_on_repository(self):
         self.log_user()
-        response = self.app.get(url(controller='admin/admin', action='index',
+        response = self.app.get(base.url(controller='admin/admin', action='index',
                                     filter='repository:xxx'))
         response.mustcontain(' 3 Entries')
 
     def test_filter_journal_filter_exact_match_on_repository_CamelCase(self):
         self.log_user()
-        response = self.app.get(url(controller='admin/admin', action='index',
+        response = self.app.get(base.url(controller='admin/admin', action='index',
                                     filter='repository:XxX'))
         response.mustcontain(' 3 Entries')
 
     def test_filter_journal_filter_wildcard_on_repository(self):
         self.log_user()
-        response = self.app.get(url(controller='admin/admin', action='index',
+        response = self.app.get(base.url(controller='admin/admin', action='index',
                                     filter='repository:*test*'))
         response.mustcontain(' 862 Entries')
 
     def test_filter_journal_filter_prefix_on_repository(self):
         self.log_user()
-        response = self.app.get(url(controller='admin/admin', action='index',
+        response = self.app.get(base.url(controller='admin/admin', action='index',
                                     filter='repository:test*'))
         response.mustcontain(' 257 Entries')
 
     def test_filter_journal_filter_prefix_on_repository_CamelCase(self):
         self.log_user()
-        response = self.app.get(url(controller='admin/admin', action='index',
+        response = self.app.get(base.url(controller='admin/admin', action='index',
                                     filter='repository:Test*'))
         response.mustcontain(' 257 Entries')
 
     def test_filter_journal_filter_prefix_on_repository_and_user(self):
         self.log_user()
-        response = self.app.get(url(controller='admin/admin', action='index',
+        response = self.app.get(base.url(controller='admin/admin', action='index',
                                     filter='repository:test* AND username:demo'))
         response.mustcontain(' 130 Entries')
 
     def test_filter_journal_filter_prefix_on_repository_or_other_repo(self):
         self.log_user()
-        response = self.app.get(url(controller='admin/admin', action='index',
+        response = self.app.get(base.url(controller='admin/admin', action='index',
                                     filter='repository:test* OR repository:xxx'))
         response.mustcontain(' 260 Entries')  # 257 + 3
 
     def test_filter_journal_filter_exact_match_on_username(self):
         self.log_user()
-        response = self.app.get(url(controller='admin/admin', action='index',
+        response = self.app.get(base.url(controller='admin/admin', action='index',
                                     filter='username:demo'))
         response.mustcontain(' 1087 Entries')
 
     def test_filter_journal_filter_exact_match_on_username_camelCase(self):
         self.log_user()
-        response = self.app.get(url(controller='admin/admin', action='index',
+        response = self.app.get(base.url(controller='admin/admin', action='index',
                                     filter='username:DemO'))
         response.mustcontain(' 1087 Entries')
 
     def test_filter_journal_filter_wildcard_on_username(self):
         self.log_user()
-        response = self.app.get(url(controller='admin/admin', action='index',
+        response = self.app.get(base.url(controller='admin/admin', action='index',
                                     filter='username:*test*'))
         response.mustcontain(' 100 Entries')
 
     def test_filter_journal_filter_prefix_on_username(self):
         self.log_user()
-        response = self.app.get(url(controller='admin/admin', action='index',
+        response = self.app.get(base.url(controller='admin/admin', action='index',
                                     filter='username:demo*'))
         response.mustcontain(' 1101 Entries')
 
     def test_filter_journal_filter_prefix_on_user_or_other_user(self):
         self.log_user()
-        response = self.app.get(url(controller='admin/admin', action='index',
+        response = self.app.get(base.url(controller='admin/admin', action='index',
                                     filter='username:demo OR username:volcan'))
         response.mustcontain(' 1095 Entries')  # 1087 + 8
 
     def test_filter_journal_filter_wildcard_on_action(self):
         self.log_user()
-        response = self.app.get(url(controller='admin/admin', action='index',
+        response = self.app.get(base.url(controller='admin/admin', action='index',
                                     filter='action:*pull_request*'))
         response.mustcontain(' 187 Entries')
 
     def test_filter_journal_filter_on_date(self):
         self.log_user()
-        response = self.app.get(url(controller='admin/admin', action='index',
+        response = self.app.get(base.url(controller='admin/admin', action='index',
                                     filter='date:20121010'))
         response.mustcontain(' 47 Entries')
 
     def test_filter_journal_filter_on_date_2(self):
         self.log_user()
-        response = self.app.get(url(controller='admin/admin', action='index',
+        response = self.app.get(base.url(controller='admin/admin', action='index',
                                     filter='date:20121020'))
         response.mustcontain(' 17 Entries')
 
-    @parametrize('filter,hit', [
+    @base.parametrize('filter,hit', [
         #### "repository:" filtering
         # "/" is used for grouping
         ('repository:group/test', 4),
@@ -189,7 +187,7 @@
     def test_filter_journal_filter_tokenization(self, filter, hit):
         self.log_user()
 
-        response = self.app.get(url(controller='admin/admin', action='index',
+        response = self.app.get(base.url(controller='admin/admin', action='index',
                                     filter=filter))
         if hit != 1:
             response.mustcontain(' %s Entries' % hit)
--- a/kallithea/tests/functional/test_admin_auth_settings.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_admin_auth_settings.py	Mon May 04 19:24:04 2020 +0200
@@ -1,10 +1,10 @@
 from kallithea.model.db import Setting
-from kallithea.tests.base import *
+from kallithea.tests import base
 
 
-class TestAuthSettingsController(TestController):
+class TestAuthSettingsController(base.TestController):
     def _enable_plugins(self, plugins_list):
-        test_url = url(controller='admin/auth_settings',
+        test_url = base.url(controller='admin/auth_settings',
                        action='auth_settings')
         params={'auth_plugins': plugins_list, '_session_csrf_secret_token': self.session_csrf_secret_token()}
 
@@ -17,16 +17,16 @@
 
     def test_index(self):
         self.log_user()
-        response = self.app.get(url(controller='admin/auth_settings',
+        response = self.app.get(base.url(controller='admin/auth_settings',
                                     action='index'))
         response.mustcontain('Authentication Plugins')
 
-    @skipif(not ldap_lib_installed, reason='skipping due to missing ldap lib')
+    @base.skipif(not base.ldap_lib_installed, reason='skipping due to missing ldap lib')
     def test_ldap_save_settings(self):
         self.log_user()
 
         params = self._enable_plugins('kallithea.lib.auth_modules.auth_internal,kallithea.lib.auth_modules.auth_ldap')
-        params.update({'auth_ldap_host': u'dc.example.com',
+        params.update({'auth_ldap_host': 'dc.example.com',
                        'auth_ldap_port': '999',
                        'auth_ldap_tls_kind': 'PLAIN',
                        'auth_ldap_tls_reqcert': 'NEVER',
@@ -41,16 +41,16 @@
                        'auth_ldap_attr_lastname': 'tester',
                        'auth_ldap_attr_email': 'test@example.com'})
 
-        test_url = url(controller='admin/auth_settings',
+        test_url = base.url(controller='admin/auth_settings',
                        action='auth_settings')
 
         response = self.app.post(url=test_url, params=params)
         self.checkSessionFlash(response, 'Auth settings updated successfully')
 
         new_settings = Setting.get_auth_settings()
-        assert new_settings['auth_ldap_host'] == u'dc.example.com', 'fail db write compare'
+        assert new_settings['auth_ldap_host'] == 'dc.example.com', 'fail db write compare'
 
-    @skipif(not ldap_lib_installed, reason='skipping due to missing ldap lib')
+    @base.skipif(not base.ldap_lib_installed, reason='skipping due to missing ldap lib')
     def test_ldap_error_form_wrong_port_number(self):
         self.log_user()
 
@@ -68,7 +68,7 @@
                        'auth_ldap_attr_firstname': '',
                        'auth_ldap_attr_lastname': '',
                        'auth_ldap_attr_email': ''})
-        test_url = url(controller='admin/auth_settings',
+        test_url = base.url(controller='admin/auth_settings',
                        action='auth_settings')
 
         response = self.app.post(url=test_url, params=params)
@@ -76,7 +76,7 @@
         response.mustcontain("""<span class="error-message">"""
                              """Please enter a number</span>""")
 
-    @skipif(not ldap_lib_installed, reason='skipping due to missing ldap lib')
+    @base.skipif(not base.ldap_lib_installed, reason='skipping due to missing ldap lib')
     def test_ldap_error_form(self):
         self.log_user()
 
@@ -95,7 +95,7 @@
                        'auth_ldap_attr_lastname': '',
                        'auth_ldap_attr_email': ''})
 
-        test_url = url(controller='admin/auth_settings',
+        test_url = base.url(controller='admin/auth_settings',
                        action='auth_settings')
 
         response = self.app.post(url=test_url, params=params)
@@ -115,7 +115,7 @@
         params = self._enable_plugins('kallithea.lib.auth_modules.auth_internal,kallithea.lib.auth_modules.auth_container')
         params.update(settings)
 
-        test_url = url(controller='admin/auth_settings',
+        test_url = base.url(controller='admin/auth_settings',
                        action='auth_settings')
 
         response = self.app.post(url=test_url, params=params)
@@ -124,7 +124,7 @@
 
     def _container_auth_verify_login(self, resulting_username, **get_kwargs):
         response = self.app.get(
-            url=url(controller='admin/my_account', action='my_account'),
+            url=base.url(controller='admin/my_account', action='my_account'),
             **get_kwargs
         )
         response.mustcontain('My Account %s' % resulting_username)
@@ -153,14 +153,14 @@
             auth_container_clean_username='False',
         )
         response = self.app.get(
-            url=url(controller='admin/my_account', action='my_account'),
+            url=base.url(controller='admin/my_account', action='my_account'),
             extra_environ={'THE_USER_NAME': 'johnd',
-                           'THE_USER_EMAIL': 'john@example.org',
+                           'THE_USER_EMAIL': 'john2@example.org',
                            'THE_USER_FIRSTNAME': 'John',
                            'THE_USER_LASTNAME': 'Doe',
                            }
         )
-        assert response.form['email'].value == 'john@example.org'
+        assert response.form['email'].value == 'john2@example.org'
         assert response.form['firstname'].value == 'John'
         assert response.form['lastname'].value == 'Doe'
 
@@ -216,10 +216,10 @@
             auth_container_clean_username='True',
         )
         response = self.app.get(
-            url=url(controller='admin/my_account', action='my_account'),
+            url=base.url(controller='admin/my_account', action='my_account'),
             extra_environ={'REMOTE_USER': 'john'},
         )
-        assert 'Log Out' not in response.normal_body
+        assert b'Log Out' not in response.normal_body
 
     def test_crowd_save_settings(self):
         self.log_user()
@@ -232,16 +232,16 @@
                        'auth_crowd_method': 'https',
                        'auth_crowd_app_name': 'xyzzy'})
 
-        test_url = url(controller='admin/auth_settings',
+        test_url = base.url(controller='admin/auth_settings',
                        action='auth_settings')
 
         response = self.app.post(url=test_url, params=params)
         self.checkSessionFlash(response, 'Auth settings updated successfully')
 
         new_settings = Setting.get_auth_settings()
-        assert new_settings['auth_crowd_host'] == u'hostname', 'fail db write compare'
+        assert new_settings['auth_crowd_host'] == 'hostname', 'fail db write compare'
 
-    @skipif(not pam_lib_installed, reason='skipping due to missing pam lib')
+    @base.skipif(not base.pam_lib_installed, reason='skipping due to missing pam lib')
     def test_pam_save_settings(self):
         self.log_user()
 
@@ -249,11 +249,11 @@
         params.update({'auth_pam_service': 'kallithea',
                        'auth_pam_gecos': '^foo-.*'})
 
-        test_url = url(controller='admin/auth_settings',
+        test_url = base.url(controller='admin/auth_settings',
                        action='auth_settings')
 
         response = self.app.post(url=test_url, params=params)
         self.checkSessionFlash(response, 'Auth settings updated successfully')
 
         new_settings = Setting.get_auth_settings()
-        assert new_settings['auth_pam_service'] == u'kallithea', 'fail db write compare'
+        assert new_settings['auth_pam_service'] == 'kallithea', 'fail db write compare'
--- a/kallithea/tests/functional/test_admin_defaults.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_admin_defaults.py	Mon May 04 19:24:04 2020 +0200
@@ -1,12 +1,12 @@
 from kallithea.model.db import Setting
-from kallithea.tests.base import *
+from kallithea.tests import base
 
 
-class TestDefaultsController(TestController):
+class TestDefaultsController(base.TestController):
 
     def test_index(self):
         self.log_user()
-        response = self.app.get(url('defaults'))
+        response = self.app.get(base.url('defaults'))
         response.mustcontain('default_repo_private')
         response.mustcontain('default_repo_enable_statistics')
         response.mustcontain('default_repo_enable_downloads')
@@ -20,7 +20,7 @@
             'default_repo_type': 'hg',
             '_session_csrf_secret_token': self.session_csrf_secret_token(),
         }
-        response = self.app.post(url('defaults_update', id='default'), params=params)
+        response = self.app.post(base.url('defaults_update', id='default'), params=params)
         self.checkSessionFlash(response, 'Default settings updated successfully')
 
         params.pop('_session_csrf_secret_token')
@@ -36,7 +36,7 @@
             'default_repo_type': 'git',
             '_session_csrf_secret_token': self.session_csrf_secret_token(),
         }
-        response = self.app.post(url('defaults_update', id='default'), params=params)
+        response = self.app.post(base.url('defaults_update', id='default'), params=params)
         self.checkSessionFlash(response, 'Default settings updated successfully')
 
         params.pop('_session_csrf_secret_token')
--- a/kallithea/tests/functional/test_admin_gists.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_admin_gists.py	Mon May 04 19:24:04 2020 +0200
@@ -1,24 +1,24 @@
 from kallithea.model.db import Gist, User
 from kallithea.model.gist import GistModel
 from kallithea.model.meta import Session
-from kallithea.tests.base import *
+from kallithea.tests import base
 
 
 def _create_gist(f_name, content='some gist', lifetime=-1,
-                 description=u'gist-desc', gist_type='public',
-                 owner=TEST_USER_ADMIN_LOGIN):
+                 description='gist-desc', gist_type='public',
+                 owner=base.TEST_USER_ADMIN_LOGIN):
     gist_mapping = {
         f_name: {'content': content}
     }
     owner = User.get_by_username(owner)
-    gist = GistModel().create(description, owner=owner, ip_addr=IP_ADDR,
+    gist = GistModel().create(description, owner=owner, ip_addr=base.IP_ADDR,
                        gist_mapping=gist_mapping, gist_type=gist_type,
                        lifetime=lifetime)
     Session().commit()
     return gist
 
 
-class TestGistsController(TestController):
+class TestGistsController(base.TestController):
 
     def teardown_method(self, method):
         for g in Gist.query():
@@ -27,15 +27,15 @@
 
     def test_index(self):
         self.log_user()
-        response = self.app.get(url('gists'))
+        response = self.app.get(base.url('gists'))
         # Test response...
         response.mustcontain('There are no gists yet')
 
         g1 = _create_gist('gist1').gist_access_id
         g2 = _create_gist('gist2', lifetime=1400).gist_access_id
-        g3 = _create_gist('gist3', description=u'gist3-desc').gist_access_id
+        g3 = _create_gist('gist3', description='gist3-desc').gist_access_id
         g4 = _create_gist('gist4', gist_type='private').gist_access_id
-        response = self.app.get(url('gists'))
+        response = self.app.get(base.url('gists'))
         # Test response...
         response.mustcontain('gist: %s' % g1)
         response.mustcontain('gist: %s' % g2)
@@ -47,7 +47,7 @@
     def test_index_private_gists(self):
         self.log_user()
         gist = _create_gist('gist5', gist_type='private')
-        response = self.app.get(url('gists', private=1))
+        response = self.app.get(base.url('gists', private=1))
         # Test response...
 
         # and privates
@@ -55,7 +55,7 @@
 
     def test_create_missing_description(self):
         self.log_user()
-        response = self.app.post(url('gists'),
+        response = self.app.post(base.url('gists'),
                                  params={'lifetime': -1, '_session_csrf_secret_token': self.session_csrf_secret_token()},
                                  status=200)
 
@@ -63,7 +63,7 @@
 
     def test_create(self):
         self.log_user()
-        response = self.app.post(url('gists'),
+        response = self.app.post(base.url('gists'),
                                  params={'lifetime': -1,
                                          'content': 'gist test',
                                          'filename': 'foo',
@@ -77,7 +77,7 @@
 
     def test_create_with_path_with_dirs(self):
         self.log_user()
-        response = self.app.post(url('gists'),
+        response = self.app.post(base.url('gists'),
                                  params={'lifetime': -1,
                                          'content': 'gist test',
                                          'filename': '/home/foo',
@@ -92,11 +92,11 @@
         gist.gist_expires = 0  # 1970
         Session().commit()
 
-        response = self.app.get(url('gist', gist_id=gist.gist_access_id), status=404)
+        response = self.app.get(base.url('gist', gist_id=gist.gist_access_id), status=404)
 
     def test_create_private(self):
         self.log_user()
-        response = self.app.post(url('gists'),
+        response = self.app.post(base.url('gists'),
                                  params={'lifetime': -1,
                                          'content': 'private gist test',
                                          'filename': 'private-foo',
@@ -110,7 +110,7 @@
 
     def test_create_with_description(self):
         self.log_user()
-        response = self.app.post(url('gists'),
+        response = self.app.post(base.url('gists'),
                                  params={'lifetime': -1,
                                          'content': 'gist test',
                                          'filename': 'foo-desc',
@@ -126,46 +126,46 @@
 
     def test_new(self):
         self.log_user()
-        response = self.app.get(url('new_gist'))
+        response = self.app.get(base.url('new_gist'))
 
     def test_delete(self):
         self.log_user()
         gist = _create_gist('delete-me')
-        response = self.app.post(url('gist_delete', gist_id=gist.gist_id),
+        response = self.app.post(base.url('gist_delete', gist_id=gist.gist_id),
             params={'_session_csrf_secret_token': self.session_csrf_secret_token()})
 
     def test_delete_normal_user_his_gist(self):
-        self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
-        gist = _create_gist('delete-me', owner=TEST_USER_REGULAR_LOGIN)
-        response = self.app.post(url('gist_delete', gist_id=gist.gist_id),
+        self.log_user(base.TEST_USER_REGULAR_LOGIN, base.TEST_USER_REGULAR_PASS)
+        gist = _create_gist('delete-me', owner=base.TEST_USER_REGULAR_LOGIN)
+        response = self.app.post(base.url('gist_delete', gist_id=gist.gist_id),
             params={'_session_csrf_secret_token': self.session_csrf_secret_token()})
 
     def test_delete_normal_user_not_his_own_gist(self):
-        self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
+        self.log_user(base.TEST_USER_REGULAR_LOGIN, base.TEST_USER_REGULAR_PASS)
         gist = _create_gist('delete-me')
-        response = self.app.post(url('gist_delete', gist_id=gist.gist_id), status=403,
+        response = self.app.post(base.url('gist_delete', gist_id=gist.gist_id), status=403,
             params={'_session_csrf_secret_token': self.session_csrf_secret_token()})
 
     def test_show(self):
         gist = _create_gist('gist-show-me')
-        response = self.app.get(url('gist', gist_id=gist.gist_access_id))
+        response = self.app.get(base.url('gist', gist_id=gist.gist_access_id))
         response.mustcontain('added file: gist-show-me<')
-        response.mustcontain('%s - created' % TEST_USER_ADMIN_LOGIN)
+        response.mustcontain('%s - created' % base.TEST_USER_ADMIN_LOGIN)
         response.mustcontain('gist-desc')
         response.mustcontain('<div class="label label-success">Public Gist</div>')
 
     def test_show_as_raw(self):
         gist = _create_gist('gist-show-me', content='GIST CONTENT')
-        response = self.app.get(url('formatted_gist',
+        response = self.app.get(base.url('formatted_gist',
                                     gist_id=gist.gist_access_id, format='raw'))
-        assert response.body == 'GIST CONTENT'
+        assert response.body == b'GIST CONTENT'
 
     def test_show_as_raw_individual_file(self):
         gist = _create_gist('gist-show-me-raw', content='GIST BODY')
-        response = self.app.get(url('formatted_gist_file',
+        response = self.app.get(base.url('formatted_gist_file',
                                     gist_id=gist.gist_access_id, format='raw',
                                     revision='tip', f_path='gist-show-me-raw'))
-        assert response.body == 'GIST BODY'
+        assert response.body == b'GIST BODY'
 
     def test_edit(self):
-        response = self.app.get(url('edit_gist', gist_id=1))
+        response = self.app.get(base.url('edit_gist', gist_id=1))
--- a/kallithea/tests/functional/test_admin_permissions.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_admin_permissions.py	Mon May 04 19:24:04 2020 +0200
@@ -1,81 +1,82 @@
+import kallithea
 from kallithea.model.db import User, UserIpMap
-from kallithea.tests.base import *
+from kallithea.tests import base
 
 
-class TestAdminPermissionsController(TestController):
+class TestAdminPermissionsController(base.TestController):
 
     def test_index(self):
         self.log_user()
-        response = self.app.get(url('admin_permissions'))
+        response = self.app.get(base.url('admin_permissions'))
         # Test response...
 
     def test_index_ips(self):
         self.log_user()
-        response = self.app.get(url('admin_permissions_ips'))
+        response = self.app.get(base.url('admin_permissions_ips'))
         # Test response...
         response.mustcontain('All IP addresses are allowed')
 
     def test_add_delete_ips(self, auto_clear_ip_permissions):
         self.log_user()
-        default_user_id = User.get_default_user().user_id
+        default_user_id = kallithea.DEFAULT_USER_ID
 
         # Add IP and verify it is shown in UI and both gives access and rejects
 
-        response = self.app.post(url('edit_user_ips_update', id=default_user_id),
+        response = self.app.post(base.url('edit_user_ips_update', id=default_user_id),
                                  params=dict(new_ip='0.0.0.0/24',
                                  _session_csrf_secret_token=self.session_csrf_secret_token()))
-        invalidate_all_caches()
-        response = self.app.get(url('admin_permissions_ips'),
+        base.invalidate_all_caches()
+        response = self.app.get(base.url('admin_permissions_ips'),
                                 extra_environ={'REMOTE_ADDR': '0.0.0.1'})
         response.mustcontain('0.0.0.0/24')
         response.mustcontain('0.0.0.0 - 0.0.0.255')
 
-        response = self.app.get(url('admin_permissions_ips'),
+        response = self.app.get(base.url('admin_permissions_ips'),
                                 extra_environ={'REMOTE_ADDR': '0.0.1.1'}, status=403)
 
         # Add another IP and verify previously rejected now works
 
-        response = self.app.post(url('edit_user_ips_update', id=default_user_id),
+        response = self.app.post(base.url('edit_user_ips_update', id=default_user_id),
                                  params=dict(new_ip='0.0.1.0/24',
                                  _session_csrf_secret_token=self.session_csrf_secret_token()))
-        invalidate_all_caches()
+        base.invalidate_all_caches()
 
-        response = self.app.get(url('admin_permissions_ips'),
+        response = self.app.get(base.url('admin_permissions_ips'),
                                 extra_environ={'REMOTE_ADDR': '0.0.1.1'})
 
         # Delete latest IP and verify same IP is rejected again
 
         x = UserIpMap.query().filter_by(ip_addr='0.0.1.0/24').first()
-        response = self.app.post(url('edit_user_ips_delete', id=default_user_id),
+        response = self.app.post(base.url('edit_user_ips_delete', id=default_user_id),
                                  params=dict(del_ip_id=x.ip_id,
                                              _session_csrf_secret_token=self.session_csrf_secret_token()))
-        invalidate_all_caches()
+        base.invalidate_all_caches()
 
-        response = self.app.get(url('admin_permissions_ips'),
+        response = self.app.get(base.url('admin_permissions_ips'),
                                 extra_environ={'REMOTE_ADDR': '0.0.1.1'}, status=403)
 
         # Delete first IP and verify unlimited access again
 
         x = UserIpMap.query().filter_by(ip_addr='0.0.0.0/24').first()
-        response = self.app.post(url('edit_user_ips_delete', id=default_user_id),
+        response = self.app.post(base.url('edit_user_ips_delete', id=default_user_id),
                                  params=dict(del_ip_id=x.ip_id,
                                              _session_csrf_secret_token=self.session_csrf_secret_token()))
-        invalidate_all_caches()
+        base.invalidate_all_caches()
 
-        response = self.app.get(url('admin_permissions_ips'),
+        response = self.app.get(base.url('admin_permissions_ips'),
                                 extra_environ={'REMOTE_ADDR': '0.0.1.1'})
 
     def test_index_overview(self):
         self.log_user()
-        response = self.app.get(url('admin_permissions_perms'))
+        response = self.app.get(base.url('admin_permissions_perms'))
         # Test response...
 
     def test_edit_permissions_permissions(self):
-        user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
+        user = User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
 
         # Test unauthenticated access - it will redirect to login page
         response = self.app.post(
-            url('edit_repo_perms_update', repo_name=HG_REPO),
+            base.url('edit_repo_perms_update', repo_name=base.HG_REPO),
             params=dict(
                 perm_new_member_1='repository.read',
                 perm_new_member_name_1=user.username,
@@ -83,24 +84,24 @@
                 _session_csrf_secret_token=self.session_csrf_secret_token()),
             status=302)
 
-        assert not response.location.endswith(url('edit_repo_perms_update', repo_name=HG_REPO))
-        assert response.location.endswith(url('login_home', came_from=url('edit_repo_perms_update', repo_name=HG_REPO)))
+        assert not response.location.endswith(base.url('edit_repo_perms_update', repo_name=base.HG_REPO))
+        assert response.location.endswith(base.url('login_home', came_from=base.url('edit_repo_perms_update', repo_name=base.HG_REPO)))
 
         response = self.app.post(
-            url('edit_repo_perms_revoke', repo_name=HG_REPO),
+            base.url('edit_repo_perms_revoke', repo_name=base.HG_REPO),
             params=dict(
                 obj_type='user',
                 user_id=user.user_id,
                 _session_csrf_secret_token=self.session_csrf_secret_token()),
             status=302)
 
-        assert response.location.endswith(url('login_home', came_from=url('edit_repo_perms_revoke', repo_name=HG_REPO)))
+        assert response.location.endswith(base.url('login_home', came_from=base.url('edit_repo_perms_revoke', repo_name=base.HG_REPO)))
 
         # Test authenticated access
         self.log_user()
 
         response = self.app.post(
-            url('edit_repo_perms_update', repo_name=HG_REPO),
+            base.url('edit_repo_perms_update', repo_name=base.HG_REPO),
             params=dict(
                 perm_new_member_1='repository.read',
                 perm_new_member_name_1=user.username,
@@ -108,10 +109,10 @@
                 _session_csrf_secret_token=self.session_csrf_secret_token()),
             status=302)
 
-        assert response.location.endswith(url('edit_repo_perms_update', repo_name=HG_REPO))
+        assert response.location.endswith(base.url('edit_repo_perms_update', repo_name=base.HG_REPO))
 
         response = self.app.post(
-            url('edit_repo_perms_revoke', repo_name=HG_REPO),
+            base.url('edit_repo_perms_revoke', repo_name=base.HG_REPO),
             params=dict(
                 obj_type='user',
                 user_id=user.user_id,
--- a/kallithea/tests/functional/test_admin_repo_groups.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_admin_repo_groups.py	Mon May 04 19:24:04 2020 +0200
@@ -11,7 +11,7 @@
 
     def test_case_insensitivity(self):
         self.log_user()
-        group_name = u'newgroup'
+        group_name = 'newgroup'
         response = self.app.post(url('repos_groups'),
                                  fixture._get_repo_group_create_params(group_name=group_name,
                                                                  _session_csrf_secret_token=self.session_csrf_secret_token()))
--- a/kallithea/tests/functional/test_admin_repos.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_admin_repos.py	Mon May 04 19:24:04 2020 +0200
@@ -1,19 +1,19 @@
 # -*- coding: utf-8 -*-
 
 import os
-import urllib
+import urllib.parse
 
 import mock
 import pytest
 
 from kallithea.lib import vcs
-from kallithea.lib.utils2 import safe_str, safe_unicode
-from kallithea.model.db import Permission, RepoGroup, Repository, Ui, User, UserRepoToPerm
+from kallithea.model import db
+from kallithea.model.db import Permission, Repository, Ui, User, UserRepoToPerm
 from kallithea.model.meta import Session
 from kallithea.model.repo import RepoModel
 from kallithea.model.repo_group import RepoGroupModel
 from kallithea.model.user import UserModel
-from kallithea.tests.base import *
+from kallithea.tests import base
 from kallithea.tests.fixture import Fixture, error_function
 
 
@@ -29,7 +29,7 @@
     return perm
 
 
-class _BaseTestCase(TestController):
+class _BaseTestCase(base.TestController):
     """
     Write all tests here
     """
@@ -41,21 +41,21 @@
 
     def test_index(self):
         self.log_user()
-        response = self.app.get(url('repos'))
+        response = self.app.get(base.url('repos'))
 
     def test_create(self):
         self.log_user()
         repo_name = self.NEW_REPO
-        description = u'description for newly created repo'
-        response = self.app.post(url('repos'),
+        description = 'description for newly created repo'
+        response = self.app.post(base.url('repos'),
                         fixture._get_repo_create_params(repo_private=False,
                                                 repo_name=repo_name,
                                                 repo_type=self.REPO_TYPE,
                                                 repo_description=description,
                                                 _session_csrf_secret_token=self.session_csrf_secret_token()))
         ## run the check page that triggers the flash message
-        response = self.app.get(url('repo_check_home', repo_name=repo_name))
-        assert response.json == {u'result': True}
+        response = self.app.get(base.url('repo_check_home', repo_name=repo_name))
+        assert response.json == {'result': True}
         self.checkSessionFlash(response,
                                'Created repository <a href="/%s">%s</a>'
                                % (repo_name, repo_name))
@@ -68,13 +68,13 @@
         assert new_repo.description == description
 
         # test if the repository is visible in the list ?
-        response = self.app.get(url('summary_home', repo_name=repo_name))
+        response = self.app.get(base.url('summary_home', repo_name=repo_name))
         response.mustcontain(repo_name)
         response.mustcontain(self.REPO_TYPE)
 
         # test if the repository was created on filesystem
         try:
-            vcs.get_repo(safe_str(os.path.join(Ui.get_by_key('paths', '/').ui_value, repo_name)))
+            vcs.get_repo(os.path.join(Ui.get_by_key('paths', '/').ui_value, repo_name))
         except vcs.exceptions.VCSError:
             pytest.fail('no repo %s in filesystem' % repo_name)
 
@@ -84,8 +84,8 @@
     def test_case_insensitivity(self):
         self.log_user()
         repo_name = self.NEW_REPO
-        description = u'description for newly created repo'
-        response = self.app.post(url('repos'),
+        description = 'description for newly created repo'
+        response = self.app.post(base.url('repos'),
                                  fixture._get_repo_create_params(repo_private=False,
                                                                  repo_name=repo_name,
                                                                  repo_type=self.REPO_TYPE,
@@ -93,7 +93,7 @@
                                                                  _session_csrf_secret_token=self.session_csrf_secret_token()))
         # try to create repo with swapped case
         swapped_repo_name = repo_name.swapcase()
-        response = self.app.post(url('repos'),
+        response = self.app.post(base.url('repos'),
                                  fixture._get_repo_create_params(repo_private=False,
                                                                  repo_name=swapped_repo_name,
                                                                  repo_type=self.REPO_TYPE,
@@ -108,16 +108,16 @@
         self.log_user()
 
         ## create GROUP
-        group_name = u'sometest_%s' % self.REPO_TYPE
+        group_name = 'sometest_%s' % self.REPO_TYPE
         gr = RepoGroupModel().create(group_name=group_name,
-                                     group_description=u'test',
-                                     owner=TEST_USER_ADMIN_LOGIN)
+                                     group_description='test',
+                                     owner=base.TEST_USER_ADMIN_LOGIN)
         Session().commit()
 
-        repo_name = u'ingroup'
-        repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
-        description = u'description for newly created repo'
-        response = self.app.post(url('repos'),
+        repo_name = 'ingroup'
+        repo_name_full = db.URL_SEP.join([group_name, repo_name])
+        description = 'description for newly created repo'
+        response = self.app.post(base.url('repos'),
                         fixture._get_repo_create_params(repo_private=False,
                                                 repo_name=repo_name,
                                                 repo_type=self.REPO_TYPE,
@@ -125,8 +125,8 @@
                                                 repo_group=gr.group_id,
                                                 _session_csrf_secret_token=self.session_csrf_secret_token()))
         ## run the check page that triggers the flash message
-        response = self.app.get(url('repo_check_home', repo_name=repo_name_full))
-        assert response.json == {u'result': True}
+        response = self.app.get(base.url('repo_check_home', repo_name=repo_name_full))
+        assert response.json == {'result': True}
         self.checkSessionFlash(response,
                                'Created repository <a href="/%s">%s</a>'
                                % (repo_name_full, repo_name_full))
@@ -139,7 +139,7 @@
         assert new_repo.description == description
 
         # test if the repository is visible in the list ?
-        response = self.app.get(url('summary_home', repo_name=repo_name_full))
+        response = self.app.get(base.url('summary_home', repo_name=repo_name_full))
         response.mustcontain(repo_name_full)
         response.mustcontain(self.REPO_TYPE)
 
@@ -149,7 +149,7 @@
 
         # test if the repository was created on filesystem
         try:
-            vcs.get_repo(safe_str(os.path.join(Ui.get_by_key('paths', '/').ui_value, repo_name_full)))
+            vcs.get_repo(os.path.join(Ui.get_by_key('paths', '/').ui_value, repo_name_full))
         except vcs.exceptions.VCSError:
             RepoGroupModel().delete(group_name)
             Session().commit()
@@ -160,41 +160,41 @@
         Session().commit()
 
     def test_create_in_group_without_needed_permissions(self):
-        usr = self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
+        usr = self.log_user(base.TEST_USER_REGULAR_LOGIN, base.TEST_USER_REGULAR_PASS)
         # avoid spurious RepoGroup DetachedInstanceError ...
         session_csrf_secret_token = self.session_csrf_secret_token()
         # revoke
         user_model = UserModel()
         # disable fork and create on default user
-        user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
-        user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
-        user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
-        user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
+        user_model.revoke_perm(User.DEFAULT_USER_NAME, 'hg.create.repository')
+        user_model.grant_perm(User.DEFAULT_USER_NAME, 'hg.create.none')
+        user_model.revoke_perm(User.DEFAULT_USER_NAME, 'hg.fork.repository')
+        user_model.grant_perm(User.DEFAULT_USER_NAME, 'hg.fork.none')
 
         # disable on regular user
-        user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
-        user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
-        user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
-        user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
+        user_model.revoke_perm(base.TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
+        user_model.grant_perm(base.TEST_USER_REGULAR_LOGIN, 'hg.create.none')
+        user_model.revoke_perm(base.TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
+        user_model.grant_perm(base.TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
         Session().commit()
 
         ## create GROUP
-        group_name = u'reg_sometest_%s' % self.REPO_TYPE
+        group_name = 'reg_sometest_%s' % self.REPO_TYPE
         gr = RepoGroupModel().create(group_name=group_name,
-                                     group_description=u'test',
-                                     owner=TEST_USER_ADMIN_LOGIN)
+                                     group_description='test',
+                                     owner=base.TEST_USER_ADMIN_LOGIN)
         Session().commit()
 
-        group_name_allowed = u'reg_sometest_allowed_%s' % self.REPO_TYPE
+        group_name_allowed = 'reg_sometest_allowed_%s' % self.REPO_TYPE
         gr_allowed = RepoGroupModel().create(group_name=group_name_allowed,
-                                     group_description=u'test',
-                                     owner=TEST_USER_REGULAR_LOGIN)
+                                     group_description='test',
+                                     owner=base.TEST_USER_REGULAR_LOGIN)
         Session().commit()
 
-        repo_name = u'ingroup'
-        repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
-        description = u'description for newly created repo'
-        response = self.app.post(url('repos'),
+        repo_name = 'ingroup'
+        repo_name_full = db.URL_SEP.join([group_name, repo_name])
+        description = 'description for newly created repo'
+        response = self.app.post(base.url('repos'),
                         fixture._get_repo_create_params(repo_private=False,
                                                 repo_name=repo_name,
                                                 repo_type=self.REPO_TYPE,
@@ -205,10 +205,10 @@
         response.mustcontain('Invalid value')
 
         # user is allowed to create in this group
-        repo_name = u'ingroup'
-        repo_name_full = RepoGroup.url_sep().join([group_name_allowed, repo_name])
-        description = u'description for newly created repo'
-        response = self.app.post(url('repos'),
+        repo_name = 'ingroup'
+        repo_name_full = db.URL_SEP.join([group_name_allowed, repo_name])
+        description = 'description for newly created repo'
+        response = self.app.post(base.url('repos'),
                         fixture._get_repo_create_params(repo_private=False,
                                                 repo_name=repo_name,
                                                 repo_type=self.REPO_TYPE,
@@ -217,8 +217,8 @@
                                                 _session_csrf_secret_token=session_csrf_secret_token))
 
         ## run the check page that triggers the flash message
-        response = self.app.get(url('repo_check_home', repo_name=repo_name_full))
-        assert response.json == {u'result': True}
+        response = self.app.get(base.url('repo_check_home', repo_name=repo_name_full))
+        assert response.json == {'result': True}
         self.checkSessionFlash(response,
                                'Created repository <a href="/%s">%s</a>'
                                % (repo_name_full, repo_name_full))
@@ -231,7 +231,7 @@
         assert new_repo.description == description
 
         # test if the repository is visible in the list ?
-        response = self.app.get(url('summary_home', repo_name=repo_name_full))
+        response = self.app.get(base.url('summary_home', repo_name=repo_name_full))
         response.mustcontain(repo_name_full)
         response.mustcontain(self.REPO_TYPE)
 
@@ -241,7 +241,7 @@
 
         # test if the repository was created on filesystem
         try:
-            vcs.get_repo(safe_str(os.path.join(Ui.get_by_key('paths', '/').ui_value, repo_name_full)))
+            vcs.get_repo(os.path.join(Ui.get_by_key('paths', '/').ui_value, repo_name_full))
         except vcs.exceptions.VCSError:
             RepoGroupModel().delete(group_name)
             Session().commit()
@@ -256,20 +256,20 @@
         self.log_user()
 
         ## create GROUP
-        group_name = u'sometest_%s' % self.REPO_TYPE
+        group_name = 'sometest_%s' % self.REPO_TYPE
         gr = RepoGroupModel().create(group_name=group_name,
-                                     group_description=u'test',
-                                     owner=TEST_USER_ADMIN_LOGIN)
+                                     group_description='test',
+                                     owner=base.TEST_USER_ADMIN_LOGIN)
         perm = Permission.get_by_key('repository.write')
-        RepoGroupModel().grant_user_permission(gr, TEST_USER_REGULAR_LOGIN, perm)
+        RepoGroupModel().grant_user_permission(gr, base.TEST_USER_REGULAR_LOGIN, perm)
 
         ## add repo permissions
         Session().commit()
 
-        repo_name = u'ingroup_inherited_%s' % self.REPO_TYPE
-        repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
-        description = u'description for newly created repo'
-        response = self.app.post(url('repos'),
+        repo_name = 'ingroup_inherited_%s' % self.REPO_TYPE
+        repo_name_full = db.URL_SEP.join([group_name, repo_name])
+        description = 'description for newly created repo'
+        response = self.app.post(base.url('repos'),
                         fixture._get_repo_create_params(repo_private=False,
                                                 repo_name=repo_name,
                                                 repo_type=self.REPO_TYPE,
@@ -279,7 +279,7 @@
                                                 _session_csrf_secret_token=self.session_csrf_secret_token()))
 
         ## run the check page that triggers the flash message
-        response = self.app.get(url('repo_check_home', repo_name=repo_name_full))
+        response = self.app.get(base.url('repo_check_home', repo_name=repo_name_full))
         self.checkSessionFlash(response,
                                'Created repository <a href="/%s">%s</a>'
                                % (repo_name_full, repo_name_full))
@@ -292,13 +292,13 @@
         assert new_repo.description == description
 
         # test if the repository is visible in the list ?
-        response = self.app.get(url('summary_home', repo_name=repo_name_full))
+        response = self.app.get(base.url('summary_home', repo_name=repo_name_full))
         response.mustcontain(repo_name_full)
         response.mustcontain(self.REPO_TYPE)
 
         # test if the repository was created on filesystem
         try:
-            vcs.get_repo(safe_str(os.path.join(Ui.get_by_key('paths', '/').ui_value, repo_name_full)))
+            vcs.get_repo(os.path.join(Ui.get_by_key('paths', '/').ui_value, repo_name_full))
         except vcs.exceptions.VCSError:
             RepoGroupModel().delete(group_name)
             Session().commit()
@@ -309,7 +309,7 @@
             .filter(UserRepoToPerm.repository_id == new_repo_id).all()
         assert len(inherited_perms) == 2
 
-        assert TEST_USER_REGULAR_LOGIN in [x.user.username
+        assert base.TEST_USER_REGULAR_LOGIN in [x.user.username
                                                     for x in inherited_perms]
         assert 'repository.write' in [x.permission.permission_name
                                                for x in inherited_perms]
@@ -321,8 +321,8 @@
     def test_create_remote_repo_wrong_clone_uri(self):
         self.log_user()
         repo_name = self.NEW_REPO
-        description = u'description for newly created repo'
-        response = self.app.post(url('repos'),
+        description = 'description for newly created repo'
+        response = self.app.post(base.url('repos'),
                         fixture._get_repo_create_params(repo_private=False,
                                                 repo_name=repo_name,
                                                 repo_type=self.REPO_TYPE,
@@ -334,8 +334,8 @@
     def test_create_remote_repo_wrong_clone_uri_hg_svn(self):
         self.log_user()
         repo_name = self.NEW_REPO
-        description = u'description for newly created repo'
-        response = self.app.post(url('repos'),
+        description = 'description for newly created repo'
+        response = self.app.post(base.url('repos'),
                         fixture._get_repo_create_params(repo_private=False,
                                                 repo_name=repo_name,
                                                 repo_type=self.REPO_TYPE,
@@ -346,16 +346,16 @@
 
     def test_delete(self):
         self.log_user()
-        repo_name = u'vcs_test_new_to_delete_%s' % self.REPO_TYPE
-        description = u'description for newly created repo'
-        response = self.app.post(url('repos'),
+        repo_name = 'vcs_test_new_to_delete_%s' % self.REPO_TYPE
+        description = 'description for newly created repo'
+        response = self.app.post(base.url('repos'),
                         fixture._get_repo_create_params(repo_private=False,
                                                 repo_type=self.REPO_TYPE,
                                                 repo_name=repo_name,
                                                 repo_description=description,
                                                 _session_csrf_secret_token=self.session_csrf_secret_token()))
         ## run the check page that triggers the flash message
-        response = self.app.get(url('repo_check_home', repo_name=repo_name))
+        response = self.app.get(base.url('repo_check_home', repo_name=repo_name))
         self.checkSessionFlash(response,
                                'Created repository <a href="/%s">%s</a>'
                                % (repo_name, repo_name))
@@ -367,17 +367,17 @@
         assert new_repo.description == description
 
         # test if the repository is visible in the list ?
-        response = self.app.get(url('summary_home', repo_name=repo_name))
+        response = self.app.get(base.url('summary_home', repo_name=repo_name))
         response.mustcontain(repo_name)
         response.mustcontain(self.REPO_TYPE)
 
         # test if the repository was created on filesystem
         try:
-            vcs.get_repo(safe_str(os.path.join(Ui.get_by_key('paths', '/').ui_value, repo_name)))
+            vcs.get_repo(os.path.join(Ui.get_by_key('paths', '/').ui_value, repo_name))
         except vcs.exceptions.VCSError:
             pytest.fail('no repo %s in filesystem' % repo_name)
 
-        response = self.app.post(url('delete_repo', repo_name=repo_name),
+        response = self.app.post(base.url('delete_repo', repo_name=repo_name),
             params={'_session_csrf_secret_token': self.session_csrf_secret_token()})
 
         self.checkSessionFlash(response, 'Deleted repository %s' % (repo_name))
@@ -395,67 +395,65 @@
     def test_delete_non_ascii(self):
         self.log_user()
         non_ascii = "ąęł"
-        repo_name = "%s%s" % (safe_str(self.NEW_REPO), non_ascii)
-        repo_name_unicode = safe_unicode(repo_name)
+        repo_name = "%s%s" % (self.NEW_REPO, non_ascii)
         description = 'description for newly created repo' + non_ascii
-        description_unicode = safe_unicode(description)
-        response = self.app.post(url('repos'),
+        response = self.app.post(base.url('repos'),
                         fixture._get_repo_create_params(repo_private=False,
                                                 repo_name=repo_name,
                                                 repo_type=self.REPO_TYPE,
                                                 repo_description=description,
                                                 _session_csrf_secret_token=self.session_csrf_secret_token()))
         ## run the check page that triggers the flash message
-        response = self.app.get(url('repo_check_home', repo_name=repo_name))
-        assert response.json == {u'result': True}
+        response = self.app.get(base.url('repo_check_home', repo_name=repo_name))
+        assert response.json == {'result': True}
         self.checkSessionFlash(response,
-                               u'Created repository <a href="/%s">%s</a>'
-                               % (urllib.quote(repo_name), repo_name_unicode))
+                               'Created repository <a href="/%s">%s</a>'
+                               % (urllib.parse.quote(repo_name), repo_name))
         # test if the repo was created in the database
         new_repo = Session().query(Repository) \
-            .filter(Repository.repo_name == repo_name_unicode).one()
+            .filter(Repository.repo_name == repo_name).one()
 
-        assert new_repo.repo_name == repo_name_unicode
-        assert new_repo.description == description_unicode
+        assert new_repo.repo_name == repo_name
+        assert new_repo.description == description
 
         # test if the repository is visible in the list ?
-        response = self.app.get(url('summary_home', repo_name=repo_name))
+        response = self.app.get(base.url('summary_home', repo_name=repo_name))
         response.mustcontain(repo_name)
         response.mustcontain(self.REPO_TYPE)
 
         # test if the repository was created on filesystem
         try:
-            vcs.get_repo(safe_str(os.path.join(Ui.get_by_key('paths', '/').ui_value, repo_name_unicode)))
+            vcs.get_repo(os.path.join(Ui.get_by_key('paths', '/').ui_value, repo_name))
         except vcs.exceptions.VCSError:
             pytest.fail('no repo %s in filesystem' % repo_name)
 
-        response = self.app.post(url('delete_repo', repo_name=repo_name),
+        response = self.app.post(base.url('delete_repo', repo_name=repo_name),
             params={'_session_csrf_secret_token': self.session_csrf_secret_token()})
-        self.checkSessionFlash(response, 'Deleted repository %s' % (repo_name_unicode))
+        self.checkSessionFlash(response, 'Deleted repository %s' % (repo_name))
         response.follow()
 
         # check if repo was deleted from db
         deleted_repo = Session().query(Repository) \
-            .filter(Repository.repo_name == repo_name_unicode).scalar()
+            .filter(Repository.repo_name == repo_name).scalar()
 
         assert deleted_repo is None
 
-        assert os.path.isdir(os.path.join(Ui.get_by_key('paths', '/').ui_value, repo_name_unicode)) == False
+        assert os.path.isdir(os.path.join(Ui.get_by_key('paths', '/').ui_value, repo_name)) == False
 
     def test_delete_repo_with_group(self):
         # TODO:
         pass
 
     def test_delete_browser_fakeout(self):
-        response = self.app.post(url('delete_repo', repo_name=self.REPO),
+        response = self.app.post(base.url('delete_repo', repo_name=self.REPO),
                                  params=dict(_session_csrf_secret_token=self.session_csrf_secret_token()))
 
     def test_show(self):
         self.log_user()
-        response = self.app.get(url('summary_home', repo_name=self.REPO))
+        response = self.app.get(base.url('summary_home', repo_name=self.REPO))
 
     def test_edit(self):
-        response = self.app.get(url('edit_repo', repo_name=self.REPO))
+        response = self.app.get(base.url('edit_repo', repo_name=self.REPO))
 
     def test_set_private_flag_sets_default_to_none(self):
         self.log_user()
@@ -465,11 +463,11 @@
         assert perm[0].permission.permission_name == 'repository.read'
         assert Repository.get_by_repo_name(self.REPO).private == False
 
-        response = self.app.post(url('update_repo', repo_name=self.REPO),
+        response = self.app.post(base.url('update_repo', repo_name=self.REPO),
                         fixture._get_repo_create_params(repo_private=1,
                                                 repo_name=self.REPO,
                                                 repo_type=self.REPO_TYPE,
-                                                owner=TEST_USER_ADMIN_LOGIN,
+                                                owner=base.TEST_USER_ADMIN_LOGIN,
                                                 _session_csrf_secret_token=self.session_csrf_secret_token()))
         self.checkSessionFlash(response,
                                msg='Repository %s updated successfully' % (self.REPO))
@@ -480,11 +478,11 @@
         assert len(perm), 1
         assert perm[0].permission.permission_name == 'repository.none'
 
-        response = self.app.post(url('update_repo', repo_name=self.REPO),
+        response = self.app.post(base.url('update_repo', repo_name=self.REPO),
                         fixture._get_repo_create_params(repo_private=False,
                                                 repo_name=self.REPO,
                                                 repo_type=self.REPO_TYPE,
-                                                owner=TEST_USER_ADMIN_LOGIN,
+                                                owner=base.TEST_USER_ADMIN_LOGIN,
                                                 _session_csrf_secret_token=self.session_csrf_secret_token()))
         self.checkSessionFlash(response,
                                msg='Repository %s updated successfully' % (self.REPO))
@@ -502,17 +500,17 @@
     def test_set_repo_fork_has_no_self_id(self):
         self.log_user()
         repo = Repository.get_by_repo_name(self.REPO)
-        response = self.app.get(url('edit_repo_advanced', repo_name=self.REPO))
+        response = self.app.get(base.url('edit_repo_advanced', repo_name=self.REPO))
         opt = """<option value="%s">%s</option>""" % (repo.repo_id, self.REPO)
         response.mustcontain(no=[opt])
 
     def test_set_fork_of_other_repo(self):
         self.log_user()
-        other_repo = u'other_%s' % self.REPO_TYPE
+        other_repo = 'other_%s' % self.REPO_TYPE
         fixture.create_repo(other_repo, repo_type=self.REPO_TYPE)
         repo = Repository.get_by_repo_name(self.REPO)
         repo2 = Repository.get_by_repo_name(other_repo)
-        response = self.app.post(url('edit_repo_advanced_fork', repo_name=self.REPO),
+        response = self.app.post(base.url('edit_repo_advanced_fork', repo_name=self.REPO),
                                 params=dict(id_fork_of=repo2.repo_id, _session_csrf_secret_token=self.session_csrf_secret_token()))
         repo = Repository.get_by_repo_name(self.REPO)
         repo2 = Repository.get_by_repo_name(other_repo)
@@ -533,7 +531,7 @@
         self.log_user()
         repo = Repository.get_by_repo_name(self.REPO)
         repo2 = Repository.get_by_repo_name(self.OTHER_TYPE_REPO)
-        response = self.app.post(url('edit_repo_advanced_fork', repo_name=self.REPO),
+        response = self.app.post(base.url('edit_repo_advanced_fork', repo_name=self.REPO),
                                 params=dict(id_fork_of=repo2.repo_id, _session_csrf_secret_token=self.session_csrf_secret_token()))
         repo = Repository.get_by_repo_name(self.REPO)
         repo2 = Repository.get_by_repo_name(self.OTHER_TYPE_REPO)
@@ -543,7 +541,7 @@
     def test_set_fork_of_none(self):
         self.log_user()
         ## mark it as None
-        response = self.app.post(url('edit_repo_advanced_fork', repo_name=self.REPO),
+        response = self.app.post(base.url('edit_repo_advanced_fork', repo_name=self.REPO),
                                 params=dict(id_fork_of=None, _session_csrf_secret_token=self.session_csrf_secret_token()))
         repo = Repository.get_by_repo_name(self.REPO)
         repo2 = Repository.get_by_repo_name(self.OTHER_TYPE_REPO)
@@ -555,34 +553,34 @@
     def test_set_fork_of_same_repo(self):
         self.log_user()
         repo = Repository.get_by_repo_name(self.REPO)
-        response = self.app.post(url('edit_repo_advanced_fork', repo_name=self.REPO),
+        response = self.app.post(base.url('edit_repo_advanced_fork', repo_name=self.REPO),
                                 params=dict(id_fork_of=repo.repo_id, _session_csrf_secret_token=self.session_csrf_secret_token()))
         self.checkSessionFlash(response,
                                'An error occurred during this operation')
 
     def test_create_on_top_level_without_permissions(self):
-        usr = self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
+        usr = self.log_user(base.TEST_USER_REGULAR_LOGIN, base.TEST_USER_REGULAR_PASS)
         # revoke
         user_model = UserModel()
         # disable fork and create on default user
-        user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
-        user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
-        user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
-        user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
+        user_model.revoke_perm(User.DEFAULT_USER_NAME, 'hg.create.repository')
+        user_model.grant_perm(User.DEFAULT_USER_NAME, 'hg.create.none')
+        user_model.revoke_perm(User.DEFAULT_USER_NAME, 'hg.fork.repository')
+        user_model.grant_perm(User.DEFAULT_USER_NAME, 'hg.fork.none')
 
         # disable on regular user
-        user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
-        user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
-        user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
-        user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
+        user_model.revoke_perm(base.TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
+        user_model.grant_perm(base.TEST_USER_REGULAR_LOGIN, 'hg.create.none')
+        user_model.revoke_perm(base.TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
+        user_model.grant_perm(base.TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
         Session().commit()
 
 
         user = User.get(usr['user_id'])
 
-        repo_name = self.NEW_REPO + u'no_perms'
+        repo_name = self.NEW_REPO + 'no_perms'
         description = 'description for newly created repo'
-        response = self.app.post(url('repos'),
+        response = self.app.post(base.url('repos'),
                         fixture._get_repo_create_params(repo_private=False,
                                                 repo_name=repo_name,
                                                 repo_type=self.REPO_TYPE,
@@ -600,7 +598,7 @@
         repo_name = self.NEW_REPO
         description = 'description for newly created repo'
 
-        response = self.app.post(url('repos'),
+        response = self.app.post(base.url('repos'),
                         fixture._get_repo_create_params(repo_private=False,
                                                 repo_name=repo_name,
                                                 repo_type=self.REPO_TYPE,
@@ -618,18 +616,18 @@
 
 
 class TestAdminReposControllerGIT(_BaseTestCase):
-    REPO = GIT_REPO
+    REPO = base.GIT_REPO
     REPO_TYPE = 'git'
-    NEW_REPO = NEW_GIT_REPO
-    OTHER_TYPE_REPO = HG_REPO
+    NEW_REPO = base.NEW_GIT_REPO
+    OTHER_TYPE_REPO = base.HG_REPO
     OTHER_TYPE = 'hg'
 
 
 class TestAdminReposControllerHG(_BaseTestCase):
-    REPO = HG_REPO
+    REPO = base.HG_REPO
     REPO_TYPE = 'hg'
-    NEW_REPO = NEW_HG_REPO
-    OTHER_TYPE_REPO = GIT_REPO
+    NEW_REPO = base.NEW_HG_REPO
+    OTHER_TYPE_REPO = base.GIT_REPO
     OTHER_TYPE = 'git'
 
     def test_permanent_url_protocol_access(self):
@@ -637,7 +635,7 @@
         permanent_name = '_%d' % repo.repo_id
 
         # 400 Bad Request - Unable to detect pull/push action
-        self.app.get(url('summary_home', repo_name=permanent_name),
+        self.app.get(base.url('summary_home', repo_name=permanent_name),
             extra_environ={'HTTP_ACCEPT': 'application/mercurial'},
             status=400,
         )
--- a/kallithea/tests/functional/test_admin_settings.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_admin_settings.py	Mon May 04 19:24:04 2020 +0200
@@ -1,54 +1,53 @@
 # -*- coding: utf-8 -*-
 
 from kallithea.model.db import Setting, Ui
-from kallithea.tests.base import *
+from kallithea.tests import base
 from kallithea.tests.fixture import Fixture
 
 
 fixture = Fixture()
 
 
-class TestAdminSettingsController(TestController):
+class TestAdminSettingsController(base.TestController):
 
     def test_index_main(self):
         self.log_user()
-        response = self.app.get(url('admin_settings'))
+        response = self.app.get(base.url('admin_settings'))
 
     def test_index_mapping(self):
         self.log_user()
-        response = self.app.get(url('admin_settings_mapping'))
+        response = self.app.get(base.url('admin_settings_mapping'))
 
     def test_index_global(self):
         self.log_user()
-        response = self.app.get(url('admin_settings_global'))
+        response = self.app.get(base.url('admin_settings_global'))
 
     def test_index_visual(self):
         self.log_user()
-        response = self.app.get(url('admin_settings_visual'))
+        response = self.app.get(base.url('admin_settings_visual'))
 
     def test_index_email(self):
         self.log_user()
-        response = self.app.get(url('admin_settings_email'))
+        response = self.app.get(base.url('admin_settings_email'))
 
     def test_index_hooks(self):
         self.log_user()
-        response = self.app.get(url('admin_settings_hooks'))
+        response = self.app.get(base.url('admin_settings_hooks'))
 
     def test_create_custom_hook(self):
         self.log_user()
-        response = self.app.post(url('admin_settings_hooks'),
+        response = self.app.post(base.url('admin_settings_hooks'),
                                 params=dict(new_hook_ui_key='test_hooks_1',
-                                            new_hook_ui_value='cd %s' % TESTS_TMP_PATH,
+                                            new_hook_ui_value='cd %s' % base.TESTS_TMP_PATH,
                                             _session_csrf_secret_token=self.session_csrf_secret_token()))
 
         self.checkSessionFlash(response, 'Added new hook')
         response = response.follow()
         response.mustcontain('test_hooks_1')
-        response.mustcontain('cd %s' % TESTS_TMP_PATH)
+        response.mustcontain('cd %s' % base.TESTS_TMP_PATH)
 
-    def test_edit_custom_hook(self):
-        self.log_user()
-        response = self.app.post(url('admin_settings_hooks'),
+        # test_edit_custom_hook
+        response = self.app.post(base.url('admin_settings_hooks'),
                                 params=dict(hook_ui_key='test_hooks_1',
                                             hook_ui_value='old_value_of_hook_1',
                                             hook_ui_value_new='new_value_of_hook_1',
@@ -58,9 +57,8 @@
         response.mustcontain('test_hooks_1')
         response.mustcontain('new_value_of_hook_1')
 
-    def test_add_existing_custom_hook(self):
-        self.log_user()
-        response = self.app.post(url('admin_settings_hooks'),
+        # test_add_existing_custom_hook
+        response = self.app.post(base.url('admin_settings_hooks'),
                                 params=dict(new_hook_ui_key='test_hooks_1',
                                             new_hook_ui_value='attempted_new_value',
                                             _session_csrf_secret_token=self.session_csrf_secret_token()))
@@ -72,27 +70,27 @@
 
     def test_create_custom_hook_delete(self):
         self.log_user()
-        response = self.app.post(url('admin_settings_hooks'),
+        response = self.app.post(base.url('admin_settings_hooks'),
                                 params=dict(new_hook_ui_key='test_hooks_2',
-                                            new_hook_ui_value='cd %s2' % TESTS_TMP_PATH,
+                                            new_hook_ui_value='cd %s2' % base.TESTS_TMP_PATH,
                                             _session_csrf_secret_token=self.session_csrf_secret_token()))
 
         self.checkSessionFlash(response, 'Added new hook')
         response = response.follow()
         response.mustcontain('test_hooks_2')
-        response.mustcontain('cd %s2' % TESTS_TMP_PATH)
+        response.mustcontain('cd %s2' % base.TESTS_TMP_PATH)
 
         hook_id = Ui.get_by_key('hooks', 'test_hooks_2').ui_id
         ## delete
-        self.app.post(url('admin_settings_hooks'),
+        self.app.post(base.url('admin_settings_hooks'),
                         params=dict(hook_id=hook_id, _session_csrf_secret_token=self.session_csrf_secret_token()))
-        response = self.app.get(url('admin_settings_hooks'))
+        response = self.app.get(base.url('admin_settings_hooks'))
         response.mustcontain(no=['test_hooks_2'])
-        response.mustcontain(no=['cd %s2' % TESTS_TMP_PATH])
+        response.mustcontain(no=['cd %s2' % base.TESTS_TMP_PATH])
 
     def test_add_existing_builtin_hook(self):
         self.log_user()
-        response = self.app.post(url('admin_settings_hooks'),
+        response = self.app.post(base.url('admin_settings_hooks'),
                                 params=dict(new_hook_ui_key='changegroup.update',
                                             new_hook_ui_value='attempted_new_value',
                                             _session_csrf_secret_token=self.session_csrf_secret_token()))
@@ -104,18 +102,18 @@
 
     def test_index_search(self):
         self.log_user()
-        response = self.app.get(url('admin_settings_search'))
+        response = self.app.get(base.url('admin_settings_search'))
 
     def test_index_system(self):
         self.log_user()
-        response = self.app.get(url('admin_settings_system'))
+        response = self.app.get(base.url('admin_settings_system'))
 
     def test_ga_code_active(self):
         self.log_user()
         old_title = 'Kallithea'
         old_realm = 'Kallithea authentication'
         new_ga_code = 'ga-test-123456789'
-        response = self.app.post(url('admin_settings_global'),
+        response = self.app.post(base.url('admin_settings_global'),
                         params=dict(title=old_title,
                                  realm=old_realm,
                                  ga_code=new_ga_code,
@@ -136,7 +134,7 @@
         old_title = 'Kallithea'
         old_realm = 'Kallithea authentication'
         new_ga_code = ''
-        response = self.app.post(url('admin_settings_global'),
+        response = self.app.post(base.url('admin_settings_global'),
                         params=dict(title=old_title,
                                  realm=old_realm,
                                  ga_code=new_ga_code,
@@ -156,7 +154,7 @@
         old_title = 'Kallithea'
         old_realm = 'Kallithea authentication'
         new_ga_code = ''
-        response = self.app.post(url('admin_settings_global'),
+        response = self.app.post(base.url('admin_settings_global'),
                         params=dict(title=old_title,
                                  realm=old_realm,
                                  ga_code=new_ga_code,
@@ -168,7 +166,7 @@
         self.checkSessionFlash(response, 'Updated application settings')
         assert Setting.get_app_settings()['captcha_private_key'] == '1234567890'
 
-        response = self.app.get(url('register'))
+        response = self.app.get(base.url('register'))
         response.mustcontain('captcha')
 
     def test_captcha_deactivate(self):
@@ -176,7 +174,7 @@
         old_title = 'Kallithea'
         old_realm = 'Kallithea authentication'
         new_ga_code = ''
-        response = self.app.post(url('admin_settings_global'),
+        response = self.app.post(base.url('admin_settings_global'),
                         params=dict(title=old_title,
                                  realm=old_realm,
                                  ga_code=new_ga_code,
@@ -188,7 +186,7 @@
         self.checkSessionFlash(response, 'Updated application settings')
         assert Setting.get_app_settings()['captcha_private_key'] == ''
 
-        response = self.app.get(url('register'))
+        response = self.app.get(base.url('register'))
         response.mustcontain(no=['captcha'])
 
     def test_title_change(self):
@@ -198,7 +196,7 @@
         old_realm = 'Kallithea authentication'
 
         for new_title in ['Changed', 'Żółwik', old_title]:
-            response = self.app.post(url('admin_settings_global'),
+            response = self.app.post(base.url('admin_settings_global'),
                         params=dict(title=new_title,
                                  realm=old_realm,
                                  ga_code='',
@@ -208,7 +206,7 @@
                                 ))
 
             self.checkSessionFlash(response, 'Updated application settings')
-            assert Setting.get_app_settings()['title'] == new_title.decode('utf-8')
+            assert Setting.get_app_settings()['title'] == new_title
 
             response = response.follow()
             response.mustcontain("""<span class="branding">%s</span>""" % new_title)
--- a/kallithea/tests/functional/test_admin_user_groups.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_admin_user_groups.py	Mon May 04 19:24:04 2020 +0200
@@ -1,25 +1,25 @@
 # -*- coding: utf-8 -*-
 from kallithea.model.db import Permission, UserGroup, UserGroupToPerm
 from kallithea.model.meta import Session
-from kallithea.tests.base import *
+from kallithea.tests import base
 
 
-TEST_USER_GROUP = u'admins_test'
+TEST_USER_GROUP = 'admins_test'
 
 
-class TestAdminUsersGroupsController(TestController):
+class TestAdminUsersGroupsController(base.TestController):
 
     def test_index(self):
         self.log_user()
-        response = self.app.get(url('users_groups'))
+        response = self.app.get(base.url('users_groups'))
         # Test response...
 
     def test_create(self):
         self.log_user()
         users_group_name = TEST_USER_GROUP
-        response = self.app.post(url('users_groups'),
+        response = self.app.post(base.url('users_groups'),
                                  {'users_group_name': users_group_name,
-                                  'user_group_description': u'DESC',
+                                  'user_group_description': 'DESC',
                                   'active': True,
                                   '_session_csrf_secret_token': self.session_csrf_secret_token()})
         response.follow()
@@ -30,21 +30,21 @@
                                '/edit">%s</a>' % TEST_USER_GROUP)
 
     def test_new(self):
-        response = self.app.get(url('new_users_group'))
+        response = self.app.get(base.url('new_users_group'))
 
     def test_update(self):
-        response = self.app.post(url('update_users_group', id=1), status=403)
+        response = self.app.post(base.url('update_users_group', id=1), status=403)
 
     def test_update_browser_fakeout(self):
-        response = self.app.post(url('update_users_group', id=1),
+        response = self.app.post(base.url('update_users_group', id=1),
                                  params=dict(_session_csrf_secret_token=self.session_csrf_secret_token()))
 
     def test_delete(self):
         self.log_user()
         users_group_name = TEST_USER_GROUP + 'another'
-        response = self.app.post(url('users_groups'),
+        response = self.app.post(base.url('users_groups'),
                                  {'users_group_name': users_group_name,
-                                  'user_group_description': u'DESC',
+                                  'user_group_description': 'DESC',
                                   'active': True,
                                   '_session_csrf_secret_token': self.session_csrf_secret_token()})
         response.follow()
@@ -55,7 +55,7 @@
         gr = Session().query(UserGroup) \
             .filter(UserGroup.users_group_name == users_group_name).one()
 
-        response = self.app.post(url('delete_users_group', id=gr.users_group_id),
+        response = self.app.post(base.url('delete_users_group', id=gr.users_group_id),
             params={'_session_csrf_secret_token': self.session_csrf_secret_token()})
 
         gr = Session().query(UserGroup) \
@@ -66,9 +66,9 @@
     def test_default_perms_enable_repository_read_on_group(self):
         self.log_user()
         users_group_name = TEST_USER_GROUP + 'another2'
-        response = self.app.post(url('users_groups'),
+        response = self.app.post(base.url('users_groups'),
                                  {'users_group_name': users_group_name,
-                                  'user_group_description': u'DESC',
+                                  'user_group_description': 'DESC',
                                   'active': True,
                                   '_session_csrf_secret_token': self.session_csrf_secret_token()})
         response.follow()
@@ -77,7 +77,7 @@
         self.checkSessionFlash(response,
                                'Created user group ')
         ## ENABLE REPO CREATE ON A GROUP
-        response = self.app.post(url('edit_user_group_default_perms_update',
+        response = self.app.post(base.url('edit_user_group_default_perms_update',
                                      id=ug.users_group_id),
                                  {'create_repo_perm': True,
                                   '_session_csrf_secret_token': self.session_csrf_secret_token()})
@@ -97,7 +97,7 @@
 
         ## DISABLE REPO CREATE ON A GROUP
         response = self.app.post(
-            url('edit_user_group_default_perms_update', id=ug.users_group_id),
+            base.url('edit_user_group_default_perms_update', id=ug.users_group_id),
             params={'_session_csrf_secret_token': self.session_csrf_secret_token()})
 
         response.follow()
@@ -118,7 +118,7 @@
         # DELETE !
         ug = UserGroup.get_by_group_name(users_group_name)
         ugid = ug.users_group_id
-        response = self.app.post(url('delete_users_group', id=ug.users_group_id),
+        response = self.app.post(base.url('delete_users_group', id=ug.users_group_id),
             params={'_session_csrf_secret_token': self.session_csrf_secret_token()})
         response = response.follow()
         gr = Session().query(UserGroup) \
@@ -135,9 +135,9 @@
     def test_default_perms_enable_repository_fork_on_group(self):
         self.log_user()
         users_group_name = TEST_USER_GROUP + 'another2'
-        response = self.app.post(url('users_groups'),
+        response = self.app.post(base.url('users_groups'),
                                  {'users_group_name': users_group_name,
-                                  'user_group_description': u'DESC',
+                                  'user_group_description': 'DESC',
                                   'active': True,
                                   '_session_csrf_secret_token': self.session_csrf_secret_token()})
         response.follow()
@@ -146,7 +146,7 @@
         self.checkSessionFlash(response,
                                'Created user group ')
         ## ENABLE REPO CREATE ON A GROUP
-        response = self.app.post(url('edit_user_group_default_perms_update',
+        response = self.app.post(base.url('edit_user_group_default_perms_update',
                                      id=ug.users_group_id),
                                  {'fork_repo_perm': True, '_session_csrf_secret_token': self.session_csrf_secret_token()})
 
@@ -165,7 +165,7 @@
                     [ug.users_group_id, p3.permission_id]])
 
         ## DISABLE REPO CREATE ON A GROUP
-        response = self.app.post(url('edit_user_group_default_perms_update', id=ug.users_group_id),
+        response = self.app.post(base.url('edit_user_group_default_perms_update', id=ug.users_group_id),
             params={'_session_csrf_secret_token': self.session_csrf_secret_token()})
 
         response.follow()
@@ -185,7 +185,7 @@
         # DELETE !
         ug = UserGroup.get_by_group_name(users_group_name)
         ugid = ug.users_group_id
-        response = self.app.post(url('delete_users_group', id=ug.users_group_id),
+        response = self.app.post(base.url('delete_users_group', id=ug.users_group_id),
             params={'_session_csrf_secret_token': self.session_csrf_secret_token()})
         response = response.follow()
         gr = Session().query(UserGroup) \
@@ -201,5 +201,5 @@
         assert perms == []
 
     def test_delete_browser_fakeout(self):
-        response = self.app.post(url('delete_users_group', id=1),
+        response = self.app.post(base.url('delete_users_group', id=1),
                                  params=dict(_session_csrf_secret_token=self.session_csrf_secret_token()))
--- a/kallithea/tests/functional/test_admin_users.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_admin_users.py	Mon May 04 19:24:04 2020 +0200
@@ -17,6 +17,7 @@
 from tg.util.webtest import test_context
 from webob.exc import HTTPNotFound
 
+import kallithea
 from kallithea.controllers.admin.users import UsersController
 from kallithea.lib import helpers as h
 from kallithea.lib.auth import check_password
@@ -24,7 +25,7 @@
 from kallithea.model.db import Permission, RepoGroup, User, UserApiKeys, UserSshKeys
 from kallithea.model.meta import Session
 from kallithea.model.user import UserModel
-from kallithea.tests.base import *
+from kallithea.tests import base
 from kallithea.tests.fixture import Fixture
 
 
@@ -34,7 +35,7 @@
 @pytest.fixture
 def user_and_repo_group_fail():
     username = 'repogrouperr'
-    groupname = u'repogroup_fail'
+    groupname = 'repogroup_fail'
     user = fixture.create_user(name=username)
     repo_group = fixture.create_repo_group(name=groupname, cur_user=username)
     yield user, repo_group
@@ -43,7 +44,7 @@
         fixture.destroy_repo_group(repo_group)
 
 
-class TestAdminUsersController(TestController):
+class TestAdminUsersController(base.TestController):
     test_user_1 = 'testme'
 
     @classmethod
@@ -54,7 +55,7 @@
 
     def test_index(self):
         self.log_user()
-        response = self.app.get(url('users'))
+        response = self.app.get(base.url('users'))
         # TODO: Test response...
 
     def test_create(self):
@@ -62,11 +63,11 @@
         username = 'newtestuser'
         password = 'test12'
         password_confirmation = password
-        name = u'name'
-        lastname = u'lastname'
+        name = 'name'
+        lastname = 'lastname'
         email = 'mail@example.com'
 
-        response = self.app.post(url('new_user'),
+        response = self.app.post(base.url('new_user'),
             {'username': username,
              'password': password,
              'password_confirmation': password_confirmation,
@@ -98,11 +99,11 @@
         self.log_user()
         username = 'new_user'
         password = ''
-        name = u'name'
-        lastname = u'lastname'
+        name = 'name'
+        lastname = 'lastname'
         email = 'errmail.example.com'
 
-        response = self.app.post(url('new_user'),
+        response = self.app.post(base.url('new_user'),
             {'username': username,
              'password': password,
              'name': name,
@@ -126,9 +127,9 @@
 
     def test_new(self):
         self.log_user()
-        response = self.app.get(url('new_user'))
+        response = self.app.get(base.url('new_user'))
 
-    @parametrize('name,attrs',
+    @base.parametrize('name,attrs',
         [('firstname', {'firstname': 'new_username'}),
          ('lastname', {'lastname': 'new_username'}),
          ('admin', {'admin': True}),
@@ -167,7 +168,7 @@
             # not filled so we use creation data
 
         params.update({'_session_csrf_secret_token': self.session_csrf_secret_token()})
-        response = self.app.post(url('update_user', id=usr.user_id), params)
+        response = self.app.post(base.url('update_user', id=usr.user_id), params)
         self.checkSessionFlash(response, 'User updated successfully')
         params.pop('_session_csrf_secret_token')
 
@@ -186,7 +187,7 @@
 
         new_user = Session().query(User) \
             .filter(User.username == username).one()
-        response = self.app.post(url('delete_user', id=new_user.user_id),
+        response = self.app.post(base.url('delete_user', id=new_user.user_id),
             params={'_session_csrf_secret_token': self.session_csrf_secret_token()})
 
         self.checkSessionFlash(response, 'Successfully deleted user')
@@ -194,25 +195,25 @@
     def test_delete_repo_err(self):
         self.log_user()
         username = 'repoerr'
-        reponame = u'repoerr_fail'
+        reponame = 'repoerr_fail'
 
         fixture.create_user(name=username)
         fixture.create_repo(name=reponame, cur_user=username)
 
         new_user = Session().query(User) \
             .filter(User.username == username).one()
-        response = self.app.post(url('delete_user', id=new_user.user_id),
+        response = self.app.post(base.url('delete_user', id=new_user.user_id),
             params={'_session_csrf_secret_token': self.session_csrf_secret_token()})
-        self.checkSessionFlash(response, 'User "%s" still '
+        self.checkSessionFlash(response, 'User &quot;%s&quot; still '
                                'owns 1 repositories and cannot be removed. '
                                'Switch owners or remove those repositories: '
                                '%s' % (username, reponame))
 
-        response = self.app.post(url('delete_repo', repo_name=reponame),
+        response = self.app.post(base.url('delete_repo', repo_name=reponame),
             params={'_session_csrf_secret_token': self.session_csrf_secret_token()})
         self.checkSessionFlash(response, 'Deleted repository %s' % reponame)
 
-        response = self.app.post(url('delete_user', id=new_user.user_id),
+        response = self.app.post(base.url('delete_user', id=new_user.user_id),
             params={'_session_csrf_secret_token': self.session_csrf_secret_token()})
         self.checkSessionFlash(response, 'Successfully deleted user')
 
@@ -223,56 +224,56 @@
 
         self.log_user()
 
-        response = self.app.post(url('delete_user', id=new_user.user_id),
+        response = self.app.post(base.url('delete_user', id=new_user.user_id),
             params={'_session_csrf_secret_token': self.session_csrf_secret_token()})
-        self.checkSessionFlash(response, 'User "%s" still '
+        self.checkSessionFlash(response, 'User &quot;%s&quot; still '
                                'owns 1 repository groups and cannot be removed. '
                                'Switch owners or remove those repository groups: '
                                '%s' % (username, groupname))
 
         # Relevant _if_ the user deletion succeeded to make sure we can render groups without owner
         # rg = RepoGroup.get_by_group_name(group_name=groupname)
-        # response = self.app.get(url('repos_groups', id=rg.group_id))
+        # response = self.app.get(base.url('repos_groups', id=rg.group_id))
 
-        response = self.app.post(url('delete_repo_group', group_name=groupname),
+        response = self.app.post(base.url('delete_repo_group', group_name=groupname),
             params={'_session_csrf_secret_token': self.session_csrf_secret_token()})
         self.checkSessionFlash(response, 'Removed repository group %s' % groupname)
 
-        response = self.app.post(url('delete_user', id=new_user.user_id),
+        response = self.app.post(base.url('delete_user', id=new_user.user_id),
             params={'_session_csrf_secret_token': self.session_csrf_secret_token()})
         self.checkSessionFlash(response, 'Successfully deleted user')
 
     def test_delete_user_group_err(self):
         self.log_user()
         username = 'usergrouperr'
-        groupname = u'usergroup_fail'
+        groupname = 'usergroup_fail'
 
         fixture.create_user(name=username)
         ug = fixture.create_user_group(name=groupname, cur_user=username)
 
         new_user = Session().query(User) \
             .filter(User.username == username).one()
-        response = self.app.post(url('delete_user', id=new_user.user_id),
+        response = self.app.post(base.url('delete_user', id=new_user.user_id),
             params={'_session_csrf_secret_token': self.session_csrf_secret_token()})
-        self.checkSessionFlash(response, 'User "%s" still '
+        self.checkSessionFlash(response, 'User &quot;%s&quot; still '
                                'owns 1 user groups and cannot be removed. '
                                'Switch owners or remove those user groups: '
                                '%s' % (username, groupname))
 
         # TODO: why do this fail?
-        #response = self.app.delete(url('delete_users_group', id=groupname))
+        #response = self.app.delete(base.url('delete_users_group', id=groupname))
         #self.checkSessionFlash(response, 'Removed user group %s' % groupname)
 
         fixture.destroy_user_group(ug.users_group_id)
 
-        response = self.app.post(url('delete_user', id=new_user.user_id),
+        response = self.app.post(base.url('delete_user', id=new_user.user_id),
             params={'_session_csrf_secret_token': self.session_csrf_secret_token()})
         self.checkSessionFlash(response, 'Successfully deleted user')
 
     def test_edit(self):
         self.log_user()
-        user = User.get_by_username(TEST_USER_ADMIN_LOGIN)
-        response = self.app.get(url('edit_user', id=user.user_id))
+        user = User.get_by_username(base.TEST_USER_ADMIN_LOGIN)
+        response = self.app.get(base.url('edit_user', id=user.user_id))
 
     def test_add_perm_create_repo(self):
         self.log_user()
@@ -280,8 +281,8 @@
         perm_create = Permission.get_by_key('hg.create.repository')
 
         user = UserModel().create_or_update(username='dummy', password='qwe',
-                                            email='dummy', firstname=u'a',
-                                            lastname=u'b')
+                                            email='dummy', firstname='a',
+                                            lastname='b')
         Session().commit()
         uid = user.user_id
 
@@ -290,7 +291,7 @@
             assert UserModel().has_perm(user, perm_none) == False
             assert UserModel().has_perm(user, perm_create) == False
 
-            response = self.app.post(url('edit_user_perms_update', id=uid),
+            response = self.app.post(base.url('edit_user_perms_update', id=uid),
                                      params=dict(create_repo_perm=True,
                                                  _session_csrf_secret_token=self.session_csrf_secret_token()))
 
@@ -310,8 +311,8 @@
         perm_create = Permission.get_by_key('hg.create.repository')
 
         user = UserModel().create_or_update(username='dummy', password='qwe',
-                                            email='dummy', firstname=u'a',
-                                            lastname=u'b')
+                                            email='dummy', firstname='a',
+                                            lastname='b')
         Session().commit()
         uid = user.user_id
 
@@ -320,7 +321,7 @@
             assert UserModel().has_perm(user, perm_none) == False
             assert UserModel().has_perm(user, perm_create) == False
 
-            response = self.app.post(url('edit_user_perms_update', id=uid),
+            response = self.app.post(base.url('edit_user_perms_update', id=uid),
                                      params=dict(_session_csrf_secret_token=self.session_csrf_secret_token()))
 
             perm_none = Permission.get_by_key('hg.create.none')
@@ -339,8 +340,8 @@
         perm_fork = Permission.get_by_key('hg.fork.repository')
 
         user = UserModel().create_or_update(username='dummy', password='qwe',
-                                            email='dummy', firstname=u'a',
-                                            lastname=u'b')
+                                            email='dummy', firstname='a',
+                                            lastname='b')
         Session().commit()
         uid = user.user_id
 
@@ -349,7 +350,7 @@
             assert UserModel().has_perm(user, perm_none) == False
             assert UserModel().has_perm(user, perm_fork) == False
 
-            response = self.app.post(url('edit_user_perms_update', id=uid),
+            response = self.app.post(base.url('edit_user_perms_update', id=uid),
                                      params=dict(create_repo_perm=True,
                                                  _session_csrf_secret_token=self.session_csrf_secret_token()))
 
@@ -369,8 +370,8 @@
         perm_fork = Permission.get_by_key('hg.fork.repository')
 
         user = UserModel().create_or_update(username='dummy', password='qwe',
-                                            email='dummy', firstname=u'a',
-                                            lastname=u'b')
+                                            email='dummy', firstname='a',
+                                            lastname='b')
         Session().commit()
         uid = user.user_id
 
@@ -379,7 +380,7 @@
             assert UserModel().has_perm(user, perm_none) == False
             assert UserModel().has_perm(user, perm_fork) == False
 
-            response = self.app.post(url('edit_user_perms_update', id=uid),
+            response = self.app.post(base.url('edit_user_perms_update', id=uid),
                                      params=dict(_session_csrf_secret_token=self.session_csrf_secret_token()))
 
             perm_none = Permission.get_by_key('hg.create.none')
@@ -394,11 +395,11 @@
 
     def test_ips(self):
         self.log_user()
-        user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
-        response = self.app.get(url('edit_user_ips', id=user.user_id))
+        user = User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
+        response = self.app.get(base.url('edit_user_ips', id=user.user_id))
         response.mustcontain('All IP addresses are allowed')
 
-    @parametrize('test_name,ip,ip_range,failure', [
+    @base.parametrize('test_name,ip,ip_range,failure', [
         ('127/24', '127.0.0.1/24', '127.0.0.0 - 127.0.0.255', False),
         ('10/32', '10.0.0.10/32', '10.0.0.10 - 10.0.0.10', False),
         ('0/16', '0.0.0.0/16', '0.0.0.0 - 0.0.255.255', False),
@@ -408,26 +409,26 @@
     ])
     def test_add_ip(self, test_name, ip, ip_range, failure, auto_clear_ip_permissions):
         self.log_user()
-        user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
+        user = User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
         user_id = user.user_id
 
-        response = self.app.post(url('edit_user_ips_update', id=user_id),
+        response = self.app.post(base.url('edit_user_ips_update', id=user_id),
                                  params=dict(new_ip=ip, _session_csrf_secret_token=self.session_csrf_secret_token()))
 
         if failure:
             self.checkSessionFlash(response, 'Please enter a valid IPv4 or IPv6 address')
-            response = self.app.get(url('edit_user_ips', id=user_id))
+            response = self.app.get(base.url('edit_user_ips', id=user_id))
             response.mustcontain(no=[ip])
             response.mustcontain(no=[ip_range])
 
         else:
-            response = self.app.get(url('edit_user_ips', id=user_id))
+            response = self.app.get(base.url('edit_user_ips', id=user_id))
             response.mustcontain(ip)
             response.mustcontain(ip_range)
 
     def test_delete_ip(self, auto_clear_ip_permissions):
         self.log_user()
-        user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
+        user = User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
         user_id = user.user_id
         ip = '127.0.0.1/32'
         ip_range = '127.0.0.1 - 127.0.0.1'
@@ -436,14 +437,14 @@
             Session().commit()
         new_ip_id = new_ip.ip_id
 
-        response = self.app.get(url('edit_user_ips', id=user_id))
+        response = self.app.get(base.url('edit_user_ips', id=user_id))
         response.mustcontain(ip)
         response.mustcontain(ip_range)
 
-        self.app.post(url('edit_user_ips_delete', id=user_id),
+        self.app.post(base.url('edit_user_ips_delete', id=user_id),
                       params=dict(del_ip_id=new_ip_id, _session_csrf_secret_token=self.session_csrf_secret_token()))
 
-        response = self.app.get(url('edit_user_ips', id=user_id))
+        response = self.app.get(base.url('edit_user_ips', id=user_id))
         response.mustcontain('All IP addresses are allowed')
         response.mustcontain(no=[ip])
         response.mustcontain(no=[ip_range])
@@ -451,22 +452,22 @@
     def test_api_keys(self):
         self.log_user()
 
-        user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
-        response = self.app.get(url('edit_user_api_keys', id=user.user_id))
+        user = User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
+        response = self.app.get(base.url('edit_user_api_keys', id=user.user_id))
         response.mustcontain(user.api_key)
         response.mustcontain('Expires: Never')
 
-    @parametrize('desc,lifetime', [
+    @base.parametrize('desc,lifetime', [
         ('forever', -1),
         ('5mins', 60*5),
         ('30days', 60*60*24*30),
     ])
     def test_add_api_keys(self, desc, lifetime):
         self.log_user()
-        user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
+        user = User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
         user_id = user.user_id
 
-        response = self.app.post(url('edit_user_api_keys_update', id=user_id),
+        response = self.app.post(base.url('edit_user_api_keys_update', id=user_id),
                  {'description': desc, 'lifetime': lifetime, '_session_csrf_secret_token': self.session_csrf_secret_token()})
         self.checkSessionFlash(response, 'API key successfully created')
         try:
@@ -481,10 +482,10 @@
 
     def test_remove_api_key(self):
         self.log_user()
-        user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
+        user = User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
         user_id = user.user_id
 
-        response = self.app.post(url('edit_user_api_keys_update', id=user_id),
+        response = self.app.post(base.url('edit_user_api_keys_update', id=user_id),
                 {'description': 'desc', 'lifetime': -1, '_session_csrf_secret_token': self.session_csrf_secret_token()})
         self.checkSessionFlash(response, 'API key successfully created')
         response = response.follow()
@@ -493,7 +494,7 @@
         keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
         assert 1 == len(keys)
 
-        response = self.app.post(url('edit_user_api_keys_delete', id=user_id),
+        response = self.app.post(base.url('edit_user_api_keys_delete', id=user_id),
                  {'del_api_key': keys[0].api_key, '_session_csrf_secret_token': self.session_csrf_secret_token()})
         self.checkSessionFlash(response, 'API key successfully deleted')
         keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
@@ -501,29 +502,29 @@
 
     def test_reset_main_api_key(self):
         self.log_user()
-        user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
+        user = User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
         user_id = user.user_id
         api_key = user.api_key
-        response = self.app.get(url('edit_user_api_keys', id=user_id))
+        response = self.app.get(base.url('edit_user_api_keys', id=user_id))
         response.mustcontain(api_key)
         response.mustcontain('Expires: Never')
 
-        response = self.app.post(url('edit_user_api_keys_delete', id=user_id),
+        response = self.app.post(base.url('edit_user_api_keys_delete', id=user_id),
                  {'del_api_key_builtin': api_key, '_session_csrf_secret_token': self.session_csrf_secret_token()})
         self.checkSessionFlash(response, 'API key successfully reset')
         response = response.follow()
         response.mustcontain(no=[api_key])
 
     def test_add_ssh_key(self):
-        description = u'something'
-        public_key = u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6Ycnc2oUZHQnQwuqgZqTTdMDZD7ataf3JM7oG2Fw8JR6cdmz4QZLe5mfDwaFwG2pWHLRpVqzfrD/Pn3rIO++bgCJH5ydczrl1WScfryV1hYMJ/4EzLGM657J1/q5EI+b9SntKjf4ax+KP322L0TNQGbZUHLbfG2MwHMrYBQpHUQ== me@localhost'
-        fingerprint = u'Ke3oUCNJM87P0jJTb3D+e3shjceP2CqMpQKVd75E9I8'
+        description = 'something'
+        public_key = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6Ycnc2oUZHQnQwuqgZqTTdMDZD7ataf3JM7oG2Fw8JR6cdmz4QZLe5mfDwaFwG2pWHLRpVqzfrD/Pn3rIO++bgCJH5ydczrl1WScfryV1hYMJ/4EzLGM657J1/q5EI+b9SntKjf4ax+KP322L0TNQGbZUHLbfG2MwHMrYBQpHUQ== me@localhost'
+        fingerprint = 'Ke3oUCNJM87P0jJTb3D+e3shjceP2CqMpQKVd75E9I8'
 
         self.log_user()
-        user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
+        user = User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
         user_id = user.user_id
 
-        response = self.app.post(url('edit_user_ssh_keys', id=user_id),
+        response = self.app.post(base.url('edit_user_ssh_keys', id=user_id),
                                  {'description': description,
                                   'public_key': public_key,
                                   '_session_csrf_secret_token': self.session_csrf_secret_token()})
@@ -538,32 +539,32 @@
         Session().commit()
 
     def test_remove_ssh_key(self):
-        description = u''
-        public_key = u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6Ycnc2oUZHQnQwuqgZqTTdMDZD7ataf3JM7oG2Fw8JR6cdmz4QZLe5mfDwaFwG2pWHLRpVqzfrD/Pn3rIO++bgCJH5ydczrl1WScfryV1hYMJ/4EzLGM657J1/q5EI+b9SntKjf4ax+KP322L0TNQGbZUHLbfG2MwHMrYBQpHUQ== me@localhost'
-        fingerprint = u'Ke3oUCNJM87P0jJTb3D+e3shjceP2CqMpQKVd75E9I8'
+        description = ''
+        public_key = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6Ycnc2oUZHQnQwuqgZqTTdMDZD7ataf3JM7oG2Fw8JR6cdmz4QZLe5mfDwaFwG2pWHLRpVqzfrD/Pn3rIO++bgCJH5ydczrl1WScfryV1hYMJ/4EzLGM657J1/q5EI+b9SntKjf4ax+KP322L0TNQGbZUHLbfG2MwHMrYBQpHUQ== me@localhost'
+        fingerprint = 'Ke3oUCNJM87P0jJTb3D+e3shjceP2CqMpQKVd75E9I8'
 
         self.log_user()
-        user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
+        user = User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
         user_id = user.user_id
 
-        response = self.app.post(url('edit_user_ssh_keys', id=user_id),
+        response = self.app.post(base.url('edit_user_ssh_keys', id=user_id),
                                  {'description': description,
                                   'public_key': public_key,
                                   '_session_csrf_secret_token': self.session_csrf_secret_token()})
         self.checkSessionFlash(response, 'SSH key %s successfully added' % fingerprint)
         response.follow()
         ssh_key = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).one()
-        assert ssh_key.description == u'me@localhost'
+        assert ssh_key.description == 'me@localhost'
 
-        response = self.app.post(url('edit_user_ssh_keys_delete', id=user_id),
-                                 {'del_public_key': ssh_key.public_key,
+        response = self.app.post(base.url('edit_user_ssh_keys_delete', id=user_id),
+                                 {'del_public_key_fingerprint': ssh_key.fingerprint,
                                   '_session_csrf_secret_token': self.session_csrf_secret_token()})
         self.checkSessionFlash(response, 'SSH key successfully deleted')
         keys = UserSshKeys.query().all()
         assert 0 == len(keys)
 
 
-class TestAdminUsersController_unittest(TestController):
+class TestAdminUsersController_unittest(base.TestController):
     """ Unit tests for the users controller """
 
     def test_get_user_or_raise_if_default(self, monkeypatch, test_context_fixture):
@@ -574,14 +575,14 @@
 
         u = UsersController()
         # a regular user should work correctly
-        user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
+        user = User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
         assert u._get_user_or_raise_if_default(user.user_id) == user
         # the default user should raise
         with pytest.raises(HTTPNotFound):
-            u._get_user_or_raise_if_default(User.get_default_user().user_id)
+            u._get_user_or_raise_if_default(kallithea.DEFAULT_USER_ID)
 
 
-class TestAdminUsersControllerForDefaultUser(TestController):
+class TestAdminUsersControllerForDefaultUser(base.TestController):
     """
     Edit actions on the default user are not allowed.
     Validate that they throw a 404 exception.
@@ -589,59 +590,59 @@
     def test_edit_default_user(self):
         self.log_user()
         user = User.get_default_user()
-        response = self.app.get(url('edit_user', id=user.user_id), status=404)
+        response = self.app.get(base.url('edit_user', id=user.user_id), status=404)
 
     def test_edit_advanced_default_user(self):
         self.log_user()
         user = User.get_default_user()
-        response = self.app.get(url('edit_user_advanced', id=user.user_id), status=404)
+        response = self.app.get(base.url('edit_user_advanced', id=user.user_id), status=404)
 
     # API keys
     def test_edit_api_keys_default_user(self):
         self.log_user()
         user = User.get_default_user()
-        response = self.app.get(url('edit_user_api_keys', id=user.user_id), status=404)
+        response = self.app.get(base.url('edit_user_api_keys', id=user.user_id), status=404)
 
     def test_add_api_keys_default_user(self):
         self.log_user()
         user = User.get_default_user()
-        response = self.app.post(url('edit_user_api_keys_update', id=user.user_id),
+        response = self.app.post(base.url('edit_user_api_keys_update', id=user.user_id),
                  {'_session_csrf_secret_token': self.session_csrf_secret_token()}, status=404)
 
     def test_delete_api_keys_default_user(self):
         self.log_user()
         user = User.get_default_user()
-        response = self.app.post(url('edit_user_api_keys_delete', id=user.user_id),
+        response = self.app.post(base.url('edit_user_api_keys_delete', id=user.user_id),
                  {'_session_csrf_secret_token': self.session_csrf_secret_token()}, status=404)
 
     # Permissions
     def test_edit_perms_default_user(self):
         self.log_user()
         user = User.get_default_user()
-        response = self.app.get(url('edit_user_perms', id=user.user_id), status=404)
+        response = self.app.get(base.url('edit_user_perms', id=user.user_id), status=404)
 
     def test_update_perms_default_user(self):
         self.log_user()
         user = User.get_default_user()
-        response = self.app.post(url('edit_user_perms_update', id=user.user_id),
+        response = self.app.post(base.url('edit_user_perms_update', id=user.user_id),
                  {'_session_csrf_secret_token': self.session_csrf_secret_token()}, status=404)
 
     # Emails
     def test_edit_emails_default_user(self):
         self.log_user()
         user = User.get_default_user()
-        response = self.app.get(url('edit_user_emails', id=user.user_id), status=404)
+        response = self.app.get(base.url('edit_user_emails', id=user.user_id), status=404)
 
     def test_add_emails_default_user(self):
         self.log_user()
         user = User.get_default_user()
-        response = self.app.post(url('edit_user_emails_update', id=user.user_id),
+        response = self.app.post(base.url('edit_user_emails_update', id=user.user_id),
                  {'_session_csrf_secret_token': self.session_csrf_secret_token()}, status=404)
 
     def test_delete_emails_default_user(self):
         self.log_user()
         user = User.get_default_user()
-        response = self.app.post(url('edit_user_emails_delete', id=user.user_id),
+        response = self.app.post(base.url('edit_user_emails_delete', id=user.user_id),
                  {'_session_csrf_secret_token': self.session_csrf_secret_token()}, status=404)
 
     # IP addresses
@@ -650,4 +651,4 @@
     def test_edit_ip_default_user(self):
         self.log_user()
         user = User.get_default_user()
-        response = self.app.get(url('edit_user_ips', id=user.user_id), status=404)
+        response = self.app.get(base.url('edit_user_ips', id=user.user_id), status=404)
--- a/kallithea/tests/functional/test_changelog.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_changelog.py	Mon May 04 19:24:04 2020 +0200
@@ -1,12 +1,12 @@
-from kallithea.tests.base import *
+from kallithea.tests import base
 
 
-class TestChangelogController(TestController):
+class TestChangelogController(base.TestController):
 
     def test_index_hg(self):
         self.log_user()
-        response = self.app.get(url(controller='changelog', action='index',
-                                    repo_name=HG_REPO))
+        response = self.app.get(base.url(controller='changelog', action='index',
+                                    repo_name=base.HG_REPO))
 
         response.mustcontain('''id="chg_20" class="mergerow"''')
         response.mustcontain(
@@ -17,7 +17,7 @@
         )
         # rev 640: code garden
         response.mustcontain(
-            """<a class="changeset_hash" href="/%s/changeset/0a4e54a4460401d6dbbd6a3604b17cd2b3606b82">r640:0a4e54a44604</a>""" % HG_REPO
+            """<a class="changeset_hash" href="/%s/changeset/0a4e54a4460401d6dbbd6a3604b17cd2b3606b82">r640:0a4e54a44604</a>""" % base.HG_REPO
         )
         response.mustcontain("""code garden""")
 
@@ -26,18 +26,18 @@
     def test_index_pagination_hg(self):
         self.log_user()
         # pagination
-        self.app.get(url(controller='changelog', action='index',
-                                    repo_name=HG_REPO), {'page': 1})
-        self.app.get(url(controller='changelog', action='index',
-                                    repo_name=HG_REPO), {'page': 2})
-        self.app.get(url(controller='changelog', action='index',
-                                    repo_name=HG_REPO), {'page': 3})
-        self.app.get(url(controller='changelog', action='index',
-                                    repo_name=HG_REPO), {'page': 4})
-        self.app.get(url(controller='changelog', action='index',
-                                    repo_name=HG_REPO), {'page': 5})
-        response = self.app.get(url(controller='changelog', action='index',
-                                    repo_name=HG_REPO), {'page': 6, 'size': 20})
+        self.app.get(base.url(controller='changelog', action='index',
+                                    repo_name=base.HG_REPO), {'page': 1})
+        self.app.get(base.url(controller='changelog', action='index',
+                                    repo_name=base.HG_REPO), {'page': 2})
+        self.app.get(base.url(controller='changelog', action='index',
+                                    repo_name=base.HG_REPO), {'page': 3})
+        self.app.get(base.url(controller='changelog', action='index',
+                                    repo_name=base.HG_REPO), {'page': 4})
+        self.app.get(base.url(controller='changelog', action='index',
+                                    repo_name=base.HG_REPO), {'page': 5})
+        response = self.app.get(base.url(controller='changelog', action='index',
+                                    repo_name=base.HG_REPO), {'page': 6, 'size': 20})
 
         # Test response after pagination...
         response.mustcontain(
@@ -53,8 +53,8 @@
 
     def test_index_git(self):
         self.log_user()
-        response = self.app.get(url(controller='changelog', action='index',
-                                    repo_name=GIT_REPO))
+        response = self.app.get(base.url(controller='changelog', action='index',
+                                    repo_name=base.GIT_REPO))
 
         response.mustcontain('''id="chg_20" class=""''') # why no mergerow for git?
         response.mustcontain(
@@ -82,18 +82,18 @@
     def test_index_pagination_git(self):
         self.log_user()
         # pagination
-        self.app.get(url(controller='changelog', action='index',
-                                    repo_name=GIT_REPO), {'page': 1})
-        self.app.get(url(controller='changelog', action='index',
-                                    repo_name=GIT_REPO), {'page': 2})
-        self.app.get(url(controller='changelog', action='index',
-                                    repo_name=GIT_REPO), {'page': 3})
-        self.app.get(url(controller='changelog', action='index',
-                                    repo_name=GIT_REPO), {'page': 4})
-        self.app.get(url(controller='changelog', action='index',
-                                    repo_name=GIT_REPO), {'page': 5})
-        response = self.app.get(url(controller='changelog', action='index',
-                                    repo_name=GIT_REPO), {'page': 6, 'size': 20})
+        self.app.get(base.url(controller='changelog', action='index',
+                                    repo_name=base.GIT_REPO), {'page': 1})
+        self.app.get(base.url(controller='changelog', action='index',
+                                    repo_name=base.GIT_REPO), {'page': 2})
+        self.app.get(base.url(controller='changelog', action='index',
+                                    repo_name=base.GIT_REPO), {'page': 3})
+        self.app.get(base.url(controller='changelog', action='index',
+                                    repo_name=base.GIT_REPO), {'page': 4})
+        self.app.get(base.url(controller='changelog', action='index',
+                                    repo_name=base.GIT_REPO), {'page': 5})
+        response = self.app.get(base.url(controller='changelog', action='index',
+                                    repo_name=base.GIT_REPO), {'page': 6, 'size': 20})
 
         # Test response after pagination...
         response.mustcontain(
@@ -109,9 +109,9 @@
 
     def test_index_hg_with_filenode(self):
         self.log_user()
-        response = self.app.get(url(controller='changelog', action='index',
+        response = self.app.get(base.url(controller='changelog', action='index',
                                     revision='tip', f_path='/vcs/exceptions.py',
-                                    repo_name=HG_REPO))
+                                    repo_name=base.HG_REPO))
         # history commits messages
         response.mustcontain('Added exceptions module, this time for real')
         response.mustcontain('Added not implemented hg backend test case')
@@ -120,9 +120,9 @@
 
     def test_index_git_with_filenode(self):
         self.log_user()
-        response = self.app.get(url(controller='changelog', action='index',
+        response = self.app.get(base.url(controller='changelog', action='index',
                                     revision='tip', f_path='/vcs/exceptions.py',
-                                    repo_name=GIT_REPO))
+                                    repo_name=base.GIT_REPO))
         # history commits messages
         response.mustcontain('Added exceptions module, this time for real')
         response.mustcontain('Added not implemented hg backend test case')
@@ -130,28 +130,28 @@
 
     def test_index_hg_with_filenode_that_is_dirnode(self):
         self.log_user()
-        response = self.app.get(url(controller='changelog', action='index',
+        response = self.app.get(base.url(controller='changelog', action='index',
                                     revision='tip', f_path='/tests',
-                                    repo_name=HG_REPO))
+                                    repo_name=base.HG_REPO))
         assert response.status == '302 Found'
 
     def test_index_git_with_filenode_that_is_dirnode(self):
         self.log_user()
-        response = self.app.get(url(controller='changelog', action='index',
+        response = self.app.get(base.url(controller='changelog', action='index',
                                     revision='tip', f_path='/tests',
-                                    repo_name=GIT_REPO))
+                                    repo_name=base.GIT_REPO))
         assert response.status == '302 Found'
 
     def test_index_hg_with_filenode_not_existing(self):
         self.log_user()
-        response = self.app.get(url(controller='changelog', action='index',
+        response = self.app.get(base.url(controller='changelog', action='index',
                                     revision='tip', f_path='/wrong_path',
-                                    repo_name=HG_REPO))
+                                    repo_name=base.HG_REPO))
         assert response.status == '302 Found'
 
     def test_index_git_with_filenode_not_existing(self):
         self.log_user()
-        response = self.app.get(url(controller='changelog', action='index',
+        response = self.app.get(base.url(controller='changelog', action='index',
                                     revision='tip', f_path='/wrong_path',
-                                    repo_name=GIT_REPO))
+                                    repo_name=base.GIT_REPO))
         assert response.status == '302 Found'
--- a/kallithea/tests/functional/test_changeset.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_changeset.py	Mon May 04 19:24:04 2020 +0200
@@ -1,24 +1,24 @@
-from kallithea.tests.base import *
+from kallithea.tests import base
 
 
-class TestChangesetController(TestController):
+class TestChangesetController(base.TestController):
 
     def test_index(self):
-        response = self.app.get(url(controller='changeset', action='index',
-                                    repo_name=HG_REPO, revision='tip'))
+        response = self.app.get(base.url(controller='changeset', action='index',
+                                    repo_name=base.HG_REPO, revision='tip'))
         # Test response...
 
     def test_changeset_range(self):
-        #print self.app.get(url(controller='changelog', action='index', repo_name=HG_REPO))
+        #print self.app.get(base.url(controller='changelog', action='index', repo_name=base.HG_REPO))
 
-        response = self.app.get(url(controller='changeset', action='index',
-                                    repo_name=HG_REPO, revision='a53d9201d4bc278910d416d94941b7ea007ecd52...96507bd11ecc815ebc6270fdf6db110928c09c1e'))
+        response = self.app.get(base.url(controller='changeset', action='index',
+                                    repo_name=base.HG_REPO, revision='a53d9201d4bc278910d416d94941b7ea007ecd52...96507bd11ecc815ebc6270fdf6db110928c09c1e'))
 
-        response = self.app.get(url(controller='changeset', action='changeset_raw',
-                                    repo_name=HG_REPO, revision='a53d9201d4bc278910d416d94941b7ea007ecd52'))
+        response = self.app.get(base.url(controller='changeset', action='changeset_raw',
+                                    repo_name=base.HG_REPO, revision='a53d9201d4bc278910d416d94941b7ea007ecd52'))
 
-        response = self.app.get(url(controller='changeset', action='changeset_patch',
-                                    repo_name=HG_REPO, revision='a53d9201d4bc278910d416d94941b7ea007ecd52'))
+        response = self.app.get(base.url(controller='changeset', action='changeset_patch',
+                                    repo_name=base.HG_REPO, revision='a53d9201d4bc278910d416d94941b7ea007ecd52'))
 
-        response = self.app.get(url(controller='changeset', action='changeset_download',
-                                    repo_name=HG_REPO, revision='a53d9201d4bc278910d416d94941b7ea007ecd52'))
+        response = self.app.get(base.url(controller='changeset', action='changeset_download',
+                                    repo_name=base.HG_REPO, revision='a53d9201d4bc278910d416d94941b7ea007ecd52'))
--- a/kallithea/tests/functional/test_changeset_pullrequests_comments.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_changeset_pullrequests_comments.py	Mon May 04 19:24:04 2020 +0200
@@ -3,10 +3,10 @@
 from kallithea.model.changeset_status import ChangesetStatusModel
 from kallithea.model.db import ChangesetComment, PullRequest
 from kallithea.model.meta import Session
-from kallithea.tests.base import *
+from kallithea.tests import base
 
 
-class TestChangeSetCommentsController(TestController):
+class TestChangeSetCommentsController(base.TestController):
 
     def setup_method(self, method):
         for x in ChangesetComment.query().all():
@@ -16,17 +16,17 @@
     def test_create(self):
         self.log_user()
         rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
-        text = u'general comment on changeset'
+        text = 'general comment on changeset'
 
         params = {'text': text, '_session_csrf_secret_token': self.session_csrf_secret_token()}
-        response = self.app.post(url(controller='changeset', action='comment',
-                                     repo_name=HG_REPO, revision=rev),
+        response = self.app.post(base.url(controller='changeset', action='comment',
+                                     repo_name=base.HG_REPO, revision=rev),
                                      params=params, extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
         # Test response...
         assert response.status == '200 OK'
 
-        response = self.app.get(url(controller='changeset', action='index',
-                                repo_name=HG_REPO, revision=rev))
+        response = self.app.get(base.url(controller='changeset', action='index',
+                                repo_name=base.HG_REPO, revision=rev))
         response.mustcontain(
             '''<div class="comments-number">'''
             ''' 1 comment (0 inline, 1 general)'''
@@ -39,19 +39,19 @@
     def test_create_inline(self):
         self.log_user()
         rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
-        text = u'inline comment on changeset'
+        text = 'inline comment on changeset'
         f_path = 'vcs/web/simplevcs/views/repository.py'
         line = 'n1'
 
         params = {'text': text, 'f_path': f_path, 'line': line, '_session_csrf_secret_token': self.session_csrf_secret_token()}
-        response = self.app.post(url(controller='changeset', action='comment',
-                                     repo_name=HG_REPO, revision=rev),
+        response = self.app.post(base.url(controller='changeset', action='comment',
+                                     repo_name=base.HG_REPO, revision=rev),
                                      params=params, extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
         # Test response...
         assert response.status == '200 OK'
 
-        response = self.app.get(url(controller='changeset', action='index',
-                                repo_name=HG_REPO, revision=rev))
+        response = self.app.get(base.url(controller='changeset', action='index',
+                                repo_name=base.HG_REPO, revision=rev))
         response.mustcontain(
             '''<div class="comments-number">'''
             ''' 1 comment (1 inline, 0 general)'''
@@ -70,22 +70,22 @@
         self.log_user()
 
         rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
-        text = u'@%s check CommentOnRevision' % TEST_USER_REGULAR_LOGIN
+        text = '@%s check CommentOnRevision' % base.TEST_USER_REGULAR_LOGIN
 
         params = {'text': text, '_session_csrf_secret_token': self.session_csrf_secret_token()}
-        response = self.app.post(url(controller='changeset', action='comment',
-                                     repo_name=HG_REPO, revision=rev),
+        response = self.app.post(base.url(controller='changeset', action='comment',
+                                     repo_name=base.HG_REPO, revision=rev),
                                      params=params, extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
         # Test response...
         assert response.status == '200 OK'
 
-        response = self.app.get(url(controller='changeset', action='index',
-                                repo_name=HG_REPO, revision=rev))
+        response = self.app.get(base.url(controller='changeset', action='index',
+                                repo_name=base.HG_REPO, revision=rev))
         response.mustcontain(
             '''<div class="comments-number">'''
             ''' 1 comment (0 inline, 1 general)'''
         )
-        response.mustcontain('<b>@%s</b> check CommentOnRevision' % TEST_USER_REGULAR_LOGIN)
+        response.mustcontain('<b>@%s</b> check CommentOnRevision' % base.TEST_USER_REGULAR_LOGIN)
 
         # test DB
         assert ChangesetComment.query().count() == 1
@@ -93,18 +93,18 @@
     def test_create_status_change(self):
         self.log_user()
         rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
-        text = u'general comment on changeset'
+        text = 'general comment on changeset'
 
         params = {'text': text, 'changeset_status': 'rejected',
                 '_session_csrf_secret_token': self.session_csrf_secret_token()}
-        response = self.app.post(url(controller='changeset', action='comment',
-                                     repo_name=HG_REPO, revision=rev),
+        response = self.app.post(base.url(controller='changeset', action='comment',
+                                     repo_name=base.HG_REPO, revision=rev),
                                      params=params, extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
         # Test response...
         assert response.status == '200 OK'
 
-        response = self.app.get(url(controller='changeset', action='index',
-                                repo_name=HG_REPO, revision=rev))
+        response = self.app.get(base.url(controller='changeset', action='index',
+                                repo_name=base.HG_REPO, revision=rev))
         response.mustcontain(
             '''<div class="comments-number">'''
             ''' 1 comment (0 inline, 1 general)'''
@@ -115,33 +115,33 @@
         assert ChangesetComment.query().count() == 1
 
         # check status
-        status = ChangesetStatusModel().get_status(repo=HG_REPO, revision=rev)
+        status = ChangesetStatusModel().get_status(repo=base.HG_REPO, revision=rev)
         assert status == 'rejected'
 
     def test_delete(self):
         self.log_user()
         rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
-        text = u'general comment on changeset to be deleted'
+        text = 'general comment on changeset to be deleted'
 
         params = {'text': text, '_session_csrf_secret_token': self.session_csrf_secret_token()}
-        response = self.app.post(url(controller='changeset', action='comment',
-                                     repo_name=HG_REPO, revision=rev),
+        response = self.app.post(base.url(controller='changeset', action='comment',
+                                     repo_name=base.HG_REPO, revision=rev),
                                      params=params, extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
 
         comments = ChangesetComment.query().all()
         assert len(comments) == 1
         comment_id = comments[0].comment_id
 
-        self.app.post(url("changeset_comment_delete",
-                                    repo_name=HG_REPO,
+        self.app.post(base.url("changeset_comment_delete",
+                                    repo_name=base.HG_REPO,
                                     comment_id=comment_id),
             params={'_session_csrf_secret_token': self.session_csrf_secret_token()})
 
         comments = ChangesetComment.query().all()
         assert len(comments) == 0
 
-        response = self.app.get(url(controller='changeset', action='index',
-                                repo_name=HG_REPO, revision=rev))
+        response = self.app.get(base.url(controller='changeset', action='index',
+                                repo_name=base.HG_REPO, revision=rev))
         response.mustcontain(
             '''<div class="comments-number">'''
             ''' 0 comments (0 inline, 0 general)'''
@@ -149,7 +149,7 @@
         response.mustcontain(no=text)
 
 
-class TestPullrequestsCommentsController(TestController):
+class TestPullrequestsCommentsController(base.TestController):
 
     def setup_method(self, method):
         for x in ChangesetComment.query().all():
@@ -157,34 +157,34 @@
         Session().commit()
 
     def _create_pr(self):
-        response = self.app.post(url(controller='pullrequests', action='create',
-                                     repo_name=HG_REPO),
-                                 {'org_repo': HG_REPO,
+        response = self.app.post(base.url(controller='pullrequests', action='create',
+                                     repo_name=base.HG_REPO),
+                                 {'org_repo': base.HG_REPO,
                                   'org_ref': 'branch:stable:4f7e2131323e0749a740c0a56ab68ae9269c562a',
-                                  'other_repo': HG_REPO,
+                                  'other_repo': base.HG_REPO,
                                   'other_ref': 'branch:default:96507bd11ecc815ebc6270fdf6db110928c09c1e',
                                   'pullrequest_title': 'title',
                                   'pullrequest_desc': 'description',
                                   '_session_csrf_secret_token': self.session_csrf_secret_token(),
                                  },
                                  status=302)
-        pr_id = int(re.search('/pull-request/(\d+)/', response.location).group(1))
+        pr_id = int(re.search(r'/pull-request/(\d+)/', response.location).group(1))
         return pr_id
 
     def test_create(self):
         self.log_user()
         pr_id = self._create_pr()
 
-        text = u'general comment on pullrequest'
+        text = 'general comment on pullrequest'
         params = {'text': text, '_session_csrf_secret_token': self.session_csrf_secret_token()}
-        response = self.app.post(url(controller='pullrequests', action='comment',
-                                     repo_name=HG_REPO, pull_request_id=pr_id),
+        response = self.app.post(base.url(controller='pullrequests', action='comment',
+                                     repo_name=base.HG_REPO, pull_request_id=pr_id),
                                      params=params, extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
         # Test response...
         assert response.status == '200 OK'
 
-        response = self.app.get(url(controller='pullrequests', action='show',
-                                repo_name=HG_REPO, pull_request_id=pr_id, extra=''))
+        response = self.app.get(base.url(controller='pullrequests', action='show',
+                                repo_name=base.HG_REPO, pull_request_id=pr_id, extra=''))
         # PRs currently always have an initial 'Under Review' status change
         # that counts as a general comment, hence '2' in the test below. That
         # could be counted as a misfeature, to be reworked later.
@@ -201,18 +201,18 @@
         self.log_user()
         pr_id = self._create_pr()
 
-        text = u'inline comment on changeset'
+        text = 'inline comment on changeset'
         f_path = 'vcs/web/simplevcs/views/repository.py'
         line = 'n1'
         params = {'text': text, 'f_path': f_path, 'line': line, '_session_csrf_secret_token': self.session_csrf_secret_token()}
-        response = self.app.post(url(controller='pullrequests', action='comment',
-                                     repo_name=HG_REPO, pull_request_id=pr_id),
+        response = self.app.post(base.url(controller='pullrequests', action='comment',
+                                     repo_name=base.HG_REPO, pull_request_id=pr_id),
                                      params=params, extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
         # Test response...
         assert response.status == '200 OK'
 
-        response = self.app.get(url(controller='pullrequests', action='show',
-                                repo_name=HG_REPO, pull_request_id=pr_id, extra=''))
+        response = self.app.get(base.url(controller='pullrequests', action='show',
+                                repo_name=base.HG_REPO, pull_request_id=pr_id, extra=''))
         response.mustcontain(
             '''<div class="comments-number">'''
             ''' 2 comments (1 inline, 1 general)'''
@@ -231,21 +231,21 @@
         self.log_user()
         pr_id = self._create_pr()
 
-        text = u'@%s check CommentOnRevision' % TEST_USER_REGULAR_LOGIN
+        text = '@%s check CommentOnRevision' % base.TEST_USER_REGULAR_LOGIN
         params = {'text': text, '_session_csrf_secret_token': self.session_csrf_secret_token()}
-        response = self.app.post(url(controller='pullrequests', action='comment',
-                                     repo_name=HG_REPO, pull_request_id=pr_id),
+        response = self.app.post(base.url(controller='pullrequests', action='comment',
+                                     repo_name=base.HG_REPO, pull_request_id=pr_id),
                                      params=params, extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
         # Test response...
         assert response.status == '200 OK'
 
-        response = self.app.get(url(controller='pullrequests', action='show',
-                                repo_name=HG_REPO, pull_request_id=pr_id, extra=''))
+        response = self.app.get(base.url(controller='pullrequests', action='show',
+                                repo_name=base.HG_REPO, pull_request_id=pr_id, extra=''))
         response.mustcontain(
             '''<div class="comments-number">'''
             ''' 2 comments (0 inline, 2 general)'''
         )
-        response.mustcontain('<b>@%s</b> check CommentOnRevision' % TEST_USER_REGULAR_LOGIN)
+        response.mustcontain('<b>@%s</b> check CommentOnRevision' % base.TEST_USER_REGULAR_LOGIN)
 
         # test DB
         assert ChangesetComment.query().count() == 2
@@ -254,17 +254,17 @@
         self.log_user()
         pr_id = self._create_pr()
 
-        text = u'general comment on pullrequest'
+        text = 'general comment on pullrequest'
         params = {'text': text, 'changeset_status': 'rejected',
                 '_session_csrf_secret_token': self.session_csrf_secret_token()}
-        response = self.app.post(url(controller='pullrequests', action='comment',
-                                     repo_name=HG_REPO, pull_request_id=pr_id),
+        response = self.app.post(base.url(controller='pullrequests', action='comment',
+                                     repo_name=base.HG_REPO, pull_request_id=pr_id),
                                      params=params, extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
         # Test response...
         assert response.status == '200 OK'
 
-        response = self.app.get(url(controller='pullrequests', action='show',
-                                repo_name=HG_REPO, pull_request_id=pr_id, extra=''))
+        response = self.app.get(base.url(controller='pullrequests', action='show',
+                                repo_name=base.HG_REPO, pull_request_id=pr_id, extra=''))
         # PRs currently always have an initial 'Under Review' status change
         # that counts as a general comment, hence '2' in the test below. That
         # could be counted as a misfeature, to be reworked later.
@@ -278,33 +278,33 @@
         assert ChangesetComment.query().count() == 2
 
         # check status
-        status = ChangesetStatusModel().get_status(repo=HG_REPO, pull_request=pr_id)
+        status = ChangesetStatusModel().get_status(repo=base.HG_REPO, pull_request=pr_id)
         assert status == 'rejected'
 
     def test_delete(self):
         self.log_user()
         pr_id = self._create_pr()
 
-        text = u'general comment on changeset to be deleted'
+        text = 'general comment on changeset to be deleted'
         params = {'text': text, '_session_csrf_secret_token': self.session_csrf_secret_token()}
-        response = self.app.post(url(controller='pullrequests', action='comment',
-                                     repo_name=HG_REPO, pull_request_id=pr_id),
+        response = self.app.post(base.url(controller='pullrequests', action='comment',
+                                     repo_name=base.HG_REPO, pull_request_id=pr_id),
                                      params=params, extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
 
         comments = ChangesetComment.query().all()
         assert len(comments) == 2
         comment_id = comments[-1].comment_id
 
-        self.app.post(url("pullrequest_comment_delete",
-                                    repo_name=HG_REPO,
+        self.app.post(base.url("pullrequest_comment_delete",
+                                    repo_name=base.HG_REPO,
                                     comment_id=comment_id),
             params={'_session_csrf_secret_token': self.session_csrf_secret_token()})
 
         comments = ChangesetComment.query().all()
         assert len(comments) == 1
 
-        response = self.app.get(url(controller='pullrequests', action='show',
-                                repo_name=HG_REPO, pull_request_id=pr_id, extra=''))
+        response = self.app.get(base.url(controller='pullrequests', action='show',
+                                repo_name=base.HG_REPO, pull_request_id=pr_id, extra=''))
         response.mustcontain(
             '''<div class="comments-number">'''
             ''' 1 comment (0 inline, 1 general)'''
@@ -315,17 +315,17 @@
         self.log_user()
         pr_id = self._create_pr()
 
-        text = u'general comment on pullrequest'
+        text = 'general comment on pullrequest'
         params = {'text': text, 'save_close': 'close',
                 '_session_csrf_secret_token': self.session_csrf_secret_token()}
-        response = self.app.post(url(controller='pullrequests', action='comment',
-                                     repo_name=HG_REPO, pull_request_id=pr_id),
+        response = self.app.post(base.url(controller='pullrequests', action='comment',
+                                     repo_name=base.HG_REPO, pull_request_id=pr_id),
                                      params=params, extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
         # Test response...
         assert response.status == '200 OK'
 
-        response = self.app.get(url(controller='pullrequests', action='show',
-                                repo_name=HG_REPO, pull_request_id=pr_id, extra=''))
+        response = self.app.get(base.url(controller='pullrequests', action='show',
+                                repo_name=base.HG_REPO, pull_request_id=pr_id, extra=''))
         response.mustcontain(
             '''title (Closed)'''
         )
@@ -338,17 +338,17 @@
         self.log_user()
         pr_id = self._create_pr()
 
-        text = u'general comment on pullrequest'
+        text = 'general comment on pullrequest'
         params = {'text': text, 'save_delete': 'delete',
                 '_session_csrf_secret_token': self.session_csrf_secret_token()}
-        response = self.app.post(url(controller='pullrequests', action='comment',
-                                     repo_name=HG_REPO, pull_request_id=pr_id),
+        response = self.app.post(base.url(controller='pullrequests', action='comment',
+                                     repo_name=base.HG_REPO, pull_request_id=pr_id),
                                      params=params, extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
         # Test response...
         assert response.status == '200 OK'
 
-        response = self.app.get(url(controller='pullrequests', action='show',
-                                repo_name=HG_REPO, pull_request_id=pr_id, extra=''), status=404)
+        response = self.app.get(base.url(controller='pullrequests', action='show',
+                                repo_name=base.HG_REPO, pull_request_id=pr_id, extra=''), status=404)
 
         # test DB
         assert PullRequest.get(pr_id) is None
@@ -358,19 +358,19 @@
         pr_id = self._create_pr()
 
         # first close
-        text = u'general comment on pullrequest'
+        text = 'general comment on pullrequest'
         params = {'text': text, 'save_close': 'close',
                 '_session_csrf_secret_token': self.session_csrf_secret_token()}
-        response = self.app.post(url(controller='pullrequests', action='comment',
-                                     repo_name=HG_REPO, pull_request_id=pr_id),
+        response = self.app.post(base.url(controller='pullrequests', action='comment',
+                                     repo_name=base.HG_REPO, pull_request_id=pr_id),
                                      params=params, extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
         assert response.status == '200 OK'
 
         # attempt delete, should fail
         params = {'text': text, 'save_delete': 'delete',
                 '_session_csrf_secret_token': self.session_csrf_secret_token()}
-        response = self.app.post(url(controller='pullrequests', action='comment',
-                                     repo_name=HG_REPO, pull_request_id=pr_id),
+        response = self.app.post(base.url(controller='pullrequests', action='comment',
+                                     repo_name=base.HG_REPO, pull_request_id=pr_id),
                                      params=params, extra_environ={'HTTP_X_PARTIAL_XHR': '1'}, status=403)
 
         # verify that PR still exists, in closed state
--- a/kallithea/tests/functional/test_compare.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_compare.py	Mon May 04 19:24:04 2020 +0200
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 from kallithea.model.meta import Session
 from kallithea.model.repo import RepoModel
-from kallithea.tests.base import *
+from kallithea.tests import base
 from kallithea.tests.fixture import Fixture
 
 
@@ -12,7 +12,7 @@
     return '''<div class="message-firstline"><a class="message-link" href="/%s/changeset/%s">%s</a></div>''' % (repo_name, sha, msg)
 
 
-class TestCompareController(TestController):
+class TestCompareController(base.TestController):
 
     def setup_method(self, method):
         self.r1_id = None
@@ -28,9 +28,9 @@
 
     def test_compare_forks_on_branch_extra_commits_hg(self):
         self.log_user()
-        repo1 = fixture.create_repo(u'one', repo_type='hg',
+        repo1 = fixture.create_repo('one', repo_type='hg',
                                     repo_description='diff-test',
-                                    cur_user=TEST_USER_ADMIN_LOGIN)
+                                    cur_user=base.TEST_USER_ADMIN_LOGIN)
         self.r1_id = repo1.repo_id
         # commit something !
         cs0 = fixture.commit_change(repo1.repo_name, filename='file1',
@@ -38,7 +38,7 @@
                 parent=None, newfile=True)
 
         # fork this repo
-        repo2 = fixture.create_fork(u'one', u'one-fork')
+        repo2 = fixture.create_fork('one', 'one-fork')
         self.r2_id = repo2.repo_id
 
         # add two extra commit into fork
@@ -53,7 +53,7 @@
         rev1 = 'default'
         rev2 = 'default'
 
-        response = self.app.get(url('compare_url',
+        response = self.app.get(base.url('compare_url',
                                     repo_name=repo1.repo_name,
                                     org_ref_type="branch",
                                     org_ref_name=rev2,
@@ -79,9 +79,9 @@
 
     def test_compare_forks_on_branch_extra_commits_git(self):
         self.log_user()
-        repo1 = fixture.create_repo(u'one-git', repo_type='git',
+        repo1 = fixture.create_repo('one-git', repo_type='git',
                                     repo_description='diff-test',
-                                    cur_user=TEST_USER_ADMIN_LOGIN)
+                                    cur_user=base.TEST_USER_ADMIN_LOGIN)
         self.r1_id = repo1.repo_id
         # commit something !
         cs0 = fixture.commit_change(repo1.repo_name, filename='file1',
@@ -89,7 +89,7 @@
                 parent=None, newfile=True)
 
         # fork this repo
-        repo2 = fixture.create_fork(u'one-git', u'one-git-fork')
+        repo2 = fixture.create_fork('one-git', 'one-git-fork')
         self.r2_id = repo2.repo_id
 
         # add two extra commit into fork
@@ -104,7 +104,7 @@
         rev1 = 'master'
         rev2 = 'master'
 
-        response = self.app.get(url('compare_url',
+        response = self.app.get(base.url('compare_url',
                                     repo_name=repo1.repo_name,
                                     org_ref_type="branch",
                                     org_ref_name=rev2,
@@ -131,9 +131,9 @@
     def test_compare_forks_on_branch_extra_commits_origin_has_incoming_hg(self):
         self.log_user()
 
-        repo1 = fixture.create_repo(u'one', repo_type='hg',
+        repo1 = fixture.create_repo('one', repo_type='hg',
                                     repo_description='diff-test',
-                                    cur_user=TEST_USER_ADMIN_LOGIN)
+                                    cur_user=base.TEST_USER_ADMIN_LOGIN)
 
         self.r1_id = repo1.repo_id
 
@@ -143,7 +143,7 @@
                 parent=None, newfile=True)
 
         # fork this repo
-        repo2 = fixture.create_fork(u'one', u'one-fork')
+        repo2 = fixture.create_fork('one', 'one-fork')
         self.r2_id = repo2.repo_id
 
         # now commit something to origin repo
@@ -163,7 +163,7 @@
         rev1 = 'default'
         rev2 = 'default'
 
-        response = self.app.get(url('compare_url',
+        response = self.app.get(base.url('compare_url',
                                     repo_name=repo1.repo_name,
                                     org_ref_type="branch",
                                     org_ref_name=rev2,
@@ -190,9 +190,9 @@
     def test_compare_forks_on_branch_extra_commits_origin_has_incoming_git(self):
         self.log_user()
 
-        repo1 = fixture.create_repo(u'one-git', repo_type='git',
+        repo1 = fixture.create_repo('one-git', repo_type='git',
                                     repo_description='diff-test',
-                                    cur_user=TEST_USER_ADMIN_LOGIN)
+                                    cur_user=base.TEST_USER_ADMIN_LOGIN)
 
         self.r1_id = repo1.repo_id
 
@@ -202,7 +202,7 @@
                 parent=None, newfile=True)
 
         # fork this repo
-        repo2 = fixture.create_fork(u'one-git', u'one-git-fork')
+        repo2 = fixture.create_fork('one-git', 'one-git-fork')
         self.r2_id = repo2.repo_id
 
         # now commit something to origin repo
@@ -222,7 +222,7 @@
         rev1 = 'master'
         rev2 = 'master'
 
-        response = self.app.get(url('compare_url',
+        response = self.app.get(base.url('compare_url',
                                     repo_name=repo1.repo_name,
                                     org_ref_type="branch",
                                     org_ref_name=rev2,
@@ -261,9 +261,9 @@
         # make repo1, and cs1+cs2
         self.log_user()
 
-        repo1 = fixture.create_repo(u'repo1', repo_type='hg',
+        repo1 = fixture.create_repo('repo1', repo_type='hg',
                                     repo_description='diff-test',
-                                    cur_user=TEST_USER_ADMIN_LOGIN)
+                                    cur_user=base.TEST_USER_ADMIN_LOGIN)
         self.r1_id = repo1.repo_id
 
         # commit something !
@@ -274,7 +274,7 @@
                 content='line1\nline2\n', message='commit2', vcs_type='hg',
                 parent=cs0)
         # fork this repo
-        repo2 = fixture.create_fork(u'repo1', u'repo1-fork')
+        repo2 = fixture.create_fork('repo1', 'repo1-fork')
         self.r2_id = repo2.repo_id
         # now make cs3-6
         cs2 = fixture.commit_change(repo1.repo_name, filename='file1',
@@ -290,7 +290,7 @@
                 content='line1\nline2\nline3\nline4\nline5\nline6\n',
                 message='commit6', vcs_type='hg', parent=cs4)
 
-        response = self.app.get(url('compare_url',
+        response = self.app.get(base.url('compare_url',
                                     repo_name=repo2.repo_name,
                                     org_ref_type="rev",
                                     org_ref_name=cs1.short_id,  # parent of cs2, in repo2
@@ -329,9 +329,9 @@
 #
         # make repo1, and cs1+cs2
         self.log_user()
-        repo1 = fixture.create_repo(u'repo1', repo_type='hg',
+        repo1 = fixture.create_repo('repo1', repo_type='hg',
                                     repo_description='diff-test',
-                                    cur_user=TEST_USER_ADMIN_LOGIN)
+                                    cur_user=base.TEST_USER_ADMIN_LOGIN)
         self.r1_id = repo1.repo_id
 
         # commit something !
@@ -342,7 +342,7 @@
                 content='line1\nline2\n', message='commit2', vcs_type='hg',
                 parent=cs0)
         # fork this repo
-        repo2 = fixture.create_fork(u'repo1', u'repo1-fork')
+        repo2 = fixture.create_fork('repo1', 'repo1-fork')
         self.r2_id = repo2.repo_id
         # now make cs3-6
         cs2 = fixture.commit_change(repo1.repo_name, filename='file1',
@@ -358,7 +358,7 @@
                 content='line1\nline2\nline3\nline4\nline5\nline6\n',
                 message='commit6', vcs_type='hg', parent=cs4)
 
-        response = self.app.get(url('compare_url',
+        response = self.app.get(base.url('compare_url',
                                     repo_name=repo1.repo_name,
                                     org_ref_type="rev",
                                     org_ref_name=cs2.short_id, # parent of cs3, not in repo2
@@ -388,27 +388,27 @@
     def test_compare_remote_branches_hg(self):
         self.log_user()
 
-        repo2 = fixture.create_fork(HG_REPO, HG_FORK)
+        repo2 = fixture.create_fork(base.HG_REPO, base.HG_FORK)
         self.r2_id = repo2.repo_id
         rev1 = '56349e29c2af'
         rev2 = '7d4bc8ec6be5'
 
-        response = self.app.get(url('compare_url',
-                                    repo_name=HG_REPO,
+        response = self.app.get(base.url('compare_url',
+                                    repo_name=base.HG_REPO,
                                     org_ref_type="rev",
                                     org_ref_name=rev1,
                                     other_ref_type="rev",
                                     other_ref_name=rev2,
-                                    other_repo=HG_FORK,
+                                    other_repo=base.HG_FORK,
                                     merge='1',))
 
-        response.mustcontain('%s@%s' % (HG_REPO, rev1))
-        response.mustcontain('%s@%s' % (HG_FORK, rev2))
+        response.mustcontain('%s@%s' % (base.HG_REPO, rev1))
+        response.mustcontain('%s@%s' % (base.HG_FORK, rev2))
         ## outgoing changesets between those revisions
 
-        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/2dda4e345facb0ccff1a191052dd1606dba6781d">r4:2dda4e345fac</a>""" % (HG_FORK))
-        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/6fff84722075f1607a30f436523403845f84cd9e">r5:6fff84722075</a>""" % (HG_FORK))
-        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/7d4bc8ec6be56c0f10425afb40b6fc315a4c25e7">r6:%s</a>""" % (HG_FORK, rev2))
+        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/2dda4e345facb0ccff1a191052dd1606dba6781d">r4:2dda4e345fac</a>""" % (base.HG_FORK))
+        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/6fff84722075f1607a30f436523403845f84cd9e">r5:6fff84722075</a>""" % (base.HG_FORK))
+        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/7d4bc8ec6be56c0f10425afb40b6fc315a4c25e7">r6:%s</a>""" % (base.HG_FORK, rev2))
 
         ## files
         response.mustcontain("""<a href="#C--9c390eb52cd6">vcs/backends/hg.py</a>""")
@@ -418,27 +418,27 @@
     def test_compare_remote_branches_git(self):
         self.log_user()
 
-        repo2 = fixture.create_fork(GIT_REPO, GIT_FORK)
+        repo2 = fixture.create_fork(base.GIT_REPO, base.GIT_FORK)
         self.r2_id = repo2.repo_id
         rev1 = '102607b09cdd60e2793929c4f90478be29f85a17'
         rev2 = 'd7e0d30fbcae12c90680eb095a4f5f02505ce501'
 
-        response = self.app.get(url('compare_url',
-                                    repo_name=GIT_REPO,
+        response = self.app.get(base.url('compare_url',
+                                    repo_name=base.GIT_REPO,
                                     org_ref_type="rev",
                                     org_ref_name=rev1,
                                     other_ref_type="rev",
                                     other_ref_name=rev2,
-                                    other_repo=GIT_FORK,
+                                    other_repo=base.GIT_FORK,
                                     merge='1',))
 
-        response.mustcontain('%s@%s' % (GIT_REPO, rev1))
-        response.mustcontain('%s@%s' % (GIT_FORK, rev2))
+        response.mustcontain('%s@%s' % (base.GIT_REPO, rev1))
+        response.mustcontain('%s@%s' % (base.GIT_FORK, rev2))
         ## outgoing changesets between those revisions
 
-        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/49d3fd156b6f7db46313fac355dca1a0b94a0017">r4:49d3fd156b6f</a>""" % (GIT_FORK))
-        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/2d1028c054665b962fa3d307adfc923ddd528038">r5:2d1028c05466</a>""" % (GIT_FORK))
-        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/d7e0d30fbcae12c90680eb095a4f5f02505ce501">r6:%s</a>""" % (GIT_FORK, rev2[:12]))
+        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/49d3fd156b6f7db46313fac355dca1a0b94a0017">r4:49d3fd156b6f</a>""" % (base.GIT_FORK))
+        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/2d1028c054665b962fa3d307adfc923ddd528038">r5:2d1028c05466</a>""" % (base.GIT_FORK))
+        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/d7e0d30fbcae12c90680eb095a4f5f02505ce501">r6:%s</a>""" % (base.GIT_FORK, rev2[:12]))
 
         ## files
         response.mustcontain("""<a href="#C--9c390eb52cd6">vcs/backends/hg.py</a>""")
@@ -448,9 +448,9 @@
     def test_org_repo_new_commits_after_forking_simple_diff_hg(self):
         self.log_user()
 
-        repo1 = fixture.create_repo(u'one', repo_type='hg',
+        repo1 = fixture.create_repo('one', repo_type='hg',
                                     repo_description='diff-test',
-                                    cur_user=TEST_USER_ADMIN_LOGIN)
+                                    cur_user=base.TEST_USER_ADMIN_LOGIN)
 
         self.r1_id = repo1.repo_id
         r1_name = repo1.repo_name
@@ -460,8 +460,8 @@
         Session().commit()
         assert repo1.scm_instance.revisions == [cs0.raw_id]
         # fork the repo1
-        repo2 = fixture.create_fork(r1_name, u'one-fork',
-                                    cur_user=TEST_USER_ADMIN_LOGIN)
+        repo2 = fixture.create_fork(r1_name, 'one-fork',
+                                    cur_user=base.TEST_USER_ADMIN_LOGIN)
         Session().commit()
         assert repo2.scm_instance.revisions == [cs0.raw_id]
         self.r2_id = repo2.repo_id
@@ -482,7 +482,7 @@
         rev1 = 'default'
         rev2 = 'default'
 
-        response = self.app.get(url('compare_url',
+        response = self.app.get(base.url('compare_url',
                                     repo_name=r2_name,
                                     org_ref_type="branch",
                                     org_ref_name=rev2,
@@ -500,7 +500,7 @@
         # compare !
         rev1 = 'default'
         rev2 = 'default'
-        response = self.app.get(url('compare_url',
+        response = self.app.get(base.url('compare_url',
                                     repo_name=r2_name,
                                     org_ref_type="branch",
                                     org_ref_name=rev2,
@@ -520,9 +520,9 @@
     def test_org_repo_new_commits_after_forking_simple_diff_git(self):
         self.log_user()
 
-        repo1 = fixture.create_repo(u'one-git', repo_type='git',
+        repo1 = fixture.create_repo('one-git', repo_type='git',
                                     repo_description='diff-test',
-                                    cur_user=TEST_USER_ADMIN_LOGIN)
+                                    cur_user=base.TEST_USER_ADMIN_LOGIN)
 
         self.r1_id = repo1.repo_id
         r1_name = repo1.repo_name
@@ -533,8 +533,8 @@
         Session().commit()
         assert repo1.scm_instance.revisions == [cs0.raw_id]
         # fork the repo1
-        repo2 = fixture.create_fork(r1_name, u'one-git-fork',
-                                    cur_user=TEST_USER_ADMIN_LOGIN)
+        repo2 = fixture.create_fork(r1_name, 'one-git-fork',
+                                    cur_user=base.TEST_USER_ADMIN_LOGIN)
         Session().commit()
         assert repo2.scm_instance.revisions == [cs0.raw_id]
         self.r2_id = repo2.repo_id
@@ -556,7 +556,7 @@
         rev1 = 'master'
         rev2 = 'master'
 
-        response = self.app.get(url('compare_url',
+        response = self.app.get(base.url('compare_url',
                                     repo_name=r2_name,
                                     org_ref_type="branch",
                                     org_ref_name=rev1,
@@ -574,7 +574,7 @@
         # compare !
         rev1 = 'master'
         rev2 = 'master'
-        response = self.app.get(url('compare_url',
+        response = self.app.get(base.url('compare_url',
                                     repo_name=r2_name,
                                     org_ref_type="branch",
                                     org_ref_name=rev1,
--- a/kallithea/tests/functional/test_compare_local.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_compare_local.py	Mon May 04 19:24:04 2020 +0200
@@ -1,31 +1,31 @@
 # -*- coding: utf-8 -*-
-from kallithea.tests.base import *
+from kallithea.tests import base
 
 
-class TestCompareController(TestController):
+class TestCompareController(base.TestController):
 
     def test_compare_tag_hg(self):
         self.log_user()
         tag1 = 'v0.1.2'
         tag2 = 'v0.1.3'
-        response = self.app.get(url('compare_url',
-                                    repo_name=HG_REPO,
+        response = self.app.get(base.url('compare_url',
+                                    repo_name=base.HG_REPO,
                                     org_ref_type="tag",
                                     org_ref_name=tag1,
                                     other_ref_type="tag",
                                     other_ref_name=tag2,
                                     ), status=200)
-        response.mustcontain('%s@%s' % (HG_REPO, tag1))
-        response.mustcontain('%s@%s' % (HG_REPO, tag2))
+        response.mustcontain('%s@%s' % (base.HG_REPO, tag1))
+        response.mustcontain('%s@%s' % (base.HG_REPO, tag2))
 
         ## outgoing changesets between tags
-        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/c5ddebc06eaaba3010c2d66ea6ec9d074eb0f678">r112:c5ddebc06eaa</a>''' % HG_REPO)
-        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/70d4cef8a37657ee4cf5aabb3bd9f68879769816">r115:70d4cef8a376</a>''' % HG_REPO)
-        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/9749bfbfc0d2eba208d7947de266303b67c87cda">r116:9749bfbfc0d2</a>''' % HG_REPO)
-        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/41fda979f02fda216374bf8edac4e83f69e7581c">r117:41fda979f02f</a>''' % HG_REPO)
-        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/bb1a3ab98cc45cb934a77dcabf87a5a598b59e97">r118:bb1a3ab98cc4</a>''' % HG_REPO)
-        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/36e0fc9d2808c5022a24f49d6658330383ed8666">r119:36e0fc9d2808</a>''' % HG_REPO)
-        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">r120:17544fbfcd33</a>''' % HG_REPO)
+        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/c5ddebc06eaaba3010c2d66ea6ec9d074eb0f678">r112:c5ddebc06eaa</a>''' % base.HG_REPO)
+        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/70d4cef8a37657ee4cf5aabb3bd9f68879769816">r115:70d4cef8a376</a>''' % base.HG_REPO)
+        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/9749bfbfc0d2eba208d7947de266303b67c87cda">r116:9749bfbfc0d2</a>''' % base.HG_REPO)
+        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/41fda979f02fda216374bf8edac4e83f69e7581c">r117:41fda979f02f</a>''' % base.HG_REPO)
+        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/bb1a3ab98cc45cb934a77dcabf87a5a598b59e97">r118:bb1a3ab98cc4</a>''' % base.HG_REPO)
+        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/36e0fc9d2808c5022a24f49d6658330383ed8666">r119:36e0fc9d2808</a>''' % base.HG_REPO)
+        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">r120:17544fbfcd33</a>''' % base.HG_REPO)
 
         response.mustcontain('11 files changed with 94 insertions and 64 deletions')
 
@@ -80,24 +80,24 @@
         self.log_user()
         tag1 = 'v0.1.2'
         tag2 = 'v0.1.3'
-        response = self.app.get(url('compare_url',
-                                    repo_name=GIT_REPO,
+        response = self.app.get(base.url('compare_url',
+                                    repo_name=base.GIT_REPO,
                                     org_ref_type="tag",
                                     org_ref_name=tag1,
                                     other_ref_type="tag",
                                     other_ref_name=tag2,
                                     ), status=200)
-        response.mustcontain('%s@%s' % (GIT_REPO, tag1))
-        response.mustcontain('%s@%s' % (GIT_REPO, tag2))
+        response.mustcontain('%s@%s' % (base.GIT_REPO, tag1))
+        response.mustcontain('%s@%s' % (base.GIT_REPO, tag2))
 
         ## outgoing changesets between tags
-        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/794bbdd31545c199f74912709ea350dedcd189a2">r113:794bbdd31545</a>''' % GIT_REPO)
-        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/e36d8c5025329bdd4212bd53d4ed8a70ff44985f">r115:e36d8c502532</a>''' % GIT_REPO)
-        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/5c9ff4f6d7508db0e72b1d2991c357d0d8e07af2">r116:5c9ff4f6d750</a>''' % GIT_REPO)
-        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/b7187fa2b8c1d773ec35e9dee12f01f74808c879">r117:b7187fa2b8c1</a>''' % GIT_REPO)
-        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/5f3b74262014a8de2dc7dade1152de9fd0c8efef">r118:5f3b74262014</a>''' % GIT_REPO)
-        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/17438a11f72b93f56d0e08e7d1fa79a378578a82">r119:17438a11f72b</a>''' % GIT_REPO)
-        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/5a3a8fb005554692b16e21dee62bf02667d8dc3e">r120:5a3a8fb00555</a>''' % GIT_REPO)
+        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/794bbdd31545c199f74912709ea350dedcd189a2">r113:794bbdd31545</a>''' % base.GIT_REPO)
+        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/e36d8c5025329bdd4212bd53d4ed8a70ff44985f">r115:e36d8c502532</a>''' % base.GIT_REPO)
+        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/5c9ff4f6d7508db0e72b1d2991c357d0d8e07af2">r116:5c9ff4f6d750</a>''' % base.GIT_REPO)
+        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/b7187fa2b8c1d773ec35e9dee12f01f74808c879">r117:b7187fa2b8c1</a>''' % base.GIT_REPO)
+        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/5f3b74262014a8de2dc7dade1152de9fd0c8efef">r118:5f3b74262014</a>''' % base.GIT_REPO)
+        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/17438a11f72b93f56d0e08e7d1fa79a378578a82">r119:17438a11f72b</a>''' % base.GIT_REPO)
+        response.mustcontain('''<a class="changeset_hash" href="/%s/changeset/5a3a8fb005554692b16e21dee62bf02667d8dc3e">r120:5a3a8fb00555</a>''' % base.GIT_REPO)
 
         response.mustcontain('11 files changed with 94 insertions and 64 deletions')
 
@@ -116,32 +116,32 @@
 
     def test_index_branch_hg(self):
         self.log_user()
-        response = self.app.get(url('compare_url',
-                                    repo_name=HG_REPO,
+        response = self.app.get(base.url('compare_url',
+                                    repo_name=base.HG_REPO,
                                     org_ref_type="branch",
                                     org_ref_name='default',
                                     other_ref_type="branch",
                                     other_ref_name='default',
                                     ))
 
-        response.mustcontain('%s@default' % (HG_REPO))
-        response.mustcontain('%s@default' % (HG_REPO))
+        response.mustcontain('%s@default' % (base.HG_REPO))
+        response.mustcontain('%s@default' % (base.HG_REPO))
         # branch are equal
         response.mustcontain('<span class="text-muted">No files</span>')
         response.mustcontain('<span class="text-muted">No changesets</span>')
 
     def test_index_branch_git(self):
         self.log_user()
-        response = self.app.get(url('compare_url',
-                                    repo_name=GIT_REPO,
+        response = self.app.get(base.url('compare_url',
+                                    repo_name=base.GIT_REPO,
                                     org_ref_type="branch",
                                     org_ref_name='master',
                                     other_ref_type="branch",
                                     other_ref_name='master',
                                     ))
 
-        response.mustcontain('%s@master' % (GIT_REPO))
-        response.mustcontain('%s@master' % (GIT_REPO))
+        response.mustcontain('%s@master' % (base.GIT_REPO))
+        response.mustcontain('%s@master' % (base.GIT_REPO))
         # branch are equal
         response.mustcontain('<span class="text-muted">No files</span>')
         response.mustcontain('<span class="text-muted">No changesets</span>')
@@ -151,18 +151,18 @@
         rev1 = 'b986218ba1c9'
         rev2 = '3d8f361e72ab'
 
-        response = self.app.get(url('compare_url',
-                                    repo_name=HG_REPO,
+        response = self.app.get(base.url('compare_url',
+                                    repo_name=base.HG_REPO,
                                     org_ref_type="rev",
                                     org_ref_name=rev1,
                                     other_ref_type="rev",
                                     other_ref_name=rev2,
                                     ))
-        response.mustcontain('%s@%s' % (HG_REPO, rev1))
-        response.mustcontain('%s@%s' % (HG_REPO, rev2))
+        response.mustcontain('%s@%s' % (base.HG_REPO, rev1))
+        response.mustcontain('%s@%s' % (base.HG_REPO, rev2))
 
         ## outgoing changesets between those revisions
-        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/3d8f361e72ab303da48d799ff1ac40d5ac37c67e">r1:%s</a>""" % (HG_REPO, rev2))
+        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/3d8f361e72ab303da48d799ff1ac40d5ac37c67e">r1:%s</a>""" % (base.HG_REPO, rev2))
 
         response.mustcontain('1 file changed with 7 insertions and 0 deletions')
         ## files
@@ -173,18 +173,18 @@
         rev1 = 'c1214f7e79e02fc37156ff215cd71275450cffc3'
         rev2 = '38b5fe81f109cb111f549bfe9bb6b267e10bc557'
 
-        response = self.app.get(url('compare_url',
-                                    repo_name=GIT_REPO,
+        response = self.app.get(base.url('compare_url',
+                                    repo_name=base.GIT_REPO,
                                     org_ref_type="rev",
                                     org_ref_name=rev1,
                                     other_ref_type="rev",
                                     other_ref_name=rev2,
                                     ))
-        response.mustcontain('%s@%s' % (GIT_REPO, rev1))
-        response.mustcontain('%s@%s' % (GIT_REPO, rev2))
+        response.mustcontain('%s@%s' % (base.GIT_REPO, rev1))
+        response.mustcontain('%s@%s' % (base.GIT_REPO, rev2))
 
         ## outgoing changesets between those revisions
-        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/38b5fe81f109cb111f549bfe9bb6b267e10bc557">r1:%s</a>""" % (GIT_REPO, rev2[:12]))
+        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/38b5fe81f109cb111f549bfe9bb6b267e10bc557">r1:%s</a>""" % (base.GIT_REPO, rev2[:12]))
         response.mustcontain('1 file changed with 7 insertions and 0 deletions')
 
         ## files
@@ -195,8 +195,8 @@
         rev1 = 'b986218ba1c9'
         rev2 = '3d8f361e72ab'
 
-        response = self.app.get(url('compare_url',
-                                    repo_name=HG_REPO,
+        response = self.app.get(base.url('compare_url',
+                                    repo_name=base.HG_REPO,
                                     org_ref_type="rev",
                                     org_ref_name=rev1,
                                     other_ref_type="rev",
@@ -206,18 +206,18 @@
                                 extra_environ={'HTTP_X_PARTIAL_XHR': '1'},)
 
         ## outgoing changesets between those revisions
-        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/3d8f361e72ab303da48d799ff1ac40d5ac37c67e">r1:%s</a>""" % (HG_REPO, rev2))
+        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/3d8f361e72ab303da48d799ff1ac40d5ac37c67e">r1:%s</a>""" % (base.HG_REPO, rev2))
 
         response.mustcontain('Merge Ancestor')
-        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/b986218ba1c9b0d6a259fac9b050b1724ed8e545">%s</a>""" % (HG_REPO, rev1))
+        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/b986218ba1c9b0d6a259fac9b050b1724ed8e545">%s</a>""" % (base.HG_REPO, rev1))
 
     def test_compare_revisions_git_is_ajax_preview(self):
         self.log_user()
         rev1 = 'c1214f7e79e02fc37156ff215cd71275450cffc3'
         rev2 = '38b5fe81f109cb111f549bfe9bb6b267e10bc557'
 
-        response = self.app.get(url('compare_url',
-                                    repo_name=GIT_REPO,
+        response = self.app.get(base.url('compare_url',
+                                    repo_name=base.GIT_REPO,
                                     org_ref_type="rev",
                                     org_ref_name=rev1,
                                     other_ref_type="rev",
@@ -226,7 +226,7 @@
                                     ),
                                 extra_environ={'HTTP_X_PARTIAL_XHR': '1'},)
         ## outgoing changesets between those revisions
-        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/38b5fe81f109cb111f549bfe9bb6b267e10bc557">r1:%s</a>""" % (GIT_REPO, rev2[:12]))
+        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/38b5fe81f109cb111f549bfe9bb6b267e10bc557">r1:%s</a>""" % (base.GIT_REPO, rev2[:12]))
 
         response.mustcontain('Merge Ancestor')
-        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/c1214f7e79e02fc37156ff215cd71275450cffc3">%s</a>""" % (GIT_REPO, rev1[:12]))
+        response.mustcontain("""<a class="changeset_hash" href="/%s/changeset/c1214f7e79e02fc37156ff215cd71275450cffc3">%s</a>""" % (base.GIT_REPO, rev1[:12]))
--- a/kallithea/tests/functional/test_feed.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_feed.py	Mon May 04 19:24:04 2020 +0200
@@ -1,20 +1,20 @@
-from kallithea.tests.base import *
+from kallithea.tests import base
 
 
-class TestFeedController(TestController):
+class TestFeedController(base.TestController):
 
     def test_rss(self):
         self.log_user()
-        response = self.app.get(url(controller='feed', action='rss',
-                                    repo_name=HG_REPO))
+        response = self.app.get(base.url(controller='feed', action='rss',
+                                    repo_name=base.HG_REPO))
 
         assert response.content_type == "application/rss+xml"
         assert """<rss version="2.0">""" in response
 
     def test_atom(self):
         self.log_user()
-        response = self.app.get(url(controller='feed', action='atom',
-                                    repo_name=HG_REPO))
+        response = self.app.get(base.url(controller='feed', action='atom',
+                                    repo_name=base.HG_REPO))
 
         assert response.content_type == """application/atom+xml"""
         assert """<?xml version="1.0" encoding="utf-8"?>""" in response
--- a/kallithea/tests/functional/test_files.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_files.py	Mon May 04 19:24:04 2020 +0200
@@ -1,10 +1,11 @@
 # -*- coding: utf-8 -*-
+import json
 import mimetypes
 import posixpath
 
 from kallithea.model.db import Repository
 from kallithea.model.meta import Session
-from kallithea.tests.base import *
+from kallithea.tests import base
 from kallithea.tests.fixture import Fixture
 
 
@@ -26,51 +27,51 @@
     Session().commit()
 
 
-class TestFilesController(TestController):
+class TestFilesController(base.TestController):
 
     def test_index(self):
         self.log_user()
-        response = self.app.get(url(controller='files', action='index',
-                                    repo_name=HG_REPO,
+        response = self.app.get(base.url(controller='files', action='index',
+                                    repo_name=base.HG_REPO,
                                     revision='tip',
                                     f_path='/'))
         # Test response...
-        response.mustcontain('<a class="browser-dir ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/docs"><i class="icon-folder-open"></i><span>docs</span></a>' % HG_REPO)
-        response.mustcontain('<a class="browser-dir ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/vcs"><i class="icon-folder-open"></i><span>vcs</span></a>' % HG_REPO)
-        response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/.gitignore"><i class="icon-doc"></i><span>.gitignore</span></a>' % HG_REPO)
-        response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/.hgignore"><i class="icon-doc"></i><span>.hgignore</span></a>' % HG_REPO)
-        response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/.hgtags"><i class="icon-doc"></i><span>.hgtags</span></a>' % HG_REPO)
-        response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/.travis.yml"><i class="icon-doc"></i><span>.travis.yml</span></a>' % HG_REPO)
-        response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/MANIFEST.in"><i class="icon-doc"></i><span>MANIFEST.in</span></a>' % HG_REPO)
-        response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/README.rst"><i class="icon-doc"></i><span>README.rst</span></a>' % HG_REPO)
-        response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/run_test_and_report.sh"><i class="icon-doc"></i><span>run_test_and_report.sh</span></a>' % HG_REPO)
-        response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/setup.cfg"><i class="icon-doc"></i><span>setup.cfg</span></a>' % HG_REPO)
-        response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/setup.py"><i class="icon-doc"></i><span>setup.py</span></a>' % HG_REPO)
-        response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/test_and_report.sh"><i class="icon-doc"></i><span>test_and_report.sh</span></a>' % HG_REPO)
-        response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/tox.ini"><i class="icon-doc"></i><span>tox.ini</span></a>' % HG_REPO)
+        response.mustcontain('<a class="browser-dir ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/docs"><i class="icon-folder-open"></i><span>docs</span></a>' % base.HG_REPO)
+        response.mustcontain('<a class="browser-dir ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/vcs"><i class="icon-folder-open"></i><span>vcs</span></a>' % base.HG_REPO)
+        response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/.gitignore"><i class="icon-doc"></i><span>.gitignore</span></a>' % base.HG_REPO)
+        response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/.hgignore"><i class="icon-doc"></i><span>.hgignore</span></a>' % base.HG_REPO)
+        response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/.hgtags"><i class="icon-doc"></i><span>.hgtags</span></a>' % base.HG_REPO)
+        response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/.travis.yml"><i class="icon-doc"></i><span>.travis.yml</span></a>' % base.HG_REPO)
+        response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/MANIFEST.in"><i class="icon-doc"></i><span>MANIFEST.in</span></a>' % base.HG_REPO)
+        response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/README.rst"><i class="icon-doc"></i><span>README.rst</span></a>' % base.HG_REPO)
+        response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/run_test_and_report.sh"><i class="icon-doc"></i><span>run_test_and_report.sh</span></a>' % base.HG_REPO)
+        response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/setup.cfg"><i class="icon-doc"></i><span>setup.cfg</span></a>' % base.HG_REPO)
+        response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/setup.py"><i class="icon-doc"></i><span>setup.py</span></a>' % base.HG_REPO)
+        response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/test_and_report.sh"><i class="icon-doc"></i><span>test_and_report.sh</span></a>' % base.HG_REPO)
+        response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/96507bd11ecc815ebc6270fdf6db110928c09c1e/tox.ini"><i class="icon-doc"></i><span>tox.ini</span></a>' % base.HG_REPO)
 
     def test_index_revision(self):
         self.log_user()
 
         response = self.app.get(
-            url(controller='files', action='index',
-                repo_name=HG_REPO,
+            base.url(controller='files', action='index',
+                repo_name=base.HG_REPO,
                 revision='7ba66bec8d6dbba14a2155be32408c435c5f4492',
                 f_path='/')
         )
 
         # Test response...
 
-        response.mustcontain('<a class="browser-dir ypjax-link" href="/%s/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/docs"><i class="icon-folder-open"></i><span>docs</span></a>' % HG_REPO)
-        response.mustcontain('<a class="browser-dir ypjax-link" href="/%s/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/tests"><i class="icon-folder-open"></i><span>tests</span></a>' % HG_REPO)
-        response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/README.rst"><i class="icon-doc"></i><span>README.rst</span></a>' % HG_REPO)
+        response.mustcontain('<a class="browser-dir ypjax-link" href="/%s/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/docs"><i class="icon-folder-open"></i><span>docs</span></a>' % base.HG_REPO)
+        response.mustcontain('<a class="browser-dir ypjax-link" href="/%s/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/tests"><i class="icon-folder-open"></i><span>tests</span></a>' % base.HG_REPO)
+        response.mustcontain('<a class="browser-file ypjax-link" href="/%s/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/README.rst"><i class="icon-doc"></i><span>README.rst</span></a>' % base.HG_REPO)
         response.mustcontain('1.1 KiB')
 
     def test_index_different_branch(self):
         self.log_user()
 
-        response = self.app.get(url(controller='files', action='index',
-                                    repo_name=HG_REPO,
+        response = self.app.get(base.url(controller='files', action='index',
+                                    repo_name=base.HG_REPO,
                                     revision='97e8b885c04894463c51898e14387d80c30ed1ee',
                                     f_path='/'))
 
@@ -85,8 +86,8 @@
                   (1, '3d8f361e72ab303da48d799ff1ac40d5ac37c67e'),
                   (0, 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]:
 
-            response = self.app.get(url(controller='files', action='index',
-                                    repo_name=HG_REPO,
+            response = self.app.get(base.url(controller='files', action='index',
+                                    repo_name=base.HG_REPO,
                                     revision=r[1],
                                     f_path='/'))
 
@@ -98,8 +99,8 @@
         import kallithea.lib.helpers
         kallithea.lib.helpers._urlify_issues_f = None
         self.log_user()
-        response = self.app.get(url(controller='files', action='index',
-                                    repo_name=HG_REPO,
+        response = self.app.get(base.url(controller='files', action='index',
+                                    repo_name=base.HG_REPO,
                                     revision='8911406ad776fdd3d0b9932a2e89677e57405a48',
                                     f_path='vcs/nodes.py'))
 
@@ -114,93 +115,93 @@
 
     def test_file_source_history(self):
         self.log_user()
-        response = self.app.get(url(controller='files', action='history',
-                                    repo_name=HG_REPO,
+        response = self.app.get(base.url(controller='files', action='history',
+                                    repo_name=base.HG_REPO,
                                     revision='tip',
                                     f_path='vcs/nodes.py'),
                                 extra_environ={'HTTP_X_PARTIAL_XHR': '1'},)
-        assert response.body == HG_NODE_HISTORY
+        assert json.loads(response.body) == json.loads(HG_NODE_HISTORY)
 
     def test_file_source_history_git(self):
         self.log_user()
-        response = self.app.get(url(controller='files', action='history',
-                                    repo_name=GIT_REPO,
+        response = self.app.get(base.url(controller='files', action='history',
+                                    repo_name=base.GIT_REPO,
                                     revision='master',
                                     f_path='vcs/nodes.py'),
                                 extra_environ={'HTTP_X_PARTIAL_XHR': '1'},)
-        assert response.body == GIT_NODE_HISTORY
+        assert json.loads(response.body) == json.loads(GIT_NODE_HISTORY)
 
     def test_file_annotation(self):
         self.log_user()
-        response = self.app.get(url(controller='files', action='index',
-                                    repo_name=HG_REPO,
+        response = self.app.get(base.url(controller='files', action='index',
+                                    repo_name=base.HG_REPO,
                                     revision='tip',
                                     f_path='vcs/nodes.py',
-                                    annotate=True))
+                                    annotate='1'))
 
         response.mustcontain("""r356:25213a5fbb04""")
 
     def test_file_annotation_git(self):
         self.log_user()
-        response = self.app.get(url(controller='files', action='index',
-                                    repo_name=GIT_REPO,
+        response = self.app.get(base.url(controller='files', action='index',
+                                    repo_name=base.GIT_REPO,
                                     revision='master',
                                     f_path='vcs/nodes.py',
-                                    annotate=True))
+                                    annotate='1'))
         response.mustcontain("""r345:c994f0de03b2""")
 
     def test_file_annotation_history(self):
         self.log_user()
-        response = self.app.get(url(controller='files', action='history',
-                                    repo_name=HG_REPO,
+        response = self.app.get(base.url(controller='files', action='history',
+                                    repo_name=base.HG_REPO,
                                     revision='tip',
                                     f_path='vcs/nodes.py',
-                                    annotate=True),
+                                    annotate='1'),
                                 extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
 
-        assert response.body == HG_NODE_HISTORY
+        assert json.loads(response.body) == json.loads(HG_NODE_HISTORY)
 
     def test_file_annotation_history_git(self):
         self.log_user()
-        response = self.app.get(url(controller='files', action='history',
-                                    repo_name=GIT_REPO,
+        response = self.app.get(base.url(controller='files', action='history',
+                                    repo_name=base.GIT_REPO,
                                     revision='master',
                                     f_path='vcs/nodes.py',
                                     annotate=True),
                                 extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
 
-        assert response.body == GIT_NODE_HISTORY
+        assert json.loads(response.body) == json.loads(GIT_NODE_HISTORY)
 
     def test_file_authors(self):
         self.log_user()
-        response = self.app.get(url(controller='files', action='authors',
-                                    repo_name=HG_REPO,
+        response = self.app.get(base.url(controller='files', action='authors',
+                                    repo_name=base.HG_REPO,
                                     revision='tip',
                                     f_path='vcs/nodes.py',
-                                    annotate=True))
+                                    annotate='1'))
         response.mustcontain('Marcin Kuzminski')
         response.mustcontain('Lukasz Balcerzak')
 
     def test_file_authors_git(self):
         self.log_user()
-        response = self.app.get(url(controller='files', action='authors',
-                                    repo_name=GIT_REPO,
+        response = self.app.get(base.url(controller='files', action='authors',
+                                    repo_name=base.GIT_REPO,
                                     revision='master',
                                     f_path='vcs/nodes.py',
-                                    annotate=True))
+                                    annotate='1'))
         response.mustcontain('Marcin Kuzminski')
         response.mustcontain('Lukasz Balcerzak')
 
     def test_archival(self):
         self.log_user()
-        _set_downloads(HG_REPO, set_to=True)
+        _set_downloads(base.HG_REPO, set_to=True)
         for arch_ext, info in ARCHIVE_SPECS.items():
             short = '27cd5cce30c9%s' % arch_ext
             fname = '27cd5cce30c96924232dffcd24178a07ffeb5dfc%s' % arch_ext
-            filename = '%s-%s' % (HG_REPO, short)
-            response = self.app.get(url(controller='files',
+            filename = '%s-%s' % (base.HG_REPO, short)
+            response = self.app.get(base.url(controller='files',
                                         action='archivefile',
-                                        repo_name=HG_REPO,
+                                        repo_name=base.HG_REPO,
                                         fname=fname))
 
             assert response.status == '200 OK'
@@ -210,29 +211,29 @@
                 ('Content-Disposition', 'attachment; filename=%s' % filename),
                 ('Content-Type', info[0]),
             ]
-            assert response.response._headers.items() == heads
+            assert sorted(response.response._headers.items()) == sorted(heads)
 
     def test_archival_wrong_ext(self):
         self.log_user()
-        _set_downloads(HG_REPO, set_to=True)
+        _set_downloads(base.HG_REPO, set_to=True)
         for arch_ext in ['tar', 'rar', 'x', '..ax', '.zipz']:
             fname = '27cd5cce30c96924232dffcd24178a07ffeb5dfc%s' % arch_ext
 
-            response = self.app.get(url(controller='files',
+            response = self.app.get(base.url(controller='files',
                                         action='archivefile',
-                                        repo_name=HG_REPO,
+                                        repo_name=base.HG_REPO,
                                         fname=fname))
             response.mustcontain('Unknown archive type')
 
     def test_archival_wrong_revision(self):
         self.log_user()
-        _set_downloads(HG_REPO, set_to=True)
+        _set_downloads(base.HG_REPO, set_to=True)
         for rev in ['00x000000', 'tar', 'wrong', '@##$@$42413232', '232dffcd']:
             fname = '%s.zip' % rev
 
-            response = self.app.get(url(controller='files',
+            response = self.app.get(base.url(controller='files',
                                         action='archivefile',
-                                        repo_name=HG_REPO,
+                                        repo_name=base.HG_REPO,
                                         fname=fname))
             response.mustcontain('Unknown revision')
 
@@ -241,8 +242,8 @@
     #==========================================================================
     def test_raw_file_ok(self):
         self.log_user()
-        response = self.app.get(url(controller='files', action='rawfile',
-                                    repo_name=HG_REPO,
+        response = self.app.get(base.url(controller='files', action='rawfile',
+                                    repo_name=base.HG_REPO,
                                     revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
                                     f_path='vcs/nodes.py'))
 
@@ -251,11 +252,11 @@
 
     def test_raw_file_wrong_cs(self):
         self.log_user()
-        rev = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
+        rev = 'ERRORce30c96924232dffcd24178a07ffeb5dfc'
         f_path = 'vcs/nodes.py'
 
-        response = self.app.get(url(controller='files', action='rawfile',
-                                    repo_name=HG_REPO,
+        response = self.app.get(base.url(controller='files', action='rawfile',
+                                    repo_name=base.HG_REPO,
                                     revision=rev,
                                     f_path=f_path), status=404)
 
@@ -266,12 +267,12 @@
         self.log_user()
         rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
         f_path = 'vcs/ERRORnodes.py'
-        response = self.app.get(url(controller='files', action='rawfile',
-                                    repo_name=HG_REPO,
+        response = self.app.get(base.url(controller='files', action='rawfile',
+                                    repo_name=base.HG_REPO,
                                     revision=rev,
                                     f_path=f_path), status=404)
 
-        msg = "There is no file nor directory at the given path: &#39;%s&#39; at revision %s" % (f_path, rev[:12])
+        msg = "There is no file nor directory at the given path: &apos;%s&apos; at revision %s" % (f_path, rev[:12])
         response.mustcontain(msg)
 
     #==========================================================================
@@ -279,8 +280,8 @@
     #==========================================================================
     def test_raw_ok(self):
         self.log_user()
-        response = self.app.get(url(controller='files', action='raw',
-                                    repo_name=HG_REPO,
+        response = self.app.get(base.url(controller='files', action='raw',
+                                    repo_name=base.HG_REPO,
                                     revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
                                     f_path='vcs/nodes.py'))
 
@@ -288,11 +289,11 @@
 
     def test_raw_wrong_cs(self):
         self.log_user()
-        rev = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
+        rev = 'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
         f_path = 'vcs/nodes.py'
 
-        response = self.app.get(url(controller='files', action='raw',
-                                    repo_name=HG_REPO,
+        response = self.app.get(base.url(controller='files', action='raw',
+                                    repo_name=base.HG_REPO,
                                     revision=rev,
                                     f_path=f_path), status=404)
 
@@ -303,18 +304,18 @@
         self.log_user()
         rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
         f_path = 'vcs/ERRORnodes.py'
-        response = self.app.get(url(controller='files', action='raw',
-                                    repo_name=HG_REPO,
+        response = self.app.get(base.url(controller='files', action='raw',
+                                    repo_name=base.HG_REPO,
                                     revision=rev,
                                     f_path=f_path), status=404)
-        msg = "There is no file nor directory at the given path: &#39;%s&#39; at revision %s" % (f_path, rev[:12])
+        msg = "There is no file nor directory at the given path: &apos;%s&apos; at revision %s" % (f_path, rev[:12])
         response.mustcontain(msg)
 
     def test_ajaxed_files_list(self):
         self.log_user()
         rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
         response = self.app.get(
-            url('files_nodelist_home', repo_name=HG_REPO, f_path='/',
+            base.url('files_nodelist_home', repo_name=base.HG_REPO, f_path='/',
                 revision=rev),
             extra_environ={'HTTP_X_PARTIAL_XHR': '1'},
         )
@@ -323,14 +324,14 @@
     # Hg - ADD FILE
     def test_add_file_view_hg(self):
         self.log_user()
-        response = self.app.get(url('files_add_home',
-                                      repo_name=HG_REPO,
+        response = self.app.get(base.url('files_add_home',
+                                      repo_name=base.HG_REPO,
                                       revision='tip', f_path='/'))
 
     def test_add_file_into_hg_missing_content(self):
         self.log_user()
-        response = self.app.post(url('files_add_home',
-                                      repo_name=HG_REPO,
+        response = self.app.post(base.url('files_add_home',
+                                      repo_name=base.HG_REPO,
                                       revision='tip', f_path='/'),
                                  params={
                                     'content': '',
@@ -342,8 +343,8 @@
 
     def test_add_file_into_hg_missing_filename(self):
         self.log_user()
-        response = self.app.post(url('files_add_home',
-                                      repo_name=HG_REPO,
+        response = self.app.post(base.url('files_add_home',
+                                      repo_name=base.HG_REPO,
                                       revision='tip', f_path='/'),
                                  params={
                                     'content': "foo",
@@ -353,15 +354,15 @@
 
         self.checkSessionFlash(response, 'No filename')
 
-    @parametrize('location,filename', [
+    @base.parametrize('location,filename', [
         ('/abs', 'foo'),
         ('../rel', 'foo'),
         ('file/../foo', 'foo'),
     ])
     def test_add_file_into_hg_bad_filenames(self, location, filename):
         self.log_user()
-        response = self.app.post(url('files_add_home',
-                                      repo_name=HG_REPO,
+        response = self.app.post(base.url('files_add_home',
+                                      repo_name=base.HG_REPO,
                                       revision='tip', f_path='/'),
                                  params={
                                     'content': "foo",
@@ -373,15 +374,15 @@
 
         self.checkSessionFlash(response, 'Location must be relative path and must not contain .. in path')
 
-    @parametrize('cnt,location,filename', [
+    @base.parametrize('cnt,location,filename', [
         (1, '', 'foo.txt'),
         (2, 'dir', 'foo.rst'),
         (3, 'rel/dir', 'foo.bar'),
     ])
     def test_add_file_into_hg(self, cnt, location, filename):
         self.log_user()
-        repo = fixture.create_repo(u'commit-test-%s' % cnt, repo_type='hg')
-        response = self.app.post(url('files_add_home',
+        repo = fixture.create_repo('commit-test-%s' % cnt, repo_type='hg')
+        response = self.app.post(base.url('files_add_home',
                                       repo_name=repo.repo_name,
                                       revision='tip', f_path='/'),
                                  params={
@@ -400,14 +401,14 @@
     # Git - add file
     def test_add_file_view_git(self):
         self.log_user()
-        response = self.app.get(url('files_add_home',
-                                      repo_name=GIT_REPO,
+        response = self.app.get(base.url('files_add_home',
+                                      repo_name=base.GIT_REPO,
                                       revision='tip', f_path='/'))
 
     def test_add_file_into_git_missing_content(self):
         self.log_user()
-        response = self.app.post(url('files_add_home',
-                                      repo_name=GIT_REPO,
+        response = self.app.post(base.url('files_add_home',
+                                      repo_name=base.GIT_REPO,
                                       revision='tip', f_path='/'),
                                  params={
                                      'content': '',
@@ -418,8 +419,8 @@
 
     def test_add_file_into_git_missing_filename(self):
         self.log_user()
-        response = self.app.post(url('files_add_home',
-                                      repo_name=GIT_REPO,
+        response = self.app.post(base.url('files_add_home',
+                                      repo_name=base.GIT_REPO,
                                       revision='tip', f_path='/'),
                                  params={
                                     'content': "foo",
@@ -429,15 +430,15 @@
 
         self.checkSessionFlash(response, 'No filename')
 
-    @parametrize('location,filename', [
+    @base.parametrize('location,filename', [
         ('/abs', 'foo'),
         ('../rel', 'foo'),
         ('file/../foo', 'foo'),
     ])
     def test_add_file_into_git_bad_filenames(self, location, filename):
         self.log_user()
-        response = self.app.post(url('files_add_home',
-                                      repo_name=GIT_REPO,
+        response = self.app.post(base.url('files_add_home',
+                                      repo_name=base.GIT_REPO,
                                       revision='tip', f_path='/'),
                                  params={
                                     'content': "foo",
@@ -449,15 +450,15 @@
 
         self.checkSessionFlash(response, 'Location must be relative path and must not contain .. in path')
 
-    @parametrize('cnt,location,filename', [
+    @base.parametrize('cnt,location,filename', [
         (1, '', 'foo.txt'),
         (2, 'dir', 'foo.rst'),
         (3, 'rel/dir', 'foo.bar'),
     ])
     def test_add_file_into_git(self, cnt, location, filename):
         self.log_user()
-        repo = fixture.create_repo(u'commit-test-%s' % cnt, repo_type='git')
-        response = self.app.post(url('files_add_home',
+        repo = fixture.create_repo('commit-test-%s' % cnt, repo_type='git')
+        response = self.app.post(base.url('files_add_home',
                                       repo_name=repo.repo_name,
                                       revision='tip', f_path='/'),
                                  params={
@@ -476,18 +477,27 @@
     # Hg - EDIT
     def test_edit_file_view_hg(self):
         self.log_user()
-        response = self.app.get(url('files_edit_home',
-                                      repo_name=HG_REPO,
+        response = self.app.get(base.url('files_edit_home',
+                                      repo_name=base.HG_REPO,
                                       revision='tip', f_path='vcs/nodes.py'))
+        # Odd error when on tip ...
+        self.checkSessionFlash(response, "You can only edit files with revision being a valid branch")
+        assert b"Commit Message" not in response.body
+
+        # Specify branch head revision to avoid "valid branch" error and get coverage of edit form
+        response = self.app.get(base.url('files_edit_home',
+                                      repo_name=base.HG_REPO,
+                                      revision='96507bd11ecc815ebc6270fdf6db110928c09c1e', f_path='vcs/nodes.py'))
+        assert b"Commit Message" in response.body
 
     def test_edit_file_view_not_on_branch_hg(self):
         self.log_user()
-        repo = fixture.create_repo(u'test-edit-repo', repo_type='hg')
+        repo = fixture.create_repo('test-edit-repo', repo_type='hg')
 
         ## add file
         location = 'vcs'
         filename = 'nodes.py'
-        response = self.app.post(url('files_add_home',
+        response = self.app.post(base.url('files_add_home',
                                       repo_name=repo.repo_name,
                                       revision='tip', f_path='/'),
                                  params={
@@ -501,7 +511,7 @@
         try:
             self.checkSessionFlash(response, 'Successfully committed to %s'
                                    % posixpath.join(location, filename))
-            response = self.app.get(url('files_edit_home',
+            response = self.app.get(base.url('files_edit_home',
                                           repo_name=repo.repo_name,
                                           revision='tip', f_path=posixpath.join(location, filename)),
                                     status=302)
@@ -512,12 +522,12 @@
 
     def test_edit_file_view_commit_changes_hg(self):
         self.log_user()
-        repo = fixture.create_repo(u'test-edit-repo', repo_type='hg')
+        repo = fixture.create_repo('test-edit-repo', repo_type='hg')
 
         ## add file
         location = 'vcs'
         filename = 'nodes.py'
-        response = self.app.post(url('files_add_home',
+        response = self.app.post(base.url('files_add_home',
                                       repo_name=repo.repo_name,
                                       revision='tip',
                                       f_path='/'),
@@ -532,7 +542,7 @@
         try:
             self.checkSessionFlash(response, 'Successfully committed to %s'
                                    % posixpath.join(location, filename))
-            response = self.app.post(url('files_edit_home',
+            response = self.app.post(base.url('files_edit_home',
                                           repo_name=repo.repo_name,
                                           revision=repo.scm_instance.DEFAULT_BRANCH_NAME,
                                           f_path=posixpath.join(location, filename)),
@@ -550,18 +560,18 @@
     # Git - edit
     def test_edit_file_view_git(self):
         self.log_user()
-        response = self.app.get(url('files_edit_home',
-                                      repo_name=GIT_REPO,
+        response = self.app.get(base.url('files_edit_home',
+                                      repo_name=base.GIT_REPO,
                                       revision='tip', f_path='vcs/nodes.py'))
 
     def test_edit_file_view_not_on_branch_git(self):
         self.log_user()
-        repo = fixture.create_repo(u'test-edit-repo', repo_type='git')
+        repo = fixture.create_repo('test-edit-repo', repo_type='git')
 
         ## add file
         location = 'vcs'
         filename = 'nodes.py'
-        response = self.app.post(url('files_add_home',
+        response = self.app.post(base.url('files_add_home',
                                       repo_name=repo.repo_name,
                                       revision='tip', f_path='/'),
                                  params={
@@ -575,7 +585,7 @@
         try:
             self.checkSessionFlash(response, 'Successfully committed to %s'
                                    % posixpath.join(location, filename))
-            response = self.app.get(url('files_edit_home',
+            response = self.app.get(base.url('files_edit_home',
                                           repo_name=repo.repo_name,
                                           revision='tip', f_path=posixpath.join(location, filename)),
                                     status=302)
@@ -586,12 +596,12 @@
 
     def test_edit_file_view_commit_changes_git(self):
         self.log_user()
-        repo = fixture.create_repo(u'test-edit-repo', repo_type='git')
+        repo = fixture.create_repo('test-edit-repo', repo_type='git')
 
         ## add file
         location = 'vcs'
         filename = 'nodes.py'
-        response = self.app.post(url('files_add_home',
+        response = self.app.post(base.url('files_add_home',
                                       repo_name=repo.repo_name,
                                       revision='tip',
                                       f_path='/'),
@@ -606,7 +616,7 @@
         try:
             self.checkSessionFlash(response, 'Successfully committed to %s'
                                    % posixpath.join(location, filename))
-            response = self.app.post(url('files_edit_home',
+            response = self.app.post(base.url('files_edit_home',
                                           repo_name=repo.repo_name,
                                           revision=repo.scm_instance.DEFAULT_BRANCH_NAME,
                                           f_path=posixpath.join(location, filename)),
@@ -624,18 +634,18 @@
     # Hg - delete
     def test_delete_file_view_hg(self):
         self.log_user()
-        response = self.app.get(url('files_delete_home',
-                                     repo_name=HG_REPO,
+        response = self.app.get(base.url('files_delete_home',
+                                     repo_name=base.HG_REPO,
                                      revision='tip', f_path='vcs/nodes.py'))
 
     def test_delete_file_view_not_on_branch_hg(self):
         self.log_user()
-        repo = fixture.create_repo(u'test-delete-repo', repo_type='hg')
+        repo = fixture.create_repo('test-delete-repo', repo_type='hg')
 
         ## add file
         location = 'vcs'
         filename = 'nodes.py'
-        response = self.app.post(url('files_add_home',
+        response = self.app.post(base.url('files_add_home',
                                       repo_name=repo.repo_name,
                                       revision='tip', f_path='/'),
                                  params={
@@ -649,7 +659,7 @@
         try:
             self.checkSessionFlash(response, 'Successfully committed to %s'
                                    % posixpath.join(location, filename))
-            response = self.app.get(url('files_delete_home',
+            response = self.app.get(base.url('files_delete_home',
                                           repo_name=repo.repo_name,
                                           revision='tip', f_path=posixpath.join(location, filename)),
                                     status=302)
@@ -660,12 +670,12 @@
 
     def test_delete_file_view_commit_changes_hg(self):
         self.log_user()
-        repo = fixture.create_repo(u'test-delete-repo', repo_type='hg')
+        repo = fixture.create_repo('test-delete-repo', repo_type='hg')
 
         ## add file
         location = 'vcs'
         filename = 'nodes.py'
-        response = self.app.post(url('files_add_home',
+        response = self.app.post(base.url('files_add_home',
                                       repo_name=repo.repo_name,
                                       revision='tip',
                                       f_path='/'),
@@ -680,7 +690,7 @@
         try:
             self.checkSessionFlash(response, 'Successfully committed to %s'
                                    % posixpath.join(location, filename))
-            response = self.app.post(url('files_delete_home',
+            response = self.app.post(base.url('files_delete_home',
                                           repo_name=repo.repo_name,
                                           revision=repo.scm_instance.DEFAULT_BRANCH_NAME,
                                           f_path=posixpath.join(location, filename)),
@@ -697,18 +707,18 @@
     # Git - delete
     def test_delete_file_view_git(self):
         self.log_user()
-        response = self.app.get(url('files_delete_home',
-                                     repo_name=HG_REPO,
+        response = self.app.get(base.url('files_delete_home',
+                                     repo_name=base.HG_REPO,
                                      revision='tip', f_path='vcs/nodes.py'))
 
     def test_delete_file_view_not_on_branch_git(self):
         self.log_user()
-        repo = fixture.create_repo(u'test-delete-repo', repo_type='git')
+        repo = fixture.create_repo('test-delete-repo', repo_type='git')
 
         ## add file
         location = 'vcs'
         filename = 'nodes.py'
-        response = self.app.post(url('files_add_home',
+        response = self.app.post(base.url('files_add_home',
                                       repo_name=repo.repo_name,
                                       revision='tip', f_path='/'),
                                  params={
@@ -722,7 +732,7 @@
         try:
             self.checkSessionFlash(response, 'Successfully committed to %s'
                                    % posixpath.join(location, filename))
-            response = self.app.get(url('files_delete_home',
+            response = self.app.get(base.url('files_delete_home',
                                           repo_name=repo.repo_name,
                                           revision='tip', f_path=posixpath.join(location, filename)),
                                     status=302)
@@ -733,12 +743,12 @@
 
     def test_delete_file_view_commit_changes_git(self):
         self.log_user()
-        repo = fixture.create_repo(u'test-delete-repo', repo_type='git')
+        repo = fixture.create_repo('test-delete-repo', repo_type='git')
 
         ## add file
         location = 'vcs'
         filename = 'nodes.py'
-        response = self.app.post(url('files_add_home',
+        response = self.app.post(base.url('files_add_home',
                                       repo_name=repo.repo_name,
                                       revision='tip',
                                       f_path='/'),
@@ -753,7 +763,7 @@
         try:
             self.checkSessionFlash(response, 'Successfully committed to %s'
                                    % posixpath.join(location, filename))
-            response = self.app.post(url('files_delete_home',
+            response = self.app.post(base.url('files_delete_home',
                                           repo_name=repo.repo_name,
                                           revision=repo.scm_instance.DEFAULT_BRANCH_NAME,
                                           f_path=posixpath.join(location, filename)),
@@ -769,16 +779,16 @@
 
     def test_png_diff_no_crash_hg(self):
         self.log_user()
-        response = self.app.get(url('files_diff_home',
-                                    repo_name=HG_REPO,
+        response = self.app.get(base.url('files_diff_home',
+                                    repo_name=base.HG_REPO,
                                     f_path='docs/theme/ADC/static/documentation.png',
                                     diff1='tip', diff2='tip'))
         response.mustcontain("""<pre>Binary file</pre>""")
 
     def test_png_diff_no_crash_git(self):
         self.log_user()
-        response = self.app.get(url('files_diff_home',
-                                    repo_name=GIT_REPO,
+        response = self.app.get(base.url('files_diff_home',
+                                    repo_name=base.GIT_REPO,
                                     f_path='docs/theme/ADC/static/documentation.png',
                                     diff1='master', diff2='master'))
         response.mustcontain("""<pre>Binary file</pre>""")
--- a/kallithea/tests/functional/test_followers.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_followers.py	Mon May 04 19:24:04 2020 +0200
@@ -1,24 +1,24 @@
-from kallithea.tests.base import *
+from kallithea.tests import base
 
 
-class TestFollowersController(TestController):
+class TestFollowersController(base.TestController):
 
     def test_index_hg(self):
         self.log_user()
-        repo_name = HG_REPO
-        response = self.app.get(url(controller='followers',
+        repo_name = base.HG_REPO
+        response = self.app.get(base.url(controller='followers',
                                     action='followers',
                                     repo_name=repo_name))
 
-        response.mustcontain(TEST_USER_ADMIN_LOGIN)
+        response.mustcontain(base.TEST_USER_ADMIN_LOGIN)
         response.mustcontain("""Started following""")
 
     def test_index_git(self):
         self.log_user()
-        repo_name = GIT_REPO
-        response = self.app.get(url(controller='followers',
+        repo_name = base.GIT_REPO
+        response = self.app.get(base.url(controller='followers',
                                     action='followers',
                                     repo_name=repo_name))
 
-        response.mustcontain(TEST_USER_ADMIN_LOGIN)
+        response.mustcontain(base.TEST_USER_ADMIN_LOGIN)
         response.mustcontain("""Started following""")
--- a/kallithea/tests/functional/test_forks.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_forks.py	Mon May 04 19:24:04 2020 +0200
@@ -1,20 +1,19 @@
 # -*- coding: utf-8 -*-
 
-import urllib
+import urllib.parse
 
-from kallithea.lib.utils2 import safe_str, safe_unicode
 from kallithea.model.db import Repository, User
 from kallithea.model.meta import Session
 from kallithea.model.repo import RepoModel
 from kallithea.model.user import UserModel
-from kallithea.tests.base import *
+from kallithea.tests import base
 from kallithea.tests.fixture import Fixture
 
 
 fixture = Fixture()
 
 
-class _BaseTestCase(TestController):
+class _BaseTestCase(base.TestController):
     """
     Write all tests here
     """
@@ -24,9 +23,9 @@
     REPO_FORK = None
 
     def setup_method(self, method):
-        self.username = u'forkuser'
-        self.password = u'qweqwe'
-        u1 = fixture.create_user(self.username, password=self.password, email=u'fork_king@example.com')
+        self.username = 'forkuser'
+        self.password = 'qweqwe'
+        u1 = fixture.create_user(self.username, password=self.password, email='fork_king@example.com')
         self.u1_id = u1.user_id
         Session().commit()
 
@@ -37,13 +36,13 @@
     def test_index(self):
         self.log_user()
         repo_name = self.REPO
-        response = self.app.get(url(controller='forks', action='forks',
+        response = self.app.get(base.url(controller='forks', action='forks',
                                     repo_name=repo_name))
 
         response.mustcontain("""There are no forks yet""")
 
     def test_no_permissions_to_fork(self):
-        self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)['user_id']
+        self.log_user(base.TEST_USER_REGULAR_LOGIN, base.TEST_USER_REGULAR_PASS)['user_id']
         try:
             user_model = UserModel()
             usr = User.get_default_user()
@@ -52,7 +51,7 @@
             Session().commit()
             # try create a fork
             repo_name = self.REPO
-            self.app.post(url(controller='forks', action='fork_create',
+            self.app.post(base.url(controller='forks', action='fork_create',
                               repo_name=repo_name), {'_session_csrf_secret_token': self.session_csrf_secret_token()}, status=403)
         finally:
             usr = User.get_default_user()
@@ -70,7 +69,7 @@
         org_repo = Repository.get_by_repo_name(repo_name)
         creation_args = {
             'repo_name': fork_name,
-            'repo_group': u'-1',
+            'repo_group': '-1',
             'fork_parent_id': org_repo.repo_id,
             'repo_type': self.REPO_TYPE,
             'description': description,
@@ -78,10 +77,10 @@
             'landing_rev': 'rev:tip',
             '_session_csrf_secret_token': self.session_csrf_secret_token()}
 
-        self.app.post(url(controller='forks', action='fork_create',
+        self.app.post(base.url(controller='forks', action='fork_create',
                           repo_name=repo_name), creation_args)
 
-        response = self.app.get(url(controller='forks', action='forks',
+        response = self.app.get(base.url(controller='forks', action='forks',
                                     repo_name=repo_name))
 
         response.mustcontain(
@@ -89,12 +88,12 @@
         )
 
         # remove this fork
-        response = self.app.post(url('delete_repo', repo_name=fork_name),
+        response = self.app.post(base.url('delete_repo', repo_name=fork_name),
             params={'_session_csrf_secret_token': self.session_csrf_secret_token()})
 
     def test_fork_create_into_group(self):
         self.log_user()
-        group = fixture.create_repo_group(u'vc')
+        group = fixture.create_repo_group('vc')
         group_id = group.group_id
         fork_name = self.REPO_FORK
         fork_name_full = 'vc/%s' % fork_name
@@ -110,13 +109,13 @@
             'private': 'False',
             'landing_rev': 'rev:tip',
             '_session_csrf_secret_token': self.session_csrf_secret_token()}
-        self.app.post(url(controller='forks', action='fork_create',
+        self.app.post(base.url(controller='forks', action='fork_create',
                           repo_name=repo_name), creation_args)
         repo = Repository.get_by_repo_name(fork_name_full)
         assert repo.fork.repo_name == self.REPO
 
         ## run the check page that triggers the flash message
-        response = self.app.get(url('repo_check_home', repo_name=fork_name_full))
+        response = self.app.get(base.url('repo_check_home', repo_name=fork_name_full))
         # test if we have a message that fork is ok
         self.checkSessionFlash(response,
                 'Forked repository %s as <a href="/%s">%s</a>'
@@ -130,7 +129,7 @@
         assert fork_repo.fork.repo_name == repo_name
 
         # test if the repository is visible in the list ?
-        response = self.app.get(url('summary_home', repo_name=fork_name_full))
+        response = self.app.get(base.url('summary_home', repo_name=fork_name_full))
         response.mustcontain(fork_name_full)
         response.mustcontain(self.REPO_TYPE)
         response.mustcontain('Fork of "<a href="/%s">%s</a>"' % (repo_name, repo_name))
@@ -144,49 +143,49 @@
         # create a fork
         repo_name = self.REPO
         org_repo = Repository.get_by_repo_name(repo_name)
-        fork_name = safe_str(self.REPO_FORK + u'-rødgrød')
+        fork_name = self.REPO_FORK + '-rødgrød'
         creation_args = {
             'repo_name': fork_name,
-            'repo_group': u'-1',
+            'repo_group': '-1',
             'fork_parent_id': org_repo.repo_id,
             'repo_type': self.REPO_TYPE,
             'description': 'unicode repo 1',
             'private': 'False',
             'landing_rev': 'rev:tip',
             '_session_csrf_secret_token': self.session_csrf_secret_token()}
-        self.app.post(url(controller='forks', action='fork_create',
+        self.app.post(base.url(controller='forks', action='fork_create',
                           repo_name=repo_name), creation_args)
-        response = self.app.get(url(controller='forks', action='forks',
+        response = self.app.get(base.url(controller='forks', action='forks',
                                     repo_name=repo_name))
         response.mustcontain(
-            """<a href="/%s">%s</a>""" % (urllib.quote(fork_name), fork_name)
+            """<a href="/%s">%s</a>""" % (urllib.parse.quote(fork_name), fork_name)
         )
-        fork_repo = Repository.get_by_repo_name(safe_unicode(fork_name))
+        fork_repo = Repository.get_by_repo_name(fork_name)
         assert fork_repo
 
         # fork the fork
-        fork_name_2 = safe_str(self.REPO_FORK + u'-blåbærgrød')
+        fork_name_2 = self.REPO_FORK + '-blåbærgrød'
         creation_args = {
             'repo_name': fork_name_2,
-            'repo_group': u'-1',
+            'repo_group': '-1',
             'fork_parent_id': fork_repo.repo_id,
             'repo_type': self.REPO_TYPE,
             'description': 'unicode repo 2',
             'private': 'False',
             'landing_rev': 'rev:tip',
             '_session_csrf_secret_token': self.session_csrf_secret_token()}
-        self.app.post(url(controller='forks', action='fork_create',
+        self.app.post(base.url(controller='forks', action='fork_create',
                           repo_name=fork_name), creation_args)
-        response = self.app.get(url(controller='forks', action='forks',
+        response = self.app.get(base.url(controller='forks', action='forks',
                                     repo_name=fork_name))
         response.mustcontain(
-            """<a href="/%s">%s</a>""" % (urllib.quote(fork_name_2), fork_name_2)
+            """<a href="/%s">%s</a>""" % (urllib.parse.quote(fork_name_2), fork_name_2)
         )
 
         # remove these forks
-        response = self.app.post(url('delete_repo', repo_name=fork_name_2),
+        response = self.app.post(base.url('delete_repo', repo_name=fork_name_2),
             params={'_session_csrf_secret_token': self.session_csrf_secret_token()})
-        response = self.app.post(url('delete_repo', repo_name=fork_name),
+        response = self.app.post(base.url('delete_repo', repo_name=fork_name),
             params={'_session_csrf_secret_token': self.session_csrf_secret_token()})
 
     def test_fork_create_and_permissions(self):
@@ -197,20 +196,20 @@
         org_repo = Repository.get_by_repo_name(repo_name)
         creation_args = {
             'repo_name': fork_name,
-            'repo_group': u'-1',
+            'repo_group': '-1',
             'fork_parent_id': org_repo.repo_id,
             'repo_type': self.REPO_TYPE,
             'description': description,
             'private': 'False',
             'landing_rev': 'rev:tip',
             '_session_csrf_secret_token': self.session_csrf_secret_token()}
-        self.app.post(url(controller='forks', action='fork_create',
+        self.app.post(base.url(controller='forks', action='fork_create',
                           repo_name=repo_name), creation_args)
         repo = Repository.get_by_repo_name(self.REPO_FORK)
         assert repo.fork.repo_name == self.REPO
 
         ## run the check page that triggers the flash message
-        response = self.app.get(url('repo_check_home', repo_name=fork_name))
+        response = self.app.get(base.url('repo_check_home', repo_name=fork_name))
         # test if we have a message that fork is ok
         self.checkSessionFlash(response,
                 'Forked repository %s as <a href="/%s">%s</a>'
@@ -224,7 +223,7 @@
         assert fork_repo.fork.repo_name == repo_name
 
         # test if the repository is visible in the list ?
-        response = self.app.get(url('summary_home', repo_name=fork_name))
+        response = self.app.get(base.url('summary_home', repo_name=fork_name))
         response.mustcontain(fork_name)
         response.mustcontain(self.REPO_TYPE)
         response.mustcontain('Fork of "<a href="/%s">%s</a>"' % (repo_name, repo_name))
@@ -242,7 +241,7 @@
                                           perm='repository.read')
         Session().commit()
 
-        response = self.app.get(url(controller='forks', action='forks',
+        response = self.app.get(base.url(controller='forks', action='forks',
                                     repo_name=repo_name))
 
         response.mustcontain('<div>fork of vcs test</div>')
@@ -257,7 +256,7 @@
             Session().commit()
 
             # fork shouldn't be visible
-            response = self.app.get(url(controller='forks', action='forks',
+            response = self.app.get(base.url(controller='forks', action='forks',
                                         repo_name=repo_name))
             response.mustcontain('There are no forks yet')
 
@@ -270,14 +269,14 @@
 
 
 class TestGIT(_BaseTestCase):
-    REPO = GIT_REPO
-    NEW_REPO = NEW_GIT_REPO
+    REPO = base.GIT_REPO
+    NEW_REPO = base.NEW_GIT_REPO
     REPO_TYPE = 'git'
-    REPO_FORK = GIT_FORK
+    REPO_FORK = base.GIT_FORK
 
 
 class TestHG(_BaseTestCase):
-    REPO = HG_REPO
-    NEW_REPO = NEW_HG_REPO
+    REPO = base.HG_REPO
+    NEW_REPO = base.NEW_HG_REPO
     REPO_TYPE = 'hg'
-    REPO_FORK = HG_FORK
+    REPO_FORK = base.HG_FORK
--- a/kallithea/tests/functional/test_home.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_home.py	Mon May 04 19:24:04 2020 +0200
@@ -4,18 +4,18 @@
 from kallithea.model.meta import Session
 from kallithea.model.repo import RepoModel
 from kallithea.model.repo_group import RepoGroupModel
-from kallithea.tests.base import *
+from kallithea.tests import base
 from kallithea.tests.fixture import Fixture
 
 
 fixture = Fixture()
 
 
-class TestHomeController(TestController):
+class TestHomeController(base.TestController):
 
     def test_index(self):
         self.log_user()
-        response = self.app.get(url(controller='home', action='index'))
+        response = self.app.get(base.url(controller='home', action='index'))
         # if global permission is set
         response.mustcontain('Add Repository')
 
@@ -28,60 +28,60 @@
         )
 
         # html in javascript variable:
-        response.mustcontain(r'href=\"/%s\"' % HG_REPO)
+        response.mustcontain(r'href=\"/%s\"' % base.HG_REPO)
 
         response.mustcontain(r'\x3ci class=\"icon-globe\"')
 
         response.mustcontain(r'\"fixes issue with having custom format for git-log\n\"')
-        response.mustcontain(r'\"/%s/changeset/5f2c6ee195929b0be80749243c18121c9864a3b3\"' % GIT_REPO)
+        response.mustcontain(r'\"/%s/changeset/5f2c6ee195929b0be80749243c18121c9864a3b3\"' % base.GIT_REPO)
 
         response.mustcontain(r'\"disable security checks on hg clone for travis\"')
-        response.mustcontain(r'\"/%s/changeset/96507bd11ecc815ebc6270fdf6db110928c09c1e\"' % HG_REPO)
+        response.mustcontain(r'\"/%s/changeset/96507bd11ecc815ebc6270fdf6db110928c09c1e\"' % base.HG_REPO)
 
     def test_repo_summary_with_anonymous_access_disabled(self):
         with fixture.anon_access(False):
-            response = self.app.get(url(controller='summary',
-                                        action='index', repo_name=HG_REPO),
+            response = self.app.get(base.url(controller='summary',
+                                        action='index', repo_name=base.HG_REPO),
                                         status=302)
             assert 'login' in response.location
 
     def test_index_with_anonymous_access_disabled(self):
         with fixture.anon_access(False):
-            response = self.app.get(url(controller='home', action='index'),
+            response = self.app.get(base.url(controller='home', action='index'),
                                     status=302)
             assert 'login' in response.location
 
     def test_index_page_on_groups(self):
         self.log_user()
-        gr = fixture.create_repo_group(u'gr1')
-        fixture.create_repo(name=u'gr1/repo_in_group', repo_group=gr)
-        response = self.app.get(url('repos_group_home', group_name=u'gr1'))
+        gr = fixture.create_repo_group('gr1')
+        fixture.create_repo(name='gr1/repo_in_group', repo_group=gr)
+        response = self.app.get(base.url('repos_group_home', group_name='gr1'))
 
         try:
-            response.mustcontain(u"gr1/repo_in_group")
+            response.mustcontain("gr1/repo_in_group")
         finally:
-            RepoModel().delete(u'gr1/repo_in_group')
-            RepoGroupModel().delete(repo_group=u'gr1', force_delete=True)
+            RepoModel().delete('gr1/repo_in_group')
+            RepoGroupModel().delete(repo_group='gr1', force_delete=True)
             Session().commit()
 
     def test_users_and_groups_data(self):
-        fixture.create_user('evil', firstname=u'D\'o\'ct"o"r', lastname=u'Évíl')
-        fixture.create_user_group(u'grrrr', user_group_description=u"Groüp")
-        response = self.app.get(url('users_and_groups_data', query=u'evi'))
+        fixture.create_user('evil', firstname='D\'o\'ct"o"r', lastname='Évíl')
+        fixture.create_user_group('grrrr', user_group_description="Groüp")
+        response = self.app.get(base.url('users_and_groups_data', query='evi'))
         assert response.status_code == 302
-        assert url('login_home') in response.location
-        self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
-        response = self.app.get(url('users_and_groups_data', query=u'evi'))
+        assert base.url('login_home') in response.location
+        self.log_user(base.TEST_USER_REGULAR_LOGIN, base.TEST_USER_REGULAR_PASS)
+        response = self.app.get(base.url('users_and_groups_data', query='evi'))
         result = json.loads(response.body)['results']
-        assert result[0].get('fname') == u'D\'o\'ct"o"r'
-        assert result[0].get('lname') == u'Évíl'
-        response = self.app.get(url('users_and_groups_data', key=u'evil'))
+        assert result[0].get('fname') == 'D\'o\'ct"o"r'
+        assert result[0].get('lname') == 'Évíl'
+        response = self.app.get(base.url('users_and_groups_data', key='evil'))
         result = json.loads(response.body)['results']
-        assert result[0].get('fname') == u'D\'o\'ct"o"r'
-        assert result[0].get('lname') == u'Évíl'
-        response = self.app.get(url('users_and_groups_data', query=u'rrrr'))
+        assert result[0].get('fname') == 'D\'o\'ct"o"r'
+        assert result[0].get('lname') == 'Évíl'
+        response = self.app.get(base.url('users_and_groups_data', query='rrrr'))
         result = json.loads(response.body)['results']
         assert not result
-        response = self.app.get(url('users_and_groups_data', types='users,groups', query=u'rrrr'))
+        response = self.app.get(base.url('users_and_groups_data', types='users,groups', query='rrrr'))
         result = json.loads(response.body)['results']
-        assert result[0].get('grname') == u'grrrr'
+        assert result[0].get('grname') == 'grrrr'
--- a/kallithea/tests/functional/test_journal.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_journal.py	Mon May 04 19:24:04 2020 +0200
@@ -1,13 +1,13 @@
 import datetime
 
-from kallithea.tests.base import *
+from kallithea.tests import base
 
 
-class TestJournalController(TestController):
+class TestJournalController(base.TestController):
 
     def test_index(self):
         self.log_user()
-        response = self.app.get(url(controller='journal', action='index'))
+        response = self.app.get(base.url(controller='journal', action='index'))
 
         response.mustcontain("""<h4>%s</h4>""" % datetime.date.today())
 
@@ -22,18 +22,18 @@
 #
 #        assert len(followings) == 1, 'Not following any repository'
 #
-#        response = self.app.post(url(controller='journal',
+#        response = self.app.post(base.url(controller='journal',
 #                                     action='toggle_following'),
 #                                     {'follows_repository_id':repo.repo_id})
 
     def test_start_following_repository(self):
         self.log_user()
-        response = self.app.get(url(controller='journal', action='index'),)
+        response = self.app.get(base.url(controller='journal', action='index'),)
 
     def test_public_journal_atom(self):
         self.log_user()
-        response = self.app.get(url(controller='journal', action='public_journal_atom'),)
+        response = self.app.get(base.url(controller='journal', action='public_journal_atom'),)
 
     def test_public_journal_rss(self):
         self.log_user()
-        response = self.app.get(url(controller='journal', action='public_journal_rss'),)
+        response = self.app.get(base.url(controller='journal', action='public_journal_rss'),)
--- a/kallithea/tests/functional/test_login.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_login.py	Mon May 04 19:24:04 2020 +0200
@@ -1,10 +1,12 @@
 # -*- coding: utf-8 -*-
 import re
 import time
-import urlparse
+import urllib.parse
 
+import mock
 from tg.util.webtest import test_context
 
+import kallithea.lib.celerylib.tasks
 from kallithea.lib import helpers as h
 from kallithea.lib.auth import check_password
 from kallithea.lib.utils2 import generate_api_key
@@ -13,61 +15,61 @@
 from kallithea.model.db import User
 from kallithea.model.meta import Session
 from kallithea.model.user import UserModel
-from kallithea.tests.base import *
+from kallithea.tests import base
 from kallithea.tests.fixture import Fixture
 
 
 fixture = Fixture()
 
 
-class TestLoginController(TestController):
+class TestLoginController(base.TestController):
 
     def test_index(self):
-        response = self.app.get(url(controller='login', action='index'))
+        response = self.app.get(base.url(controller='login', action='index'))
         assert response.status == '200 OK'
         # Test response...
 
     def test_login_admin_ok(self):
-        response = self.app.post(url(controller='login', action='index'),
-                                 {'username': TEST_USER_ADMIN_LOGIN,
-                                  'password': TEST_USER_ADMIN_PASS,
+        response = self.app.post(base.url(controller='login', action='index'),
+                                 {'username': base.TEST_USER_ADMIN_LOGIN,
+                                  'password': base.TEST_USER_ADMIN_PASS,
                                   '_session_csrf_secret_token': self.session_csrf_secret_token()})
         assert response.status == '302 Found'
-        self.assert_authenticated_user(response, TEST_USER_ADMIN_LOGIN)
+        self.assert_authenticated_user(response, base.TEST_USER_ADMIN_LOGIN)
 
         response = response.follow()
-        response.mustcontain('/%s' % HG_REPO)
+        response.mustcontain('/%s' % base.HG_REPO)
 
     def test_login_regular_ok(self):
-        response = self.app.post(url(controller='login', action='index'),
-                                 {'username': TEST_USER_REGULAR_LOGIN,
-                                  'password': TEST_USER_REGULAR_PASS,
+        response = self.app.post(base.url(controller='login', action='index'),
+                                 {'username': base.TEST_USER_REGULAR_LOGIN,
+                                  'password': base.TEST_USER_REGULAR_PASS,
                                   '_session_csrf_secret_token': self.session_csrf_secret_token()})
 
         assert response.status == '302 Found'
-        self.assert_authenticated_user(response, TEST_USER_REGULAR_LOGIN)
+        self.assert_authenticated_user(response, base.TEST_USER_REGULAR_LOGIN)
 
         response = response.follow()
-        response.mustcontain('/%s' % HG_REPO)
+        response.mustcontain('/%s' % base.HG_REPO)
 
     def test_login_regular_email_ok(self):
-        response = self.app.post(url(controller='login', action='index'),
-                                 {'username': TEST_USER_REGULAR_EMAIL,
-                                  'password': TEST_USER_REGULAR_PASS,
+        response = self.app.post(base.url(controller='login', action='index'),
+                                 {'username': base.TEST_USER_REGULAR_EMAIL,
+                                  'password': base.TEST_USER_REGULAR_PASS,
                                   '_session_csrf_secret_token': self.session_csrf_secret_token()})
 
         assert response.status == '302 Found'
-        self.assert_authenticated_user(response, TEST_USER_REGULAR_LOGIN)
+        self.assert_authenticated_user(response, base.TEST_USER_REGULAR_LOGIN)
 
         response = response.follow()
-        response.mustcontain('/%s' % HG_REPO)
+        response.mustcontain('/%s' % base.HG_REPO)
 
     def test_login_ok_came_from(self):
         test_came_from = '/_admin/users'
-        response = self.app.post(url(controller='login', action='index',
+        response = self.app.post(base.url(controller='login', action='index',
                                      came_from=test_came_from),
-                                 {'username': TEST_USER_ADMIN_LOGIN,
-                                  'password': TEST_USER_ADMIN_PASS,
+                                 {'username': base.TEST_USER_ADMIN_LOGIN,
+                                  'password': base.TEST_USER_ADMIN_PASS,
                                   '_session_csrf_secret_token': self.session_csrf_secret_token()})
         assert response.status == '302 Found'
         response = response.follow()
@@ -76,9 +78,9 @@
         response.mustcontain('Users Administration')
 
     def test_login_do_not_remember(self):
-        response = self.app.post(url(controller='login', action='index'),
-                                 {'username': TEST_USER_REGULAR_LOGIN,
-                                  'password': TEST_USER_REGULAR_PASS,
+        response = self.app.post(base.url(controller='login', action='index'),
+                                 {'username': base.TEST_USER_REGULAR_LOGIN,
+                                  'password': base.TEST_USER_REGULAR_PASS,
                                   'remember': False,
                                   '_session_csrf_secret_token': self.session_csrf_secret_token()})
 
@@ -87,9 +89,9 @@
             assert not re.search(r';\s+(Max-Age|Expires)=', cookie, re.IGNORECASE), 'Cookie %r has expiration date, but should be a session cookie' % cookie
 
     def test_login_remember(self):
-        response = self.app.post(url(controller='login', action='index'),
-                                 {'username': TEST_USER_REGULAR_LOGIN,
-                                  'password': TEST_USER_REGULAR_PASS,
+        response = self.app.post(base.url(controller='login', action='index'),
+                                 {'username': base.TEST_USER_REGULAR_LOGIN,
+                                  'password': base.TEST_USER_REGULAR_PASS,
                                   'remember': True,
                                   '_session_csrf_secret_token': self.session_csrf_secret_token()})
 
@@ -98,23 +100,23 @@
             assert re.search(r';\s+(Max-Age|Expires)=', cookie, re.IGNORECASE), 'Cookie %r should have expiration date, but is a session cookie' % cookie
 
     def test_logout(self):
-        response = self.app.post(url(controller='login', action='index'),
-                                 {'username': TEST_USER_REGULAR_LOGIN,
-                                  'password': TEST_USER_REGULAR_PASS,
+        response = self.app.post(base.url(controller='login', action='index'),
+                                 {'username': base.TEST_USER_REGULAR_LOGIN,
+                                  'password': base.TEST_USER_REGULAR_PASS,
                                   '_session_csrf_secret_token': self.session_csrf_secret_token()})
 
         # Verify that a login session has been established.
-        response = self.app.get(url(controller='login', action='index'))
+        response = self.app.get(base.url(controller='login', action='index'))
         response = response.follow()
         assert 'authuser' in response.session
 
         response.click('Log Out')
 
         # Verify that the login session has been terminated.
-        response = self.app.get(url(controller='login', action='index'))
+        response = self.app.get(base.url(controller='login', action='index'))
         assert 'authuser' not in response.session
 
-    @parametrize('url_came_from', [
+    @base.parametrize('url_came_from', [
           ('data:text/html,<script>window.alert("xss")</script>',),
           ('mailto:test@example.com',),
           ('file:///etc/passwd',),
@@ -126,16 +128,16 @@
           ('non-absolute-path',),
     ])
     def test_login_bad_came_froms(self, url_came_from):
-        response = self.app.post(url(controller='login', action='index',
+        response = self.app.post(base.url(controller='login', action='index',
                                      came_from=url_came_from),
-                                 {'username': TEST_USER_ADMIN_LOGIN,
-                                  'password': TEST_USER_ADMIN_PASS,
+                                 {'username': base.TEST_USER_ADMIN_LOGIN,
+                                  'password': base.TEST_USER_ADMIN_PASS,
                                   '_session_csrf_secret_token': self.session_csrf_secret_token()},
                                  status=400)
 
     def test_login_short_password(self):
-        response = self.app.post(url(controller='login', action='index'),
-                                 {'username': TEST_USER_ADMIN_LOGIN,
+        response = self.app.post(base.url(controller='login', action='index'),
+                                 {'username': base.TEST_USER_ADMIN_LOGIN,
                                   'password': 'as',
                                   '_session_csrf_secret_token': self.session_csrf_secret_token()})
         assert response.status == '200 OK'
@@ -143,7 +145,7 @@
         response.mustcontain('Enter 3 characters or more')
 
     def test_login_wrong_username_password(self):
-        response = self.app.post(url(controller='login', action='index'),
+        response = self.app.post(base.url(controller='login', action='index'),
                                  {'username': 'error',
                                   'password': 'test12',
                                   '_session_csrf_secret_token': self.session_csrf_secret_token()})
@@ -151,8 +153,8 @@
         response.mustcontain('Invalid username or password')
 
     def test_login_non_ascii(self):
-        response = self.app.post(url(controller='login', action='index'),
-                                 {'username': TEST_USER_REGULAR_LOGIN,
+        response = self.app.post(base.url(controller='login', action='index'),
+                                 {'username': base.TEST_USER_REGULAR_LOGIN,
                                   'password': 'blåbærgrød',
                                   '_session_csrf_secret_token': self.session_csrf_secret_token()})
 
@@ -160,63 +162,61 @@
 
     # verify that get arguments are correctly passed along login redirection
 
-    @parametrize('args,args_encoded', [
-        ({'foo':'one', 'bar':'two'}, (('foo', 'one'), ('bar', 'two'))),
-        ({'blue': u'blå'.encode('utf-8'), 'green':u'grøn'},
-             (('blue', u'blå'.encode('utf-8')), ('green', u'grøn'.encode('utf-8')))),
+    @base.parametrize('args', [
+        {'foo':'one', 'bar':'two'},
+        {'blue': 'blå', 'green': 'grøn'},
     ])
-    def test_redirection_to_login_form_preserves_get_args(self, args, args_encoded):
+    def test_redirection_to_login_form_preserves_get_args(self, args):
         with fixture.anon_access(False):
-            response = self.app.get(url(controller='summary', action='index',
-                                        repo_name=HG_REPO,
+            response = self.app.get(base.url(controller='summary', action='index',
+                                        repo_name=base.HG_REPO,
                                         **args))
             assert response.status == '302 Found'
-            came_from = urlparse.parse_qs(urlparse.urlparse(response.location).query)['came_from'][0]
-            came_from_qs = urlparse.parse_qsl(urlparse.urlparse(came_from).query)
-            for encoded in args_encoded:
-                assert encoded in came_from_qs
+            came_from = urllib.parse.parse_qs(urllib.parse.urlparse(response.location).query)['came_from'][0]
+            came_from_qs = urllib.parse.parse_qsl(urllib.parse.urlparse(came_from).query)
+            assert sorted(came_from_qs) == sorted(args.items())
 
-    @parametrize('args,args_encoded', [
+    @base.parametrize('args,args_encoded', [
         ({'foo':'one', 'bar':'two'}, ('foo=one', 'bar=two')),
-        ({'blue': u'blå', 'green':u'grøn'},
+        ({'blue': 'blå', 'green':'grøn'},
              ('blue=bl%C3%A5', 'green=gr%C3%B8n')),
     ])
     def test_login_form_preserves_get_args(self, args, args_encoded):
-        response = self.app.get(url(controller='login', action='index',
-                                    came_from=url('/_admin/users', **args)))
-        came_from = urlparse.parse_qs(urlparse.urlparse(response.form.action).query)['came_from'][0]
+        response = self.app.get(base.url(controller='login', action='index',
+                                    came_from=base.url('/_admin/users', **args)))
+        came_from = urllib.parse.parse_qs(urllib.parse.urlparse(response.form.action).query)['came_from'][0]
         for encoded in args_encoded:
             assert encoded in came_from
 
-    @parametrize('args,args_encoded', [
+    @base.parametrize('args,args_encoded', [
         ({'foo':'one', 'bar':'two'}, ('foo=one', 'bar=two')),
-        ({'blue': u'blå', 'green':u'grøn'},
+        ({'blue': 'blå', 'green':'grøn'},
              ('blue=bl%C3%A5', 'green=gr%C3%B8n')),
     ])
     def test_redirection_after_successful_login_preserves_get_args(self, args, args_encoded):
-        response = self.app.post(url(controller='login', action='index',
-                                     came_from=url('/_admin/users', **args)),
-                                 {'username': TEST_USER_ADMIN_LOGIN,
-                                  'password': TEST_USER_ADMIN_PASS,
+        response = self.app.post(base.url(controller='login', action='index',
+                                     came_from=base.url('/_admin/users', **args)),
+                                 {'username': base.TEST_USER_ADMIN_LOGIN,
+                                  'password': base.TEST_USER_ADMIN_PASS,
                                   '_session_csrf_secret_token': self.session_csrf_secret_token()})
         assert response.status == '302 Found'
         for encoded in args_encoded:
             assert encoded in response.location
 
-    @parametrize('args,args_encoded', [
+    @base.parametrize('args,args_encoded', [
         ({'foo':'one', 'bar':'two'}, ('foo=one', 'bar=two')),
-        ({'blue': u'blå', 'green':u'grøn'},
+        ({'blue': 'blå', 'green':'grøn'},
              ('blue=bl%C3%A5', 'green=gr%C3%B8n')),
     ])
     def test_login_form_after_incorrect_login_preserves_get_args(self, args, args_encoded):
-        response = self.app.post(url(controller='login', action='index',
-                                     came_from=url('/_admin/users', **args)),
+        response = self.app.post(base.url(controller='login', action='index',
+                                     came_from=base.url('/_admin/users', **args)),
                                  {'username': 'error',
                                   'password': 'test12',
                                   '_session_csrf_secret_token': self.session_csrf_secret_token()})
 
         response.mustcontain('Invalid username or password')
-        came_from = urlparse.parse_qs(urlparse.urlparse(response.form.action).query)['came_from'][0]
+        came_from = urllib.parse.parse_qs(urllib.parse.urlparse(response.form.action).query)['came_from'][0]
         for encoded in args_encoded:
             assert encoded in came_from
 
@@ -224,12 +224,12 @@
     # REGISTRATIONS
     #==========================================================================
     def test_register(self):
-        response = self.app.get(url(controller='login', action='register'))
+        response = self.app.get(base.url(controller='login', action='register'))
         response.mustcontain('Sign Up')
 
     def test_register_err_same_username(self):
-        uname = TEST_USER_ADMIN_LOGIN
-        response = self.app.post(url(controller='login', action='register'),
+        uname = base.TEST_USER_ADMIN_LOGIN
+        response = self.app.post(base.url(controller='login', action='register'),
                                             {'username': uname,
                                              'password': 'test12',
                                              'password_confirmation': 'test12',
@@ -244,11 +244,11 @@
         response.mustcontain(msg)
 
     def test_register_err_same_email(self):
-        response = self.app.post(url(controller='login', action='register'),
+        response = self.app.post(base.url(controller='login', action='register'),
                                             {'username': 'test_admin_0',
                                              'password': 'test12',
                                              'password_confirmation': 'test12',
-                                             'email': TEST_USER_ADMIN_EMAIL,
+                                             'email': base.TEST_USER_ADMIN_EMAIL,
                                              'firstname': 'test',
                                              'lastname': 'test',
                                              '_session_csrf_secret_token': self.session_csrf_secret_token()})
@@ -258,11 +258,11 @@
         response.mustcontain(msg)
 
     def test_register_err_same_email_case_sensitive(self):
-        response = self.app.post(url(controller='login', action='register'),
+        response = self.app.post(base.url(controller='login', action='register'),
                                             {'username': 'test_admin_1',
                                              'password': 'test12',
                                              'password_confirmation': 'test12',
-                                             'email': TEST_USER_ADMIN_EMAIL.title(),
+                                             'email': base.TEST_USER_ADMIN_EMAIL.title(),
                                              'firstname': 'test',
                                              'lastname': 'test',
                                              '_session_csrf_secret_token': self.session_csrf_secret_token()})
@@ -271,7 +271,7 @@
         response.mustcontain(msg)
 
     def test_register_err_wrong_data(self):
-        response = self.app.post(url(controller='login', action='register'),
+        response = self.app.post(base.url(controller='login', action='register'),
                                             {'username': 'xs',
                                              'password': 'test',
                                              'password_confirmation': 'test',
@@ -284,7 +284,7 @@
         response.mustcontain('Enter a value 6 characters long or more')
 
     def test_register_err_username(self):
-        response = self.app.post(url(controller='login', action='register'),
+        response = self.app.post(base.url(controller='login', action='register'),
                                             {'username': 'error user',
                                              'password': 'test12',
                                              'password_confirmation': 'test12',
@@ -300,8 +300,8 @@
                 'alphanumeric character')
 
     def test_register_err_case_sensitive(self):
-        usr = TEST_USER_ADMIN_LOGIN.title()
-        response = self.app.post(url(controller='login', action='register'),
+        usr = base.TEST_USER_ADMIN_LOGIN.title()
+        response = self.app.post(base.url(controller='login', action='register'),
                                             {'username': usr,
                                              'password': 'test12',
                                              'password_confirmation': 'test12',
@@ -317,7 +317,7 @@
         response.mustcontain(msg)
 
     def test_register_special_chars(self):
-        response = self.app.post(url(controller='login', action='register'),
+        response = self.app.post(base.url(controller='login', action='register'),
                                         {'username': 'xxxaxn',
                                          'password': 'ąćźżąśśśś',
                                          'password_confirmation': 'ąćźżąśśśś',
@@ -331,7 +331,7 @@
         response.mustcontain(msg)
 
     def test_register_password_mismatch(self):
-        response = self.app.post(url(controller='login', action='register'),
+        response = self.app.post(base.url(controller='login', action='register'),
                                             {'username': 'xs',
                                              'password': '123qwe',
                                              'password_confirmation': 'qwe123',
@@ -350,7 +350,7 @@
         name = 'testname'
         lastname = 'testlastname'
 
-        response = self.app.post(url(controller='login', action='register'),
+        response = self.app.post(base.url(controller='login', action='register'),
                                             {'username': username,
                                              'password': password,
                                              'password_confirmation': password,
@@ -378,22 +378,22 @@
     def test_forgot_password_wrong_mail(self):
         bad_email = 'username%wrongmail.org'
         response = self.app.post(
-                        url(controller='login', action='password_reset'),
+                        base.url(controller='login', action='password_reset'),
                             {'email': bad_email,
                              '_session_csrf_secret_token': self.session_csrf_secret_token()})
 
         response.mustcontain('An email address must contain a single @')
 
     def test_forgot_password(self):
-        response = self.app.get(url(controller='login',
+        response = self.app.get(base.url(controller='login',
                                     action='password_reset'))
         assert response.status == '200 OK'
 
         username = 'test_password_reset_1'
         password = 'qweqwe'
         email = 'username@example.com'
-        name = u'passwd'
-        lastname = u'reset'
+        name = 'passwd'
+        lastname = 'reset'
         timestamp = int(time.time())
 
         new = User()
@@ -406,26 +406,47 @@
         Session().add(new)
         Session().commit()
 
-        response = self.app.post(url(controller='login',
-                                     action='password_reset'),
-                                 {'email': email,
-                                  '_session_csrf_secret_token': self.session_csrf_secret_token()})
+        token = UserModel().get_reset_password_token(
+            User.get_by_username(username), timestamp, self.session_csrf_secret_token())
+
+        collected = []
+        def mock_send_email(recipients, subject, body='', html_body='', headers=None, from_name=None):
+            collected.append((recipients, subject, body, html_body))
+
+        with mock.patch.object(kallithea.lib.celerylib.tasks, 'send_email', mock_send_email), \
+                mock.patch.object(time, 'time', lambda: timestamp):
+            response = self.app.post(base.url(controller='login',
+                                         action='password_reset'),
+                                     {'email': email,
+                                      '_session_csrf_secret_token': self.session_csrf_secret_token()})
 
         self.checkSessionFlash(response, 'A password reset confirmation code has been sent')
 
+        ((recipients, subject, body, html_body),) = collected
+        assert recipients == ['username@example.com']
+        assert subject == 'Password reset link'
+        assert '\n%s\n' % token in body
+        (confirmation_url,) = (line for line in body.splitlines() if line.startswith('http://'))
+        assert ' href="%s"' % confirmation_url.replace('&', '&amp;').replace('@', '%40') in html_body
+
+        d = urllib.parse.parse_qs(urllib.parse.urlparse(confirmation_url).query)
+        assert d['token'] == [token]
+        assert d['timestamp'] == [str(timestamp)]
+        assert d['email'] == [email]
+
         response = response.follow()
 
         # BAD TOKEN
 
-        token = "bad"
+        bad_token = "bad"
 
-        response = self.app.post(url(controller='login',
+        response = self.app.post(base.url(controller='login',
                                      action='password_reset_confirmation'),
                                  {'email': email,
                                   'timestamp': timestamp,
                                   'password': "p@ssw0rd",
                                   'password_confirm': "p@ssw0rd",
-                                  'token': token,
+                                  'token': bad_token,
                                   '_session_csrf_secret_token': self.session_csrf_secret_token(),
                                  })
         assert response.status == '200 OK'
@@ -433,21 +454,17 @@
 
         # GOOD TOKEN
 
-        # TODO: The token should ideally be taken from the mail sent
-        # above, instead of being recalculated.
-
-        token = UserModel().get_reset_password_token(
-            User.get_by_username(username), timestamp, self.session_csrf_secret_token())
-
-        response = self.app.get(url(controller='login',
-                                    action='password_reset_confirmation',
-                                    email=email,
-                                    timestamp=timestamp,
-                                    token=token))
+        response = self.app.get(confirmation_url)
         assert response.status == '200 OK'
         response.mustcontain("You are about to set a new password for the email address %s" % email)
+        response.mustcontain('<form action="%s" method="post">' % base.url(controller='login', action='password_reset_confirmation'))
+        response.mustcontain('value="%s"' % self.session_csrf_secret_token())
+        response.mustcontain('value="%s"' % token)
+        response.mustcontain('value="%s"' % timestamp)
+        response.mustcontain('value="username@example.com"')
 
-        response = self.app.post(url(controller='login',
+        # fake a submit of that form
+        response = self.app.post(base.url(controller='login',
                                      action='password_reset_confirmation'),
                                  {'email': email,
                                   'timestamp': timestamp,
@@ -483,16 +500,16 @@
                 params = {'api_key': api_key}
                 headers = {'Authorization': 'Bearer ' + str(api_key)}
 
-            self.app.get(url(controller='changeset', action='changeset_raw',
-                             repo_name=HG_REPO, revision='tip', **params),
+            self.app.get(base.url(controller='changeset', action='changeset_raw',
+                             repo_name=base.HG_REPO, revision='tip', **params),
                          status=status)
 
-            self.app.get(url(controller='changeset', action='changeset_raw',
-                             repo_name=HG_REPO, revision='tip'),
+            self.app.get(base.url(controller='changeset', action='changeset_raw',
+                             repo_name=base.HG_REPO, revision='tip'),
                          headers=headers,
                          status=status)
 
-    @parametrize('test_name,api_key,code', [
+    @base.parametrize('test_name,api_key,code', [
         ('none', None, 302),
         ('empty_string', '', 403),
         ('fake_number', '123456', 403),
@@ -504,12 +521,12 @@
         self._api_key_test(api_key, code)
 
     def test_access_page_via_extra_api_key(self):
-        new_api_key = ApiKeyModel().create(TEST_USER_ADMIN_LOGIN, u'test')
+        new_api_key = ApiKeyModel().create(base.TEST_USER_ADMIN_LOGIN, 'test')
         Session().commit()
         self._api_key_test(new_api_key.api_key, status=200)
 
     def test_access_page_via_expired_api_key(self):
-        new_api_key = ApiKeyModel().create(TEST_USER_ADMIN_LOGIN, u'test')
+        new_api_key = ApiKeyModel().create(base.TEST_USER_ADMIN_LOGIN, 'test')
         Session().commit()
         # patch the API key and make it expired
         new_api_key.expires = 0
--- a/kallithea/tests/functional/test_my_account.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_my_account.py	Mon May 04 19:24:04 2020 +0200
@@ -6,14 +6,14 @@
 from kallithea.model.db import Repository, User, UserApiKeys, UserFollowing, UserSshKeys
 from kallithea.model.meta import Session
 from kallithea.model.user import UserModel
-from kallithea.tests.base import *
+from kallithea.tests import base
 from kallithea.tests.fixture import Fixture
 
 
 fixture = Fixture()
 
 
-class TestMyAccountController(TestController):
+class TestMyAccountController(base.TestController):
     test_user_1 = 'testme'
 
     @classmethod
@@ -24,74 +24,74 @@
 
     def test_my_account(self):
         self.log_user()
-        response = self.app.get(url('my_account'))
+        response = self.app.get(base.url('my_account'))
 
-        response.mustcontain('value="%s' % TEST_USER_ADMIN_LOGIN)
+        response.mustcontain('value="%s' % base.TEST_USER_ADMIN_LOGIN)
 
     def test_my_account_my_repos(self):
         self.log_user()
-        response = self.app.get(url('my_account_repos'))
+        response = self.app.get(base.url('my_account_repos'))
         cnt = Repository.query().filter(Repository.owner ==
-                           User.get_by_username(TEST_USER_ADMIN_LOGIN)).count()
-        response.mustcontain('"raw_name": "%s"' % HG_REPO)
-        response.mustcontain('"just_name": "%s"' % GIT_REPO)
+                           User.get_by_username(base.TEST_USER_ADMIN_LOGIN)).count()
+        response.mustcontain('"raw_name": "%s"' % base.HG_REPO)
+        response.mustcontain('"just_name": "%s"' % base.GIT_REPO)
 
     def test_my_account_my_watched(self):
         self.log_user()
-        response = self.app.get(url('my_account_watched'))
+        response = self.app.get(base.url('my_account_watched'))
 
         cnt = UserFollowing.query().filter(UserFollowing.user ==
-                            User.get_by_username(TEST_USER_ADMIN_LOGIN)).count()
-        response.mustcontain('"raw_name": "%s"' % HG_REPO)
-        response.mustcontain('"just_name": "%s"' % GIT_REPO)
+                            User.get_by_username(base.TEST_USER_ADMIN_LOGIN)).count()
+        response.mustcontain('"raw_name": "%s"' % base.HG_REPO)
+        response.mustcontain('"just_name": "%s"' % base.GIT_REPO)
 
     def test_my_account_my_emails(self):
         self.log_user()
-        response = self.app.get(url('my_account_emails'))
+        response = self.app.get(base.url('my_account_emails'))
         response.mustcontain('No additional emails specified')
 
     def test_my_account_my_emails_add_existing_email(self):
         self.log_user()
-        response = self.app.get(url('my_account_emails'))
+        response = self.app.get(base.url('my_account_emails'))
         response.mustcontain('No additional emails specified')
-        response = self.app.post(url('my_account_emails'),
-                                 {'new_email': TEST_USER_REGULAR_EMAIL, '_session_csrf_secret_token': self.session_csrf_secret_token()})
+        response = self.app.post(base.url('my_account_emails'),
+                                 {'new_email': base.TEST_USER_REGULAR_EMAIL, '_session_csrf_secret_token': self.session_csrf_secret_token()})
         self.checkSessionFlash(response, 'This email address is already in use')
 
     def test_my_account_my_emails_add_missing_email_in_form(self):
         self.log_user()
-        response = self.app.get(url('my_account_emails'))
+        response = self.app.get(base.url('my_account_emails'))
         response.mustcontain('No additional emails specified')
-        response = self.app.post(url('my_account_emails'),
+        response = self.app.post(base.url('my_account_emails'),
             {'_session_csrf_secret_token': self.session_csrf_secret_token()})
         self.checkSessionFlash(response, 'Please enter an email address')
 
     def test_my_account_my_emails_add_remove(self):
         self.log_user()
-        response = self.app.get(url('my_account_emails'))
+        response = self.app.get(base.url('my_account_emails'))
         response.mustcontain('No additional emails specified')
 
-        response = self.app.post(url('my_account_emails'),
+        response = self.app.post(base.url('my_account_emails'),
                                  {'new_email': 'barz@example.com', '_session_csrf_secret_token': self.session_csrf_secret_token()})
 
-        response = self.app.get(url('my_account_emails'))
+        response = self.app.get(base.url('my_account_emails'))
 
         from kallithea.model.db import UserEmailMap
         email_id = UserEmailMap.query() \
-            .filter(UserEmailMap.user == User.get_by_username(TEST_USER_ADMIN_LOGIN)) \
+            .filter(UserEmailMap.user == User.get_by_username(base.TEST_USER_ADMIN_LOGIN)) \
             .filter(UserEmailMap.email == 'barz@example.com').one().email_id
 
         response.mustcontain('barz@example.com')
         response.mustcontain('<input id="del_email_id" name="del_email_id" type="hidden" value="%s" />' % email_id)
 
-        response = self.app.post(url('my_account_emails_delete'),
+        response = self.app.post(base.url('my_account_emails_delete'),
                                  {'del_email_id': email_id, '_session_csrf_secret_token': self.session_csrf_secret_token()})
         self.checkSessionFlash(response, 'Removed email from user')
-        response = self.app.get(url('my_account_emails'))
+        response = self.app.get(base.url('my_account_emails'))
         response.mustcontain('No additional emails specified')
 
 
-    @parametrize('name,attrs',
+    @base.parametrize('name,attrs',
         [('firstname', {'firstname': 'new_username'}),
          ('lastname', {'lastname': 'new_username'}),
          ('admin', {'admin': True}),
@@ -123,7 +123,7 @@
         params.update({'_session_csrf_secret_token': self.session_csrf_secret_token()})
 
         params.update(attrs)
-        response = self.app.post(url('my_account'), params)
+        response = self.app.post(base.url('my_account'), params)
 
         self.checkSessionFlash(response,
                                'Your account was updated successfully')
@@ -155,14 +155,14 @@
     def test_my_account_update_err_email_exists(self):
         self.log_user()
 
-        new_email = TEST_USER_REGULAR_EMAIL  # already existing email
-        response = self.app.post(url('my_account'),
+        new_email = base.TEST_USER_REGULAR_EMAIL  # already existing email
+        response = self.app.post(base.url('my_account'),
                                 params=dict(
-                                    username=TEST_USER_ADMIN_LOGIN,
-                                    new_password=TEST_USER_ADMIN_PASS,
+                                    username=base.TEST_USER_ADMIN_LOGIN,
+                                    new_password=base.TEST_USER_ADMIN_PASS,
                                     password_confirmation='test122',
-                                    firstname=u'NewName',
-                                    lastname=u'NewLastname',
+                                    firstname='NewName',
+                                    lastname='NewLastname',
                                     email=new_email,
                                     _session_csrf_secret_token=self.session_csrf_secret_token())
                                 )
@@ -170,16 +170,16 @@
         response.mustcontain('This email address is already in use')
 
     def test_my_account_update_err(self):
-        self.log_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS)
+        self.log_user(base.TEST_USER_REGULAR2_LOGIN, base.TEST_USER_REGULAR2_PASS)
 
         new_email = 'newmail.pl'
-        response = self.app.post(url('my_account'),
+        response = self.app.post(base.url('my_account'),
                                  params=dict(
-                                            username=TEST_USER_ADMIN_LOGIN,
-                                            new_password=TEST_USER_ADMIN_PASS,
+                                            username=base.TEST_USER_ADMIN_LOGIN,
+                                            new_password=base.TEST_USER_ADMIN_PASS,
                                             password_confirmation='test122',
-                                            firstname=u'NewName',
-                                            lastname=u'NewLastname',
+                                            firstname='NewName',
+                                            lastname='NewLastname',
                                             email=new_email,
                                             _session_csrf_secret_token=self.session_csrf_secret_token()))
 
@@ -188,25 +188,25 @@
         with test_context(self.app):
             msg = validators.ValidUsername(edit=False, old_data={}) \
                     ._messages['username_exists']
-        msg = h.html_escape(msg % {'username': TEST_USER_ADMIN_LOGIN})
+        msg = h.html_escape(msg % {'username': base.TEST_USER_ADMIN_LOGIN})
         response.mustcontain(msg)
 
     def test_my_account_api_keys(self):
-        usr = self.log_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS)
+        usr = self.log_user(base.TEST_USER_REGULAR2_LOGIN, base.TEST_USER_REGULAR2_PASS)
         user = User.get(usr['user_id'])
-        response = self.app.get(url('my_account_api_keys'))
+        response = self.app.get(base.url('my_account_api_keys'))
         response.mustcontain(user.api_key)
         response.mustcontain('Expires: Never')
 
-    @parametrize('desc,lifetime', [
+    @base.parametrize('desc,lifetime', [
         ('forever', -1),
         ('5mins', 60*5),
         ('30days', 60*60*24*30),
     ])
     def test_my_account_add_api_keys(self, desc, lifetime):
-        usr = self.log_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS)
+        usr = self.log_user(base.TEST_USER_REGULAR2_LOGIN, base.TEST_USER_REGULAR2_PASS)
         user = User.get(usr['user_id'])
-        response = self.app.post(url('my_account_api_keys'),
+        response = self.app.post(base.url('my_account_api_keys'),
                                  {'description': desc, 'lifetime': lifetime, '_session_csrf_secret_token': self.session_csrf_secret_token()})
         self.checkSessionFlash(response, 'API key successfully created')
         try:
@@ -220,9 +220,9 @@
                 Session().commit()
 
     def test_my_account_remove_api_key(self):
-        usr = self.log_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS)
+        usr = self.log_user(base.TEST_USER_REGULAR2_LOGIN, base.TEST_USER_REGULAR2_PASS)
         user = User.get(usr['user_id'])
-        response = self.app.post(url('my_account_api_keys'),
+        response = self.app.post(base.url('my_account_api_keys'),
                                  {'description': 'desc', 'lifetime': -1, '_session_csrf_secret_token': self.session_csrf_secret_token()})
         self.checkSessionFlash(response, 'API key successfully created')
         response = response.follow()
@@ -231,33 +231,33 @@
         keys = UserApiKeys.query().all()
         assert 1 == len(keys)
 
-        response = self.app.post(url('my_account_api_keys_delete'),
+        response = self.app.post(base.url('my_account_api_keys_delete'),
                  {'del_api_key': keys[0].api_key, '_session_csrf_secret_token': self.session_csrf_secret_token()})
         self.checkSessionFlash(response, 'API key successfully deleted')
         keys = UserApiKeys.query().all()
         assert 0 == len(keys)
 
     def test_my_account_reset_main_api_key(self):
-        usr = self.log_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS)
+        usr = self.log_user(base.TEST_USER_REGULAR2_LOGIN, base.TEST_USER_REGULAR2_PASS)
         user = User.get(usr['user_id'])
         api_key = user.api_key
-        response = self.app.get(url('my_account_api_keys'))
+        response = self.app.get(base.url('my_account_api_keys'))
         response.mustcontain(api_key)
         response.mustcontain('Expires: Never')
 
-        response = self.app.post(url('my_account_api_keys_delete'),
+        response = self.app.post(base.url('my_account_api_keys_delete'),
                  {'del_api_key_builtin': api_key, '_session_csrf_secret_token': self.session_csrf_secret_token()})
         self.checkSessionFlash(response, 'API key successfully reset')
         response = response.follow()
         response.mustcontain(no=[api_key])
 
     def test_my_account_add_ssh_key(self):
-        description = u'something'
-        public_key = u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6Ycnc2oUZHQnQwuqgZqTTdMDZD7ataf3JM7oG2Fw8JR6cdmz4QZLe5mfDwaFwG2pWHLRpVqzfrD/Pn3rIO++bgCJH5ydczrl1WScfryV1hYMJ/4EzLGM657J1/q5EI+b9SntKjf4ax+KP322L0TNQGbZUHLbfG2MwHMrYBQpHUQ== me@localhost'
-        fingerprint = u'Ke3oUCNJM87P0jJTb3D+e3shjceP2CqMpQKVd75E9I8'
+        description = 'something'
+        public_key = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6Ycnc2oUZHQnQwuqgZqTTdMDZD7ataf3JM7oG2Fw8JR6cdmz4QZLe5mfDwaFwG2pWHLRpVqzfrD/Pn3rIO++bgCJH5ydczrl1WScfryV1hYMJ/4EzLGM657J1/q5EI+b9SntKjf4ax+KP322L0TNQGbZUHLbfG2MwHMrYBQpHUQ== me@localhost'
+        fingerprint = 'Ke3oUCNJM87P0jJTb3D+e3shjceP2CqMpQKVd75E9I8'
 
-        self.log_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS)
-        response = self.app.post(url('my_account_ssh_keys'),
+        self.log_user(base.TEST_USER_REGULAR2_LOGIN, base.TEST_USER_REGULAR2_PASS)
+        response = self.app.post(base.url('my_account_ssh_keys'),
                                  {'description': description,
                                   'public_key': public_key,
                                   '_session_csrf_secret_token': self.session_csrf_secret_token()})
@@ -273,12 +273,12 @@
         Session().commit()
 
     def test_my_account_remove_ssh_key(self):
-        description = u''
-        public_key = u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6Ycnc2oUZHQnQwuqgZqTTdMDZD7ataf3JM7oG2Fw8JR6cdmz4QZLe5mfDwaFwG2pWHLRpVqzfrD/Pn3rIO++bgCJH5ydczrl1WScfryV1hYMJ/4EzLGM657J1/q5EI+b9SntKjf4ax+KP322L0TNQGbZUHLbfG2MwHMrYBQpHUQ== me@localhost'
-        fingerprint = u'Ke3oUCNJM87P0jJTb3D+e3shjceP2CqMpQKVd75E9I8'
+        description = ''
+        public_key = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6Ycnc2oUZHQnQwuqgZqTTdMDZD7ataf3JM7oG2Fw8JR6cdmz4QZLe5mfDwaFwG2pWHLRpVqzfrD/Pn3rIO++bgCJH5ydczrl1WScfryV1hYMJ/4EzLGM657J1/q5EI+b9SntKjf4ax+KP322L0TNQGbZUHLbfG2MwHMrYBQpHUQ== me@localhost'
+        fingerprint = 'Ke3oUCNJM87P0jJTb3D+e3shjceP2CqMpQKVd75E9I8'
 
-        self.log_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS)
-        response = self.app.post(url('my_account_ssh_keys'),
+        self.log_user(base.TEST_USER_REGULAR2_LOGIN, base.TEST_USER_REGULAR2_PASS)
+        response = self.app.post(base.url('my_account_ssh_keys'),
                                  {'description': description,
                                   'public_key': public_key,
                                   '_session_csrf_secret_token': self.session_csrf_secret_token()})
@@ -286,10 +286,10 @@
         response.follow()
         user_id = response.session['authuser']['user_id']
         ssh_key = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).one()
-        assert ssh_key.description == u'me@localhost'
+        assert ssh_key.description == 'me@localhost'
 
-        response = self.app.post(url('my_account_ssh_keys_delete'),
-                                 {'del_public_key': ssh_key.public_key,
+        response = self.app.post(base.url('my_account_ssh_keys_delete'),
+                                 {'del_public_key_fingerprint': ssh_key.fingerprint,
                                   '_session_csrf_secret_token': self.session_csrf_secret_token()})
         self.checkSessionFlash(response, 'SSH key successfully deleted')
         keys = UserSshKeys.query().all()
--- a/kallithea/tests/functional/test_pullrequests.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_pullrequests.py	Mon May 04 19:24:04 2020 +0200
@@ -5,27 +5,27 @@
 from kallithea.controllers.pullrequests import PullrequestsController
 from kallithea.model.db import PullRequest, User
 from kallithea.model.meta import Session
-from kallithea.tests.base import *
+from kallithea.tests import base
 from kallithea.tests.fixture import Fixture
 
 
 fixture = Fixture()
 
 
-class TestPullrequestsController(TestController):
+class TestPullrequestsController(base.TestController):
 
     def test_index(self):
         self.log_user()
-        response = self.app.get(url(controller='pullrequests', action='index',
-                                    repo_name=HG_REPO))
+        response = self.app.get(base.url(controller='pullrequests', action='index',
+                                    repo_name=base.HG_REPO))
 
     def test_create_trivial(self):
         self.log_user()
-        response = self.app.post(url(controller='pullrequests', action='create',
-                                     repo_name=HG_REPO),
-                                 {'org_repo': HG_REPO,
+        response = self.app.post(base.url(controller='pullrequests', action='create',
+                                     repo_name=base.HG_REPO),
+                                 {'org_repo': base.HG_REPO,
                                   'org_ref': 'branch:stable:4f7e2131323e0749a740c0a56ab68ae9269c562a',
-                                  'other_repo': HG_REPO,
+                                  'other_repo': base.HG_REPO,
                                   'other_ref': 'branch:default:96507bd11ecc815ebc6270fdf6db110928c09c1e',
                                   'pullrequest_title': 'title',
                                   'pullrequest_desc': 'description',
@@ -40,11 +40,11 @@
 
     def test_available(self):
         self.log_user()
-        response = self.app.post(url(controller='pullrequests', action='create',
-                                     repo_name=HG_REPO),
-                                 {'org_repo': HG_REPO,
+        response = self.app.post(base.url(controller='pullrequests', action='create',
+                                     repo_name=base.HG_REPO),
+                                 {'org_repo': base.HG_REPO,
                                   'org_ref': 'rev:94f45ed825a1:94f45ed825a113e61af7e141f44ca578374abef0',
-                                  'other_repo': HG_REPO,
+                                  'other_repo': base.HG_REPO,
                                   'other_ref': 'branch:default:96507bd11ecc815ebc6270fdf6db110928c09c1e',
                                   'pullrequest_title': 'title',
                                   'pullrequest_desc': 'description',
@@ -60,11 +60,11 @@
 
     def test_range(self):
         self.log_user()
-        response = self.app.post(url(controller='pullrequests', action='create',
-                                     repo_name=HG_REPO),
-                                 {'org_repo': HG_REPO,
+        response = self.app.post(base.url(controller='pullrequests', action='create',
+                                     repo_name=base.HG_REPO),
+                                 {'org_repo': base.HG_REPO,
                                   'org_ref': 'branch:stable:4f7e2131323e0749a740c0a56ab68ae9269c562a',
-                                  'other_repo': HG_REPO,
+                                  'other_repo': base.HG_REPO,
                                   'other_ref': 'rev:94f45ed825a1:94f45ed825a113e61af7e141f44ca578374abef0',
                                   'pullrequest_title': 'title',
                                   'pullrequest_desc': 'description',
@@ -78,57 +78,57 @@
 
     def test_update_reviewers(self):
         self.log_user()
-        regular_user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
-        regular_user2 = User.get_by_username(TEST_USER_REGULAR2_LOGIN)
-        admin_user = User.get_by_username(TEST_USER_ADMIN_LOGIN)
+        regular_user = User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
+        regular_user2 = User.get_by_username(base.TEST_USER_REGULAR2_LOGIN)
+        admin_user = User.get_by_username(base.TEST_USER_ADMIN_LOGIN)
 
         # create initial PR
-        response = self.app.post(url(controller='pullrequests', action='create',
-                                     repo_name=HG_REPO),
-                                 {'org_repo': HG_REPO,
+        response = self.app.post(base.url(controller='pullrequests', action='create',
+                                     repo_name=base.HG_REPO),
+                                 {'org_repo': base.HG_REPO,
                                   'org_ref': 'rev:94f45ed825a1:94f45ed825a113e61af7e141f44ca578374abef0',
-                                  'other_repo': HG_REPO,
+                                  'other_repo': base.HG_REPO,
                                   'other_ref': 'branch:default:96507bd11ecc815ebc6270fdf6db110928c09c1e',
                                   'pullrequest_title': 'title',
                                   'pullrequest_desc': 'description',
                                   '_session_csrf_secret_token': self.session_csrf_secret_token(),
                                  },
                                  status=302)
-        pull_request1_id = re.search('/pull-request/(\d+)/', response.location).group(1)
-        assert response.location == 'http://localhost/%s/pull-request/%s/_/stable' % (HG_REPO, pull_request1_id)
+        pull_request1_id = re.search(r'/pull-request/(\d+)/', response.location).group(1)
+        assert response.location == 'http://localhost/%s/pull-request/%s/_/stable' % (base.HG_REPO, pull_request1_id)
 
         # create new iteration
-        response = self.app.post(url(controller='pullrequests', action='post',
-                                     repo_name=HG_REPO, pull_request_id=pull_request1_id),
+        response = self.app.post(base.url(controller='pullrequests', action='post',
+                                     repo_name=base.HG_REPO, pull_request_id=pull_request1_id),
                                  {
                                   'updaterev': '4f7e2131323e0749a740c0a56ab68ae9269c562a',
                                   'pullrequest_title': 'title',
                                   'pullrequest_desc': 'description',
-                                  'owner': TEST_USER_ADMIN_LOGIN,
+                                  'owner': base.TEST_USER_ADMIN_LOGIN,
                                   '_session_csrf_secret_token': self.session_csrf_secret_token(),
                                   'review_members': [regular_user.user_id],
                                  },
                                  status=302)
-        pull_request2_id = re.search('/pull-request/(\d+)/', response.location).group(1)
+        pull_request2_id = re.search(r'/pull-request/(\d+)/', response.location).group(1)
         assert pull_request2_id != pull_request1_id
-        assert response.location == 'http://localhost/%s/pull-request/%s/_/stable' % (HG_REPO, pull_request2_id)
+        assert response.location == 'http://localhost/%s/pull-request/%s/_/stable' % (base.HG_REPO, pull_request2_id)
         response = response.follow()
         # verify reviewer was added
         response.mustcontain('<input type="hidden" value="%s" name="review_members" />' % regular_user.user_id)
 
         # update without creating new iteration
-        response = self.app.post(url(controller='pullrequests', action='post',
-                                     repo_name=HG_REPO, pull_request_id=pull_request2_id),
+        response = self.app.post(base.url(controller='pullrequests', action='post',
+                                     repo_name=base.HG_REPO, pull_request_id=pull_request2_id),
                                  {
                                   'pullrequest_title': 'Title',
                                   'pullrequest_desc': 'description',
-                                  'owner': TEST_USER_ADMIN_LOGIN,
+                                  'owner': base.TEST_USER_ADMIN_LOGIN,
                                   '_session_csrf_secret_token': self.session_csrf_secret_token(),
                                   'org_review_members': [admin_user.user_id], # fake - just to get some 'meanwhile' warning ... but it is also added ...
                                   'review_members': [regular_user2.user_id, admin_user.user_id],
                                  },
                                  status=302)
-        assert response.location == 'http://localhost/%s/pull-request/%s/_/stable' % (HG_REPO, pull_request2_id)
+        assert response.location == 'http://localhost/%s/pull-request/%s/_/stable' % (base.HG_REPO, pull_request2_id)
         response = response.follow()
         # verify reviewers were added / removed
         response.mustcontain('Meanwhile, the following reviewers have been added: test_regular')
@@ -141,12 +141,12 @@
         invalid_user_id = 99999
         self.log_user()
         # create a valid pull request
-        response = self.app.post(url(controller='pullrequests', action='create',
-                                     repo_name=HG_REPO),
+        response = self.app.post(base.url(controller='pullrequests', action='create',
+                                     repo_name=base.HG_REPO),
                                  {
-                                  'org_repo': HG_REPO,
+                                  'org_repo': base.HG_REPO,
                                   'org_ref': 'rev:94f45ed825a1:94f45ed825a113e61af7e141f44ca578374abef0',
-                                  'other_repo': HG_REPO,
+                                  'other_repo': base.HG_REPO,
                                   'other_ref': 'branch:default:96507bd11ecc815ebc6270fdf6db110928c09c1e',
                                   'pullrequest_title': 'title',
                                   'pullrequest_desc': 'description',
@@ -155,34 +155,34 @@
                                 status=302)
         # location is of the form:
         # http://localhost/vcs_test_hg/pull-request/54/_/title
-        m = re.search('/pull-request/(\d+)/', response.location)
+        m = re.search(r'/pull-request/(\d+)/', response.location)
         assert m is not None
         pull_request_id = m.group(1)
 
         # update it
-        response = self.app.post(url(controller='pullrequests', action='post',
-                                     repo_name=HG_REPO, pull_request_id=pull_request_id),
+        response = self.app.post(base.url(controller='pullrequests', action='post',
+                                     repo_name=base.HG_REPO, pull_request_id=pull_request_id),
                                  {
                                   'updaterev': '4f7e2131323e0749a740c0a56ab68ae9269c562a',
                                   'pullrequest_title': 'title',
                                   'pullrequest_desc': 'description',
-                                  'owner': TEST_USER_ADMIN_LOGIN,
+                                  'owner': base.TEST_USER_ADMIN_LOGIN,
                                   '_session_csrf_secret_token': self.session_csrf_secret_token(),
                                   'review_members': [str(invalid_user_id)],
                                  },
                                  status=400)
-        response.mustcontain('Invalid reviewer &#34;%s&#34; specified' % invalid_user_id)
+        response.mustcontain('Invalid reviewer &quot;%s&quot; specified' % invalid_user_id)
 
     def test_edit_with_invalid_reviewer(self):
         invalid_user_id = 99999
         self.log_user()
         # create a valid pull request
-        response = self.app.post(url(controller='pullrequests', action='create',
-                                     repo_name=HG_REPO),
+        response = self.app.post(base.url(controller='pullrequests', action='create',
+                                     repo_name=base.HG_REPO),
                                  {
-                                  'org_repo': HG_REPO,
+                                  'org_repo': base.HG_REPO,
                                   'org_ref': 'branch:stable:4f7e2131323e0749a740c0a56ab68ae9269c562a',
-                                  'other_repo': HG_REPO,
+                                  'other_repo': base.HG_REPO,
                                   'other_ref': 'branch:default:96507bd11ecc815ebc6270fdf6db110928c09c1e',
                                   'pullrequest_title': 'title',
                                   'pullrequest_desc': 'description',
@@ -191,22 +191,22 @@
                                 status=302)
         # location is of the form:
         # http://localhost/vcs_test_hg/pull-request/54/_/title
-        m = re.search('/pull-request/(\d+)/', response.location)
+        m = re.search(r'/pull-request/(\d+)/', response.location)
         assert m is not None
         pull_request_id = m.group(1)
 
         # edit it
-        response = self.app.post(url(controller='pullrequests', action='post',
-                                     repo_name=HG_REPO, pull_request_id=pull_request_id),
+        response = self.app.post(base.url(controller='pullrequests', action='post',
+                                     repo_name=base.HG_REPO, pull_request_id=pull_request_id),
                                  {
                                   'pullrequest_title': 'title',
                                   'pullrequest_desc': 'description',
-                                  'owner': TEST_USER_ADMIN_LOGIN,
+                                  'owner': base.TEST_USER_ADMIN_LOGIN,
                                   '_session_csrf_secret_token': self.session_csrf_secret_token(),
                                   'review_members': [str(invalid_user_id)],
                                  },
                                  status=400)
-        response.mustcontain('Invalid reviewer &#34;%s&#34; specified' % invalid_user_id)
+        response.mustcontain('Invalid reviewer &quot;%s&quot; specified' % invalid_user_id)
 
     def test_iteration_refs(self):
         # Repo graph excerpt:
@@ -226,18 +226,18 @@
 
         # create initial PR
         response = self.app.post(
-            url(controller='pullrequests', action='create', repo_name=HG_REPO),
+            base.url(controller='pullrequests', action='create', repo_name=base.HG_REPO),
             {
-                'org_repo': HG_REPO,
+                'org_repo': base.HG_REPO,
                 'org_ref': 'rev:9e6119747791:9e6119747791ff886a5abe1193a730b6bf874e1c',
-                'other_repo': HG_REPO,
+                'other_repo': base.HG_REPO,
                 'other_ref': 'branch:default:3d1091ee5a533b1f4577ec7d8a226bb315fb1336',
                 'pullrequest_title': 'title',
                 'pullrequest_desc': 'description',
                 '_session_csrf_secret_token': self.session_csrf_secret_token(),
             },
             status=302)
-        pr1_id = int(re.search('/pull-request/(\d+)/', response.location).group(1))
+        pr1_id = int(re.search(r'/pull-request/(\d+)/', response.location).group(1))
         pr1 = PullRequest.get(pr1_id)
 
         assert pr1.org_ref == 'branch:webvcs:9e6119747791ff886a5abe1193a730b6bf874e1c'
@@ -247,16 +247,16 @@
 
         # create PR 2 (new iteration with same ancestor)
         response = self.app.post(
-            url(controller='pullrequests', action='post', repo_name=HG_REPO, pull_request_id=pr1_id),
+            base.url(controller='pullrequests', action='post', repo_name=base.HG_REPO, pull_request_id=pr1_id),
             {
                 'updaterev': '5ec21f21aafe95220f1fc4843a4a57c378498b71',
                 'pullrequest_title': 'title',
                 'pullrequest_desc': 'description',
-                'owner': TEST_USER_REGULAR_LOGIN,
+                'owner': base.TEST_USER_REGULAR_LOGIN,
                 '_session_csrf_secret_token': self.session_csrf_secret_token(),
              },
              status=302)
-        pr2_id = int(re.search('/pull-request/(\d+)/', response.location).group(1))
+        pr2_id = int(re.search(r'/pull-request/(\d+)/', response.location).group(1))
         pr1 = PullRequest.get(pr1_id)
         pr2 = PullRequest.get(pr2_id)
 
@@ -269,16 +269,16 @@
 
         # create PR 3 (new iteration with new ancestor)
         response = self.app.post(
-            url(controller='pullrequests', action='post', repo_name=HG_REPO, pull_request_id=pr2_id),
+            base.url(controller='pullrequests', action='post', repo_name=base.HG_REPO, pull_request_id=pr2_id),
             {
                 'updaterev': 'fb95b340e0d03fa51f33c56c991c08077c99303e',
                 'pullrequest_title': 'title',
                 'pullrequest_desc': 'description',
-                'owner': TEST_USER_REGULAR_LOGIN,
+                'owner': base.TEST_USER_REGULAR_LOGIN,
                 '_session_csrf_secret_token': self.session_csrf_secret_token(),
              },
              status=302)
-        pr3_id = int(re.search('/pull-request/(\d+)/', response.location).group(1))
+        pr3_id = int(re.search(r'/pull-request/(\d+)/', response.location).group(1))
         pr2 = PullRequest.get(pr2_id)
         pr3 = PullRequest.get(pr3_id)
 
@@ -289,17 +289,17 @@
 
 
 @pytest.mark.usefixtures("test_context_fixture") # apply fixture for all test methods
-class TestPullrequestsGetRepoRefs(TestController):
+class TestPullrequestsGetRepoRefs(base.TestController):
 
     def setup_method(self, method):
-        self.repo_name = u'main'
+        self.repo_name = 'main'
         repo = fixture.create_repo(self.repo_name, repo_type='hg')
         self.repo_scm_instance = repo.scm_instance
         Session().commit()
         self.c = PullrequestsController()
 
     def teardown_method(self, method):
-        fixture.destroy_repo(u'main')
+        fixture.destroy_repo('main')
         Session().commit()
         Session.remove()
 
--- a/kallithea/tests/functional/test_repo_groups.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_repo_groups.py	Mon May 04 19:24:04 2020 +0200
@@ -1,16 +1,16 @@
-from kallithea.tests.base import *
+from kallithea.tests import base
 
 
-class TestRepoGroupsController(TestController):
+class TestRepoGroupsController(base.TestController):
 
     def test_index(self):
         self.log_user()
-        response = self.app.get(url('repos_groups'))
+        response = self.app.get(base.url('repos_groups'))
         response.mustcontain('"records": []')
 
     def test_new(self):
         self.log_user()
-        response = self.app.get(url('new_repos_group'))
+        response = self.app.get(base.url('new_repos_group'))
 
     def test_create(self):
         self.log_user()
@@ -18,14 +18,14 @@
         group_name = 'foo'
 
         # creation with form error
-        response = self.app.post(url('repos_groups'),
+        response = self.app.post(base.url('repos_groups'),
                                          {'group_name': group_name,
                                           '_session_csrf_secret_token': self.session_csrf_secret_token()})
         response.mustcontain('name="group_name" type="text" value="%s"' % group_name)
         response.mustcontain('<!-- for: group_description -->')
 
         # creation
-        response = self.app.post(url('repos_groups'),
+        response = self.app.post(base.url('repos_groups'),
                                          {'group_name': group_name,
                                          'group_description': 'lala',
                                          'parent_group_id': '-1',
@@ -34,18 +34,18 @@
         self.checkSessionFlash(response, 'Created repository group %s' % group_name)
 
         # edit form
-        response = self.app.get(url('edit_repo_group', group_name=group_name))
+        response = self.app.get(base.url('edit_repo_group', group_name=group_name))
         response.mustcontain('>lala<')
 
         # edit with form error
-        response = self.app.post(url('update_repos_group', group_name=group_name),
+        response = self.app.post(base.url('update_repos_group', group_name=group_name),
                                          {'group_name': group_name,
                                           '_session_csrf_secret_token': self.session_csrf_secret_token()})
         response.mustcontain('name="group_name" type="text" value="%s"' % group_name)
         response.mustcontain('<!-- for: group_description -->')
 
         # edit
-        response = self.app.post(url('update_repos_group', group_name=group_name),
+        response = self.app.post(base.url('update_repos_group', group_name=group_name),
                                          {'group_name': group_name,
                                          'group_description': 'lolo',
                                           '_session_csrf_secret_token': self.session_csrf_secret_token()})
@@ -56,22 +56,22 @@
         response.mustcontain('>lolo<')
 
         # listing
-        response = self.app.get(url('repos_groups'))
+        response = self.app.get(base.url('repos_groups'))
         response.mustcontain('raw_name": "%s"' % group_name)
 
         # show
-        response = self.app.get(url('repos_group', group_name=group_name))
+        response = self.app.get(base.url('repos_group', group_name=group_name))
         response.mustcontain('href="/_admin/repo_groups/%s/edit"' % group_name)
 
         # show ignores extra trailing slashes in the URL
-        response = self.app.get(url('repos_group', group_name='%s//' % group_name))
+        response = self.app.get(base.url('repos_group', group_name='%s//' % group_name))
         response.mustcontain('href="/_admin/repo_groups/%s/edit"' % group_name)
 
         # delete
-        response = self.app.post(url('delete_repo_group', group_name=group_name),
+        response = self.app.post(base.url('delete_repo_group', group_name=group_name),
                                  {'_session_csrf_secret_token': self.session_csrf_secret_token()})
         self.checkSessionFlash(response, 'Removed repository group %s' % group_name)
 
     def test_new_by_regular_user(self):
-        self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
-        response = self.app.get(url('new_repos_group'), status=403)
+        self.log_user(base.TEST_USER_REGULAR_LOGIN, base.TEST_USER_REGULAR_PASS)
+        response = self.app.get(base.url('new_repos_group'), status=403)
--- a/kallithea/tests/functional/test_search.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_search.py	Mon May 04 19:24:04 2020 +0200
@@ -1,13 +1,13 @@
 import mock
 
-from kallithea.tests.base import *
+from kallithea.tests import base
 
 
-class TestSearchController(TestController):
+class TestSearchController(base.TestController):
 
     def test_index(self):
         self.log_user()
-        response = self.app.get(url(controller='search', action='index'))
+        response = self.app.get(base.url(controller='search', action='index'))
 
         response.mustcontain('class="form-control" id="q" name="q" type="text"')
         # Test response...
@@ -20,33 +20,33 @@
             'index_dir': str(tmpdir),
         }
         with mock.patch('kallithea.controllers.search.config', config_mock):
-            response = self.app.get(url(controller='search', action='index'),
-                                    {'q': HG_REPO})
+            response = self.app.get(base.url(controller='search', action='index'),
+                                    {'q': base.HG_REPO})
             response.mustcontain('The server has no search index.')
 
     def test_normal_search(self):
         self.log_user()
-        response = self.app.get(url(controller='search', action='index'),
+        response = self.app.get(base.url(controller='search', action='index'),
                                 {'q': 'def repo'})
         response.mustcontain('58 results')
 
     def test_repo_search(self):
         self.log_user()
-        response = self.app.get(url(controller='search', action='index'),
-                                {'q': 'repository:%s def test' % HG_REPO})
+        response = self.app.get(base.url(controller='search', action='index'),
+                                {'q': 'repository:%s def test' % base.HG_REPO})
 
         response.mustcontain('18 results')
 
     def test_search_last(self):
         self.log_user()
-        response = self.app.get(url(controller='search', action='index'),
+        response = self.app.get(base.url(controller='search', action='index'),
                                 {'q': 'last:t', 'type': 'commit'})
 
         response.mustcontain('2 results')
 
     def test_search_commit_message(self):
         self.log_user()
-        response = self.app.get(url(controller='search', action='index'),
+        response = self.app.get(base.url(controller='search', action='index'),
                     {'q': 'bother to ask where to fetch repo during tests',
                      'type': 'commit'})
 
@@ -56,8 +56,8 @@
 
     def test_search_commit_message_hg_repo(self):
         self.log_user()
-        response = self.app.get(url(controller='search', action='index',
-                                    repo_name=HG_REPO),
+        response = self.app.get(base.url(controller='search', action='index',
+                                    repo_name=base.HG_REPO),
                     {'q': 'bother to ask where to fetch repo during tests',
                      'type': 'commit'})
 
@@ -66,7 +66,7 @@
 
     def test_search_commit_changed_file(self):
         self.log_user()
-        response = self.app.get(url(controller='search', action='index'),
+        response = self.app.get(base.url(controller='search', action='index'),
                                 {'q': 'changed:tests/utils.py',
                                  'type': 'commit'})
 
@@ -74,7 +74,7 @@
 
     def test_search_commit_changed_files_get_commit(self):
         self.log_user()
-        response = self.app.get(url(controller='search', action='index'),
+        response = self.app.get(base.url(controller='search', action='index'),
                                 {'q': 'changed:vcs/utils/archivers.py',
                                  'type': 'commit'})
 
@@ -90,7 +90,7 @@
 
     def test_search_commit_added_file(self):
         self.log_user()
-        response = self.app.get(url(controller='search', action='index'),
+        response = self.app.get(base.url(controller='search', action='index'),
                                 {'q': 'added:README.rst',
                                  'type': 'commit'})
 
@@ -102,7 +102,7 @@
 
     def test_search_author(self):
         self.log_user()
-        response = self.app.get(url(controller='search', action='index'),
+        response = self.app.get(base.url(controller='search', action='index'),
                     {'q': 'author:marcin@python-blog.com raw_id:b986218ba1c9b0d6a259fac9b050b1724ed8e545',
                      'type': 'commit'})
 
@@ -110,7 +110,7 @@
 
     def test_search_file_name(self):
         self.log_user()
-        response = self.app.get(url(controller='search', action='index'),
+        response = self.app.get(base.url(controller='search', action='index'),
                     {'q': 'README.rst', 'type': 'path'})
 
         response.mustcontain('2 results')
--- a/kallithea/tests/functional/test_search_indexing.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_search_indexing.py	Mon May 04 19:24:04 2020 +0200
@@ -5,7 +5,7 @@
 from kallithea.model.meta import Session
 from kallithea.model.repo import RepoModel
 from kallithea.model.repo_group import RepoGroupModel
-from kallithea.tests.base import *
+from kallithea.tests import base
 from kallithea.tests.fixture import Fixture, create_test_index
 
 
@@ -39,12 +39,12 @@
 
 repos = [
     # reponame,              init func or fork base, groupname
-    (u'indexing_test',       init_indexing_test,     None),
-    (u'indexing_test-fork',  u'indexing_test',       None),
-    (u'group/indexing_test', u'indexing_test',       u'group'),
-    (u'this-is-it',          u'indexing_test',       None),
-    (u'indexing_test-foo',   u'indexing_test',       None),
-    (u'stopword_test',       init_stopword_test,     None),
+    ('indexing_test',       init_indexing_test,     None),
+    ('indexing_test-fork',  'indexing_test',       None),
+    ('group/indexing_test', 'indexing_test',       'group'),
+    ('this-is-it',          'indexing_test',       None),
+    ('indexing_test-foo',   'indexing_test',       None),
+    ('stopword_test',       init_stopword_test,     None),
 ]
 
 
@@ -66,10 +66,10 @@
         # (FYI, ENOMEM occurs at forking "git" with python 2.7.3,
         # Linux 3.2.78-1 x86_64, 3GB memory, and no ulimit
         # configuration for memory)
-        create_test_index(TESTS_TMP_PATH, CONFIG, full_index=full_index)
+        create_test_index(base.TESTS_TMP_PATH, CONFIG, full_index=full_index)
 
 
-class TestSearchControllerIndexing(TestController):
+class TestSearchControllerIndexing(base.TestController):
     @classmethod
     def setup_class(cls):
         for reponame, init_or_fork, groupname in repos:
@@ -108,15 +108,15 @@
 
         rebuild_index(full_index=True) # rebuild fully for subsequent tests
 
-    @parametrize('reponame', [
-        (u'indexing_test'),
-        (u'indexing_test-fork'),
-        (u'group/indexing_test'),
-        (u'this-is-it'),
-        (u'*-fork'),
-        (u'group/*'),
+    @base.parametrize('reponame', [
+        ('indexing_test'),
+        ('indexing_test-fork'),
+        ('group/indexing_test'),
+        ('this-is-it'),
+        ('*-fork'),
+        ('group/*'),
     ])
-    @parametrize('searchtype,query,hit', [
+    @base.parametrize('searchtype,query,hit', [
         ('content', 'this_should_be_unique_content', 1),
         ('commit', 'this_should_be_unique_commit_log', 1),
         ('path', 'this_should_be_unique_filename.txt', 1),
@@ -125,17 +125,17 @@
         self.log_user()
 
         q = 'repository:%s %s' % (reponame, query)
-        response = self.app.get(url(controller='search', action='index'),
+        response = self.app.get(base.url(controller='search', action='index'),
                                 {'q': q, 'type': searchtype})
         response.mustcontain('>%d results' % hit)
 
-    @parametrize('reponame', [
-        (u'indexing_test'),
-        (u'indexing_test-fork'),
-        (u'group/indexing_test'),
-        (u'this-is-it'),
+    @base.parametrize('reponame', [
+        ('indexing_test'),
+        ('indexing_test-fork'),
+        ('group/indexing_test'),
+        ('this-is-it'),
     ])
-    @parametrize('searchtype,query,hit', [
+    @base.parametrize('searchtype,query,hit', [
         ('content', 'this_should_be_unique_content', 1),
         ('commit', 'this_should_be_unique_commit_log', 1),
         ('path', 'this_should_be_unique_filename.txt', 1),
@@ -143,12 +143,12 @@
     def test_searching_under_repository(self, reponame, searchtype, query, hit):
         self.log_user()
 
-        response = self.app.get(url(controller='search', action='index',
+        response = self.app.get(base.url(controller='search', action='index',
                                     repo_name=reponame),
                                 {'q': query, 'type': searchtype})
         response.mustcontain('>%d results' % hit)
 
-    @parametrize('searchtype,query,hit', [
+    @base.parametrize('searchtype,query,hit', [
         ('content', 'path:this/is/it def test', 1),
         ('commit', 'added:this/is/it bother to ask where', 1),
         # this condition matches against files below, because
@@ -161,12 +161,12 @@
         ('path', 'extension:us', 1),
     ])
     def test_filename_stopword(self, searchtype, query, hit):
-        response = self.app.get(url(controller='search', action='index'),
+        response = self.app.get(base.url(controller='search', action='index'),
                                 {'q': query, 'type': searchtype})
 
         response.mustcontain('>%d results' % hit)
 
-    @parametrize('searchtype,query,hit', [
+    @base.parametrize('searchtype,query,hit', [
         # matching against both 2 files
         ('content', 'owner:"this is it"', 0),
         ('content', 'owner:this-is-it', 0),
@@ -182,7 +182,7 @@
         ('commit', 'author:"this-is-it"', 1),
     ])
     def test_mailaddr_stopword(self, searchtype, query, hit):
-        response = self.app.get(url(controller='search', action='index'),
+        response = self.app.get(base.url(controller='search', action='index'),
                                 {'q': query, 'type': searchtype})
 
         response.mustcontain('>%d results' % hit)
--- a/kallithea/tests/functional/test_summary.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/functional/test_summary.py	Mon May 04 19:24:04 2020 +0200
@@ -18,7 +18,7 @@
 from kallithea.model.meta import Session
 from kallithea.model.repo import RepoModel
 from kallithea.model.scm import ScmModel
-from kallithea.tests.base import *
+from kallithea.tests import base
 from kallithea.tests.fixture import Fixture
 
 
@@ -32,14 +32,14 @@
         )
 
 
-class TestSummaryController(TestController):
+class TestSummaryController(base.TestController):
 
     def test_index_hg(self, custom_settings):
         self.log_user()
-        ID = Repository.get_by_repo_name(HG_REPO).repo_id
-        response = self.app.get(url(controller='summary',
+        ID = Repository.get_by_repo_name(base.HG_REPO).repo_id
+        response = self.app.get(base.url(controller='summary',
                                     action='index',
-                                    repo_name=HG_REPO))
+                                    repo_name=base.HG_REPO))
 
         # repo type
         response.mustcontain(
@@ -52,24 +52,24 @@
         # clone URLs
         response.mustcontain(
             '''<input class="form-control" size="80" readonly="readonly" value="http://%s@localhost:80/%s"/>''' %
-            (TEST_USER_ADMIN_LOGIN, HG_REPO)
+            (base.TEST_USER_ADMIN_LOGIN, base.HG_REPO)
         )
         response.mustcontain(
             '''<input class="form-control" size="80" readonly="readonly" value="http://%s@localhost:80/_%s"/>''' %
-            (TEST_USER_ADMIN_LOGIN, ID)
+            (base.TEST_USER_ADMIN_LOGIN, ID)
         )
         response.mustcontain(
             '''<input id="ssh_url" class="form-control" size="80" readonly="readonly" value="ssh://ssh_user@ssh_hostname/%s"/>''' %
-            (HG_REPO)
+            (base.HG_REPO)
         )
 
 
     def test_index_git(self, custom_settings):
         self.log_user()
-        ID = Repository.get_by_repo_name(GIT_REPO).repo_id
-        response = self.app.get(url(controller='summary',
+        ID = Repository.get_by_repo_name(base.GIT_REPO).repo_id
+        response = self.app.get(base.url(controller='summary',
                                     action='index',
-                                    repo_name=GIT_REPO))
+                                    repo_name=base.GIT_REPO))
 
         # repo type
         response.mustcontain(
@@ -82,21 +82,21 @@
         # clone URLs
         response.mustcontain(
             '''<input class="form-control" size="80" readonly="readonly" value="http://%s@localhost:80/%s"/>''' %
-            (TEST_USER_ADMIN_LOGIN, GIT_REPO)
+            (base.TEST_USER_ADMIN_LOGIN, base.GIT_REPO)
         )
         response.mustcontain(
             '''<input class="form-control" size="80" readonly="readonly" value="http://%s@localhost:80/_%s"/>''' %
-            (TEST_USER_ADMIN_LOGIN, ID)
+            (base.TEST_USER_ADMIN_LOGIN, ID)
         )
         response.mustcontain(
             '''<input id="ssh_url" class="form-control" size="80" readonly="readonly" value="ssh://ssh_user@ssh_hostname/%s"/>''' %
-            (GIT_REPO)
+            (base.GIT_REPO)
         )
 
     def test_index_by_id_hg(self):
         self.log_user()
-        ID = Repository.get_by_repo_name(HG_REPO).repo_id
-        response = self.app.get(url(controller='summary',
+        ID = Repository.get_by_repo_name(base.HG_REPO).repo_id
+        response = self.app.get(base.url(controller='summary',
                                     action='index',
                                     repo_name='_%s' % ID))
 
@@ -111,21 +111,21 @@
 
     def test_index_by_repo_having_id_path_in_name_hg(self):
         self.log_user()
-        fixture.create_repo(name=u'repo_1')
-        response = self.app.get(url(controller='summary',
+        fixture.create_repo(name='repo_1')
+        response = self.app.get(base.url(controller='summary',
                                     action='index',
                                     repo_name='repo_1'))
 
         try:
             response.mustcontain("repo_1")
         finally:
-            RepoModel().delete(Repository.get_by_repo_name(u'repo_1'))
+            RepoModel().delete(Repository.get_by_repo_name('repo_1'))
             Session().commit()
 
     def test_index_by_id_git(self):
         self.log_user()
-        ID = Repository.get_by_repo_name(GIT_REPO).repo_id
-        response = self.app.get(url(controller='summary',
+        ID = Repository.get_by_repo_name(base.GIT_REPO).repo_id
+        response = self.app.get(base.url(controller='summary',
                                     action='index',
                                     repo_name='_%s' % ID))
 
@@ -146,14 +146,14 @@
     def test_index_trending(self):
         self.log_user()
         # codes stats
-        self._enable_stats(HG_REPO)
+        self._enable_stats(base.HG_REPO)
 
-        ScmModel().mark_for_invalidation(HG_REPO)
+        ScmModel().mark_for_invalidation(base.HG_REPO)
         # generate statistics first
-        response = self.app.get(url(controller='summary', action='statistics',
-                                    repo_name=HG_REPO))
-        response = self.app.get(url(controller='summary', action='index',
-                                    repo_name=HG_REPO))
+        response = self.app.get(base.url(controller='summary', action='statistics',
+                                    repo_name=base.HG_REPO))
+        response = self.app.get(base.url(controller='summary', action='index',
+                                    repo_name=base.HG_REPO))
         response.mustcontain(
             '[["py", {"count": 68, "desc": ["Python"]}], '
             '["rst", {"count": 16, "desc": ["Rst"]}], '
@@ -170,23 +170,23 @@
     def test_index_statistics(self):
         self.log_user()
         # codes stats
-        self._enable_stats(HG_REPO)
+        self._enable_stats(base.HG_REPO)
 
-        ScmModel().mark_for_invalidation(HG_REPO)
-        response = self.app.get(url(controller='summary', action='statistics',
-                                    repo_name=HG_REPO))
+        ScmModel().mark_for_invalidation(base.HG_REPO)
+        response = self.app.get(base.url(controller='summary', action='statistics',
+                                    repo_name=base.HG_REPO))
 
     def test_index_trending_git(self):
         self.log_user()
         # codes stats
-        self._enable_stats(GIT_REPO)
+        self._enable_stats(base.GIT_REPO)
 
-        ScmModel().mark_for_invalidation(GIT_REPO)
+        ScmModel().mark_for_invalidation(base.GIT_REPO)
         # generate statistics first
-        response = self.app.get(url(controller='summary', action='statistics',
-                                    repo_name=GIT_REPO))
-        response = self.app.get(url(controller='summary', action='index',
-                                    repo_name=GIT_REPO))
+        response = self.app.get(base.url(controller='summary', action='statistics',
+                                    repo_name=base.GIT_REPO))
+        response = self.app.get(base.url(controller='summary', action='index',
+                                    repo_name=base.GIT_REPO))
         response.mustcontain(
             '[["py", {"count": 68, "desc": ["Python"]}], '
             '["rst", {"count": 16, "desc": ["Rst"]}], '
@@ -203,8 +203,8 @@
     def test_index_statistics_git(self):
         self.log_user()
         # codes stats
-        self._enable_stats(GIT_REPO)
+        self._enable_stats(base.GIT_REPO)
 
-        ScmModel().mark_for_invalidation(GIT_REPO)
-        response = self.app.get(url(controller='summary', action='statistics',
-                                    repo_name=GIT_REPO))
+        ScmModel().mark_for_invalidation(base.GIT_REPO)
+        response = self.app.get(base.url(controller='summary', action='statistics',
+                                    repo_name=base.GIT_REPO))
--- a/kallithea/tests/models/common.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/models/common.py	Mon May 04 19:24:04 2020 +0200
@@ -12,7 +12,7 @@
 
 def _destroy_project_tree(test_u1_id):
     Session.remove()
-    repo_group = RepoGroup.get_by_group_name(group_name=u'g0')
+    repo_group = RepoGroup.get_by_group_name(group_name='g0')
     for el in reversed(repo_group.recursive_groups_and_repos()):
         if isinstance(el, Repository):
             RepoModel().delete(el)
@@ -50,21 +50,21 @@
 
     """
     test_u1 = UserModel().create_or_update(
-        username=u'test_u1', password=u'qweqwe',
-        email=u'test_u1@example.com', firstname=u'test_u1', lastname=u'test_u1'
+        username='test_u1', password='qweqwe',
+        email='test_u1@example.com', firstname='test_u1', lastname='test_u1'
     )
-    g0 = fixture.create_repo_group(u'g0')
-    g0_1 = fixture.create_repo_group(u'g0_1', parent_group_id=g0)
-    g0_1_1 = fixture.create_repo_group(u'g0_1_1', parent_group_id=g0_1)
-    g0_1_1_r1 = fixture.create_repo(u'g0/g0_1/g0_1_1/g0_1_1_r1', repo_group=g0_1_1)
-    g0_1_1_r2 = fixture.create_repo(u'g0/g0_1/g0_1_1/g0_1_1_r2', repo_group=g0_1_1)
-    g0_1_r1 = fixture.create_repo(u'g0/g0_1/g0_1_r1', repo_group=g0_1)
-    g0_2 = fixture.create_repo_group(u'g0_2', parent_group_id=g0)
-    g0_2_r1 = fixture.create_repo(u'g0/g0_2/g0_2_r1', repo_group=g0_2)
-    g0_2_r2 = fixture.create_repo(u'g0/g0_2/g0_2_r2', repo_group=g0_2)
-    g0_3 = fixture.create_repo_group(u'g0_3', parent_group_id=g0)
-    g0_3_r1 = fixture.create_repo(u'g0/g0_3/g0_3_r1', repo_group=g0_3)
-    g0_3_r2_private = fixture.create_repo(u'g0/g0_3/g0_3_r1_private',
+    g0 = fixture.create_repo_group('g0')
+    g0_1 = fixture.create_repo_group('g0_1', parent_group_id=g0)
+    g0_1_1 = fixture.create_repo_group('g0_1_1', parent_group_id=g0_1)
+    g0_1_1_r1 = fixture.create_repo('g0/g0_1/g0_1_1/g0_1_1_r1', repo_group=g0_1_1)
+    g0_1_1_r2 = fixture.create_repo('g0/g0_1/g0_1_1/g0_1_1_r2', repo_group=g0_1_1)
+    g0_1_r1 = fixture.create_repo('g0/g0_1/g0_1_r1', repo_group=g0_1)
+    g0_2 = fixture.create_repo_group('g0_2', parent_group_id=g0)
+    g0_2_r1 = fixture.create_repo('g0/g0_2/g0_2_r1', repo_group=g0_2)
+    g0_2_r2 = fixture.create_repo('g0/g0_2/g0_2_r2', repo_group=g0_2)
+    g0_3 = fixture.create_repo_group('g0_3', parent_group_id=g0)
+    g0_3_r1 = fixture.create_repo('g0/g0_3/g0_3_r1', repo_group=g0_3)
+    g0_3_r2_private = fixture.create_repo('g0/g0_3/g0_3_r1_private',
                                           repo_group=g0_3, repo_private=True)
     return test_u1
 
--- a/kallithea/tests/models/test_changeset_status.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/models/test_changeset_status.py	Mon May 04 19:24:04 2020 +0200
@@ -1,6 +1,6 @@
 from kallithea.model.changeset_status import ChangesetStatusModel
 from kallithea.model.db import ChangesetStatus as CS
-from kallithea.tests.base import *
+from kallithea.tests import base
 
 
 class CSM(object): # ChangesetStatusMock
@@ -9,12 +9,12 @@
         self.status = status
 
 
-class TestChangesetStatusCalculation(TestController):
+class TestChangesetStatusCalculation(base.TestController):
 
     def setup_method(self, method):
         self.m = ChangesetStatusModel()
 
-    @parametrize('name,expected_result,statuses', [
+    @base.parametrize('name,expected_result,statuses', [
         ('empty list', CS.STATUS_UNDER_REVIEW, []),
         ('approve', CS.STATUS_APPROVED, [CSM(CS.STATUS_APPROVED)]),
         ('approve2', CS.STATUS_APPROVED, [CSM(CS.STATUS_APPROVED), CSM(CS.STATUS_APPROVED)]),
--- a/kallithea/tests/models/test_comments.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/models/test_comments.py	Mon May 04 19:24:04 2020 +0200
@@ -3,10 +3,10 @@
 
 from kallithea.model.comment import ChangesetCommentsModel
 from kallithea.model.db import Repository
-from kallithea.tests.base import *
+from kallithea.tests import base
 
 
-class TestComments(TestController):
+class TestComments(base.TestController):
 
     def _check_comment_count(self, repo_id, revision,
             expected_len_comments, expected_len_inline_comments,
@@ -23,17 +23,17 @@
 
     def test_create_delete_general_comment(self):
         with test_context(self.app):
-            repo_id = Repository.get_by_repo_name(HG_REPO).repo_id
+            repo_id = Repository.get_by_repo_name(base.HG_REPO).repo_id
             revision = '9a7b4ff9e8b40bbda72fc75f162325b9baa45cda'
 
             self._check_comment_count(repo_id, revision,
                     expected_len_comments=0, expected_len_inline_comments=0)
 
-            text = u'a comment'
+            text = 'a comment'
             new_comment = ChangesetCommentsModel().create(
                     text=text,
-                    repo=HG_REPO,
-                    author=TEST_USER_REGULAR_LOGIN,
+                    repo=base.HG_REPO,
+                    author=base.TEST_USER_REGULAR_LOGIN,
                     revision=revision,
                     send_email=False)
 
@@ -47,19 +47,19 @@
 
     def test_create_delete_inline_comment(self):
         with test_context(self.app):
-            repo_id = Repository.get_by_repo_name(HG_REPO).repo_id
+            repo_id = Repository.get_by_repo_name(base.HG_REPO).repo_id
             revision = '9a7b4ff9e8b40bbda72fc75f162325b9baa45cda'
 
             self._check_comment_count(repo_id, revision,
                     expected_len_comments=0, expected_len_inline_comments=0)
 
-            text = u'an inline comment'
-            f_path = u'vcs/tests/base.py'
-            line_no = u'n50'
+            text = 'an inline comment'
+            f_path = 'vcs/tests/base.py'
+            line_no = 'n50'
             new_comment = ChangesetCommentsModel().create(
                     text=text,
-                    repo=HG_REPO,
-                    author=TEST_USER_REGULAR_LOGIN,
+                    repo=base.HG_REPO,
+                    author=base.TEST_USER_REGULAR_LOGIN,
                     revision=revision,
                     f_path=f_path,
                     line_no=line_no,
@@ -81,42 +81,42 @@
 
     def test_create_delete_multiple_inline_comments(self):
         with test_context(self.app):
-            repo_id = Repository.get_by_repo_name(HG_REPO).repo_id
+            repo_id = Repository.get_by_repo_name(base.HG_REPO).repo_id
             revision = '9a7b4ff9e8b40bbda72fc75f162325b9baa45cda'
 
             self._check_comment_count(repo_id, revision,
                     expected_len_comments=0, expected_len_inline_comments=0)
 
-            text = u'an inline comment'
-            f_path = u'vcs/tests/base.py'
-            line_no = u'n50'
+            text = 'an inline comment'
+            f_path = 'vcs/tests/base.py'
+            line_no = 'n50'
             new_comment = ChangesetCommentsModel().create(
                     text=text,
-                    repo=HG_REPO,
-                    author=TEST_USER_REGULAR_LOGIN,
+                    repo=base.HG_REPO,
+                    author=base.TEST_USER_REGULAR_LOGIN,
                     revision=revision,
                     f_path=f_path,
                     line_no=line_no,
                     send_email=False)
 
-            text2 = u'another inline comment, same file'
-            line_no2 = u'o41'
+            text2 = 'another inline comment, same file'
+            line_no2 = 'o41'
             new_comment2 = ChangesetCommentsModel().create(
                     text=text2,
-                    repo=HG_REPO,
-                    author=TEST_USER_REGULAR_LOGIN,
+                    repo=base.HG_REPO,
+                    author=base.TEST_USER_REGULAR_LOGIN,
                     revision=revision,
                     f_path=f_path,
                     line_no=line_no2,
                     send_email=False)
 
-            text3 = u'another inline comment, same file'
-            f_path3 = u'vcs/tests/test_hg.py'
-            line_no3 = u'n159'
+            text3 = 'another inline comment, same file'
+            f_path3 = 'vcs/tests/test_hg.py'
+            line_no3 = 'n159'
             new_comment3 = ChangesetCommentsModel().create(
                     text=text3,
-                    repo=HG_REPO,
-                    author=TEST_USER_REGULAR_LOGIN,
+                    repo=base.HG_REPO,
+                    author=base.TEST_USER_REGULAR_LOGIN,
                     revision=revision,
                     f_path=f_path3,
                     line_no=line_no3,
@@ -126,15 +126,15 @@
                     expected_len_comments=0, expected_len_inline_comments=2)
             # inline_comments is a list of tuples (file_path, dict)
             # where the dict keys are line numbers and values are lists of comments
-            assert inline_comments[1][0] == f_path
-            assert len(inline_comments[1][1]) == 2
-            assert inline_comments[1][1][line_no][0].text == text
-            assert inline_comments[1][1][line_no2][0].text == text2
+            assert inline_comments[0][0] == f_path
+            assert len(inline_comments[0][1]) == 2
+            assert inline_comments[0][1][line_no][0].text == text
+            assert inline_comments[0][1][line_no2][0].text == text2
 
-            assert inline_comments[0][0] == f_path3
-            assert len(inline_comments[0][1]) == 1
-            assert line_no3 in inline_comments[0][1]
-            assert inline_comments[0][1][line_no3][0].text == text3
+            assert inline_comments[1][0] == f_path3
+            assert len(inline_comments[1][1]) == 1
+            assert line_no3 in inline_comments[1][1]
+            assert inline_comments[1][1][line_no3][0].text == text3
 
             # now delete only one comment
             ChangesetCommentsModel().delete(new_comment2)
@@ -143,14 +143,14 @@
                     expected_len_comments=0, expected_len_inline_comments=2)
             # inline_comments is a list of tuples (file_path, dict)
             # where the dict keys are line numbers and values are lists of comments
-            assert inline_comments[1][0] == f_path
-            assert len(inline_comments[1][1]) == 1
-            assert inline_comments[1][1][line_no][0].text == text
+            assert inline_comments[0][0] == f_path
+            assert len(inline_comments[0][1]) == 1
+            assert inline_comments[0][1][line_no][0].text == text
 
-            assert inline_comments[0][0] == f_path3
-            assert len(inline_comments[0][1]) == 1
-            assert line_no3 in inline_comments[0][1]
-            assert inline_comments[0][1][line_no3][0].text == text3
+            assert inline_comments[1][0] == f_path3
+            assert len(inline_comments[1][1]) == 1
+            assert line_no3 in inline_comments[1][1]
+            assert inline_comments[1][1][line_no3][0].text == text3
 
             # now delete all others
             ChangesetCommentsModel().delete(new_comment)
@@ -161,42 +161,42 @@
 
     def test_selective_retrieval_of_inline_comments(self):
         with test_context(self.app):
-            repo_id = Repository.get_by_repo_name(HG_REPO).repo_id
+            repo_id = Repository.get_by_repo_name(base.HG_REPO).repo_id
             revision = '9a7b4ff9e8b40bbda72fc75f162325b9baa45cda'
 
             self._check_comment_count(repo_id, revision,
                     expected_len_comments=0, expected_len_inline_comments=0)
 
-            text = u'an inline comment'
-            f_path = u'vcs/tests/base.py'
-            line_no = u'n50'
+            text = 'an inline comment'
+            f_path = 'vcs/tests/base.py'
+            line_no = 'n50'
             new_comment = ChangesetCommentsModel().create(
                     text=text,
-                    repo=HG_REPO,
-                    author=TEST_USER_REGULAR_LOGIN,
+                    repo=base.HG_REPO,
+                    author=base.TEST_USER_REGULAR_LOGIN,
                     revision=revision,
                     f_path=f_path,
                     line_no=line_no,
                     send_email=False)
 
-            text2 = u'another inline comment, same file'
-            line_no2 = u'o41'
+            text2 = 'another inline comment, same file'
+            line_no2 = 'o41'
             new_comment2 = ChangesetCommentsModel().create(
                     text=text2,
-                    repo=HG_REPO,
-                    author=TEST_USER_REGULAR_LOGIN,
+                    repo=base.HG_REPO,
+                    author=base.TEST_USER_REGULAR_LOGIN,
                     revision=revision,
                     f_path=f_path,
                     line_no=line_no2,
                     send_email=False)
 
-            text3 = u'another inline comment, same file'
-            f_path3 = u'vcs/tests/test_hg.py'
-            line_no3 = u'n159'
+            text3 = 'another inline comment, same file'
+            f_path3 = 'vcs/tests/test_hg.py'
+            line_no3 = 'n159'
             new_comment3 = ChangesetCommentsModel().create(
                     text=text3,
-                    repo=HG_REPO,
-                    author=TEST_USER_REGULAR_LOGIN,
+                    repo=base.HG_REPO,
+                    author=base.TEST_USER_REGULAR_LOGIN,
                     revision=revision,
                     f_path=f_path3,
                     line_no=line_no3,
--- a/kallithea/tests/models/test_diff_parsers.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/models/test_diff_parsers.py	Mon May 04 19:24:04 2020 +0200
@@ -1,5 +1,5 @@
 from kallithea.lib.diffs import BIN_FILENODE, CHMOD_FILENODE, COPIED_FILENODE, DEL_FILENODE, MOD_FILENODE, NEW_FILENODE, RENAMED_FILENODE, DiffProcessor
-from kallithea.tests.base import *
+from kallithea.tests import base
 from kallithea.tests.fixture import Fixture
 
 
@@ -271,9 +271,9 @@
 }
 
 
-class TestDiffLib(TestController):
+class TestDiffLib(base.TestController):
 
-    @parametrize('diff_fixture', DIFF_FIXTURES)
+    @base.parametrize('diff_fixture', DIFF_FIXTURES)
     def test_diff(self, diff_fixture):
         raw_diff = fixture.load_resource(diff_fixture, strip=False)
         vcs = 'hg'
@@ -295,20 +295,20 @@
             l.append('%(action)-7s %(new_lineno)3s %(old_lineno)3s %(line)r\n' % d)
         s = ''.join(l)
         assert s == r'''
-context ... ... u'@@ -51,6 +51,13 @@\n'
-unmod    51  51 u'<u>\t</u>begin();\n'
-unmod    52  52 u'<u>\t</u>\n'
-add      53     u'<u>\t</u>int foo;<u class="cr"></u>\n'
-add      54     u'<u>\t</u>int bar; <u class="cr"></u>\n'
-add      55     u'<u>\t</u>int baz;<u>\t</u><u class="cr"></u>\n'
-add      56     u'<u>\t</u>int space; <i></i>'
-add      57     u'<u>\t</u>int tab;<u>\t</u>\n'
-add      58     u'<u>\t</u>\n'
-unmod    59  53 u' <i></i>'
-del          54 u'<u>\t</u>#define MAX_STEPS (48)\n'
-add      60     u'<u>\t</u><u class="cr"></u>\n'
-add      61     u'<u>\t</u>#define MAX_STEPS (64)<u class="cr"></u>\n'
-unmod    62  55 u'\n'
-del          56 u'<u>\t</u>#define MIN_STEPS (<del>48</del>)\n'
-add      63     u'<u>\t</u>#define MIN_STEPS (<ins>42</ins>)\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'
+add      54     '<u>\t</u>int bar; <u class="cr"></u>\n'
+add      55     '<u>\t</u>int baz;<u>\t</u><u class="cr"></u>\n'
+add      56     '<u>\t</u>int space; <i></i>'
+add      57     '<u>\t</u>int tab;<u>\t</u>\n'
+add      58     '<u>\t</u>\n'
+unmod    59  53 ' <i></i>'
+del          54 '<u>\t</u>#define MAX_STEPS (48)\n'
+add      60     '<u>\t</u><u class="cr"></u>\n'
+add      61     '<u>\t</u>#define MAX_STEPS (64)<u class="cr"></u>\n'
+unmod    62  55 '\n'
+del          56 '<u>\t</u>#define MIN_STEPS (<del>48</del>)\n'
+add      63     '<u>\t</u>#define MIN_STEPS (<ins>42</ins>)\n'
 '''
--- a/kallithea/tests/models/test_dump_html_mails.ref.html	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/models/test_dump_html_mails.ref.html	Mon May 04 19:24:04 2020 +0200
@@ -5,7 +5,7 @@
 <hr/>
 <h1>cs_comment, is_mention=False, status_change=None</h1>
 <pre>
-From: u1
+From: u1 u1 <name@example.com>
 To: u2@example.com
 Subject: [Comment] repo/name changeset cafe1234 "This changeset did something cl..." on brunch
 </pre>
@@ -164,7 +164,7 @@
 <hr/>
 <h1>cs_comment, is_mention=True, status_change=None</h1>
 <pre>
-From: u1
+From: u1 u1 <name@example.com>
 To: u2@example.com
 Subject: [Comment] repo/name changeset cafe1234 "This changeset did something cl..." on brunch
 </pre>
@@ -323,7 +323,7 @@
 <hr/>
 <h1>cs_comment, is_mention=False, status_change='Approved'</h1>
 <pre>
-From: u1
+From: u1 u1 <name@example.com>
 To: u2@example.com
 Subject: [Approved: Comment] repo/name changeset cafe1234 "This changeset did something cl..." on brunch
 </pre>
@@ -500,7 +500,7 @@
 <hr/>
 <h1>cs_comment, is_mention=True, status_change='Approved'</h1>
 <pre>
-From: u1
+From: u1 u1 <name@example.com>
 To: u2@example.com
 Subject: [Approved: Comment] repo/name changeset cafe1234 "This changeset did something cl..." on brunch
 </pre>
@@ -677,7 +677,7 @@
 <hr/>
 <h1>message</h1>
 <pre>
-From: u1
+From: u1 u1 <name@example.com>
 To: u2@example.com
 Subject: Test Message
 </pre>
@@ -748,7 +748,7 @@
 <hr/>
 <h1>registration</h1>
 <pre>
-From: u1
+From: u1 u1 <name@example.com>
 To: u2@example.com
 Subject: New user newbie registered
 </pre>
@@ -881,7 +881,7 @@
 <hr/>
 <h1>pull_request, is_mention=False</h1>
 <pre>
-From: u1
+From: u1 u1 <name@example.com>
 To: u2@example.com
 Subject: [Review] repo/name PR #7 "The Title" from devbranch by u2
 </pre>
@@ -1072,7 +1072,7 @@
 <hr/>
 <h1>pull_request, is_mention=True</h1>
 <pre>
-From: u1
+From: u1 u1 <name@example.com>
 To: u2@example.com
 Subject: [Review] repo/name PR #7 "The Title" from devbranch by u2
 </pre>
@@ -1263,7 +1263,7 @@
 <hr/>
 <h1>pull_request_comment, is_mention=False, status_change=None, closing_pr=False</h1>
 <pre>
-From: u1
+From: u1 u1 <name@example.com>
 To: u2@example.com
 Subject: [Comment] repo/name PR #7 "The Title" from devbranch by u2
 </pre>
@@ -1430,7 +1430,7 @@
 <hr/>
 <h1>pull_request_comment, is_mention=True, status_change=None, closing_pr=False</h1>
 <pre>
-From: u1
+From: u1 u1 <name@example.com>
 To: u2@example.com
 Subject: [Comment] repo/name PR #7 "The Title" from devbranch by u2
 </pre>
@@ -1597,7 +1597,7 @@
 <hr/>
 <h1>pull_request_comment, is_mention=False, status_change='Under Review', closing_pr=False</h1>
 <pre>
-From: u1
+From: u1 u1 <name@example.com>
 To: u2@example.com
 Subject: [Under Review: Comment] repo/name PR #7 "The Title" from devbranch by u2
 </pre>
@@ -1782,7 +1782,7 @@
 <hr/>
 <h1>pull_request_comment, is_mention=True, status_change='Under Review', closing_pr=False</h1>
 <pre>
-From: u1
+From: u1 u1 <name@example.com>
 To: u2@example.com
 Subject: [Under Review: Comment] repo/name PR #7 "The Title" from devbranch by u2
 </pre>
@@ -1967,7 +1967,7 @@
 <hr/>
 <h1>pull_request_comment, is_mention=False, status_change=None, closing_pr=True</h1>
 <pre>
-From: u1
+From: u1 u1 <name@example.com>
 To: u2@example.com
 Subject: [Closing: Comment] repo/name PR #7 "The Title" from devbranch by u2
 </pre>
@@ -2151,7 +2151,7 @@
 <hr/>
 <h1>pull_request_comment, is_mention=True, status_change=None, closing_pr=True</h1>
 <pre>
-From: u1
+From: u1 u1 <name@example.com>
 To: u2@example.com
 Subject: [Closing: Comment] repo/name PR #7 "The Title" from devbranch by u2
 </pre>
@@ -2335,7 +2335,7 @@
 <hr/>
 <h1>pull_request_comment, is_mention=False, status_change='Under Review', closing_pr=True</h1>
 <pre>
-From: u1
+From: u1 u1 <name@example.com>
 To: u2@example.com
 Subject: [Under Review, Closing: Comment] repo/name PR #7 "The Title" from devbranch by u2
 </pre>
@@ -2525,7 +2525,7 @@
 <hr/>
 <h1>pull_request_comment, is_mention=True, status_change='Under Review', closing_pr=True</h1>
 <pre>
-From: u1
+From: u1 u1 <name@example.com>
 To: u2@example.com
 Subject: [Under Review, Closing: Comment] repo/name PR #7 "The Title" from devbranch by u2
 </pre>
@@ -2715,7 +2715,7 @@
 <hr/>
 <h1>TYPE_PASSWORD_RESET</h1>
 <pre>
-From: u1
+From: u1 u1 <name@example.com>
 To: john@doe.com
 Subject: Password reset link
 </pre>
--- a/kallithea/tests/models/test_notifications.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/models/test_notifications.py	Mon May 04 19:24:04 2020 +0200
@@ -11,31 +11,31 @@
 from kallithea.model.meta import Session
 from kallithea.model.notification import EmailNotificationModel, NotificationModel
 from kallithea.model.user import UserModel
-from kallithea.tests.base import *
+from kallithea.tests import base
 
 
-class TestNotifications(TestController):
+class TestNotifications(base.TestController):
 
     def setup_method(self, method):
         Session.remove()
-        u1 = UserModel().create_or_update(username=u'u1',
-                                        password=u'qweqwe',
-                                        email=u'u1@example.com',
-                                        firstname=u'u1', lastname=u'u1')
+        u1 = UserModel().create_or_update(username='u1',
+                                        password='qweqwe',
+                                        email='u1@example.com',
+                                        firstname='u1', lastname='u1')
         Session().commit()
         self.u1 = u1.user_id
 
-        u2 = UserModel().create_or_update(username=u'u2',
-                                        password=u'qweqwe',
-                                        email=u'u2@example.com',
-                                        firstname=u'u2', lastname=u'u3')
+        u2 = UserModel().create_or_update(username='u2',
+                                        password='qweqwe',
+                                        email='u2@example.com',
+                                        firstname='u2', lastname='u3')
         Session().commit()
         self.u2 = u2.user_id
 
-        u3 = UserModel().create_or_update(username=u'u3',
-                                        password=u'qweqwe',
-                                        email=u'u3@example.com',
-                                        firstname=u'u3', lastname=u'u3')
+        u3 = UserModel().create_or_update(username='u3',
+                                        password='qweqwe',
+                                        email='u3@example.com',
+                                        firstname='u3', lastname='u3')
         Session().commit()
         self.u3 = u3.user_id
 
@@ -43,15 +43,15 @@
         with test_context(self.app):
             usrs = [self.u1, self.u2]
 
-            def send_email(recipients, subject, body='', html_body='', headers=None, author=None):
+            def send_email(recipients, subject, body='', html_body='', headers=None, from_name=None):
                 assert recipients == ['u2@example.com']
                 assert subject == 'Test Message'
-                assert body == u"hi there"
+                assert body == "hi there"
                 assert '>hi there<' in html_body
-                assert author.username == 'u1'
+                assert from_name == 'u1 u1'
             with mock.patch.object(kallithea.lib.celerylib.tasks, 'send_email', send_email):
                 NotificationModel().create(created_by=self.u1,
-                                                   subject=u'subj', body=u'hi there',
+                                                   subject='subj', body='hi there',
                                                    recipients=usrs)
 
     @mock.patch.object(h, 'canonical_url', (lambda arg, **kwargs: 'http://%s/?%s' % (arg, '&'.join('%s=%s' % (k, v) for (k, v) in sorted(kwargs.items())))))
@@ -59,11 +59,11 @@
         # Exercise all notification types and dump them to one big html file
         l = []
 
-        def send_email(recipients, subject, body='', html_body='', headers=None, author=None):
+        def send_email(recipients, subject, body='', html_body='', headers=None, from_name=None):
             l.append('<hr/>\n')
             l.append('<h1>%s</h1>\n' % desc) # desc is from outer scope
             l.append('<pre>\n')
-            l.append('From: %s\n' % author.username)
+            l.append('From: %s <name@example.com>\n' % from_name)
             l.append('To: %s\n' % ' '.join(recipients))
             l.append('Subject: %s\n' % subject)
             l.append('</pre>\n')
@@ -90,7 +90,7 @@
 
                 for type_, body, kwargs in [
                         (NotificationModel.TYPE_CHANGESET_COMMENT,
-                         u'This is the new \'comment\'.\n\n - and here it ends indented.',
+                         'This is the new \'comment\'.\n\n - and here it ends indented.',
                          dict(
                             short_id='cafe1234',
                             raw_id='cafe1234c0ffeecafe',
@@ -105,18 +105,18 @@
                             cs_url='http://changeset.com',
                             cs_author=User.get(self.u2))),
                         (NotificationModel.TYPE_MESSAGE,
-                         u'This is the \'body\' of the "test" message\n - nothing interesting here except indentation.',
+                         'This is the \'body\' of the "test" message\n - nothing interesting here except indentation.',
                          dict()),
                         #(NotificationModel.TYPE_MENTION, '$body', None), # not used
                         (NotificationModel.TYPE_REGISTRATION,
-                         u'Registration body',
+                         'Registration body',
                          dict(
                             new_username='newbie',
                             registered_user_url='http://newbie.org',
                             new_email='new@email.com',
                             new_full_name='New Full Name')),
                         (NotificationModel.TYPE_PULL_REQUEST,
-                         u'This PR is \'awesome\' because it does <stuff>\n - please approve indented!',
+                         'This PR is \'awesome\' because it does <stuff>\n - please approve indented!',
                          dict(
                             pr_user_created='Requesting User (root)', # pr_owner should perhaps be used for @mention in description ...
                             is_mention=[False, True],
@@ -124,7 +124,7 @@
                             org_repo_name='repo_org',
                             **pr_kwargs)),
                         (NotificationModel.TYPE_PULL_REQUEST_COMMENT,
-                         u'Me too!\n\n - and indented on second line',
+                         'Me too!\n\n - and indented on second line',
                          dict(
                             closing_pr=[False, True],
                             is_mention=[False, True],
@@ -133,7 +133,7 @@
                             status_change=[None, 'Under Review'],
                             **pr_kwargs)),
                         ]:
-                    kwargs['repo_name'] = u'repo/name'
+                    kwargs['repo_name'] = 'repo/name'
                     params = [(type_, type_, body, kwargs)]
                     for param_name in ['is_mention', 'status_change', 'closing_pr']: # TODO: inline/general
                         if not isinstance(kwargs.get(param_name), list):
@@ -149,7 +149,7 @@
                     for desc, type_, body, kwargs in params:
                         # desc is used as "global" variable
                         NotificationModel().create(created_by=self.u1,
-                                                           subject=u'unused', body=body, email_kwargs=kwargs,
+                                                           subject='unused', body=body, email_kwargs=kwargs,
                                                            recipients=[self.u2], type_=type_)
 
                 # Email type TYPE_PASSWORD_RESET has no corresponding notification type - test it directly:
@@ -159,7 +159,7 @@
                     "Password reset link",
                     EmailNotificationModel().get_email_tmpl(EmailNotificationModel.TYPE_PASSWORD_RESET, 'txt', **kwargs),
                     EmailNotificationModel().get_email_tmpl(EmailNotificationModel.TYPE_PASSWORD_RESET, 'html', **kwargs),
-                    author=User.get(self.u1))
+                    from_name=User.get(self.u1).full_name_or_username)
 
         out = '<!doctype html>\n<html lang="en">\n<head><title>Notifications</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head>\n<body>\n%s\n</body>\n</html>\n' % \
             re.sub(r'<(/?(?:!doctype|html|head|title|meta|body)\b[^>]*)>', r'<!--\1-->', ''.join(l))
--- a/kallithea/tests/models/test_permissions.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/models/test_permissions.py	Mon May 04 19:24:04 2020 +0200
@@ -1,44 +1,45 @@
 from kallithea.lib.auth import AuthUser
-from kallithea.model.db import Permission, RepoGroup, User, UserGroupRepoGroupToPerm, UserToPerm
+from kallithea.model import db
+from kallithea.model.db import Permission, User, UserGroupRepoGroupToPerm, UserToPerm
 from kallithea.model.meta import Session
 from kallithea.model.permission import PermissionModel
 from kallithea.model.repo import RepoModel
 from kallithea.model.repo_group import RepoGroupModel
 from kallithea.model.user import UserModel
 from kallithea.model.user_group import UserGroupModel
-from kallithea.tests.base import *
+from kallithea.tests import base
 from kallithea.tests.fixture import Fixture
 
 
 fixture = Fixture()
 
 
-class TestPermissions(TestController):
+class TestPermissions(base.TestController):
 
     @classmethod
     def setup_class(cls):
         # recreate default user to get a clean start
-        PermissionModel().create_default_permissions(user=User.DEFAULT_USER,
+        PermissionModel().create_default_permissions(user=User.DEFAULT_USER_NAME,
                                                      force=True)
         Session().commit()
 
     def setup_method(self, method):
         self.u1 = UserModel().create_or_update(
-            username=u'u1', password=u'qweqwe',
-            email=u'u1@example.com', firstname=u'u1', lastname=u'u1'
+            username='u1', password='qweqwe',
+            email='u1@example.com', firstname='u1', lastname='u1'
         )
         self.u2 = UserModel().create_or_update(
-            username=u'u2', password=u'qweqwe',
-            email=u'u2@example.com', firstname=u'u2', lastname=u'u2'
+            username='u2', password='qweqwe',
+            email='u2@example.com', firstname='u2', lastname='u2'
         )
         self.u3 = UserModel().create_or_update(
-            username=u'u3', password=u'qweqwe',
-            email=u'u3@example.com', firstname=u'u3', lastname=u'u3'
+            username='u3', password='qweqwe',
+            email='u3@example.com', firstname='u3', lastname='u3'
         )
         self.anon = User.get_default_user()
         self.a1 = UserModel().create_or_update(
-            username=u'a1', password=u'qweqwe',
-            email=u'a1@example.com', firstname=u'a1', lastname=u'a1', admin=True
+            username='a1', password='qweqwe',
+            email='a1@example.com', firstname='a1', lastname='a1', admin=True
         )
         Session().commit()
 
@@ -67,145 +68,113 @@
 
     def test_default_perms_set(self):
         u1_auth = AuthUser(user_id=self.u1.user_id)
-        perms = {
-            'repositories_groups': {},
-            'global': set(['hg.create.repository', 'repository.read',
-                           'hg.register.manual_activate']),
-            'repositories': {HG_REPO: 'repository.read'}
-        }
-        assert u1_auth.permissions['repositories'][HG_REPO] == perms['repositories'][HG_REPO]
+        assert u1_auth.permissions['repositories'][base.HG_REPO] == 'repository.read'
         new_perm = 'repository.write'
-        RepoModel().grant_user_permission(repo=HG_REPO, user=self.u1,
+        RepoModel().grant_user_permission(repo=base.HG_REPO, user=self.u1,
                                           perm=new_perm)
         Session().commit()
 
         u1_auth = AuthUser(user_id=self.u1.user_id)
-        assert u1_auth.permissions['repositories'][HG_REPO] == new_perm
+        assert u1_auth.permissions['repositories'][base.HG_REPO] == new_perm
 
     def test_default_admin_perms_set(self):
         a1_auth = AuthUser(user_id=self.a1.user_id)
-        perms = {
-            'repositories_groups': {},
-            'global': set(['hg.admin', 'hg.create.write_on_repogroup.true']),
-            'repositories': {HG_REPO: 'repository.admin'}
-        }
-        assert a1_auth.permissions['repositories'][HG_REPO] == perms['repositories'][HG_REPO]
+        assert a1_auth.permissions['repositories'][base.HG_REPO] == 'repository.admin'
         new_perm = 'repository.write'
-        RepoModel().grant_user_permission(repo=HG_REPO, user=self.a1,
+        RepoModel().grant_user_permission(repo=base.HG_REPO, user=self.a1,
                                           perm=new_perm)
         Session().commit()
         # cannot really downgrade admins permissions !? they still gets set as
         # admin !
         u1_auth = AuthUser(user_id=self.a1.user_id)
-        assert u1_auth.permissions['repositories'][HG_REPO] == perms['repositories'][HG_REPO]
+        assert u1_auth.permissions['repositories'][base.HG_REPO] == 'repository.admin'
 
     def test_default_group_perms(self):
-        self.g1 = fixture.create_repo_group(u'test1', skip_if_exists=True)
-        self.g2 = fixture.create_repo_group(u'test2', skip_if_exists=True)
+        self.g1 = fixture.create_repo_group('test1', skip_if_exists=True)
+        self.g2 = fixture.create_repo_group('test2', skip_if_exists=True)
         u1_auth = AuthUser(user_id=self.u1.user_id)
-        perms = {
-            'repositories_groups': {u'test1': 'group.read', u'test2': 'group.read'},
-            'global': set(Permission.DEFAULT_USER_PERMISSIONS),
-            'repositories': {HG_REPO: 'repository.read'}
-        }
-        assert u1_auth.permissions['repositories'][HG_REPO] == perms['repositories'][HG_REPO]
-        assert u1_auth.permissions['repositories_groups'] == perms['repositories_groups']
-        assert u1_auth.permissions['global'] == perms['global']
+        assert u1_auth.permissions['repositories'][base.HG_REPO] == 'repository.read'
+        assert u1_auth.permissions['repositories_groups'].get('test1') == 'group.read'
+        assert u1_auth.permissions['repositories_groups'].get('test2') == 'group.read'
+        assert u1_auth.permissions['global'] == set(Permission.DEFAULT_USER_PERMISSIONS)
 
     def test_default_admin_group_perms(self):
-        self.g1 = fixture.create_repo_group(u'test1', skip_if_exists=True)
-        self.g2 = fixture.create_repo_group(u'test2', skip_if_exists=True)
+        self.g1 = fixture.create_repo_group('test1', skip_if_exists=True)
+        self.g2 = fixture.create_repo_group('test2', skip_if_exists=True)
         a1_auth = AuthUser(user_id=self.a1.user_id)
-        perms = {
-            'repositories_groups': {u'test1': 'group.admin', u'test2': 'group.admin'},
-            'global': set(['hg.admin', 'hg.create.write_on_repogroup.true']),
-            'repositories': {HG_REPO: 'repository.admin'}
-        }
-
-        assert a1_auth.permissions['repositories'][HG_REPO] == perms['repositories'][HG_REPO]
-        assert a1_auth.permissions['repositories_groups'] == perms['repositories_groups']
+        assert a1_auth.permissions['repositories'][base.HG_REPO] == 'repository.admin'
+        assert a1_auth.permissions['repositories_groups'].get('test1') == 'group.admin'
+        assert a1_auth.permissions['repositories_groups'].get('test2') == 'group.admin'
 
     def test_propagated_permission_from_users_group_by_explicit_perms_exist(self):
         # make group
-        self.ug1 = fixture.create_user_group(u'G1')
+        self.ug1 = fixture.create_user_group('G1')
         UserGroupModel().add_user_to_group(self.ug1, self.u1)
 
         # set user permission none
-        RepoModel().grant_user_permission(repo=HG_REPO, user=self.u1, perm='repository.none')
+        RepoModel().grant_user_permission(repo=base.HG_REPO, user=self.u1, perm='repository.none')
         Session().commit()
         u1_auth = AuthUser(user_id=self.u1.user_id)
-        assert u1_auth.permissions['repositories'][HG_REPO] == 'repository.read' # inherit from default user
+        assert u1_auth.permissions['repositories'][base.HG_REPO] == 'repository.read' # inherit from default user
 
         # grant perm for group this should override permission from user
-        RepoModel().grant_user_group_permission(repo=HG_REPO,
+        RepoModel().grant_user_group_permission(repo=base.HG_REPO,
                                                  group_name=self.ug1,
                                                  perm='repository.write')
 
         # verify that user group permissions win
         u1_auth = AuthUser(user_id=self.u1.user_id)
-        assert u1_auth.permissions['repositories'][HG_REPO] == 'repository.write'
+        assert u1_auth.permissions['repositories'][base.HG_REPO] == 'repository.write'
 
     def test_propagated_permission_from_users_group(self):
         # make group
-        self.ug1 = fixture.create_user_group(u'G1')
+        self.ug1 = fixture.create_user_group('G1')
         UserGroupModel().add_user_to_group(self.ug1, self.u3)
 
         # grant perm for group this should override default permission from user
         new_perm_gr = 'repository.write'
-        RepoModel().grant_user_group_permission(repo=HG_REPO,
+        RepoModel().grant_user_group_permission(repo=base.HG_REPO,
                                                  group_name=self.ug1,
                                                  perm=new_perm_gr)
         # check perms
         u3_auth = AuthUser(user_id=self.u3.user_id)
-        perms = {
-            'repositories_groups': {},
-            'global': set(['hg.create.repository', 'repository.read',
-                           'hg.register.manual_activate']),
-            'repositories': {HG_REPO: 'repository.read'}
-        }
-        assert u3_auth.permissions['repositories'][HG_REPO] == new_perm_gr
-        assert u3_auth.permissions['repositories_groups'] == perms['repositories_groups']
+        assert u3_auth.permissions['repositories'][base.HG_REPO] == new_perm_gr
 
     def test_propagated_permission_from_users_group_lower_weight(self):
         # make group
-        self.ug1 = fixture.create_user_group(u'G1')
+        self.ug1 = fixture.create_user_group('G1')
         # add user to group
         UserGroupModel().add_user_to_group(self.ug1, self.u1)
 
         # set permission to lower
         new_perm_h = 'repository.write'
-        RepoModel().grant_user_permission(repo=HG_REPO, user=self.u1,
+        RepoModel().grant_user_permission(repo=base.HG_REPO, user=self.u1,
                                           perm=new_perm_h)
         Session().commit()
         u1_auth = AuthUser(user_id=self.u1.user_id)
-        assert u1_auth.permissions['repositories'][HG_REPO] == new_perm_h
+        assert u1_auth.permissions['repositories'][base.HG_REPO] == new_perm_h
 
         # grant perm for group this should NOT override permission from user
         # since it's lower than granted
         new_perm_l = 'repository.read'
-        RepoModel().grant_user_group_permission(repo=HG_REPO,
+        RepoModel().grant_user_group_permission(repo=base.HG_REPO,
                                                  group_name=self.ug1,
                                                  perm=new_perm_l)
         # check perms
         u1_auth = AuthUser(user_id=self.u1.user_id)
-        perms = {
-            'repositories_groups': {},
-            'global': set(['hg.create.repository', 'repository.read',
-                           'hg.register.manual_activate']),
-            'repositories': {HG_REPO: 'repository.write'}
-        }
-        assert u1_auth.permissions['repositories'][HG_REPO] == new_perm_h
-        assert u1_auth.permissions['repositories_groups'] == perms['repositories_groups']
+        assert u1_auth.permissions['repositories'][base.HG_REPO] == new_perm_h
 
     def test_repo_in_group_permissions(self):
-        self.g1 = fixture.create_repo_group(u'group1', skip_if_exists=True)
-        self.g2 = fixture.create_repo_group(u'group2', skip_if_exists=True)
+        self.g1 = fixture.create_repo_group('group1', skip_if_exists=True)
+        self.g2 = fixture.create_repo_group('group2', skip_if_exists=True)
         # both perms should be read !
         u1_auth = AuthUser(user_id=self.u1.user_id)
-        assert u1_auth.permissions['repositories_groups'] == {u'group1': u'group.read', u'group2': u'group.read'}
+        assert u1_auth.permissions['repositories_groups'].get('group1') == 'group.read'
+        assert u1_auth.permissions['repositories_groups'].get('group2') == 'group.read'
 
         a1_auth = AuthUser(user_id=self.anon.user_id)
-        assert a1_auth.permissions['repositories_groups'] == {u'group1': u'group.read', u'group2': u'group.read'}
+        assert a1_auth.permissions['repositories_groups'].get('group1') == 'group.read'
+        assert a1_auth.permissions['repositories_groups'].get('group2') == 'group.read'
 
         # Change perms to none for both groups
         RepoGroupModel().grant_user_permission(repo_group=self.g1,
@@ -216,23 +185,27 @@
                                                perm='group.none')
 
         u1_auth = AuthUser(user_id=self.u1.user_id)
-        assert u1_auth.permissions['repositories_groups'] == {u'group1': u'group.none', u'group2': u'group.none'}
+        assert u1_auth.permissions['repositories_groups'].get('group1') == 'group.none'
+        assert u1_auth.permissions['repositories_groups'].get('group2') == 'group.none'
 
         a1_auth = AuthUser(user_id=self.anon.user_id)
-        assert a1_auth.permissions['repositories_groups'] == {u'group1': u'group.none', u'group2': u'group.none'}
+        assert a1_auth.permissions['repositories_groups'].get('group1') == 'group.none'
+        assert a1_auth.permissions['repositories_groups'].get('group2') == 'group.none'
 
         # add repo to group
-        name = RepoGroup.url_sep().join([self.g1.group_name, 'test_perm'])
+        name = db.URL_SEP.join([self.g1.group_name, 'test_perm'])
         self.test_repo = fixture.create_repo(name=name,
                                              repo_type='hg',
                                              repo_group=self.g1,
                                              cur_user=self.u1,)
 
         u1_auth = AuthUser(user_id=self.u1.user_id)
-        assert u1_auth.permissions['repositories_groups'] == {u'group1': u'group.none', u'group2': u'group.none'}
+        assert u1_auth.permissions['repositories_groups'].get('group1') == 'group.none'
+        assert u1_auth.permissions['repositories_groups'].get('group2') == 'group.none'
 
         a1_auth = AuthUser(user_id=self.anon.user_id)
-        assert a1_auth.permissions['repositories_groups'] == {u'group1': u'group.none', u'group2': u'group.none'}
+        assert a1_auth.permissions['repositories_groups'].get('group1') == 'group.none'
+        assert a1_auth.permissions['repositories_groups'].get('group2') == 'group.none'
 
         # grant permission for u2 !
         RepoGroupModel().grant_user_permission(repo_group=self.g1, user=self.u2,
@@ -243,27 +216,30 @@
         assert self.u1 != self.u2
         # u1 and anon should have not change perms while u2 should !
         u1_auth = AuthUser(user_id=self.u1.user_id)
-        assert u1_auth.permissions['repositories_groups'] == {u'group1': u'group.none', u'group2': u'group.none'}
+        assert u1_auth.permissions['repositories_groups'].get('group1') == 'group.none'
+        assert u1_auth.permissions['repositories_groups'].get('group2') == 'group.none'
 
         u2_auth = AuthUser(user_id=self.u2.user_id)
-        assert u2_auth.permissions['repositories_groups'] == {u'group1': u'group.read', u'group2': u'group.read'}
+        assert u2_auth.permissions['repositories_groups'].get('group1') == 'group.read'
+        assert u2_auth.permissions['repositories_groups'].get('group2') == 'group.read'
 
         a1_auth = AuthUser(user_id=self.anon.user_id)
-        assert a1_auth.permissions['repositories_groups'] == {u'group1': u'group.none', u'group2': u'group.none'}
+        assert a1_auth.permissions['repositories_groups'].get('group1') == 'group.none'
+        assert a1_auth.permissions['repositories_groups'].get('group2') == 'group.none'
 
     def test_repo_group_user_as_user_group_member(self):
         # create Group1
-        self.g1 = fixture.create_repo_group(u'group1', skip_if_exists=True)
+        self.g1 = fixture.create_repo_group('group1', skip_if_exists=True)
         a1_auth = AuthUser(user_id=self.anon.user_id)
 
-        assert a1_auth.permissions['repositories_groups'] == {u'group1': u'group.read'}
+        assert a1_auth.permissions['repositories_groups'].get('group1') == 'group.read'
 
         # set default permission to none
         RepoGroupModel().grant_user_permission(repo_group=self.g1,
                                                user=self.anon,
                                                perm='group.none')
         # make group
-        self.ug1 = fixture.create_user_group(u'G1')
+        self.ug1 = fixture.create_user_group('G1')
         # add user to group
         UserGroupModel().add_user_to_group(self.ug1, self.u1)
         Session().commit()
@@ -275,10 +251,10 @@
 
         # check his permissions
         a1_auth = AuthUser(user_id=self.anon.user_id)
-        assert a1_auth.permissions['repositories_groups'] == {u'group1': u'group.none'}
+        assert a1_auth.permissions['repositories_groups'].get('group1') == 'group.none'
 
         u1_auth = AuthUser(user_id=self.u1.user_id)
-        assert u1_auth.permissions['repositories_groups'] == {u'group1': u'group.none'}
+        assert u1_auth.permissions['repositories_groups'].get('group1') == 'group.none'
 
         # grant ug1 read permissions for
         RepoGroupModel().grant_user_group_permission(repo_group=self.g1,
@@ -294,10 +270,10 @@
 
         a1_auth = AuthUser(user_id=self.anon.user_id)
 
-        assert a1_auth.permissions['repositories_groups'] == {u'group1': u'group.none'}
+        assert a1_auth.permissions['repositories_groups'].get('group1') == 'group.none'
 
         u1_auth = AuthUser(user_id=self.u1.user_id)
-        assert u1_auth.permissions['repositories_groups'] == {u'group1': u'group.read'}
+        assert u1_auth.permissions['repositories_groups'].get('group1') == 'group.read'
 
     def test_inherit_nice_permissions_from_default_user(self):
         user_model = UserModel()
@@ -388,7 +364,7 @@
     def test_inactive_user_group_does_not_affect_global_permissions(self):
         # Add user to inactive user group, set specific permissions on user
         # group and and verify it really is inactive.
-        self.ug1 = fixture.create_user_group(u'G1')
+        self.ug1 = fixture.create_user_group('G1')
         user_group_model = UserGroupModel()
         user_group_model.add_user_to_group(self.ug1, self.u1)
         user_group_model.update(self.ug1, {'users_group_active': False})
@@ -420,7 +396,7 @@
     def test_inactive_user_group_does_not_affect_global_permissions_inverse(self):
         # Add user to inactive user group, set specific permissions on user
         # group and and verify it really is inactive.
-        self.ug1 = fixture.create_user_group(u'G1')
+        self.ug1 = fixture.create_user_group('G1')
         user_group_model = UserGroupModel()
         user_group_model.add_user_to_group(self.ug1, self.u1)
         user_group_model.update(self.ug1, {'users_group_active': False})
@@ -450,14 +426,14 @@
                               'hg.create.write_on_repogroup.true'])
 
     def test_inactive_user_group_does_not_affect_repo_permissions(self):
-        self.ug1 = fixture.create_user_group(u'G1')
+        self.ug1 = fixture.create_user_group('G1')
         user_group_model = UserGroupModel()
         user_group_model.add_user_to_group(self.ug1, self.u1)
         user_group_model.update(self.ug1, {'users_group_active': False})
 
         # note: make u2 repo owner rather than u1, because the owner always has
         # admin permissions
-        self.test_repo = fixture.create_repo(name=u'myownrepo',
+        self.test_repo = fixture.create_repo(name='myownrepo',
                                              repo_type='hg',
                                              cur_user=self.u2)
 
@@ -474,14 +450,14 @@
         assert u1_auth.permissions['repositories']['myownrepo'] == 'repository.write'
 
     def test_inactive_user_group_does_not_affect_repo_permissions_inverse(self):
-        self.ug1 = fixture.create_user_group(u'G1')
+        self.ug1 = fixture.create_user_group('G1')
         user_group_model = UserGroupModel()
         user_group_model.add_user_to_group(self.ug1, self.u1)
         user_group_model.update(self.ug1, {'users_group_active': False})
 
         # note: make u2 repo owner rather than u1, because the owner always has
         # admin permissions
-        self.test_repo = fixture.create_repo(name=u'myownrepo',
+        self.test_repo = fixture.create_repo(name='myownrepo',
                                              repo_type='hg',
                                              cur_user=self.u2)
 
@@ -498,12 +474,12 @@
         assert u1_auth.permissions['repositories']['myownrepo'] == 'repository.admin'
 
     def test_inactive_user_group_does_not_affect_repo_group_permissions(self):
-        self.ug1 = fixture.create_user_group(u'G1')
+        self.ug1 = fixture.create_user_group('G1')
         user_group_model = UserGroupModel()
         user_group_model.add_user_to_group(self.ug1, self.u1)
         user_group_model.update(self.ug1, {'users_group_active': False})
 
-        self.g1 = fixture.create_repo_group(u'group1', skip_if_exists=True)
+        self.g1 = fixture.create_repo_group('group1', skip_if_exists=True)
 
         # enable admin access for user group on repo group
         RepoGroupModel().grant_user_group_permission(self.g1,
@@ -515,15 +491,15 @@
                                                perm='group.write')
         Session().commit()
         u1_auth = AuthUser(user_id=self.u1.user_id)
-        assert u1_auth.permissions['repositories_groups'] == {u'group1': u'group.write'}
+        assert u1_auth.permissions['repositories_groups'].get('group1') == 'group.write'
 
     def test_inactive_user_group_does_not_affect_repo_group_permissions_inverse(self):
-        self.ug1 = fixture.create_user_group(u'G1')
+        self.ug1 = fixture.create_user_group('G1')
         user_group_model = UserGroupModel()
         user_group_model.add_user_to_group(self.ug1, self.u1)
         user_group_model.update(self.ug1, {'users_group_active': False})
 
-        self.g1 = fixture.create_repo_group(u'group1', skip_if_exists=True)
+        self.g1 = fixture.create_repo_group('group1', skip_if_exists=True)
 
         # enable only write access for user group on repo group
         RepoGroupModel().grant_user_group_permission(self.g1,
@@ -535,15 +511,15 @@
                                                perm='group.admin')
         Session().commit()
         u1_auth = AuthUser(user_id=self.u1.user_id)
-        assert u1_auth.permissions['repositories_groups'] == {u'group1': u'group.admin'}
+        assert u1_auth.permissions['repositories_groups'].get('group1') == 'group.admin'
 
     def test_inactive_user_group_does_not_affect_user_group_permissions(self):
-        self.ug1 = fixture.create_user_group(u'G1')
+        self.ug1 = fixture.create_user_group('G1')
         user_group_model = UserGroupModel()
         user_group_model.add_user_to_group(self.ug1, self.u1)
         user_group_model.update(self.ug1, {'users_group_active': False})
 
-        self.ug2 = fixture.create_user_group(u'G2')
+        self.ug2 = fixture.create_user_group('G2')
 
         # enable admin access for user group on user group
         UserGroupModel().grant_user_group_permission(self.ug2,
@@ -555,16 +531,16 @@
                                                perm='usergroup.write')
         Session().commit()
         u1_auth = AuthUser(user_id=self.u1.user_id)
-        assert u1_auth.permissions['user_groups'][u'G1'] == u'usergroup.read'
-        assert u1_auth.permissions['user_groups'][u'G2'] == u'usergroup.write'
+        assert u1_auth.permissions['user_groups']['G1'] == 'usergroup.read'
+        assert u1_auth.permissions['user_groups']['G2'] == 'usergroup.write'
 
     def test_inactive_user_group_does_not_affect_user_group_permissions_inverse(self):
-        self.ug1 = fixture.create_user_group(u'G1')
+        self.ug1 = fixture.create_user_group('G1')
         user_group_model = UserGroupModel()
         user_group_model.add_user_to_group(self.ug1, self.u1)
         user_group_model.update(self.ug1, {'users_group_active': False})
 
-        self.ug2 = fixture.create_user_group(u'G2')
+        self.ug2 = fixture.create_user_group('G2')
 
         # enable only write access for user group on user group
         UserGroupModel().grant_user_group_permission(self.ug2,
@@ -576,12 +552,12 @@
                                                perm='usergroup.admin')
         Session().commit()
         u1_auth = AuthUser(user_id=self.u1.user_id)
-        assert u1_auth.permissions['user_groups'][u'G1'] == u'usergroup.read'
-        assert u1_auth.permissions['user_groups'][u'G2'] == u'usergroup.admin'
+        assert u1_auth.permissions['user_groups']['G1'] == 'usergroup.read'
+        assert u1_auth.permissions['user_groups']['G2'] == 'usergroup.admin'
 
     def test_owner_permissions_doesnot_get_overwritten_by_group(self):
         # create repo as USER,
-        self.test_repo = fixture.create_repo(name=u'myownrepo',
+        self.test_repo = fixture.create_repo(name='myownrepo',
                                              repo_type='hg',
                                              cur_user=self.u1)
 
@@ -589,7 +565,7 @@
         u1_auth = AuthUser(user_id=self.u1.user_id)
         assert u1_auth.permissions['repositories']['myownrepo'] == 'repository.admin'
         # set his permission as user group, he should still be admin
-        self.ug1 = fixture.create_user_group(u'G1')
+        self.ug1 = fixture.create_user_group('G1')
         UserGroupModel().add_user_to_group(self.ug1, self.u1)
         RepoModel().grant_user_group_permission(self.test_repo,
                                                  group_name=self.ug1,
@@ -601,7 +577,7 @@
 
     def test_owner_permissions_doesnot_get_overwritten_by_others(self):
         # create repo as USER,
-        self.test_repo = fixture.create_repo(name=u'myownrepo',
+        self.test_repo = fixture.create_repo(name='myownrepo',
                                              repo_type='hg',
                                              cur_user=self.u1)
 
@@ -641,7 +617,7 @@
         PermissionModel().create_default_permissions(user=self.u1)
         self._test_def_perm_equal(user=self.u1)
 
-    @parametrize('perm,modify_to', [
+    @base.parametrize('perm,modify_to', [
         ('repository.read', 'repository.none'),
         ('group.read', 'group.none'),
         ('usergroup.read', 'usergroup.none'),
--- a/kallithea/tests/models/test_repo_groups.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/models/test_repo_groups.py	Mon May 04 19:24:04 2020 +0200
@@ -3,18 +3,19 @@
 import pytest
 from sqlalchemy.exc import IntegrityError
 
+from kallithea.model import db
 from kallithea.model.db import RepoGroup
 from kallithea.model.meta import Session
 from kallithea.model.repo import RepoModel
 from kallithea.model.repo_group import RepoGroupModel
-from kallithea.tests.base import *
+from kallithea.tests import base
 from kallithea.tests.fixture import Fixture
 
 
 fixture = Fixture()
 
 
-def _update_repo_group(id_, group_name, desc=u'desc', parent_id=None):
+def _update_repo_group(id_, group_name, desc='desc', parent_id=None):
     form_data = dict(
         group_name=group_name,
         group_description=desc,
@@ -34,12 +35,12 @@
     return r
 
 
-class TestRepoGroups(TestController):
+class TestRepoGroups(base.TestController):
 
     def setup_method(self, method):
-        self.g1 = fixture.create_repo_group(u'test1', skip_if_exists=True)
-        self.g2 = fixture.create_repo_group(u'test2', skip_if_exists=True)
-        self.g3 = fixture.create_repo_group(u'test3', skip_if_exists=True)
+        self.g1 = fixture.create_repo_group('test1', skip_if_exists=True)
+        self.g2 = fixture.create_repo_group('test2', skip_if_exists=True)
+        self.g3 = fixture.create_repo_group('test3', skip_if_exists=True)
 
     def teardown_method(self, method):
         Session.remove()
@@ -48,7 +49,7 @@
         """
         Checks the path for existence !
         """
-        path = [TESTS_TMP_PATH] + list(path)
+        path = [base.TESTS_TMP_PATH] + list(path)
         path = os.path.join(*path)
         return os.path.isdir(path)
 
@@ -56,93 +57,92 @@
         RepoGroupModel().delete(id_)
 
     def test_create_group(self):
-        g = fixture.create_repo_group(u'newGroup')
+        g = fixture.create_repo_group('newGroup')
         Session().commit()
         assert g.full_path == 'newGroup'
 
         assert self.__check_path('newGroup')
 
-    def test_create_same_name_group(self):
+        # test_create_same_name_group
         with pytest.raises(IntegrityError):
-            fixture.create_repo_group(u'newGroup')
+            fixture.create_repo_group('newGroup')
         Session().rollback()
 
     def test_same_subgroup(self):
-        sg1 = fixture.create_repo_group(u'sub1', parent_group_id=self.g1.group_id)
+        sg1 = fixture.create_repo_group('sub1', parent_group_id=self.g1.group_id)
         assert sg1.parent_group == self.g1
         assert sg1.full_path == 'test1/sub1'
         assert self.__check_path('test1', 'sub1')
 
-        ssg1 = fixture.create_repo_group(u'subsub1', parent_group_id=sg1.group_id)
+        ssg1 = fixture.create_repo_group('subsub1', parent_group_id=sg1.group_id)
         assert ssg1.parent_group == sg1
         assert ssg1.full_path == 'test1/sub1/subsub1'
         assert self.__check_path('test1', 'sub1', 'subsub1')
 
     def test_remove_group(self):
-        sg1 = fixture.create_repo_group(u'deleteme')
+        sg1 = fixture.create_repo_group('deleteme')
         self.__delete_group(sg1.group_id)
 
         assert RepoGroup.get(sg1.group_id) is None
         assert not self.__check_path('deteteme')
 
-        sg1 = fixture.create_repo_group(u'deleteme', parent_group_id=self.g1.group_id)
+        sg1 = fixture.create_repo_group('deleteme', parent_group_id=self.g1.group_id)
         self.__delete_group(sg1.group_id)
 
         assert RepoGroup.get(sg1.group_id) is None
         assert not self.__check_path('test1', 'deteteme')
 
     def test_rename_single_group(self):
-        sg1 = fixture.create_repo_group(u'initial')
+        sg1 = fixture.create_repo_group('initial')
 
-        new_sg1 = _update_repo_group(sg1.group_id, u'after')
+        new_sg1 = _update_repo_group(sg1.group_id, 'after')
         assert self.__check_path('after')
-        assert RepoGroup.get_by_group_name(u'initial') is None
+        assert RepoGroup.get_by_group_name('initial') is None
 
     def test_update_group_parent(self):
 
-        sg1 = fixture.create_repo_group(u'initial', parent_group_id=self.g1.group_id)
+        sg1 = fixture.create_repo_group('initial', parent_group_id=self.g1.group_id)
 
-        new_sg1 = _update_repo_group(sg1.group_id, u'after', parent_id=self.g1.group_id)
+        new_sg1 = _update_repo_group(sg1.group_id, 'after', parent_id=self.g1.group_id)
         assert self.__check_path('test1', 'after')
-        assert RepoGroup.get_by_group_name(u'test1/initial') is None
+        assert RepoGroup.get_by_group_name('test1/initial') is None
 
-        new_sg1 = _update_repo_group(sg1.group_id, u'after', parent_id=self.g3.group_id)
+        new_sg1 = _update_repo_group(sg1.group_id, 'after', parent_id=self.g3.group_id)
         assert self.__check_path('test3', 'after')
-        assert RepoGroup.get_by_group_name(u'test3/initial') == None
+        assert RepoGroup.get_by_group_name('test3/initial') == None
 
-        new_sg1 = _update_repo_group(sg1.group_id, u'hello')
+        new_sg1 = _update_repo_group(sg1.group_id, 'hello')
         assert self.__check_path('hello')
 
-        assert RepoGroup.get_by_group_name(u'hello') == new_sg1
+        assert RepoGroup.get_by_group_name('hello') == new_sg1
 
     def test_subgrouping_with_repo(self):
 
-        g1 = fixture.create_repo_group(u'g1')
-        g2 = fixture.create_repo_group(u'g2')
+        g1 = fixture.create_repo_group('g1')
+        g2 = fixture.create_repo_group('g2')
         # create new repo
-        r = fixture.create_repo(u'john')
+        r = fixture.create_repo('john')
 
         assert r.repo_name == 'john'
         # put repo into group
-        r = _update_repo(u'john', repo_group=g1.group_id)
+        r = _update_repo('john', repo_group=g1.group_id)
         Session().commit()
         assert r.repo_name == 'g1/john'
 
-        _update_repo_group(g1.group_id, u'g1', parent_id=g2.group_id)
+        _update_repo_group(g1.group_id, 'g1', parent_id=g2.group_id)
         assert self.__check_path('g2', 'g1')
 
         # test repo
-        assert r.repo_name == RepoGroup.url_sep().join(['g2', 'g1',
-                                                                r.just_name])
+        assert r.repo_name == db.URL_SEP.join(['g2', 'g1', r.just_name])
 
     def test_move_to_root(self):
-        g1 = fixture.create_repo_group(u't11')
-        g2 = fixture.create_repo_group(u't22', parent_group_id=g1.group_id)
+        g1 = fixture.create_repo_group('t11')
+        g2 = fixture.create_repo_group('t22', parent_group_id=g1.group_id)
 
         assert g2.full_path == 't11/t22'
         assert self.__check_path('t11', 't22')
 
-        g2 = _update_repo_group(g2.group_id, u'g22', parent_id=None)
+        g2 = _update_repo_group(g2.group_id, 'g22', parent_id=None)
         Session().commit()
 
         assert g2.group_name == 'g22'
@@ -152,14 +152,14 @@
         assert self.__check_path('g22')
 
     def test_rename_top_level_group_in_nested_setup(self):
-        g1 = fixture.create_repo_group(u'L1')
-        g2 = fixture.create_repo_group(u'L2', parent_group_id=g1.group_id)
-        g3 = fixture.create_repo_group(u'L3', parent_group_id=g2.group_id)
+        g1 = fixture.create_repo_group('L1')
+        g2 = fixture.create_repo_group('L2', parent_group_id=g1.group_id)
+        g3 = fixture.create_repo_group('L3', parent_group_id=g2.group_id)
 
-        r = fixture.create_repo(u'L1/L2/L3/L3_REPO', repo_group=g3.group_id)
+        r = fixture.create_repo('L1/L2/L3/L3_REPO', repo_group=g3.group_id)
 
         ## rename L1 all groups should be now changed
-        _update_repo_group(g1.group_id, u'L1_NEW')
+        _update_repo_group(g1.group_id, 'L1_NEW')
         Session().commit()
         assert g1.full_path == 'L1_NEW'
         assert g2.full_path == 'L1_NEW/L2'
@@ -167,14 +167,14 @@
         assert r.repo_name == 'L1_NEW/L2/L3/L3_REPO'
 
     def test_change_parent_of_top_level_group_in_nested_setup(self):
-        g1 = fixture.create_repo_group(u'R1')
-        g2 = fixture.create_repo_group(u'R2', parent_group_id=g1.group_id)
-        g3 = fixture.create_repo_group(u'R3', parent_group_id=g2.group_id)
-        g4 = fixture.create_repo_group(u'R1_NEW')
+        g1 = fixture.create_repo_group('R1')
+        g2 = fixture.create_repo_group('R2', parent_group_id=g1.group_id)
+        g3 = fixture.create_repo_group('R3', parent_group_id=g2.group_id)
+        g4 = fixture.create_repo_group('R1_NEW')
 
-        r = fixture.create_repo(u'R1/R2/R3/R3_REPO', repo_group=g3.group_id)
+        r = fixture.create_repo('R1/R2/R3/R3_REPO', repo_group=g3.group_id)
         ## rename L1 all groups should be now changed
-        _update_repo_group(g1.group_id, u'R1', parent_id=g4.group_id)
+        _update_repo_group(g1.group_id, 'R1', parent_id=g4.group_id)
         Session().commit()
         assert g1.full_path == 'R1_NEW/R1'
         assert g2.full_path == 'R1_NEW/R1/R2'
@@ -182,15 +182,15 @@
         assert r.repo_name == 'R1_NEW/R1/R2/R3/R3_REPO'
 
     def test_change_parent_of_top_level_group_in_nested_setup_with_rename(self):
-        g1 = fixture.create_repo_group(u'X1')
-        g2 = fixture.create_repo_group(u'X2', parent_group_id=g1.group_id)
-        g3 = fixture.create_repo_group(u'X3', parent_group_id=g2.group_id)
-        g4 = fixture.create_repo_group(u'X1_NEW')
+        g1 = fixture.create_repo_group('X1')
+        g2 = fixture.create_repo_group('X2', parent_group_id=g1.group_id)
+        g3 = fixture.create_repo_group('X3', parent_group_id=g2.group_id)
+        g4 = fixture.create_repo_group('X1_NEW')
 
-        r = fixture.create_repo(u'X1/X2/X3/X3_REPO', repo_group=g3.group_id)
+        r = fixture.create_repo('X1/X2/X3/X3_REPO', repo_group=g3.group_id)
 
         ## rename L1 all groups should be now changed
-        _update_repo_group(g1.group_id, u'X1_PRIM', parent_id=g4.group_id)
+        _update_repo_group(g1.group_id, 'X1_PRIM', parent_id=g4.group_id)
         Session().commit()
         assert g1.full_path == 'X1_NEW/X1_PRIM'
         assert g2.full_path == 'X1_NEW/X1_PRIM/X2'
--- a/kallithea/tests/models/test_repos.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/models/test_repos.py	Mon May 04 19:24:04 2020 +0200
@@ -4,78 +4,78 @@
 from kallithea.model.db import Repository
 from kallithea.model.meta import Session
 from kallithea.model.repo import RepoModel
-from kallithea.tests.base import *
+from kallithea.tests import base
 from kallithea.tests.fixture import Fixture
 
 
 fixture = Fixture()
 
 
-class TestRepos(TestController):
+class TestRepos(base.TestController):
 
     def teardown_method(self, method):
         Session.remove()
 
     def test_remove_repo(self):
-        repo = fixture.create_repo(name=u'test-repo-1')
+        repo = fixture.create_repo(name='test-repo-1')
         Session().commit()
 
         RepoModel().delete(repo=repo)
         Session().commit()
 
-        assert Repository.get_by_repo_name(repo_name=u'test-repo-1') is None
+        assert Repository.get_by_repo_name(repo_name='test-repo-1') is None
 
     def test_remove_repo_repo_raises_exc_when_attached_forks(self):
-        repo = fixture.create_repo(name=u'test-repo-1')
+        repo = fixture.create_repo(name='test-repo-1')
         Session().commit()
 
-        fixture.create_fork(repo.repo_name, u'test-repo-fork-1')
+        fixture.create_fork(repo.repo_name, 'test-repo-fork-1')
         Session().commit()
 
         with pytest.raises(AttachedForksError):
             RepoModel().delete(repo=repo)
         # cleanup
-        RepoModel().delete(repo=u'test-repo-fork-1')
-        RepoModel().delete(repo=u'test-repo-1')
+        RepoModel().delete(repo='test-repo-fork-1')
+        RepoModel().delete(repo='test-repo-1')
         Session().commit()
 
     def test_remove_repo_delete_forks(self):
-        repo = fixture.create_repo(name=u'test-repo-1')
+        repo = fixture.create_repo(name='test-repo-1')
         Session().commit()
 
-        fork = fixture.create_fork(repo.repo_name, u'test-repo-fork-1')
+        fork = fixture.create_fork(repo.repo_name, 'test-repo-fork-1')
         Session().commit()
 
         # fork of fork
-        fixture.create_fork(fork.repo_name, u'test-repo-fork-fork-1')
+        fixture.create_fork(fork.repo_name, 'test-repo-fork-fork-1')
         Session().commit()
 
         RepoModel().delete(repo=repo, forks='delete')
         Session().commit()
 
-        assert Repository.get_by_repo_name(repo_name=u'test-repo-1') is None
-        assert Repository.get_by_repo_name(repo_name=u'test-repo-fork-1') is None
-        assert Repository.get_by_repo_name(repo_name=u'test-repo-fork-fork-1') is None
+        assert Repository.get_by_repo_name(repo_name='test-repo-1') is None
+        assert Repository.get_by_repo_name(repo_name='test-repo-fork-1') is None
+        assert Repository.get_by_repo_name(repo_name='test-repo-fork-fork-1') is None
 
     def test_remove_repo_detach_forks(self):
-        repo = fixture.create_repo(name=u'test-repo-1')
+        repo = fixture.create_repo(name='test-repo-1')
         Session().commit()
 
-        fork = fixture.create_fork(repo.repo_name, u'test-repo-fork-1')
+        fork = fixture.create_fork(repo.repo_name, 'test-repo-fork-1')
         Session().commit()
 
         # fork of fork
-        fixture.create_fork(fork.repo_name, u'test-repo-fork-fork-1')
+        fixture.create_fork(fork.repo_name, 'test-repo-fork-fork-1')
         Session().commit()
 
         RepoModel().delete(repo=repo, forks='detach')
         Session().commit()
 
         try:
-            assert Repository.get_by_repo_name(repo_name=u'test-repo-1') is None
-            assert Repository.get_by_repo_name(repo_name=u'test-repo-fork-1') is not None
-            assert Repository.get_by_repo_name(repo_name=u'test-repo-fork-fork-1') is not None
+            assert Repository.get_by_repo_name(repo_name='test-repo-1') is None
+            assert Repository.get_by_repo_name(repo_name='test-repo-fork-1') is not None
+            assert Repository.get_by_repo_name(repo_name='test-repo-fork-fork-1') is not None
         finally:
-            RepoModel().delete(repo=u'test-repo-fork-fork-1')
-            RepoModel().delete(repo=u'test-repo-fork-1')
+            RepoModel().delete(repo='test-repo-fork-fork-1')
+            RepoModel().delete(repo='test-repo-fork-1')
             Session().commit()
--- a/kallithea/tests/models/test_settings.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/models/test_settings.py	Mon May 04 19:24:04 2020 +0200
@@ -35,12 +35,12 @@
     setting = Setting.create_or_update(name, 'spam', type='list')
     Session().flush() # must flush so we can delete it below
     try:
-        assert setting.app_settings_value == [u'spam']
+        assert setting.app_settings_value == ['spam']
         # Assign back setting value.
         setting.app_settings_value = setting.app_settings_value
         # Quirk: value is stringified on write and listified on read.
-        assert setting.app_settings_value == ["[u'spam']"]
+        assert setting.app_settings_value == ["['spam']"]
         setting.app_settings_value = setting.app_settings_value
-        assert setting.app_settings_value == ["[u\"[u'spam']\"]"]
+        assert setting.app_settings_value == ["[\"['spam']\"]"]
     finally:
         Session().delete(setting)
--- a/kallithea/tests/models/test_user_group_permissions_on_repo_groups.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/models/test_user_group_permissions_on_repo_groups.py	Mon May 04 19:24:04 2020 +0200
@@ -16,7 +16,7 @@
 _get_group_perms = None
 
 
-def permissions_setup_func(group_name=u'g0', perm='group.read', recursive='all'):
+def permissions_setup_func(group_name='g0', perm='group.read', recursive='all'):
     """
     Resets all permissions to perm attribute
     """
@@ -43,7 +43,7 @@
     Session().commit()
     test_u2_id = test_u2.user_id
 
-    gr1 = fixture.create_user_group(u'perms_group_1')
+    gr1 = fixture.create_user_group('perms_group_1')
     Session().commit()
     test_u2_gr_id = gr1.users_group_id
     UserGroupModel().add_user_to_group(gr1, user=test_u2_id)
@@ -57,13 +57,13 @@
 
 def teardown_module():
     _destroy_project_tree(test_u2_id)
-    fixture.destroy_user_group(u'perms_group_1')
+    fixture.destroy_user_group('perms_group_1')
 
 
 def test_user_permissions_on_group_without_recursive_mode():
     # set permission to g0 non-recursive mode
     recursive = 'none'
-    group = u'g0'
+    group = 'g0'
     permissions_setup_func(group, 'group.write', recursive=recursive)
 
     items = [x for x in _get_repo_perms(group, recursive)]
@@ -82,7 +82,7 @@
 def test_user_permissions_on_group_without_recursive_mode_subgroup():
     # set permission to g0 non-recursive mode
     recursive = 'none'
-    group = u'g0/g0_1'
+    group = 'g0/g0_1'
     permissions_setup_func(group, 'group.write', recursive=recursive)
 
     items = [x for x in _get_repo_perms(group, recursive)]
@@ -103,7 +103,7 @@
     # set permission to g0 recursive mode, all children including
     # other repos and groups should have this permission now set !
     recursive = 'all'
-    group = u'g0'
+    group = 'g0'
     permissions_setup_func(group, 'group.write', recursive=recursive)
 
     repo_items = [x for x in _get_repo_perms(group, recursive)]
@@ -120,7 +120,7 @@
 def test_user_permissions_on_group_with_recursive_mode_inner_group():
     ## set permission to g0_3 group to none
     recursive = 'all'
-    group = u'g0/g0_3'
+    group = 'g0/g0_3'
     permissions_setup_func(group, 'group.none', recursive=recursive)
 
     repo_items = [x for x in _get_repo_perms(group, recursive)]
@@ -137,7 +137,7 @@
 def test_user_permissions_on_group_with_recursive_mode_deepest():
     ## set permission to g0/g0_1/g0_1_1 group to write
     recursive = 'all'
-    group = u'g0/g0_1/g0_1_1'
+    group = 'g0/g0_1/g0_1_1'
     permissions_setup_func(group, 'group.write', recursive=recursive)
 
     repo_items = [x for x in _get_repo_perms(group, recursive)]
@@ -154,7 +154,7 @@
 def test_user_permissions_on_group_with_recursive_mode_only_with_repos():
     ## set permission to g0/g0_2 group to admin
     recursive = 'all'
-    group = u'g0/g0_2'
+    group = 'g0/g0_2'
     permissions_setup_func(group, 'group.admin', recursive=recursive)
 
     repo_items = [x for x in _get_repo_perms(group, recursive)]
@@ -171,7 +171,7 @@
 def test_user_permissions_on_group_with_recursive_mode_on_repos():
     # set permission to g0/g0_1 with recursive mode on just repositories
     recursive = 'repos'
-    group = u'g0/g0_1'
+    group = 'g0/g0_1'
     perm = 'group.write'
     permissions_setup_func(group, perm, recursive=recursive)
 
@@ -195,7 +195,7 @@
 def test_user_permissions_on_group_with_recursive_mode_on_repo_groups():
     # set permission to g0/g0_1 with recursive mode on just repository groups
     recursive = 'groups'
-    group = u'g0/g0_1'
+    group = 'g0/g0_1'
     perm = 'group.none'
     permissions_setup_func(group, perm, recursive=recursive)
 
--- a/kallithea/tests/models/test_user_groups.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/models/test_user_groups.py	Mon May 04 19:24:04 2020 +0200
@@ -1,14 +1,14 @@
 from kallithea.model.db import User, UserGroup
 from kallithea.model.meta import Session
 from kallithea.model.user_group import UserGroupModel
-from kallithea.tests.base import *
+from kallithea.tests import base
 from kallithea.tests.fixture import Fixture
 
 
 fixture = Fixture()
 
 
-class TestUserGroups(TestController):
+class TestUserGroups(base.TestController):
 
     def teardown_method(self, method):
         # delete all groups
@@ -16,14 +16,14 @@
             fixture.destroy_user_group(gr)
         Session().commit()
 
-    @parametrize('pre_existing,regular_should_be,external_should_be,groups,expected', [
+    @base.parametrize('pre_existing,regular_should_be,external_should_be,groups,expected', [
         ([], [], [], [], []),
-        ([], [u'regular'], [], [], [u'regular']),  # no changes of regular
-        ([u'some_other'], [], [], [u'some_other'], []),   # not added to regular group
-        ([], [u'regular'], [u'container'], [u'container'], [u'regular', u'container']),
-        ([], [u'regular'], [], [u'container', u'container2'], [u'regular', u'container', u'container2']),
-        ([], [u'regular'], [u'other'], [], [u'regular']),  # remove not used
-        ([u'some_other'], [u'regular'], [u'other', u'container'], [u'container', u'container2'], [u'regular', u'container', u'container2']),
+        ([], ['regular'], [], [], ['regular']),  # no changes of regular
+        (['some_other'], [], [], ['some_other'], []),   # not added to regular group
+        ([], ['regular'], ['container'], ['container'], ['regular', 'container']),
+        ([], ['regular'], [], ['container', 'container2'], ['regular', 'container', 'container2']),
+        ([], ['regular'], ['other'], [], ['regular']),  # remove not used
+        (['some_other'], ['regular'], ['other', 'container'], ['container', 'container2'], ['regular', 'container', 'container2']),
     ])
     def test_enforce_groups(self, pre_existing, regular_should_be,
                             external_should_be, groups, expected):
@@ -32,7 +32,7 @@
             fixture.destroy_user_group(gr)
         Session().commit()
 
-        user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
+        user = User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
         for gr in pre_existing:
             gr = fixture.create_user_group(gr)
         Session().commit()
@@ -54,6 +54,6 @@
         UserGroupModel().enforce_groups(user, groups, 'container')
         Session().commit()
 
-        user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
+        user = User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
         in_groups = user.group_member
-        assert expected == [x.users_group.users_group_name for x in in_groups]
+        assert sorted(expected) == sorted(x.users_group.users_group_name for x in in_groups)
--- a/kallithea/tests/models/test_user_permissions_on_repo_groups.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/models/test_user_permissions_on_repo_groups.py	Mon May 04 19:24:04 2020 +0200
@@ -1,6 +1,7 @@
 import functools
 
-from kallithea.model.db import RepoGroup, Repository, User
+import kallithea
+from kallithea.model.db import RepoGroup, Repository
 from kallithea.model.meta import Session
 from kallithea.model.repo_group import RepoGroupModel
 from kallithea.tests.models.common import _check_expected_count, _create_project_tree, _destroy_project_tree, _get_perms, check_tree_perms, expected_count
@@ -11,7 +12,7 @@
 _get_group_perms = None
 
 
-def permissions_setup_func(group_name=u'g0', perm='group.read', recursive='all',
+def permissions_setup_func(group_name='g0', perm='group.read', recursive='all',
                            user_id=None):
     """
     Resets all permissions to perm attribute
@@ -19,7 +20,7 @@
     if not user_id:
         user_id = test_u1_id
         permissions_setup_func(group_name, perm, recursive,
-                               user_id=User.get_default_user().user_id)
+                               user_id=kallithea.DEFAULT_USER_ID)
 
     repo_group = RepoGroup.get_by_group_name(group_name=group_name)
     if not repo_group:
@@ -56,7 +57,7 @@
 def test_user_permissions_on_group_without_recursive_mode():
     # set permission to g0 non-recursive mode
     recursive = 'none'
-    group = u'g0'
+    group = 'g0'
     permissions_setup_func(group, 'group.write', recursive=recursive)
 
     items = [x for x in _get_repo_perms(group, recursive)]
@@ -75,7 +76,7 @@
 def test_user_permissions_on_group_without_recursive_mode_subgroup():
     # set permission to g0 non-recursive mode
     recursive = 'none'
-    group = u'g0/g0_1'
+    group = 'g0/g0_1'
     permissions_setup_func(group, 'group.write', recursive=recursive)
 
     items = [x for x in _get_repo_perms(group, recursive)]
@@ -96,7 +97,7 @@
     # set permission to g0 recursive mode, all children including
     # other repos and groups should have this permission now set !
     recursive = 'all'
-    group = u'g0'
+    group = 'g0'
     permissions_setup_func(group, 'group.write', recursive=recursive)
 
     repo_items = [x for x in _get_repo_perms(group, recursive)]
@@ -115,8 +116,8 @@
     # set permission to g0 recursive mode, all children including
     # other repos and groups should have this permission now set !
     recursive = 'all'
-    group = u'g0'
-    default_user_id = User.get_default_user().user_id
+    group = 'g0'
+    default_user_id = kallithea.DEFAULT_USER_ID
     permissions_setup_func(group, 'group.write', recursive=recursive,
                            user_id=default_user_id)
 
@@ -142,7 +143,7 @@
 def test_user_permissions_on_group_with_recursive_mode_inner_group():
     ## set permission to g0_3 group to none
     recursive = 'all'
-    group = u'g0/g0_3'
+    group = 'g0/g0_3'
     permissions_setup_func(group, 'group.none', recursive=recursive)
 
     repo_items = [x for x in _get_repo_perms(group, recursive)]
@@ -159,7 +160,7 @@
 def test_user_permissions_on_group_with_recursive_mode_deepest():
     ## set permission to g0_3 group to none
     recursive = 'all'
-    group = u'g0/g0_1/g0_1_1'
+    group = 'g0/g0_1/g0_1_1'
     permissions_setup_func(group, 'group.write', recursive=recursive)
 
     repo_items = [x for x in _get_repo_perms(group, recursive)]
@@ -176,7 +177,7 @@
 def test_user_permissions_on_group_with_recursive_mode_only_with_repos():
     ## set permission to g0_3 group to none
     recursive = 'all'
-    group = u'g0/g0_2'
+    group = 'g0/g0_2'
     permissions_setup_func(group, 'group.admin', recursive=recursive)
 
     repo_items = [x for x in _get_repo_perms(group, recursive)]
@@ -194,9 +195,9 @@
     # set permission to g0/g0_1 recursive repos only mode, all children including
     # other repos should have this permission now set, inner groups are excluded!
     recursive = 'repos'
-    group = u'g0/g0_1'
+    group = 'g0/g0_1'
     perm = 'group.none'
-    default_user_id = User.get_default_user().user_id
+    default_user_id = kallithea.DEFAULT_USER_ID
 
     permissions_setup_func(group, perm, recursive=recursive,
                            user_id=default_user_id)
@@ -227,7 +228,7 @@
 def test_user_permissions_on_group_with_recursive_repo_mode_inner_group():
     ## set permission to g0_3 group to none, with recursive repos only
     recursive = 'repos'
-    group = u'g0/g0_3'
+    group = 'g0/g0_3'
     perm = 'group.none'
     permissions_setup_func(group, perm, recursive=recursive)
 
@@ -253,8 +254,8 @@
     # other groups should have this permission now set. repositories should
     # remain intact as we use groups only mode !
     recursive = 'groups'
-    group = u'g0/g0_1'
-    default_user_id = User.get_default_user().user_id
+    group = 'g0/g0_1'
+    default_user_id = kallithea.DEFAULT_USER_ID
     permissions_setup_func(group, 'group.write', recursive=recursive,
                            user_id=default_user_id)
 
@@ -278,7 +279,7 @@
 def test_user_permissions_on_group_with_recursive_group_mode_inner_group():
     ## set permission to g0_3 group to none, with recursive mode for groups only
     recursive = 'groups'
-    group = u'g0/g0_3'
+    group = 'g0/g0_3'
     permissions_setup_func(group, 'group.none', recursive=recursive)
 
     repo_items = [x for x in _get_repo_perms(group, recursive)]
--- a/kallithea/tests/models/test_users.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/models/test_users.py	Mon May 04 19:24:04 2020 +0200
@@ -4,14 +4,14 @@
 from kallithea.model.meta import Session
 from kallithea.model.user import UserModel
 from kallithea.model.user_group import UserGroupModel
-from kallithea.tests.base import *
+from kallithea.tests import base
 from kallithea.tests.fixture import Fixture
 
 
 fixture = Fixture()
 
 
-class TestUser(TestController):
+class TestUser(base.TestController):
 
     @classmethod
     def setup_class(cls):
@@ -21,21 +21,21 @@
         Session.remove()
 
     def test_create_and_remove(self):
-        usr = UserModel().create_or_update(username=u'test_user',
-                                           password=u'qweqwe',
-                                           email=u'u232@example.com',
-                                           firstname=u'u1', lastname=u'u1')
+        usr = UserModel().create_or_update(username='test_user',
+                                           password='qweqwe',
+                                           email='u232@example.com',
+                                           firstname='u1', lastname='u1')
         Session().commit()
-        assert User.get_by_username(u'test_user') == usr
-        assert User.get_by_username(u'test_USER', case_insensitive=True) == usr
+        assert User.get_by_username('test_user') == usr
+        assert User.get_by_username('test_USER', case_insensitive=True) == usr
         # User.get_by_username without explicit request for case insensitivty
         # will use database case sensitivity. The following will thus return
         # None on for example PostgreSQL but find test_user on MySQL - we are
         # fine with leaving that as undefined as long as it doesn't crash.
-        User.get_by_username(u'test_USER', case_insensitive=False)
+        User.get_by_username('test_USER', case_insensitive=False)
 
         # make user group
-        user_group = fixture.create_user_group(u'some_example_group')
+        user_group = fixture.create_user_group('some_example_group')
         Session().commit()
 
         UserGroupModel().add_user_to_group(user_group, usr)
@@ -49,15 +49,15 @@
         assert UserGroupMember.query().all() == []
 
     def test_additional_email_as_main(self):
-        usr = UserModel().create_or_update(username=u'test_user',
-                                           password=u'qweqwe',
-                                     email=u'main_email@example.com',
-                                     firstname=u'u1', lastname=u'u1')
+        usr = UserModel().create_or_update(username='test_user',
+                                           password='qweqwe',
+                                     email='main_email@example.com',
+                                     firstname='u1', lastname='u1')
         Session().commit()
 
         with pytest.raises(AttributeError):
             m = UserEmailMap()
-            m.email = u'main_email@example.com'
+            m.email = 'main_email@example.com'
             m.user = usr
             Session().add(m)
             Session().commit()
@@ -66,14 +66,14 @@
         Session().commit()
 
     def test_extra_email_map(self):
-        usr = UserModel().create_or_update(username=u'test_user',
-                                           password=u'qweqwe',
-                                     email=u'main_email@example.com',
-                                     firstname=u'u1', lastname=u'u1')
+        usr = UserModel().create_or_update(username='test_user',
+                                           password='qweqwe',
+                                     email='main_email@example.com',
+                                     firstname='u1', lastname='u1')
         Session().commit()
 
         m = UserEmailMap()
-        m.email = u'main_email2@example.com'
+        m.email = 'main_email2@example.com'
         m.user = usr
         Session().add(m)
         Session().commit()
@@ -101,13 +101,13 @@
         Session().commit()
 
 
-class TestUsers(TestController):
+class TestUsers(base.TestController):
 
     def setup_method(self, method):
-        self.u1 = UserModel().create_or_update(username=u'u1',
-                                        password=u'qweqwe',
-                                        email=u'u1@example.com',
-                                        firstname=u'u1', lastname=u'u1')
+        self.u1 = UserModel().create_or_update(username='u1',
+                                        password='qweqwe',
+                                        email='u1@example.com',
+                                        firstname='u1', lastname='u1')
 
     def teardown_method(self, method):
         perm = Permission.query().all()
--- a/kallithea/tests/other/test_auth_ldap.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/other/test_auth_ldap.py	Mon May 04 19:24:04 2020 +0200
@@ -22,9 +22,9 @@
         pass
 
     def authenticate_ldap(self, username, password):
-        return 'spam dn', dict(test_ldap_firstname=[u'spam ldap first name'],
-                               test_ldap_lastname=[u'spam ldap last name'],
-                               test_ldap_email=['spam ldap email'])
+        return 'spam dn', dict(test_ldap_firstname=['spam ldap first name'],
+                               test_ldap_lastname=['spam ldap last name'],
+                               test_ldap_email=['%s ldap email' % username])
 
 
 def test_update_user_attributes_from_ldap(monkeypatch, create_test_user,
@@ -39,8 +39,8 @@
     user_input = dict(username='test-user-{0}'.format(uniqifier),
                       password='spam password',
                       email='spam-email-{0}'.format(uniqifier),
-                      firstname=u'spam first name',
-                      lastname=u'spam last name',
+                      firstname='spam first name',
+                      lastname='spam last name',
                       active=True,
                       admin=False)
     user = create_test_user(user_input)
@@ -54,15 +54,15 @@
     # Verify that authenication succeeded and retrieved correct attributes
     # from LDAP.
     assert user_data is not None
-    assert user_data.get('firstname') == u'spam ldap first name'
-    assert user_data.get('lastname') == u'spam ldap last name'
-    assert user_data.get('email') == 'spam ldap email'
+    assert user_data.get('firstname') == 'spam ldap first name'
+    assert user_data.get('lastname') == 'spam ldap last name'
+    assert user_data.get('email') == '%s ldap email' % username
 
     # Verify that authentication overwrote user attributes with the ones
     # retrieved from LDAP.
-    assert user.firstname == u'spam ldap first name'
-    assert user.lastname == u'spam ldap last name'
-    assert user.email == 'spam ldap email'
+    assert user.firstname == 'spam ldap first name'
+    assert user.lastname == 'spam ldap last name'
+    assert user.email == '%s ldap email' % username
 
 
 def test_init_user_attributes_from_ldap(monkeypatch, arrange_ldap_auth):
@@ -83,17 +83,17 @@
     # Verify that authenication succeeded and retrieved correct attributes
     # from LDAP.
     assert user_data is not None
-    assert user_data.get('firstname') == u'spam ldap first name'
-    assert user_data.get('lastname') == u'spam ldap last name'
-    assert user_data.get('email') == 'spam ldap email'
+    assert user_data.get('firstname') == 'spam ldap first name'
+    assert user_data.get('lastname') == 'spam ldap last name'
+    assert user_data.get('email') == '%s ldap email' % username
 
     # Verify that authentication created new user with attributes
     # retrieved from LDAP.
     new_user = User.get_by_username(username)
     assert new_user is not None
-    assert new_user.firstname == u'spam ldap first name'
-    assert new_user.lastname == u'spam ldap last name'
-    assert new_user.email == 'spam ldap email'
+    assert new_user.firstname == 'spam ldap first name'
+    assert new_user.lastname == 'spam ldap last name'
+    assert new_user.email == '%s ldap email' % username
 
 
 class _AuthLdapNoEmailMock():
@@ -126,14 +126,14 @@
     # Verify that authenication succeeded and retrieved correct attributes
     # from LDAP, with empty email.
     assert user_data is not None
-    assert user_data.get('firstname') == u'spam ldap first name'
-    assert user_data.get('lastname') == u'spam ldap last name'
+    assert user_data.get('firstname') == 'spam ldap first name'
+    assert user_data.get('lastname') == 'spam ldap last name'
     assert user_data.get('email') == ''
 
     # Verify that authentication created new user with attributes
     # retrieved from LDAP, with email == None.
     new_user = User.get_by_username(username)
     assert new_user is not None
-    assert new_user.firstname == u'spam ldap first name'
-    assert new_user.lastname == u'spam ldap last name'
+    assert new_user.firstname == 'spam ldap first name'
+    assert new_user.lastname == 'spam ldap last name'
     assert new_user.email is None
--- a/kallithea/tests/other/test_libs.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/other/test_libs.py	Mon May 04 19:24:04 2020 +0200
@@ -31,9 +31,9 @@
 import mock
 from tg.util.webtest import test_context
 
-from kallithea.lib.utils2 import AttributeDict
+from kallithea.lib.utils2 import AttributeDict, safe_bytes
 from kallithea.model.db import Repository
-from kallithea.tests.base import *
+from kallithea.tests import base
 
 
 proto = 'http'
@@ -91,19 +91,19 @@
         return self.current_url % kwargs
 
 
-class TestLibs(TestController):
+class TestLibs(base.TestController):
 
-    @parametrize('test_url,expected,expected_creds', TEST_URLS)
+    @base.parametrize('test_url,expected,expected_creds', TEST_URLS)
     def test_uri_filter(self, test_url, expected, expected_creds):
         from kallithea.lib.utils2 import uri_filter
         assert uri_filter(test_url) == expected
 
-    @parametrize('test_url,expected,expected_creds', TEST_URLS)
+    @base.parametrize('test_url,expected,expected_creds', TEST_URLS)
     def test_credentials_filter(self, test_url, expected, expected_creds):
         from kallithea.lib.utils2 import credentials_filter
         assert credentials_filter(test_url) == expected_creds
 
-    @parametrize('str_bool,expected', [
+    @base.parametrize('str_bool,expected', [
                            ('t', True),
                            ('true', True),
                            ('y', True),
@@ -141,21 +141,21 @@
             'marian.user', 'marco-polo', 'marco_polo', 'world'])
         assert expected == set(extract_mentioned_usernames(sample))
 
-    @parametrize('age_args,expected', [
-        (dict(), u'just now'),
-        (dict(seconds= -1), u'1 second ago'),
-        (dict(seconds= -60 * 2), u'2 minutes ago'),
-        (dict(hours= -1), u'1 hour ago'),
-        (dict(hours= -24), u'1 day ago'),
-        (dict(hours= -24 * 5), u'5 days ago'),
-        (dict(months= -1), u'1 month ago'),
-        (dict(months= -1, days= -2), u'1 month and 2 days ago'),
-        (dict(months= -1, days= -20), u'1 month and 19 days ago'),
-        (dict(years= -1, months= -1), u'1 year and 1 month ago'),
-        (dict(years= -1, months= -10), u'1 year and 10 months ago'),
-        (dict(years= -2, months= -4), u'2 years and 4 months ago'),
-        (dict(years= -2, months= -11), u'2 years and 11 months ago'),
-        (dict(years= -3, months= -2), u'3 years and 2 months ago'),
+    @base.parametrize('age_args,expected', [
+        (dict(), 'just now'),
+        (dict(seconds= -1), '1 second ago'),
+        (dict(seconds= -60 * 2), '2 minutes ago'),
+        (dict(hours= -1), '1 hour ago'),
+        (dict(hours= -24), '1 day ago'),
+        (dict(hours= -24 * 5), '5 days ago'),
+        (dict(months= -1), '1 month ago'),
+        (dict(months= -1, days= -2), '1 month and 2 days ago'),
+        (dict(months= -1, days= -20), '1 month and 19 days ago'),
+        (dict(years= -1, months= -1), '1 year and 1 month ago'),
+        (dict(years= -1, months= -10), '1 year and 10 months ago'),
+        (dict(years= -2, months= -4), '2 years and 4 months ago'),
+        (dict(years= -2, months= -11), '2 years and 11 months ago'),
+        (dict(years= -3, months= -2), '3 years and 2 months ago'),
     ])
     def test_age(self, age_args, expected):
         from kallithea.lib.utils2 import age
@@ -165,22 +165,22 @@
             delt = lambda *args, **kwargs: relativedelta.relativedelta(*args, **kwargs)
             assert age(n + delt(**age_args), now=n) == expected
 
-    @parametrize('age_args,expected', [
-        (dict(), u'just now'),
-        (dict(seconds= -1), u'1 second ago'),
-        (dict(seconds= -60 * 2), u'2 minutes ago'),
-        (dict(hours= -1), u'1 hour ago'),
-        (dict(hours= -24), u'1 day ago'),
-        (dict(hours= -24 * 5), u'5 days ago'),
-        (dict(months= -1), u'1 month ago'),
-        (dict(months= -1, days= -2), u'1 month ago'),
-        (dict(months= -1, days= -20), u'1 month ago'),
-        (dict(years= -1, months= -1), u'13 months ago'),
-        (dict(years= -1, months= -10), u'22 months ago'),
-        (dict(years= -2, months= -4), u'2 years ago'),
-        (dict(years= -2, months= -11), u'3 years ago'),
-        (dict(years= -3, months= -2), u'3 years ago'),
-        (dict(years= -4, months= -8), u'5 years ago'),
+    @base.parametrize('age_args,expected', [
+        (dict(), 'just now'),
+        (dict(seconds= -1), '1 second ago'),
+        (dict(seconds= -60 * 2), '2 minutes ago'),
+        (dict(hours= -1), '1 hour ago'),
+        (dict(hours= -24), '1 day ago'),
+        (dict(hours= -24 * 5), '5 days ago'),
+        (dict(months= -1), '1 month ago'),
+        (dict(months= -1, days= -2), '1 month ago'),
+        (dict(months= -1, days= -20), '1 month ago'),
+        (dict(years= -1, months= -1), '13 months ago'),
+        (dict(years= -1, months= -10), '22 months ago'),
+        (dict(years= -2, months= -4), '2 years ago'),
+        (dict(years= -2, months= -11), '3 years ago'),
+        (dict(years= -3, months= -2), '3 years ago'),
+        (dict(years= -4, months= -8), '5 years ago'),
     ])
     def test_age_short(self, age_args, expected):
         from kallithea.lib.utils2 import age
@@ -190,16 +190,16 @@
             delt = lambda *args, **kwargs: relativedelta.relativedelta(*args, **kwargs)
             assert age(n + delt(**age_args), show_short_version=True, now=n) == expected
 
-    @parametrize('age_args,expected', [
-        (dict(), u'just now'),
-        (dict(seconds=1), u'in 1 second'),
-        (dict(seconds=60 * 2), u'in 2 minutes'),
-        (dict(hours=1), u'in 1 hour'),
-        (dict(hours=24), u'in 1 day'),
-        (dict(hours=24 * 5), u'in 5 days'),
-        (dict(months=1), u'in 1 month'),
-        (dict(months=1, days=1), u'in 1 month and 1 day'),
-        (dict(years=1, months=1), u'in 1 year and 1 month')
+    @base.parametrize('age_args,expected', [
+        (dict(), 'just now'),
+        (dict(seconds=1), 'in 1 second'),
+        (dict(seconds=60 * 2), 'in 2 minutes'),
+        (dict(hours=1), 'in 1 hour'),
+        (dict(hours=24), 'in 1 day'),
+        (dict(hours=24 * 5), 'in 5 days'),
+        (dict(months=1), 'in 1 month'),
+        (dict(months=1, days=1), 'in 1 month and 1 day'),
+        (dict(years=1, months=1), 'in 1 year and 1 month')
     ])
     def test_age_in_future(self, age_args, expected):
         from kallithea.lib.utils2 import age
@@ -227,7 +227,7 @@
 
     def test_alternative_gravatar(self):
         from kallithea.lib.helpers import gravatar_url
-        _md5 = lambda s: hashlib.md5(s).hexdigest()
+        _md5 = lambda s: hashlib.md5(safe_bytes(s)).hexdigest()
 
         # mock tg.tmpl_context
         def fake_tmpl_context(_url):
@@ -270,7 +270,7 @@
                 grav = gravatar_url(email_address=em, size=24)
                 assert grav == 'https://example.com/%s/%s' % (_md5(em), 24)
 
-    @parametrize('clone_uri_tmpl,repo_name,username,prefix,expected', [
+    @base.parametrize('clone_uri_tmpl,repo_name,username,prefix,expected', [
         (Repository.DEFAULT_CLONE_URI, 'group/repo1', None, '', 'http://vps1:8000/group/repo1'),
         (Repository.DEFAULT_CLONE_URI, 'group/repo1', 'username', '', 'http://username@vps1:8000/group/repo1'),
         (Repository.DEFAULT_CLONE_URI, 'group/repo1', None, '/prefix', 'http://vps1:8000/prefix/group/repo1'),
@@ -307,7 +307,7 @@
             return tmpl % (url_ or '/repo_name/changeset/%s' % _url, _url)
         return url_pattern.sub(url_func, text)
 
-    @parametrize('sample,expected', [
+    @base.parametrize('sample,expected', [
       ("",
        ""),
       ("git-svn-id: https://svn.apache.org/repos/asf/libcloud/trunk@1441655 13f79535-47bb-0310-9956-ffa450edef68",
@@ -341,7 +341,7 @@
             from kallithea.lib.helpers import urlify_text
             assert urlify_text(sample, 'repo_name') == expected
 
-    @parametrize('sample,expected,url_', [
+    @base.parametrize('sample,expected,url_', [
       ("",
        "",
        ""),
@@ -396,7 +396,7 @@
             from kallithea.lib.helpers import urlify_text
             assert urlify_text(sample, 'repo_name', stylize=True) == expected
 
-    @parametrize('sample,expected', [
+    @base.parametrize('sample,expected', [
       ("deadbeefcafe @mention, and http://foo.bar/ yo",
        """<a class="changeset_hash" href="/repo_name/changeset/deadbeefcafe">deadbeefcafe</a>"""
        """<a class="message-link" href="#the-link"> <b>@mention</b>, and </a>"""
@@ -409,7 +409,7 @@
             from kallithea.lib.helpers import urlify_text
             assert urlify_text(sample, 'repo_name', link_='#the-link') == expected
 
-    @parametrize('issue_pat,issue_server,issue_sub,sample,expected', [
+    @base.parametrize('issue_pat,issue_server,issue_sub,sample,expected', [
         (r'#(\d+)', 'http://foo/{repo}/issue/\\1', '#\\1',
             'issue #123 and issue#456',
             """issue <a class="issue-tracker-link" href="http://foo/repo_name/issue/123">#123</a> and """
@@ -482,7 +482,7 @@
             """empty issue_sub <a class="issue-tracker-link" href="http://foo/repo_name/issue/123">$123</a> and """
             """issue$456"""),
         # named groups
-        (r'(PR|pullrequest|pull request) ?(?P<sitecode>BRU|CPH|BER)-(?P<id>\d+)', 'http://foo/\g<sitecode>/pullrequest/\g<id>/', 'PR-\g<sitecode>-\g<id>',
+        (r'(PR|pullrequest|pull request) ?(?P<sitecode>BRU|CPH|BER)-(?P<id>\d+)', r'http://foo/\g<sitecode>/pullrequest/\g<id>/', r'PR-\g<sitecode>-\g<id>',
             'pullrequest CPH-789 is similar to PRBRU-747',
             """<a class="issue-tracker-link" href="http://foo/CPH/pullrequest/789/">PR-CPH-789</a> is similar to """
             """<a class="issue-tracker-link" href="http://foo/BRU/pullrequest/747/">PR-BRU-747</a>"""),
@@ -500,7 +500,7 @@
             with mock.patch('kallithea.CONFIG', config_stub):
                 assert urlify_text(sample, 'repo_name') == expected
 
-    @parametrize('sample,expected', [
+    @base.parametrize('sample,expected', [
         ('abc X5', 'abc <a class="issue-tracker-link" href="http://main/repo_name/main/5/">#5</a>'),
         ('abc pullrequest #6 xyz', 'abc <a class="issue-tracker-link" href="http://pr/repo_name/pr/6">PR#6</a> xyz'),
         ('pull request7 #', '<a class="issue-tracker-link" href="http://pr/repo_name/pr/7">PR#7</a> #'),
@@ -512,28 +512,28 @@
     def test_urlify_issues_multiple_issue_patterns(self, sample, expected):
         from kallithea.lib.helpers import urlify_text
         config_stub = {
-            'sqlalchemy.url': 'foo',
-            'issue_pat': 'X(\d+)',
-            'issue_server_link': 'http://main/{repo}/main/\\1/',
-            'issue_sub': '#\\1',
-            'issue_pat_pr': '(?:pullrequest|pull request|PR|pr) ?#?(\d+)',
-            'issue_server_link_pr': 'http://pr/{repo}/pr/\\1',
-            'issue_sub_pr': 'PR#\\1',
-            'issue_pat_bug': '(?:BUG|bug|issue) ?#?(\d+)',
-            'issue_server_link_bug': 'http://bug/{repo}/bug/\\1',
-            'issue_sub_bug': 'bug#\\1',
-            'issue_pat_empty_prefix': 'FAIL(\d+)',
-            'issue_server_link_empty_prefix': 'http://fail/{repo}/\\1',
-            'issue_sub_empty_prefix': '',
-            'issue_pat_absent_prefix': 'FAILMORE(\d+)',
-            'issue_server_link_absent_prefix': 'http://failmore/{repo}/\\1',
+            'sqlalchemy.url': r'foo',
+            'issue_pat': r'X(\d+)',
+            'issue_server_link': r'http://main/{repo}/main/\1/',
+            'issue_sub': r'#\1',
+            'issue_pat_pr': r'(?:pullrequest|pull request|PR|pr) ?#?(\d+)',
+            'issue_server_link_pr': r'http://pr/{repo}/pr/\1',
+            'issue_sub_pr': r'PR#\1',
+            'issue_pat_bug': r'(?:BUG|bug|issue) ?#?(\d+)',
+            'issue_server_link_bug': r'http://bug/{repo}/bug/\1',
+            'issue_sub_bug': r'bug#\1',
+            'issue_pat_empty_prefix': r'FAIL(\d+)',
+            'issue_server_link_empty_prefix': r'http://fail/{repo}/\1',
+            'issue_sub_empty_prefix': r'',
+            'issue_pat_absent_prefix': r'FAILMORE(\d+)',
+            'issue_server_link_absent_prefix': r'http://failmore/{repo}/\1',
         }
         # force recreation of lazy function
         with mock.patch('kallithea.lib.helpers._urlify_issues_f', None):
             with mock.patch('kallithea.CONFIG', config_stub):
                 assert urlify_text(sample, 'repo_name') == expected
 
-    @parametrize('test,expected', [
+    @base.parametrize('test,expected', [
       ("", None),
       ("/_2", None),
       ("_2", 2),
@@ -542,9 +542,9 @@
     def test_get_permanent_id(self, test, expected):
         from kallithea.lib.utils import _get_permanent_id
         extracted = _get_permanent_id(test)
-        assert extracted == expected, 'url:%s, got:`%s` expected: `%s`' % (test, _test, expected)
+        assert extracted == expected, 'url:%s, got:`%s` expected: `%s`' % (test, base._test, expected)
 
-    @parametrize('test,expected', [
+    @base.parametrize('test,expected', [
       ("", ""),
       ("/", "/"),
       ("/_ID", '/_ID'),
@@ -555,14 +555,14 @@
       ("_IDa", '_IDa'),
     ])
     def test_fix_repo_id_name(self, test, expected):
-        repo = Repository.get_by_repo_name(HG_REPO)
+        repo = Repository.get_by_repo_name(base.HG_REPO)
         test = test.replace('ID', str(repo.repo_id))
         expected = expected.replace('NAME', repo.repo_name).replace('ID', str(repo.repo_id))
         from kallithea.lib.utils import fix_repo_id_name
         replaced = fix_repo_id_name(test)
         assert replaced == expected, 'url:%s, got:`%s` expected: `%s`' % (test, replaced, expected)
 
-    @parametrize('canonical,test,expected', [
+    @base.parametrize('canonical,test,expected', [
         ('http://www.example.org/', '/abc/xyz', 'http://www.example.org/abc/xyz'),
         ('http://www.example.org', '/abc/xyz', 'http://www.example.org/abc/xyz'),
         ('http://www.example.org', '/abc/xyz/', 'http://www.example.org/abc/xyz/'),
@@ -590,7 +590,7 @@
             with mock.patch('kallithea.CONFIG', config_mock):
                 assert canonical_url(test) == expected
 
-    @parametrize('canonical,expected', [
+    @base.parametrize('canonical,expected', [
         ('http://www.example.org', 'www.example.org'),
         ('http://www.example.org/repos/', 'www.example.org'),
         ('http://www.example.org/kallithea/repos/', 'www.example.org'),
--- a/kallithea/tests/other/test_mail.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/other/test_mail.py	Mon May 04 19:24:04 2020 +0200
@@ -1,8 +1,10 @@
+# -*- coding: utf-8 -*-
+
 import mock
 
 import kallithea
 from kallithea.model.db import User
-from kallithea.tests.base import *
+from kallithea.tests import base
 
 
 class smtplib_mock(object):
@@ -25,7 +27,7 @@
 
 
 @mock.patch('kallithea.lib.rcmail.smtp_mailer.smtplib', smtplib_mock)
-class TestMail(TestController):
+class TestMail(base.TestController):
 
     def test_send_mail_trivial(self):
         mailserver = 'smtp.mailserver.org'
@@ -66,7 +68,7 @@
         with mock.patch('kallithea.lib.celerylib.tasks.config', config_mock):
             kallithea.lib.celerylib.tasks.send_email(recipients, subject, body, html_body)
 
-        assert smtplib_mock.lastdest == set([TEST_USER_ADMIN_EMAIL, email_to])
+        assert smtplib_mock.lastdest == set([base.TEST_USER_ADMIN_EMAIL, email_to])
         assert smtplib_mock.lastsender == envelope_from
         assert 'From: %s' % envelope_from in smtplib_mock.lastmsg
         assert 'Subject: %s' % subject in smtplib_mock.lastmsg
@@ -90,7 +92,7 @@
         with mock.patch('kallithea.lib.celerylib.tasks.config', config_mock):
             kallithea.lib.celerylib.tasks.send_email(recipients, subject, body, html_body)
 
-        assert smtplib_mock.lastdest == set([TEST_USER_ADMIN_EMAIL] + email_to.split(','))
+        assert smtplib_mock.lastdest == set([base.TEST_USER_ADMIN_EMAIL] + email_to.split(','))
         assert smtplib_mock.lastsender == envelope_from
         assert 'From: %s' % envelope_from in smtplib_mock.lastmsg
         assert 'Subject: %s' % subject in smtplib_mock.lastmsg
@@ -112,7 +114,7 @@
         with mock.patch('kallithea.lib.celerylib.tasks.config', config_mock):
             kallithea.lib.celerylib.tasks.send_email(recipients, subject, body, html_body)
 
-        assert smtplib_mock.lastdest == set([TEST_USER_ADMIN_EMAIL])
+        assert smtplib_mock.lastdest == set([base.TEST_USER_ADMIN_EMAIL])
         assert smtplib_mock.lastsender == envelope_from
         assert 'From: %s' % envelope_from in smtplib_mock.lastmsg
         assert 'Subject: %s' % subject in smtplib_mock.lastmsg
@@ -126,14 +128,14 @@
         subject = 'subject'
         body = 'body'
         html_body = 'html_body'
-        author = User.get_by_username(TEST_USER_REGULAR_LOGIN)
+        author = User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
 
         config_mock = {
             'smtp_server': mailserver,
             'app_email_from': envelope_from,
         }
         with mock.patch('kallithea.lib.celerylib.tasks.config', config_mock):
-            kallithea.lib.celerylib.tasks.send_email(recipients, subject, body, html_body, author=author)
+            kallithea.lib.celerylib.tasks.send_email(recipients, subject, body, html_body, from_name=author.full_name_or_username)
 
         assert smtplib_mock.lastdest == set(recipients)
         assert smtplib_mock.lastsender == envelope_from
@@ -144,20 +146,20 @@
 
     def test_send_mail_with_author_full_mail_from(self):
         mailserver = 'smtp.mailserver.org'
-        recipients = ['rcpt1', 'rcpt2']
+        recipients = ['ræcpt1', 'receptor2 <rcpt2@example.com>', 'tæst@example.com', 'Tæst <test@example.com>']
         envelope_addr = 'noreply@mailserver.org'
-        envelope_from = 'Some Name <%s>' % envelope_addr
+        envelope_from = 'Söme Næme <%s>' % envelope_addr
         subject = 'subject'
         body = 'body'
         html_body = 'html_body'
-        author = User.get_by_username(TEST_USER_REGULAR_LOGIN)
+        author = User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
 
         config_mock = {
             'smtp_server': mailserver,
             'app_email_from': envelope_from,
         }
         with mock.patch('kallithea.lib.celerylib.tasks.config', config_mock):
-            kallithea.lib.celerylib.tasks.send_email(recipients, subject, body, html_body, author=author)
+            kallithea.lib.celerylib.tasks.send_email(recipients, subject, body, html_body, from_name=author.full_name_or_username)
 
         assert smtplib_mock.lastdest == set(recipients)
         assert smtplib_mock.lastsender == envelope_from
@@ -173,7 +175,7 @@
         subject = 'subject'
         body = 'body'
         html_body = 'html_body'
-        author = User(name='foo', lastname=u'(fubar) "baz"')
+        author = User(name='foo', lastname='(fubar) "baz"')
         headers = {'extra': 'yes'}
 
         config_mock = {
@@ -182,7 +184,7 @@
         }
         with mock.patch('kallithea.lib.celerylib.tasks.config', config_mock):
             kallithea.lib.celerylib.tasks.send_email(recipients, subject, body, html_body,
-                                                     author=author, headers=headers)
+                                                     from_name=author.full_name_or_username, headers=headers)
 
         assert smtplib_mock.lastdest == set(recipients)
         assert smtplib_mock.lastsender == envelope_from
--- a/kallithea/tests/other/test_validators.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/other/test_validators.py	Mon May 04 19:24:04 2020 +0200
@@ -6,7 +6,7 @@
 from kallithea.model.meta import Session
 from kallithea.model.repo_group import RepoGroupModel
 from kallithea.model.user_group import UserGroupModel
-from kallithea.tests.base import *
+from kallithea.tests import base
 from kallithea.tests.fixture import Fixture
 
 
@@ -14,7 +14,7 @@
 
 
 @pytest.mark.usefixtures("test_context_fixture") # apply fixture for all test methods
-class TestRepoGroups(TestController):
+class TestRepoGroups(base.TestController):
 
     def teardown_method(self, method):
         Session.remove()
@@ -40,7 +40,7 @@
         with pytest.raises(formencode.Invalid):
             validator.to_python('.,')
         with pytest.raises(formencode.Invalid):
-            validator.to_python(TEST_USER_ADMIN_LOGIN)
+            validator.to_python(base.TEST_USER_ADMIN_LOGIN)
         assert 'test' == validator.to_python('test')
 
         validator = v.ValidUsername(edit=True, old_data={'user_id': 1})
@@ -49,31 +49,31 @@
         validator = v.ValidRepoUser()
         with pytest.raises(formencode.Invalid):
             validator.to_python('nouser')
-        assert TEST_USER_ADMIN_LOGIN == validator.to_python(TEST_USER_ADMIN_LOGIN)
+        assert base.TEST_USER_ADMIN_LOGIN == validator.to_python(base.TEST_USER_ADMIN_LOGIN)
 
     def test_ValidUserGroup(self):
         validator = v.ValidUserGroup()
         with pytest.raises(formencode.Invalid):
-            validator.to_python(u'default')
+            validator.to_python('default')
         with pytest.raises(formencode.Invalid):
-            validator.to_python(u'.,')
+            validator.to_python('.,')
 
-        gr = fixture.create_user_group(u'test')
-        gr2 = fixture.create_user_group(u'tes2')
+        gr = fixture.create_user_group('test')
+        gr2 = fixture.create_user_group('tes2')
         Session().commit()
         with pytest.raises(formencode.Invalid):
-            validator.to_python(u'test')
+            validator.to_python('test')
         assert gr.users_group_id is not None
         validator = v.ValidUserGroup(edit=True,
                                     old_data={'users_group_id':
                                               gr2.users_group_id})
 
         with pytest.raises(formencode.Invalid):
-            validator.to_python(u'test')
+            validator.to_python('test')
         with pytest.raises(formencode.Invalid):
-            validator.to_python(u'TesT')
+            validator.to_python('TesT')
         with pytest.raises(formencode.Invalid):
-            validator.to_python(u'TEST')
+            validator.to_python('TEST')
         UserGroupModel().delete(gr)
         UserGroupModel().delete(gr2)
         Session().commit()
@@ -82,11 +82,11 @@
         validator = v.ValidRepoGroup()
         model = RepoGroupModel()
         with pytest.raises(formencode.Invalid):
-            validator.to_python({'group_name': HG_REPO, })
-        gr = model.create(group_name=u'test_gr', group_description=u'desc',
+            validator.to_python({'group_name': base.HG_REPO, })
+        gr = model.create(group_name='test_gr', group_description='desc',
                           parent=None,
                           just_db=True,
-                          owner=TEST_USER_ADMIN_LOGIN)
+                          owner=base.TEST_USER_ADMIN_LOGIN)
         with pytest.raises(formencode.Invalid):
             validator.to_python({'group_name': gr.group_name, })
 
@@ -127,8 +127,8 @@
     def test_ValidAuth(self):
         validator = v.ValidAuth()
         valid_creds = {
-            'username': TEST_USER_REGULAR2_LOGIN,
-            'password': TEST_USER_REGULAR2_PASS,
+            'username': base.TEST_USER_REGULAR2_LOGIN,
+            'password': base.TEST_USER_REGULAR2_PASS,
         }
         invalid_creds = {
             'username': 'err',
@@ -145,12 +145,12 @@
             validator.to_python({'repo_name': ''})
 
         with pytest.raises(formencode.Invalid):
-            validator.to_python({'repo_name': HG_REPO})
+            validator.to_python({'repo_name': base.HG_REPO})
 
-        gr = RepoGroupModel().create(group_name=u'group_test',
-                                      group_description=u'desc',
+        gr = RepoGroupModel().create(group_name='group_test',
+                                      group_description='desc',
                                       parent=None,
-                                      owner=TEST_USER_ADMIN_LOGIN)
+                                      owner=base.TEST_USER_ADMIN_LOGIN)
         with pytest.raises(formencode.Invalid):
             validator.to_python({'repo_name': gr.group_name})
 
@@ -163,7 +163,7 @@
         # this uses ValidRepoName validator
         assert True
 
-    @parametrize('name,expected', [
+    @base.parametrize('name,expected', [
         ('test', 'test'), ('lolz!', 'lolz'), ('  aavv', 'aavv'),
         ('ala ma kota', 'ala-ma-kota'), ('@nooo', 'nooo'),
         ('$!haha lolz !', 'haha-lolz'), ('$$$$$', ''), ('{}OK!', 'OK'),
@@ -196,7 +196,7 @@
 
     def test_ValidPath(self):
             validator = v.ValidPath()
-            assert TESTS_TMP_PATH == validator.to_python(TESTS_TMP_PATH)
+            assert base.TESTS_TMP_PATH == validator.to_python(base.TESTS_TMP_PATH)
             with pytest.raises(formencode.Invalid):
                 validator.to_python('/no_such_dir')
 
@@ -205,20 +205,20 @@
 
         assert 'mail@python.org' == validator.to_python('MaiL@Python.org')
 
-        email = TEST_USER_REGULAR2_EMAIL
+        email = base.TEST_USER_REGULAR2_EMAIL
         with pytest.raises(formencode.Invalid):
             validator.to_python(email)
 
     def test_ValidSystemEmail(self):
         validator = v.ValidSystemEmail()
-        email = TEST_USER_REGULAR2_EMAIL
+        email = base.TEST_USER_REGULAR2_EMAIL
 
         assert email == validator.to_python(email)
         with pytest.raises(formencode.Invalid):
             validator.to_python('err')
 
     def test_LdapLibValidator(self):
-        if ldap_lib_installed:
+        if base.ldap_lib_installed:
             validator = v.LdapLibValidator()
             assert "DN" == validator.to_python('DN')
         else:
--- a/kallithea/tests/other/test_vcs_operations.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/other/test_vcs_operations.py	Mon May 04 19:24:04 2020 +0200
@@ -25,25 +25,24 @@
 
 """
 
-from __future__ import print_function
-
 import json
 import os
 import re
 import tempfile
 import time
-import urllib2
+import urllib.request
 from subprocess import PIPE, Popen
 from tempfile import _RandomNameSequence
 
 import pytest
 
 from kallithea import CONFIG
-from kallithea.model.db import CacheInvalidation, Repository, Ui, User, UserIpMap, UserLog
+from kallithea.lib.utils2 import ascii_bytes, safe_str
+from kallithea.model.db import Repository, Ui, User, UserIpMap, UserLog
 from kallithea.model.meta import Session
 from kallithea.model.ssh_key import SshKeyModel
 from kallithea.model.user import UserModel
-from kallithea.tests.base import *
+from kallithea.tests import base
 from kallithea.tests.fixture import Fixture
 
 
@@ -64,18 +63,18 @@
 
 class SshVcsTest(object):
     public_keys = {
-        TEST_USER_REGULAR_LOGIN: u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6Ycnc2oUZHQnQwuqgZqTTdMDZD7ataf3JM7oG2Fw8JR6cdmz4QZLe5mfDwaFwG2pWHLRpVqzfrD/Pn3rIO++bgCJH5ydczrl1WScfryV1hYMJ/4EzLGM657J1/q5EI+b9SntKjf4ax+KP322L0TNQGbZUHLbfG2MwHMrYBQpHUQ== kallithea@localhost',
-        TEST_USER_ADMIN_LOGIN: u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6Ycnc2oUZHQnQwuqgZqTTdMDZD7ataf3JM7oG2Fw8JR6cdmz4QZLe5mfDwaFwG2pWHLRpVqzfrD/Pn3rIO++bgCJH5ydczrl1WScfryV1hYMJ/4EzLGM657J1/q5EI+b9SntKjf4ax+KP322L0TNQGbZUHLbfG2MwHMrYBQpHUq== kallithea@localhost',
+        base.TEST_USER_REGULAR_LOGIN: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6Ycnc2oUZHQnQwuqgZqTTdMDZD7ataf3JM7oG2Fw8JR6cdmz4QZLe5mfDwaFwG2pWHLRpVqzfrD/Pn3rIO++bgCJH5ydczrl1WScfryV1hYMJ/4EzLGM657J1/q5EI+b9SntKjf4ax+KP322L0TNQGbZUHLbfG2MwHMrYBQpHUQ== kallithea@localhost',
+        base.TEST_USER_ADMIN_LOGIN: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6Ycnc2oUZHQnQwuqgZqTTdMDZD7ataf3JM7oG2Fw8JR6cdmz4QZLe5mfDwaFwG2pWHLRpVqzfrD/Pn3rIO++bgCJH5ydczrl1WScfryV1hYMJ/4EzLGM657J1/q5EI+b9SntKjf4ax+KP322L0TNQGbZUHLbfG2MwHMrYBQpHUq== kallithea@localhost',
     }
 
     @classmethod
-    def repo_url_param(cls, webserver, repo_name, username=TEST_USER_ADMIN_LOGIN, password=TEST_USER_ADMIN_PASS, client_ip=IP_ADDR):
+    def repo_url_param(cls, webserver, repo_name, username=base.TEST_USER_ADMIN_LOGIN, password=base.TEST_USER_ADMIN_PASS, client_ip=base.IP_ADDR):
         user = User.get_by_username(username)
         if user.ssh_keys:
             ssh_key = user.ssh_keys[0]
         else:
             sshkeymodel = SshKeyModel()
-            ssh_key = sshkeymodel.create(user, u'test key', cls.public_keys[user.username])
+            ssh_key = sshkeymodel.create(user, 'test key', cls.public_keys[user.username])
             Session().commit()
 
         return cls._ssh_param(repo_name, user, ssh_key, client_ip)
@@ -83,11 +82,11 @@
 # Mixins for using Mercurial and Git
 class HgVcsTest(object):
     repo_type = 'hg'
-    repo_name = HG_REPO
+    repo_name = base.HG_REPO
 
 class GitVcsTest(object):
     repo_type = 'git'
-    repo_name = GIT_REPO
+    repo_name = base.GIT_REPO
 
 # Combine mixins to give the combinations we want to parameterize tests with
 class HgHttpVcsTest(HgVcsTest, HttpVcsTest):
@@ -118,17 +117,17 @@
             ssh_key.user_ssh_key_id)
         return "ssh://someuser@somehost/%s""" % repo_name
 
-parametrize_vcs_test = parametrize('vt', [
+parametrize_vcs_test = base.parametrize('vt', [
     HgHttpVcsTest,
     GitHttpVcsTest,
     HgSshVcsTest,
     GitSshVcsTest,
 ])
-parametrize_vcs_test_hg = parametrize('vt', [
+parametrize_vcs_test_hg = base.parametrize('vt', [
     HgHttpVcsTest,
     HgSshVcsTest,
 ])
-parametrize_vcs_test_http = parametrize('vt', [
+parametrize_vcs_test_http = base.parametrize('vt', [
     HgHttpVcsTest,
     GitHttpVcsTest,
 ])
@@ -162,11 +161,11 @@
                 print('stderr:', stderr)
         if not ignoreReturnCode:
             assert p.returncode == 0
-        return stdout, stderr
+        return safe_str(stdout), safe_str(stderr)
 
 
 def _get_tmp_dir(prefix='vcs_operations-', suffix=''):
-    return tempfile.mkdtemp(dir=TESTS_TMP_PATH, prefix=prefix, suffix=suffix)
+    return tempfile.mkdtemp(dir=base.TESTS_TMP_PATH, prefix=prefix, suffix=suffix)
 
 
 def _add_files(vcs, dest_dir, files_no=3):
@@ -177,7 +176,7 @@
     :param vcs:
     :param dest_dir:
     """
-    added_file = '%ssetup.py' % _RandomNameSequence().next()
+    added_file = '%ssetup.py' % next(_RandomNameSequence())
     open(os.path.join(dest_dir, added_file), 'a').close()
     Command(dest_dir).execute(vcs, 'add', added_file)
 
@@ -186,7 +185,7 @@
         author_str = 'User <%s>' % email
     else:
         author_str = 'User ǝɯɐᴎ <%s>' % email
-    for i in xrange(files_no):
+    for i in range(files_no):
         cmd = """echo "added_line%s" >> %s""" % (i, added_file)
         Command(dest_dir).execute(cmd)
         if vcs == 'hg':
@@ -242,7 +241,7 @@
 
 
 @pytest.mark.usefixtures("test_context_fixture")
-class TestVCSOperations(TestController):
+class TestVCSOperations(base.TestController):
 
     @classmethod
     def setup_class(cls):
@@ -262,16 +261,16 @@
     @pytest.fixture(scope="module")
     def testfork(self):
         # create fork so the repo stays untouched
-        git_fork_name = u'%s_fork%s' % (GIT_REPO, _RandomNameSequence().next())
-        fixture.create_fork(GIT_REPO, git_fork_name)
-        hg_fork_name = u'%s_fork%s' % (HG_REPO, _RandomNameSequence().next())
-        fixture.create_fork(HG_REPO, hg_fork_name)
+        git_fork_name = '%s_fork%s' % (base.GIT_REPO, next(_RandomNameSequence()))
+        fixture.create_fork(base.GIT_REPO, git_fork_name)
+        hg_fork_name = '%s_fork%s' % (base.HG_REPO, next(_RandomNameSequence()))
+        fixture.create_fork(base.HG_REPO, hg_fork_name)
         return {'git': git_fork_name, 'hg': hg_fork_name}
 
     @parametrize_vcs_test
     def test_clone_repo_by_admin(self, webserver, vt):
         clone_url = vt.repo_url_param(webserver, vt.repo_name)
-        stdout, stderr = Command(TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, _get_tmp_dir())
+        stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, _get_tmp_dir())
 
         if vt.repo_type == 'git':
             assert 'Cloning into' in stdout + stderr
@@ -286,26 +285,26 @@
     @parametrize_vcs_test_http
     def test_clone_wrong_credentials(self, webserver, vt):
         clone_url = vt.repo_url_param(webserver, vt.repo_name, password='bad!')
-        stdout, stderr = Command(TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, _get_tmp_dir(), ignoreReturnCode=True)
+        stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, _get_tmp_dir(), ignoreReturnCode=True)
         if vt.repo_type == 'git':
             assert 'fatal: Authentication failed' in stderr
         elif vt.repo_type == 'hg':
             assert 'abort: authorization failed' in stderr
 
     def test_clone_git_dir_as_hg(self, webserver):
-        clone_url = HgHttpVcsTest.repo_url_param(webserver, GIT_REPO)
-        stdout, stderr = Command(TESTS_TMP_PATH).execute('hg clone', clone_url, _get_tmp_dir(), ignoreReturnCode=True)
+        clone_url = HgHttpVcsTest.repo_url_param(webserver, base.GIT_REPO)
+        stdout, stderr = Command(base.TESTS_TMP_PATH).execute('hg clone', clone_url, _get_tmp_dir(), ignoreReturnCode=True)
         assert 'HTTP Error 404: Not Found' in stderr or "not a valid repository" in stdout and 'abort:' in stderr
 
     def test_clone_hg_repo_as_git(self, webserver):
-        clone_url = GitHttpVcsTest.repo_url_param(webserver, HG_REPO)
-        stdout, stderr = Command(TESTS_TMP_PATH).execute('git clone', clone_url, _get_tmp_dir(), ignoreReturnCode=True)
+        clone_url = GitHttpVcsTest.repo_url_param(webserver, base.HG_REPO)
+        stdout, stderr = Command(base.TESTS_TMP_PATH).execute('git clone', clone_url, _get_tmp_dir(), ignoreReturnCode=True)
         assert 'not found' in stderr
 
     @parametrize_vcs_test
     def test_clone_non_existing_path(self, webserver, vt):
         clone_url = vt.repo_url_param(webserver, 'trololo')
-        stdout, stderr = Command(TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, _get_tmp_dir(), ignoreReturnCode=True)
+        stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, _get_tmp_dir(), ignoreReturnCode=True)
         if vt.repo_type == 'git':
             assert 'not found' in stderr or 'abort: Access to %r denied' % 'trololo' in stderr
         elif vt.repo_type == 'hg':
@@ -318,30 +317,30 @@
         Session().commit()
 
         # Create an empty server repo using the API
-        repo_name = u'new_%s_%s' % (vt.repo_type, _RandomNameSequence().next())
-        usr = User.get_by_username(TEST_USER_ADMIN_LOGIN)
+        repo_name = 'new_%s_%s' % (vt.repo_type, next(_RandomNameSequence()))
+        usr = User.get_by_username(base.TEST_USER_ADMIN_LOGIN)
         params = {
             "id": 7,
             "api_key": usr.api_key,
             "method": 'create_repo',
             "args": dict(repo_name=repo_name,
-                         owner=TEST_USER_ADMIN_LOGIN,
+                         owner=base.TEST_USER_ADMIN_LOGIN,
                          repo_type=vt.repo_type),
         }
-        req = urllib2.Request(
+        req = urllib.request.Request(
             'http://%s:%s/_admin/api' % webserver.server_address,
-            data=json.dumps(params),
+            data=ascii_bytes(json.dumps(params)),
             headers={'content-type': 'application/json'})
-        response = urllib2.urlopen(req)
+        response = urllib.request.urlopen(req)
         result = json.loads(response.read())
         # Expect something like:
         # {u'result': {u'msg': u'Created new repository `new_XXX`', u'task': None, u'success': True}, u'id': 7, u'error': None}
-        assert result[u'result'][u'success']
+        assert result['result']['success']
 
         # Create local clone of the empty server repo
         local_clone_dir = _get_tmp_dir()
         clone_url = vt.repo_url_param(webserver, repo_name)
-        stdout, stderr = Command(TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, local_clone_dir)
+        stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, local_clone_dir)
 
         # Make 3 commits and push to the empty server repo.
         # The server repo doesn't have any other heads than the
@@ -361,15 +360,15 @@
         # <UserLog('id:new_git_XXX:push:aed9d4c1732a1927da3be42c47eb9afdc200d427,d38b083a07af10a9f44193486959a96a23db78da,4841ff9a2b385bec995f4679ef649adb3f437622')>
         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] == ([
-            (u'started_following_repo', 0),
-            (u'user_created_repo', 0),
-            (u'pull', 0),
-            (u'push', 3)]
+            ('started_following_repo', 0),
+            ('user_created_repo', 0),
+            ('pull', 0),
+            ('push', 3)]
             if vt.repo_type == 'git' else [
-            (u'started_following_repo', 0),
-            (u'user_created_repo', 0),
+            ('started_following_repo', 0),
+            ('user_created_repo', 0),
             # (u'pull', 0), # Mercurial outgoing hook is not called for empty clones
-            (u'push', 3)])
+            ('push', 3)])
 
     @parametrize_vcs_test
     def test_push_new_file(self, webserver, testfork, vt):
@@ -378,7 +377,7 @@
 
         dest_dir = _get_tmp_dir()
         clone_url = vt.repo_url_param(webserver, vt.repo_name)
-        stdout, stderr = Command(TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, dest_dir)
+        stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, dest_dir)
 
         clone_url = vt.repo_url_param(webserver, testfork[vt.repo_type])
         stdout, stderr = _add_files_and_push(webserver, vt, dest_dir, clone_url=clone_url)
@@ -392,7 +391,7 @@
 
         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] == \
-            [(u'pull', 0), (u'push', 3)]
+            [('pull', 0), ('push', 3)]
 
     @parametrize_vcs_test
     def test_pull(self, webserver, testfork, vt):
@@ -400,7 +399,7 @@
         Session().commit()
 
         dest_dir = _get_tmp_dir()
-        stdout, stderr = Command(TESTS_TMP_PATH).execute(vt.repo_type, 'init', dest_dir)
+        stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'init', dest_dir)
 
         clone_url = vt.repo_url_param(webserver, vt.repo_name)
         stdout, stderr = Command(dest_dir).execute(vt.repo_type, 'pull', clone_url)
@@ -411,42 +410,57 @@
             assert 'new changesets' in stdout
 
         action_parts = [ul.action for ul in UserLog.query().order_by(UserLog.user_log_id)]
-        assert action_parts == [u'pull']
+        assert action_parts == ['pull']
+
+        # Test handling of URLs with extra '/' around repo_name
+        stdout, stderr = Command(dest_dir).execute(vt.repo_type, 'pull', clone_url.replace('/' + vt.repo_name, '/./%s/' % vt.repo_name), ignoreReturnCode=True)
+        if issubclass(vt, HttpVcsTest):
+            if vt.repo_type == 'git':
+                # NOTE: when pulling from http://hostname/./vcs_test_git/ , the git client will normalize that and issue an HTTP request to /vcs_test_git/info/refs
+                assert 'Already up to date.' in stdout
+            else:
+                assert vt.repo_type == 'hg'
+                assert "abort: HTTP Error 404: Not Found" in stderr
+        else:
+            assert issubclass(vt, SshVcsTest)
+            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
+
+        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':
+            assert 'Already up to date.' in stdout
+        else:
+            assert vt.repo_type == 'hg'
+            assert "no changes found" in stdout
+        assert "denied" not in stderr
+        assert "denied" not in stdout
+        assert "404" not in stdout
 
     @parametrize_vcs_test
     def test_push_invalidates_cache(self, webserver, testfork, vt):
         pre_cached_tip = [repo.get_api_data()['last_changeset']['short_id'] for repo in Repository.query().filter(Repository.repo_name == testfork[vt.repo_type])]
 
-        key = CacheInvalidation.query().filter(CacheInvalidation.cache_key
-                                               == testfork[vt.repo_type]).scalar()
-        if not key:
-            key = CacheInvalidation(testfork[vt.repo_type], testfork[vt.repo_type])
-            Session().add(key)
-
-        key.cache_active = True
-        Session().commit()
-
         dest_dir = _get_tmp_dir()
         clone_url = vt.repo_url_param(webserver, testfork[vt.repo_type])
-        stdout, stderr = Command(TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, dest_dir)
+        stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, dest_dir)
 
         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)
 
         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
 
-        key = CacheInvalidation.query().filter(CacheInvalidation.cache_key
-                                               == testfork[vt.repo_type]).all()
-        assert key == []
-
     @parametrize_vcs_test_http
     def test_push_wrong_credentials(self, webserver, vt):
         dest_dir = _get_tmp_dir()
         clone_url = vt.repo_url_param(webserver, vt.repo_name)
-        stdout, stderr = Command(TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, dest_dir)
+        stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, dest_dir)
 
         clone_url = webserver.repo_url(vt.repo_name, username='bad', password='name')
         stdout, stderr = _add_files_and_push(webserver, vt, dest_dir,
@@ -463,8 +477,8 @@
         Session().commit()
 
         dest_dir = _get_tmp_dir()
-        clone_url = vt.repo_url_param(webserver, vt.repo_name, username=TEST_USER_REGULAR_LOGIN, password=TEST_USER_REGULAR_PASS)
-        stdout, stderr = Command(TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, dest_dir)
+        clone_url = vt.repo_url_param(webserver, vt.repo_name, username=base.TEST_USER_REGULAR_LOGIN, password=base.TEST_USER_REGULAR_PASS)
+        stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, dest_dir)
 
         stdout, stderr = _add_files_and_push(webserver, vt, dest_dir, ignoreReturnCode=True, clone_url=clone_url)
 
@@ -475,13 +489,13 @@
 
         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] == \
-            [(u'pull', 0)]
+            [('pull', 0)]
 
     @parametrize_vcs_test
     def test_push_back_to_wrong_url(self, webserver, vt):
         dest_dir = _get_tmp_dir()
         clone_url = vt.repo_url_param(webserver, vt.repo_name)
-        stdout, stderr = Command(TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, dest_dir)
+        stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, dest_dir)
 
         stdout, stderr = _add_files_and_push(
             webserver, vt, dest_dir, clone_url='http://%s:%s/tmp' % (
@@ -498,12 +512,12 @@
         user_model = UserModel()
         try:
             # Add IP constraint that excludes the test context:
-            user_model.add_extra_ip(TEST_USER_ADMIN_LOGIN, '10.10.10.10/32')
+            user_model.add_extra_ip(base.TEST_USER_ADMIN_LOGIN, '10.10.10.10/32')
             Session().commit()
             # IP permissions are cached, need to wait for the cache in the server process to expire
             time.sleep(1.5)
             clone_url = vt.repo_url_param(webserver, vt.repo_name)
-            stdout, stderr = Command(TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, _get_tmp_dir(), ignoreReturnCode=True)
+            stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, _get_tmp_dir(), ignoreReturnCode=True)
             if vt.repo_type == 'git':
                 # 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
@@ -518,7 +532,7 @@
             time.sleep(1.5)
 
         clone_url = vt.repo_url_param(webserver, vt.repo_name)
-        stdout, stderr = Command(TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, _get_tmp_dir())
+        stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, _get_tmp_dir())
 
         if vt.repo_type == 'git':
             assert 'Cloning into' in stdout + stderr
@@ -537,9 +551,9 @@
         Ui.create_or_update_hook('preoutgoing.testhook', 'python:kallithea.tests.fixture.failing_test_hook')
         Session().commit()
         # clone repo
-        clone_url = vt.repo_url_param(webserver, testfork[vt.repo_type], username=TEST_USER_ADMIN_LOGIN, password=TEST_USER_ADMIN_PASS)
+        clone_url = vt.repo_url_param(webserver, testfork[vt.repo_type], username=base.TEST_USER_ADMIN_LOGIN, password=base.TEST_USER_ADMIN_PASS)
         dest_dir = _get_tmp_dir()
-        stdout, stderr = Command(TESTS_TMP_PATH) \
+        stdout, stderr = Command(base.TESTS_TMP_PATH) \
             .execute(vt.repo_type, 'clone', clone_url, dest_dir, ignoreReturnCode=True)
         if vt.repo_type == 'hg':
             assert 'preoutgoing.testhook hook failed' in stdout
@@ -552,9 +566,9 @@
         Ui.create_or_update_hook('prechangegroup.testhook', 'python:kallithea.tests.fixture.failing_test_hook')
         Session().commit()
         # clone repo
-        clone_url = vt.repo_url_param(webserver, testfork[vt.repo_type], username=TEST_USER_ADMIN_LOGIN, password=TEST_USER_ADMIN_PASS)
+        clone_url = vt.repo_url_param(webserver, testfork[vt.repo_type], username=base.TEST_USER_ADMIN_LOGIN, password=base.TEST_USER_ADMIN_PASS)
         dest_dir = _get_tmp_dir()
-        stdout, stderr = Command(TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, dest_dir)
+        stdout, stderr = Command(base.TESTS_TMP_PATH).execute(vt.repo_type, 'clone', clone_url, dest_dir)
 
         stdout, stderr = _add_files_and_push(webserver, vt, dest_dir, clone_url,
                                              ignoreReturnCode=True)
@@ -595,19 +609,19 @@
 
     def test_add_submodule_git(self, webserver, testfork):
         dest_dir = _get_tmp_dir()
-        clone_url = GitHttpVcsTest.repo_url_param(webserver, GIT_REPO)
+        clone_url = GitHttpVcsTest.repo_url_param(webserver, base.GIT_REPO)
 
         fork_url = GitHttpVcsTest.repo_url_param(webserver, testfork['git'])
 
         # add submodule
-        stdout, stderr = Command(TESTS_TMP_PATH).execute('git clone', fork_url, dest_dir)
+        stdout, stderr = Command(base.TESTS_TMP_PATH).execute('git clone', fork_url, dest_dir)
         stdout, stderr = Command(dest_dir).execute('git submodule add', clone_url, 'testsubmodule')
-        stdout, stderr = Command(dest_dir).execute('git commit -am "added testsubmodule pointing to', clone_url, '"', EMAIL=TEST_USER_ADMIN_EMAIL)
+        stdout, stderr = Command(dest_dir).execute('git commit -am "added testsubmodule pointing to', clone_url, '"', EMAIL=base.TEST_USER_ADMIN_EMAIL)
         stdout, stderr = Command(dest_dir).execute('git push', fork_url, 'master')
 
         # check for testsubmodule link in files page
         self.log_user()
-        response = self.app.get(url(controller='files', action='index',
+        response = self.app.get(base.url(controller='files', action='index',
                                     repo_name=testfork['git'],
                                     revision='tip',
                                     f_path='/'))
@@ -617,7 +631,7 @@
         response.mustcontain('<a class="submodule-dir" href="%s" target="_blank"><i class="icon-file-submodule"></i><span>testsubmodule @ ' % clone_url)
 
         # check that following a submodule link actually works - and redirects
-        response = self.app.get(url(controller='files', action='index',
+        response = self.app.get(base.url(controller='files', action='index',
                                     repo_name=testfork['git'],
                                     revision='tip',
                                     f_path='/testsubmodule'),
--- a/kallithea/tests/performance/test_vcs.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/performance/test_vcs.py	Mon May 04 19:24:04 2020 +0200
@@ -15,11 +15,11 @@
 import pytest
 
 from kallithea.model.db import Repository
-from kallithea.tests.base import *
+from kallithea.tests import base
 
 
-@pytest.mark.skipif("not os.environ.has_key('TEST_PERFORMANCE')", reason="skipping performance tests, set TEST_PERFORMANCE in environment if desired")
-class TestVCSPerformance(TestController):
+@pytest.mark.skipif("'TEST_PERFORMANCE' not in os.environ", reason="skipping performance tests, set TEST_PERFORMANCE in environment if desired")
+class TestVCSPerformance(base.TestController):
 
     def graphmod(self, repo):
         """ Simple test for running the graph_data function for profiling/testing performance. """
@@ -31,7 +31,7 @@
         jsdata = graph_data(scm_inst, revs)
 
     def test_graphmod_hg(self, benchmark):
-        benchmark(self.graphmod, HG_REPO)
+        benchmark(self.graphmod, base.HG_REPO)
 
     def test_graphmod_git(self, benchmark):
-        benchmark(self.graphmod, GIT_REPO)
+        benchmark(self.graphmod, base.GIT_REPO)
--- a/kallithea/tests/scripts/manual_test_concurrency.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/scripts/manual_test_concurrency.py	Mon May 04 19:24:04 2020 +0200
@@ -26,8 +26,6 @@
 
 """
 
-from __future__ import print_function
-
 import logging
 import os
 import shutil
@@ -41,7 +39,6 @@
 
 from kallithea.config.environment import load_environment
 from kallithea.lib.auth import get_crypt_password
-from kallithea.lib.utils import setup_cache_regions
 from kallithea.model import meta
 from kallithea.model.base import init_model
 from kallithea.model.db import Repository, Ui, User
@@ -52,8 +49,6 @@
 conf = appconfig('config:development.ini', relative_to=rel_path)
 load_environment(conf.global_conf, conf.local_conf)
 
-setup_cache_regions(conf)
-
 USER = TEST_USER_ADMIN_LOGIN
 PASS = TEST_USER_ADMIN_PASS
 HOST = 'server.local'
@@ -205,7 +200,7 @@
             backend = 'hg'
 
         if METHOD == 'pull':
-            seq = tempfile._RandomNameSequence().next()
+            seq = next(tempfile._RandomNameSequence())
             test_clone_with_credentials(repo=sys.argv[1], method='clone',
                                         backend=backend)
         s = time.time()
--- a/kallithea/tests/scripts/manual_test_crawler.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/scripts/manual_test_crawler.py	Mon May 04 19:24:04 2020 +0200
@@ -1,4 +1,4 @@
-#!/usr/bin/env python2
+#!/usr/bin/env python3
 # -*- 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
@@ -30,15 +30,13 @@
 :license: GPLv3, see LICENSE.md for more details.
 """
 
-from __future__ import print_function
-
-import cookielib
+import http.cookiejar
 import os
 import sys
 import tempfile
 import time
-import urllib
-import urllib2
+import urllib.parse
+import urllib.request
 from os.path import dirname
 
 from kallithea.lib import vcs
@@ -72,18 +70,18 @@
 ]
 
 
-cj = cookielib.FileCookieJar(os.path.join(tempfile.gettempdir(), 'rc_test_cookie.txt'))
-o = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
+cj = http.cookiejar.FileCookieJar(os.path.join(tempfile.gettempdir(), 'rc_test_cookie.txt'))
+o = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
 o.addheaders = [
     ('User-agent', 'kallithea-crawler'),
     ('Accept-Language', 'en - us, en;q = 0.5')
 ]
 
-urllib2.install_opener(o)
+urllib.request.install_opener(o)
 
 
 def _get_repo(proj):
-    if isinstance(proj, basestring):
+    if isinstance(proj, str):
         repo = vcs.get_repo(os.path.join(PROJECT_PATH, proj))
         proj = proj
     else:
@@ -101,7 +99,7 @@
 
         page = '/'.join((proj, 'changelog',))
 
-        full_uri = (BASE_URI % page) + '?' + urllib.urlencode({'page': i})
+        full_uri = (BASE_URI % page) + '?' + urllib.parse.urlencode({'page': i})
         s = time.time()
         f = o.open(full_uri)
 
@@ -130,13 +128,13 @@
             break
 
         full_uri = (BASE_URI % raw_cs)
-        print('%s visiting %s\%s' % (cnt, full_uri, i))
+        print('%s visiting %s/%s' % (cnt, full_uri, i))
         s = time.time()
         f = o.open(full_uri)
         size = len(f.read())
         e = time.time() - s
         total_time += e
-        print('%s visited %s\%s size:%s req:%s ms' % (cnt, full_uri, i, size, e))
+        print('%s visited %s/%s size:%s req:%s ms' % (cnt, full_uri, i, size, e))
 
     print('total_time', total_time)
     print('average on req', total_time / float(cnt))
--- a/kallithea/tests/vcs/base.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/vcs/base.py	Mon May 04 19:24:04 2020 +0200
@@ -27,8 +27,8 @@
     def _get_commits(cls):
         commits = [
             {
-                'message': u'Initial commit',
-                'author': u'Joe Doe <joe.doe@example.com>',
+                'message': 'Initial commit',
+                'author': 'Joe Doe <joe.doe@example.com>',
                 'date': datetime.datetime(2010, 1, 1, 20),
                 'added': [
                     FileNode('foobar', content='Foobar'),
@@ -37,8 +37,8 @@
                 ],
             },
             {
-                'message': u'Changes...',
-                'author': u'Jane Doe <jane.doe@example.com>',
+                'message': 'Changes...',
+                'author': 'Jane Doe <jane.doe@example.com>',
                 'date': datetime.datetime(2010, 1, 1, 21),
                 'added': [
                     FileNode('some/new.txt', content='news...'),
@@ -79,8 +79,8 @@
             for node in commit.get('removed', []):
                 cls.imc.remove(FileNode(node.path))
 
-            cls.tip = cls.imc.commit(message=unicode(commit['message']),
-                                     author=unicode(commit['author']),
+            cls.tip = cls.imc.commit(message=commit['message'],
+                                     author=commit['author'],
                                      date=commit['date'])
 
     @pytest.fixture(autouse=True)
--- a/kallithea/tests/vcs/conf.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/vcs/conf.py	Mon May 04 19:24:04 2020 +0200
@@ -7,8 +7,8 @@
 # Retrieve the necessary configuration options from the test base
 # module. Some of these configuration options are subsequently
 # consumed by the VCS test module.
-from kallithea.tests.base import (
-    GIT_REMOTE_REPO, HG_REMOTE_REPO, TEST_GIT_REPO, TEST_GIT_REPO_CLONE, TEST_HG_REPO, TEST_HG_REPO_CLONE, TEST_HG_REPO_PULL, TESTS_TMP_PATH)
+from kallithea.tests.base import (GIT_REMOTE_REPO, HG_REMOTE_REPO, TEST_GIT_REPO, TEST_GIT_REPO_CLONE, TEST_HG_REPO, TEST_HG_REPO_CLONE, TEST_HG_REPO_PULL,
+                                  TESTS_TMP_PATH)
 
 
 __all__ = (
--- a/kallithea/tests/vcs/test_archives.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/vcs/test_archives.py	Mon May 04 19:24:04 2020 +0200
@@ -1,6 +1,6 @@
 import datetime
+import io
 import os
-import StringIO
 import tarfile
 import tempfile
 import zipfile
@@ -18,7 +18,7 @@
     @classmethod
     def _get_commits(cls):
         start_date = datetime.datetime(2010, 1, 1, 20)
-        for x in xrange(5):
+        for x in range(5):
             yield {
                 'message': 'Commit %d' % x,
                 'author': 'Joe Doe <joe.doe@example.com>',
@@ -35,11 +35,10 @@
             self.tip.fill_archive(stream=f, kind='zip', prefix='repo')
         out = zipfile.ZipFile(path)
 
-        for x in xrange(5):
+        for x in range(5):
             node_path = '%d/file_%d.txt' % (x, x)
-            decompressed = StringIO.StringIO()
-            decompressed.write(out.read('repo/' + node_path))
-            assert decompressed.getvalue() == self.tip.get_node(node_path).content
+            decompressed = out.read('repo/' + node_path)
+            assert decompressed == self.tip.get_node(node_path).content
 
     def test_archive_tgz(self):
         path = tempfile.mkstemp(dir=TESTS_TMP_PATH, prefix='test_archive_tgz-')[1]
@@ -50,9 +49,9 @@
         outfile = tarfile.open(path, 'r|gz')
         outfile.extractall(outdir)
 
-        for x in xrange(5):
+        for x in range(5):
             node_path = '%d/file_%d.txt' % (x, x)
-            assert open(os.path.join(outdir, 'repo/' + node_path)).read() == self.tip.get_node(node_path).content
+            assert open(os.path.join(outdir, 'repo/' + node_path), 'rb').read() == self.tip.get_node(node_path).content
 
     def test_archive_tbz2(self):
         path = tempfile.mkstemp(dir=TESTS_TMP_PATH, prefix='test_archive_tbz2-')[1]
@@ -63,15 +62,15 @@
         outfile = tarfile.open(path, 'r|bz2')
         outfile.extractall(outdir)
 
-        for x in xrange(5):
+        for x in range(5):
             node_path = '%d/file_%d.txt' % (x, x)
-            assert open(os.path.join(outdir, 'repo/' + node_path)).read() == self.tip.get_node(node_path).content
+            assert open(os.path.join(outdir, 'repo/' + node_path), 'rb').read() == self.tip.get_node(node_path).content
 
     def test_archive_default_stream(self):
         tmppath = tempfile.mkstemp(dir=TESTS_TMP_PATH, prefix='test_archive_default_stream-')[1]
         with open(tmppath, 'wb') as stream:
             self.tip.fill_archive(stream=stream)
-        mystream = StringIO.StringIO()
+        mystream = io.BytesIO()
         self.tip.fill_archive(stream=mystream)
         mystream.seek(0)
         with open(tmppath, 'rb') as f:
--- a/kallithea/tests/vcs/test_branches.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/vcs/test_branches.py	Mon May 04 19:24:04 2020 +0200
@@ -46,8 +46,8 @@
         self.imc.add(vcs.nodes.FileNode('docs/index.txt',
             content='Documentation\n'))
         foobar_tip = self.imc.commit(
-            message=u'New branch: foobar',
-            author=u'joe',
+            message='New branch: foobar',
+            author='joe',
             branch='foobar',
         )
         assert 'foobar' in self.repo.branches
@@ -59,23 +59,23 @@
         self.imc.add(vcs.nodes.FileNode('docs/index.txt',
             content='Documentation\n'))
         foobar_tip = self.imc.commit(
-            message=u'New branch: foobar',
-            author=u'joe',
+            message='New branch: foobar',
+            author='joe',
             branch='foobar',
             parents=[tip],
         )
         self.imc.change(vcs.nodes.FileNode('docs/index.txt',
             content='Documentation\nand more...\n'))
         newtip = self.imc.commit(
-            message=u'At default branch',
-            author=u'joe',
+            message='At default branch',
+            author='joe',
             branch=foobar_tip.branch,
             parents=[foobar_tip],
         )
 
         newest_tip = self.imc.commit(
-            message=u'Merged with %s' % foobar_tip.raw_id,
-            author=u'joe',
+            message='Merged with %s' % foobar_tip.raw_id,
+            author='joe',
             branch=self.backend_class.DEFAULT_BRANCH_NAME,
             parents=[newtip, foobar_tip],
         )
@@ -85,16 +85,16 @@
 
     def test_branch_with_slash_in_name(self):
         self.imc.add(vcs.nodes.FileNode('extrafile', content='Some data\n'))
-        self.imc.commit(u'Branch with a slash!', author=u'joe',
+        self.imc.commit('Branch with a slash!', author='joe',
             branch='issue/123')
         assert 'issue/123' in self.repo.branches
 
     def test_branch_with_slash_in_name_and_similar_without(self):
         self.imc.add(vcs.nodes.FileNode('extrafile', content='Some data\n'))
-        self.imc.commit(u'Branch with a slash!', author=u'joe',
+        self.imc.commit('Branch with a slash!', author='joe',
             branch='issue/123')
         self.imc.add(vcs.nodes.FileNode('extrafile II', content='Some data\n'))
-        self.imc.commit(u'Branch without a slash...', author=u'joe',
+        self.imc.commit('Branch without a slash...', author='joe',
             branch='123')
         assert 'issue/123' in self.repo.branches
         assert '123' in self.repo.branches
--- a/kallithea/tests/vcs/test_changesets.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/vcs/test_changesets.py	Mon May 04 19:24:04 2020 +0200
@@ -15,18 +15,16 @@
 
     def test_as_dict(self):
         changeset = BaseChangeset()
-        changeset.id = 'ID'
         changeset.raw_id = 'RAW_ID'
         changeset.short_id = 'SHORT_ID'
         changeset.revision = 1009
         changeset.date = datetime.datetime(2011, 1, 30, 1, 45)
         changeset.message = 'Message of a commit'
         changeset.author = 'Joe Doe <joe.doe@example.com>'
-        changeset.added = [FileNode('foo/bar/baz'), FileNode(u'foobar'), FileNode(u'blåbærgrød')]
+        changeset.added = [FileNode('foo/bar/baz'), FileNode('foobar'), FileNode('blåbærgrød')]
         changeset.changed = []
         changeset.removed = []
         assert changeset.as_dict() == {
-            'id': 'ID',
             'raw_id': 'RAW_ID',
             'short_id': 'SHORT_ID',
             'revision': 1009,
@@ -36,7 +34,7 @@
                 'name': 'Joe Doe',
                 'email': 'joe.doe@example.com',
             },
-            'added': ['foo/bar/baz', 'foobar', u'bl\xe5b\xe6rgr\xf8d'],
+            'added': ['foo/bar/baz', 'foobar', 'bl\xe5b\xe6rgr\xf8d'],
             'changed': [],
             'removed': [],
         }
@@ -47,7 +45,7 @@
     @classmethod
     def _get_commits(cls):
         start_date = datetime.datetime(2010, 1, 1, 20)
-        for x in xrange(5):
+        for x in range(5):
             yield {
                 'message': 'Commit %d' % x,
                 'author': 'Joe Doe <joe.doe@example.com>',
@@ -61,15 +59,15 @@
         self.imc.add(vcs.nodes.FileNode('docs/index.txt',
             content='Documentation\n'))
         foobar_tip = self.imc.commit(
-            message=u'New branch: foobar',
-            author=u'joe',
+            message='New branch: foobar',
+            author='joe',
             branch='foobar',
         )
         assert 'foobar' in self.repo.branches
         assert foobar_tip.branch == 'foobar'
         assert foobar_tip.branches == ['foobar']
         # 'foobar' should be the only branch that contains the new commit
-        branch_tips = self.repo.branches.values()
+        branch_tips = list(self.repo.branches.values())
         assert branch_tips.count(str(foobar_tip.raw_id)) == 1
 
     def test_new_head_in_default_branch(self):
@@ -77,23 +75,23 @@
         self.imc.add(vcs.nodes.FileNode('docs/index.txt',
             content='Documentation\n'))
         foobar_tip = self.imc.commit(
-            message=u'New branch: foobar',
-            author=u'joe',
+            message='New branch: foobar',
+            author='joe',
             branch='foobar',
             parents=[tip],
         )
         self.imc.change(vcs.nodes.FileNode('docs/index.txt',
             content='Documentation\nand more...\n'))
         newtip = self.imc.commit(
-            message=u'At default branch',
-            author=u'joe',
+            message='At default branch',
+            author='joe',
             branch=foobar_tip.branch,
             parents=[foobar_tip],
         )
 
         newest_tip = self.imc.commit(
-            message=u'Merged with %s' % foobar_tip.raw_id,
-            author=u'joe',
+            message='Merged with %s' % foobar_tip.raw_id,
+            author='joe',
             branch=self.backend_class.DEFAULT_BRANCH_NAME,
             parents=[newtip, foobar_tip],
         )
@@ -106,14 +104,14 @@
         self.imc.add(vcs.nodes.FileNode('docs/index.txt',
             content='Documentation\n'))
         doc_changeset = self.imc.commit(
-            message=u'New branch: docs',
-            author=u'joe',
+            message='New branch: docs',
+            author='joe',
             branch='docs',
         )
         self.imc.add(vcs.nodes.FileNode('newfile', content=''))
         self.imc.commit(
-            message=u'Back in default branch',
-            author=u'joe',
+            message='Back in default branch',
+            author='joe',
             parents=[tip],
         )
         default_branch_changesets = self.repo.get_changesets(
@@ -121,11 +119,11 @@
         assert doc_changeset not in default_branch_changesets
 
     def test_get_changeset_by_branch(self):
-        for branch, sha in self.repo.branches.iteritems():
+        for branch, sha in self.repo.branches.items():
             assert sha == self.repo.get_changeset(branch).raw_id
 
     def test_get_changeset_by_tag(self):
-        for tag, sha in self.repo.tags.iteritems():
+        for tag, sha in self.repo.tags.items():
             assert sha == self.repo.get_changeset(tag).raw_id
 
     def test_get_changeset_parents(self):
@@ -145,10 +143,10 @@
     @classmethod
     def _get_commits(cls):
         start_date = datetime.datetime(2010, 1, 1, 20)
-        for x in xrange(5):
+        for x in range(5):
             yield {
-                'message': u'Commit %d' % x,
-                'author': u'Joe Doe <joe.doe@example.com>',
+                'message': 'Commit %d' % x,
+                'author': 'Joe Doe <joe.doe@example.com>',
                 'date': start_date + datetime.timedelta(hours=12 * x),
                 'added': [
                     FileNode('file_%d.txt' % x, content='Foobar %d' % x),
@@ -240,7 +238,7 @@
     def test_get_filenodes_generator(self):
         tip = self.repo.get_changeset()
         filepaths = [node.path for node in tip.get_filenodes_generator()]
-        assert filepaths == ['file_%d.txt' % x for x in xrange(5)]
+        assert filepaths == ['file_%d.txt' % x for x in range(5)]
 
     def test_size(self):
         tip = self.repo.get_changeset()
@@ -249,15 +247,15 @@
 
     def test_author(self):
         tip = self.repo.get_changeset()
-        assert tip.author == u'Joe Doe <joe.doe@example.com>'
+        assert tip.author == 'Joe Doe <joe.doe@example.com>'
 
     def test_author_name(self):
         tip = self.repo.get_changeset()
-        assert tip.author_name == u'Joe Doe'
+        assert tip.author_name == 'Joe Doe'
 
     def test_author_email(self):
         tip = self.repo.get_changeset()
-        assert tip.author_email == u'joe.doe@example.com'
+        assert tip.author_email == 'joe.doe@example.com'
 
     def test_get_changesets_raise_changesetdoesnotexist_for_wrong_start(self):
         with pytest.raises(ChangesetDoesNotExistError):
@@ -299,8 +297,8 @@
     def _get_commits(cls):
         return [
             {
-                'message': u'Initial',
-                'author': u'Joe Doe <joe.doe@example.com>',
+                'message': 'Initial',
+                'author': 'Joe Doe <joe.doe@example.com>',
                 'date': datetime.datetime(2010, 1, 1, 20),
                 'added': [
                     FileNode('foo/bar', content='foo'),
@@ -310,8 +308,8 @@
                 ],
             },
             {
-                'message': u'Massive changes',
-                'author': u'Joe Doe <joe.doe@example.com>',
+                'message': 'Massive changes',
+                'author': 'Joe Doe <joe.doe@example.com>',
                 'date': datetime.datetime(2010, 1, 1, 22),
                 'added': [FileNode('fallout', content='War never changes')],
                 'changed': [
@@ -332,8 +330,8 @@
         ])
         assert list(changeset.changed) == []
         assert list(changeset.removed) == []
-        assert u'foo/ba\u0142' in changeset.as_dict()['added']
-        assert u'foo/ba\u0142' in changeset.__json__(with_file_list=True)['added']
+        assert 'foo/ba\u0142' in changeset.as_dict()['added']
+        assert 'foo/ba\u0142' in changeset.__json__(with_file_list=True)['added']
 
     def test_head_added(self):
         changeset = self.repo.get_changeset()
@@ -357,7 +355,7 @@
     def test_get_filemode_non_ascii(self):
         changeset = self.repo.get_changeset()
         assert 33188 == changeset.get_file_mode('foo/bał')
-        assert 33188 == changeset.get_file_mode(u'foo/bał')
+        assert 33188 == changeset.get_file_mode('foo/bał')
 
 
 class TestGitChangesetsWithCommits(_ChangesetsWithCommitsTestCaseixin):
--- a/kallithea/tests/vcs/test_filenodes_unicode_path.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/vcs/test_filenodes_unicode_path.py	Mon May 04 19:24:04 2020 +0200
@@ -9,28 +9,21 @@
 class FileNodeUnicodePathTestsMixin(_BackendTestMixin):
 
     fname = 'ąśðąęłąć.txt'
-    ufname = (fname).decode('utf-8')
 
     @classmethod
     def _get_commits(cls):
-        cls.nodes = [
-            FileNode(cls.fname, content='Foobar'),
-        ]
-
-        commits = [
+        return [
             {
                 'message': 'Initial commit',
                 'author': 'Joe Doe <joe.doe@example.com>',
                 'date': datetime.datetime(2010, 1, 1, 20),
-                'added': cls.nodes,
+                'added': [FileNode(cls.fname, content='Foobar')],
             },
         ]
-        return commits
 
     def test_filenode_path(self):
         node = self.tip.get_node(self.fname)
-        unode = self.tip.get_node(self.ufname)
-        assert node == unode
+        assert node.path == self.fname
 
 
 class TestGitFileNodeUnicodePath(FileNodeUnicodePathTestsMixin):
--- a/kallithea/tests/vcs/test_getitem.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/vcs/test_getitem.py	Mon May 04 19:24:04 2020 +0200
@@ -9,7 +9,7 @@
     @classmethod
     def _get_commits(cls):
         start_date = datetime.datetime(2010, 1, 1, 20)
-        for x in xrange(5):
+        for x in range(5):
             yield {
                 'message': 'Commit %d' % x,
                 'author': 'Joe Doe <joe.doe@example.com>',
@@ -23,7 +23,7 @@
         assert self.repo[-1] == self.repo.get_changeset()
 
     def test__getitem__returns_correct_items(self):
-        changesets = [self.repo[x] for x in xrange(len(self.repo.revisions))]
+        changesets = [self.repo[x] for x in range(len(self.repo.revisions))]
         assert changesets == list(self.repo.get_changesets())
 
 
--- a/kallithea/tests/vcs/test_getslice.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/vcs/test_getslice.py	Mon May 04 19:24:04 2020 +0200
@@ -9,7 +9,7 @@
     @classmethod
     def _get_commits(cls):
         start_date = datetime.datetime(2010, 1, 1, 20)
-        for x in xrange(5):
+        for x in range(5):
             yield {
                 'message': 'Commit %d' % x,
                 'author': 'Joe Doe <joe.doe@example.com>',
--- a/kallithea/tests/vcs/test_git.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/vcs/test_git.py	Mon May 04 19:24:04 2020 +0200
@@ -1,7 +1,6 @@
 import datetime
 import os
 import sys
-import urllib2
 
 import mock
 import pytest
@@ -31,8 +30,8 @@
             GitRepository(wrong_repo_path)
 
     def test_git_cmd_injection(self):
-        repo_inject_path = TEST_GIT_REPO + '; echo "Cake";'
-        with pytest.raises(urllib2.URLError):
+        repo_inject_path = 'file:/%s; echo "Cake";' % TEST_GIT_REPO
+        with pytest.raises(RepositoryError):
             # Should fail because URL will contain the parts after ; too
             GitRepository(get_new_dir('injection-repo'), src_url=repo_inject_path, update_after_clone=True, create=True)
 
@@ -229,7 +228,7 @@
     def test_changeset10(self):
 
         chset10 = self.repo.get_changeset(self.repo.revisions[9])
-        readme = """===
+        readme = b"""===
 VCS
 ===
 
@@ -343,7 +342,7 @@
         start = offset
         end = limit and offset + limit or None
         sliced = list(self.repo[start:end])
-        pytest.failUnlessEqual(result, sliced,
+        pytest.assertEqual(result, sliced,
             msg="Comparison failed for limit=%s, offset=%s"
             "(get_changeset returned: %s and sliced: %s"
             % (limit, offset, result, sliced))
@@ -588,19 +587,19 @@
             'vcs/nodes.py']
         assert set(changed) == set([f.path for f in chset.changed])
 
-    def test_commit_message_is_unicode(self):
+    def test_commit_message_is_str(self):
         for cs in self.repo:
-            assert type(cs.message) == unicode
+            assert isinstance(cs.message, str)
 
-    def test_changeset_author_is_unicode(self):
+    def test_changeset_author_is_str(self):
         for cs in self.repo:
-            assert type(cs.author) == unicode
+            assert isinstance(cs.author, str)
 
-    def test_repo_files_content_is_unicode(self):
+    def test_repo_files_content_is_bytes(self):
         changeset = self.repo.get_changeset()
         for node in changeset.get_node('/'):
             if node.is_file():
-                assert type(node.content) == unicode
+                assert isinstance(node.content, bytes)
 
     def test_wrong_path(self):
         # There is 'setup.py' in the root dir but not there:
@@ -620,30 +619,6 @@
         assert 'marcink none@none' == self.repo.get_changeset('8430a588b43b5d6da365400117c89400326e7992').author_name
 
 
-class TestGitSpecific():
-
-    def test_error_is_raised_for_added_if_diff_name_status_is_wrong(self):
-        repo = mock.MagicMock()
-        changeset = GitChangeset(repo, 'foobar')
-        changeset._diff_name_status = 'foobar'
-        with pytest.raises(VCSError):
-            changeset.added
-
-    def test_error_is_raised_for_changed_if_diff_name_status_is_wrong(self):
-        repo = mock.MagicMock()
-        changeset = GitChangeset(repo, 'foobar')
-        changeset._diff_name_status = 'foobar'
-        with pytest.raises(VCSError):
-            changeset.added
-
-    def test_error_is_raised_for_removed_if_diff_name_status_is_wrong(self):
-        repo = mock.MagicMock()
-        changeset = GitChangeset(repo, 'foobar')
-        changeset._diff_name_status = 'foobar'
-        with pytest.raises(VCSError):
-            changeset.added
-
-
 class TestGitSpecificWithRepo(_BackendTestMixin):
     backend_alias = 'git'
 
@@ -657,7 +632,7 @@
                 'added': [
                     FileNode('foobar/static/js/admin/base.js', content='base'),
                     FileNode('foobar/static/admin', content='admin',
-                        mode=0120000), # this is a link
+                        mode=0o120000), # this is a link
                     FileNode('foo', content='foo'),
                 ],
             },
@@ -673,11 +648,11 @@
 
     def test_paths_slow_traversing(self):
         cs = self.repo.get_changeset()
-        assert cs.get_node('foobar').get_node('static').get_node('js').get_node('admin').get_node('base.js').content == 'base'
+        assert cs.get_node('foobar').get_node('static').get_node('js').get_node('admin').get_node('base.js').content == b'base'
 
     def test_paths_fast_traversing(self):
         cs = self.repo.get_changeset()
-        assert cs.get_node('foobar/static/js/admin/base.js').content == 'base'
+        assert cs.get_node('foobar/static/js/admin/base.js').content == b'base'
 
     def test_workdir_get_branch(self):
         self.repo.run_git_command(['checkout', '-b', 'production'])
@@ -689,65 +664,65 @@
         assert self.repo.workdir.get_branch() == 'master'
 
     def test_get_diff_runs_git_command_with_hashes(self):
-        self.repo.run_git_command = mock.Mock(return_value=['', ''])
+        self.repo._run_git_command = mock.Mock(return_value=(b'', b''))
         self.repo.get_diff(0, 1)
-        self.repo.run_git_command.assert_called_once_with(
+        self.repo._run_git_command.assert_called_once_with(
             ['diff', '-U3', '--full-index', '--binary', '-p', '-M', '--abbrev=40',
-             self.repo._get_revision(0), self.repo._get_revision(1)])
+             self.repo._get_revision(0), self.repo._get_revision(1)], cwd=self.repo.path)
 
     def test_get_diff_runs_git_command_with_str_hashes(self):
-        self.repo.run_git_command = mock.Mock(return_value=['', ''])
+        self.repo._run_git_command = mock.Mock(return_value=(b'', b''))
         self.repo.get_diff(self.repo.EMPTY_CHANGESET, 1)
-        self.repo.run_git_command.assert_called_once_with(
+        self.repo._run_git_command.assert_called_once_with(
             ['show', '-U3', '--full-index', '--binary', '-p', '-M', '--abbrev=40',
-             self.repo._get_revision(1)])
+             self.repo._get_revision(1)], cwd=self.repo.path)
 
     def test_get_diff_runs_git_command_with_path_if_its_given(self):
-        self.repo.run_git_command = mock.Mock(return_value=['', ''])
+        self.repo._run_git_command = mock.Mock(return_value=(b'', b''))
         self.repo.get_diff(0, 1, 'foo')
-        self.repo.run_git_command.assert_called_once_with(
+        self.repo._run_git_command.assert_called_once_with(
             ['diff', '-U3', '--full-index', '--binary', '-p', '-M', '--abbrev=40',
-             self.repo._get_revision(0), self.repo._get_revision(1), '--', 'foo'])
+             self.repo._get_revision(0), self.repo._get_revision(1), '--', 'foo'], cwd=self.repo.path)
 
     def test_get_diff_does_not_sanitize_valid_context(self):
         almost_overflowed_long_int = 2**31-1
 
-        self.repo.run_git_command = mock.Mock(return_value=['', ''])
+        self.repo._run_git_command = mock.Mock(return_value=(b'', b''))
         self.repo.get_diff(0, 1, 'foo', context=almost_overflowed_long_int)
-        self.repo.run_git_command.assert_called_once_with(
+        self.repo._run_git_command.assert_called_once_with(
             ['diff', '-U' + str(almost_overflowed_long_int), '--full-index', '--binary', '-p', '-M', '--abbrev=40',
-             self.repo._get_revision(0), self.repo._get_revision(1), '--', 'foo'])
+             self.repo._get_revision(0), self.repo._get_revision(1), '--', 'foo'], cwd=self.repo.path)
 
     def test_get_diff_sanitizes_overflowing_context(self):
         overflowed_long_int = 2**31
         sanitized_overflowed_long_int = overflowed_long_int-1
 
-        self.repo.run_git_command = mock.Mock(return_value=['', ''])
+        self.repo._run_git_command = mock.Mock(return_value=(b'', b''))
         self.repo.get_diff(0, 1, 'foo', context=overflowed_long_int)
 
-        self.repo.run_git_command.assert_called_once_with(
+        self.repo._run_git_command.assert_called_once_with(
             ['diff', '-U' + str(sanitized_overflowed_long_int), '--full-index', '--binary', '-p', '-M', '--abbrev=40',
-             self.repo._get_revision(0), self.repo._get_revision(1), '--', 'foo'])
+             self.repo._get_revision(0), self.repo._get_revision(1), '--', 'foo'], cwd=self.repo.path)
 
     def test_get_diff_does_not_sanitize_zero_context(self):
         zero_context = 0
 
-        self.repo.run_git_command = mock.Mock(return_value=['', ''])
+        self.repo._run_git_command = mock.Mock(return_value=(b'', b''))
         self.repo.get_diff(0, 1, 'foo', context=zero_context)
 
-        self.repo.run_git_command.assert_called_once_with(
+        self.repo._run_git_command.assert_called_once_with(
             ['diff', '-U' + str(zero_context), '--full-index', '--binary', '-p', '-M', '--abbrev=40',
-             self.repo._get_revision(0), self.repo._get_revision(1), '--', 'foo'])
+             self.repo._get_revision(0), self.repo._get_revision(1), '--', 'foo'], cwd=self.repo.path)
 
     def test_get_diff_sanitizes_negative_context(self):
         negative_context = -10
 
-        self.repo.run_git_command = mock.Mock(return_value=['', ''])
+        self.repo._run_git_command = mock.Mock(return_value=(b'', b''))
         self.repo.get_diff(0, 1, 'foo', context=negative_context)
 
-        self.repo.run_git_command.assert_called_once_with(
+        self.repo._run_git_command.assert_called_once_with(
             ['diff', '-U0', '--full-index', '--binary', '-p', '-M', '--abbrev=40',
-             self.repo._get_revision(0), self.repo._get_revision(1), '--', 'foo'])
+             self.repo._get_revision(0), self.repo._get_revision(1), '--', 'foo'], cwd=self.repo.path)
 
 
 class TestGitRegression(_BackendTestMixin):
@@ -804,22 +779,24 @@
         self.repo = GitRepository(self.repo_directory, create=True)
 
         # Create a dictionary where keys are hook names, and values are paths to
-        # them. Deduplicates code in tests a bit.
-        self.hook_directory = self.repo.get_hook_location()
-        self.kallithea_hooks = dict((h, os.path.join(self.hook_directory, h)) for h in ("pre-receive", "post-receive"))
+        # them in the non-bare repo. Deduplicates code in tests a bit.
+        self.kallithea_hooks = {
+            "pre-receive": os.path.join(self.repo.path, '.git', 'hooks', "pre-receive"),
+            "post-receive": os.path.join(self.repo.path, '.git', 'hooks', "post-receive"),
+        }
 
     def test_hooks_created_if_missing(self):
         """
         Tests if hooks are installed in repository if they are missing.
         """
 
-        for hook, hook_path in self.kallithea_hooks.iteritems():
+        for hook, hook_path in self.kallithea_hooks.items():
             if os.path.exists(hook_path):
                 os.remove(hook_path)
 
         ScmModel().install_git_hooks(repo=self.repo)
 
-        for hook, hook_path in self.kallithea_hooks.iteritems():
+        for hook, hook_path in self.kallithea_hooks.items():
             assert os.path.exists(hook_path)
 
     def test_kallithea_hooks_updated(self):
@@ -827,13 +804,13 @@
         Tests if hooks are updated if they are Kallithea hooks already.
         """
 
-        for hook, hook_path in self.kallithea_hooks.iteritems():
+        for hook, hook_path in self.kallithea_hooks.items():
             with open(hook_path, "w") as f:
                 f.write("KALLITHEA_HOOK_VER=0.0.0\nJUST_BOGUS")
 
         ScmModel().install_git_hooks(repo=self.repo)
 
-        for hook, hook_path in self.kallithea_hooks.iteritems():
+        for hook, hook_path in self.kallithea_hooks.items():
             with open(hook_path) as f:
                 assert "JUST_BOGUS" not in f.read()
 
@@ -842,13 +819,13 @@
         Tests if hooks are left untouched if they are not Kallithea hooks.
         """
 
-        for hook, hook_path in self.kallithea_hooks.iteritems():
+        for hook, hook_path in self.kallithea_hooks.items():
             with open(hook_path, "w") as f:
                 f.write("#!/bin/bash\n#CUSTOM_HOOK")
 
         ScmModel().install_git_hooks(repo=self.repo)
 
-        for hook, hook_path in self.kallithea_hooks.iteritems():
+        for hook, hook_path in self.kallithea_hooks.items():
             with open(hook_path) as f:
                 assert "CUSTOM_HOOK" in f.read()
 
@@ -857,12 +834,12 @@
         Tests if hooks are forcefully updated even though they are custom hooks.
         """
 
-        for hook, hook_path in self.kallithea_hooks.iteritems():
+        for hook, hook_path in self.kallithea_hooks.items():
             with open(hook_path, "w") as f:
                 f.write("#!/bin/bash\n#CUSTOM_HOOK")
 
         ScmModel().install_git_hooks(repo=self.repo, force_create=True)
 
-        for hook, hook_path in self.kallithea_hooks.iteritems():
+        for hook, hook_path in self.kallithea_hooks.items():
             with open(hook_path) as f:
                 assert "KALLITHEA_HOOK_VER" in f.read()
--- a/kallithea/tests/vcs/test_hg.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/vcs/test_hg.py	Mon May 04 19:24:04 2020 +0200
@@ -3,7 +3,6 @@
 import mock
 import pytest
 
-from kallithea.lib.utils2 import safe_str
 from kallithea.lib.vcs.backends.hg import MercurialChangeset, MercurialRepository
 from kallithea.lib.vcs.exceptions import NodeDoesNotExistError, RepositoryError, VCSError
 from kallithea.lib.vcs.nodes import NodeKind, NodeState
@@ -19,7 +18,7 @@
                       % TEST_HG_REPO_CLONE)
 
     def setup_method(self):
-        self.repo = MercurialRepository(safe_str(TEST_HG_REPO))
+        self.repo = MercurialRepository(TEST_HG_REPO)
 
     def test_wrong_repo_path(self):
         wrong_repo_path = os.path.join(TESTS_TMP_PATH, 'errorrepo')
@@ -28,11 +27,11 @@
 
     def test_unicode_path_repo(self):
         with pytest.raises(VCSError):
-            MercurialRepository(u'iShouldFail')
+            MercurialRepository('iShouldFail')
 
     def test_repo_clone(self):
         self.__check_for_existing_repo()
-        repo = MercurialRepository(safe_str(TEST_HG_REPO))
+        repo = MercurialRepository(TEST_HG_REPO)
         repo_clone = MercurialRepository(TEST_HG_REPO_CLONE,
             src_url=TEST_HG_REPO, update_after_clone=True)
         assert len(repo.revisions) == len(repo_clone.revisions)
@@ -42,7 +41,7 @@
             assert raw_id == repo_clone.get_changeset(raw_id).raw_id
 
     def test_repo_clone_with_update(self):
-        repo = MercurialRepository(safe_str(TEST_HG_REPO))
+        repo = MercurialRepository(TEST_HG_REPO)
         repo_clone = MercurialRepository(TEST_HG_REPO_CLONE + '_w_update',
             src_url=TEST_HG_REPO, update_after_clone=True)
         assert len(repo.revisions) == len(repo_clone.revisions)
@@ -55,7 +54,7 @@
         )
 
     def test_repo_clone_without_update(self):
-        repo = MercurialRepository(safe_str(TEST_HG_REPO))
+        repo = MercurialRepository(TEST_HG_REPO)
         repo_clone = MercurialRepository(TEST_HG_REPO_CLONE + '_wo_update',
             src_url=TEST_HG_REPO, update_after_clone=False)
         assert len(repo.revisions) == len(repo_clone.revisions)
@@ -219,7 +218,7 @@
     def test_changeset10(self):
 
         chset10 = self.repo.get_changeset(10)
-        readme = """===
+        readme = b"""===
 VCS
 ===
 
@@ -235,7 +234,7 @@
         assert node.kind == NodeKind.FILE
         assert node.content == readme
 
-    @mock.patch('kallithea.lib.vcs.backends.hg.repository.diffopts')
+    @mock.patch('mercurial.mdiff.diffopts')
     def test_get_diff_does_not_sanitize_zero_context(self, mock_diffopts):
         zero_context = 0
 
@@ -243,7 +242,7 @@
 
         mock_diffopts.assert_called_once_with(git=True, showfunc=True, ignorews=False, context=zero_context)
 
-    @mock.patch('kallithea.lib.vcs.backends.hg.repository.diffopts')
+    @mock.patch('mercurial.mdiff.diffopts')
     def test_get_diff_sanitizes_negative_context(self, mock_diffopts):
         negative_context = -10
         zero_context = 0
@@ -256,7 +255,7 @@
 class TestMercurialChangeset(object):
 
     def setup_method(self):
-        self.repo = MercurialRepository(safe_str(TEST_HG_REPO))
+        self.repo = MercurialRepository(TEST_HG_REPO)
 
     def _test_equality(self, changeset):
         revision = changeset.revision
@@ -444,20 +443,20 @@
         #    added:   20
         #    removed: 1
         changed = set(['.hgignore'
-            , 'README.rst' , 'docs/conf.py' , 'docs/index.rst' , 'setup.py'
-            , 'tests/test_hg.py' , 'tests/test_nodes.py' , 'vcs/__init__.py'
-            , 'vcs/backends/__init__.py' , 'vcs/backends/base.py'
-            , 'vcs/backends/hg.py' , 'vcs/nodes.py' , 'vcs/utils/__init__.py'])
+            , 'README.rst', 'docs/conf.py', 'docs/index.rst', 'setup.py'
+            , 'tests/test_hg.py', 'tests/test_nodes.py', 'vcs/__init__.py'
+            , 'vcs/backends/__init__.py', 'vcs/backends/base.py'
+            , 'vcs/backends/hg.py', 'vcs/nodes.py', 'vcs/utils/__init__.py'])
 
         added = set(['docs/api/backends/hg.rst'
-            , 'docs/api/backends/index.rst' , 'docs/api/index.rst'
-            , 'docs/api/nodes.rst' , 'docs/api/web/index.rst'
-            , 'docs/api/web/simplevcs.rst' , 'docs/installation.rst'
-            , 'docs/quickstart.rst' , 'setup.cfg' , 'vcs/utils/baseui_config.py'
-            , 'vcs/utils/web.py' , 'vcs/web/__init__.py' , 'vcs/web/exceptions.py'
-            , 'vcs/web/simplevcs/__init__.py' , 'vcs/web/simplevcs/exceptions.py'
-            , 'vcs/web/simplevcs/middleware.py' , 'vcs/web/simplevcs/models.py'
-            , 'vcs/web/simplevcs/settings.py' , 'vcs/web/simplevcs/utils.py'
+            , 'docs/api/backends/index.rst', 'docs/api/index.rst'
+            , 'docs/api/nodes.rst', 'docs/api/web/index.rst'
+            , 'docs/api/web/simplevcs.rst', 'docs/installation.rst'
+            , 'docs/quickstart.rst', 'setup.cfg', 'vcs/utils/baseui_config.py'
+            , 'vcs/utils/web.py', 'vcs/web/__init__.py', 'vcs/web/exceptions.py'
+            , 'vcs/web/simplevcs/__init__.py', 'vcs/web/simplevcs/exceptions.py'
+            , 'vcs/web/simplevcs/middleware.py', 'vcs/web/simplevcs/models.py'
+            , 'vcs/web/simplevcs/settings.py', 'vcs/web/simplevcs/utils.py'
             , 'vcs/web/simplevcs/views.py'])
 
         removed = set(['docs/api.rst'])
@@ -536,19 +535,19 @@
         # but it would be one of ``removed`` (changeset's attribute)
         assert path in [rf.path for rf in chset.removed]
 
-    def test_commit_message_is_unicode(self):
+    def test_commit_message_is_str(self):
         for cm in self.repo:
-            assert type(cm.message) == unicode
+            assert isinstance(cm.message, str)
 
-    def test_changeset_author_is_unicode(self):
+    def test_changeset_author_is_str(self):
         for cm in self.repo:
-            assert type(cm.author) == unicode
+            assert isinstance(cm.author, str)
 
-    def test_repo_files_content_is_unicode(self):
+    def test_repo_files_content_is_bytes(self):
         test_changeset = self.repo.get_changeset(100)
         for node in test_changeset.get_node('/'):
             if node.is_file():
-                assert type(node.content) == unicode
+                assert isinstance(node.content, bytes)
 
     def test_wrong_path(self):
         # There is 'setup.py' in the root dir but not there:
--- a/kallithea/tests/vcs/test_inmemchangesets.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/vcs/test_inmemchangesets.py	Mon May 04 19:24:04 2020 +0200
@@ -7,11 +7,9 @@
 
 import pytest
 
-from kallithea.lib import vcs
-from kallithea.lib.vcs.exceptions import (
-    EmptyRepositoryError, NodeAlreadyAddedError, NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError, NodeDoesNotExistError, NodeNotChangedError)
+from kallithea.lib.vcs.exceptions import (EmptyRepositoryError, NodeAlreadyAddedError, NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
+                                          NodeDoesNotExistError, NodeNotChangedError)
 from kallithea.lib.vcs.nodes import DirNode, FileNode
-from kallithea.lib.vcs.utils import safe_unicode
 from kallithea.tests.vcs.base import _BackendTestMixin
 
 
@@ -38,8 +36,8 @@
             for node in self.nodes]
         for node in to_add:
             self.imc.add(node)
-        message = u'Added: %s' % ', '.join((node.path for node in self.nodes))
-        author = unicode(self.__class__)
+        message = 'Added: %s' % ', '.join((node.path for node in self.nodes))
+        author = str(self.__class__)
         changeset = self.imc.commit(message=message, author=author)
 
         newtip = self.repo.get_changeset()
@@ -60,8 +58,8 @@
         to_add = [FileNode(node.path, content=node.content)
             for node in self.nodes]
         self.imc.add(*to_add)
-        message = u'Added: %s' % ', '.join((node.path for node in self.nodes))
-        author = unicode(self.__class__)
+        message = 'Added: %s' % ', '.join((node.path for node in self.nodes))
+        author = str(self.__class__)
         changeset = self.imc.commit(message=message, author=author)
 
         newtip = self.repo.get_changeset()
@@ -80,11 +78,11 @@
     def test_add_actually_adds_all_nodes_at_second_commit_too(self):
         self.imc.add(FileNode('foo/bar/image.png', content='\0'))
         self.imc.add(FileNode('foo/README.txt', content='readme!'))
-        changeset = self.imc.commit(u'Initial', u'joe.doe@example.com')
+        changeset = self.imc.commit('Initial', 'joe.doe@example.com')
         assert isinstance(changeset.get_node('foo'), DirNode)
         assert isinstance(changeset.get_node('foo/bar'), DirNode)
-        assert changeset.get_node('foo/bar/image.png').content == '\0'
-        assert changeset.get_node('foo/README.txt').content == 'readme!'
+        assert changeset.get_node('foo/bar/image.png').content == b'\0'
+        assert changeset.get_node('foo/README.txt').content == b'readme!'
 
         # commit some more files again
         to_add = [
@@ -95,23 +93,23 @@
             FileNode('foobar/barbaz', content='foo'),
         ]
         self.imc.add(*to_add)
-        changeset = self.imc.commit(u'Another', u'joe.doe@example.com')
-        changeset.get_node('foo/bar/foobaz/bar').content == 'foo'
-        changeset.get_node('foo/bar/another/bar').content == 'foo'
-        changeset.get_node('foo/baz.txt').content == 'foo'
-        changeset.get_node('foobar/foobaz/file').content == 'foo'
-        changeset.get_node('foobar/barbaz').content == 'foo'
+        changeset = self.imc.commit('Another', 'joe.doe@example.com')
+        changeset.get_node('foo/bar/foobaz/bar').content == b'foo'
+        changeset.get_node('foo/bar/another/bar').content == b'foo'
+        changeset.get_node('foo/baz.txt').content == b'foo'
+        changeset.get_node('foobar/foobaz/file').content == b'foo'
+        changeset.get_node('foobar/barbaz').content == b'foo'
 
     def test_add_non_ascii_files(self):
         rev_count = len(self.repo.revisions)
         to_add = [
             FileNode('żółwik/zwierzątko', content='ćććć'),
-            FileNode(u'żółwik/zwierzątko_uni', content=u'ćććć'),
+            FileNode('żółwik/zwierzątko_uni', content='ćććć'),
         ]
         for node in to_add:
             self.imc.add(node)
-        message = u'Added: %s' % ', '.join((node.path for node in self.nodes))
-        author = unicode(self.__class__)
+        message = 'Added: %s' % ', '.join((node.path for node in self.nodes))
+        author = str(self.__class__)
         changeset = self.imc.commit(message=message, author=author)
 
         newtip = self.repo.get_changeset()
@@ -136,7 +134,7 @@
     def test_check_integrity_raise_already_exist(self):
         node = FileNode('foobar', content='baz')
         self.imc.add(node)
-        self.imc.commit(message=u'Added foobar', author=unicode(self))
+        self.imc.commit(message='Added foobar', author=str(self))
         self.imc.add(node)
         with pytest.raises(NodeAlreadyExistsError):
             self.imc.commit(message='new message',
@@ -145,45 +143,43 @@
     def test_change(self):
         self.imc.add(FileNode('foo/bar/baz', content='foo'))
         self.imc.add(FileNode('foo/fbar', content='foobar'))
-        tip = self.imc.commit(u'Initial', u'joe.doe@example.com')
+        tip = self.imc.commit('Initial', 'joe.doe@example.com')
 
         # Change node's content
         node = FileNode('foo/bar/baz', content='My **changed** content')
         self.imc.change(node)
-        self.imc.commit(u'Changed %s' % node.path, u'joe.doe@example.com')
+        self.imc.commit('Changed %s' % node.path, 'joe.doe@example.com')
 
         newtip = self.repo.get_changeset()
         assert tip != newtip
-        assert tip.id != newtip.id
-        assert newtip.get_node('foo/bar/baz').content == 'My **changed** content'
+        assert tip.raw_id != newtip.raw_id
+        assert newtip.get_node('foo/bar/baz').content == b'My **changed** content'
 
     def test_change_non_ascii(self):
         to_add = [
             FileNode('żółwik/zwierzątko', content='ćććć'),
-            FileNode(u'żółwik/zwierzątko_uni', content=u'ćććć'),
+            FileNode('żółwik/zwierzątko_uni', content='ćććć'),
         ]
         for node in to_add:
             self.imc.add(node)
 
-        tip = self.imc.commit(u'Initial', u'joe.doe@example.com')
+        tip = self.imc.commit('Initial', 'joe.doe@example.com')
 
         # Change node's content
         node = FileNode('żółwik/zwierzątko', content='My **changed** content')
         self.imc.change(node)
-        self.imc.commit(u'Changed %s' % safe_unicode(node.path),
-                        u'joe.doe@example.com')
+        self.imc.commit('Changed %s' % node.path, 'joe.doe@example.com')
 
-        node = FileNode(u'żółwik/zwierzątko_uni', content=u'My **changed** content')
+        node = FileNode('żółwik/zwierzątko_uni', content='My **changed** content')
         self.imc.change(node)
-        self.imc.commit(u'Changed %s' % safe_unicode(node.path),
-                        u'joe.doe@example.com')
+        self.imc.commit('Changed %s' % node.path, 'joe.doe@example.com')
 
         newtip = self.repo.get_changeset()
         assert tip != newtip
-        assert tip.id != newtip.id
+        assert tip.raw_id != newtip.raw_id
 
-        assert newtip.get_node('żółwik/zwierzątko').content == 'My **changed** content'
-        assert newtip.get_node('żółwik/zwierzątko_uni').content == 'My **changed** content'
+        assert newtip.get_node('żółwik/zwierzątko').content == b'My **changed** content'
+        assert newtip.get_node('żółwik/zwierzątko_uni').content == b'My **changed** content'
 
     def test_change_raise_empty_repository(self):
         node = FileNode('foobar')
@@ -193,7 +189,7 @@
     def test_check_integrity_change_raise_node_does_not_exist(self):
         node = FileNode('foobar', content='baz')
         self.imc.add(node)
-        self.imc.commit(message=u'Added foobar', author=unicode(self))
+        self.imc.commit(message='Added foobar', author=str(self))
         node = FileNode('not-foobar', content='')
         self.imc.change(node)
         with pytest.raises(NodeDoesNotExistError):
@@ -202,7 +198,7 @@
     def test_change_raise_node_already_changed(self):
         node = FileNode('foobar', content='baz')
         self.imc.add(node)
-        self.imc.commit(message=u'Added foobar', author=unicode(self))
+        self.imc.commit(message='Added foobar', author=str(self))
         node = FileNode('foobar', content='more baz')
         self.imc.change(node)
         with pytest.raises(NodeAlreadyChangedError):
@@ -215,14 +211,14 @@
         self.imc.change(node)
         with pytest.raises(NodeNotChangedError):
             self.imc.commit(
-                message=u'Trying to mark node as changed without touching it',
-                author=unicode(self)
+                message='Trying to mark node as changed without touching it',
+                author=str(self),
             )
 
     def test_change_raise_node_already_removed(self):
         node = FileNode('foobar', content='baz')
         self.imc.add(node)
-        self.imc.commit(message=u'Added foobar', author=unicode(self))
+        self.imc.commit(message='Added foobar', author=str(self))
         self.imc.remove(FileNode('foobar'))
         with pytest.raises(NodeAlreadyRemovedError):
             self.imc.change(node)
@@ -234,21 +230,21 @@
         node = self.nodes[0]
         assert node.content == tip.get_node(node.path).content
         self.imc.remove(node)
-        self.imc.commit(message=u'Removed %s' % node.path, author=unicode(self))
+        self.imc.commit(message='Removed %s' % node.path, author=str(self))
 
         newtip = self.repo.get_changeset()
         assert tip != newtip
-        assert tip.id != newtip.id
+        assert tip.raw_id != newtip.raw_id
         with pytest.raises(NodeDoesNotExistError):
             newtip.get_node(node.path)
 
     def test_remove_last_file_from_directory(self):
         node = FileNode('omg/qwe/foo/bar', content='foobar')
         self.imc.add(node)
-        self.imc.commit(u'added', u'joe doe')
+        self.imc.commit('added', 'joe doe')
 
         self.imc.remove(node)
-        tip = self.imc.commit(u'removed', u'joe doe')
+        tip = self.imc.commit('removed', 'joe doe')
         with pytest.raises(NodeDoesNotExistError):
             tip.get_node('omg/qwe/foo/bar')
 
@@ -257,7 +253,7 @@
         with pytest.raises(NodeDoesNotExistError):
             self.imc.commit(
                 message='Trying to remove node at empty repository',
-                author=str(self)
+                author=str(self),
             )
 
     def test_check_integrity_remove_raise_node_does_not_exist(self):
@@ -267,8 +263,8 @@
         self.imc.remove(node)
         with pytest.raises(NodeDoesNotExistError):
             self.imc.commit(
-                message=u'Trying to remove not existing node',
-                author=unicode(self)
+                message='Trying to remove not existing node',
+                author=str(self),
             )
 
     def test_remove_raise_node_already_removed(self):
@@ -301,12 +297,12 @@
     def test_multiple_commits(self):
         N = 3  # number of commits to perform
         last = None
-        for x in xrange(N):
+        for x in range(N):
             fname = 'file%s' % str(x).rjust(5, '0')
             content = 'foobar\n' * x
             node = FileNode(fname, content=content)
             self.imc.add(node)
-            commit = self.imc.commit(u"Commit no. %s" % (x + 1), author=u'vcs')
+            commit = self.imc.commit("Commit no. %s" % (x + 1), author='vcs')
             assert last != commit
             last = commit
 
@@ -320,8 +316,8 @@
         node = FileNode('foobar.txt', content='Foobared!')
         self.imc.add(node)
         date = datetime.datetime(1985, 1, 30, 1, 45)
-        commit = self.imc.commit(u"Committed at time when I was born ;-)",
-            author=u'lb <lb@example.com>', date=date)
+        commit = self.imc.commit("Committed at time when I was born ;-)",
+            author='lb <lb@example.com>', date=date)
 
         assert commit.date == date
 
--- a/kallithea/tests/vcs/test_nodes.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/vcs/test_nodes.py	Mon May 04 19:24:04 2020 +0200
@@ -3,6 +3,7 @@
 
 import pytest
 
+from kallithea.lib.vcs.backends.base import EmptyChangeset
 from kallithea.lib.vcs.nodes import DirNode, FileNode, Node, NodeError, NodeKind
 
 
@@ -48,11 +49,6 @@
         with pytest.raises(NodeError):
             Node('', NodeKind.FILE)
 
-    def test_kind_setter(self):
-        node = Node('', NodeKind.DIR)
-        with pytest.raises(NodeError):
-            setattr(node, 'kind', NodeKind.FILE)
-
     def _test_parent_path(self, node_path, expected_parent_path):
         """
         Tests if node's parent path are properly computed.
@@ -103,7 +99,7 @@
         node = DirNode('any_dir')
 
         assert node.is_dir()
-        with pytest.raises(NodeError):
+        with pytest.raises(AttributeError):  # Note: this used to raise NodeError
             getattr(node, 'content')
 
     def test_dir_node_iter(self):
@@ -144,13 +140,13 @@
         assert not mode & stat.S_IXOTH
 
     def test_file_node_is_executable(self):
-        node = FileNode('foobar', 'empty... almost', mode=0100755)
+        node = FileNode('foobar', 'empty... almost', mode=0o100755)
         assert node.is_executable
 
-        node = FileNode('foobar', 'empty... almost', mode=0100500)
+        node = FileNode('foobar', 'empty... almost', mode=0o100500)
         assert node.is_executable
 
-        node = FileNode('foobar', 'empty... almost', mode=0100644)
+        node = FileNode('foobar', 'empty... almost', mode=0o100644)
         assert not node.is_executable
 
     def test_mimetype(self):
@@ -158,10 +154,10 @@
         tar_node = FileNode('test.tar.gz')
 
         my_node2 = FileNode('myfile2')
-        my_node2._content = 'foobar'
+        my_node2._content = b'foobar'
 
         my_node3 = FileNode('myfile3')
-        my_node3._content = '\0foobar'
+        my_node3._content = b'\0foobar'
 
         assert py_node.mimetype == mimetypes.guess_type(py_node.name)[0]
         assert py_node.get_mimetype() == mimetypes.guess_type(py_node.name)
@@ -182,3 +178,8 @@
         data = """\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f??a\x00\x00\x00\x04gAMA\x00\x00\xaf?7\x05\x8a?\x00\x00\x00\x19tEXtSoftware\x00Adobe ImageReadyq?e<\x00\x00\x025IDAT8?\xa5\x93?K\x94Q\x14\x87\x9f\xf7?Q\x1bs4?\x03\x9a\xa8?B\x02\x8b$\x10[U;i\x13?6h?&h[?"\x14j?\xa2M\x7fB\x14F\x9aQ?&\x842?\x0b\x89"\x82??!?\x9c!\x9c2l??{N\x8bW\x9dY\xb4\t/\x1c?=\x9b?}????\xa9*;9!?\x83\x91?[?\\v*?D\x04\'`EpNp\xa2X\'U?pVq"Sw.\x1e?\x08\x01D?jw????\xbc??7{|\x9b?\x89$\x01??W@\x15\x9c\x05q`Lt/\x97?\x94\xa1d?\x18~?\x18?\x18W[%\xb0?\x83??\x14\x88\x8dB?\xa6H\tL\tl\x19>/\x01`\xac\xabx?\x9cl\nx\xb0\x98\x07\x95\x88D$"q[\x19?d\x00(o\n\xa0??\x7f\xb9\xa4?\x1bF\x1f\x8e\xac\xa8?j??eUU}?.?\x9f\x8cE??x\x94??\r\xbdtoJU5"0N\x10U?\x00??V\t\x02\x9f\x81?U?\x00\x9eM\xae2?r\x9b7\x83\x82\x8aP3????.?&"?\xb7ZP \x0c<?O\xa5\t}\xb8?\x99\xa6?\x87?\x1di|/\xa0??0\xbe\x1fp?d&\x1a\xad\x95\x8a\x07?\t*\x10??b:?d?.\x13C\x8a?\x12\xbe\xbf\x8e?{???\x08?\x80\xa7\x13+d\x13>J?\x80\x15T\x95\x9a\x00??S\x8c\r?\xa1\x03\x07?\x96\x9b\xa7\xab=E??\xa4\xb3?\x19q??B\x91=\x8d??k?J\x0bV"??\xf7x?\xa1\x00?\\.\x87\x87???\x02F@D\x99],??\x10#?X\xb7=\xb9\x10?Z\x1by???cI??\x1ag?\x92\xbc?T?t[\x92\x81?<_\x17~\x92\x88?H%?\x10Q\x02\x9f\n\x81qQ\x0bm?\x1bX?\xb1AK\xa6\x9e\xb9?u\xb2?1\xbe|/\x92M@\xa2!F?\xa9>"\r<DT?>\x92\x8e?>\x9a9Qv\x127?a\xac?Y?8?:??]X???9\x80\xb7?u?\x0b#BZ\x8d=\x1d?p\x00\x00\x00\x00IEND\xaeB`\x82"""
         filenode = FileNode('calendar.png', content=data)
         assert filenode.is_binary
+
+    def test_if_binary_empty(self):
+        empty_cs = EmptyChangeset()
+        filenode = FileNode('foo', changeset=empty_cs)
+        assert not filenode.is_binary
--- a/kallithea/tests/vcs/test_repository.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/vcs/test_repository.py	Mon May 04 19:24:04 2020 +0200
@@ -85,7 +85,7 @@
                 'removed': [FileNode('foobar')],
             },
             {
-                'message': u'Commit that contains glob pattern in filename',
+                'message': 'Commit that contains glob pattern in filename',
                 'author': 'Jane Doe <jane.doe@example.com>',
                 'date': datetime.datetime(2010, 1, 1, 22),
                 'added': [
@@ -110,7 +110,7 @@
 
     def test_initial_commit_diff(self):
         initial_rev = self.repo.revisions[0]
-        assert self.repo.get_diff(self.repo.EMPTY_CHANGESET, initial_rev) == '''diff --git a/foobar b/foobar
+        assert self.repo.get_diff(self.repo.EMPTY_CHANGESET, initial_rev) == br'''diff --git a/foobar b/foobar
 new file mode 100644
 index 0000000000000000000000000000000000000000..f6ea0495187600e7b2288c8ac19c5886383a4632
 --- /dev/null
@@ -130,7 +130,7 @@
 
     def test_second_changeset_diff(self):
         revs = self.repo.revisions
-        assert self.repo.get_diff(revs[0], revs[1]) == '''diff --git a/foobar b/foobar
+        assert self.repo.get_diff(revs[0], revs[1]) == br'''diff --git a/foobar b/foobar
 index f6ea0495187600e7b2288c8ac19c5886383a4632..389865bb681b358c9b102d79abd8d5f941e96551 100644
 --- a/foobar
 +++ b/foobar
@@ -151,7 +151,7 @@
 
     def test_third_changeset_diff(self):
         revs = self.repo.revisions
-        assert self.repo.get_diff(revs[1], revs[2]) == '''diff --git a/foobar b/foobar
+        assert self.repo.get_diff(revs[1], revs[2]) == br'''diff --git a/foobar b/foobar
 deleted file mode 100644
 index 389865bb681b358c9b102d79abd8d5f941e96551..0000000000000000000000000000000000000000
 --- a/foobar
@@ -173,7 +173,7 @@
 
     def test_fourth_changeset_diff(self):
         revs = self.repo.revisions
-        assert self.repo.get_diff(revs[2], revs[3]) == '''diff --git a/README{ b/README{
+        assert self.repo.get_diff(revs[2], revs[3]) == br'''diff --git a/README{ b/README{
 new file mode 100644
 index 0000000000000000000000000000000000000000..cdc0c1b5d234feedb37bbac19cd1b6442061102d
 --- /dev/null
@@ -189,7 +189,7 @@
 
     def test_initial_commit_diff(self):
         initial_rev = self.repo.revisions[0]
-        assert self.repo.get_diff(self.repo.EMPTY_CHANGESET, initial_rev) == '''diff --git a/foobar b/foobar
+        assert self.repo.get_diff(self.repo.EMPTY_CHANGESET, initial_rev) == br'''diff --git a/foobar b/foobar
 new file mode 100644
 --- /dev/null
 +++ b/foobar
@@ -207,7 +207,7 @@
 
     def test_second_changeset_diff(self):
         revs = self.repo.revisions
-        assert self.repo.get_diff(revs[0], revs[1]) == '''diff --git a/foobar b/foobar
+        assert self.repo.get_diff(revs[0], revs[1]) == br'''diff --git a/foobar b/foobar
 --- a/foobar
 +++ b/foobar
 @@ -1,1 +1,1 @@
@@ -226,7 +226,7 @@
 
     def test_third_changeset_diff(self):
         revs = self.repo.revisions
-        assert self.repo.get_diff(revs[1], revs[2]) == '''diff --git a/foobar b/foobar
+        assert self.repo.get_diff(revs[1], revs[2]) == br'''diff --git a/foobar b/foobar
 deleted file mode 100644
 --- a/foobar
 +++ /dev/null
@@ -246,7 +246,7 @@
 
     def test_fourth_changeset_diff(self):
         revs = self.repo.revisions
-        assert self.repo.get_diff(revs[2], revs[3]) == '''diff --git a/README{ b/README{
+        assert self.repo.get_diff(revs[2], revs[3]) == br'''diff --git a/README{ b/README{
 new file mode 100644
 --- /dev/null
 +++ b/README{
--- a/kallithea/tests/vcs/test_utils.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/vcs/test_utils.py	Mon May 04 19:24:04 2020 +0200
@@ -191,8 +191,8 @@
                    ('justname', '')),
                   ('Mr Double Name withemail@example.com ',
                    ('Mr Double Name', 'withemail@example.com')),
-                  (u'John Doe <джондо à éẋàṁṗłê.ç°ḿ>',
-                   (u'John Doe <\u0434\u0436\u043e\u043d\u0434\u043e \xe0 \xe9\u1e8b\xe0\u1e41\u1e57\u0142\xea.\xe7\xb0\u1e3f>', '')),
+                  ('John Doe <джондо à éẋàṁṗłê.ç°ḿ>',
+                   ('John Doe <\u0434\u0436\u043e\u043d\u0434\u043e \xe0 \xe9\u1e8b\xe0\u1e41\u1e57\u0142\xea.\xe7\xb0\u1e3f>', '')),
                   ]
 
     def test_author_email(self):
--- a/kallithea/tests/vcs/test_vcs.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/vcs/test_vcs.py	Mon May 04 19:24:04 2020 +0200
@@ -3,7 +3,6 @@
 
 import pytest
 
-from kallithea.lib.utils2 import safe_str
 from kallithea.lib.vcs import VCSError, get_backend, get_repo
 from kallithea.lib.vcs.backends.hg import MercurialRepository
 from kallithea.tests.vcs.conf import TEST_GIT_REPO, TEST_HG_REPO, TESTS_TMP_PATH
@@ -22,14 +21,14 @@
         alias = 'hg'
         path = TEST_HG_REPO
         backend = get_backend(alias)
-        repo = backend(safe_str(path))
+        repo = backend(path)
         assert 'hg' == repo.alias
 
     def test_alias_detect_git(self):
         alias = 'git'
         path = TEST_GIT_REPO
         backend = get_backend(alias)
-        repo = backend(safe_str(path))
+        repo = backend(path)
         assert 'git' == repo.alias
 
     def test_wrong_alias(self):
@@ -41,28 +40,28 @@
         alias = 'hg'
         path = TEST_HG_REPO
         backend = get_backend(alias)
-        repo = backend(safe_str(path))
+        repo = backend(path)
 
-        assert repo.__class__ == get_repo(safe_str(path), alias).__class__
-        assert repo.path == get_repo(safe_str(path), alias).path
+        assert repo.__class__ == get_repo(path, alias).__class__
+        assert repo.path == get_repo(path, alias).path
 
     def test_get_repo_autoalias_hg(self):
         alias = 'hg'
         path = TEST_HG_REPO
         backend = get_backend(alias)
-        repo = backend(safe_str(path))
+        repo = backend(path)
 
-        assert repo.__class__ == get_repo(safe_str(path)).__class__
-        assert repo.path == get_repo(safe_str(path)).path
+        assert repo.__class__ == get_repo(path).__class__
+        assert repo.path == get_repo(path).path
 
     def test_get_repo_autoalias_git(self):
         alias = 'git'
         path = TEST_GIT_REPO
         backend = get_backend(alias)
-        repo = backend(safe_str(path))
+        repo = backend(path)
 
-        assert repo.__class__ == get_repo(safe_str(path)).__class__
-        assert repo.path == get_repo(safe_str(path)).path
+        assert repo.__class__ == get_repo(path).__class__
+        assert repo.path == get_repo(path).path
 
     def test_get_repo_err(self):
         blank_repo_path = os.path.join(TESTS_TMP_PATH, 'blank-error-repo')
--- a/kallithea/tests/vcs/test_workdirs.py	Mon May 04 18:25:09 2020 +0200
+++ b/kallithea/tests/vcs/test_workdirs.py	Mon May 04 19:24:04 2020 +0200
@@ -12,8 +12,8 @@
     def _get_commits(cls):
         commits = [
             {
-                'message': u'Initial commit',
-                'author': u'Joe Doe <joe.doe@example.com>',
+                'message': 'Initial commit',
+                'author': 'Joe Doe <joe.doe@example.com>',
                 'date': datetime.datetime(2010, 1, 1, 20),
                 'added': [
                     FileNode('foobar', content='Foobar'),
@@ -22,8 +22,8 @@
                 ],
             },
             {
-                'message': u'Changes...',
-                'author': u'Jane Doe <jane.doe@example.com>',
+                'message': 'Changes...',
+                'author': 'Jane Doe <jane.doe@example.com>',
                 'date': datetime.datetime(2010, 1, 1, 21),
                 'added': [
                     FileNode('some/new.txt', content='news...'),
@@ -43,8 +43,8 @@
         self.imc.add(FileNode('docs/index.txt',
             content='Documentation\n'))
         self.imc.commit(
-            message=u'New branch: foobar',
-            author=u'joe',
+            message='New branch: foobar',
+            author='joe',
             branch='foobar',
         )
         assert self.repo.workdir.get_branch() == self.default_branch
@@ -54,8 +54,8 @@
         self.imc.add(FileNode('docs/index.txt',
             content='Documentation\n'))
         head = self.imc.commit(
-            message=u'New branch: foobar',
-            author=u'joe',
+            message='New branch: foobar',
+            author='joe',
             branch='foobar',
         )
         assert self.repo.workdir.get_branch() == self.default_branch
@@ -73,7 +73,7 @@
             self.repo.workdir.checkout_branch(branch='foobranch')
         # create new branch 'foobranch'.
         self.imc.add(FileNode('file1', content='blah'))
-        self.imc.commit(message=u'asd', author=u'john', branch='foobranch')
+        self.imc.commit(message='asd', author='john', branch='foobranch')
         # go back to the default branch
         self.repo.workdir.checkout_branch()
         assert self.repo.workdir.get_branch() == self.backend_class.DEFAULT_BRANCH_NAME
--- a/scripts/docs-headings.py	Mon May 04 18:25:09 2020 +0200
+++ b/scripts/docs-headings.py	Mon May 04 19:24:04 2020 +0200
@@ -1,11 +1,9 @@
-#!/usr/bin/env python2
+#!/usr/bin/env python3
 
 """
 Consistent formatting of rst section titles
 """
 
-from __future__ import print_function
-
 import re
 import subprocess
 
@@ -35,6 +33,7 @@
 def main():
     filenames = subprocess.check_output(['hg', 'loc', 'set:**.rst+kallithea/i18n/how_to']).splitlines()
     for fn in filenames:
+        fn = fn.decode()
         print('processing %s' % fn)
         s = open(fn).read()
 
--- a/scripts/generate-ini.py	Mon May 04 18:25:09 2020 +0200
+++ b/scripts/generate-ini.py	Mon May 04 19:24:04 2020 +0200
@@ -1,10 +1,8 @@
-#!/usr/bin/env python2
+#!/usr/bin/env python3
 """
 Based on kallithea/lib/paster_commands/template.ini.mako, generate development.ini
 """
 
-from __future__ import print_function
-
 import re
 
 from kallithea.lib import inifile
@@ -62,6 +60,13 @@
         print('writing:', makofile)
         open(makofile, 'w').write(mako_marked_up)
 
+    lines = re.findall(r'\n(# [^ ].*)', mako_marked_up)
+    if lines:
+        print('ERROR: the template .ini file convention is to use "## Foo Bar" for text comments and "#foo = bar" for disabled settings')
+        for line in lines:
+            print(line)
+        raise SystemExit(1)
+
     # create ini files
     for fn, settings in ini_files:
         print('updating:', fn)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scripts/i18n	Mon May 04 19:24:04 2020 +0200
@@ -0,0 +1,140 @@
+#!/usr/bin/env python3
+
+# -*- 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/>.
+
+import os
+import shutil
+import sys
+
+import click
+
+import i18n_utils
+
+
+"""
+Tool for maintenance of .po and .pot files
+
+Normally, the i18n-related files contain for each translatable string a
+reference to all the source code locations where this string is found. This
+meta data is useful for translators to assess how strings are used, but is not
+relevant for normal development nor for running Kallithea. Such meta data, or
+derived data like kallithea.pot, will inherently be outdated, and create
+unnecessary churn and repository growth, making it harder to spot actual and
+important changes.
+"""
+
+@click.group()
+@click.option('--debug/--no-debug', default=False)
+def cli(debug):
+    if (debug):
+        i18n_utils.do_debug = True
+    pass
+
+@cli.command()
+@click.argument('po_files', nargs=-1)
+@click.option('--merge-pot-file', default=None)
+@click.option('--strip/--no-strip', default=False)
+def normalize_po_files(po_files, merge_pot_file, strip):
+    """Normalize the specified .po and .pot files.
+
+    By default, only actual translations and essential headers will be
+    preserved, just as we want it in the main branches with minimal noise.
+
+    If a .pot file is specified, the po files will instead be updated by
+    running GNU msgmerge with this .pot file, thus updating source code
+    references and preserving comments and outdated translations.
+    """
+    for po_file in po_files:
+        i18n_utils._normalize_po_file(po_file, merge_pot_file=merge_pot_file, strip=strip)
+
+@cli.command()
+@click.argument('local')
+@click.argument('base')
+@click.argument('other')
+@click.argument('output')
+@click.option('--merge-pot-file', default=None)
+@click.option('--strip/--no-strip', default=False)
+def normalized_merge(local, base, other, output, merge_pot_file, strip):
+    """Merge tool for use with 'hg merge/rebase/graft --tool'
+
+    i18n files are partially manually editored original source of content, and
+    partially automatically generated and updated. That create a lot of churn
+    and often cause a lot of merge conflicts.
+
+    To avoid that, this merge tool wrapper will normalize .po content before
+    running the merge tool.
+
+    By default, only actual translations and essential headers will be
+    preserved, just as we want it in the main branches with minimal noise.
+
+    If a .pot file is specified, the po files will instead be updated by
+    running GNU msgmerge with this .pot file, thus updating source code
+    references and preserving comments and outdated translations.
+
+    Add the following to your user or repository-specific .hgrc file to use it:
+        [merge-tools]
+        i18n.executable = /path/to/scripts/i18n
+        i18n.args = normalized-merge $local $base $other $output
+
+    and then invoke merge/rebase/graft with the additional argument '--tool i18n'.
+    """
+    from mercurial import (
+        context,
+        simplemerge,
+        ui as uimod,
+    )
+
+    print('i18n normalized-merge: normalizing and merging %s' % output)
+
+    i18n_utils._normalize_po_file(local, merge_pot_file=merge_pot_file, strip=strip)
+    i18n_utils._normalize_po_file(base, merge_pot_file=merge_pot_file, strip=strip)
+    i18n_utils._normalize_po_file(other, merge_pot_file=merge_pot_file, strip=strip)
+    i18n_utils._normalize_po_file(output, merge_pot_file=merge_pot_file, strip=strip)
+
+    # simplemerge will write markers to 'local' if it fails, keep a copy without markers
+    localkeep = local + '.keep'
+    shutil.copyfile(local, localkeep)
+
+    ret = simplemerge.simplemerge(uimod.ui.load(),
+         context.arbitraryfilectx(local.encode('utf-8')),
+         context.arbitraryfilectx(base.encode('utf-8')),
+         context.arbitraryfilectx(other.encode('utf-8')),
+         label=[b'local', b'other', b'base'],
+         mode='merge',
+    )
+    shutil.copyfile(local, output)  # simplemerge wrote to local - either resolved or with conflict markers
+    if ret:
+        shutil.copyfile(localkeep, local)
+        basekeep = base + '.keep'
+        otherkeep = other + '.keep'
+        shutil.copyfile(base, basekeep)
+        shutil.copyfile(other, otherkeep)
+        sys.stderr.write("Error: simple merge failed and %s is left with conflict markers. Resolve the conflicts, then use 'hg resolve -m'.\n" % output)
+        sys.stderr.write('Resolve with e.g.: kdiff3 %s %s %s -o %s\n' % (basekeep, localkeep, otherkeep, output))
+        sys.exit(ret)
+
+    os.remove(localkeep)
+
+@cli.command()
+@click.argument('file1')
+@click.argument('file2')
+@click.option('--merge-pot-file', default=None)
+@click.option('--strip/--no-strip', default=False)
+def normalized_diff(file1, file2, merge_pot_file, strip):
+    """Compare two files while transparently normalizing them."""
+    sys.exit(i18n_utils._normalized_diff(file1, file2, merge_pot_file=merge_pot_file, strip=strip))
+
+if __name__ == '__main__':
+    cli()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scripts/i18n_utils.py	Mon May 04 19:24:04 2020 +0200
@@ -0,0 +1,197 @@
+# 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/>.
+
+from __future__ import print_function
+
+import os
+import re
+import shutil
+import subprocess
+import tempfile
+
+
+do_debug = False  # set from scripts/i18n --debug
+
+def debug(*args, **kwargs):
+    if do_debug:
+        print(*args, **kwargs)
+
+def runcmd(cmd, *args, **kwargs):
+    debug('... Executing command: %s' % ' '.join(cmd))
+    subprocess.check_call(cmd, *args, **kwargs)
+
+header_comment_strip_re = re.compile(r'''
+    ^
+    [#][ ]Translations[ ]template[ ]for[ ]Kallithea[.] \n
+    |
+    ^
+    [#][ ]FIRST[ ]AUTHOR[ ]<EMAIL@ADDRESS>,[ ]\d+[.] \n
+    (?:[#] \n)?
+    |
+    ^
+    (?:[#] \n)?
+    [#],[ ]fuzzy \n
+    |
+    ^
+    [#][ ][#],[ ]fuzzy \n
+    ''',
+    re.MULTILINE|re.VERBOSE)
+
+header_normalize_re = re.compile(r'''
+    ^ "
+    (POT-Creation-Date|PO-Revision-Date|Last-Translator|Language-Team|X-Generator|Generated-By|Project-Id-Version):
+    [ ][^\\]*\\n
+    " \n
+    ''',
+    re.MULTILINE|re.IGNORECASE|re.VERBOSE)
+
+def _normalize_po(raw_content):
+    r"""
+    >>> print(_normalize_po(r'''
+    ... # header comment
+    ...
+    ...
+    ... # comment before header
+    ... msgid ""
+    ... msgstr "yada"
+    ... "POT-Creation-Date: 2019-05-04 21:13+0200\n"
+    ... "MIME-Version: "
+    ... "1.0\n"
+    ... "Last-Translator: Jabba"
+    ... "the Hutt\n"
+    ... "X-Generator: Weblate 1.2.3\n"
+    ...
+    ... # comment, but not in header
+    ... msgid "None"
+    ... msgstr "Ingen"
+    ...
+    ...
+    ... line 2
+    ... # third comment
+    ...
+    ... msgid "Special"
+    ... msgstr ""
+    ...
+    ... msgid "Specialist"
+    ... # odd comment
+    ... msgstr ""
+    ... "Expert"
+    ...
+    ... # crazy fuzzy auto translation by msgmerge, using foo for bar
+    ... #, fuzzy
+    ... #| msgid "some foo string"
+    ... msgid "some bar string."
+    ... msgstr "translation of foo string"
+    ...
+    ... msgid "%d minute"
+    ... msgid_plural "%d minutes"
+    ... msgstr[0] "minut"
+    ... msgstr[1] "minutter"
+    ... msgstr[2] ""
+    ...
+    ... msgid "%d year"
+    ... msgid_plural "%d years"
+    ... msgstr[0] ""
+    ... msgstr[1] ""
+    ...
+    ... # last comment
+    ... ''') + '^^^')
+    # header comment
+    <BLANKLINE>
+    <BLANKLINE>
+    # comment before header
+    <BLANKLINE>
+    msgid ""
+    msgstr "yada"
+    "MIME-Version: "
+    "1.0\n"
+    <BLANKLINE>
+    msgid "None"
+    msgstr "Ingen"
+    <BLANKLINE>
+    line 2
+    <BLANKLINE>
+    msgid "Specialist"
+    msgstr ""
+    "Expert"
+    <BLANKLINE>
+    msgid "%d minute"
+    msgid_plural "%d minutes"
+    msgstr[0] "minut"
+    msgstr[1] "minutter"
+    msgstr[2] ""
+    ^^^
+    """
+    header_start = raw_content.find('\nmsgid ""\n') + 1
+    header_end = raw_content.find('\n\n', header_start) + 1 or len(raw_content)
+    chunks = [
+        header_comment_strip_re.sub('', raw_content[0:header_start])
+            .strip(),
+        '',
+        header_normalize_re.sub('', raw_content[header_start:header_end])
+            .replace(
+                r'"Content-Type: text/plain; charset=utf-8\n"',
+                r'"Content-Type: text/plain; charset=UTF-8\n"')  # maintain msgmerge casing
+            .strip(),
+        '']  # preserve normalized header
+    # all chunks are separated by empty line
+    for raw_chunk in raw_content[header_end:].split('\n\n'):
+        if '\n#, fuzzy' in raw_chunk:  # might be like "#, fuzzy, python-format"
+            continue  # drop crazy auto translation that is worse than useless
+        # strip all comment lines from chunk
+        chunk_lines = [
+            line
+            for line in raw_chunk.splitlines()
+            if line
+            and not line.startswith('#')
+        ]
+        if not chunk_lines:
+            continue
+        # check lines starting from first msgstr, skip chunk if no translation lines
+        msgstr_i = [i for i, line in enumerate(chunk_lines) if line.startswith('msgstr')]
+        if (
+            chunk_lines[0].startswith('msgid') and
+            msgstr_i and
+            all(line.endswith(' ""') for line in chunk_lines[msgstr_i[0]:])
+        ):  # skip translation chunks that doesn't have any actual translations
+            continue
+        chunks.append('\n'.join(chunk_lines) + '\n')
+    return '\n'.join(chunks)
+
+def _normalize_po_file(po_file, merge_pot_file=None, strip=False):
+    if merge_pot_file:
+        runcmd(['msgmerge', '--width=76', '--backup=none', '--previous',
+                '--update', po_file, '-q', merge_pot_file])
+    if strip:
+        po_tmp = po_file + '.tmp'
+        with open(po_file, 'r') as src, open(po_tmp, 'w') as dest:
+            raw_content = src.read()
+            normalized_content = _normalize_po(raw_content)
+            dest.write(normalized_content)
+        os.rename(po_tmp, po_file)
+
+def _normalized_diff(file1, file2, merge_pot_file=None, strip=False):
+    # Create temporary copies of both files
+    temp1 = tempfile.NamedTemporaryFile(prefix=os.path.basename(file1))
+    temp2 = tempfile.NamedTemporaryFile(prefix=os.path.basename(file2))
+    debug('normalized_diff: %s -> %s / %s -> %s' % (file1, temp1.name, file2, temp2.name))
+    shutil.copyfile(file1, temp1.name)
+    shutil.copyfile(file2, temp2.name)
+    # Normalize them in place
+    _normalize_po_file(temp1.name, merge_pot_file=merge_pot_file, strip=strip)
+    _normalize_po_file(temp2.name, merge_pot_file=merge_pot_file, strip=strip)
+    # Now compare
+    try:
+        runcmd(['diff', '-u', temp1.name, temp2.name])
+    except subprocess.CalledProcessError as e:
+        return e.returncode
--- a/scripts/logformat.py	Mon May 04 18:25:09 2020 +0200
+++ b/scripts/logformat.py	Mon May 04 19:24:04 2020 +0200
@@ -1,6 +1,4 @@
-#!/usr/bin/env python2
-
-from __future__ import print_function
+#!/usr/bin/env python3
 
 import re
 import sys
--- a/scripts/make-release	Mon May 04 18:25:09 2020 +0200
+++ b/scripts/make-release	Mon May 04 19:24:04 2020 +0200
@@ -15,7 +15,7 @@
 trap cleanup EXIT
 
 echo "Setting up a fresh virtualenv in $venv"
-virtualenv -p python2 "$venv"
+python3 -m venv "$venv"
 . "$venv/bin/activate"
 
 echo "Install/verify tools needed for building and uploading stuff"
@@ -35,8 +35,8 @@
 sed -e 's/[^ ]*[ ]*\([^ ]*\).*/\1/g' MANIFEST.in | xargs ls -lad
 
 echo "Build dist"
-python2 setup.py compile_catalog
-python2 setup.py sdist
+python3 setup.py compile_catalog
+python3 setup.py sdist
 
 echo "Verify VERSION from kallithea/__init__.py"
 namerel=$(cd dist && echo Kallithea-*.tar.gz)
@@ -46,10 +46,10 @@
 echo "Releasing Kallithea $version in directory $namerel"
 
 echo "Verify dist file content"
-diff -u <((hg mani | grep -v '^\.hg') | LANG=C sort) <(tar tf dist/Kallithea-$version.tar.gz | sed "s|^$namerel/||" | grep . | grep -v '^kallithea/i18n/.*/LC_MESSAGES/kallithea.mo$\|^Kallithea.egg-info/\|^PKG-INFO$\|/$' | LANG=C sort)
+diff -u <((hg mani | grep -v '^\.hg\|^kallithea/i18n/en/LC_MESSAGES/kallithea.mo$') | LANG=C sort) <(tar tf dist/Kallithea-$version.tar.gz | sed "s|^$namerel/||" | grep . | grep -v '^kallithea/i18n/.*/LC_MESSAGES/kallithea.mo$\|^Kallithea.egg-info/\|^PKG-INFO$\|/$' | LANG=C sort)
 
 echo "Verify docs build"
-python2 setup.py build_sphinx # the results are not actually used, but we want to make sure it builds
+python3 setup.py build_sphinx # the results are not actually used, but we want to make sure it builds
 
 echo "Shortlog for inclusion in the release announcement"
 scripts/shortlog.py "only('.', branch('stable') & tagged() & public() & not '.')"
@@ -74,7 +74,7 @@
 echo "Rebuild readthedocs for docs.kallithea-scm.org"
 xdg-open https://readthedocs.org/projects/kallithea/
 curl -X POST http://readthedocs.org/build/kallithea
-xdg-open https://readthedocs.org/builds/kallithea/
+xdg-open https://readthedocs.org/projects/kallithea/builds
 xdg-open http://docs.kallithea-scm.org/en/latest/ # or whatever the branch is
 
 twine upload dist/*
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scripts/pyflakes	Mon May 04 19:24:04 2020 +0200
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+"""
+pyflakes with filter configuration for Kallithea.
+Inspired by pyflakes/api.py and flake8/plugins/pyflakes.py .
+"""
+
+import sys
+
+import pyflakes.api
+import pyflakes.messages
+
+
+class Reporter:
+
+    warned = False
+
+    def flake(self, warning):
+        # ignore known warnings
+        if isinstance(warning, pyflakes.messages.UnusedVariable):
+            return
+        if warning.filename == 'kallithea/bin/kallithea_cli_ishell.py':
+            if isinstance(warning, pyflakes.messages.ImportStarUsed) and warning.message_args == ('kallithea.model.db',):
+                return
+            if isinstance(warning, pyflakes.messages.UnusedImport) and warning.message_args == ('kallithea.model.db.*',):
+                return
+
+        print('%s:%s %s   [%s %s]' % (warning.filename, warning.lineno, warning.message % warning.message_args, type(warning).__name__, warning.message_args))
+        self.warned = True
+
+    def unexpectedError(self, filename, msg):
+        print('Unexpected error for %s: %s' % (filename, msg))
+
+
+reporter = Reporter()
+
+for filename in sorted(set(sys.argv[1:])):
+    pyflakes.api.checkPath(filename, reporter=reporter)
+if reporter.warned:
+    raise SystemExit(1)
--- a/scripts/run-all-cleanup	Mon May 04 18:25:09 2020 +0200
+++ b/scripts/run-all-cleanup	Mon May 04 19:24:04 2020 +0200
@@ -8,3 +8,6 @@
 scripts/docs-headings.py
 scripts/generate-ini.py
 scripts/whitespacecleanup.sh
+
+hg loc 'set:!binary()&grep("^#!.*python")' '*.py' | xargs scripts/pyflakes
+echo "no blocking problems found by $0"
--- a/scripts/shortlog.py	Mon May 04 18:25:09 2020 +0200
+++ b/scripts/shortlog.py	Mon May 04 19:24:04 2020 +0200
@@ -1,4 +1,4 @@
-#!/usr/bin/env python2
+#!/usr/bin/env python3
 # -*- coding: utf-8 -*-
 
 """
--- a/scripts/update-copyrights.py	Mon May 04 18:25:09 2020 +0200
+++ b/scripts/update-copyrights.py	Mon May 04 19:24:04 2020 +0200
@@ -1,4 +1,4 @@
-#!/usr/bin/env python2
+#!/usr/bin/env python3
 # -*- coding: utf-8 -*-
 
 """
@@ -51,11 +51,14 @@
     * first contribution
     * number of contribution years
     * name (with some unicode normalization)
-    The entries must be 2-tuples of a list of string years and the unicode name"""
-    return (x[0] and -int(x[0][-1]),
-            x[0] and int(x[0][0]),
-            -len(x[0]),
-            x[1].decode('utf-8').lower().replace(u'\xe9', u'e').replace(u'\u0142', u'l')
+    The entries must be 2-tuples of a list of string years and the name"""
+    years, name = x
+    if not years:
+        years = ['0']
+    return (-int(years[-1]),  # primarily sort by latest contribution
+            int(years[0]),  # then sort by first contribution
+            -len(years),  # then sort by length of contribution (no gaps)
+            name.lower().replace('\xe9', 'e').replace('\u0142', 'l')  # finally sort by name
         )
 
 
@@ -100,6 +103,9 @@
     for year, name in all_entries:
         if name in no_entries or (name, year) in no_entries:
             continue
+        parts = name.split(' <', 1)
+        if len(parts) == 2:
+            name = parts[0] + ' <' + parts[1].lower()
         domain = name.split('@', 1)[-1].rstrip('>')
         if domain in domain_extra:
             name_years[domain_extra[domain]].add(year)
@@ -131,7 +137,7 @@
         all_entries=repo_entries + contributor_data.other_about + contributor_data.other,
         no_entries=contributor_data.no_about,
         domain_extra=contributor_data.domain_extra,
-        split_re=r'(?:  <li>Copyright &copy; [^\n]*</li>\n)*',
+        split_re=r'(?:  <li>Copyright &copy; [^\n]+</li>\n)+',
         normalize_name=lambda name: name.split('<', 1)[0].strip(),
         format_f=lambda years, name: '  <li>Copyright &copy; %s, %s</li>\n' % (nice_years(years, '&ndash;', ', '), name),
         )
@@ -141,7 +147,7 @@
         all_entries=repo_entries + contributor_data.other_contributors + contributor_data.other,
         no_entries=contributor_data.total_ignore,
         domain_extra=contributor_data.domain_extra,
-        split_re=r'(?:    [^\n]*\n)*',
+        split_re=r'(?:    [^\n]+\n)+',
         normalize_name=lambda name: name,
         format_f=lambda years, name: ('    %s%s%s\n' % (name, ' ' if years else '', nice_years(years))),
         )
@@ -151,7 +157,7 @@
         all_entries=repo_entries,
         no_entries=contributor_data.total_ignore,
         domain_extra={},
-        split_re=r'(?<=&copy;) .* (?=by various authors)',
+        split_re=r'(?<=&copy;) .+ (?=by various authors)',
         normalize_name=lambda name: '',
         format_f=lambda years, name: ' ' + nice_years(years, '&ndash;', ', ') + ' ',
         )
@@ -162,7 +168,7 @@
         all_entries=repo_entries,
         no_entries=contributor_data.total_ignore,
         domain_extra={},
-        split_re=r"(?<=copyright = u').*(?= by various authors)",
+        split_re=r"(?<=copyright = ').+(?= by various authors)",
         normalize_name=lambda name: '',
         format_f=lambda years, name: nice_years(years, '-', ', '),
         )
--- a/scripts/validate-commits	Mon May 04 18:25:09 2020 +0200
+++ b/scripts/validate-commits	Mon May 04 19:24:04 2020 +0200
@@ -34,20 +34,24 @@
     hg update "$rev"
 
     cleanup
-    virtualenv -p "$(command -v python2)" "$venv"
+    python3 -m venv "$venv"
     source "$venv/bin/activate"
     pip install --upgrade pip setuptools
     pip install -e . -r dev_requirements.txt python-ldap python-pam
 
     # run-all-cleanup
-    scripts/run-all-cleanup
-    if ! hg update --check -q .; then
-        echo "run-all-cleanup did not give clean results!"
+    if ! scripts/run-all-cleanup ; then
+        echo "run-all-cleanup encountered errors!"
         result="NOK"
-        hg diff
-        hg revert -a
     else
-        result=" OK"
+        if ! hg update --check -q .; then
+            echo "run-all-cleanup did not give clean results!"
+            result="NOK"
+            hg diff
+            hg revert -a
+        else
+            result=" OK"
+        fi
     fi
     echo "$result: $rev (run-all-cleanup)" >> "$resultfile"
 
--- a/scripts/validate-minimum-dependency-versions	Mon May 04 18:25:09 2020 +0200
+++ b/scripts/validate-minimum-dependency-versions	Mon May 04 19:24:04 2020 +0200
@@ -28,14 +28,11 @@
 sed -n 's/.*"\(.*\)>=\(.*\)".*/\1==\2/p' setup.py > "$min_requirements"
 sed 's/>=/==/p' dev_requirements.txt >> "$min_requirements"
 
-virtualenv -p "$(command -v python2)" "$venv"
+python3 -m venv "$venv"
 source "$venv/bin/activate"
 pip install --upgrade pip setuptools
 pip install -e . -r "$min_requirements" python-ldap python-pam 2> >(tee "$log" >&2)
 
-# Strip out the known Python 2.7 deprecation message.
-sed -i '/DEPRECATION: Python 2\.7 will reach the end of its life/d' "$log"
-
 # Treat any message on stderr as a problem, for the caller to interpret.
 if [ -s "$log" ]; then
     echo
--- a/scripts/whitespacecleanup.sh	Mon May 04 18:25:09 2020 +0200
+++ b/scripts/whitespacecleanup.sh	Mon May 04 19:24:04 2020 +0200
@@ -1,4 +1,4 @@
-#!/bin/bash -x
+#!/bin/bash -xe
 
 # Enforce some consistency in whitespace - just to avoid spurious whitespaces changes
 
@@ -18,6 +18,7 @@
 hg loc 'set:!binary()&grep("^#!")&!(**_tmpl.py)&!(**/template**)' | xargs chmod +x
 
 # isort is installed from dev_requirements.txt
-isort --line-width 160 --wrap-length 160 --lines-after-imports 2 `hg loc '*.py'`
+hg loc 'set:!binary()&grep("^#!.*python")' '*.py' | xargs isort --line-width 160 --lines-after-imports 2
 
+echo "diff after $0:"
 hg diff
--- a/setup.py	Mon May 04 18:25:09 2020 +0200
+++ b/setup.py	Mon May 04 19:24:04 2020 +0200
@@ -1,4 +1,4 @@
-#!/usr/bin/env python2
+#!/usr/bin/env python3
 # -*- coding: utf-8 -*-
 import os
 import platform
@@ -9,8 +9,8 @@
 from setuptools.command import sdist
 
 
-if sys.version_info < (2, 6) or sys.version_info >= (3,):
-    raise Exception('Kallithea requires python 2.7')
+if sys.version_info < (3, 6):
+    raise Exception('Kallithea requires Python 3.6 or later')
 
 
 here = os.path.abspath(os.path.dirname(__file__))
@@ -20,16 +20,17 @@
     import re
     matches = re.compile(r'(?:%s)\s*=\s*(.*)' % name).search(data)
     if matches:
-        if not callable(callback_handler):
-            callback_handler = lambda v: v
+        s = eval(matches.groups()[0])
+        if callable(callback_handler):
+            return callback_handler(s)
+        return s
 
-        return callback_handler(eval(matches.groups()[0]))
-
-_meta = open(os.path.join(here, 'kallithea', '__init__.py'), 'rb')
+_meta = open(os.path.join(here, 'kallithea', '__init__.py'), 'r')
 _metadata = _meta.read()
 _meta.close()
 
-callback = lambda V: ('.'.join(map(str, V[:3])) + '.'.join(V[3:]))
+def callback(V):
+    return '.'.join(map(str, V[:3])) + '.'.join(V[3:])
 __version__ = _get_meta_var('VERSION', _metadata, callback)
 __license__ = _get_meta_var('__license__', _metadata)
 __author__ = _get_meta_var('__author__', _metadata)
@@ -40,40 +41,40 @@
 is_windows = __platform__ in ['Windows']
 
 requirements = [
-    "alembic >= 0.8.0, < 1.1",
+    "alembic >= 1.0.10, < 1.5",
     "gearbox >= 0.1.0, < 1",
-    "waitress >= 0.8.8, < 1.4",
-    "WebOb >= 1.7, < 1.9",
+    "waitress >= 0.8.8, < 1.5",
+    "WebOb >= 1.8, < 1.9",
     "backlash >= 0.1.2, < 1",
-    "TurboGears2 >= 2.3.10, < 2.5",
+    "TurboGears2 >= 2.4, < 2.5",
     "tgext.routes >= 0.2.0, < 1",
-    "Beaker >= 1.7.0, < 2",
-    "WebHelpers >= 1.3, < 1.4",
+    "Beaker >= 1.10.1, < 2",
     "WebHelpers2 >= 2.0, < 2.1",
-    "FormEncode >= 1.3.0, < 1.4",
-    "SQLAlchemy >= 1.1, < 1.4",
-    "Mako >= 0.9.0, < 1.1",
-    "Pygments >= 2.2.0, < 2.5",
-    "Whoosh >= 2.5.0, < 2.8",
-    "celery >= 3.1, < 4.0", # TODO: celery 4 doesn't work
-    "Babel >= 1.3, < 2.8",
-    "python-dateutil >= 1.5.0, < 2.9",
+    "FormEncode >= 1.3.1, < 1.4",
+    "SQLAlchemy >= 1.2.9, < 1.4",
+    "Mako >= 0.9.1, < 1.2",
+    "Pygments >= 2.2.0, < 2.6",
+    "Whoosh >= 2.7.1, < 2.8",
+    "celery >= 4.3, < 4.5",
+    "Babel >= 1.3, < 2.9",
+    "python-dateutil >= 2.1.0, < 2.9",
     "Markdown >= 2.2.1, < 3.2",
-    "docutils >= 0.11, < 0.15",
+    "docutils >= 0.11, < 0.17",
     "URLObject >= 2.3.4, < 2.5",
-    "Routes >= 1.13, < 2", # TODO: bumping to 2.0 will make test_file_annotation fail
-    "dulwich >= 0.14.1, < 0.20",
-    "mercurial >= 4.5, < 5.3",
-    "decorator >= 3.3.2, < 4.5",
-    "Paste >= 2.0.3, < 3.1",
-    "bleach >= 3.0, < 3.2",
+    "Routes >= 2.0, < 2.5",
+    "dulwich >= 0.19.0, < 0.20",
+    "mercurial >= 5.2, < 5.5",
+    "decorator >= 4.2.1, < 4.5",
+    "Paste >= 2.0.3, < 3.4",
+    "bleach >= 3.0, < 3.1.4",
     "Click >= 7.0, < 8",
-    "ipaddr >= 2.1.10, < 2.3",
+    "ipaddr >= 2.2.0, < 2.3",
+    "paginate >= 0.5, < 0.6",
+    "paginate_sqlalchemy >= 0.3.0, < 0.4",
+    "bcrypt >= 3.1.0, < 3.2",
+    "pip >= 20.0, < 999",
 ]
 
-if not is_windows:
-    requirements.append("bcrypt >= 3.1.0, < 3.2")
-
 dependency_links = [
 ]
 
@@ -84,8 +85,9 @@
     'Intended Audience :: Developers',
     'License :: OSI Approved :: GNU General Public License (GPL)',
     'Operating System :: OS Independent',
-    'Programming Language :: Python',
-    'Programming Language :: Python :: 2.7',
+    'Programming Language :: Python :: 3.6',
+    'Programming Language :: Python :: 3.7',
+    'Programming Language :: Python :: 3.8',
     'Topic :: Software Development :: Version Control',
 ]
 
@@ -110,8 +112,8 @@
     long_description = open(README_FILE).read()
 except IOError as err:
     sys.stderr.write(
-        "[WARNING] Cannot find file specified as long_description (%s)\n"
-        % README_FILE
+        "[WARNING] Cannot find file specified as long_description (%s): %s\n"
+        % (README_FILE, err)
     )
     long_description = description