changeset 8015:d2319cb2ba9b

Merge stable
author Mads Kiilerich <mads@kiilerich.com>
date Thu, 19 Dec 2019 20:50:33 +0100
parents e8e9f33e9ff6 (diff) 01dbd21d206c (current diff)
children b84f495d82ce
files
diffstat 54 files changed, 240 insertions(+), 537 deletions(-) [+]
line wrap: on
line diff
--- a/kallithea/__init__.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/__init__.py	Thu Dec 19 20:50:33 2019 +0100
@@ -31,7 +31,7 @@
 import sys
 
 
-VERSION = (0, 5, 0)
+VERSION = (0, 5, 99)
 BACKENDS = {
     'hg': 'Mercurial repository',
     'git': 'Git repository',
--- a/kallithea/bin/kallithea_api.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/bin/kallithea_api.py	Thu Dec 19 20:50:33 2019 +0100
@@ -101,7 +101,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_base.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/bin/kallithea_cli_base.py	Thu Dec 19 20:50:33 2019 +0100
@@ -12,8 +12,8 @@
 # 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 functools
+import io
 import logging.config
 import os
 import re
@@ -44,7 +44,7 @@
         return m.group(0)
 
     with open(ini_file_name) as f:
-        return re.sub(r'^\[([^:]+):(.*)]', repl, f.read(), flags=re.MULTILINE)
+        return re.sub(r'^\[([^:]+):(.*)]', repl, f.read().decode(), flags=re.MULTILINE)
 
 
 # This placeholder is the main entry point for the kallithea-cli command
@@ -71,8 +71,8 @@
             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))
+                config_string = read_config(path_to_ini_file, strip_section_prefix=annotated.__name__)
+                logging.config.fileConfig(io.StringIO(config_string))
                 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)
--- a/kallithea/config/app_cfg.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/config/app_cfg.py	Thu Dec 19 20:50:33 2019 +0100
@@ -34,7 +34,7 @@
 
 import kallithea.lib.locale
 import kallithea.model.base
-from kallithea.lib.auth import set_available_permissions
+import kallithea.model.meta
 from kallithea.lib.middleware.https_fixup import HttpsFixup
 from kallithea.lib.middleware.permanent_repo_url import PermanentRepoUrl
 from kallithea.lib.middleware.simplegit import SimpleGit
@@ -162,7 +162,6 @@
 
     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)
@@ -183,6 +182,8 @@
 
     check_git_version()
 
+    kallithea.model.meta.Session.remove()
+
 
 hooks.register('configure_new_app', setup_configuration)
 
