hooks.py
798 lines
| 24.4 KiB
| text/x-python
|
PythonLexer
/ vcsserver / hooks.py
r0 | # RhodeCode VCSServer provides access to different vcs backends via network. | |||
r1323 | # Copyright (C) 2014-2024 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 | ||||
r1204 | class HooksCeleryClient: | |||
TASK_TIMEOUT = 60 # time in seconds | ||||
r1324 | custom_opts = {} | |||
r0 | ||||
r1324 | def __init__(self, broker_url, result_backend, **kwargs): | |||
_celery_opts = { | ||||
'broker_url': broker_url, | ||||
'result_backend': result_backend, | ||||
r1205 | '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'] | |||
r1324 | } | |||
if custom_opts := kwargs.pop('_celery_opts', {}): | ||||
_celery_opts.update(custom_opts) | ||||
self.custom_opts = custom_opts | ||||
celery_app.config_from_object(_celery_opts) | ||||
r1204 | self.celery_app = celery_app | |||
def __call__(self, method, extras): | ||||
r1323 | # NOTE: exception handling for those tasks executed is in | |||
# @adapt_for_celery decorator | ||||
# also see: _maybe_handle_exception which is handling exceptions | ||||
r1324 | ||||
inquired_task = self.celery_app.signature(f'rhodecode.lib.celerylib.tasks.{method}') | ||||
task_meta = inquired_task.apply_async(args=(extras,)) | ||||
result = task_meta.get(timeout=self.TASK_TIMEOUT) | ||||
r1296 | ||||
return result | ||||
r0 | ||||
r1324 | def __repr__(self): | |||
return f'HooksCeleryClient(opts={self.custom_opts})' | ||||
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): | |||
r1323 | args = (message.encode('utf-8'),) | |||
self.ui._writemsg(self.ui._fmsgerr, type=b'status', *args) | ||||
r0 | ||||
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): | |||
r1323 | self.stdout.write(message + "\n" if message else "") | |||
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 | ||||
r1323 | def _maybe_handle_exception(writer, result): | |||
""" | ||||
adopt_for_celery defines the exception/exception_traceback | ||||
Ths result is a direct output from a celery task | ||||
""" | ||||
r1315 | ||||
r0 | exception_class = result.get('exception') | |||
r171 | exception_traceback = result.get('exception_traceback') | |||
r1315 | ||||
r1323 | match exception_class: | |||
# NOTE: the underlying exceptions are setting _vcs_kind special marker | ||||
# which is later handled by `handle_vcs_exception` and translated into a special HTTP exception | ||||
# propagated later to the client | ||||
case 'HTTPLockedRepo': | ||||
raise exceptions.LockedRepoException()(*result['exception_args']) | ||||
case 'ClientNotSupported': | ||||
raise exceptions.ClientNotSupportedException()(*result['exception_args']) | ||||
case 'HTTPBranchProtected': | ||||
raise exceptions.RepositoryBranchProtectedException()(*result['exception_args']) | ||||
case 'RepositoryError': | ||||
raise exceptions.VcsException()(*result['exception_args']) | ||||
case _: | ||||
if exception_class: | ||||
r1325 | # level here should be info/debug so the remote client wouldn't see this as a stderr message | |||
log.info('ERROR: Handling hook-call exception. Got traceback from remote call:%s', exception_traceback) | ||||
r1323 | raise Exception( | |||
f"""Got remote exception "{exception_class}" with args "{result['exception_args']}" """ | ||||
) | ||||
r0 | ||||
def _get_hooks_client(extras): | ||||
r780 | is_shadow_repo = extras.get('is_shadow_repo') | |||
r1324 | hooks_protocol = extras.get('hooks_protocol') | |||
r1118 | ||||
r1324 | if hooks_protocol == 'celery': | |||
try: | ||||
celery_config = extras['hooks_config'] | ||||
broker_url = celery_config['broker_url'] | ||||
result_backend = celery_config['result_backend'] | ||||
except Exception: | ||||
log.exception("Failed to get celery task queue and backend") | ||||
raise | ||||
return HooksCeleryClient(broker_url, result_backend, _celery_opts=celery_config) | ||||
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) | |||
r1323 | _maybe_handle_exception(writer, 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()) | ||||