changeset 8745:aa067dbcdc82

hooks: move the vcs hook entry points and setup code out of lib Mercurial hooks are running in a process that already has been initialized, so they invoke the hooks lib directly. Git hooks are binaries and need a lot of initialization before they can do the same. Move this extra setup code elsewhere. Having this high level code in bin is perhaps also not ideal, but it also doesn't seem that bad: that is where other command line entry points invoke make_app. (It seems like it could be adventageous to somehow use "real" bin commands for hooks ... but for now we use the home-made templates.) Note: As a side effect of this change, all git hooks *must* be re-installed when upgrading.
author Mads Kiilerich <mads@kiilerich.com>
date Wed, 04 Nov 2020 21:00:44 +0100
parents 3f8cd215f5eb
children 259213d96dca
files kallithea/bin/vcs_hooks.py kallithea/config/middleware/simplegit.py kallithea/lib/hooks.py kallithea/lib/utils.py kallithea/lib/vcs/ssh/hg.py kallithea/lib/vcs/utils/helpers.py kallithea/templates/py/git_post_receive_hook.py kallithea/templates/py/git_pre_receive_hook.py
diffstat 8 files changed, 229 insertions(+), 193 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/kallithea/bin/vcs_hooks.py	Wed Nov 04 21:00:44 2020 +0100
@@ -0,0 +1,191 @@
+# -*- 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.bin.vcs_hooks
+~~~~~~~~~~~~~~~~~~~
+
+Entry points for Kallithea hooking into Mercurial and Git.
+
+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: Aug 6, 2010
+:author: marcink
+:copyright: (c) 2013 RhodeCode GmbH, and others.
+:license: GPLv3, see LICENSE.md for more details.
+"""
+
+import os
+import sys
+
+import mercurial.scmutil
+import paste.deploy
+
+import kallithea
+import kallithea.config.application
+from kallithea.lib import hooks, webutils
+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.helpers import get_scm_size
+from kallithea.model import db
+
+
+def repo_size(ui, repo, hooktype=None, **kwargs):
+    """Show size of Mercurial repository.
+
+    Called as Mercurial hook changegroup.repo_size after push.
+    """
+    size_hg, size_root = 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') % (
+        webutils.format_byte_size(size_hg),
+        webutils.format_byte_size(size_root),
+        webutils.format_byte_size(size_hg + size_root),
+        last_cs.rev(),
+        ascii_str(last_cs.hex())[:12],
+    )
+    ui.status(safe_bytes(msg))
+
+
+def pull_action(ui, repo, **kwargs):
+    """Logs user pull action
+
+    Called as Mercurial hook outgoing.kallithea_pull_action.
+    """
+    hooks.log_pull_action()
+
+
+def push_action(ui, repo, node, node_last, **kwargs):
+    """
+    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_push_action .
+
+    The pushed changesets is given by the revset 'node:node_last'.
+    """
+    revs = [ascii_str(repo[r].hex()) for r in mercurial.scmutil.revrange(repo, [b'%s:%s' % (node, node_last)])]
+    hooks.process_pushed_raw_ids(revs)
+
+
+def _git_hook_environment(repo_path):
+    """
+    Create a light-weight environment for stand-alone scripts and return an UI and the
+    db repository.
+
+    Git hooks are executed as subprocess of Git while Kallithea is waiting, and
+    they thus need enough info to be able to create an app environment and
+    connect to the database.
+    """
+    extras = get_hook_environment()
+
+    path_to_ini_file = extras['config']
+    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.application.make_app(config.global_conf, **config.local_conf)
+
+    # fix if it's not a bare repo
+    if repo_path.endswith(os.sep + '.git'):
+        repo_path = repo_path[:-5]
+
+    repo = db.Repository.get_by_full_path(repo_path)
+    if not repo:
+        raise OSError('Repository %s not found in database' % repo_path)
+
+    return repo
+
+
+def pre_receive(repo_path, git_stdin_lines):
+    """Called from Git pre-receive hook.
+    The returned value is used as hook exit code and must be 0.
+    """
+    # Currently unused. TODO: remove?
+    return 0
+
+
+def post_receive(repo_path, git_stdin_lines):
+    """Called from Git post-receive hook.
+    The returned value is used as hook exit code and must be 0.
+    """
+    try:
+        repo = _git_hook_environment(repo_path)
+    except HookEnvironmentError as e:
+        sys.stderr.write("Skipping Kallithea Git post-receive 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()
+
+    rev_data = []
+    for l in git_stdin_lines:
+        old_rev, new_rev, ref = l.strip().split(' ')
+        _ref_data = ref.split('/')
+        if _ref_data[1] in ['tags', 'heads']:
+            rev_data.append({'old_rev': old_rev,
+                             'new_rev': new_rev,
+                             'ref': ref,
+                             'type': _ref_data[1],
+                             'name': '/'.join(_ref_data[2:])})
+
+    git_revs = []
+    for push_ref in rev_data:
+        _type = push_ref['type']
+        if _type == 'heads':
+            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(
+                        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 = 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 = scm_repo.run_git_command(cmd)
+                git_revs += stdout.splitlines()
+
+            elif push_ref['new_rev'] == EmptyChangeset().raw_id:
+                # delete branch case
+                git_revs += ['delete_branch=>%s' % push_ref['name']]
+            else:
+                cmd = ['log', '%(old_rev)s..%(new_rev)s' % push_ref,
+                       '--reverse', '--pretty=format:%H']
+                stdout = scm_repo.run_git_command(cmd)
+                git_revs += stdout.splitlines()
+
+        elif _type == 'tags':
+            git_revs += ['tag=>%s' % push_ref['name']]
+
+    hooks.process_pushed_raw_ids(git_revs)
+
+    return 0
+
+
+# Almost exactly like Mercurial contrib/hg-ssh:
+def rejectpush(ui, **kwargs):
+    """Mercurial hook to be installed as pretxnopen and prepushkey for read-only repos.
+    Return value 1 will make the hook fail and reject the push.
+    """
+    ex = get_hook_environment()
+    ui.warn(safe_bytes("Push access to %r denied\n" % ex.repository))
+    return 1
--- a/kallithea/config/middleware/simplegit.py	Mon Nov 02 15:40:18 2020 +0100
+++ b/kallithea/config/middleware/simplegit.py	Wed Nov 04 21:00:44 2020 +0100
@@ -84,7 +84,7 @@
             if (parsed_request.cmd == 'info/refs' and
                 parsed_request.service == 'git-upload-pack'
             ):
-                # Run hooks like Mercurial outgoing.kallithea_log_pull_action does
+                # Run hooks like Mercurial outgoing.kallithea_pull_action does
                 hooks.log_pull_action()
             # Note: push hooks are handled by post-receive hook
 
--- a/kallithea/lib/hooks.py	Mon Nov 02 15:40:18 2020 +0100
+++ b/kallithea/lib/hooks.py	Wed Nov 04 21:00:44 2020 +0100
@@ -15,7 +15,8 @@
 kallithea.lib.hooks
 ~~~~~~~~~~~~~~~~~~~
 
-Hooks run by Kallithea
+Hooks run by Kallithea. Generally called 'log_*', but will also do important
+invalidation of caches and run extension hooks.
 
 This file was forked by the Kallithea project in July 2014.
 Original author and date, and relevant copyright and licensing information is below:
@@ -25,69 +26,17 @@
 :license: GPLv3, see LICENSE.md for more details.
 """
 
