##// END OF EJS Templates
feat(celery-hooks): added HooksCeleryClient, removed support od HooksDummyClient, updated tests. Fixes: RCCE-55
ilin.s -
r1204:79967b24 default
parent child Browse files
Show More
@@ -1,57 +1,77 b''
1 # deps, generated via pipdeptree --exclude setuptools,wheel,pipdeptree,pip -f | tr '[:upper:]' '[:lower:]'
1 # deps, generated via pipdeptree --exclude setuptools,wheel,pipdeptree,pip -f | tr '[:upper:]' '[:lower:]'
2
2
3 async-timeout==4.0.3
3 async-timeout==4.0.3
4 atomicwrites==1.4.1
4 atomicwrites==1.4.1
5 celery==5.3.4
6 billiard==4.1.0
7 click==8.1.3
8 click-didyoumean==0.3.0
9 click==8.1.3
10 click-plugins==1.1.1
11 click==8.1.3
12 click-repl==0.2.0
13 click==8.1.3
14 prompt-toolkit==3.0.38
15 wcwidth==0.2.6
16 six==1.16.0
17 kombu==5.3.2
18 amqp==5.1.1
19 vine==5.1.0
20 vine==5.1.0
21 python-dateutil==2.8.2
22 six==1.16.0
23 tzdata==2023.4
24 vine==5.1.0
5 contextlib2==21.6.0
25 contextlib2==21.6.0
6 cov-core==1.15.0
26 cov-core==1.15.0
7 coverage==7.2.3
27 coverage==7.2.3
8 diskcache==5.6.3
28 diskcache==5.6.3
9 dogpile.cache==1.3.0
29 dogpile.cache==1.3.0
10 decorator==5.1.1
30 decorator==5.1.1
11 stevedore==5.1.0
31 stevedore==5.1.0
12 pbr==5.11.1
32 pbr==5.11.1
13 dulwich==0.21.6
33 dulwich==0.21.6
14 urllib3==1.26.14
34 urllib3==1.26.14
15 gunicorn==21.2.0
35 gunicorn==21.2.0
16 packaging==23.1
36 packaging==23.1
17 hg-evolve==11.0.2
37 hg-evolve==11.0.2
18 importlib-metadata==6.0.0
38 importlib-metadata==6.0.0
19 zipp==3.15.0
39 zipp==3.15.0
20 mercurial==6.3.3
40 mercurial==6.3.3
21 mock==5.0.2
41 mock==5.0.2
22 more-itertools==9.1.0
42 more-itertools==9.1.0
23 msgpack==1.0.7
43 msgpack==1.0.7
24 orjson==3.9.13
44 orjson==3.9.13
25 psutil==5.9.8
45 psutil==5.9.8
26 py==1.11.0
46 py==1.11.0
27 pygit2==1.13.3
47 pygit2==1.13.3
28 cffi==1.16.0
48 cffi==1.16.0
29 pycparser==2.21
49 pycparser==2.21
30 pygments==2.15.1
50 pygments==2.15.1
31 pyparsing==3.1.1
51 pyparsing==3.1.1
32 pyramid==2.0.2
52 pyramid==2.0.2
33 hupper==1.12
53 hupper==1.12
34 plaster==1.1.2
54 plaster==1.1.2
35 plaster-pastedeploy==1.0.1
55 plaster-pastedeploy==1.0.1
36 pastedeploy==3.1.0
56 pastedeploy==3.1.0
37 plaster==1.1.2
57 plaster==1.1.2
38 translationstring==1.4
58 translationstring==1.4
39 venusian==3.0.0
59 venusian==3.0.0
40 webob==1.8.7
60 webob==1.8.7
41 zope.deprecation==5.0.0
61 zope.deprecation==5.0.0
42 zope.interface==6.1.0
62 zope.interface==6.1.0
43 redis==5.0.1
63 redis==5.0.1
44 async-timeout==4.0.3
64 async-timeout==4.0.3
45 repoze.lru==0.7
65 repoze.lru==0.7
46 scandir==1.10.0
66 scandir==1.10.0
47 setproctitle==1.3.3
67 setproctitle==1.3.3
48 subvertpy==0.11.0
68 subvertpy==0.11.0
49 waitress==3.0.0
69 waitress==3.0.0
50 wcwidth==0.2.6
70 wcwidth==0.2.6
51
71
52
72
53 ## test related requirements
73 ## test related requirements
54 #-r requirements_test.txt
74 #-r requirements_test.txt
55
75
56 ## uncomment to add the debug libraries
76 ## uncomment to add the debug libraries
57 #-r requirements_debug.txt
77 #-r requirements_debug.txt
@@ -1,785 +1,796 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 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 importlib
23 import importlib
24 import base64
24 import base64
25 import msgpack
25 import msgpack
26 import dataclasses
26 import dataclasses
27 import pygit2
27 import pygit2
28
28
29 import http.client
29 import http.client
30 from celery import Celery
30
31
31
32
32 import mercurial.scmutil
33 import mercurial.scmutil
33 import mercurial.node
34 import mercurial.node
34
35
35 from vcsserver.lib.rc_json import json
36 from vcsserver.lib.rc_json import json
36 from vcsserver import exceptions, subprocessio, settings
37 from vcsserver import exceptions, subprocessio, settings
37 from vcsserver.str_utils import ascii_str, safe_str
38 from vcsserver.str_utils import ascii_str, safe_str
38 from vcsserver.remote.git_remote import Repository
39 from vcsserver.remote.git_remote import Repository
39
40
41 celery_app = Celery()
40 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
41
43
42
44
43 class HooksHttpClient:
45 class HooksHttpClient:
44 proto = 'msgpack.v1'
46 proto = 'msgpack.v1'
45 connection = None
47 connection = None
46
48
47 def __init__(self, hooks_uri):
49 def __init__(self, hooks_uri):
48 self.hooks_uri = hooks_uri
50 self.hooks_uri = hooks_uri
49
51
50 def __repr__(self):
52 def __repr__(self):
51 return f'{self.__class__}(hook_uri={self.hooks_uri}, proto={self.proto})'
53 return f'{self.__class__}(hook_uri={self.hooks_uri}, proto={self.proto})'
52
54
53 def __call__(self, method, extras):
55 def __call__(self, method, extras):
54 connection = http.client.HTTPConnection(self.hooks_uri)
56 connection = http.client.HTTPConnection(self.hooks_uri)
55 # binary msgpack body
57 # binary msgpack body
56 headers, body = self._serialize(method, extras)
58 headers, body = self._serialize(method, extras)
57 log.debug('Doing a new hooks call using HTTPConnection to %s', self.hooks_uri)
59 log.debug('Doing a new hooks call using HTTPConnection to %s', self.hooks_uri)
58
60
59 try:
61 try:
60 try:
62 try:
61 connection.request('POST', '/', body, headers)
63 connection.request('POST', '/', body, headers)
62 except Exception as error:
64 except Exception as error:
63 log.error('Hooks calling Connection failed on %s, org error: %s', connection.__dict__, error)
65 log.error('Hooks calling Connection failed on %s, org error: %s', connection.__dict__, error)
64 raise
66 raise
65
67
66 response = connection.getresponse()
68 response = connection.getresponse()
67 try:
69 try:
68 return msgpack.load(response)
70 return msgpack.load(response)
69 except Exception:
71 except Exception:
70 response_data = response.read()
72 response_data = response.read()
71 log.exception('Failed to decode hook response json data. '
73 log.exception('Failed to decode hook response json data. '
72 'response_code:%s, raw_data:%s',
74 'response_code:%s, raw_data:%s',
73 response.status, response_data)
75 response.status, response_data)
74 raise
76 raise
75 finally:
77 finally:
76 connection.close()
78 connection.close()
77
79
78 @classmethod
80 @classmethod
79 def _serialize(cls, hook_name, extras):
81 def _serialize(cls, hook_name, extras):
80 data = {
82 data = {
81 'method': hook_name,
83 'method': hook_name,
82 'extras': extras
84 'extras': extras
83 }
85 }
84 headers = {
86 headers = {
85 "rc-hooks-protocol": cls.proto,
87 "rc-hooks-protocol": cls.proto,
86 "Connection": "keep-alive"
88 "Connection": "keep-alive"
87 }
89 }
88 return headers, msgpack.packb(data)
90 return headers, msgpack.packb(data)
89
91
90
92
91 class HooksDummyClient:
93 class HooksCeleryClient:
92 def __init__(self, hooks_module):
94 TASK_TIMEOUT = 60 # time in seconds
93 log.debug('HooksDummyClient import: %s', hooks_module)
94 self._hooks_module = importlib.import_module(hooks_module)
95
95
96 def __call__(self, hook_name, extras):
96 def __init__(self, queue, backend):
97 with self._hooks_module.Hooks() as hooks:
97 celery_app.config_from_object({'broker_url': queue, 'result_backend': backend,
98 return getattr(hooks, hook_name)(extras)
98 'broker_connection_retry_on_startup': True,
99 'task_serializer': 'msgpack',
100 'accept_content': ['json', 'msgpack'],
101 'result_serializer': 'msgpack',
102 'result_accept_content': ['json', 'msgpack']
103 })
104 self.celery_app = celery_app
105
106 def __call__(self, method, extras):
107 inquired_task = self.celery_app.signature(
108 f'rhodecode.lib.celerylib.tasks.{method}'
109 )
110 return inquired_task.delay(extras).get(timeout=self.TASK_TIMEOUT)
99
111
100
112
101 class HooksShadowRepoClient:
113 class HooksShadowRepoClient:
102
114
103 def __call__(self, hook_name, extras):
115 def __call__(self, hook_name, extras):
104 return {'output': '', 'status': 0}
116 return {'output': '', 'status': 0}
105
117
106
118
107 class RemoteMessageWriter:
119 class RemoteMessageWriter:
108 """Writer base class."""
120 """Writer base class."""
109 def write(self, message):
121 def write(self, message):
110 raise NotImplementedError()
122 raise NotImplementedError()
111
123
112
124
113 class HgMessageWriter(RemoteMessageWriter):
125 class HgMessageWriter(RemoteMessageWriter):
114 """Writer that knows how to send messages to mercurial clients."""
126 """Writer that knows how to send messages to mercurial clients."""
115
127
116 def __init__(self, ui):
128 def __init__(self, ui):
117 self.ui = ui
129 self.ui = ui
118
130
119 def write(self, message: str):
131 def write(self, message: str):
120 # TODO: Check why the quiet flag is set by default.
132 # TODO: Check why the quiet flag is set by default.
121 old = self.ui.quiet
133 old = self.ui.quiet
122 self.ui.quiet = False
134 self.ui.quiet = False
123 self.ui.status(message.encode('utf-8'))
135 self.ui.status(message.encode('utf-8'))
124 self.ui.quiet = old
136 self.ui.quiet = old
125
137
126
138
127 class GitMessageWriter(RemoteMessageWriter):
139 class GitMessageWriter(RemoteMessageWriter):
128 """Writer that knows how to send messages to git clients."""
140 """Writer that knows how to send messages to git clients."""
129
141
130 def __init__(self, stdout=None):
142 def __init__(self, stdout=None):
131 self.stdout = stdout or sys.stdout
143 self.stdout = stdout or sys.stdout
132
144
133 def write(self, message: str):
145 def write(self, message: str):
134 self.stdout.write(message)
146 self.stdout.write(message)
135
147
136
148
137 class SvnMessageWriter(RemoteMessageWriter):
149 class SvnMessageWriter(RemoteMessageWriter):
138 """Writer that knows how to send messages to svn clients."""
150 """Writer that knows how to send messages to svn clients."""
139
151
140 def __init__(self, stderr=None):
152 def __init__(self, stderr=None):
141 # SVN needs data sent to stderr for back-to-client messaging
153 # SVN needs data sent to stderr for back-to-client messaging
142 self.stderr = stderr or sys.stderr
154 self.stderr = stderr or sys.stderr
143
155
144 def write(self, message):
156 def write(self, message):
145 self.stderr.write(message.encode('utf-8'))
157 self.stderr.write(message.encode('utf-8'))
146
158
147
159
148 def _handle_exception(result):
160 def _handle_exception(result):
149 exception_class = result.get('exception')
161 exception_class = result.get('exception')
150 exception_traceback = result.get('exception_traceback')
162 exception_traceback = result.get('exception_traceback')
151 log.debug('Handling hook-call exception: %s', exception_class)
163 log.debug('Handling hook-call exception: %s', exception_class)
152
164
153 if exception_traceback:
165 if exception_traceback:
154 log.error('Got traceback from remote call:%s', exception_traceback)
166 log.error('Got traceback from remote call:%s', exception_traceback)
155
167
156 if exception_class == 'HTTPLockedRC':
168 if exception_class == 'HTTPLockedRC':
157 raise exceptions.RepositoryLockedException()(*result['exception_args'])
169 raise exceptions.RepositoryLockedException()(*result['exception_args'])
158 elif exception_class == 'HTTPBranchProtected':
170 elif exception_class == 'HTTPBranchProtected':
159 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
171 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
160 elif exception_class == 'RepositoryError':
172 elif exception_class == 'RepositoryError':
161 raise exceptions.VcsException()(*result['exception_args'])
173 raise exceptions.VcsException()(*result['exception_args'])
162 elif exception_class:
174 elif exception_class:
163 raise Exception(
175 raise Exception(
164 f"""Got remote exception "{exception_class}" with args "{result['exception_args']}" """
176 f"""Got remote exception "{exception_class}" with args "{result['exception_args']}" """
165 )
177 )
166
178
167
179
168 def _get_hooks_client(extras):
180 def _get_hooks_client(extras):
169 hooks_uri = extras.get('hooks_uri')
181 hooks_uri = extras.get('hooks_uri')
182 task_queue = extras.get('task_queue')
183 task_backend = extras.get('task_backend')
170 is_shadow_repo = extras.get('is_shadow_repo')
184 is_shadow_repo = extras.get('is_shadow_repo')
171
185
172 if hooks_uri:
186 if hooks_uri:
173 return HooksHttpClient(extras['hooks_uri'])
187 return HooksHttpClient(hooks_uri)
188 elif task_queue and task_backend:
189 return HooksCeleryClient(task_queue, task_backend)
174 elif is_shadow_repo:
190 elif is_shadow_repo:
175 return HooksShadowRepoClient()
191 return HooksShadowRepoClient()
176 else:
192 else:
177 try:
193 raise Exception("Hooks client not found!")
178 import_module = extras['hooks_module']
179 except KeyError:
180 log.error('Failed to get "hooks_module" from extras: %s', extras)
181 raise
182 return HooksDummyClient(import_module)
183
194
184
195
185 def _call_hook(hook_name, extras, writer):
196 def _call_hook(hook_name, extras, writer):
186 hooks_client = _get_hooks_client(extras)
197 hooks_client = _get_hooks_client(extras)
187 log.debug('Hooks, using client:%s', hooks_client)
198 log.debug('Hooks, using client:%s', hooks_client)
188 result = hooks_client(hook_name, extras)
199 result = hooks_client(hook_name, extras)
189 log.debug('Hooks got result: %s', result)
200 log.debug('Hooks got result: %s', result)
190 _handle_exception(result)
201 _handle_exception(result)
191 writer.write(result['output'])
202 writer.write(result['output'])
192
203
193 return result['status']
204 return result['status']
194
205
195
206
196 def _extras_from_ui(ui):
207 def _extras_from_ui(ui):
197 hook_data = ui.config(b'rhodecode', b'RC_SCM_DATA')
208 hook_data = ui.config(b'rhodecode', b'RC_SCM_DATA')
198 if not hook_data:
209 if not hook_data:
199 # maybe it's inside environ ?
210 # maybe it's inside environ ?
200 env_hook_data = os.environ.get('RC_SCM_DATA')
211 env_hook_data = os.environ.get('RC_SCM_DATA')
201 if env_hook_data:
212 if env_hook_data:
202 hook_data = env_hook_data
213 hook_data = env_hook_data
203
214
204 extras = {}
215 extras = {}
205 if hook_data:
216 if hook_data:
206 extras = json.loads(hook_data)
217 extras = json.loads(hook_data)
207 return extras
218 return extras
208
219
209
220
210 def _rev_range_hash(repo, node, check_heads=False):
221 def _rev_range_hash(repo, node, check_heads=False):
211 from vcsserver.hgcompat import get_ctx
222 from vcsserver.hgcompat import get_ctx
212
223
213 commits = []
224 commits = []
214 revs = []
225 revs = []
215 start = get_ctx(repo, node).rev()
226 start = get_ctx(repo, node).rev()
216 end = len(repo)
227 end = len(repo)
217 for rev in range(start, end):
228 for rev in range(start, end):
218 revs.append(rev)
229 revs.append(rev)
219 ctx = get_ctx(repo, rev)
230 ctx = get_ctx(repo, rev)
220 commit_id = ascii_str(mercurial.node.hex(ctx.node()))
231 commit_id = ascii_str(mercurial.node.hex(ctx.node()))
221 branch = safe_str(ctx.branch())
232 branch = safe_str(ctx.branch())
222 commits.append((commit_id, branch))
233 commits.append((commit_id, branch))
223
234
224 parent_heads = []
235 parent_heads = []
225 if check_heads:
236 if check_heads:
226 parent_heads = _check_heads(repo, start, end, revs)
237 parent_heads = _check_heads(repo, start, end, revs)
227 return commits, parent_heads
238 return commits, parent_heads
228
239
229
240
230 def _check_heads(repo, start, end, commits):
241 def _check_heads(repo, start, end, commits):
231 from vcsserver.hgcompat import get_ctx
242 from vcsserver.hgcompat import get_ctx
232 changelog = repo.changelog
243 changelog = repo.changelog
233 parents = set()
244 parents = set()
234
245
235 for new_rev in commits:
246 for new_rev in commits:
236 for p in changelog.parentrevs(new_rev):
247 for p in changelog.parentrevs(new_rev):
237 if p == mercurial.node.nullrev:
248 if p == mercurial.node.nullrev:
238 continue
249 continue
239 if p < start:
250 if p < start:
240 parents.add(p)
251 parents.add(p)
241
252
242 for p in parents:
253 for p in parents:
243 branch = get_ctx(repo, p).branch()
254 branch = get_ctx(repo, p).branch()
244 # The heads descending from that parent, on the same branch
255 # The heads descending from that parent, on the same branch
245 parent_heads = {p}
256 parent_heads = {p}
246 reachable = {p}
257 reachable = {p}
247 for x in range(p + 1, end):
258 for x in range(p + 1, end):
248 if get_ctx(repo, x).branch() != branch:
259 if get_ctx(repo, x).branch() != branch:
249 continue
260 continue
250 for pp in changelog.parentrevs(x):
261 for pp in changelog.parentrevs(x):
251 if pp in reachable:
262 if pp in reachable:
252 reachable.add(x)
263 reachable.add(x)
253 parent_heads.discard(pp)
264 parent_heads.discard(pp)
254 parent_heads.add(x)
265 parent_heads.add(x)
255 # More than one head? Suggest merging
266 # More than one head? Suggest merging
256 if len(parent_heads) > 1:
267 if len(parent_heads) > 1:
257 return list(parent_heads)
268 return list(parent_heads)
258
269
259 return []
270 return []
260
271
261
272
262 def _get_git_env():
273 def _get_git_env():
263 env = {}
274 env = {}
264 for k, v in os.environ.items():
275 for k, v in os.environ.items():
265 if k.startswith('GIT'):
276 if k.startswith('GIT'):
266 env[k] = v
277 env[k] = v
267
278
268 # serialized version
279 # serialized version
269 return [(k, v) for k, v in env.items()]
280 return [(k, v) for k, v in env.items()]
270
281
271
282
272 def _get_hg_env(old_rev, new_rev, txnid, repo_path):
283 def _get_hg_env(old_rev, new_rev, txnid, repo_path):
273 env = {}
284 env = {}
274 for k, v in os.environ.items():
285 for k, v in os.environ.items():
275 if k.startswith('HG'):
286 if k.startswith('HG'):
276 env[k] = v
287 env[k] = v
277
288
278 env['HG_NODE'] = old_rev
289 env['HG_NODE'] = old_rev
279 env['HG_NODE_LAST'] = new_rev
290 env['HG_NODE_LAST'] = new_rev
280 env['HG_TXNID'] = txnid
291 env['HG_TXNID'] = txnid
281 env['HG_PENDING'] = repo_path
292 env['HG_PENDING'] = repo_path
282
293
283 return [(k, v) for k, v in env.items()]
294 return [(k, v) for k, v in env.items()]
284
295
285
296
286 def repo_size(ui, repo, **kwargs):
297 def repo_size(ui, repo, **kwargs):
287 extras = _extras_from_ui(ui)
298 extras = _extras_from_ui(ui)
288 return _call_hook('repo_size', extras, HgMessageWriter(ui))
299 return _call_hook('repo_size', extras, HgMessageWriter(ui))
289
300
290
301
291 def pre_pull(ui, repo, **kwargs):
302 def pre_pull(ui, repo, **kwargs):
292 extras = _extras_from_ui(ui)
303 extras = _extras_from_ui(ui)
293 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
304 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
294
305
295
306
296 def pre_pull_ssh(ui, repo, **kwargs):
307 def pre_pull_ssh(ui, repo, **kwargs):
297 extras = _extras_from_ui(ui)
308 extras = _extras_from_ui(ui)
298 if extras and extras.get('SSH'):
309 if extras and extras.get('SSH'):
299 return pre_pull(ui, repo, **kwargs)
310 return pre_pull(ui, repo, **kwargs)
300 return 0
311 return 0
301
312
302
313
303 def post_pull(ui, repo, **kwargs):
314 def post_pull(ui, repo, **kwargs):
304 extras = _extras_from_ui(ui)
315 extras = _extras_from_ui(ui)
305 return _call_hook('post_pull', extras, HgMessageWriter(ui))
316 return _call_hook('post_pull', extras, HgMessageWriter(ui))
306
317
307
318
308 def post_pull_ssh(ui, repo, **kwargs):
319 def post_pull_ssh(ui, repo, **kwargs):
309 extras = _extras_from_ui(ui)
320 extras = _extras_from_ui(ui)
310 if extras and extras.get('SSH'):
321 if extras and extras.get('SSH'):
311 return post_pull(ui, repo, **kwargs)
322 return post_pull(ui, repo, **kwargs)
312 return 0
323 return 0
313
324
314
325
315 def pre_push(ui, repo, node=None, **kwargs):
326 def pre_push(ui, repo, node=None, **kwargs):
316 """
327 """
317 Mercurial pre_push hook
328 Mercurial pre_push hook
318 """
329 """
319 extras = _extras_from_ui(ui)
330 extras = _extras_from_ui(ui)
320 detect_force_push = extras.get('detect_force_push')
331 detect_force_push = extras.get('detect_force_push')
321
332
322 rev_data = []
333 rev_data = []
323 hook_type: str = safe_str(kwargs.get('hooktype'))
334 hook_type: str = safe_str(kwargs.get('hooktype'))
324
335
325 if node and hook_type == 'pretxnchangegroup':
336 if node and hook_type == 'pretxnchangegroup':
326 branches = collections.defaultdict(list)
337 branches = collections.defaultdict(list)
327 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
338 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
328 for commit_id, branch in commits:
339 for commit_id, branch in commits:
329 branches[branch].append(commit_id)
340 branches[branch].append(commit_id)
330
341
331 for branch, commits in branches.items():
342 for branch, commits in branches.items():
332 old_rev = ascii_str(kwargs.get('node_last')) or commits[0]
343 old_rev = ascii_str(kwargs.get('node_last')) or commits[0]
333 rev_data.append({
344 rev_data.append({
334 'total_commits': len(commits),
345 'total_commits': len(commits),
335 'old_rev': old_rev,
346 'old_rev': old_rev,
336 'new_rev': commits[-1],
347 'new_rev': commits[-1],
337 'ref': '',
348 'ref': '',
338 'type': 'branch',
349 'type': 'branch',
339 'name': branch,
350 'name': branch,
340 })
351 })
341
352
342 for push_ref in rev_data:
353 for push_ref in rev_data:
343 push_ref['multiple_heads'] = _heads
354 push_ref['multiple_heads'] = _heads
344
355
345 repo_path = os.path.join(
356 repo_path = os.path.join(
346 extras.get('repo_store', ''), extras.get('repository', ''))
357 extras.get('repo_store', ''), extras.get('repository', ''))
347 push_ref['hg_env'] = _get_hg_env(
358 push_ref['hg_env'] = _get_hg_env(
348 old_rev=push_ref['old_rev'],
359 old_rev=push_ref['old_rev'],
349 new_rev=push_ref['new_rev'], txnid=ascii_str(kwargs.get('txnid')),
360 new_rev=push_ref['new_rev'], txnid=ascii_str(kwargs.get('txnid')),
350 repo_path=repo_path)
361 repo_path=repo_path)
351
362
352 extras['hook_type'] = hook_type or 'pre_push'
363 extras['hook_type'] = hook_type or 'pre_push'
353 extras['commit_ids'] = rev_data
364 extras['commit_ids'] = rev_data
354
365
355 return _call_hook('pre_push', extras, HgMessageWriter(ui))
366 return _call_hook('pre_push', extras, HgMessageWriter(ui))
356
367
357
368
358 def pre_push_ssh(ui, repo, node=None, **kwargs):
369 def pre_push_ssh(ui, repo, node=None, **kwargs):
359 extras = _extras_from_ui(ui)
370 extras = _extras_from_ui(ui)
360 if extras.get('SSH'):
371 if extras.get('SSH'):
361 return pre_push(ui, repo, node, **kwargs)
372 return pre_push(ui, repo, node, **kwargs)
362
373
363 return 0
374 return 0
364
375
365
376
366 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
377 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
367 """
378 """
368 Mercurial pre_push hook for SSH
379 Mercurial pre_push hook for SSH
369 """
380 """
370 extras = _extras_from_ui(ui)
381 extras = _extras_from_ui(ui)
371 if extras.get('SSH'):
382 if extras.get('SSH'):
372 permission = extras['SSH_PERMISSIONS']
383 permission = extras['SSH_PERMISSIONS']
373
384
374 if 'repository.write' == permission or 'repository.admin' == permission:
385 if 'repository.write' == permission or 'repository.admin' == permission:
375 return 0
386 return 0
376
387
377 # non-zero ret code
388 # non-zero ret code
378 return 1
389 return 1
379
390
380 return 0
391 return 0
381
392
382
393
383 def post_push(ui, repo, node, **kwargs):
394 def post_push(ui, repo, node, **kwargs):
384 """
395 """
385 Mercurial post_push hook
396 Mercurial post_push hook
386 """
397 """
387 extras = _extras_from_ui(ui)
398 extras = _extras_from_ui(ui)
388
399
389 commit_ids = []
400 commit_ids = []
390 branches = []
401 branches = []
391 bookmarks = []
402 bookmarks = []
392 tags = []
403 tags = []
393 hook_type: str = safe_str(kwargs.get('hooktype'))
404 hook_type: str = safe_str(kwargs.get('hooktype'))
394
405
395 commits, _heads = _rev_range_hash(repo, node)
406 commits, _heads = _rev_range_hash(repo, node)
396 for commit_id, branch in commits:
407 for commit_id, branch in commits:
397 commit_ids.append(commit_id)
408 commit_ids.append(commit_id)
398 if branch not in branches:
409 if branch not in branches:
399 branches.append(branch)
410 branches.append(branch)
400
411
401 if hasattr(ui, '_rc_pushkey_bookmarks'):
412 if hasattr(ui, '_rc_pushkey_bookmarks'):
402 bookmarks = ui._rc_pushkey_bookmarks
413 bookmarks = ui._rc_pushkey_bookmarks
403
414
404 extras['hook_type'] = hook_type or 'post_push'
415 extras['hook_type'] = hook_type or 'post_push'
405 extras['commit_ids'] = commit_ids
416 extras['commit_ids'] = commit_ids
406
417
407 extras['new_refs'] = {
418 extras['new_refs'] = {
408 'branches': branches,
419 'branches': branches,
409 'bookmarks': bookmarks,
420 'bookmarks': bookmarks,
410 'tags': tags
421 'tags': tags
411 }
422 }
412
423
413 return _call_hook('post_push', extras, HgMessageWriter(ui))
424 return _call_hook('post_push', extras, HgMessageWriter(ui))
414
425
415
426
416 def post_push_ssh(ui, repo, node, **kwargs):
427 def post_push_ssh(ui, repo, node, **kwargs):
417 """
428 """
418 Mercurial post_push hook for SSH
429 Mercurial post_push hook for SSH
419 """
430 """
420 if _extras_from_ui(ui).get('SSH'):
431 if _extras_from_ui(ui).get('SSH'):
421 return post_push(ui, repo, node, **kwargs)
432 return post_push(ui, repo, node, **kwargs)
422 return 0
433 return 0
423
434
424
435
425 def key_push(ui, repo, **kwargs):
436 def key_push(ui, repo, **kwargs):
426 from vcsserver.hgcompat import get_ctx
437 from vcsserver.hgcompat import get_ctx
427
438
428 if kwargs['new'] != b'0' and kwargs['namespace'] == b'bookmarks':
439 if kwargs['new'] != b'0' and kwargs['namespace'] == b'bookmarks':
429 # store new bookmarks in our UI object propagated later to post_push
440 # store new bookmarks in our UI object propagated later to post_push
430 ui._rc_pushkey_bookmarks = get_ctx(repo, kwargs['key']).bookmarks()
441 ui._rc_pushkey_bookmarks = get_ctx(repo, kwargs['key']).bookmarks()
431 return
442 return
432
443
433
444
434 # backward compat
445 # backward compat
435 log_pull_action = post_pull
446 log_pull_action = post_pull
436
447
437 # backward compat
448 # backward compat
438 log_push_action = post_push
449 log_push_action = post_push
439
450
440
451
441 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
452 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
442 """
453 """
443 Old hook name: keep here for backward compatibility.
454 Old hook name: keep here for backward compatibility.
444
455
445 This is only required when the installed git hooks are not upgraded.
456 This is only required when the installed git hooks are not upgraded.
446 """
457 """
447 pass
458 pass
448
459
449
460
450 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
461 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
451 """
462 """
452 Old hook name: keep here for backward compatibility.
463 Old hook name: keep here for backward compatibility.
453
464
454 This is only required when the installed git hooks are not upgraded.
465 This is only required when the installed git hooks are not upgraded.
455 """
466 """
456 pass
467 pass
457
468
458
469
459 @dataclasses.dataclass
470 @dataclasses.dataclass
460 class HookResponse:
471 class HookResponse:
461 status: int
472 status: int
462 output: str
473 output: str
463
474
464
475
465 def git_pre_pull(extras) -> HookResponse:
476 def git_pre_pull(extras) -> HookResponse:
466 """
477 """
467 Pre pull hook.
478 Pre pull hook.
468
479
469 :param extras: dictionary containing the keys defined in simplevcs
480 :param extras: dictionary containing the keys defined in simplevcs
470 :type extras: dict
481 :type extras: dict
471
482
472 :return: status code of the hook. 0 for success.
483 :return: status code of the hook. 0 for success.
473 :rtype: int
484 :rtype: int
474 """
485 """
475
486
476 if 'pull' not in extras['hooks']:
487 if 'pull' not in extras['hooks']:
477 return HookResponse(0, '')
488 return HookResponse(0, '')
478
489
479 stdout = io.StringIO()
490 stdout = io.StringIO()
480 try:
491 try:
481 status_code = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
492 status_code = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
482
493
483 except Exception as error:
494 except Exception as error:
484 log.exception('Failed to call pre_pull hook')
495 log.exception('Failed to call pre_pull hook')
485 status_code = 128
496 status_code = 128
486 stdout.write(f'ERROR: {error}\n')
497 stdout.write(f'ERROR: {error}\n')
487
498
488 return HookResponse(status_code, stdout.getvalue())
499 return HookResponse(status_code, stdout.getvalue())
489
500
490
501
491 def git_post_pull(extras) -> HookResponse:
502 def git_post_pull(extras) -> HookResponse:
492 """
503 """
493 Post pull hook.
504 Post pull hook.
494
505
495 :param extras: dictionary containing the keys defined in simplevcs
506 :param extras: dictionary containing the keys defined in simplevcs
496 :type extras: dict
507 :type extras: dict
497
508
498 :return: status code of the hook. 0 for success.
509 :return: status code of the hook. 0 for success.
499 :rtype: int
510 :rtype: int
500 """
511 """
501 if 'pull' not in extras['hooks']:
512 if 'pull' not in extras['hooks']:
502 return HookResponse(0, '')
513 return HookResponse(0, '')
503
514
504 stdout = io.StringIO()
515 stdout = io.StringIO()
505 try:
516 try:
506 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
517 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
507 except Exception as error:
518 except Exception as error:
508 status = 128
519 status = 128
509 stdout.write(f'ERROR: {error}\n')
520 stdout.write(f'ERROR: {error}\n')
510
521
511 return HookResponse(status, stdout.getvalue())
522 return HookResponse(status, stdout.getvalue())
512
523
513
524
514 def _parse_git_ref_lines(revision_lines):
525 def _parse_git_ref_lines(revision_lines):
515 rev_data = []
526 rev_data = []
516 for revision_line in revision_lines or []:
527 for revision_line in revision_lines or []:
517 old_rev, new_rev, ref = revision_line.strip().split(' ')
528 old_rev, new_rev, ref = revision_line.strip().split(' ')
518 ref_data = ref.split('/', 2)
529 ref_data = ref.split('/', 2)
519 if ref_data[1] in ('tags', 'heads'):
530 if ref_data[1] in ('tags', 'heads'):
520 rev_data.append({
531 rev_data.append({
521 # NOTE(marcink):
532 # NOTE(marcink):
522 # we're unable to tell total_commits for git at this point
533 # we're unable to tell total_commits for git at this point
523 # but we set the variable for consistency with GIT
534 # but we set the variable for consistency with GIT
524 'total_commits': -1,
535 'total_commits': -1,
525 'old_rev': old_rev,
536 'old_rev': old_rev,
526 'new_rev': new_rev,
537 'new_rev': new_rev,
527 'ref': ref,
538 'ref': ref,
528 'type': ref_data[1],
539 'type': ref_data[1],
529 'name': ref_data[2],
540 'name': ref_data[2],
530 })
541 })
531 return rev_data
542 return rev_data
532
543
533
544
534 def git_pre_receive(unused_repo_path, revision_lines, env) -> int:
545 def git_pre_receive(unused_repo_path, revision_lines, env) -> int:
535 """
546 """
536 Pre push hook.
547 Pre push hook.
537
548
538 :return: status code of the hook. 0 for success.
549 :return: status code of the hook. 0 for success.
539 """
550 """
540 extras = json.loads(env['RC_SCM_DATA'])
551 extras = json.loads(env['RC_SCM_DATA'])
541 rev_data = _parse_git_ref_lines(revision_lines)
552 rev_data = _parse_git_ref_lines(revision_lines)
542 if 'push' not in extras['hooks']:
553 if 'push' not in extras['hooks']:
543 return 0
554 return 0
544 empty_commit_id = '0' * 40
555 empty_commit_id = '0' * 40
545
556
546 detect_force_push = extras.get('detect_force_push')
557 detect_force_push = extras.get('detect_force_push')
547
558
548 for push_ref in rev_data:
559 for push_ref in rev_data:
549 # store our git-env which holds the temp store
560 # store our git-env which holds the temp store
550 push_ref['git_env'] = _get_git_env()
561 push_ref['git_env'] = _get_git_env()
551 push_ref['pruned_sha'] = ''
562 push_ref['pruned_sha'] = ''
552 if not detect_force_push:
563 if not detect_force_push:
553 # don't check for forced-push when we don't need to
564 # don't check for forced-push when we don't need to
554 continue
565 continue
555
566
556 type_ = push_ref['type']
567 type_ = push_ref['type']
557 new_branch = push_ref['old_rev'] == empty_commit_id
568 new_branch = push_ref['old_rev'] == empty_commit_id
558 delete_branch = push_ref['new_rev'] == empty_commit_id
569 delete_branch = push_ref['new_rev'] == empty_commit_id
559 if type_ == 'heads' and not (new_branch or delete_branch):
570 if type_ == 'heads' and not (new_branch or delete_branch):
560 old_rev = push_ref['old_rev']
571 old_rev = push_ref['old_rev']
561 new_rev = push_ref['new_rev']
572 new_rev = push_ref['new_rev']
562 cmd = [settings.GIT_EXECUTABLE, 'rev-list', old_rev, f'^{new_rev}']
573 cmd = [settings.GIT_EXECUTABLE, 'rev-list', old_rev, f'^{new_rev}']
563 stdout, stderr = subprocessio.run_command(
574 stdout, stderr = subprocessio.run_command(
564 cmd, env=os.environ.copy())
575 cmd, env=os.environ.copy())
565 # means we're having some non-reachable objects, this forced push was used
576 # means we're having some non-reachable objects, this forced push was used
566 if stdout:
577 if stdout:
567 push_ref['pruned_sha'] = stdout.splitlines()
578 push_ref['pruned_sha'] = stdout.splitlines()
568
579
569 extras['hook_type'] = 'pre_receive'
580 extras['hook_type'] = 'pre_receive'
570 extras['commit_ids'] = rev_data
581 extras['commit_ids'] = rev_data
571
582
572 stdout = sys.stdout
583 stdout = sys.stdout
573 status_code = _call_hook('pre_push', extras, GitMessageWriter(stdout))
584 status_code = _call_hook('pre_push', extras, GitMessageWriter(stdout))
574
585
575 return status_code
586 return status_code
576
587
577
588
578 def git_post_receive(unused_repo_path, revision_lines, env) -> int:
589 def git_post_receive(unused_repo_path, revision_lines, env) -> int:
579 """
590 """
580 Post push hook.
591 Post push hook.
581
592
582 :return: status code of the hook. 0 for success.
593 :return: status code of the hook. 0 for success.
583 """
594 """
584 extras = json.loads(env['RC_SCM_DATA'])
595 extras = json.loads(env['RC_SCM_DATA'])
585 if 'push' not in extras['hooks']:
596 if 'push' not in extras['hooks']:
586 return 0
597 return 0
587
598
588 rev_data = _parse_git_ref_lines(revision_lines)
599 rev_data = _parse_git_ref_lines(revision_lines)
589
600
590 git_revs = []
601 git_revs = []
591
602
592 # N.B.(skreft): it is ok to just call git, as git before calling a
603 # N.B.(skreft): it is ok to just call git, as git before calling a
593 # subcommand sets the PATH environment variable so that it point to the
604 # subcommand sets the PATH environment variable so that it point to the
594 # correct version of the git executable.
605 # correct version of the git executable.
595 empty_commit_id = '0' * 40
606 empty_commit_id = '0' * 40
596 branches = []
607 branches = []
597 tags = []
608 tags = []
598 for push_ref in rev_data:
609 for push_ref in rev_data:
599 type_ = push_ref['type']
610 type_ = push_ref['type']
600
611
601 if type_ == 'heads':
612 if type_ == 'heads':
602 # starting new branch case
613 # starting new branch case
603 if push_ref['old_rev'] == empty_commit_id:
614 if push_ref['old_rev'] == empty_commit_id:
604 push_ref_name = push_ref['name']
615 push_ref_name = push_ref['name']
605
616
606 if push_ref_name not in branches:
617 if push_ref_name not in branches:
607 branches.append(push_ref_name)
618 branches.append(push_ref_name)
608
619
609 need_head_set = ''
620 need_head_set = ''
610 with Repository(os.getcwd()) as repo:
621 with Repository(os.getcwd()) as repo:
611 try:
622 try:
612 repo.head
623 repo.head
613 except pygit2.GitError:
624 except pygit2.GitError:
614 need_head_set = f'refs/heads/{push_ref_name}'
625 need_head_set = f'refs/heads/{push_ref_name}'
615
626
616 if need_head_set:
627 if need_head_set:
617 repo.set_head(need_head_set)
628 repo.set_head(need_head_set)
618 print(f"Setting default branch to {push_ref_name}")
629 print(f"Setting default branch to {push_ref_name}")
619
630
620 cmd = [settings.GIT_EXECUTABLE, 'for-each-ref', '--format=%(refname)', 'refs/heads/*']
631 cmd = [settings.GIT_EXECUTABLE, 'for-each-ref', '--format=%(refname)', 'refs/heads/*']
621 stdout, stderr = subprocessio.run_command(
632 stdout, stderr = subprocessio.run_command(
622 cmd, env=os.environ.copy())
633 cmd, env=os.environ.copy())
623 heads = safe_str(stdout)
634 heads = safe_str(stdout)
624 heads = heads.replace(push_ref['ref'], '')
635 heads = heads.replace(push_ref['ref'], '')
625 heads = ' '.join(head for head
636 heads = ' '.join(head for head
626 in heads.splitlines() if head) or '.'
637 in heads.splitlines() if head) or '.'
627 cmd = [settings.GIT_EXECUTABLE, 'log', '--reverse',
638 cmd = [settings.GIT_EXECUTABLE, 'log', '--reverse',
628 '--pretty=format:%H', '--', push_ref['new_rev'],
639 '--pretty=format:%H', '--', push_ref['new_rev'],
629 '--not', heads]
640 '--not', heads]
630 stdout, stderr = subprocessio.run_command(
641 stdout, stderr = subprocessio.run_command(
631 cmd, env=os.environ.copy())
642 cmd, env=os.environ.copy())
632 git_revs.extend(list(map(ascii_str, stdout.splitlines())))
643 git_revs.extend(list(map(ascii_str, stdout.splitlines())))
633
644
634 # delete branch case
645 # delete branch case
635 elif push_ref['new_rev'] == empty_commit_id:
646 elif push_ref['new_rev'] == empty_commit_id:
636 git_revs.append(f'delete_branch=>{push_ref["name"]}')
647 git_revs.append(f'delete_branch=>{push_ref["name"]}')
637 else:
648 else:
638 if push_ref['name'] not in branches:
649 if push_ref['name'] not in branches:
639 branches.append(push_ref['name'])
650 branches.append(push_ref['name'])
640
651
641 cmd = [settings.GIT_EXECUTABLE, 'log',
652 cmd = [settings.GIT_EXECUTABLE, 'log',
642 f'{push_ref["old_rev"]}..{push_ref["new_rev"]}',
653 f'{push_ref["old_rev"]}..{push_ref["new_rev"]}',
643 '--reverse', '--pretty=format:%H']
654 '--reverse', '--pretty=format:%H']
644 stdout, stderr = subprocessio.run_command(
655 stdout, stderr = subprocessio.run_command(
645 cmd, env=os.environ.copy())
656 cmd, env=os.environ.copy())
646 # we get bytes from stdout, we need str to be consistent
657 # we get bytes from stdout, we need str to be consistent
647 log_revs = list(map(ascii_str, stdout.splitlines()))
658 log_revs = list(map(ascii_str, stdout.splitlines()))
648 git_revs.extend(log_revs)
659 git_revs.extend(log_revs)
649
660
650 # Pure pygit2 impl. but still 2-3x slower :/
661 # Pure pygit2 impl. but still 2-3x slower :/
651 # results = []
662 # results = []
652 #
663 #
653 # with Repository(os.getcwd()) as repo:
664 # with Repository(os.getcwd()) as repo:
654 # repo_new_rev = repo[push_ref['new_rev']]
665 # repo_new_rev = repo[push_ref['new_rev']]
655 # repo_old_rev = repo[push_ref['old_rev']]
666 # repo_old_rev = repo[push_ref['old_rev']]
656 # walker = repo.walk(repo_new_rev.id, pygit2.GIT_SORT_TOPOLOGICAL)
667 # walker = repo.walk(repo_new_rev.id, pygit2.GIT_SORT_TOPOLOGICAL)
657 #
668 #
658 # for commit in walker:
669 # for commit in walker:
659 # if commit.id == repo_old_rev.id:
670 # if commit.id == repo_old_rev.id:
660 # break
671 # break
661 # results.append(commit.id.hex)
672 # results.append(commit.id.hex)
662 # # reverse the order, can't use GIT_SORT_REVERSE
673 # # reverse the order, can't use GIT_SORT_REVERSE
663 # log_revs = results[::-1]
674 # log_revs = results[::-1]
664
675
665 elif type_ == 'tags':
676 elif type_ == 'tags':
666 if push_ref['name'] not in tags:
677 if push_ref['name'] not in tags:
667 tags.append(push_ref['name'])
678 tags.append(push_ref['name'])
668 git_revs.append(f'tag=>{push_ref["name"]}')
679 git_revs.append(f'tag=>{push_ref["name"]}')
669
680
670 extras['hook_type'] = 'post_receive'
681 extras['hook_type'] = 'post_receive'
671 extras['commit_ids'] = git_revs
682 extras['commit_ids'] = git_revs
672 extras['new_refs'] = {
683 extras['new_refs'] = {
673 'branches': branches,
684 'branches': branches,
674 'bookmarks': [],
685 'bookmarks': [],
675 'tags': tags,
686 'tags': tags,
676 }
687 }
677
688
678 stdout = sys.stdout
689 stdout = sys.stdout
679
690
680 if 'repo_size' in extras['hooks']:
691 if 'repo_size' in extras['hooks']:
681 try:
692 try:
682 _call_hook('repo_size', extras, GitMessageWriter(stdout))
693 _call_hook('repo_size', extras, GitMessageWriter(stdout))
683 except Exception:
694 except Exception:
684 pass
695 pass
685
696
686 status_code = _call_hook('post_push', extras, GitMessageWriter(stdout))
697 status_code = _call_hook('post_push', extras, GitMessageWriter(stdout))
687 return status_code
698 return status_code
688
699
689
700
690 def _get_extras_from_txn_id(path, txn_id):
701 def _get_extras_from_txn_id(path, txn_id):
691 extras = {}
702 extras = {}
692 try:
703 try:
693 cmd = [settings.SVNLOOK_EXECUTABLE, 'pget',
704 cmd = [settings.SVNLOOK_EXECUTABLE, 'pget',
694 '-t', txn_id,
705 '-t', txn_id,
695 '--revprop', path, 'rc-scm-extras']
706 '--revprop', path, 'rc-scm-extras']
696 stdout, stderr = subprocessio.run_command(
707 stdout, stderr = subprocessio.run_command(
697 cmd, env=os.environ.copy())
708 cmd, env=os.environ.copy())
698 extras = json.loads(base64.urlsafe_b64decode(stdout))
709 extras = json.loads(base64.urlsafe_b64decode(stdout))
699 except Exception:
710 except Exception:
700 log.exception('Failed to extract extras info from txn_id')
711 log.exception('Failed to extract extras info from txn_id')
701
712
702 return extras
713 return extras
703
714
704
715
705 def _get_extras_from_commit_id(commit_id, path):
716 def _get_extras_from_commit_id(commit_id, path):
706 extras = {}
717 extras = {}
707 try:
718 try:
708 cmd = [settings.SVNLOOK_EXECUTABLE, 'pget',
719 cmd = [settings.SVNLOOK_EXECUTABLE, 'pget',
709 '-r', commit_id,
720 '-r', commit_id,
710 '--revprop', path, 'rc-scm-extras']
721 '--revprop', path, 'rc-scm-extras']
711 stdout, stderr = subprocessio.run_command(
722 stdout, stderr = subprocessio.run_command(
712 cmd, env=os.environ.copy())
723 cmd, env=os.environ.copy())
713 extras = json.loads(base64.urlsafe_b64decode(stdout))
724 extras = json.loads(base64.urlsafe_b64decode(stdout))
714 except Exception:
725 except Exception:
715 log.exception('Failed to extract extras info from commit_id')
726 log.exception('Failed to extract extras info from commit_id')
716
727
717 return extras
728 return extras
718
729
719
730
720 def svn_pre_commit(repo_path, commit_data, env):
731 def svn_pre_commit(repo_path, commit_data, env):
721 path, txn_id = commit_data
732 path, txn_id = commit_data
722 branches = []
733 branches = []
723 tags = []
734 tags = []
724
735
725 if env.get('RC_SCM_DATA'):
736 if env.get('RC_SCM_DATA'):
726 extras = json.loads(env['RC_SCM_DATA'])
737 extras = json.loads(env['RC_SCM_DATA'])
727 else:
738 else:
728 # fallback method to read from TXN-ID stored data
739 # fallback method to read from TXN-ID stored data
729 extras = _get_extras_from_txn_id(path, txn_id)
740 extras = _get_extras_from_txn_id(path, txn_id)
730 if not extras:
741 if not extras:
731 return 0
742 return 0
732
743
733 extras['hook_type'] = 'pre_commit'
744 extras['hook_type'] = 'pre_commit'
734 extras['commit_ids'] = [txn_id]
745 extras['commit_ids'] = [txn_id]
735 extras['txn_id'] = txn_id
746 extras['txn_id'] = txn_id
736 extras['new_refs'] = {
747 extras['new_refs'] = {
737 'total_commits': 1,
748 'total_commits': 1,
738 'branches': branches,
749 'branches': branches,
739 'bookmarks': [],
750 'bookmarks': [],
740 'tags': tags,
751 'tags': tags,
741 }
752 }
742
753
743 return _call_hook('pre_push', extras, SvnMessageWriter())
754 return _call_hook('pre_push', extras, SvnMessageWriter())
744
755
745
756
746 def svn_post_commit(repo_path, commit_data, env):
757 def svn_post_commit(repo_path, commit_data, env):
747 """
758 """
748 commit_data is path, rev, txn_id
759 commit_data is path, rev, txn_id
749 """
760 """
750 if len(commit_data) == 3:
761 if len(commit_data) == 3:
751 path, commit_id, txn_id = commit_data
762 path, commit_id, txn_id = commit_data
752 elif len(commit_data) == 2:
763 elif len(commit_data) == 2:
753 log.error('Failed to extract txn_id from commit_data using legacy method. '
764 log.error('Failed to extract txn_id from commit_data using legacy method. '
754 'Some functionality might be limited')
765 'Some functionality might be limited')
755 path, commit_id = commit_data
766 path, commit_id = commit_data
756 txn_id = None
767 txn_id = None
757
768
758 branches = []
769 branches = []
759 tags = []
770 tags = []
760
771
761 if env.get('RC_SCM_DATA'):
772 if env.get('RC_SCM_DATA'):
762 extras = json.loads(env['RC_SCM_DATA'])
773 extras = json.loads(env['RC_SCM_DATA'])
763 else:
774 else:
764 # fallback method to read from TXN-ID stored data
775 # fallback method to read from TXN-ID stored data
765 extras = _get_extras_from_commit_id(commit_id, path)
776 extras = _get_extras_from_commit_id(commit_id, path)
766 if not extras:
777 if not extras:
767 return 0
778 return 0
768
779
769 extras['hook_type'] = 'post_commit'
780 extras['hook_type'] = 'post_commit'
770 extras['commit_ids'] = [commit_id]
781 extras['commit_ids'] = [commit_id]
771 extras['txn_id'] = txn_id
782 extras['txn_id'] = txn_id
772 extras['new_refs'] = {
783 extras['new_refs'] = {
773 'branches': branches,
784 'branches': branches,
774 'bookmarks': [],
785 'bookmarks': [],
775 'tags': tags,
786 'tags': tags,
776 'total_commits': 1,
787 'total_commits': 1,
777 }
788 }
778
789
779 if 'repo_size' in extras['hooks']:
790 if 'repo_size' in extras['hooks']:
780 try:
791 try:
781 _call_hook('repo_size', extras, SvnMessageWriter())
792 _call_hook('repo_size', extras, SvnMessageWriter())
782 except Exception:
793 except Exception:
783 pass
794 pass
784
795
785 return _call_hook('post_push', extras, SvnMessageWriter())
796 return _call_hook('post_push', extras, SvnMessageWriter())
@@ -1,286 +1,257 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 threading
18 import threading
19 import msgpack
19 import msgpack
20
20
21 from http.server import BaseHTTPRequestHandler
21 from http.server import BaseHTTPRequestHandler
22 from socketserver import TCPServer
22 from socketserver import TCPServer
23
23
24 import mercurial.ui
24 import mercurial.ui
25 import mock
25 import mock
26 import pytest
26 import pytest
27
27
28 from vcsserver.hooks import HooksHttpClient
28 from vcsserver.hooks import HooksHttpClient
29 from vcsserver.lib.rc_json import json
29 from vcsserver.lib.rc_json import json
30 from vcsserver import hooks
30 from vcsserver import hooks
31
31
32
32
33 def get_hg_ui(extras=None):
33 def get_hg_ui(extras=None):
34 """Create a Config object with a valid RC_SCM_DATA entry."""
34 """Create a Config object with a valid RC_SCM_DATA entry."""
35 extras = extras or {}
35 extras = extras or {}
36 required_extras = {
36 required_extras = {
37 'username': '',
37 'username': '',
38 'repository': '',
38 'repository': '',
39 'locked_by': '',
39 'locked_by': '',
40 'scm': '',
40 'scm': '',
41 'make_lock': '',
41 'make_lock': '',
42 'action': '',
42 'action': '',
43 'ip': '',
43 'ip': '',
44 'hooks_uri': 'fake_hooks_uri',
44 'hooks_uri': 'fake_hooks_uri',
45 }
45 }
46 required_extras.update(extras)
46 required_extras.update(extras)
47 hg_ui = mercurial.ui.ui()
47 hg_ui = mercurial.ui.ui()
48 hg_ui.setconfig(b'rhodecode', b'RC_SCM_DATA', json.dumps(required_extras))
48 hg_ui.setconfig(b'rhodecode', b'RC_SCM_DATA', json.dumps(required_extras))
49
49
50 return hg_ui
50 return hg_ui
51
51
52
52
53 def test_git_pre_receive_is_disabled():
53 def test_git_pre_receive_is_disabled():
54 extras = {'hooks': ['pull']}
54 extras = {'hooks': ['pull']}
55 response = hooks.git_pre_receive(None, None,
55 response = hooks.git_pre_receive(None, None,
56 {'RC_SCM_DATA': json.dumps(extras)})
56 {'RC_SCM_DATA': json.dumps(extras)})
57
57
58 assert response == 0
58 assert response == 0
59
59
60
60
61 def test_git_post_receive_is_disabled():
61 def test_git_post_receive_is_disabled():
62 extras = {'hooks': ['pull']}
62 extras = {'hooks': ['pull']}
63 response = hooks.git_post_receive(None, '',
63 response = hooks.git_post_receive(None, '',
64 {'RC_SCM_DATA': json.dumps(extras)})
64 {'RC_SCM_DATA': json.dumps(extras)})
65
65
66 assert response == 0
66 assert response == 0
67
67
68
68
69 def test_git_post_receive_calls_repo_size():
69 def test_git_post_receive_calls_repo_size():
70 extras = {'hooks': ['push', 'repo_size']}
70 extras = {'hooks': ['push', 'repo_size']}
71
71
72 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
72 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
73 hooks.git_post_receive(
73 hooks.git_post_receive(
74 None, '', {'RC_SCM_DATA': json.dumps(extras)})
74 None, '', {'RC_SCM_DATA': json.dumps(extras)})
75 extras.update({'commit_ids': [], 'hook_type': 'post_receive',
75 extras.update({'commit_ids': [], 'hook_type': 'post_receive',
76 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
76 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
77 expected_calls = [
77 expected_calls = [
78 mock.call('repo_size', extras, mock.ANY),
78 mock.call('repo_size', extras, mock.ANY),
79 mock.call('post_push', extras, mock.ANY),
79 mock.call('post_push', extras, mock.ANY),
80 ]
80 ]
81 assert call_hook_mock.call_args_list == expected_calls
81 assert call_hook_mock.call_args_list == expected_calls
82
82
83
83
84 def test_git_post_receive_does_not_call_disabled_repo_size():
84 def test_git_post_receive_does_not_call_disabled_repo_size():
85 extras = {'hooks': ['push']}
85 extras = {'hooks': ['push']}
86
86
87 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
87 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
88 hooks.git_post_receive(
88 hooks.git_post_receive(
89 None, '', {'RC_SCM_DATA': json.dumps(extras)})
89 None, '', {'RC_SCM_DATA': json.dumps(extras)})
90 extras.update({'commit_ids': [], 'hook_type': 'post_receive',
90 extras.update({'commit_ids': [], 'hook_type': 'post_receive',
91 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
91 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
92 expected_calls = [
92 expected_calls = [
93 mock.call('post_push', extras, mock.ANY)
93 mock.call('post_push', extras, mock.ANY)
94 ]
94 ]
95 assert call_hook_mock.call_args_list == expected_calls
95 assert call_hook_mock.call_args_list == expected_calls
96
96
97
97
98 def test_repo_size_exception_does_not_affect_git_post_receive():
98 def test_repo_size_exception_does_not_affect_git_post_receive():
99 extras = {'hooks': ['push', 'repo_size']}
99 extras = {'hooks': ['push', 'repo_size']}
100 status = 0
100 status = 0
101
101
102 def side_effect(name, *args, **kwargs):
102 def side_effect(name, *args, **kwargs):
103 if name == 'repo_size':
103 if name == 'repo_size':
104 raise Exception('Fake exception')
104 raise Exception('Fake exception')
105 else:
105 else:
106 return status
106 return status
107
107
108 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
108 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
109 call_hook_mock.side_effect = side_effect
109 call_hook_mock.side_effect = side_effect
110 result = hooks.git_post_receive(
110 result = hooks.git_post_receive(
111 None, '', {'RC_SCM_DATA': json.dumps(extras)})
111 None, '', {'RC_SCM_DATA': json.dumps(extras)})
112 assert result == status
112 assert result == status
113
113
114
114
115 def test_git_pre_pull_is_disabled():
115 def test_git_pre_pull_is_disabled():
116 assert hooks.git_pre_pull({'hooks': ['push']}) == hooks.HookResponse(0, '')
116 assert hooks.git_pre_pull({'hooks': ['push']}) == hooks.HookResponse(0, '')
117
117
118
118
119 def test_git_post_pull_is_disabled():
119 def test_git_post_pull_is_disabled():
120 assert (
120 assert (
121 hooks.git_post_pull({'hooks': ['push']}) == hooks.HookResponse(0, ''))
121 hooks.git_post_pull({'hooks': ['push']}) == hooks.HookResponse(0, ''))
122
122
123
123
124 class TestGetHooksClient:
124 class TestGetHooksClient:
125
125
126 def test_returns_http_client_when_protocol_matches(self):
126 def test_returns_http_client_when_protocol_matches(self):
127 hooks_uri = 'localhost:8000'
127 hooks_uri = 'localhost:8000'
128 result = hooks._get_hooks_client({
128 result = hooks._get_hooks_client({
129 'hooks_uri': hooks_uri,
129 'hooks_uri': hooks_uri,
130 'hooks_protocol': 'http'
130 'hooks_protocol': 'http'
131 })
131 })
132 assert isinstance(result, hooks.HooksHttpClient)
132 assert isinstance(result, hooks.HooksHttpClient)
133 assert result.hooks_uri == hooks_uri
133 assert result.hooks_uri == hooks_uri
134
134
135 def test_returns_dummy_client_when_hooks_uri_not_specified(self):
135 def test_return_celery_client_when_queue_and_backend_provided(self):
136 fake_module = mock.Mock()
136 task_queue = 'redis://task_queue:0'
137 import_patcher = mock.patch.object(
137 task_backend = task_queue
138 hooks.importlib, 'import_module', return_value=fake_module)
138 result = hooks._get_hooks_client({
139 fake_module_name = 'fake.module'
139 'task_queue': task_queue,
140 with import_patcher as import_mock:
140 'task_backend': task_backend
141 result = hooks._get_hooks_client(
141 })
142 {'hooks_module': fake_module_name})
142 assert isinstance(result, hooks.HooksCeleryClient)
143
144 import_mock.assert_called_once_with(fake_module_name)
145 assert isinstance(result, hooks.HooksDummyClient)
146 assert result._hooks_module == fake_module
147
143
148
144
149 class TestHooksHttpClient:
145 class TestHooksHttpClient:
150 def test_init_sets_hooks_uri(self):
146 def test_init_sets_hooks_uri(self):
151 uri = 'localhost:3000'
147 uri = 'localhost:3000'
152 client = hooks.HooksHttpClient(uri)
148 client = hooks.HooksHttpClient(uri)
153 assert client.hooks_uri == uri
149 assert client.hooks_uri == uri
154
150
155 def test_serialize_returns_serialized_string(self):
151 def test_serialize_returns_serialized_string(self):
156 client = hooks.HooksHttpClient('localhost:3000')
152 client = hooks.HooksHttpClient('localhost:3000')
157 hook_name = 'test'
153 hook_name = 'test'
158 extras = {
154 extras = {
159 'first': 1,
155 'first': 1,
160 'second': 'two'
156 'second': 'two'
161 }
157 }
162 hooks_proto, result = client._serialize(hook_name, extras)
158 hooks_proto, result = client._serialize(hook_name, extras)
163 expected_result = msgpack.packb({
159 expected_result = msgpack.packb({
164 'method': hook_name,
160 'method': hook_name,
165 'extras': extras,
161 'extras': extras,
166 })
162 })
167 assert hooks_proto == {'rc-hooks-protocol': 'msgpack.v1', 'Connection': 'keep-alive'}
163 assert hooks_proto == {'rc-hooks-protocol': 'msgpack.v1', 'Connection': 'keep-alive'}
168 assert result == expected_result
164 assert result == expected_result
169
165
170 def test_call_queries_http_server(self, http_mirror):
166 def test_call_queries_http_server(self, http_mirror):
171 client = hooks.HooksHttpClient(http_mirror.uri)
167 client = hooks.HooksHttpClient(http_mirror.uri)
172 hook_name = 'test'
168 hook_name = 'test'
173 extras = {
169 extras = {
174 'first': 1,
170 'first': 1,
175 'second': 'two'
171 'second': 'two'
176 }
172 }
177 result = client(hook_name, extras)
173 result = client(hook_name, extras)
178 expected_result = msgpack.unpackb(msgpack.packb({
174 expected_result = msgpack.unpackb(msgpack.packb({
179 'method': hook_name,
175 'method': hook_name,
180 'extras': extras
176 'extras': extras
181 }), raw=False)
177 }), raw=False)
182 assert result == expected_result
178 assert result == expected_result
183
179
184
180
185 class TestHooksDummyClient:
186 def test_init_imports_hooks_module(self):
187 hooks_module_name = 'rhodecode.fake.module'
188 hooks_module = mock.MagicMock()
189
190 import_patcher = mock.patch.object(
191 hooks.importlib, 'import_module', return_value=hooks_module)
192 with import_patcher as import_mock:
193 client = hooks.HooksDummyClient(hooks_module_name)
194 import_mock.assert_called_once_with(hooks_module_name)
195 assert client._hooks_module == hooks_module
196
197 def test_call_returns_hook_result(self):
198 hooks_module_name = 'rhodecode.fake.module'
199 hooks_module = mock.MagicMock()
200 import_patcher = mock.patch.object(
201 hooks.importlib, 'import_module', return_value=hooks_module)
202 with import_patcher:
203 client = hooks.HooksDummyClient(hooks_module_name)
204
205 result = client('post_push', {})
206 hooks_module.Hooks.assert_called_once_with()
207 assert result == hooks_module.Hooks().__enter__().post_push()
208
209
210 @pytest.fixture
181 @pytest.fixture
211 def http_mirror(request):
182 def http_mirror(request):
212 server = MirrorHttpServer()
183 server = MirrorHttpServer()
213 request.addfinalizer(server.stop)
184 request.addfinalizer(server.stop)
214 return server
185 return server
215
186
216
187
217 class MirrorHttpHandler(BaseHTTPRequestHandler):
188 class MirrorHttpHandler(BaseHTTPRequestHandler):
218
189
219 def do_POST(self):
190 def do_POST(self):
220 length = int(self.headers['Content-Length'])
191 length = int(self.headers['Content-Length'])
221 body = self.rfile.read(length)
192 body = self.rfile.read(length)
222 self.send_response(200)
193 self.send_response(200)
223 self.end_headers()
194 self.end_headers()
224 self.wfile.write(body)
195 self.wfile.write(body)
225
196
226
197
227 class MirrorHttpServer:
198 class MirrorHttpServer:
228 ip_address = '127.0.0.1'
199 ip_address = '127.0.0.1'
229 port = 0
200 port = 0
230
201
231 def __init__(self):
202 def __init__(self):
232 self._daemon = TCPServer((self.ip_address, 0), MirrorHttpHandler)
203 self._daemon = TCPServer((self.ip_address, 0), MirrorHttpHandler)
233 _, self.port = self._daemon.server_address
204 _, self.port = self._daemon.server_address
234 self._thread = threading.Thread(target=self._daemon.serve_forever)
205 self._thread = threading.Thread(target=self._daemon.serve_forever)
235 self._thread.daemon = True
206 self._thread.daemon = True
236 self._thread.start()
207 self._thread.start()
237
208
238 def stop(self):
209 def stop(self):
239 self._daemon.shutdown()
210 self._daemon.shutdown()
240 self._thread.join()
211 self._thread.join()
241 self._daemon = None
212 self._daemon = None
242 self._thread = None
213 self._thread = None
243
214
244 @property
215 @property
245 def uri(self):
216 def uri(self):
246 return '{}:{}'.format(self.ip_address, self.port)
217 return '{}:{}'.format(self.ip_address, self.port)
247
218
248
219
249 def test_hooks_http_client_init():
220 def test_hooks_http_client_init():
250 hooks_uri = 'http://localhost:8000'
221 hooks_uri = 'http://localhost:8000'
251 client = HooksHttpClient(hooks_uri)
222 client = HooksHttpClient(hooks_uri)
252 assert client.hooks_uri == hooks_uri
223 assert client.hooks_uri == hooks_uri
253
224
254
225
255 def test_hooks_http_client_call():
226 def test_hooks_http_client_call():
256 hooks_uri = 'http://localhost:8000'
227 hooks_uri = 'http://localhost:8000'
257
228
258 method = 'test_method'
229 method = 'test_method'
259 extras = {'key': 'value'}
230 extras = {'key': 'value'}
260
231
261 with \
232 with \
262 mock.patch('http.client.HTTPConnection') as mock_connection,\
233 mock.patch('http.client.HTTPConnection') as mock_connection,\
263 mock.patch('msgpack.load') as mock_load:
234 mock.patch('msgpack.load') as mock_load:
264
235
265 client = HooksHttpClient(hooks_uri)
236 client = HooksHttpClient(hooks_uri)
266
237
267 mock_load.return_value = {'result': 'success'}
238 mock_load.return_value = {'result': 'success'}
268 response = mock.MagicMock()
239 response = mock.MagicMock()
269 response.status = 200
240 response.status = 200
270 mock_connection.request.side_effect = None
241 mock_connection.request.side_effect = None
271 mock_connection.getresponse.return_value = response
242 mock_connection.getresponse.return_value = response
272
243
273 result = client(method, extras)
244 result = client(method, extras)
274
245
275 mock_connection.assert_called_with(hooks_uri)
246 mock_connection.assert_called_with(hooks_uri)
276 mock_connection.return_value.request.assert_called_once()
247 mock_connection.return_value.request.assert_called_once()
277 assert result == {'result': 'success'}
248 assert result == {'result': 'success'}
278
249
279
250
280 def test_hooks_http_client_serialize():
251 def test_hooks_http_client_serialize():
281 method = 'test_method'
252 method = 'test_method'
282 extras = {'key': 'value'}
253 extras = {'key': 'value'}
283 headers, body = HooksHttpClient._serialize(method, extras)
254 headers, body = HooksHttpClient._serialize(method, extras)
284
255
285 assert headers == {'rc-hooks-protocol': HooksHttpClient.proto, 'Connection': 'keep-alive'}
256 assert headers == {'rc-hooks-protocol': HooksHttpClient.proto, 'Connection': 'keep-alive'}
286 assert msgpack.unpackb(body) == {'method': method, 'extras': extras}
257 assert msgpack.unpackb(body) == {'method': method, 'extras': extras}
General Comments 0
You need to be logged in to leave comments. Login now