--- a/kallithea/config/routing.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/config/routing.py	Thu Dec 19 20:50:33 2019 +0100
@@ -86,7 +86,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 +106,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 +121,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 +161,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 +216,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 +263,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 +369,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 +395,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 +407,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,
@@ -602,7 +601,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 +615,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 +673,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 +718,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	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/controllers/admin/admin.py	Thu Dec 19 20:50:33 2019 +0100
@@ -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
@@ -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/gists.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/controllers/admin/gists.py	Thu Dec 19 20:50:33 2019 +0100
@@ -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()
--- a/kallithea/controllers/admin/repo_groups.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/controllers/admin/repo_groups.py	Thu Dec 19 20:50:33 2019 +0100
@@ -25,7 +25,6 @@
 :license: GPLv3, see LICENSE.md for more details.
 """
 
-import itertools
 import logging
 import traceback
 
@@ -42,7 +41,7 @@
 from kallithea.lib import helpers as h
 from kallithea.lib.auth import HasPermissionAny, HasRepoGroupPermissionLevel, HasRepoGroupPermissionLevelDecorator, LoginRequired
 from kallithea.lib.base import BaseController, render
-from kallithea.lib.utils2 import safe_int
+from kallithea.lib.utils2 import safe_int, safe_unicode
 from kallithea.model.db import RepoGroup, Repository
 from kallithea.model.forms import RepoGroupForm, RepoGroupPermsForm
 from kallithea.model.meta import Session
@@ -93,10 +92,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
@@ -120,9 +117,7 @@
         )
 
         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 = [safe_unicode(g.name) for g in repo_gr.parents] + [safe_unicode(repo_gr.name)]
             repo_count = repo_gr.repositories.count()
             repo_groups_data.append({
                 "raw_name": repo_gr.group_name,
--- a/kallithea/controllers/admin/user_groups.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/controllers/admin/user_groups.py	Thu Dec 19 20:50:33 2019 +0100
@@ -32,7 +32,7 @@
 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
@@ -61,7 +61,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),
--- a/kallithea/controllers/admin/users.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/controllers/admin/users.py	Thu Dec 19 20:50:33 2019 +0100
@@ -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) \
--- a/kallithea/controllers/api/__init__.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/controllers/api/__init__.py	Thu Dec 19 20:50:33 2019 +0100
@@ -168,8 +168,8 @@
         # 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
+        defaults = [type(arg) for arg in argspec[3] or []]
+        default_empty = type(NotImplemented)
 
         # kw arguments required by this method
         func_kwargs = dict(itertools.izip_longest(reversed(arglist), reversed(defaults),
--- a/kallithea/controllers/changelog.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/controllers/changelog.py	Thu Dec 19 20:50:33 2019 +0100
@@ -38,7 +38,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.page import RepoPage
+from kallithea.lib.page import Page
 from kallithea.lib.utils2 import safe_int, safe_str
 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, ChangesetError, EmptyRepositoryError, NodeDoesNotExistError, RepositoryError
 
@@ -113,14 +113,13 @@
                     except RepositoryError as e:
                         h.flash(safe_str(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)
--- a/kallithea/controllers/changeset.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/controllers/changeset.py	Thu Dec 19 20:50:33 2019 +0100
@@ -25,6 +25,7 @@
 :license: GPLv3, see LICENSE.md for more details.
 """
 
+import binascii
 import logging
 import traceback
 from collections import OrderedDict, defaultdict
@@ -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]
@@ -395,6 +396,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 = c.changeset.extra.get(b'source', b'')
+            c.changeset_transplant_source_hash = 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' \
--- a/kallithea/controllers/feed.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/controllers/feed.py	Thu Dec 19 20:50:33 2019 +0100
@@ -100,19 +100,19 @@
             desc_msg.append('\n\n')
             desc_msg.append(raw_diff)
         desc_msg.append('</pre>')
-        return map(safe_unicode, desc_msg)
+        return [safe_unicode(chunk) for chunk in desc_msg]
 
-    def atom(self, repo_name):
-        """Produce an atom-1.0 feed via feedgenerator module"""
+    def _feed(self, repo_name, kind, feed_factory):
+        """Produce a simple feed"""
 
         @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 = Atom1Feed(
+            feed = feed_factory(
                 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
+                ttl=ttl,  # rss only
             )
 
             rss_items_per_page = safe_int(CONFIG.get('rss_items_per_page', 20))
@@ -128,34 +128,12 @@
             response.content_type = feed.mime_type
             return feed.writeString('utf-8')
 
-        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, 'ATOM', Atom1Feed)
+
     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'))
+        return self._feed(repo_name, 'RSS', Rss201rev2Feed)
--- a/kallithea/controllers/home.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/controllers/home.py	Thu Dec 19 20:50:33 2019 +0100
@@ -37,7 +37,6 @@
 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.model.db import RepoGroup, Repository, User, UserGroup
 from kallithea.model.repo import RepoModel
 from kallithea.model.scm import UserGroupList
@@ -67,9 +66,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 +93,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()
 
--- a/kallithea/controllers/journal.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/controllers/journal.py	Thu Dec 19 20:50:33 2019 +0100
@@ -39,7 +39,6 @@
 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.auth import LoginRequired
 from kallithea.lib.base import BaseController, render
@@ -105,21 +104,16 @@
 
         return journal
 
-    def _atom_feed(self, repos, public=True):
+    def _feed(self, repos, feed_factory, link, desc):
         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)
+        feed = feed_factory(
+            title=desc,
+            link=link,
+            description=desc,
+            language=language,
+            ttl=ttl,
+        )
 
         for entry in journal[:feed_nr]:
             user = entry.user
@@ -131,7 +125,6 @@
             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',