-import os
-import sys
 import time
 
-import mercurial.scmutil
-import paste.deploy
-
 import kallithea
-from kallithea.lib import webutils
 from kallithea.lib.exceptions import UserCreationError
-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.model import db
+from kallithea.lib.utils import action_logger
+from kallithea.lib.utils2 import get_hook_environment
 
 
-def _get_scm_size(alias, root_path):
-    if not alias.startswith('.'):
-        alias += '.'
-
-    size_scm, size_root = 0, 0
-    for path, dirs, files in os.walk(root_path):
-        if path.find(alias) != -1:
-            for f in files:
-                try:
-                    size_scm += os.path.getsize(os.path.join(path, f))
-                except OSError:
-                    pass
-        else:
-            for f in files:
-                try:
-                    size_root += os.path.getsize(os.path.join(path, f))
-                except OSError:
-                    pass
-
-    size_scm_f = webutils.format_byte_size(size_scm)
-    size_root_f = webutils.format_byte_size(size_root)
-    size_total_f = webutils.format_byte_size(size_root + size_scm)
-
-    return size_scm_f, size_root_f, size_total_f
-
-
-def repo_size(ui, repo, hooktype=None, **kwargs):
-    """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(), ascii_str(last_cs.hex())[:12]
-    )
-    ui.status(safe_bytes(msg))
-
-
-def log_pull_action(*args, **kwargs):
+def log_pull_action():
     """Logs user last pull action
 
