hooks.py
426 lines
| 12.3 KiB
| text/x-python
|
PythonLexer
/ vcsserver / hooks.py
r0 | # -*- coding: utf-8 -*- | |||
# RhodeCode VCSServer provides access to different vcs backends via network. | ||||
r149 | # Copyright (C) 2014-2017 RodeCode GmbH | |||
r0 | # | |||
# 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, write to the Free Software Foundation, | ||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | ||||
r171 | import io | |||
import sys | ||||
import json | ||||
import logging | ||||
r0 | import collections | |||
import importlib | ||||
import subprocess | ||||
r171 | ||||
r0 | from httplib import HTTPConnection | |||
import mercurial.scmutil | ||||
import mercurial.node | ||||
import simplejson as json | ||||
from vcsserver import exceptions | ||||
r171 | log = logging.getLogger(__name__) | |||
r0 | ||||
class HooksHttpClient(object): | ||||
connection = None | ||||
def __init__(self, hooks_uri): | ||||
self.hooks_uri = hooks_uri | ||||
def __call__(self, method, extras): | ||||
connection = HTTPConnection(self.hooks_uri) | ||||
body = self._serialize(method, extras) | ||||
connection.request('POST', '/', body) | ||||
response = connection.getresponse() | ||||
return json.loads(response.read()) | ||||
def _serialize(self, hook_name, extras): | ||||
data = { | ||||
'method': hook_name, | ||||
'extras': extras | ||||
} | ||||
return json.dumps(data) | ||||
class HooksDummyClient(object): | ||||
def __init__(self, hooks_module): | ||||
self._hooks_module = importlib.import_module(hooks_module) | ||||
def __call__(self, hook_name, extras): | ||||
with self._hooks_module.Hooks() as hooks: | ||||
return getattr(hooks, hook_name)(extras) | ||||
class RemoteMessageWriter(object): | ||||
"""Writer base class.""" | ||||
r222 | def write(self, message): | |||
r0 | raise NotImplementedError() | |||
class HgMessageWriter(RemoteMessageWriter): | ||||
"""Writer that knows how to send messages to mercurial clients.""" | ||||
def __init__(self, ui): | ||||
self.ui = ui | ||||
def write(self, message): | ||||
# TODO: Check why the quiet flag is set by default. | ||||
old = self.ui.quiet | ||||
self.ui.quiet = False | ||||
self.ui.status(message.encode('utf-8')) | ||||
self.ui.quiet = old | ||||
class GitMessageWriter(RemoteMessageWriter): | ||||
"""Writer that knows how to send messages to git clients.""" | ||||
def __init__(self, stdout=None): | ||||
self.stdout = stdout or sys.stdout | ||||
def write(self, message): | ||||
self.stdout.write(message.encode('utf-8')) | ||||
def _handle_exception(result): | ||||
exception_class = result.get('exception') | ||||
r171 | exception_traceback = result.get('exception_traceback') | |||
if exception_traceback: | ||||
log.error('Got traceback from remote call:%s', exception_traceback) | ||||
r0 | if exception_class == 'HTTPLockedRC': | |||
raise exceptions.RepositoryLockedException(*result['exception_args']) | ||||
elif exception_class == 'RepositoryError': | ||||
raise exceptions.VcsException(*result['exception_args']) | ||||
elif exception_class: | ||||
raise Exception('Got remote exception "%s" with args "%s"' % | ||||
(exception_class, result['exception_args'])) | ||||
def _get_hooks_client(extras): | ||||
if 'hooks_uri' in extras: | ||||
protocol = extras.get('hooks_protocol') | ||||
r213 | return HooksHttpClient(extras['hooks_uri']) | |||
r0 | else: | |||
return HooksDummyClient(extras['hooks_module']) | ||||
def _call_hook(hook_name, extras, writer): | ||||
hooks = _get_hooks_client(extras) | ||||
result = hooks(hook_name, extras) | ||||
writer.write(result['output']) | ||||
_handle_exception(result) | ||||
return result['status'] | ||||
def _extras_from_ui(ui): | ||||
extras = json.loads(ui.config('rhodecode', 'RC_SCM_DATA')) | ||||
return extras | ||||
def repo_size(ui, repo, **kwargs): | ||||
return _call_hook('repo_size', _extras_from_ui(ui), HgMessageWriter(ui)) | ||||
def pre_pull(ui, repo, **kwargs): | ||||
return _call_hook('pre_pull', _extras_from_ui(ui), HgMessageWriter(ui)) | ||||
def post_pull(ui, repo, **kwargs): | ||||
return _call_hook('post_pull', _extras_from_ui(ui), HgMessageWriter(ui)) | ||||
r223 | def _rev_range_hash(repo, node): | |||
commits = [] | ||||
for rev in xrange(repo[node], len(repo)): | ||||
ctx = repo[rev] | ||||
commit_id = mercurial.node.hex(ctx.node()) | ||||
branch = ctx.branch() | ||||
commits.append((commit_id, branch)) | ||||
return commits | ||||
r170 | def pre_push(ui, repo, node=None, **kwargs): | |||
extras = _extras_from_ui(ui) | ||||
rev_data = [] | ||||
if node and kwargs.get('hooktype') == 'pretxnchangegroup': | ||||
branches = collections.defaultdict(list) | ||||
r223 | for commit_id, branch in _rev_range_hash(repo, node): | |||
r170 | branches[branch].append(commit_id) | |||
for branch, commits in branches.iteritems(): | ||||
old_rev = kwargs.get('node_last') or commits[0] | ||||
rev_data.append({ | ||||
'old_rev': old_rev, | ||||
'new_rev': commits[-1], | ||||
'ref': '', | ||||
'type': 'branch', | ||||
'name': branch, | ||||
}) | ||||
extras['commit_ids'] = rev_data | ||||
return _call_hook('pre_push', extras, HgMessageWriter(ui)) | ||||
r0 | ||||
r223 | def post_push(ui, repo, node, **kwargs): | |||
extras = _extras_from_ui(ui) | ||||
commit_ids = [] | ||||
branches = [] | ||||
bookmarks = [] | ||||
tags = [] | ||||
r0 | ||||
r223 | for commit_id, branch in _rev_range_hash(repo, node): | |||
commit_ids.append(commit_id) | ||||
if branch not in branches: | ||||
branches.append(branch) | ||||
r0 | ||||
r223 | if hasattr(ui, '_rc_pushkey_branches'): | |||
bookmarks = ui._rc_pushkey_branches | ||||
r0 | ||||
extras['commit_ids'] = commit_ids | ||||
r223 | extras['new_refs'] = { | |||
'branches': branches, | ||||
'bookmarks': bookmarks, | ||||
'tags': tags | ||||
} | ||||
r0 | ||||
return _call_hook('post_push', extras, HgMessageWriter(ui)) | ||||
r221 | def key_push(ui, repo, **kwargs): | |||
if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks': | ||||
# store new bookmarks in our UI object propagated later to post_push | ||||
ui._rc_pushkey_branches = repo[kwargs['key']].bookmarks() | ||||
return | ||||
r0 | # backward compat | |||
log_pull_action = post_pull | ||||
# backward compat | ||||
log_push_action = post_push | ||||
def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env): | ||||
""" | ||||
Old hook name: keep here for backward compatibility. | ||||
This is only required when the installed git hooks are not upgraded. | ||||
""" | ||||
pass | ||||
def handle_git_post_receive(unused_repo_path, unused_revs, unused_env): | ||||
""" | ||||
Old hook name: keep here for backward compatibility. | ||||
This is only required when the installed git hooks are not upgraded. | ||||
""" | ||||
pass | ||||
HookResponse = collections.namedtuple('HookResponse', ('status', 'output')) | ||||
def git_pre_pull(extras): | ||||
""" | ||||
Pre pull hook. | ||||
:param extras: dictionary containing the keys defined in simplevcs | ||||
:type extras: dict | ||||
:return: status code of the hook. 0 for success. | ||||
:rtype: int | ||||
""" | ||||
if 'pull' not in extras['hooks']: | ||||
return HookResponse(0, '') | ||||
stdout = io.BytesIO() | ||||
try: | ||||
status = _call_hook('pre_pull', extras, GitMessageWriter(stdout)) | ||||
except Exception as error: | ||||
status = 128 | ||||
stdout.write('ERROR: %s\n' % str(error)) | ||||
return HookResponse(status, stdout.getvalue()) | ||||
def git_post_pull(extras): | ||||
""" | ||||
Post pull hook. | ||||
:param extras: dictionary containing the keys defined in simplevcs | ||||
:type extras: dict | ||||
:return: status code of the hook. 0 for success. | ||||
:rtype: int | ||||
""" | ||||
if 'pull' not in extras['hooks']: | ||||
return HookResponse(0, '') | ||||
stdout = io.BytesIO() | ||||
try: | ||||
status = _call_hook('post_pull', extras, GitMessageWriter(stdout)) | ||||
except Exception as error: | ||||
status = 128 | ||||
stdout.write('ERROR: %s\n' % error) | ||||
return HookResponse(status, stdout.getvalue()) | ||||
r170 | def _parse_git_ref_lines(revision_lines): | |||
rev_data = [] | ||||
for revision_line in revision_lines or []: | ||||
old_rev, new_rev, ref = revision_line.strip().split(' ') | ||||
ref_data = ref.split('/', 2) | ||||
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': ref_data[2], | ||||
}) | ||||
return rev_data | ||||
def git_pre_receive(unused_repo_path, revision_lines, env): | ||||
r0 | """ | |||
Pre push hook. | ||||
:param extras: dictionary containing the keys defined in simplevcs | ||||
:type extras: dict | ||||
:return: status code of the hook. 0 for success. | ||||
:rtype: int | ||||
""" | ||||
extras = json.loads(env['RC_SCM_DATA']) | ||||
r170 | rev_data = _parse_git_ref_lines(revision_lines) | |||
r0 | if 'push' not in extras['hooks']: | |||
return 0 | ||||
r170 | extras['commit_ids'] = rev_data | |||
r0 | return _call_hook('pre_push', extras, GitMessageWriter()) | |||
def _run_command(arguments): | ||||
""" | ||||
Run the specified command and return the stdout. | ||||
r170 | :param arguments: sequence of program arguments (including the program name) | |||
r0 | :type arguments: list[str] | |||
""" | ||||
# TODO(skreft): refactor this method and all the other similar ones. | ||||
# Probably this should be using subprocessio. | ||||
process = subprocess.Popen( | ||||
arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | ||||
r220 | stdout, stderr = process.communicate() | |||
r0 | ||||
if process.returncode != 0: | ||||
raise Exception( | ||||
r220 | 'Command %s exited with exit code %s: stderr:%s' % ( | |||
arguments, process.returncode, stderr)) | ||||
r0 | ||||
return stdout | ||||
def git_post_receive(unused_repo_path, revision_lines, env): | ||||
""" | ||||
Post push hook. | ||||
:param extras: dictionary containing the keys defined in simplevcs | ||||
:type extras: dict | ||||
:return: status code of the hook. 0 for success. | ||||
:rtype: int | ||||
""" | ||||
extras = json.loads(env['RC_SCM_DATA']) | ||||
if 'push' not in extras['hooks']: | ||||
return 0 | ||||
r170 | rev_data = _parse_git_ref_lines(revision_lines) | |||
r0 | ||||
git_revs = [] | ||||
# N.B.(skreft): it is ok to just call git, as git before calling a | ||||
# subcommand sets the PATH environment variable so that it point to the | ||||
# correct version of the git executable. | ||||
empty_commit_id = '0' * 40 | ||||
r223 | branches = [] | |||
tags = [] | ||||
r0 | for push_ref in rev_data: | |||
type_ = push_ref['type'] | ||||
r223 | ||||
r0 | if type_ == 'heads': | |||
if push_ref['old_rev'] == empty_commit_id: | ||||
r223 | # starting new branch case | |||
if push_ref['name'] not in branches: | ||||
branches.append(push_ref['name']) | ||||
r0 | ||||
# Fix up head revision if needed | ||||
cmd = ['git', 'show', 'HEAD'] | ||||
try: | ||||
_run_command(cmd) | ||||
except Exception: | ||||
cmd = ['git', 'symbolic-ref', 'HEAD', | ||||
'refs/heads/%s' % push_ref['name']] | ||||
r170 | print("Setting default branch to %s" % push_ref['name']) | |||
r0 | _run_command(cmd) | |||
cmd = ['git', 'for-each-ref', '--format=%(refname)', | ||||
'refs/heads/*'] | ||||
heads = _run_command(cmd) | ||||
heads = heads.replace(push_ref['ref'], '') | ||||
heads = ' '.join(head for head in heads.splitlines() if head) | ||||
cmd = ['git', 'log', '--reverse', '--pretty=format:%H', | ||||
'--', push_ref['new_rev'], '--not', heads] | ||||
git_revs.extend(_run_command(cmd).splitlines()) | ||||
elif push_ref['new_rev'] == empty_commit_id: | ||||
# delete branch case | ||||
git_revs.append('delete_branch=>%s' % push_ref['name']) | ||||
else: | ||||
r223 | if push_ref['name'] not in branches: | |||
branches.append(push_ref['name']) | ||||
r0 | cmd = ['git', 'log', | |||
'{old_rev}..{new_rev}'.format(**push_ref), | ||||
'--reverse', '--pretty=format:%H'] | ||||
git_revs.extend(_run_command(cmd).splitlines()) | ||||
elif type_ == 'tags': | ||||
r223 | if push_ref['name'] not in tags: | |||
tags.append(push_ref['name']) | ||||
r0 | git_revs.append('tag=>%s' % push_ref['name']) | |||
extras['commit_ids'] = git_revs | ||||
r223 | extras['new_refs'] = { | |||
'branches': branches, | ||||
'bookmarks': [], | ||||
'tags': tags, | ||||
} | ||||
r0 | ||||
if 'repo_size' in extras['hooks']: | ||||
try: | ||||
_call_hook('repo_size', extras, GitMessageWriter()) | ||||
except: | ||||
pass | ||||
return _call_hook('post_push', extras, GitMessageWriter()) | ||||