@@ -142,52 +135,32 @@
                           link=_url or h.canonical_url(''),
                           author_email=user.email,
                           author_name=user.full_contact,
-                          description=desc)
+                          description=action_extra())
 
         response.content_type = feed.mime_type
         return feed.writeString('utf-8')
 
-    def _rss_feed(self, repos, public=True):
-        journal = self._get_journal_data(repos)
+    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'),
+            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, Atom1Feed, link, desc)
+
+    def _rss_feed(self, repos, public=True):
+        if public:
+            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, Rss201rev2Feed, 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'):
--- a/kallithea/controllers/pullrequests.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/controllers/pullrequests.py	Thu Dec 19 20:50:33 2019 +0100
@@ -201,6 +201,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 +215,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')
 
--- a/kallithea/controllers/search.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/controllers/search.py	Thu Dec 19 20:50:33 2019 +0100
@@ -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
@@ -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=safe_str(c.cur_type),
+                        q=safe_str(c.cur_query),
                     )
 
                 except QueryParserError:
--- a/kallithea/controllers/summary.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/controllers/summary.py	Thu Dec 19 20:50:33 2019 +0100
@@ -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.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
@@ -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(safe_str(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)
--- a/kallithea/lib/annotate.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/lib/annotate.py	Thu Dec 19 20:50:33 2019 +0100
@@ -25,8 +25,6 @@
 :license: GPLv3, see LICENSE.md for more details.
 """
 
-import StringIO
-
 from pygments import highlight
 from pygments.formatters import HtmlFormatter
 
@@ -111,12 +109,12 @@
             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))
@@ -176,7 +174,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 +202,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	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/lib/app_globals.py	Thu Dec 19 20:50:33 2019 +0100
@@ -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	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/lib/auth.py	Thu Dec 19 20:50:33 2019 +0100
@@ -594,24 +594,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
 #==============================================================================
--- a/kallithea/lib/base.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/lib/base.py	Thu Dec 19 20:50:33 2019 +0100
@@ -30,7 +30,6 @@
 
 import datetime
 import logging
-import time
 import traceback
 import warnings
 
@@ -300,7 +299,6 @@
         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)
@@ -343,10 +341,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):
@@ -634,7 +628,7 @@
         warnings.warn(msg, Warning, 2)
         log.warning(msg)
     log.debug("Returning JSON wrapped action output")
-    return json.dumps(data, encoding='utf-8')
+    return json.dumps(data)
 
 @decorator.decorator
 def IfSshEnabled(func, *args, **kwargs):
--- a/kallithea/lib/caching_query.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/lib/caching_query.py	Thu Dec 19 20:50:33 2019 +0100
@@ -1,3 +1,5 @@
+# apparently based on https://github.com/sqlalchemy/sqlalchemy/blob/rel_0_7/examples/beaker_caching/caching_query.py
+
 """caching_query.py
 
 Represent persistence structures which allow the usage of
@@ -137,8 +139,7 @@
     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)]))
+        args.extend([k for k in [str(query._limit), str(query._offset)] if k not in ['None', None, u'None']])
 
         cache_key = " ".join(args)
 
--- a/kallithea/lib/celerylib/tasks.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/lib/celerylib/tasks.py	Thu Dec 19 20:50:33 2019 +0100
@@ -26,9 +26,9 @@
 :license: GPLv3, see LICENSE.md for more details.
 """
 
+import email.utils
 import logging
 import os
-import rfc822
 import traceback
 from collections import OrderedDict
 from operator import itemgetter
@@ -282,7 +282,7 @@
         # 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)' % author.full_name_or_username),
             envelope_addr)
 
     user = email_config.get('smtp_username')
--- a/kallithea/lib/diffs.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/lib/diffs.py	Thu Dec 19 20:50:33 2019 +0100
@@ -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,8 +234,7 @@
     """
     # 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 ''
 
--- a/kallithea/lib/helpers.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/lib/helpers.py	Thu Dec 19 20:50:33 2019 +0100
@@ -22,7 +22,6 @@
 import logging
 import random
 import re
-import StringIO
 import textwrap
 import urlparse
 
@@ -246,12 +245,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 +303,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>'
 
 
@@ -380,7 +379,7 @@
             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)
