hooks.py
824 lines
| 24.8 KiB
| text/x-python
|
PythonLexer
/ vcsserver / hooks.py
r0 | # RhodeCode VCSServer provides access to different vcs backends via network. | |||
r1126 | # Copyright (C) 2014-2023 RhodeCode 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 | |||
r276 | import os | |||
r171 | import sys | |||
import logging | ||||
r0 | import collections | |||
r407 | import base64 | |||
r1048 | import msgpack | |||
r1108 | import dataclasses | |||
import pygit2 | ||||
r171 | ||||
r1110 | import http.client | |||
r1204 | from celery import Celery | |||
r0 | ||||
import mercurial.scmutil | ||||
import mercurial.node | ||||
r1261 | from vcsserver import exceptions, subprocessio, settings | |||
r1243 | from vcsserver.lib.ext_json import json | |||
r1249 | from vcsserver.lib.str_utils import ascii_str, safe_str | |||
r1261 | from vcsserver.lib.svn_txn_utils import get_txn_id_from_store | |||
r1145 | from vcsserver.remote.git_remote import Repository | |||
r0 | ||||
r1205 | celery_app = Celery('__vcsserver__') | |||
r171 | log = logging.getLogger(__name__) | |||
r0 | ||||
r1152 | class HooksHttpClient: | |||
r1048 | proto = 'msgpack.v1' | |||
r0 | connection = None | |||
def __init__(self, hooks_uri): | ||||
self.hooks_uri = hooks_uri | ||||
r1110 | def __repr__(self): | |||
return f'{self.__class__}(hook_uri={self.hooks_uri}, proto={self.proto})' | ||||
r0 | def __call__(self, method, extras): | |||
r1110 | connection = http.client.HTTPConnection(self.hooks_uri) | |||
r1048 | # binary msgpack body | |||
headers, body = self._serialize(method, extras) | ||||
r1110 | log.debug('Doing a new hooks call using HTTPConnection to %s', self.hooks_uri) | |||
r591 | try: | |||
r1110 | try: | |||
connection.request('POST', '/', body, headers) | ||||
except Exception as error: | ||||
log.error('Hooks calling Connection failed on %s, org error: %s', connection.__dict__, error) | ||||
raise | ||||
response = connection.getresponse() | ||||
try: | ||||
return msgpack.load(response) | ||||
except Exception: | ||||
response_data = response.read() | ||||
log.exception('Failed to decode hook response json data. ' | ||||
'response_code:%s, raw_data:%s', | ||||
response.status, response_data) | ||||
raise | ||||
finally: | ||||
connection.close() | ||||
r0 | ||||
r1048 | @classmethod | |||
def _serialize(cls, hook_name, extras): | ||||
r0 | data = { | |||
'method': hook_name, | ||||
'extras': extras | ||||
} | ||||
r1048 | headers = { | |||
r1110 | "rc-hooks-protocol": cls.proto, | |||
"Connection": "keep-alive" | ||||
r1048 | } | |||
return headers, msgpack.packb(data) | ||||
r0 | ||||
r1204 | class HooksCeleryClient: | |||
TASK_TIMEOUT = 60 # time in seconds | ||||
r0 | ||||
r1204 | def __init__(self, queue, backend): | |||
r1205 | celery_app.config_from_object({ | |||
'broker_url': queue, 'result_backend': backend, | ||||
'broker_connection_retry_on_startup': True, | ||||
r1235 | 'task_serializer': 'json', | |||
r1205 | 'accept_content': ['json', 'msgpack'], | |||
r1235 | 'result_serializer': 'json', | |||
r1205 | 'result_accept_content': ['json', 'msgpack'] | |||
}) | ||||
r1204 | self.celery_app = celery_app | |||
def __call__(self, method, extras): | ||||
inquired_task = self.celery_app.signature( | ||||
f'rhodecode.lib.celerylib.tasks.{method}' | ||||
) | ||||
return inquired_task.delay(extras).get(timeout=self.TASK_TIMEOUT) | ||||
r0 | ||||
r1152 | class HooksShadowRepoClient: | |||
r780 | ||||
def __call__(self, hook_name, extras): | ||||
return {'output': '', 'status': 0} | ||||
r1152 | class RemoteMessageWriter: | |||
r0 | """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 | ||||
r1108 | def write(self, message: str): | |||
r0 | # 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 | ||||
r1108 | def write(self, message: str): | |||
self.stdout.write(message) | ||||
r0 | ||||
r407 | class SvnMessageWriter(RemoteMessageWriter): | |||
"""Writer that knows how to send messages to svn clients.""" | ||||
def __init__(self, stderr=None): | ||||
# SVN needs data sent to stderr for back-to-client messaging | ||||
self.stderr = stderr or sys.stderr | ||||
def write(self, message): | ||||
r1214 | self.stderr.write(message) | |||
r407 | ||||
r0 | def _handle_exception(result): | |||
exception_class = result.get('exception') | ||||
r171 | exception_traceback = result.get('exception_traceback') | |||
r1118 | log.debug('Handling hook-call exception: %s', exception_class) | |||
r171 | ||||
if exception_traceback: | ||||
log.error('Got traceback from remote call:%s', exception_traceback) | ||||
r0 | if exception_class == 'HTTPLockedRC': | |||
r490 | raise exceptions.RepositoryLockedException()(*result['exception_args']) | |||
r1283 | elif exception_class == 'ClientNotSupportedError': | |||
raise exceptions.ClientNotSupportedException()(*result['exception_args']) | ||||
r509 | elif exception_class == 'HTTPBranchProtected': | |||
raise exceptions.RepositoryBranchProtectedException()(*result['exception_args']) | ||||
r0 | elif exception_class == 'RepositoryError': | |||
r490 | raise exceptions.VcsException()(*result['exception_args']) | |||
r0 | elif exception_class: | |||
r1108 | raise Exception( | |||
f"""Got remote exception "{exception_class}" with args "{result['exception_args']}" """ | ||||
) | ||||
r0 | ||||
def _get_hooks_client(extras): | ||||
r780 | hooks_uri = extras.get('hooks_uri') | |||
r1204 | task_queue = extras.get('task_queue') | |||
task_backend = extras.get('task_backend') | ||||
r780 | is_shadow_repo = extras.get('is_shadow_repo') | |||
r1118 | ||||
r780 | if hooks_uri: | |||
r1204 | return HooksHttpClient(hooks_uri) | |||
elif task_queue and task_backend: | ||||
return HooksCeleryClient(task_queue, task_backend) | ||||
r780 | elif is_shadow_repo: | |||
return HooksShadowRepoClient() | ||||
r0 | else: | |||
r1204 | raise Exception("Hooks client not found!") | |||
r0 | ||||
def _call_hook(hook_name, extras, writer): | ||||
r407 | hooks_client = _get_hooks_client(extras) | |||
log.debug('Hooks, using client:%s', hooks_client) | ||||
result = hooks_client(hook_name, extras) | ||||
r276 | log.debug('Hooks got result: %s', result) | |||
r529 | _handle_exception(result) | |||
r0 | writer.write(result['output']) | |||
return result['status'] | ||||
def _extras_from_ui(ui): | ||||
r1052 | hook_data = ui.config(b'rhodecode', b'RC_SCM_DATA') | |||
r276 | if not hook_data: | |||
# maybe it's inside environ ? | ||||
r333 | env_hook_data = os.environ.get('RC_SCM_DATA') | |||
if env_hook_data: | ||||
hook_data = env_hook_data | ||||
r334 | extras = {} | |||
if hook_data: | ||||
extras = json.loads(hook_data) | ||||
r0 | return extras | |||
r509 | def _rev_range_hash(repo, node, check_heads=False): | |||
r802 | from vcsserver.hgcompat import get_ctx | |||
r223 | ||||
commits = [] | ||||
r509 | revs = [] | |||
r660 | start = get_ctx(repo, node).rev() | |||
r509 | end = len(repo) | |||
for rev in range(start, end): | ||||
revs.append(rev) | ||||
r660 | ctx = get_ctx(repo, rev) | |||
r1108 | commit_id = ascii_str(mercurial.node.hex(ctx.node())) | |||
branch = safe_str(ctx.branch()) | ||||
r223 | commits.append((commit_id, branch)) | |||
r509 | parent_heads = [] | |||
if check_heads: | ||||
parent_heads = _check_heads(repo, start, end, revs) | ||||
return commits, parent_heads | ||||
def _check_heads(repo, start, end, commits): | ||||
r802 | from vcsserver.hgcompat import get_ctx | |||
r509 | changelog = repo.changelog | |||
parents = set() | ||||
for new_rev in commits: | ||||
for p in changelog.parentrevs(new_rev): | ||||
if p == mercurial.node.nullrev: | ||||
continue | ||||
if p < start: | ||||
parents.add(p) | ||||
for p in parents: | ||||
r660 | branch = get_ctx(repo, p).branch() | |||
r509 | # The heads descending from that parent, on the same branch | |||
r1108 | parent_heads = {p} | |||
reachable = {p} | ||||
r982 | for x in range(p + 1, end): | |||
r660 | if get_ctx(repo, x).branch() != branch: | |||
r509 | continue | |||
for pp in changelog.parentrevs(x): | ||||
if pp in reachable: | ||||
reachable.add(x) | ||||
parent_heads.discard(pp) | ||||
parent_heads.add(x) | ||||
# More than one head? Suggest merging | ||||
if len(parent_heads) > 1: | ||||
return list(parent_heads) | ||||
return [] | ||||
r223 | ||||
r557 | def _get_git_env(): | |||
env = {} | ||||
for k, v in os.environ.items(): | ||||
if k.startswith('GIT'): | ||||
env[k] = v | ||||
# serialized version | ||||
return [(k, v) for k, v in env.items()] | ||||
def _get_hg_env(old_rev, new_rev, txnid, repo_path): | ||||
env = {} | ||||
for k, v in os.environ.items(): | ||||
if k.startswith('HG'): | ||||
env[k] = v | ||||
env['HG_NODE'] = old_rev | ||||
env['HG_NODE_LAST'] = new_rev | ||||
env['HG_TXNID'] = txnid | ||||
env['HG_PENDING'] = repo_path | ||||
return [(k, v) for k, v in env.items()] | ||||
r1261 | def _get_ini_settings(ini_file): | |||
from vcsserver.http_main import sanitize_settings_and_apply_defaults | ||||
from vcsserver.lib.config_utils import get_app_config_lightweight, configure_and_store_settings | ||||
global_config = {'__file__': ini_file} | ||||
ini_settings = get_app_config_lightweight(ini_file) | ||||
sanitize_settings_and_apply_defaults(global_config, ini_settings) | ||||
configure_and_store_settings(global_config, ini_settings) | ||||
return ini_settings | ||||
r1230 | def _fix_hooks_executables(ini_path=''): | |||
r1214 | """ | |||
This is a trick to set proper settings.EXECUTABLE paths for certain execution patterns | ||||
especially for subversion where hooks strip entire env, and calling just 'svn' command will most likely fail | ||||
because svn is not on PATH | ||||
""" | ||||
r1261 | # set defaults, in case we can't read from ini_file | |||
r1230 | core_binary_dir = settings.BINARY_DIR or '/usr/local/bin/rhodecode_bin/vcs_bin' | |||
if ini_path: | ||||
r1261 | ini_settings = _get_ini_settings(ini_path) | |||
r1230 | core_binary_dir = ini_settings['core.binary_dir'] | |||
settings.BINARY_DIR = core_binary_dir | ||||
r1214 | ||||
r276 | def repo_size(ui, repo, **kwargs): | |||
extras = _extras_from_ui(ui) | ||||
return _call_hook('repo_size', extras, HgMessageWriter(ui)) | ||||
def pre_pull(ui, repo, **kwargs): | ||||
extras = _extras_from_ui(ui) | ||||
return _call_hook('pre_pull', extras, HgMessageWriter(ui)) | ||||
def pre_pull_ssh(ui, repo, **kwargs): | ||||
r334 | extras = _extras_from_ui(ui) | |||
if extras and extras.get('SSH'): | ||||
r276 | return pre_pull(ui, repo, **kwargs) | |||
return 0 | ||||
def post_pull(ui, repo, **kwargs): | ||||
extras = _extras_from_ui(ui) | ||||
return _call_hook('post_pull', extras, HgMessageWriter(ui)) | ||||
def post_pull_ssh(ui, repo, **kwargs): | ||||
r334 | extras = _extras_from_ui(ui) | |||
if extras and extras.get('SSH'): | ||||
r276 | return post_pull(ui, repo, **kwargs) | |||
return 0 | ||||
r170 | def pre_push(ui, repo, node=None, **kwargs): | |||
r509 | """ | |||
Mercurial pre_push hook | ||||
""" | ||||
r170 | extras = _extras_from_ui(ui) | |||
r509 | detect_force_push = extras.get('detect_force_push') | |||
r170 | ||||
rev_data = [] | ||||
r1108 | hook_type: str = safe_str(kwargs.get('hooktype')) | |||
if node and hook_type == 'pretxnchangegroup': | ||||
r170 | branches = collections.defaultdict(list) | |||
r509 | commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push) | |||
for commit_id, branch in commits: | ||||
r170 | branches[branch].append(commit_id) | |||
r509 | for branch, commits in branches.items(): | |||
r1108 | old_rev = ascii_str(kwargs.get('node_last')) or commits[0] | |||
r170 | rev_data.append({ | |||
r557 | 'total_commits': len(commits), | |||
r170 | 'old_rev': old_rev, | |||
'new_rev': commits[-1], | ||||
'ref': '', | ||||
'type': 'branch', | ||||
'name': branch, | ||||
}) | ||||
r509 | for push_ref in rev_data: | |||
push_ref['multiple_heads'] = _heads | ||||
r557 | repo_path = os.path.join( | |||
extras.get('repo_store', ''), extras.get('repository', '')) | ||||
push_ref['hg_env'] = _get_hg_env( | ||||
old_rev=push_ref['old_rev'], | ||||
r1108 | new_rev=push_ref['new_rev'], txnid=ascii_str(kwargs.get('txnid')), | |||
r557 | repo_path=repo_path) | |||
r1108 | extras['hook_type'] = hook_type or 'pre_push' | |||
r170 | extras['commit_ids'] = rev_data | |||
r557 | ||||
r170 | return _call_hook('pre_push', extras, HgMessageWriter(ui)) | |||
r0 | ||||
r276 | def pre_push_ssh(ui, repo, node=None, **kwargs): | |||
r510 | extras = _extras_from_ui(ui) | |||
if extras.get('SSH'): | ||||
r276 | return pre_push(ui, repo, node, **kwargs) | |||
return 0 | ||||
def pre_push_ssh_auth(ui, repo, node=None, **kwargs): | ||||
r509 | """ | |||
Mercurial pre_push hook for SSH | ||||
""" | ||||
r276 | extras = _extras_from_ui(ui) | |||
if extras.get('SSH'): | ||||
permission = extras['SSH_PERMISSIONS'] | ||||
if 'repository.write' == permission or 'repository.admin' == permission: | ||||
return 0 | ||||
# non-zero ret code | ||||
return 1 | ||||
return 0 | ||||
r223 | def post_push(ui, repo, node, **kwargs): | |||
r509 | """ | |||
Mercurial post_push hook | ||||
""" | ||||
r223 | extras = _extras_from_ui(ui) | |||
commit_ids = [] | ||||
branches = [] | ||||
bookmarks = [] | ||||
tags = [] | ||||
r1108 | hook_type: str = safe_str(kwargs.get('hooktype')) | |||
r0 | ||||
r509 | commits, _heads = _rev_range_hash(repo, node) | |||
for commit_id, branch in commits: | ||||
r223 | commit_ids.append(commit_id) | |||
if branch not in branches: | ||||
branches.append(branch) | ||||
r0 | ||||
r1108 | if hasattr(ui, '_rc_pushkey_bookmarks'): | |||
bookmarks = ui._rc_pushkey_bookmarks | ||||
r0 | ||||
r1108 | extras['hook_type'] = hook_type or 'post_push' | |||
r0 | extras['commit_ids'] = commit_ids | |||
r1108 | ||||
r223 | extras['new_refs'] = { | |||
'branches': branches, | ||||
'bookmarks': bookmarks, | ||||
'tags': tags | ||||
} | ||||
r0 | ||||
return _call_hook('post_push', extras, HgMessageWriter(ui)) | ||||
r276 | def post_push_ssh(ui, repo, node, **kwargs): | |||
r509 | """ | |||
Mercurial post_push hook for SSH | ||||
""" | ||||
r276 | if _extras_from_ui(ui).get('SSH'): | |||
return post_push(ui, repo, node, **kwargs) | ||||
return 0 | ||||
r221 | def key_push(ui, repo, **kwargs): | |||
r802 | from vcsserver.hgcompat import get_ctx | |||
r1108 | ||||
if kwargs['new'] != b'0' and kwargs['namespace'] == b'bookmarks': | ||||
r221 | # store new bookmarks in our UI object propagated later to post_push | |||
r1108 | ui._rc_pushkey_bookmarks = get_ctx(repo, kwargs['key']).bookmarks() | |||
r221 | return | |||
r276 | ||||
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 | ||||
r1108 | @dataclasses.dataclass | |||
class HookResponse: | ||||
status: int | ||||
output: str | ||||
r0 | ||||
r1108 | def git_pre_pull(extras) -> HookResponse: | |||
r0 | """ | |||
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 | ||||
""" | ||||
r1048 | ||||
r0 | if 'pull' not in extras['hooks']: | |||
return HookResponse(0, '') | ||||
r1108 | stdout = io.StringIO() | |||
r0 | try: | |||
r1108 | status_code = _call_hook('pre_pull', extras, GitMessageWriter(stdout)) | |||
r1048 | ||||
r0 | except Exception as error: | |||
r1048 | log.exception('Failed to call pre_pull hook') | |||
r1108 | status_code = 128 | |||
stdout.write(f'ERROR: {error}\n') | ||||
r0 | ||||
r1108 | return HookResponse(status_code, stdout.getvalue()) | |||
r0 | ||||
r1108 | def git_post_pull(extras) -> HookResponse: | |||
r0 | """ | |||
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, '') | ||||
r1108 | stdout = io.StringIO() | |||
r0 | try: | |||
status = _call_hook('post_pull', extras, GitMessageWriter(stdout)) | ||||
except Exception as error: | ||||
status = 128 | ||||
r1108 | stdout.write(f'ERROR: {error}\n') | |||
r0 | ||||
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({ | ||||
r557 | # NOTE(marcink): | |||
# we're unable to tell total_commits for git at this point | ||||
# but we set the variable for consistency with GIT | ||||
'total_commits': -1, | ||||
r170 | 'old_rev': old_rev, | |||
'new_rev': new_rev, | ||||
'ref': ref, | ||||
'type': ref_data[1], | ||||
'name': ref_data[2], | ||||
}) | ||||
return rev_data | ||||
r1108 | def git_pre_receive(unused_repo_path, revision_lines, env) -> int: | |||
r0 | """ | |||
Pre push hook. | ||||
:return: status code of the hook. 0 for success. | ||||
""" | ||||
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 | ||||
r1261 | _fix_hooks_executables(env.get('RC_INI_FILE')) | |||
r1230 | ||||
r509 | empty_commit_id = '0' * 40 | |||
detect_force_push = extras.get('detect_force_push') | ||||
r1230 | ||||
r509 | for push_ref in rev_data: | |||
r510 | # store our git-env which holds the temp store | |||
r557 | push_ref['git_env'] = _get_git_env() | |||
r509 | push_ref['pruned_sha'] = '' | |||
if not detect_force_push: | ||||
# don't check for forced-push when we don't need to | ||||
continue | ||||
type_ = push_ref['type'] | ||||
new_branch = push_ref['old_rev'] == empty_commit_id | ||||
r643 | delete_branch = push_ref['new_rev'] == empty_commit_id | |||
if type_ == 'heads' and not (new_branch or delete_branch): | ||||
r509 | old_rev = push_ref['old_rev'] | |||
new_rev = push_ref['new_rev'] | ||||
r1230 | cmd = [settings.GIT_EXECUTABLE(), 'rev-list', old_rev, f'^{new_rev}'] | |||
r509 | stdout, stderr = subprocessio.run_command( | |||
cmd, env=os.environ.copy()) | ||||
r643 | # means we're having some non-reachable objects, this forced push was used | |||
r509 | if stdout: | |||
push_ref['pruned_sha'] = stdout.splitlines() | ||||
r555 | extras['hook_type'] = 'pre_receive' | |||
r170 | extras['commit_ids'] = rev_data | |||
r1108 | ||||
stdout = sys.stdout | ||||
status_code = _call_hook('pre_push', extras, GitMessageWriter(stdout)) | ||||
return status_code | ||||
r0 | ||||
r1108 | def git_post_receive(unused_repo_path, revision_lines, env) -> int: | |||
r0 | """ | |||
Post push hook. | ||||
:return: status code of the hook. 0 for success. | ||||
""" | ||||
extras = json.loads(env['RC_SCM_DATA']) | ||||
if 'push' not in extras['hooks']: | ||||
return 0 | ||||
r1230 | ||||
r1261 | _fix_hooks_executables(env.get('RC_INI_FILE')) | |||
r0 | ||||
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': | |||
r1108 | # starting new branch case | |||
r0 | if push_ref['old_rev'] == empty_commit_id: | |||
r1108 | push_ref_name = push_ref['name'] | |||
if push_ref_name not in branches: | ||||
branches.append(push_ref_name) | ||||
r0 | ||||
r1108 | need_head_set = '' | |||
with Repository(os.getcwd()) as repo: | ||||
try: | ||||
repo.head | ||||
except pygit2.GitError: | ||||
need_head_set = f'refs/heads/{push_ref_name}' | ||||
r0 | ||||
r1108 | if need_head_set: | |||
repo.set_head(need_head_set) | ||||
print(f"Setting default branch to {push_ref_name}") | ||||
r1230 | cmd = [settings.GIT_EXECUTABLE(), 'for-each-ref', '--format=%(refname)', 'refs/heads/*'] | |||
r370 | stdout, stderr = subprocessio.run_command( | |||
cmd, env=os.environ.copy()) | ||||
r1108 | heads = safe_str(stdout) | |||
r0 | heads = heads.replace(push_ref['ref'], '') | |||
r434 | heads = ' '.join(head for head | |||
in heads.splitlines() if head) or '.' | ||||
r1230 | cmd = [settings.GIT_EXECUTABLE(), 'log', '--reverse', | |||
r370 | '--pretty=format:%H', '--', push_ref['new_rev'], | |||
'--not', heads] | ||||
stdout, stderr = subprocessio.run_command( | ||||
cmd, env=os.environ.copy()) | ||||
r1108 | git_revs.extend(list(map(ascii_str, stdout.splitlines()))) | |||
# delete branch case | ||||
r0 | elif push_ref['new_rev'] == empty_commit_id: | |||
r1152 | git_revs.append(f'delete_branch=>{push_ref["name"]}') | |||
r0 | else: | |||
r223 | if push_ref['name'] not in branches: | |||
branches.append(push_ref['name']) | ||||
r1230 | cmd = [settings.GIT_EXECUTABLE(), 'log', | |||
r1154 | f'{push_ref["old_rev"]}..{push_ref["new_rev"]}', | |||
r0 | '--reverse', '--pretty=format:%H'] | |||
r370 | stdout, stderr = subprocessio.run_command( | |||
cmd, env=os.environ.copy()) | ||||
r1108 | # we get bytes from stdout, we need str to be consistent | |||
log_revs = list(map(ascii_str, stdout.splitlines())) | ||||
git_revs.extend(log_revs) | ||||
# Pure pygit2 impl. but still 2-3x slower :/ | ||||
# results = [] | ||||
# | ||||
# with Repository(os.getcwd()) as repo: | ||||
# repo_new_rev = repo[push_ref['new_rev']] | ||||
# repo_old_rev = repo[push_ref['old_rev']] | ||||
# walker = repo.walk(repo_new_rev.id, pygit2.GIT_SORT_TOPOLOGICAL) | ||||
# | ||||
# for commit in walker: | ||||
# if commit.id == repo_old_rev.id: | ||||
# break | ||||
# results.append(commit.id.hex) | ||||
# # reverse the order, can't use GIT_SORT_REVERSE | ||||
# log_revs = results[::-1] | ||||
r0 | elif type_ == 'tags': | |||
r223 | if push_ref['name'] not in tags: | |||
tags.append(push_ref['name']) | ||||
r1152 | git_revs.append(f'tag=>{push_ref["name"]}') | |||
r0 | ||||
r555 | extras['hook_type'] = 'post_receive' | |||
r0 | extras['commit_ids'] = git_revs | |||
r223 | extras['new_refs'] = { | |||
'branches': branches, | ||||
'bookmarks': [], | ||||
'tags': tags, | ||||
} | ||||
r0 | ||||
r1108 | stdout = sys.stdout | |||
r0 | if 'repo_size' in extras['hooks']: | |||
try: | ||||
r1108 | _call_hook('repo_size', extras, GitMessageWriter(stdout)) | |||
r1095 | except Exception: | |||
r0 | pass | |||
r1108 | status_code = _call_hook('post_push', extras, GitMessageWriter(stdout)) | |||
return status_code | ||||
r407 | ||||
r1261 | def get_extras_from_txn_id(repo_path, txn_id): | |||
extras = get_txn_id_from_store(repo_path, txn_id) | ||||
r557 | return extras | |||
r407 | def svn_pre_commit(repo_path, commit_data, env): | |||
r1230 | ||||
r407 | path, txn_id = commit_data | |||
branches = [] | ||||
tags = [] | ||||
r436 | if env.get('RC_SCM_DATA'): | |||
extras = json.loads(env['RC_SCM_DATA']) | ||||
else: | ||||
r1261 | ini_path = env.get('RC_INI_FILE') | |||
if ini_path: | ||||
_get_ini_settings(ini_path) | ||||
r436 | # fallback method to read from TXN-ID stored data | |||
r1261 | extras = get_extras_from_txn_id(path, txn_id) | |||
r1231 | ||||
if not extras: | ||||
r1261 | raise ValueError('SVN-PRE-COMMIT: Failed to extract context data in called extras for hook execution') | |||
if extras.get('rc_internal_commit'): | ||||
# special marker for internal commit, we don't call hooks client | ||||
r1234 | return 0 | |||
r407 | ||||
r575 | extras['hook_type'] = 'pre_commit' | |||
r670 | extras['commit_ids'] = [txn_id] | |||
r407 | extras['txn_id'] = txn_id | |||
extras['new_refs'] = { | ||||
r557 | 'total_commits': 1, | |||
r407 | 'branches': branches, | |||
'bookmarks': [], | ||||
'tags': tags, | ||||
} | ||||
r436 | ||||
r407 | return _call_hook('pre_push', extras, SvnMessageWriter()) | |||
def svn_post_commit(repo_path, commit_data, env): | ||||
""" | ||||
commit_data is path, rev, txn_id | ||||
""" | ||||
r1214 | ||||
r824 | if len(commit_data) == 3: | |||
path, commit_id, txn_id = commit_data | ||||
elif len(commit_data) == 2: | ||||
log.error('Failed to extract txn_id from commit_data using legacy method. ' | ||||
'Some functionality might be limited') | ||||
path, commit_id = commit_data | ||||
txn_id = None | ||||
r1214 | else: | |||
return 0 | ||||
r824 | ||||
r407 | branches = [] | |||
tags = [] | ||||
r436 | if env.get('RC_SCM_DATA'): | |||
extras = json.loads(env['RC_SCM_DATA']) | ||||
else: | ||||
r1261 | ini_path = env.get('RC_INI_FILE') | |||
if ini_path: | ||||
_get_ini_settings(ini_path) | ||||
r436 | # fallback method to read from TXN-ID stored data | |||
r1261 | extras = get_extras_from_txn_id(path, txn_id) | |||
r1231 | ||||
r1261 | if not extras and txn_id: | |||
raise ValueError('SVN-POST-COMMIT: Failed to extract context data in called extras for hook execution') | ||||
if extras.get('rc_internal_commit'): | ||||
# special marker for internal commit, we don't call hooks client | ||||
r1234 | return 0 | |||
r407 | ||||
r575 | extras['hook_type'] = 'post_commit' | |||
r407 | extras['commit_ids'] = [commit_id] | |||
extras['txn_id'] = txn_id | ||||
extras['new_refs'] = { | ||||
'branches': branches, | ||||
'bookmarks': [], | ||||
'tags': tags, | ||||
r557 | 'total_commits': 1, | |||
r407 | } | |||
if 'repo_size' in extras['hooks']: | ||||
try: | ||||
_call_hook('repo_size', extras, SvnMessageWriter()) | ||||
r436 | except Exception: | |||
r407 | pass | |||
return _call_hook('post_push', extras, SvnMessageWriter()) | ||||