##// END OF EJS Templates
hooks: cleanups hooks protocol code, and ensure we can propagate options from CE into VCS
super-admin -
r1324:a8f7c36d default
parent child Browse files
Show More
@@ -1,782 +1,797 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2024 RhodeCode GmbH
2 # Copyright (C) 2014-2024 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import io
18 import io
19 import os
19 import os
20 import sys
20 import sys
21 import logging
21 import logging
22 import collections
22 import collections
23 import base64
23 import base64
24 import msgpack
24 import msgpack
25 import dataclasses
25 import dataclasses
26 import pygit2
26 import pygit2
27
27
28 import http.client
28 import http.client
29 from celery import Celery
29 from celery import Celery
30
30
31 import mercurial.scmutil
31 import mercurial.scmutil
32 import mercurial.node
32 import mercurial.node
33
33
34 from vcsserver import exceptions, subprocessio, settings
34 from vcsserver import exceptions, subprocessio, settings
35 from vcsserver.lib.ext_json import json
35 from vcsserver.lib.ext_json import json
36 from vcsserver.lib.str_utils import ascii_str, safe_str
36 from vcsserver.lib.str_utils import ascii_str, safe_str
37 from vcsserver.lib.svn_txn_utils import get_txn_id_from_store
37 from vcsserver.lib.svn_txn_utils import get_txn_id_from_store
38 from vcsserver.remote.git_remote import Repository
38 from vcsserver.remote.git_remote import Repository
39
39
40 celery_app = Celery('__vcsserver__')
40 celery_app = Celery('__vcsserver__')
41 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
42
42
43
43
44 class HooksCeleryClient:
44 class HooksCeleryClient:
45 TASK_TIMEOUT = 60 # time in seconds
45 TASK_TIMEOUT = 60 # time in seconds
46 custom_opts = {}
46
47
47 def __init__(self, queue, backend):
48 def __init__(self, broker_url, result_backend, **kwargs):
48 celery_app.config_from_object({
49 _celery_opts = {
49 'broker_url': queue, 'result_backend': backend,
50 'broker_url': broker_url,
51 'result_backend': result_backend,
50 'broker_connection_retry_on_startup': True,
52 'broker_connection_retry_on_startup': True,
51 'task_serializer': 'json',
53 'task_serializer': 'json',
52 'accept_content': ['json', 'msgpack'],
54 'accept_content': ['json', 'msgpack'],
53 'result_serializer': 'json',
55 'result_serializer': 'json',
54 'result_accept_content': ['json', 'msgpack']
56 'result_accept_content': ['json', 'msgpack']
55 })
57 }
58 if custom_opts := kwargs.pop('_celery_opts', {}):
59 _celery_opts.update(custom_opts)
60 self.custom_opts = custom_opts
61 celery_app.config_from_object(_celery_opts)
62
56 self.celery_app = celery_app
63 self.celery_app = celery_app
57
64
58 def __call__(self, method, extras):
65 def __call__(self, method, extras):
59 # NOTE: exception handling for those tasks executed is in
66 # NOTE: exception handling for those tasks executed is in
60 # @adapt_for_celery decorator
67 # @adapt_for_celery decorator
61 # also see: _maybe_handle_exception which is handling exceptions
68 # also see: _maybe_handle_exception which is handling exceptions
62 inquired_task = self.celery_app.signature(
69
63 f'rhodecode.lib.celerylib.tasks.{method}'
70 inquired_task = self.celery_app.signature(f'rhodecode.lib.celerylib.tasks.{method}')
64 )
71 task_meta = inquired_task.apply_async(args=(extras,))
65 result = inquired_task.delay(extras).get(timeout=self.TASK_TIMEOUT)
72 result = task_meta.get(timeout=self.TASK_TIMEOUT)
66
73
67 return result
74 return result
68
75
76 def __repr__(self):
77 return f'HooksCeleryClient(opts={self.custom_opts})'
69
78
70 class HooksShadowRepoClient:
79 class HooksShadowRepoClient:
71
80
72 def __call__(self, hook_name, extras):
81 def __call__(self, hook_name, extras):
73 return {'output': '', 'status': 0}
82 return {'output': '', 'status': 0}
74
83
75
84
76 class RemoteMessageWriter:
85 class RemoteMessageWriter:
77 """Writer base class."""
86 """Writer base class."""
78 def write(self, message):
87 def write(self, message):
79 raise NotImplementedError()
88 raise NotImplementedError()
80
89
81
90
82 class HgMessageWriter(RemoteMessageWriter):
91 class HgMessageWriter(RemoteMessageWriter):
83 """Writer that knows how to send messages to mercurial clients."""
92 """Writer that knows how to send messages to mercurial clients."""
84
93
85 def __init__(self, ui):
94 def __init__(self, ui):
86 self.ui = ui
95 self.ui = ui
87
96
88 def write(self, message: str):
97 def write(self, message: str):
89 args = (message.encode('utf-8'),)
98 args = (message.encode('utf-8'),)
90 self.ui._writemsg(self.ui._fmsgerr, type=b'status', *args)
99 self.ui._writemsg(self.ui._fmsgerr, type=b'status', *args)
91
100
92
101
93 class GitMessageWriter(RemoteMessageWriter):
102 class GitMessageWriter(RemoteMessageWriter):
94 """Writer that knows how to send messages to git clients."""
103 """Writer that knows how to send messages to git clients."""
95
104
96 def __init__(self, stdout=None):
105 def __init__(self, stdout=None):
97 self.stdout = stdout or sys.stdout
106 self.stdout = stdout or sys.stdout
98
107
99 def write(self, message: str):
108 def write(self, message: str):
100 self.stdout.write(message + "\n" if message else "")
109 self.stdout.write(message + "\n" if message else "")
101
110
102
111
103 class SvnMessageWriter(RemoteMessageWriter):
112 class SvnMessageWriter(RemoteMessageWriter):
104 """Writer that knows how to send messages to svn clients."""
113 """Writer that knows how to send messages to svn clients."""
105
114
106 def __init__(self, stderr=None):
115 def __init__(self, stderr=None):
107 # SVN needs data sent to stderr for back-to-client messaging
116 # SVN needs data sent to stderr for back-to-client messaging
108 self.stderr = stderr or sys.stderr
117 self.stderr = stderr or sys.stderr
109
118
110 def write(self, message):
119 def write(self, message):
111 self.stderr.write(message)
120 self.stderr.write(message)
112
121
113
122
114 def _maybe_handle_exception(writer, result):
123 def _maybe_handle_exception(writer, result):
115 """
124 """
116 adopt_for_celery defines the exception/exception_traceback
125 adopt_for_celery defines the exception/exception_traceback
117 Ths result is a direct output from a celery task
126 Ths result is a direct output from a celery task
118 """
127 """
119
128
120 exception_class = result.get('exception')
129 exception_class = result.get('exception')
121 exception_traceback = result.get('exception_traceback')
130 exception_traceback = result.get('exception_traceback')
122
131
123 match exception_class:
132 match exception_class:
124 # NOTE: the underlying exceptions are setting _vcs_kind special marker
133 # NOTE: the underlying exceptions are setting _vcs_kind special marker
125 # which is later handled by `handle_vcs_exception` and translated into a special HTTP exception
134 # which is later handled by `handle_vcs_exception` and translated into a special HTTP exception
126 # propagated later to the client
135 # propagated later to the client
127 case 'HTTPLockedRepo':
136 case 'HTTPLockedRepo':
128 raise exceptions.LockedRepoException()(*result['exception_args'])
137 raise exceptions.LockedRepoException()(*result['exception_args'])
129 case 'ClientNotSupported':
138 case 'ClientNotSupported':
130 raise exceptions.ClientNotSupportedException()(*result['exception_args'])
139 raise exceptions.ClientNotSupportedException()(*result['exception_args'])
131 case 'HTTPBranchProtected':
140 case 'HTTPBranchProtected':
132 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
141 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
133 case 'RepositoryError':
142 case 'RepositoryError':
134 raise exceptions.VcsException()(*result['exception_args'])
143 raise exceptions.VcsException()(*result['exception_args'])
135 case _:
144 case _:
136 if exception_class:
145 if exception_class:
137 log.error('Handling hook-call exception. Got traceback from remote call:%s', exception_traceback)
146 log.error('Handling hook-call exception. Got traceback from remote call:%s', exception_traceback)
138 raise Exception(
147 raise Exception(
139 f"""Got remote exception "{exception_class}" with args "{result['exception_args']}" """
148 f"""Got remote exception "{exception_class}" with args "{result['exception_args']}" """
140 )
149 )
141
150
142
151
143 def _get_hooks_client(extras):
152 def _get_hooks_client(extras):
144 task_queue = extras.get('task_queue')
145 task_backend = extras.get('task_backend')
146 is_shadow_repo = extras.get('is_shadow_repo')
153 is_shadow_repo = extras.get('is_shadow_repo')
154 hooks_protocol = extras.get('hooks_protocol')
147
155
148 if task_queue and task_backend:
156 if hooks_protocol == 'celery':
149 return HooksCeleryClient(task_queue, task_backend)
157 try:
158 celery_config = extras['hooks_config']
159 broker_url = celery_config['broker_url']
160 result_backend = celery_config['result_backend']
161 except Exception:
162 log.exception("Failed to get celery task queue and backend")
163 raise
164 return HooksCeleryClient(broker_url, result_backend, _celery_opts=celery_config)
150 elif is_shadow_repo:
165 elif is_shadow_repo:
151 return HooksShadowRepoClient()
166 return HooksShadowRepoClient()
152 else:
167 else:
153 raise Exception("Hooks client not found!")
168 raise Exception("Hooks client not found!")
154
169
155
170
156 def _call_hook(hook_name, extras, writer):
171 def _call_hook(hook_name, extras, writer):
157 hooks_client = _get_hooks_client(extras)
172 hooks_client = _get_hooks_client(extras)
158 log.debug('Hooks, using client:%s', hooks_client)
173 log.debug('Hooks, using client:%s', hooks_client)
159 result = hooks_client(hook_name, extras)
174 result = hooks_client(hook_name, extras)
160 log.debug('Hooks got result: %s', result)
175 log.debug('Hooks got result: %s', result)
161 _maybe_handle_exception(writer, result)
176 _maybe_handle_exception(writer, result)
162 writer.write(result['output'])
177 writer.write(result['output'])
163
178
164 return result['status']
179 return result['status']
165
180
166
181
167 def _extras_from_ui(ui):
182 def _extras_from_ui(ui):
168 hook_data = ui.config(b'rhodecode', b'RC_SCM_DATA')
183 hook_data = ui.config(b'rhodecode', b'RC_SCM_DATA')
169 if not hook_data:
184 if not hook_data:
170 # maybe it's inside environ ?
185 # maybe it's inside environ ?
171 env_hook_data = os.environ.get('RC_SCM_DATA')
186 env_hook_data = os.environ.get('RC_SCM_DATA')
172 if env_hook_data:
187 if env_hook_data:
173 hook_data = env_hook_data
188 hook_data = env_hook_data
174
189
175 extras = {}
190 extras = {}
176 if hook_data:
191 if hook_data:
177 extras = json.loads(hook_data)
192 extras = json.loads(hook_data)
178 return extras
193 return extras
179
194
180
195
181 def _rev_range_hash(repo, node, check_heads=False):
196 def _rev_range_hash(repo, node, check_heads=False):
182 from vcsserver.hgcompat import get_ctx
197 from vcsserver.hgcompat import get_ctx
183
198
184 commits = []
199 commits = []
185 revs = []
200 revs = []
186 start = get_ctx(repo, node).rev()
201 start = get_ctx(repo, node).rev()
187 end = len(repo)
202 end = len(repo)
188 for rev in range(start, end):
203 for rev in range(start, end):
189 revs.append(rev)
204 revs.append(rev)
190 ctx = get_ctx(repo, rev)
205 ctx = get_ctx(repo, rev)
191 commit_id = ascii_str(mercurial.node.hex(ctx.node()))
206 commit_id = ascii_str(mercurial.node.hex(ctx.node()))
192 branch = safe_str(ctx.branch())
207 branch = safe_str(ctx.branch())
193 commits.append((commit_id, branch))
208 commits.append((commit_id, branch))
194
209
195 parent_heads = []
210 parent_heads = []
196 if check_heads:
211 if check_heads:
197 parent_heads = _check_heads(repo, start, end, revs)
212 parent_heads = _check_heads(repo, start, end, revs)
198 return commits, parent_heads
213 return commits, parent_heads
199
214
200
215
201 def _check_heads(repo, start, end, commits):
216 def _check_heads(repo, start, end, commits):
202 from vcsserver.hgcompat import get_ctx
217 from vcsserver.hgcompat import get_ctx
203 changelog = repo.changelog
218 changelog = repo.changelog
204 parents = set()
219 parents = set()
205
220
206 for new_rev in commits:
221 for new_rev in commits:
207 for p in changelog.parentrevs(new_rev):
222 for p in changelog.parentrevs(new_rev):
208 if p == mercurial.node.nullrev:
223 if p == mercurial.node.nullrev:
209 continue
224 continue
210 if p < start:
225 if p < start:
211 parents.add(p)
226 parents.add(p)
212
227
213 for p in parents:
228 for p in parents:
214 branch = get_ctx(repo, p).branch()
229 branch = get_ctx(repo, p).branch()
215 # The heads descending from that parent, on the same branch
230 # The heads descending from that parent, on the same branch
216 parent_heads = {p}
231 parent_heads = {p}
217 reachable = {p}
232 reachable = {p}
218 for x in range(p + 1, end):
233 for x in range(p + 1, end):
219 if get_ctx(repo, x).branch() != branch:
234 if get_ctx(repo, x).branch() != branch:
220 continue
235 continue
221 for pp in changelog.parentrevs(x):
236 for pp in changelog.parentrevs(x):
222 if pp in reachable:
237 if pp in reachable:
223 reachable.add(x)
238 reachable.add(x)
224 parent_heads.discard(pp)
239 parent_heads.discard(pp)
225 parent_heads.add(x)
240 parent_heads.add(x)
226 # More than one head? Suggest merging
241 # More than one head? Suggest merging
227 if len(parent_heads) > 1:
242 if len(parent_heads) > 1:
228 return list(parent_heads)
243 return list(parent_heads)
229
244
230 return []
245 return []
231
246
232
247
233 def _get_git_env():
248 def _get_git_env():
234 env = {}
249 env = {}
235 for k, v in os.environ.items():
250 for k, v in os.environ.items():
236 if k.startswith('GIT'):
251 if k.startswith('GIT'):
237 env[k] = v
252 env[k] = v
238
253
239 # serialized version
254 # serialized version
240 return [(k, v) for k, v in env.items()]
255 return [(k, v) for k, v in env.items()]
241
256
242
257
243 def _get_hg_env(old_rev, new_rev, txnid, repo_path):
258 def _get_hg_env(old_rev, new_rev, txnid, repo_path):
244 env = {}
259 env = {}
245 for k, v in os.environ.items():
260 for k, v in os.environ.items():
246 if k.startswith('HG'):
261 if k.startswith('HG'):
247 env[k] = v
262 env[k] = v
248
263
249 env['HG_NODE'] = old_rev
264 env['HG_NODE'] = old_rev
250 env['HG_NODE_LAST'] = new_rev
265 env['HG_NODE_LAST'] = new_rev
251 env['HG_TXNID'] = txnid
266 env['HG_TXNID'] = txnid
252 env['HG_PENDING'] = repo_path
267 env['HG_PENDING'] = repo_path
253
268
254 return [(k, v) for k, v in env.items()]
269 return [(k, v) for k, v in env.items()]
255
270
256
271
257 def _get_ini_settings(ini_file):
272 def _get_ini_settings(ini_file):
258 from vcsserver.http_main import sanitize_settings_and_apply_defaults
273 from vcsserver.http_main import sanitize_settings_and_apply_defaults
259 from vcsserver.lib.config_utils import get_app_config_lightweight, configure_and_store_settings
274 from vcsserver.lib.config_utils import get_app_config_lightweight, configure_and_store_settings
260
275
261 global_config = {'__file__': ini_file}
276 global_config = {'__file__': ini_file}
262 ini_settings = get_app_config_lightweight(ini_file)
277 ini_settings = get_app_config_lightweight(ini_file)
263 sanitize_settings_and_apply_defaults(global_config, ini_settings)
278 sanitize_settings_and_apply_defaults(global_config, ini_settings)
264 configure_and_store_settings(global_config, ini_settings)
279 configure_and_store_settings(global_config, ini_settings)
265
280
266 return ini_settings
281 return ini_settings
267
282
268
283
269 def _fix_hooks_executables(ini_path=''):
284 def _fix_hooks_executables(ini_path=''):
270 """
285 """
271 This is a trick to set proper settings.EXECUTABLE paths for certain execution patterns
286 This is a trick to set proper settings.EXECUTABLE paths for certain execution patterns
272 especially for subversion where hooks strip entire env, and calling just 'svn' command will most likely fail
287 especially for subversion where hooks strip entire env, and calling just 'svn' command will most likely fail
273 because svn is not on PATH
288 because svn is not on PATH
274 """
289 """
275 # set defaults, in case we can't read from ini_file
290 # set defaults, in case we can't read from ini_file
276 core_binary_dir = settings.BINARY_DIR or '/usr/local/bin/rhodecode_bin/vcs_bin'
291 core_binary_dir = settings.BINARY_DIR or '/usr/local/bin/rhodecode_bin/vcs_bin'
277 if ini_path:
292 if ini_path:
278 ini_settings = _get_ini_settings(ini_path)
293 ini_settings = _get_ini_settings(ini_path)
279 core_binary_dir = ini_settings['core.binary_dir']
294 core_binary_dir = ini_settings['core.binary_dir']
280
295
281 settings.BINARY_DIR = core_binary_dir
296 settings.BINARY_DIR = core_binary_dir
282
297
283
298
284 def repo_size(ui, repo, **kwargs):
299 def repo_size(ui, repo, **kwargs):
285 extras = _extras_from_ui(ui)
300 extras = _extras_from_ui(ui)
286 return _call_hook('repo_size', extras, HgMessageWriter(ui))
301 return _call_hook('repo_size', extras, HgMessageWriter(ui))
287
302
288
303
289 def pre_pull(ui, repo, **kwargs):
304 def pre_pull(ui, repo, **kwargs):
290 extras = _extras_from_ui(ui)
305 extras = _extras_from_ui(ui)
291 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
306 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
292
307
293
308
294 def pre_pull_ssh(ui, repo, **kwargs):
309 def pre_pull_ssh(ui, repo, **kwargs):
295 extras = _extras_from_ui(ui)
310 extras = _extras_from_ui(ui)
296 if extras and extras.get('SSH'):
311 if extras and extras.get('SSH'):
297 return pre_pull(ui, repo, **kwargs)
312 return pre_pull(ui, repo, **kwargs)
298 return 0
313 return 0
299
314
300
315
301 def post_pull(ui, repo, **kwargs):
316 def post_pull(ui, repo, **kwargs):
302 extras = _extras_from_ui(ui)
317 extras = _extras_from_ui(ui)
303 return _call_hook('post_pull', extras, HgMessageWriter(ui))
318 return _call_hook('post_pull', extras, HgMessageWriter(ui))
304
319
305
320
306 def post_pull_ssh(ui, repo, **kwargs):
321 def post_pull_ssh(ui, repo, **kwargs):
307 extras = _extras_from_ui(ui)
322 extras = _extras_from_ui(ui)
308 if extras and extras.get('SSH'):
323 if extras and extras.get('SSH'):
309 return post_pull(ui, repo, **kwargs)
324 return post_pull(ui, repo, **kwargs)
310 return 0
325 return 0
311
326
312
327
313 def pre_push(ui, repo, node=None, **kwargs):
328 def pre_push(ui, repo, node=None, **kwargs):
314 """
329 """
315 Mercurial pre_push hook
330 Mercurial pre_push hook
316 """
331 """
317 extras = _extras_from_ui(ui)
332 extras = _extras_from_ui(ui)
318 detect_force_push = extras.get('detect_force_push')
333 detect_force_push = extras.get('detect_force_push')
319
334
320 rev_data = []
335 rev_data = []
321 hook_type: str = safe_str(kwargs.get('hooktype'))
336 hook_type: str = safe_str(kwargs.get('hooktype'))
322
337
323 if node and hook_type == 'pretxnchangegroup':
338 if node and hook_type == 'pretxnchangegroup':
324 branches = collections.defaultdict(list)
339 branches = collections.defaultdict(list)
325 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
340 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
326 for commit_id, branch in commits:
341 for commit_id, branch in commits:
327 branches[branch].append(commit_id)
342 branches[branch].append(commit_id)
328
343
329 for branch, commits in branches.items():
344 for branch, commits in branches.items():
330 old_rev = ascii_str(kwargs.get('node_last')) or commits[0]
345 old_rev = ascii_str(kwargs.get('node_last')) or commits[0]
331 rev_data.append({
346 rev_data.append({
332 'total_commits': len(commits),
347 'total_commits': len(commits),
333 'old_rev': old_rev,
348 'old_rev': old_rev,
334 'new_rev': commits[-1],
349 'new_rev': commits[-1],
335 'ref': '',
350 'ref': '',
336 'type': 'branch',
351 'type': 'branch',
337 'name': branch,
352 'name': branch,
338 })
353 })
339
354
340 for push_ref in rev_data:
355 for push_ref in rev_data:
341 push_ref['multiple_heads'] = _heads
356 push_ref['multiple_heads'] = _heads
342
357
343 repo_path = os.path.join(
358 repo_path = os.path.join(
344 extras.get('repo_store', ''), extras.get('repository', ''))
359 extras.get('repo_store', ''), extras.get('repository', ''))
345 push_ref['hg_env'] = _get_hg_env(
360 push_ref['hg_env'] = _get_hg_env(
346 old_rev=push_ref['old_rev'],
361 old_rev=push_ref['old_rev'],
347 new_rev=push_ref['new_rev'], txnid=ascii_str(kwargs.get('txnid')),
362 new_rev=push_ref['new_rev'], txnid=ascii_str(kwargs.get('txnid')),
348 repo_path=repo_path)
363 repo_path=repo_path)
349
364
350 extras['hook_type'] = hook_type or 'pre_push'
365 extras['hook_type'] = hook_type or 'pre_push'
351 extras['commit_ids'] = rev_data
366 extras['commit_ids'] = rev_data
352
367
353 return _call_hook('pre_push', extras, HgMessageWriter(ui))
368 return _call_hook('pre_push', extras, HgMessageWriter(ui))
354
369
355
370
356 def pre_push_ssh(ui, repo, node=None, **kwargs):
371 def pre_push_ssh(ui, repo, node=None, **kwargs):
357 extras = _extras_from_ui(ui)
372 extras = _extras_from_ui(ui)
358 if extras.get('SSH'):
373 if extras.get('SSH'):
359 return pre_push(ui, repo, node, **kwargs)
374 return pre_push(ui, repo, node, **kwargs)
360
375
361 return 0
376 return 0
362
377
363
378
364 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
379 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
365 """
380 """
366 Mercurial pre_push hook for SSH
381 Mercurial pre_push hook for SSH
367 """
382 """
368 extras = _extras_from_ui(ui)
383 extras = _extras_from_ui(ui)
369 if extras.get('SSH'):
384 if extras.get('SSH'):
370 permission = extras['SSH_PERMISSIONS']
385 permission = extras['SSH_PERMISSIONS']
371
386
372 if 'repository.write' == permission or 'repository.admin' == permission:
387 if 'repository.write' == permission or 'repository.admin' == permission:
373 return 0
388 return 0
374
389
375 # non-zero ret code
390 # non-zero ret code
376 return 1
391 return 1
377
392
378 return 0
393 return 0
379
394
380
395
381 def post_push(ui, repo, node, **kwargs):
396 def post_push(ui, repo, node, **kwargs):
382 """
397 """
383 Mercurial post_push hook
398 Mercurial post_push hook
384 """
399 """
385 extras = _extras_from_ui(ui)
400 extras = _extras_from_ui(ui)
386
401
387 commit_ids = []
402 commit_ids = []
388 branches = []
403 branches = []
389 bookmarks = []
404 bookmarks = []
390 tags = []
405 tags = []
391 hook_type: str = safe_str(kwargs.get('hooktype'))
406 hook_type: str = safe_str(kwargs.get('hooktype'))
392
407
393 commits, _heads = _rev_range_hash(repo, node)
408 commits, _heads = _rev_range_hash(repo, node)
394 for commit_id, branch in commits:
409 for commit_id, branch in commits:
395 commit_ids.append(commit_id)
410 commit_ids.append(commit_id)
396 if branch not in branches:
411 if branch not in branches:
397 branches.append(branch)
412 branches.append(branch)
398
413
399 if hasattr(ui, '_rc_pushkey_bookmarks'):
414 if hasattr(ui, '_rc_pushkey_bookmarks'):
400 bookmarks = ui._rc_pushkey_bookmarks
415 bookmarks = ui._rc_pushkey_bookmarks
401
416
402 extras['hook_type'] = hook_type or 'post_push'
417 extras['hook_type'] = hook_type or 'post_push'
403 extras['commit_ids'] = commit_ids
418 extras['commit_ids'] = commit_ids
404
419
405 extras['new_refs'] = {
420 extras['new_refs'] = {
406 'branches': branches,
421 'branches': branches,
407 'bookmarks': bookmarks,
422 'bookmarks': bookmarks,
408 'tags': tags
423 'tags': tags
409 }
424 }
410
425
411 return _call_hook('post_push', extras, HgMessageWriter(ui))
426 return _call_hook('post_push', extras, HgMessageWriter(ui))
412
427
413
428
414 def post_push_ssh(ui, repo, node, **kwargs):
429 def post_push_ssh(ui, repo, node, **kwargs):
415 """
430 """
416 Mercurial post_push hook for SSH
431 Mercurial post_push hook for SSH
417 """
432 """
418 if _extras_from_ui(ui).get('SSH'):
433 if _extras_from_ui(ui).get('SSH'):
419 return post_push(ui, repo, node, **kwargs)
434 return post_push(ui, repo, node, **kwargs)
420 return 0
435 return 0
421
436
422
437
423 def key_push(ui, repo, **kwargs):
438 def key_push(ui, repo, **kwargs):
424 from vcsserver.hgcompat import get_ctx
439 from vcsserver.hgcompat import get_ctx
425
440
426 if kwargs['new'] != b'0' and kwargs['namespace'] == b'bookmarks':
441 if kwargs['new'] != b'0' and kwargs['namespace'] == b'bookmarks':
427 # store new bookmarks in our UI object propagated later to post_push
442 # store new bookmarks in our UI object propagated later to post_push
428 ui._rc_pushkey_bookmarks = get_ctx(repo, kwargs['key']).bookmarks()
443 ui._rc_pushkey_bookmarks = get_ctx(repo, kwargs['key']).bookmarks()
429 return
444 return
430
445
431
446
432 # backward compat
447 # backward compat
433 log_pull_action = post_pull
448 log_pull_action = post_pull
434
449
435 # backward compat
450 # backward compat
436 log_push_action = post_push
451 log_push_action = post_push
437
452
438
453
439 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
454 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
440 """
455 """
441 Old hook name: keep here for backward compatibility.
456 Old hook name: keep here for backward compatibility.
442
457
443 This is only required when the installed git hooks are not upgraded.
458 This is only required when the installed git hooks are not upgraded.
444 """
459 """
445 pass
460 pass
446
461
447
462
448 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
463 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
449 """
464 """
450 Old hook name: keep here for backward compatibility.
465 Old hook name: keep here for backward compatibility.
451
466
452 This is only required when the installed git hooks are not upgraded.
467 This is only required when the installed git hooks are not upgraded.
453 """
468 """
454 pass
469 pass
455
470
456
471
457 @dataclasses.dataclass
472 @dataclasses.dataclass
458 class HookResponse:
473 class HookResponse:
459 status: int
474 status: int
460 output: str
475 output: str
461
476
462
477
463 def git_pre_pull(extras) -> HookResponse:
478 def git_pre_pull(extras) -> HookResponse:
464 """
479 """
465 Pre pull hook.
480 Pre pull hook.
466
481
467 :param extras: dictionary containing the keys defined in simplevcs
482 :param extras: dictionary containing the keys defined in simplevcs
468 :type extras: dict
483 :type extras: dict
469
484
470 :return: status code of the hook. 0 for success.
485 :return: status code of the hook. 0 for success.
471 :rtype: int
486 :rtype: int
472 """
487 """
473
488
474 if 'pull' not in extras['hooks']:
489 if 'pull' not in extras['hooks']:
475 return HookResponse(0, '')
490 return HookResponse(0, '')
476
491
477 stdout = io.StringIO()
492 stdout = io.StringIO()
478 try:
493 try:
479 status_code = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
494 status_code = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
480
495
481 except Exception as error:
496 except Exception as error:
482 log.exception('Failed to call pre_pull hook')
497 log.exception('Failed to call pre_pull hook')
483 status_code = 128
498 status_code = 128
484 stdout.write(f'ERROR: {error}\n')
499 stdout.write(f'ERROR: {error}\n')
485
500
486 return HookResponse(status_code, stdout.getvalue())
501 return HookResponse(status_code, stdout.getvalue())
487
502
488
503
489 def git_post_pull(extras) -> HookResponse:
504 def git_post_pull(extras) -> HookResponse:
490 """
505 """
491 Post pull hook.
506 Post pull hook.
492
507
493 :param extras: dictionary containing the keys defined in simplevcs
508 :param extras: dictionary containing the keys defined in simplevcs
494 :type extras: dict
509 :type extras: dict
495
510
496 :return: status code of the hook. 0 for success.
511 :return: status code of the hook. 0 for success.
497 :rtype: int
512 :rtype: int
498 """
513 """
499 if 'pull' not in extras['hooks']:
514 if 'pull' not in extras['hooks']:
500 return HookResponse(0, '')
515 return HookResponse(0, '')
501
516
502 stdout = io.StringIO()
517 stdout = io.StringIO()
503 try:
518 try:
504 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
519 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
505 except Exception as error:
520 except Exception as error:
506 status = 128
521 status = 128
507 stdout.write(f'ERROR: {error}\n')
522 stdout.write(f'ERROR: {error}\n')
508
523
509 return HookResponse(status, stdout.getvalue())
524 return HookResponse(status, stdout.getvalue())
510
525
511
526
512 def _parse_git_ref_lines(revision_lines):
527 def _parse_git_ref_lines(revision_lines):
513 rev_data = []
528 rev_data = []
514 for revision_line in revision_lines or []:
529 for revision_line in revision_lines or []:
515 old_rev, new_rev, ref = revision_line.strip().split(' ')
530 old_rev, new_rev, ref = revision_line.strip().split(' ')
516 ref_data = ref.split('/', 2)
531 ref_data = ref.split('/', 2)
517 if ref_data[1] in ('tags', 'heads'):
532 if ref_data[1] in ('tags', 'heads'):
518 rev_data.append({
533 rev_data.append({
519 # NOTE(marcink):
534 # NOTE(marcink):
520 # we're unable to tell total_commits for git at this point
535 # we're unable to tell total_commits for git at this point
521 # but we set the variable for consistency with GIT
536 # but we set the variable for consistency with GIT
522 'total_commits': -1,
537 'total_commits': -1,
523 'old_rev': old_rev,
538 'old_rev': old_rev,
524 'new_rev': new_rev,
539 'new_rev': new_rev,
525 'ref': ref,
540 'ref': ref,
526 'type': ref_data[1],
541 'type': ref_data[1],
527 'name': ref_data[2],
542 'name': ref_data[2],
528 })
543 })
529 return rev_data
544 return rev_data
530
545
531
546
532 def git_pre_receive(unused_repo_path, revision_lines, env) -> int:
547 def git_pre_receive(unused_repo_path, revision_lines, env) -> int:
533 """
548 """
534 Pre push hook.
549 Pre push hook.
535
550
536 :return: status code of the hook. 0 for success.
551 :return: status code of the hook. 0 for success.
537 """
552 """
538 extras = json.loads(env['RC_SCM_DATA'])
553 extras = json.loads(env['RC_SCM_DATA'])
539 rev_data = _parse_git_ref_lines(revision_lines)
554 rev_data = _parse_git_ref_lines(revision_lines)
540 if 'push' not in extras['hooks']:
555 if 'push' not in extras['hooks']:
541 return 0
556 return 0
542 _fix_hooks_executables(env.get('RC_INI_FILE'))
557 _fix_hooks_executables(env.get('RC_INI_FILE'))
543
558
544 empty_commit_id = '0' * 40
559 empty_commit_id = '0' * 40
545
560
546 detect_force_push = extras.get('detect_force_push')
561 detect_force_push = extras.get('detect_force_push')
547
562
548 for push_ref in rev_data:
563 for push_ref in rev_data:
549 # store our git-env which holds the temp store
564 # store our git-env which holds the temp store
550 push_ref['git_env'] = _get_git_env()
565 push_ref['git_env'] = _get_git_env()
551 push_ref['pruned_sha'] = ''
566 push_ref['pruned_sha'] = ''
552 if not detect_force_push:
567 if not detect_force_push:
553 # don't check for forced-push when we don't need to
568 # don't check for forced-push when we don't need to
554 continue
569 continue
555
570
556 type_ = push_ref['type']
571 type_ = push_ref['type']
557 new_branch = push_ref['old_rev'] == empty_commit_id
572 new_branch = push_ref['old_rev'] == empty_commit_id
558 delete_branch = push_ref['new_rev'] == empty_commit_id
573 delete_branch = push_ref['new_rev'] == empty_commit_id
559 if type_ == 'heads' and not (new_branch or delete_branch):
574 if type_ == 'heads' and not (new_branch or delete_branch):
560 old_rev = push_ref['old_rev']
575 old_rev = push_ref['old_rev']
561 new_rev = push_ref['new_rev']
576 new_rev = push_ref['new_rev']
562 cmd = [settings.GIT_EXECUTABLE(), 'rev-list', old_rev, f'^{new_rev}']
577 cmd = [settings.GIT_EXECUTABLE(), 'rev-list', old_rev, f'^{new_rev}']
563 stdout, stderr = subprocessio.run_command(
578 stdout, stderr = subprocessio.run_command(
564 cmd, env=os.environ.copy())
579 cmd, env=os.environ.copy())
565 # means we're having some non-reachable objects, this forced push was used
580 # means we're having some non-reachable objects, this forced push was used
566 if stdout:
581 if stdout:
567 push_ref['pruned_sha'] = stdout.splitlines()
582 push_ref['pruned_sha'] = stdout.splitlines()
568
583
569 extras['hook_type'] = 'pre_receive'
584 extras['hook_type'] = 'pre_receive'
570 extras['commit_ids'] = rev_data
585 extras['commit_ids'] = rev_data
571
586
572 stdout = sys.stdout
587 stdout = sys.stdout
573 status_code = _call_hook('pre_push', extras, GitMessageWriter(stdout))
588 status_code = _call_hook('pre_push', extras, GitMessageWriter(stdout))
574
589
575 return status_code
590 return status_code
576
591
577
592
578 def git_post_receive(unused_repo_path, revision_lines, env) -> int:
593 def git_post_receive(unused_repo_path, revision_lines, env) -> int:
579 """
594 """
580 Post push hook.
595 Post push hook.
581
596
582 :return: status code of the hook. 0 for success.
597 :return: status code of the hook. 0 for success.
583 """
598 """
584 extras = json.loads(env['RC_SCM_DATA'])
599 extras = json.loads(env['RC_SCM_DATA'])
585 if 'push' not in extras['hooks']:
600 if 'push' not in extras['hooks']:
586 return 0
601 return 0
587
602
588 _fix_hooks_executables(env.get('RC_INI_FILE'))
603 _fix_hooks_executables(env.get('RC_INI_FILE'))
589
604
590 rev_data = _parse_git_ref_lines(revision_lines)
605 rev_data = _parse_git_ref_lines(revision_lines)
591
606
592 git_revs = []
607 git_revs = []
593
608
594 # N.B.(skreft): it is ok to just call git, as git before calling a
609 # N.B.(skreft): it is ok to just call git, as git before calling a
595 # subcommand sets the PATH environment variable so that it point to the
610 # subcommand sets the PATH environment variable so that it point to the
596 # correct version of the git executable.
611 # correct version of the git executable.
597 empty_commit_id = '0' * 40
612 empty_commit_id = '0' * 40
598 branches = []
613 branches = []
599 tags = []
614 tags = []
600 for push_ref in rev_data:
615 for push_ref in rev_data:
601 type_ = push_ref['type']
616 type_ = push_ref['type']
602
617
603 if type_ == 'heads':
618 if type_ == 'heads':
604 # starting new branch case
619 # starting new branch case
605 if push_ref['old_rev'] == empty_commit_id:
620 if push_ref['old_rev'] == empty_commit_id:
606 push_ref_name = push_ref['name']
621 push_ref_name = push_ref['name']
607
622
608 if push_ref_name not in branches:
623 if push_ref_name not in branches:
609 branches.append(push_ref_name)
624 branches.append(push_ref_name)
610
625
611 need_head_set = ''
626 need_head_set = ''
612 with Repository(os.getcwd()) as repo:
627 with Repository(os.getcwd()) as repo:
613 try:
628 try:
614 repo.head
629 repo.head
615 except pygit2.GitError:
630 except pygit2.GitError:
616 need_head_set = f'refs/heads/{push_ref_name}'
631 need_head_set = f'refs/heads/{push_ref_name}'
617
632
618 if need_head_set:
633 if need_head_set:
619 repo.set_head(need_head_set)
634 repo.set_head(need_head_set)
620 print(f"Setting default branch to {push_ref_name}")
635 print(f"Setting default branch to {push_ref_name}")
621
636
622 cmd = [settings.GIT_EXECUTABLE(), 'for-each-ref', '--format=%(refname)', 'refs/heads/*']
637 cmd = [settings.GIT_EXECUTABLE(), 'for-each-ref', '--format=%(refname)', 'refs/heads/*']
623 stdout, stderr = subprocessio.run_command(
638 stdout, stderr = subprocessio.run_command(
624 cmd, env=os.environ.copy())
639 cmd, env=os.environ.copy())
625 heads = safe_str(stdout)
640 heads = safe_str(stdout)
626 heads = heads.replace(push_ref['ref'], '')
641 heads = heads.replace(push_ref['ref'], '')
627 heads = ' '.join(head for head
642 heads = ' '.join(head for head
628 in heads.splitlines() if head) or '.'
643 in heads.splitlines() if head) or '.'
629 cmd = [settings.GIT_EXECUTABLE(), 'log', '--reverse',
644 cmd = [settings.GIT_EXECUTABLE(), 'log', '--reverse',
630 '--pretty=format:%H', '--', push_ref['new_rev'],
645 '--pretty=format:%H', '--', push_ref['new_rev'],
631 '--not', heads]
646 '--not', heads]
632 stdout, stderr = subprocessio.run_command(
647 stdout, stderr = subprocessio.run_command(
633 cmd, env=os.environ.copy())
648 cmd, env=os.environ.copy())
634 git_revs.extend(list(map(ascii_str, stdout.splitlines())))
649 git_revs.extend(list(map(ascii_str, stdout.splitlines())))
635
650
636 # delete branch case
651 # delete branch case
637 elif push_ref['new_rev'] == empty_commit_id:
652 elif push_ref['new_rev'] == empty_commit_id:
638 git_revs.append(f'delete_branch=>{push_ref["name"]}')
653 git_revs.append(f'delete_branch=>{push_ref["name"]}')
639 else:
654 else:
640 if push_ref['name'] not in branches:
655 if push_ref['name'] not in branches:
641 branches.append(push_ref['name'])
656 branches.append(push_ref['name'])
642
657
643 cmd = [settings.GIT_EXECUTABLE(), 'log',
658 cmd = [settings.GIT_EXECUTABLE(), 'log',
644 f'{push_ref["old_rev"]}..{push_ref["new_rev"]}',
659 f'{push_ref["old_rev"]}..{push_ref["new_rev"]}',
645 '--reverse', '--pretty=format:%H']
660 '--reverse', '--pretty=format:%H']
646 stdout, stderr = subprocessio.run_command(
661 stdout, stderr = subprocessio.run_command(
647 cmd, env=os.environ.copy())
662 cmd, env=os.environ.copy())
648 # we get bytes from stdout, we need str to be consistent
663 # we get bytes from stdout, we need str to be consistent
649 log_revs = list(map(ascii_str, stdout.splitlines()))
664 log_revs = list(map(ascii_str, stdout.splitlines()))
650 git_revs.extend(log_revs)
665 git_revs.extend(log_revs)
651
666
652 # Pure pygit2 impl. but still 2-3x slower :/
667 # Pure pygit2 impl. but still 2-3x slower :/
653 # results = []
668 # results = []
654 #
669 #
655 # with Repository(os.getcwd()) as repo:
670 # with Repository(os.getcwd()) as repo:
656 # repo_new_rev = repo[push_ref['new_rev']]
671 # repo_new_rev = repo[push_ref['new_rev']]
657 # repo_old_rev = repo[push_ref['old_rev']]
672 # repo_old_rev = repo[push_ref['old_rev']]
658 # walker = repo.walk(repo_new_rev.id, pygit2.GIT_SORT_TOPOLOGICAL)
673 # walker = repo.walk(repo_new_rev.id, pygit2.GIT_SORT_TOPOLOGICAL)
659 #
674 #
660 # for commit in walker:
675 # for commit in walker:
661 # if commit.id == repo_old_rev.id:
676 # if commit.id == repo_old_rev.id:
662 # break
677 # break
663 # results.append(commit.id.hex)
678 # results.append(commit.id.hex)
664 # # reverse the order, can't use GIT_SORT_REVERSE
679 # # reverse the order, can't use GIT_SORT_REVERSE
665 # log_revs = results[::-1]
680 # log_revs = results[::-1]
666
681
667 elif type_ == 'tags':
682 elif type_ == 'tags':
668 if push_ref['name'] not in tags:
683 if push_ref['name'] not in tags:
669 tags.append(push_ref['name'])
684 tags.append(push_ref['name'])
670 git_revs.append(f'tag=>{push_ref["name"]}')
685 git_revs.append(f'tag=>{push_ref["name"]}')
671
686
672 extras['hook_type'] = 'post_receive'
687 extras['hook_type'] = 'post_receive'
673 extras['commit_ids'] = git_revs
688 extras['commit_ids'] = git_revs
674 extras['new_refs'] = {
689 extras['new_refs'] = {
675 'branches': branches,
690 'branches': branches,
676 'bookmarks': [],
691 'bookmarks': [],
677 'tags': tags,
692 'tags': tags,
678 }
693 }
679
694
680 stdout = sys.stdout
695 stdout = sys.stdout
681
696
682 if 'repo_size' in extras['hooks']:
697 if 'repo_size' in extras['hooks']:
683 try:
698 try:
684 _call_hook('repo_size', extras, GitMessageWriter(stdout))
699 _call_hook('repo_size', extras, GitMessageWriter(stdout))
685 except Exception:
700 except Exception:
686 pass
701 pass
687
702
688 status_code = _call_hook('post_push', extras, GitMessageWriter(stdout))
703 status_code = _call_hook('post_push', extras, GitMessageWriter(stdout))
689 return status_code
704 return status_code
690
705
691
706
692 def get_extras_from_txn_id(repo_path, txn_id):
707 def get_extras_from_txn_id(repo_path, txn_id):
693 extras = get_txn_id_from_store(repo_path, txn_id)
708 extras = get_txn_id_from_store(repo_path, txn_id)
694 return extras
709 return extras
695
710
696
711
697 def svn_pre_commit(repo_path, commit_data, env):
712 def svn_pre_commit(repo_path, commit_data, env):
698
713
699 path, txn_id = commit_data
714 path, txn_id = commit_data
700 branches = []
715 branches = []
701 tags = []
716 tags = []
702
717
703 if env.get('RC_SCM_DATA'):
718 if env.get('RC_SCM_DATA'):
704 extras = json.loads(env['RC_SCM_DATA'])
719 extras = json.loads(env['RC_SCM_DATA'])
705 else:
720 else:
706 ini_path = env.get('RC_INI_FILE')
721 ini_path = env.get('RC_INI_FILE')
707 if ini_path:
722 if ini_path:
708 _get_ini_settings(ini_path)
723 _get_ini_settings(ini_path)
709 # fallback method to read from TXN-ID stored data
724 # fallback method to read from TXN-ID stored data
710 extras = get_extras_from_txn_id(path, txn_id)
725 extras = get_extras_from_txn_id(path, txn_id)
711
726
712 if not extras:
727 if not extras:
713 raise ValueError('SVN-PRE-COMMIT: Failed to extract context data in called extras for hook execution')
728 raise ValueError('SVN-PRE-COMMIT: Failed to extract context data in called extras for hook execution')
714
729
715 if extras.get('rc_internal_commit'):
730 if extras.get('rc_internal_commit'):
716 # special marker for internal commit, we don't call hooks client
731 # special marker for internal commit, we don't call hooks client
717 return 0
732 return 0
718
733
719 extras['hook_type'] = 'pre_commit'
734 extras['hook_type'] = 'pre_commit'
720 extras['commit_ids'] = [txn_id]
735 extras['commit_ids'] = [txn_id]
721 extras['txn_id'] = txn_id
736 extras['txn_id'] = txn_id
722 extras['new_refs'] = {
737 extras['new_refs'] = {
723 'total_commits': 1,
738 'total_commits': 1,
724 'branches': branches,
739 'branches': branches,
725 'bookmarks': [],
740 'bookmarks': [],
726 'tags': tags,
741 'tags': tags,
727 }
742 }
728
743
729 return _call_hook('pre_push', extras, SvnMessageWriter())
744 return _call_hook('pre_push', extras, SvnMessageWriter())
730
745
731
746
732 def svn_post_commit(repo_path, commit_data, env):
747 def svn_post_commit(repo_path, commit_data, env):
733 """
748 """
734 commit_data is path, rev, txn_id
749 commit_data is path, rev, txn_id
735 """
750 """
736
751
737 if len(commit_data) == 3:
752 if len(commit_data) == 3:
738 path, commit_id, txn_id = commit_data
753 path, commit_id, txn_id = commit_data
739 elif len(commit_data) == 2:
754 elif len(commit_data) == 2:
740 log.error('Failed to extract txn_id from commit_data using legacy method. '
755 log.error('Failed to extract txn_id from commit_data using legacy method. '
741 'Some functionality might be limited')
756 'Some functionality might be limited')
742 path, commit_id = commit_data
757 path, commit_id = commit_data
743 txn_id = None
758 txn_id = None
744 else:
759 else:
745 return 0
760 return 0
746
761
747 branches = []
762 branches = []
748 tags = []
763 tags = []
749
764
750 if env.get('RC_SCM_DATA'):
765 if env.get('RC_SCM_DATA'):
751 extras = json.loads(env['RC_SCM_DATA'])
766 extras = json.loads(env['RC_SCM_DATA'])
752 else:
767 else:
753 ini_path = env.get('RC_INI_FILE')
768 ini_path = env.get('RC_INI_FILE')
754 if ini_path:
769 if ini_path:
755 _get_ini_settings(ini_path)
770 _get_ini_settings(ini_path)
756 # fallback method to read from TXN-ID stored data
771 # fallback method to read from TXN-ID stored data
757 extras = get_extras_from_txn_id(path, txn_id)
772 extras = get_extras_from_txn_id(path, txn_id)
758
773
759 if not extras and txn_id:
774 if not extras and txn_id:
760 raise ValueError('SVN-POST-COMMIT: Failed to extract context data in called extras for hook execution')
775 raise ValueError('SVN-POST-COMMIT: Failed to extract context data in called extras for hook execution')
761
776
762 if extras.get('rc_internal_commit'):
777 if extras.get('rc_internal_commit'):
763 # special marker for internal commit, we don't call hooks client
778 # special marker for internal commit, we don't call hooks client
764 return 0
779 return 0
765
780
766 extras['hook_type'] = 'post_commit'
781 extras['hook_type'] = 'post_commit'
767 extras['commit_ids'] = [commit_id]
782 extras['commit_ids'] = [commit_id]
768 extras['txn_id'] = txn_id
783 extras['txn_id'] = txn_id
769 extras['new_refs'] = {
784 extras['new_refs'] = {
770 'branches': branches,
785 'branches': branches,
771 'bookmarks': [],
786 'bookmarks': [],
772 'tags': tags,
787 'tags': tags,
773 'total_commits': 1,
788 'total_commits': 1,
774 }
789 }
775
790
776 if 'repo_size' in extras['hooks']:
791 if 'repo_size' in extras['hooks']:
777 try:
792 try:
778 _call_hook('repo_size', extras, SvnMessageWriter())
793 _call_hook('repo_size', extras, SvnMessageWriter())
779 except Exception:
794 except Exception:
780 pass
795 pass
781
796
782 return _call_hook('post_push', extras, SvnMessageWriter())
797 return _call_hook('post_push', extras, SvnMessageWriter())
@@ -1,135 +1,141 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2023 RhodeCode GmbH
2 # Copyright (C) 2014-2023 RhodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import pytest
18 import pytest
19
19
20 import mercurial.ui
20 import mercurial.ui
21 import mock
21 import mock
22
22
23 from vcsserver.lib.ext_json import json
23 from vcsserver.lib.ext_json import json
24 from vcsserver import hooks
24 from vcsserver import hooks
25
25
26
26
27 def get_hg_ui(extras=None):
27 def get_hg_ui(extras=None):
28 """Create a Config object with a valid RC_SCM_DATA entry."""
28 """Create a Config object with a valid RC_SCM_DATA entry."""
29 extras = extras or {}
29 extras = extras or {}
30 required_extras = {
30 required_extras = {
31 'username': '',
31 'username': '',
32 'repository': '',
32 'repository': '',
33 'locked_by': '',
33 'locked_by': '',
34 'scm': '',
34 'scm': '',
35 'make_lock': '',
35 'make_lock': '',
36 'action': '',
36 'action': '',
37 'ip': '',
37 'ip': '',
38 }
38 }
39 required_extras.update(extras)
39 required_extras.update(extras)
40 hg_ui = mercurial.ui.ui()
40 hg_ui = mercurial.ui.ui()
41 hg_ui.setconfig(b'rhodecode', b'RC_SCM_DATA', json.dumps(required_extras))
41 hg_ui.setconfig(b'rhodecode', b'RC_SCM_DATA', json.dumps(required_extras))
42
42
43 return hg_ui
43 return hg_ui
44
44
45
45
46 def test_git_pre_receive_is_disabled():
46 def test_git_pre_receive_is_disabled():
47 extras = {'hooks': ['pull']}
47 extras = {'hooks': ['pull']}
48 response = hooks.git_pre_receive(None, None,
48 response = hooks.git_pre_receive(None, None,
49 {'RC_SCM_DATA': json.dumps(extras)})
49 {'RC_SCM_DATA': json.dumps(extras)})
50
50
51 assert response == 0
51 assert response == 0
52
52
53
53
54 def test_git_post_receive_is_disabled():
54 def test_git_post_receive_is_disabled():
55 extras = {'hooks': ['pull']}
55 extras = {'hooks': ['pull']}
56 response = hooks.git_post_receive(None, '',
56 response = hooks.git_post_receive(None, '',
57 {'RC_SCM_DATA': json.dumps(extras)})
57 {'RC_SCM_DATA': json.dumps(extras)})
58
58
59 assert response == 0
59 assert response == 0
60
60
61
61
62 def test_git_post_receive_calls_repo_size():
62 def test_git_post_receive_calls_repo_size():
63 extras = {'hooks': ['push', 'repo_size']}
63 extras = {'hooks': ['push', 'repo_size']}
64
64
65 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
65 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
66 hooks.git_post_receive(
66 hooks.git_post_receive(
67 None, '', {'RC_SCM_DATA': json.dumps(extras)})
67 None, '', {'RC_SCM_DATA': json.dumps(extras)})
68 extras.update({'commit_ids': [], 'hook_type': 'post_receive',
68 extras.update({'commit_ids': [], 'hook_type': 'post_receive',
69 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
69 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
70 expected_calls = [
70 expected_calls = [
71 mock.call('repo_size', extras, mock.ANY),
71 mock.call('repo_size', extras, mock.ANY),
72 mock.call('post_push', extras, mock.ANY),
72 mock.call('post_push', extras, mock.ANY),
73 ]
73 ]
74 assert call_hook_mock.call_args_list == expected_calls
74 assert call_hook_mock.call_args_list == expected_calls
75
75
76
76
77 def test_git_post_receive_does_not_call_disabled_repo_size():
77 def test_git_post_receive_does_not_call_disabled_repo_size():
78 extras = {'hooks': ['push']}
78 extras = {'hooks': ['push']}
79
79
80 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
80 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
81 hooks.git_post_receive(
81 hooks.git_post_receive(
82 None, '', {'RC_SCM_DATA': json.dumps(extras)})
82 None, '', {'RC_SCM_DATA': json.dumps(extras)})
83 extras.update({'commit_ids': [], 'hook_type': 'post_receive',
83 extras.update({'commit_ids': [], 'hook_type': 'post_receive',
84 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
84 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
85 expected_calls = [
85 expected_calls = [
86 mock.call('post_push', extras, mock.ANY)
86 mock.call('post_push', extras, mock.ANY)
87 ]
87 ]
88 assert call_hook_mock.call_args_list == expected_calls
88 assert call_hook_mock.call_args_list == expected_calls
89
89
90
90
91 def test_repo_size_exception_does_not_affect_git_post_receive():
91 def test_repo_size_exception_does_not_affect_git_post_receive():
92 extras = {'hooks': ['push', 'repo_size']}
92 extras = {'hooks': ['push', 'repo_size']}
93 status = 0
93 status = 0
94
94
95 def side_effect(name, *args, **kwargs):
95 def side_effect(name, *args, **kwargs):
96 if name == 'repo_size':
96 if name == 'repo_size':
97 raise Exception('Fake exception')
97 raise Exception('Fake exception')
98 else:
98 else:
99 return status
99 return status
100
100
101 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
101 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
102 call_hook_mock.side_effect = side_effect
102 call_hook_mock.side_effect = side_effect
103 result = hooks.git_post_receive(
103 result = hooks.git_post_receive(
104 None, '', {'RC_SCM_DATA': json.dumps(extras)})
104 None, '', {'RC_SCM_DATA': json.dumps(extras)})
105 assert result == status
105 assert result == status
106
106
107
107
108 def test_git_pre_pull_is_disabled():
108 def test_git_pre_pull_is_disabled():
109 assert hooks.git_pre_pull({'hooks': ['push']}) == hooks.HookResponse(0, '')
109 assert hooks.git_pre_pull({'hooks': ['push']}) == hooks.HookResponse(0, '')
110
110
111
111
112 def test_git_post_pull_is_disabled():
112 def test_git_post_pull_is_disabled():
113 assert (
113 assert (
114 hooks.git_post_pull({'hooks': ['push']}) == hooks.HookResponse(0, ''))
114 hooks.git_post_pull({'hooks': ['push']}) == hooks.HookResponse(0, ''))
115
115
116
116
117 class TestGetHooksClient:
117 class TestGetHooksClient:
118
118
119 def test_return_celery_client_when_queue_and_backend_provided(self):
119 def test_return_celery_client_when_queue_and_backend_provided(self):
120 task_queue = 'redis://task_queue:0'
120 extras = {
121 task_backend = task_queue
121 'hooks_protocol': 'celery',
122 result = hooks._get_hooks_client({
122 'hooks_config': {'broker_url': 'redis://task_queue:0', 'result_backend': 'redis://task_queue:0'}
123 'task_queue': task_queue,
123 }
124 'task_backend': task_backend
124 result = hooks._get_hooks_client(extras)
125 })
126 assert isinstance(result, hooks.HooksCeleryClient)
125 assert isinstance(result, hooks.HooksCeleryClient)
127
126
128
127
129 class TestHooksCeleryClient:
128 class TestHooksCeleryClient:
130
129
131 def test_hooks_http_client_init(self):
130 def test_hooks_http_client_init(self):
132 queue = 'redis://redis:6379/0'
131 queue = 'redis://redis:6379/0'
133 backend = 'redis://redis:6379/0'
132 backend = 'redis://redis:6379/0'
134 client = hooks.HooksCeleryClient(queue, backend)
133 client = hooks.HooksCeleryClient(queue, backend)
135 assert client.celery_app.conf.broker_url == queue
134 assert client.celery_app.conf.broker_url == queue
135
136 def test_hooks_http_client_init_with_extra_opts(self):
137 queue = 'redis://redis:6379/0'
138 backend = 'redis://redis:6379/0'
139 client = hooks.HooksCeleryClient(queue, backend, _celery_opts={'task_always_eager': True})
140 assert client.celery_app.conf.broker_url == queue
141 assert client.celery_app.conf.task_always_eager == True
General Comments 0
You need to be logged in to leave comments. Login now