-    Called as Mercurial hook outgoing.kallithea_log_pull_action or from Kallithea before invoking Git.
-
     Does *not* use the action from the hook environment but is always 'pull'.
     """
     ex = get_hook_environment()
@@ -102,25 +51,11 @@
         callback(**kw)
 
 
-def log_push_action(ui, repo, node, node_last, **kwargs):
-    """
-    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'.
-    """
-    revs = [ascii_str(repo[r].hex()) for r in mercurial.scmutil.revrange(repo, [b'%s:%s' % (node, node_last)])]
-    process_pushed_raw_ids(revs)
-
-
 def process_pushed_raw_ids(revs):
     """
     Register that changes have been added to the repo - log the action *and* invalidate caches.
 
-    Called from Mercurial changegroup.kallithea_log_push_action calling hook log_push_action,
+    Called from Mercurial changegroup.kallithea_push_action calling hook push_action,
     or from the Git post-receive hook calling handle_git_post_receive ...
     or from scm _handle_push.
     """
@@ -290,115 +225,3 @@
     callback = getattr(kallithea.EXTENSIONS, 'DELETE_USER_HOOK', None)
     if callable(callback):
         callback(deleted_by=deleted_by, **user_dict)
-
-
-def _hook_environment(repo_path):
-    """
-    Create a light-weight environment for stand-alone scripts and return an UI and the
-    db repository.
-
-    Git hooks are executed as subprocess of Git while Kallithea is waiting, and
-    they thus need enough info to be able to create an app environment and
-    connect to the database.
-    """
-    import kallithea.config.application
-
-    extras = get_hook_environment()
-
-    path_to_ini_file = extras['config']
-    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.application.make_app(config.global_conf, **config.local_conf)
-
-    # fix if it's not a bare repo
-    if repo_path.endswith(os.sep + '.git'):
-        repo_path = repo_path[:-5]
-
-    repo = db.Repository.get_by_full_path(repo_path)
-    if not repo:
-        raise OSError('Repository %s not found in database' % repo_path)
-
-    baseui = make_ui()
-    return baseui, repo
-
-
-def handle_git_pre_receive(repo_path, git_stdin_lines):
-    """Called from Git pre-receive hook.
-    The returned value is used as hook exit code and must be 0.
-    """
-    # Currently unused. TODO: remove?
-    return 0
-
-
-def handle_git_post_receive(repo_path, git_stdin_lines):
-    """Called from Git post-receive hook.
-    The returned value is used as hook exit code and must be 0.
-    """
-    try:
-        baseui, repo = _hook_environment(repo_path)
-    except HookEnvironmentError as e:
-        sys.stderr.write("Skipping Kallithea Git post-receive 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()
-
-    rev_data = []
-    for l in git_stdin_lines:
-        old_rev, new_rev, ref = l.strip().split(' ')
-        _ref_data = ref.split('/')
-        if _ref_data[1] in ['tags', 'heads']:
-            rev_data.append({'old_rev': old_rev,
-                             'new_rev': new_rev,
-                             'ref': ref,
-                             'type': _ref_data[1],
-                             'name': '/'.join(_ref_data[2:])})
-
-    git_revs = []
-    for push_ref in rev_data:
-        _type = push_ref['type']
-        if _type == 'heads':
-            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(
-                        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 = 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 = scm_repo.run_git_command(cmd)
-                git_revs += stdout.splitlines()
-
-            elif push_ref['new_rev'] == EmptyChangeset().raw_id:
-                # delete branch case
-                git_revs += ['delete_branch=>%s' % push_ref['name']]
-            else:
-                cmd = ['log', '%(old_rev)s..%(new_rev)s' % push_ref,
-                       '--reverse', '--pretty=format:%H']
-                stdout = scm_repo.run_git_command(cmd)
-                git_revs += stdout.splitlines()
-
-        elif _type == 'tags':
-            git_revs += ['tag=>%s' % push_ref['name']]
-
-    process_pushed_raw_ids(git_revs)
-
-    return 0
-
-
-# Almost exactly like Mercurial contrib/hg-ssh:
-def rejectpush(ui, **kwargs):
-    """Mercurial hook to be installed as pretxnopen and prepushkey for read-only repos.
-    Return value 1 will make the hook fail and reject the push.
-    """
-    ex = get_hook_environment()
-    ui.warn(safe_bytes("Push access to %r denied\n" % ex.repository))
-    return 1
--- a/kallithea/lib/utils.py	Mon Nov 02 15:40:18 2020 +0100
+++ b/kallithea/lib/utils.py	Wed Nov 04 21:00:44 2020 +0100
@@ -333,10 +333,10 @@
     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(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')
+    baseui.setconfig(b'hooks', b'changegroup.kallithea_push_action', b'python:kallithea.bin.vcs_hooks.push_action')
+    baseui.setconfig(b'hooks', b'outgoing.kallithea_pull_action', b'python:kallithea.bin.vcs_hooks.pull_action')
     if baseui.config(b'hooks', ascii_bytes(db.Ui.HOOK_REPO_SIZE)):  # ignore actual value
-        baseui.setconfig(b'hooks', ascii_bytes(db.Ui.HOOK_REPO_SIZE), b'python:kallithea.lib.hooks.repo_size')
+        baseui.setconfig(b'hooks', ascii_bytes(db.Ui.HOOK_REPO_SIZE), b'python:kallithea.bin.vcs_hooks.repo_size')
 
     if repo_path is not None:
         # Note: MercurialRepository / mercurial.localrepo.instance will do this too, so it will always be possible to override db settings or what is hardcoded above
--- a/kallithea/lib/vcs/ssh/hg.py	Mon Nov 02 15:40:18 2020 +0100
+++ b/kallithea/lib/vcs/ssh/hg.py	Wed Nov 04 21:00:44 2020 +0100
@@ -55,8 +55,8 @@
         # 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(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')
+            baseui.setconfig(b'hooks', b'pretxnopen._ssh_reject', b'python:kallithea.bin.vcs_hooks.rejectpush')
+            baseui.setconfig(b'hooks', b'prepushkey._ssh_reject', b'python:kallithea.bin.vcs_hooks.rejectpush')
 
         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)
--- a/kallithea/lib/vcs/utils/helpers.py	Mon Nov 02 15:40:18 2020 +0100
+++ b/kallithea/lib/vcs/utils/helpers.py	Wed Nov 04 21:00:44 2020 +0100
@@ -103,6 +103,28 @@
     return result
 
 
+def get_scm_size(alias, root_path):
+    if not alias.startswith('.'):
+        alias += '.'
+
+    size_scm, size_root = 0, 0
+    for path, dirs, files in os.walk(root_path):
+        if path.find(alias) != -1:
+            for f in files:
+                try:
+                    size_scm += os.path.getsize(os.path.join(path, f))
+                except OSError:
+                    pass
+        else:
+            for f in files:
+                try:
+                    size_root += os.path.getsize(os.path.join(path, f))
+                except OSError:
+                    pass
+
+    return size_scm, size_root
+
+
 def get_highlighted_code(name, code, type='terminal'):
     """
     If pygments are available on the system