+            yield [str(int(x * 256)) for x in RGB_tuple]
 
     cgenerator = gen_color()
 
@@ -677,7 +676,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)
--- a/kallithea/lib/indexers/daemon.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/lib/indexers/daemon.py	Thu Dec 19 20:50:33 2019 +0100
@@ -78,7 +78,7 @@
         # filter repo list
         if repo_list:
             # Fix non-ascii repo names to unicode
-            repo_list = map(safe_unicode, repo_list)
+            repo_list = set(safe_unicode(repo_name) for repo_name in repo_list)
             self.filtered_repo_paths = {}
             for repo_name, repo in self.repo_paths.items():
                 if repo_name in repo_list:
--- a/kallithea/lib/middleware/pygrack.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/lib/middleware/pygrack.py	Thu Dec 19 20:50:33 2019 +0100
@@ -186,7 +186,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/page.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/lib/page.py	Thu Dec 19 20:50:33 2019 +0100
@@ -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/pygmentsutils.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/lib/pygmentsutils.py	Thu Dec 19 20:50:33 2019 +0100
@@ -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/response.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/lib/rcmail/response.py	Thu Dec 19 20:50:33 2019 +0100
@@ -422,7 +422,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:
--- a/kallithea/lib/utils.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/lib/utils.py	Thu Dec 19 20:50:33 2019 +0100
@@ -322,7 +322,7 @@
                 '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.
@@ -342,8 +342,6 @@
                       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()
 
     # force set push_ssl requirement to False, Kallithea handles that
     baseui.setconfig('web', 'push_ssl', False)
@@ -377,12 +375,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
 
 
 def set_vcs_config(config):
--- a/kallithea/lib/utils2.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/lib/utils2.py	Thu Dec 19 20:50:33 2019 +0100
@@ -394,7 +394,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):
--- a/kallithea/lib/vcs/backends/git/inmemory.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/lib/vcs/backends/git/inmemory.py	Thu Dec 19 20:50:33 2019 +0100
@@ -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_str(dirpath).split('/') if dirpath else []
             parent = commit_tree
             ancestors = [('', parent)]
 
--- a/kallithea/lib/vcs/backends/git/repository.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/lib/vcs/backends/git/repository.py	Thu Dec 19 20:50:33 2019 +0100
@@ -572,7 +572,8 @@
 
         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,
--- a/kallithea/lib/vcs/backends/hg/changeset.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/lib/vcs/backends/hg/changeset.py	Thu Dec 19 20:50:33 2019 +0100
@@ -27,7 +27,7 @@
 
     @LazyProperty
     def tags(self):
-        return map(safe_unicode, self._ctx.tags())
+        return [safe_unicode(tag) for tag in self._ctx.tags()]
 
     @LazyProperty
     def branch(self):
@@ -95,7 +95,7 @@
 
     @LazyProperty
     def bookmarks(self):
-        return map(safe_unicode, self._ctx.bookmarks())
+        return [safe_unicode(bookmark) for bookmark in self._ctx.bookmarks()]
 
     @LazyProperty
     def message(self):
@@ -246,9 +246,9 @@
         """
         fctx = self._get_filectx(path)
         if 'x' in fctx.flags():
-            return 0100755
+            return 0o100755
         else:
-            return 0100644
+            return 0o100644
 
     def get_file_content(self, path):
         """
--- a/kallithea/lib/vcs/backends/hg/repository.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/lib/vcs/backends/hg/repository.py	Thu Dec 19 20:50:33 2019 +0100
@@ -553,7 +553,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)
 
--- a/kallithea/lib/vcs/nodes.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/lib/vcs/nodes.py	Thu Dec 19 20:50:33 2019 +0100
@@ -120,10 +120,6 @@
         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
@@ -259,7 +255,7 @@
         super(FileNode, self).__init__(path, kind=NodeKind.FILE)
         self.changeset = changeset
         self._content = content
-        self._mode = mode or 0100644
+        self._mode = mode or 0o100644
 
     @LazyProperty
     def mode(self):