--- a/kallithea/templates/py/git_post_receive_hook.py	Mon Nov 02 15:40:18 2020 +0100
+++ b/kallithea/templates/py/git_post_receive_hook.py	Wed Nov 04 21:00:44 2020 +0100
@@ -11,7 +11,7 @@
 import os
 import sys
 
-import kallithea.lib.hooks
+import kallithea.bin.vcs_hooks
 
 
 # Set output mode on windows to binary for stderr.
@@ -30,7 +30,7 @@
 def main():
     repo_path = os.path.abspath('.')
     git_stdin_lines = sys.stdin.readlines()
-    sys.exit(kallithea.lib.hooks.handle_git_post_receive(repo_path, git_stdin_lines))
+    sys.exit(kallithea.bin.vcs_hooks.post_receive(repo_path, git_stdin_lines))
 
 
 if __name__ == '__main__':
--- a/kallithea/templates/py/git_pre_receive_hook.py	Mon Nov 02 15:40:18 2020 +0100
+++ b/kallithea/templates/py/git_pre_receive_hook.py	Wed Nov 04 21:00:44 2020 +0100
@@ -11,7 +11,7 @@
 import os
 import sys
 
-import kallithea.lib.hooks
+import kallithea.bin.vcs_hooks
 
 
 # Set output mode on windows to binary for stderr.
@@ -30,7 +30,7 @@
 def main():
     repo_path = os.path.abspath('.')
     git_stdin_lines = sys.stdin.readlines()
-    sys.exit(kallithea.lib.hooks.handle_git_pre_receive(repo_path, git_stdin_lines))
+    sys.exit(kallithea.bin.vcs_hooks.pre_receive(repo_path, git_stdin_lines))
 
 
 if __name__ == '__main__':