--- a/kallithea/lib/vcs/utils/annotate.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/lib/vcs/utils/annotate.py	Thu Dec 19 20:50:33 2019 +0100
@@ -1,5 +1,3 @@
-import StringIO
-
 from pygments import highlight
 from pygments.formatters import HtmlFormatter
 
@@ -83,12 +81,12 @@
             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))
@@ -147,7 +145,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>'
 
         '''
@@ -175,5 +173,5 @@
                   ''.join(headers_row) +
                   ''.join(body_row_start)
                   )
-        yield 0, dummyoutfile.getvalue()
+        yield 0, ''.join(inner_lines)
         yield 0, '</td></tr></table>'
--- a/kallithea/model/db.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/model/db.py	Thu Dec 19 20:50:33 2019 +0100
@@ -205,7 +205,7 @@
 
     @validates('_app_settings_value')
     def validate_settings_value(self, key, val):
-        assert type(val) == unicode
+        assert isinstance(val, unicode)
         return val
 
     @hybrid_property
@@ -1163,7 +1163,7 @@
         # 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))
+        return os.path.join(*(safe_unicode(d) for d in p))
 
     @property
     def cache_keys(self):
@@ -1190,7 +1190,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):
@@ -2511,8 +2511,7 @@
     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])))
+        return get_repo(os.path.join(safe_str(base_path), safe_str(self.gist_access_id)))
 
 
 class UserSshKeys(Base, BaseDbModel):
--- a/kallithea/model/permission.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/model/permission.py	Thu Dec 19 20:50:33 2019 +0100
@@ -73,8 +73,7 @@
             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
 
--- a/kallithea/model/repo.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/model/repo.py	Thu Dec 19 20:50:33 2019 +0100
@@ -128,7 +128,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,
@@ -290,7 +290,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 +306,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:
@@ -360,7 +359,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
 
@@ -644,7 +643,7 @@
         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(*(safe_str(x) for x in _paths))
 
         # check if this path is not a repository
         if is_valid_repo(repo_path, self.repos_path):
@@ -661,7 +660,7 @@
         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'):
--- a/kallithea/model/scm.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/model/scm.py	Thu Dec 19 20:50:33 2019 +0100
@@ -25,7 +25,6 @@
 :license: GPLv3, see LICENSE.md for more details.
 """
 
-import cStringIO
 import logging
 import os
 import posixpath
@@ -485,12 +484,8 @@
             # 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,)):
+            else:
                 content = content.read()
-            else:
-                raise Exception('Content is of unrecognized type %s' % (
-                    type(content)
-                ))
             processed_nodes.append((f_path, content))
 
         message = safe_unicode(message)
@@ -755,7 +750,7 @@
                     with open(_hook_file, 'wb') as f:
                         tmpl = tmpl.replace('_TMPL_', 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/validators.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/model/validators.py	Thu Dec 19 20:50:33 2019 +0100
@@ -412,7 +412,7 @@
 
             if url and url != value.get('clone_uri_hidden'):
                 try:
-                    is_valid_repo_uri(repo_type, url, make_ui(clear_session=False))
+                    is_valid_repo_uri(repo_type, url, make_ui())
                 except Exception:
                     log.exception('URL validation failed')
                     msg = self.message('clone_uri', state)
@@ -782,7 +782,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/templates/admin/gists/index.html	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/templates/admin/gists/index.html	Thu Dec 19 20:50:33 2019 +0100
@@ -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/changeset/changeset.html	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/templates/changeset/changeset.html	Thu Dec 19 20:50:33 2019 +0100
@@ -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
 
--- a/kallithea/templates/pullrequests/pullrequest_data.html	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/templates/pullrequests/pullrequest_data.html	Thu Dec 19 20:50:33 2019 +0100
@@ -80,7 +80,7 @@
 </div>
 
 %if hasattr(pullrequests, 'pager'):
-    ${pullrequests.pager(**request.GET.mixed())}
+    ${pullrequests.pager()}
 %endif
 
 </%def>
--- a/kallithea/tests/functional/test_files.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/tests/functional/test_files.py	Thu Dec 19 20:50:33 2019 +0100
@@ -136,7 +136,7 @@
                                     repo_name=HG_REPO,
                                     revision='tip',
                                     f_path='vcs/nodes.py',
-                                    annotate=True))
+                                    annotate='1'))
 
         response.mustcontain("""r356:25213a5fbb04""")
 
@@ -146,7 +146,7 @@
                                     repo_name=GIT_REPO,
                                     revision='master',
                                     f_path='vcs/nodes.py',
-                                    annotate=True))
+                                    annotate='1'))
         response.mustcontain("""r345:c994f0de03b2""")
 
     def test_file_annotation_history(self):
@@ -155,7 +155,7 @@
                                     repo_name=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
@@ -177,7 +177,7 @@
                                     repo_name=HG_REPO,
                                     revision='tip',
                                     f_path='vcs/nodes.py',
-                                    annotate=True))
+                                    annotate='1'))
         response.mustcontain('Marcin Kuzminski')
         response.mustcontain('Lukasz Balcerzak')
 
@@ -187,7 +187,7 @@
                                     repo_name=GIT_REPO,
                                     revision='master',
                                     f_path='vcs/nodes.py',
-                                    annotate=True))
+                                    annotate='1'))
         response.mustcontain('Marcin Kuzminski')
         response.mustcontain('Lukasz Balcerzak')
 
--- a/kallithea/tests/vcs/test_archives.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/tests/vcs/test_archives.py	Thu Dec 19 20:50:33 2019 +0100
@@ -1,6 +1,6 @@
 import datetime
+import io
 import os
-import StringIO
 import tarfile
 import tempfile
 import zipfile
@@ -37,9 +37,8 @@
 
         for x in xrange(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]
@@ -71,7 +70,7 @@
         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_git.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/tests/vcs/test_git.py	Thu Dec 19 20:50:33 2019 +0100
@@ -590,17 +590,17 @@
 
     def test_commit_message_is_unicode(self):
         for cs in self.repo:
-            assert type(cs.message) == unicode
+            assert isinstance(cs.message, unicode)
 
     def test_changeset_author_is_unicode(self):
         for cs in self.repo:
-            assert type(cs.author) == unicode
+            assert isinstance(cs.author, unicode)
 
     def test_repo_files_content_is_unicode(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, unicode)
 
     def test_wrong_path(self):
         # There is 'setup.py' in the root dir but not there:
@@ -657,7 +657,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'),
                 ],
             },
--- a/kallithea/tests/vcs/test_hg.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/tests/vcs/test_hg.py	Thu Dec 19 20:50:33 2019 +0100
@@ -444,20 +444,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'])
@@ -538,17 +538,17 @@
 
     def test_commit_message_is_unicode(self):
         for cm in self.repo:
-            assert type(cm.message) == unicode
+            assert isinstance(cm.message, unicode)
 
     def test_changeset_author_is_unicode(self):
         for cm in self.repo:
-            assert type(cm.author) == unicode
+            assert isinstance(cm.author, unicode)
 
     def test_repo_files_content_is_unicode(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, unicode)
 
     def test_wrong_path(self):
         # There is 'setup.py' in the root dir but not there:
--- a/kallithea/tests/vcs/test_nodes.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/kallithea/tests/vcs/test_nodes.py	Thu Dec 19 20:50:33 2019 +0100
@@ -144,13 +144,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):
--- a/setup.py	Sat Nov 30 10:39:37 2019 +0100
+++ b/setup.py	Thu Dec 19 20:50:33 2019 +0100
@@ -61,7 +61,7 @@
     "Markdown >= 2.2.1, < 3.2",
     "docutils >= 0.11, < 0.15",
     "URLObject >= 2.3.4, < 2.5",
-    "Routes >= 1.13, < 2", # TODO: bumping to 2.0 will make test_file_annotation fail
+    "Routes >= 2.0, < 2.5",
     "dulwich >= 0.14.1, < 0.20",
     "mercurial >= 4.5, < 5.3",
     "decorator >= 3.3.2, < 4.5",
@@ -69,6 +69,8 @@
     "bleach >= 3.0, < 3.2",
     "Click >= 7.0, < 8",
     "ipaddr >= 2.1.10, < 2.3",
+    "paginate >= 0.5, < 0.6",
+    "paginate_sqlalchemy >= 0.3.0, < 0.4",
 ]
 
 if not is_windows: