##// END OF EJS Templates
hooks: store hook type for extensions.
marcink -
r555:315f8a04 default
parent child Browse files
Show More
@@ -1,658 +1,662 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # RhodeCode VCSServer provides access to different vcs backends via network.
3 # RhodeCode VCSServer provides access to different vcs backends via network.
4 # Copyright (C) 2014-2018 RhodeCode GmbH
4 # Copyright (C) 2014-2018 RhodeCode GmbH
5 #
5 #
6 # This program is free software; you can redistribute it and/or modify
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
9 # (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software Foundation,
17 # along with this program; if not, write to the Free Software Foundation,
18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
19
20 import io
20 import io
21 import os
21 import os
22 import sys
22 import sys
23 import logging
23 import logging
24 import collections
24 import collections
25 import importlib
25 import importlib
26 import base64
26 import base64
27
27
28 from httplib import HTTPConnection
28 from httplib import HTTPConnection
29
29
30
30
31 import mercurial.scmutil
31 import mercurial.scmutil
32 import mercurial.node
32 import mercurial.node
33 import simplejson as json
33 import simplejson as json
34
34
35 from vcsserver import exceptions, subprocessio, settings
35 from vcsserver import exceptions, subprocessio, settings
36
36
37 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
38
38
39
39
40 class HooksHttpClient(object):
40 class HooksHttpClient(object):
41 connection = None
41 connection = None
42
42
43 def __init__(self, hooks_uri):
43 def __init__(self, hooks_uri):
44 self.hooks_uri = hooks_uri
44 self.hooks_uri = hooks_uri
45
45
46 def __call__(self, method, extras):
46 def __call__(self, method, extras):
47 connection = HTTPConnection(self.hooks_uri)
47 connection = HTTPConnection(self.hooks_uri)
48 body = self._serialize(method, extras)
48 body = self._serialize(method, extras)
49 try:
49 try:
50 connection.request('POST', '/', body)
50 connection.request('POST', '/', body)
51 except Exception:
51 except Exception:
52 log.error('Connection failed on %s', connection)
52 log.error('Connection failed on %s', connection)
53 raise
53 raise
54 response = connection.getresponse()
54 response = connection.getresponse()
55 return json.loads(response.read())
55 return json.loads(response.read())
56
56
57 def _serialize(self, hook_name, extras):
57 def _serialize(self, hook_name, extras):
58 data = {
58 data = {
59 'method': hook_name,
59 'method': hook_name,
60 'extras': extras
60 'extras': extras
61 }
61 }
62 return json.dumps(data)
62 return json.dumps(data)
63
63
64
64
65 class HooksDummyClient(object):
65 class HooksDummyClient(object):
66 def __init__(self, hooks_module):
66 def __init__(self, hooks_module):
67 self._hooks_module = importlib.import_module(hooks_module)
67 self._hooks_module = importlib.import_module(hooks_module)
68
68
69 def __call__(self, hook_name, extras):
69 def __call__(self, hook_name, extras):
70 with self._hooks_module.Hooks() as hooks:
70 with self._hooks_module.Hooks() as hooks:
71 return getattr(hooks, hook_name)(extras)
71 return getattr(hooks, hook_name)(extras)
72
72
73
73
74 class RemoteMessageWriter(object):
74 class RemoteMessageWriter(object):
75 """Writer base class."""
75 """Writer base class."""
76 def write(self, message):
76 def write(self, message):
77 raise NotImplementedError()
77 raise NotImplementedError()
78
78
79
79
80 class HgMessageWriter(RemoteMessageWriter):
80 class HgMessageWriter(RemoteMessageWriter):
81 """Writer that knows how to send messages to mercurial clients."""
81 """Writer that knows how to send messages to mercurial clients."""
82
82
83 def __init__(self, ui):
83 def __init__(self, ui):
84 self.ui = ui
84 self.ui = ui
85
85
86 def write(self, message):
86 def write(self, message):
87 # TODO: Check why the quiet flag is set by default.
87 # TODO: Check why the quiet flag is set by default.
88 old = self.ui.quiet
88 old = self.ui.quiet
89 self.ui.quiet = False
89 self.ui.quiet = False
90 self.ui.status(message.encode('utf-8'))
90 self.ui.status(message.encode('utf-8'))
91 self.ui.quiet = old
91 self.ui.quiet = old
92
92
93
93
94 class GitMessageWriter(RemoteMessageWriter):
94 class GitMessageWriter(RemoteMessageWriter):
95 """Writer that knows how to send messages to git clients."""
95 """Writer that knows how to send messages to git clients."""
96
96
97 def __init__(self, stdout=None):
97 def __init__(self, stdout=None):
98 self.stdout = stdout or sys.stdout
98 self.stdout = stdout or sys.stdout
99
99
100 def write(self, message):
100 def write(self, message):
101 self.stdout.write(message.encode('utf-8'))
101 self.stdout.write(message.encode('utf-8'))
102
102
103
103
104 class SvnMessageWriter(RemoteMessageWriter):
104 class SvnMessageWriter(RemoteMessageWriter):
105 """Writer that knows how to send messages to svn clients."""
105 """Writer that knows how to send messages to svn clients."""
106
106
107 def __init__(self, stderr=None):
107 def __init__(self, stderr=None):
108 # SVN needs data sent to stderr for back-to-client messaging
108 # SVN needs data sent to stderr for back-to-client messaging
109 self.stderr = stderr or sys.stderr
109 self.stderr = stderr or sys.stderr
110
110
111 def write(self, message):
111 def write(self, message):
112 self.stderr.write(message.encode('utf-8'))
112 self.stderr.write(message.encode('utf-8'))
113
113
114
114
115 def _handle_exception(result):
115 def _handle_exception(result):
116 exception_class = result.get('exception')
116 exception_class = result.get('exception')
117 exception_traceback = result.get('exception_traceback')
117 exception_traceback = result.get('exception_traceback')
118
118
119 if exception_traceback:
119 if exception_traceback:
120 log.error('Got traceback from remote call:%s', exception_traceback)
120 log.error('Got traceback from remote call:%s', exception_traceback)
121
121
122 if exception_class == 'HTTPLockedRC':
122 if exception_class == 'HTTPLockedRC':
123 raise exceptions.RepositoryLockedException()(*result['exception_args'])
123 raise exceptions.RepositoryLockedException()(*result['exception_args'])
124 elif exception_class == 'HTTPBranchProtected':
124 elif exception_class == 'HTTPBranchProtected':
125 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
125 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
126 elif exception_class == 'RepositoryError':
126 elif exception_class == 'RepositoryError':
127 raise exceptions.VcsException()(*result['exception_args'])
127 raise exceptions.VcsException()(*result['exception_args'])
128 elif exception_class:
128 elif exception_class:
129 raise Exception('Got remote exception "%s" with args "%s"' %
129 raise Exception('Got remote exception "%s" with args "%s"' %
130 (exception_class, result['exception_args']))
130 (exception_class, result['exception_args']))
131
131
132
132
133 def _get_hooks_client(extras):
133 def _get_hooks_client(extras):
134 if 'hooks_uri' in extras:
134 if 'hooks_uri' in extras:
135 protocol = extras.get('hooks_protocol')
135 protocol = extras.get('hooks_protocol')
136 return HooksHttpClient(extras['hooks_uri'])
136 return HooksHttpClient(extras['hooks_uri'])
137 else:
137 else:
138 return HooksDummyClient(extras['hooks_module'])
138 return HooksDummyClient(extras['hooks_module'])
139
139
140
140
141 def _call_hook(hook_name, extras, writer):
141 def _call_hook(hook_name, extras, writer):
142 hooks_client = _get_hooks_client(extras)
142 hooks_client = _get_hooks_client(extras)
143 log.debug('Hooks, using client:%s', hooks_client)
143 log.debug('Hooks, using client:%s', hooks_client)
144 result = hooks_client(hook_name, extras)
144 result = hooks_client(hook_name, extras)
145 log.debug('Hooks got result: %s', result)
145 log.debug('Hooks got result: %s', result)
146
146
147 _handle_exception(result)
147 _handle_exception(result)
148 writer.write(result['output'])
148 writer.write(result['output'])
149
149
150 return result['status']
150 return result['status']
151
151
152
152
153 def _extras_from_ui(ui):
153 def _extras_from_ui(ui):
154 hook_data = ui.config('rhodecode', 'RC_SCM_DATA')
154 hook_data = ui.config('rhodecode', 'RC_SCM_DATA')
155 if not hook_data:
155 if not hook_data:
156 # maybe it's inside environ ?
156 # maybe it's inside environ ?
157 env_hook_data = os.environ.get('RC_SCM_DATA')
157 env_hook_data = os.environ.get('RC_SCM_DATA')
158 if env_hook_data:
158 if env_hook_data:
159 hook_data = env_hook_data
159 hook_data = env_hook_data
160
160
161 extras = {}
161 extras = {}
162 if hook_data:
162 if hook_data:
163 extras = json.loads(hook_data)
163 extras = json.loads(hook_data)
164 return extras
164 return extras
165
165
166
166
167 def _rev_range_hash(repo, node, check_heads=False):
167 def _rev_range_hash(repo, node, check_heads=False):
168
168
169 commits = []
169 commits = []
170 revs = []
170 revs = []
171 start = repo[node].rev()
171 start = repo[node].rev()
172 end = len(repo)
172 end = len(repo)
173 for rev in range(start, end):
173 for rev in range(start, end):
174 revs.append(rev)
174 revs.append(rev)
175 ctx = repo[rev]
175 ctx = repo[rev]
176 commit_id = mercurial.node.hex(ctx.node())
176 commit_id = mercurial.node.hex(ctx.node())
177 branch = ctx.branch()
177 branch = ctx.branch()
178 commits.append((commit_id, branch))
178 commits.append((commit_id, branch))
179
179
180 parent_heads = []
180 parent_heads = []
181 if check_heads:
181 if check_heads:
182 parent_heads = _check_heads(repo, start, end, revs)
182 parent_heads = _check_heads(repo, start, end, revs)
183 return commits, parent_heads
183 return commits, parent_heads
184
184
185
185
186 def _check_heads(repo, start, end, commits):
186 def _check_heads(repo, start, end, commits):
187 changelog = repo.changelog
187 changelog = repo.changelog
188 parents = set()
188 parents = set()
189
189
190 for new_rev in commits:
190 for new_rev in commits:
191 for p in changelog.parentrevs(new_rev):
191 for p in changelog.parentrevs(new_rev):
192 if p == mercurial.node.nullrev:
192 if p == mercurial.node.nullrev:
193 continue
193 continue
194 if p < start:
194 if p < start:
195 parents.add(p)
195 parents.add(p)
196
196
197 for p in parents:
197 for p in parents:
198 branch = repo[p].branch()
198 branch = repo[p].branch()
199 # The heads descending from that parent, on the same branch
199 # The heads descending from that parent, on the same branch
200 parent_heads = set([p])
200 parent_heads = set([p])
201 reachable = set([p])
201 reachable = set([p])
202 for x in xrange(p + 1, end):
202 for x in xrange(p + 1, end):
203 if repo[x].branch() != branch:
203 if repo[x].branch() != branch:
204 continue
204 continue
205 for pp in changelog.parentrevs(x):
205 for pp in changelog.parentrevs(x):
206 if pp in reachable:
206 if pp in reachable:
207 reachable.add(x)
207 reachable.add(x)
208 parent_heads.discard(pp)
208 parent_heads.discard(pp)
209 parent_heads.add(x)
209 parent_heads.add(x)
210 # More than one head? Suggest merging
210 # More than one head? Suggest merging
211 if len(parent_heads) > 1:
211 if len(parent_heads) > 1:
212 return list(parent_heads)
212 return list(parent_heads)
213
213
214 return []
214 return []
215
215
216
216
217 def repo_size(ui, repo, **kwargs):
217 def repo_size(ui, repo, **kwargs):
218 extras = _extras_from_ui(ui)
218 extras = _extras_from_ui(ui)
219 return _call_hook('repo_size', extras, HgMessageWriter(ui))
219 return _call_hook('repo_size', extras, HgMessageWriter(ui))
220
220
221
221
222 def pre_pull(ui, repo, **kwargs):
222 def pre_pull(ui, repo, **kwargs):
223 extras = _extras_from_ui(ui)
223 extras = _extras_from_ui(ui)
224 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
224 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
225
225
226
226
227 def pre_pull_ssh(ui, repo, **kwargs):
227 def pre_pull_ssh(ui, repo, **kwargs):
228 extras = _extras_from_ui(ui)
228 extras = _extras_from_ui(ui)
229 if extras and extras.get('SSH'):
229 if extras and extras.get('SSH'):
230 return pre_pull(ui, repo, **kwargs)
230 return pre_pull(ui, repo, **kwargs)
231 return 0
231 return 0
232
232
233
233
234 def post_pull(ui, repo, **kwargs):
234 def post_pull(ui, repo, **kwargs):
235 extras = _extras_from_ui(ui)
235 extras = _extras_from_ui(ui)
236 return _call_hook('post_pull', extras, HgMessageWriter(ui))
236 return _call_hook('post_pull', extras, HgMessageWriter(ui))
237
237
238
238
239 def post_pull_ssh(ui, repo, **kwargs):
239 def post_pull_ssh(ui, repo, **kwargs):
240 extras = _extras_from_ui(ui)
240 extras = _extras_from_ui(ui)
241 if extras and extras.get('SSH'):
241 if extras and extras.get('SSH'):
242 return post_pull(ui, repo, **kwargs)
242 return post_pull(ui, repo, **kwargs)
243 return 0
243 return 0
244
244
245
245
246 def pre_push(ui, repo, node=None, **kwargs):
246 def pre_push(ui, repo, node=None, **kwargs):
247 """
247 """
248 Mercurial pre_push hook
248 Mercurial pre_push hook
249 """
249 """
250 extras = _extras_from_ui(ui)
250 extras = _extras_from_ui(ui)
251 detect_force_push = extras.get('detect_force_push')
251 detect_force_push = extras.get('detect_force_push')
252
252
253 rev_data = []
253 rev_data = []
254 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
254 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
255 branches = collections.defaultdict(list)
255 branches = collections.defaultdict(list)
256 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
256 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
257 for commit_id, branch in commits:
257 for commit_id, branch in commits:
258 branches[branch].append(commit_id)
258 branches[branch].append(commit_id)
259
259
260 for branch, commits in branches.items():
260 for branch, commits in branches.items():
261 old_rev = kwargs.get('node_last') or commits[0]
261 old_rev = kwargs.get('node_last') or commits[0]
262 rev_data.append({
262 rev_data.append({
263 'old_rev': old_rev,
263 'old_rev': old_rev,
264 'new_rev': commits[-1],
264 'new_rev': commits[-1],
265 'ref': '',
265 'ref': '',
266 'type': 'branch',
266 'type': 'branch',
267 'name': branch,
267 'name': branch,
268 })
268 })
269
269
270 for push_ref in rev_data:
270 for push_ref in rev_data:
271 push_ref['multiple_heads'] = _heads
271 push_ref['multiple_heads'] = _heads
272
272
273 extras['hook_type'] = kwargs.get('hooktype', 'pre_push')
273 extras['commit_ids'] = rev_data
274 extras['commit_ids'] = rev_data
274 return _call_hook('pre_push', extras, HgMessageWriter(ui))
275 return _call_hook('pre_push', extras, HgMessageWriter(ui))
275
276
276
277
277 def pre_push_ssh(ui, repo, node=None, **kwargs):
278 def pre_push_ssh(ui, repo, node=None, **kwargs):
278 extras = _extras_from_ui(ui)
279 extras = _extras_from_ui(ui)
279 if extras.get('SSH'):
280 if extras.get('SSH'):
280 return pre_push(ui, repo, node, **kwargs)
281 return pre_push(ui, repo, node, **kwargs)
281
282
282 return 0
283 return 0
283
284
284
285
285 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
286 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
286 """
287 """
287 Mercurial pre_push hook for SSH
288 Mercurial pre_push hook for SSH
288 """
289 """
289 extras = _extras_from_ui(ui)
290 extras = _extras_from_ui(ui)
290 if extras.get('SSH'):
291 if extras.get('SSH'):
291 permission = extras['SSH_PERMISSIONS']
292 permission = extras['SSH_PERMISSIONS']
292
293
293 if 'repository.write' == permission or 'repository.admin' == permission:
294 if 'repository.write' == permission or 'repository.admin' == permission:
294 return 0
295 return 0
295
296
296 # non-zero ret code
297 # non-zero ret code
297 return 1
298 return 1
298
299
299 return 0
300 return 0
300
301
301
302
302 def post_push(ui, repo, node, **kwargs):
303 def post_push(ui, repo, node, **kwargs):
303 """
304 """
304 Mercurial post_push hook
305 Mercurial post_push hook
305 """
306 """
306 extras = _extras_from_ui(ui)
307 extras = _extras_from_ui(ui)
307
308
308 commit_ids = []
309 commit_ids = []
309 branches = []
310 branches = []
310 bookmarks = []
311 bookmarks = []
311 tags = []
312 tags = []
312
313
313 commits, _heads = _rev_range_hash(repo, node)
314 commits, _heads = _rev_range_hash(repo, node)
314 for commit_id, branch in commits:
315 for commit_id, branch in commits:
315 commit_ids.append(commit_id)
316 commit_ids.append(commit_id)
316 if branch not in branches:
317 if branch not in branches:
317 branches.append(branch)
318 branches.append(branch)
318
319
319 if hasattr(ui, '_rc_pushkey_branches'):
320 if hasattr(ui, '_rc_pushkey_branches'):
320 bookmarks = ui._rc_pushkey_branches
321 bookmarks = ui._rc_pushkey_branches
321
322
323 extras['hook_type'] = kwargs.get('hooktype', 'post_push')
322 extras['commit_ids'] = commit_ids
324 extras['commit_ids'] = commit_ids
323 extras['new_refs'] = {
325 extras['new_refs'] = {
324 'branches': branches,
326 'branches': branches,
325 'bookmarks': bookmarks,
327 'bookmarks': bookmarks,
326 'tags': tags
328 'tags': tags
327 }
329 }
328
330
329 return _call_hook('post_push', extras, HgMessageWriter(ui))
331 return _call_hook('post_push', extras, HgMessageWriter(ui))
330
332
331
333
332 def post_push_ssh(ui, repo, node, **kwargs):
334 def post_push_ssh(ui, repo, node, **kwargs):
333 """
335 """
334 Mercurial post_push hook for SSH
336 Mercurial post_push hook for SSH
335 """
337 """
336 if _extras_from_ui(ui).get('SSH'):
338 if _extras_from_ui(ui).get('SSH'):
337 return post_push(ui, repo, node, **kwargs)
339 return post_push(ui, repo, node, **kwargs)
338 return 0
340 return 0
339
341
340
342
341 def key_push(ui, repo, **kwargs):
343 def key_push(ui, repo, **kwargs):
342 if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks':
344 if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks':
343 # store new bookmarks in our UI object propagated later to post_push
345 # store new bookmarks in our UI object propagated later to post_push
344 ui._rc_pushkey_branches = repo[kwargs['key']].bookmarks()
346 ui._rc_pushkey_branches = repo[kwargs['key']].bookmarks()
345 return
347 return
346
348
347
349
348 # backward compat
350 # backward compat
349 log_pull_action = post_pull
351 log_pull_action = post_pull
350
352
351 # backward compat
353 # backward compat
352 log_push_action = post_push
354 log_push_action = post_push
353
355
354
356
355 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
357 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
356 """
358 """
357 Old hook name: keep here for backward compatibility.
359 Old hook name: keep here for backward compatibility.
358
360
359 This is only required when the installed git hooks are not upgraded.
361 This is only required when the installed git hooks are not upgraded.
360 """
362 """
361 pass
363 pass
362
364
363
365
364 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
366 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
365 """
367 """
366 Old hook name: keep here for backward compatibility.
368 Old hook name: keep here for backward compatibility.
367
369
368 This is only required when the installed git hooks are not upgraded.
370 This is only required when the installed git hooks are not upgraded.
369 """
371 """
370 pass
372 pass
371
373
372
374
373 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
375 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
374
376
375
377
376 def git_pre_pull(extras):
378 def git_pre_pull(extras):
377 """
379 """
378 Pre pull hook.
380 Pre pull hook.
379
381
380 :param extras: dictionary containing the keys defined in simplevcs
382 :param extras: dictionary containing the keys defined in simplevcs
381 :type extras: dict
383 :type extras: dict
382
384
383 :return: status code of the hook. 0 for success.
385 :return: status code of the hook. 0 for success.
384 :rtype: int
386 :rtype: int
385 """
387 """
386 if 'pull' not in extras['hooks']:
388 if 'pull' not in extras['hooks']:
387 return HookResponse(0, '')
389 return HookResponse(0, '')
388
390
389 stdout = io.BytesIO()
391 stdout = io.BytesIO()
390 try:
392 try:
391 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
393 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
392 except Exception as error:
394 except Exception as error:
393 status = 128
395 status = 128
394 stdout.write('ERROR: %s\n' % str(error))
396 stdout.write('ERROR: %s\n' % str(error))
395
397
396 return HookResponse(status, stdout.getvalue())
398 return HookResponse(status, stdout.getvalue())
397
399
398
400
399 def git_post_pull(extras):
401 def git_post_pull(extras):
400 """
402 """
401 Post pull hook.
403 Post pull hook.
402
404
403 :param extras: dictionary containing the keys defined in simplevcs
405 :param extras: dictionary containing the keys defined in simplevcs
404 :type extras: dict
406 :type extras: dict
405
407
406 :return: status code of the hook. 0 for success.
408 :return: status code of the hook. 0 for success.
407 :rtype: int
409 :rtype: int
408 """
410 """
409 if 'pull' not in extras['hooks']:
411 if 'pull' not in extras['hooks']:
410 return HookResponse(0, '')
412 return HookResponse(0, '')
411
413
412 stdout = io.BytesIO()
414 stdout = io.BytesIO()
413 try:
415 try:
414 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
416 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
415 except Exception as error:
417 except Exception as error:
416 status = 128
418 status = 128
417 stdout.write('ERROR: %s\n' % error)
419 stdout.write('ERROR: %s\n' % error)
418
420
419 return HookResponse(status, stdout.getvalue())
421 return HookResponse(status, stdout.getvalue())
420
422
421
423
422 def _parse_git_ref_lines(revision_lines):
424 def _parse_git_ref_lines(revision_lines):
423 rev_data = []
425 rev_data = []
424 for revision_line in revision_lines or []:
426 for revision_line in revision_lines or []:
425 old_rev, new_rev, ref = revision_line.strip().split(' ')
427 old_rev, new_rev, ref = revision_line.strip().split(' ')
426 ref_data = ref.split('/', 2)
428 ref_data = ref.split('/', 2)
427 if ref_data[1] in ('tags', 'heads'):
429 if ref_data[1] in ('tags', 'heads'):
428 rev_data.append({
430 rev_data.append({
429 'old_rev': old_rev,
431 'old_rev': old_rev,
430 'new_rev': new_rev,
432 'new_rev': new_rev,
431 'ref': ref,
433 'ref': ref,
432 'type': ref_data[1],
434 'type': ref_data[1],
433 'name': ref_data[2],
435 'name': ref_data[2],
434 })
436 })
435 return rev_data
437 return rev_data
436
438
437
439
438 def git_pre_receive(unused_repo_path, revision_lines, env):
440 def git_pre_receive(unused_repo_path, revision_lines, env):
439 """
441 """
440 Pre push hook.
442 Pre push hook.
441
443
442 :param extras: dictionary containing the keys defined in simplevcs
444 :param extras: dictionary containing the keys defined in simplevcs
443 :type extras: dict
445 :type extras: dict
444
446
445 :return: status code of the hook. 0 for success.
447 :return: status code of the hook. 0 for success.
446 :rtype: int
448 :rtype: int
447 """
449 """
448 extras = json.loads(env['RC_SCM_DATA'])
450 extras = json.loads(env['RC_SCM_DATA'])
449 rev_data = _parse_git_ref_lines(revision_lines)
451 rev_data = _parse_git_ref_lines(revision_lines)
450 if 'push' not in extras['hooks']:
452 if 'push' not in extras['hooks']:
451 return 0
453 return 0
452 empty_commit_id = '0' * 40
454 empty_commit_id = '0' * 40
453
455
454 detect_force_push = extras.get('detect_force_push')
456 detect_force_push = extras.get('detect_force_push')
455
457
456 for push_ref in rev_data:
458 for push_ref in rev_data:
457 # store our git-env which holds the temp store
459 # store our git-env which holds the temp store
458 push_ref['git_env'] = [
460 push_ref['git_env'] = [
459 (k, v) for k, v in os.environ.items() if k.startswith('GIT')]
461 (k, v) for k, v in os.environ.items() if k.startswith('GIT')]
460 push_ref['pruned_sha'] = ''
462 push_ref['pruned_sha'] = ''
461 if not detect_force_push:
463 if not detect_force_push:
462 # don't check for forced-push when we don't need to
464 # don't check for forced-push when we don't need to
463 continue
465 continue
464
466
465 type_ = push_ref['type']
467 type_ = push_ref['type']
466 new_branch = push_ref['old_rev'] == empty_commit_id
468 new_branch = push_ref['old_rev'] == empty_commit_id
467 if type_ == 'heads' and not new_branch:
469 if type_ == 'heads' and not new_branch:
468 old_rev = push_ref['old_rev']
470 old_rev = push_ref['old_rev']
469 new_rev = push_ref['new_rev']
471 new_rev = push_ref['new_rev']
470 cmd = [settings.GIT_EXECUTABLE, 'rev-list',
472 cmd = [settings.GIT_EXECUTABLE, 'rev-list',
471 old_rev, '^{}'.format(new_rev)]
473 old_rev, '^{}'.format(new_rev)]
472 stdout, stderr = subprocessio.run_command(
474 stdout, stderr = subprocessio.run_command(
473 cmd, env=os.environ.copy())
475 cmd, env=os.environ.copy())
474 # means we're having some non-reachable objects, this forced push
476 # means we're having some non-reachable objects, this forced push
475 # was used
477 # was used
476 if stdout:
478 if stdout:
477 push_ref['pruned_sha'] = stdout.splitlines()
479 push_ref['pruned_sha'] = stdout.splitlines()
478
480
481 extras['hook_type'] = 'pre_receive'
479 extras['commit_ids'] = rev_data
482 extras['commit_ids'] = rev_data
480 return _call_hook('pre_push', extras, GitMessageWriter())
483 return _call_hook('pre_push', extras, GitMessageWriter())
481
484
482
485
483 def git_post_receive(unused_repo_path, revision_lines, env):
486 def git_post_receive(unused_repo_path, revision_lines, env):
484 """
487 """
485 Post push hook.
488 Post push hook.
486
489
487 :param extras: dictionary containing the keys defined in simplevcs
490 :param extras: dictionary containing the keys defined in simplevcs
488 :type extras: dict
491 :type extras: dict
489
492
490 :return: status code of the hook. 0 for success.
493 :return: status code of the hook. 0 for success.
491 :rtype: int
494 :rtype: int
492 """
495 """
493 extras = json.loads(env['RC_SCM_DATA'])
496 extras = json.loads(env['RC_SCM_DATA'])
494 if 'push' not in extras['hooks']:
497 if 'push' not in extras['hooks']:
495 return 0
498 return 0
496
499
497 rev_data = _parse_git_ref_lines(revision_lines)
500 rev_data = _parse_git_ref_lines(revision_lines)
498
501
499 git_revs = []
502 git_revs = []
500
503
501 # N.B.(skreft): it is ok to just call git, as git before calling a
504 # N.B.(skreft): it is ok to just call git, as git before calling a
502 # subcommand sets the PATH environment variable so that it point to the
505 # subcommand sets the PATH environment variable so that it point to the
503 # correct version of the git executable.
506 # correct version of the git executable.
504 empty_commit_id = '0' * 40
507 empty_commit_id = '0' * 40
505 branches = []
508 branches = []
506 tags = []
509 tags = []
507 for push_ref in rev_data:
510 for push_ref in rev_data:
508 type_ = push_ref['type']
511 type_ = push_ref['type']
509
512
510 if type_ == 'heads':
513 if type_ == 'heads':
511 if push_ref['old_rev'] == empty_commit_id:
514 if push_ref['old_rev'] == empty_commit_id:
512 # starting new branch case
515 # starting new branch case
513 if push_ref['name'] not in branches:
516 if push_ref['name'] not in branches:
514 branches.append(push_ref['name'])
517 branches.append(push_ref['name'])
515
518
516 # Fix up head revision if needed
519 # Fix up head revision if needed
517 cmd = [settings.GIT_EXECUTABLE, 'show', 'HEAD']
520 cmd = [settings.GIT_EXECUTABLE, 'show', 'HEAD']
518 try:
521 try:
519 subprocessio.run_command(cmd, env=os.environ.copy())
522 subprocessio.run_command(cmd, env=os.environ.copy())
520 except Exception:
523 except Exception:
521 cmd = [settings.GIT_EXECUTABLE, 'symbolic-ref', 'HEAD',
524 cmd = [settings.GIT_EXECUTABLE, 'symbolic-ref', 'HEAD',
522 'refs/heads/%s' % push_ref['name']]
525 'refs/heads/%s' % push_ref['name']]
523 print("Setting default branch to %s" % push_ref['name'])
526 print("Setting default branch to %s" % push_ref['name'])
524 subprocessio.run_command(cmd, env=os.environ.copy())
527 subprocessio.run_command(cmd, env=os.environ.copy())
525
528
526 cmd = [settings.GIT_EXECUTABLE, 'for-each-ref',
529 cmd = [settings.GIT_EXECUTABLE, 'for-each-ref',
527 '--format=%(refname)', 'refs/heads/*']
530 '--format=%(refname)', 'refs/heads/*']
528 stdout, stderr = subprocessio.run_command(
531 stdout, stderr = subprocessio.run_command(
529 cmd, env=os.environ.copy())
532 cmd, env=os.environ.copy())
530 heads = stdout
533 heads = stdout
531 heads = heads.replace(push_ref['ref'], '')
534 heads = heads.replace(push_ref['ref'], '')
532 heads = ' '.join(head for head
535 heads = ' '.join(head for head
533 in heads.splitlines() if head) or '.'
536 in heads.splitlines() if head) or '.'
534 cmd = [settings.GIT_EXECUTABLE, 'log', '--reverse',
537 cmd = [settings.GIT_EXECUTABLE, 'log', '--reverse',
535 '--pretty=format:%H', '--', push_ref['new_rev'],
538 '--pretty=format:%H', '--', push_ref['new_rev'],
536 '--not', heads]
539 '--not', heads]
537 stdout, stderr = subprocessio.run_command(
540 stdout, stderr = subprocessio.run_command(
538 cmd, env=os.environ.copy())
541 cmd, env=os.environ.copy())
539 git_revs.extend(stdout.splitlines())
542 git_revs.extend(stdout.splitlines())
540 elif push_ref['new_rev'] == empty_commit_id:
543 elif push_ref['new_rev'] == empty_commit_id:
541 # delete branch case
544 # delete branch case
542 git_revs.append('delete_branch=>%s' % push_ref['name'])
545 git_revs.append('delete_branch=>%s' % push_ref['name'])
543 else:
546 else:
544 if push_ref['name'] not in branches:
547 if push_ref['name'] not in branches:
545 branches.append(push_ref['name'])
548 branches.append(push_ref['name'])
546
549
547 cmd = [settings.GIT_EXECUTABLE, 'log',
550 cmd = [settings.GIT_EXECUTABLE, 'log',
548 '{old_rev}..{new_rev}'.format(**push_ref),
551 '{old_rev}..{new_rev}'.format(**push_ref),
549 '--reverse', '--pretty=format:%H']
552 '--reverse', '--pretty=format:%H']
550 stdout, stderr = subprocessio.run_command(
553 stdout, stderr = subprocessio.run_command(
551 cmd, env=os.environ.copy())
554 cmd, env=os.environ.copy())
552 git_revs.extend(stdout.splitlines())
555 git_revs.extend(stdout.splitlines())
553 elif type_ == 'tags':
556 elif type_ == 'tags':
554 if push_ref['name'] not in tags:
557 if push_ref['name'] not in tags:
555 tags.append(push_ref['name'])
558 tags.append(push_ref['name'])
556 git_revs.append('tag=>%s' % push_ref['name'])
559 git_revs.append('tag=>%s' % push_ref['name'])
557
560
561 extras['hook_type'] = 'post_receive'
558 extras['commit_ids'] = git_revs
562 extras['commit_ids'] = git_revs
559 extras['new_refs'] = {
563 extras['new_refs'] = {
560 'branches': branches,
564 'branches': branches,
561 'bookmarks': [],
565 'bookmarks': [],
562 'tags': tags,
566 'tags': tags,
563 }
567 }
564
568
565 if 'repo_size' in extras['hooks']:
569 if 'repo_size' in extras['hooks']:
566 try:
570 try:
567 _call_hook('repo_size', extras, GitMessageWriter())
571 _call_hook('repo_size', extras, GitMessageWriter())
568 except:
572 except:
569 pass
573 pass
570
574
571 return _call_hook('post_push', extras, GitMessageWriter())
575 return _call_hook('post_push', extras, GitMessageWriter())
572
576
573
577
574 def _get_extras_from_txn_id(path, txn_id):
578 def _get_extras_from_txn_id(path, txn_id):
575 extras = {}
579 extras = {}
576 try:
580 try:
577 cmd = ['svnlook', 'pget',
581 cmd = ['svnlook', 'pget',
578 '-t', txn_id,
582 '-t', txn_id,
579 '--revprop', path, 'rc-scm-extras']
583 '--revprop', path, 'rc-scm-extras']
580 stdout, stderr = subprocessio.run_command(
584 stdout, stderr = subprocessio.run_command(
581 cmd, env=os.environ.copy())
585 cmd, env=os.environ.copy())
582 extras = json.loads(base64.urlsafe_b64decode(stdout))
586 extras = json.loads(base64.urlsafe_b64decode(stdout))
583 except Exception:
587 except Exception:
584 log.exception('Failed to extract extras info from txn_id')
588 log.exception('Failed to extract extras info from txn_id')
585
589
586 return extras
590 return extras
587
591
588
592
589 def svn_pre_commit(repo_path, commit_data, env):
593 def svn_pre_commit(repo_path, commit_data, env):
590 path, txn_id = commit_data
594 path, txn_id = commit_data
591 branches = []
595 branches = []
592 tags = []
596 tags = []
593
597
594 if env.get('RC_SCM_DATA'):
598 if env.get('RC_SCM_DATA'):
595 extras = json.loads(env['RC_SCM_DATA'])
599 extras = json.loads(env['RC_SCM_DATA'])
596 else:
600 else:
597 # fallback method to read from TXN-ID stored data
601 # fallback method to read from TXN-ID stored data
598 extras = _get_extras_from_txn_id(path, txn_id)
602 extras = _get_extras_from_txn_id(path, txn_id)
599 if not extras:
603 if not extras:
600 return 0
604 return 0
601
605
602 extras['commit_ids'] = []
606 extras['commit_ids'] = []
603 extras['txn_id'] = txn_id
607 extras['txn_id'] = txn_id
604 extras['new_refs'] = {
608 extras['new_refs'] = {
605 'branches': branches,
609 'branches': branches,
606 'bookmarks': [],
610 'bookmarks': [],
607 'tags': tags,
611 'tags': tags,
608 }
612 }
609
613
610 return _call_hook('pre_push', extras, SvnMessageWriter())
614 return _call_hook('pre_push', extras, SvnMessageWriter())
611
615
612
616
613 def _get_extras_from_commit_id(commit_id, path):
617 def _get_extras_from_commit_id(commit_id, path):
614 extras = {}
618 extras = {}
615 try:
619 try:
616 cmd = ['svnlook', 'pget',
620 cmd = ['svnlook', 'pget',
617 '-r', commit_id,
621 '-r', commit_id,
618 '--revprop', path, 'rc-scm-extras']
622 '--revprop', path, 'rc-scm-extras']
619 stdout, stderr = subprocessio.run_command(
623 stdout, stderr = subprocessio.run_command(
620 cmd, env=os.environ.copy())
624 cmd, env=os.environ.copy())
621 extras = json.loads(base64.urlsafe_b64decode(stdout))
625 extras = json.loads(base64.urlsafe_b64decode(stdout))
622 except Exception:
626 except Exception:
623 log.exception('Failed to extract extras info from commit_id')
627 log.exception('Failed to extract extras info from commit_id')
624
628
625 return extras
629 return extras
626
630
627
631
628 def svn_post_commit(repo_path, commit_data, env):
632 def svn_post_commit(repo_path, commit_data, env):
629 """
633 """
630 commit_data is path, rev, txn_id
634 commit_data is path, rev, txn_id
631 """
635 """
632 path, commit_id, txn_id = commit_data
636 path, commit_id, txn_id = commit_data
633 branches = []
637 branches = []
634 tags = []
638 tags = []
635
639
636 if env.get('RC_SCM_DATA'):
640 if env.get('RC_SCM_DATA'):
637 extras = json.loads(env['RC_SCM_DATA'])
641 extras = json.loads(env['RC_SCM_DATA'])
638 else:
642 else:
639 # fallback method to read from TXN-ID stored data
643 # fallback method to read from TXN-ID stored data
640 extras = _get_extras_from_commit_id(commit_id, path)
644 extras = _get_extras_from_commit_id(commit_id, path)
641 if not extras:
645 if not extras:
642 return 0
646 return 0
643
647
644 extras['commit_ids'] = [commit_id]
648 extras['commit_ids'] = [commit_id]
645 extras['txn_id'] = txn_id
649 extras['txn_id'] = txn_id
646 extras['new_refs'] = {
650 extras['new_refs'] = {
647 'branches': branches,
651 'branches': branches,
648 'bookmarks': [],
652 'bookmarks': [],
649 'tags': tags,
653 'tags': tags,
650 }
654 }
651
655
652 if 'repo_size' in extras['hooks']:
656 if 'repo_size' in extras['hooks']:
653 try:
657 try:
654 _call_hook('repo_size', extras, SvnMessageWriter())
658 _call_hook('repo_size', extras, SvnMessageWriter())
655 except Exception:
659 except Exception:
656 pass
660 pass
657
661
658 return _call_hook('post_push', extras, SvnMessageWriter())
662 return _call_hook('post_push', extras, SvnMessageWriter())
@@ -1,86 +1,86 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-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2018 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 os
18 import os
19 import shutil
19 import shutil
20 import tempfile
20 import tempfile
21
21
22 import configobj
22 import configobj
23
23
24
24
25 class ContextINI(object):
25 class ContextINI(object):
26 """
26 """
27 Allows to create a new test.ini file as a copy of existing one with edited
27 Allows to create a new test.ini file as a copy of existing one with edited
28 data. If existing file is not present, it creates a new one. Example usage::
28 data. If existing file is not present, it creates a new one. Example usage::
29
29
30 with TestINI('test.ini', [{'section': {'key': 'val'}}]) as new_test_ini_path:
30 with TestINI('test.ini', [{'section': {'key': 'val'}}]) as new_test_ini_path:
31 print 'vcsserver --config=%s' % new_test_ini
31 print 'vcsserver --config=%s' % new_test_ini
32 """
32 """
33
33
34 def __init__(self, ini_file_path, ini_params, new_file_prefix=None,
34 def __init__(self, ini_file_path, ini_params, new_file_prefix=None,
35 destroy=True):
35 destroy=True):
36 self.ini_file_path = ini_file_path
36 self.ini_file_path = ini_file_path
37 self.ini_params = ini_params
37 self.ini_params = ini_params
38 self.new_path = None
38 self.new_path = None
39 self.new_path_prefix = new_file_prefix or 'test'
39 self.new_path_prefix = new_file_prefix or 'test'
40 self.destroy = destroy
40 self.destroy = destroy
41
41
42 def __enter__(self):
42 def __enter__(self):
43 _, pref = tempfile.mkstemp()
43 _, pref = tempfile.mkstemp()
44 loc = tempfile.gettempdir()
44 loc = tempfile.gettempdir()
45 self.new_path = os.path.join(loc, '{}_{}_{}'.format(
45 self.new_path = os.path.join(loc, '{}_{}_{}'.format(
46 pref, self.new_path_prefix, self.ini_file_path))
46 pref, self.new_path_prefix, self.ini_file_path))
47
47
48 # copy ini file and modify according to the params, if we re-use a file
48 # copy ini file and modify according to the params, if we re-use a file
49 if os.path.isfile(self.ini_file_path):
49 if os.path.isfile(self.ini_file_path):
50 shutil.copy(self.ini_file_path, self.new_path)
50 shutil.copy(self.ini_file_path, self.new_path)
51 else:
51 else:
52 # create new dump file for configObj to write to.
52 # create new dump file for configObj to write to.
53 with open(self.new_path, 'wb'):
53 with open(self.new_path, 'wb'):
54 pass
54 pass
55
55
56 config = configobj.ConfigObj(
56 config = configobj.ConfigObj(
57 self.new_path, file_error=True, write_empty_values=True)
57 self.new_path, file_error=True, write_empty_values=True)
58
58
59 for data in self.ini_params:
59 for data in self.ini_params:
60 section, ini_params = data.items()[0]
60 section, ini_params = data.items()[0]
61 key, val = ini_params.items()[0]
61 key, val = ini_params.items()[0]
62 if section not in config:
62 if section not in config:
63 config[section] = {}
63 config[section] = {}
64 config[section][key] = val
64 config[section][key] = val
65
65
66 config.write()
66 config.write()
67 return self.new_path
67 return self.new_path
68
68
69 def __exit__(self, exc_type, exc_val, exc_tb):
69 def __exit__(self, exc_type, exc_val, exc_tb):
70 if self.destroy:
70 if self.destroy:
71 os.remove(self.new_path)
71 os.remove(self.new_path)
72
72
73
73
74 def no_newline_id_generator(test_name):
74 def no_newline_id_generator(test_name):
75 """
75 """
76 Generates a test name without spaces or newlines characters. Used for
76 Generates a test name without spaces or newlines characters. Used for
77 nicer output of progress of test
77 nicer output of progress of test
78 """
78 """
79 org_name = test_name
79 org_name = test_name
80 test_name = test_name\
80 test_name = str(test_name)\
81 .replace('\n', '_N') \
81 .replace('\n', '_N') \
82 .replace('\r', '_N') \
82 .replace('\r', '_N') \
83 .replace('\t', '_T') \
83 .replace('\t', '_T') \
84 .replace(' ', '_S')
84 .replace(' ', '_S')
85
85
86 return test_name or 'test-with-empty-name'
86 return test_name or 'test-with-empty-name'
@@ -1,241 +1,241 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-2018 RhodeCode GmbH
2 # Copyright (C) 2014-2018 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 contextlib
18 import contextlib
19 import io
19 import io
20 import threading
20 import threading
21 from BaseHTTPServer import BaseHTTPRequestHandler
21 from BaseHTTPServer 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 import simplejson as json
27 import simplejson as json
28
28
29 from vcsserver import hooks
29 from vcsserver import hooks
30
30
31
31
32 def get_hg_ui(extras=None):
32 def get_hg_ui(extras=None):
33 """Create a Config object with a valid RC_SCM_DATA entry."""
33 """Create a Config object with a valid RC_SCM_DATA entry."""
34 extras = extras or {}
34 extras = extras or {}
35 required_extras = {
35 required_extras = {
36 'username': '',
36 'username': '',
37 'repository': '',
37 'repository': '',
38 'locked_by': '',
38 'locked_by': '',
39 'scm': '',
39 'scm': '',
40 'make_lock': '',
40 'make_lock': '',
41 'action': '',
41 'action': '',
42 'ip': '',
42 'ip': '',
43 'hooks_uri': 'fake_hooks_uri',
43 'hooks_uri': 'fake_hooks_uri',
44 }
44 }
45 required_extras.update(extras)
45 required_extras.update(extras)
46 hg_ui = mercurial.ui.ui()
46 hg_ui = mercurial.ui.ui()
47 hg_ui.setconfig('rhodecode', 'RC_SCM_DATA', json.dumps(required_extras))
47 hg_ui.setconfig('rhodecode', 'RC_SCM_DATA', json.dumps(required_extras))
48
48
49 return hg_ui
49 return hg_ui
50
50
51
51
52 def test_git_pre_receive_is_disabled():
52 def test_git_pre_receive_is_disabled():
53 extras = {'hooks': ['pull']}
53 extras = {'hooks': ['pull']}
54 response = hooks.git_pre_receive(None, None,
54 response = hooks.git_pre_receive(None, None,
55 {'RC_SCM_DATA': json.dumps(extras)})
55 {'RC_SCM_DATA': json.dumps(extras)})
56
56
57 assert response == 0
57 assert response == 0
58
58
59
59
60 def test_git_post_receive_is_disabled():
60 def test_git_post_receive_is_disabled():
61 extras = {'hooks': ['pull']}
61 extras = {'hooks': ['pull']}
62 response = hooks.git_post_receive(None, '',
62 response = hooks.git_post_receive(None, '',
63 {'RC_SCM_DATA': json.dumps(extras)})
63 {'RC_SCM_DATA': json.dumps(extras)})
64
64
65 assert response == 0
65 assert response == 0
66
66
67
67
68 def test_git_post_receive_calls_repo_size():
68 def test_git_post_receive_calls_repo_size():
69 extras = {'hooks': ['push', 'repo_size']}
69 extras = {'hooks': ['push', 'repo_size']}
70 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
70 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
71 hooks.git_post_receive(
71 hooks.git_post_receive(
72 None, '', {'RC_SCM_DATA': json.dumps(extras)})
72 None, '', {'RC_SCM_DATA': json.dumps(extras)})
73 extras.update({'commit_ids': [],
73 extras.update({'commit_ids': [], 'hook_type': 'post_receive',
74 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
74 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
75 expected_calls = [
75 expected_calls = [
76 mock.call('repo_size', extras, mock.ANY),
76 mock.call('repo_size', extras, mock.ANY),
77 mock.call('post_push', extras, mock.ANY),
77 mock.call('post_push', extras, mock.ANY),
78 ]
78 ]
79 assert call_hook_mock.call_args_list == expected_calls
79 assert call_hook_mock.call_args_list == expected_calls
80
80
81
81
82 def test_git_post_receive_does_not_call_disabled_repo_size():
82 def test_git_post_receive_does_not_call_disabled_repo_size():
83 extras = {'hooks': ['push']}
83 extras = {'hooks': ['push']}
84 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
84 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
85 hooks.git_post_receive(
85 hooks.git_post_receive(
86 None, '', {'RC_SCM_DATA': json.dumps(extras)})
86 None, '', {'RC_SCM_DATA': json.dumps(extras)})
87 extras.update({'commit_ids': [],
87 extras.update({'commit_ids': [], 'hook_type': 'post_receive',
88 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
88 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
89 expected_calls = [
89 expected_calls = [
90 mock.call('post_push', extras, mock.ANY)
90 mock.call('post_push', extras, mock.ANY)
91 ]
91 ]
92 assert call_hook_mock.call_args_list == expected_calls
92 assert call_hook_mock.call_args_list == expected_calls
93
93
94
94
95 def test_repo_size_exception_does_not_affect_git_post_receive():
95 def test_repo_size_exception_does_not_affect_git_post_receive():
96 extras = {'hooks': ['push', 'repo_size']}
96 extras = {'hooks': ['push', 'repo_size']}
97 status = 0
97 status = 0
98
98
99 def side_effect(name, *args, **kwargs):
99 def side_effect(name, *args, **kwargs):
100 if name == 'repo_size':
100 if name == 'repo_size':
101 raise Exception('Fake exception')
101 raise Exception('Fake exception')
102 else:
102 else:
103 return status
103 return status
104
104
105 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
105 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
106 call_hook_mock.side_effect = side_effect
106 call_hook_mock.side_effect = side_effect
107 result = hooks.git_post_receive(
107 result = hooks.git_post_receive(
108 None, '', {'RC_SCM_DATA': json.dumps(extras)})
108 None, '', {'RC_SCM_DATA': json.dumps(extras)})
109 assert result == status
109 assert result == status
110
110
111
111
112 def test_git_pre_pull_is_disabled():
112 def test_git_pre_pull_is_disabled():
113 assert hooks.git_pre_pull({'hooks': ['push']}) == hooks.HookResponse(0, '')
113 assert hooks.git_pre_pull({'hooks': ['push']}) == hooks.HookResponse(0, '')
114
114
115
115
116 def test_git_post_pull_is_disabled():
116 def test_git_post_pull_is_disabled():
117 assert (
117 assert (
118 hooks.git_post_pull({'hooks': ['push']}) == hooks.HookResponse(0, ''))
118 hooks.git_post_pull({'hooks': ['push']}) == hooks.HookResponse(0, ''))
119
119
120
120
121 class TestGetHooksClient(object):
121 class TestGetHooksClient(object):
122
122
123 def test_returns_http_client_when_protocol_matches(self):
123 def test_returns_http_client_when_protocol_matches(self):
124 hooks_uri = 'localhost:8000'
124 hooks_uri = 'localhost:8000'
125 result = hooks._get_hooks_client({
125 result = hooks._get_hooks_client({
126 'hooks_uri': hooks_uri,
126 'hooks_uri': hooks_uri,
127 'hooks_protocol': 'http'
127 'hooks_protocol': 'http'
128 })
128 })
129 assert isinstance(result, hooks.HooksHttpClient)
129 assert isinstance(result, hooks.HooksHttpClient)
130 assert result.hooks_uri == hooks_uri
130 assert result.hooks_uri == hooks_uri
131
131
132 def test_returns_dummy_client_when_hooks_uri_not_specified(self):
132 def test_returns_dummy_client_when_hooks_uri_not_specified(self):
133 fake_module = mock.Mock()
133 fake_module = mock.Mock()
134 import_patcher = mock.patch.object(
134 import_patcher = mock.patch.object(
135 hooks.importlib, 'import_module', return_value=fake_module)
135 hooks.importlib, 'import_module', return_value=fake_module)
136 fake_module_name = 'fake.module'
136 fake_module_name = 'fake.module'
137 with import_patcher as import_mock:
137 with import_patcher as import_mock:
138 result = hooks._get_hooks_client(
138 result = hooks._get_hooks_client(
139 {'hooks_module': fake_module_name})
139 {'hooks_module': fake_module_name})
140
140
141 import_mock.assert_called_once_with(fake_module_name)
141 import_mock.assert_called_once_with(fake_module_name)
142 assert isinstance(result, hooks.HooksDummyClient)
142 assert isinstance(result, hooks.HooksDummyClient)
143 assert result._hooks_module == fake_module
143 assert result._hooks_module == fake_module
144
144
145
145
146 class TestHooksHttpClient(object):
146 class TestHooksHttpClient(object):
147 def test_init_sets_hooks_uri(self):
147 def test_init_sets_hooks_uri(self):
148 uri = 'localhost:3000'
148 uri = 'localhost:3000'
149 client = hooks.HooksHttpClient(uri)
149 client = hooks.HooksHttpClient(uri)
150 assert client.hooks_uri == uri
150 assert client.hooks_uri == uri
151
151
152 def test_serialize_returns_json_string(self):
152 def test_serialize_returns_json_string(self):
153 client = hooks.HooksHttpClient('localhost:3000')
153 client = hooks.HooksHttpClient('localhost:3000')
154 hook_name = 'test'
154 hook_name = 'test'
155 extras = {
155 extras = {
156 'first': 1,
156 'first': 1,
157 'second': 'two'
157 'second': 'two'
158 }
158 }
159 result = client._serialize(hook_name, extras)
159 result = client._serialize(hook_name, extras)
160 expected_result = json.dumps({
160 expected_result = json.dumps({
161 'method': hook_name,
161 'method': hook_name,
162 'extras': extras
162 'extras': extras
163 })
163 })
164 assert result == expected_result
164 assert result == expected_result
165
165
166 def test_call_queries_http_server(self, http_mirror):
166 def test_call_queries_http_server(self, http_mirror):
167 client = hooks.HooksHttpClient(http_mirror.uri)
167 client = hooks.HooksHttpClient(http_mirror.uri)
168 hook_name = 'test'
168 hook_name = 'test'
169 extras = {
169 extras = {
170 'first': 1,
170 'first': 1,
171 'second': 'two'
171 'second': 'two'
172 }
172 }
173 result = client(hook_name, extras)
173 result = client(hook_name, extras)
174 expected_result = {
174 expected_result = {
175 'method': hook_name,
175 'method': hook_name,
176 'extras': extras
176 'extras': extras
177 }
177 }
178 assert result == expected_result
178 assert result == expected_result
179
179
180
180
181 class TestHooksDummyClient(object):
181 class TestHooksDummyClient(object):
182 def test_init_imports_hooks_module(self):
182 def test_init_imports_hooks_module(self):
183 hooks_module_name = 'rhodecode.fake.module'
183 hooks_module_name = 'rhodecode.fake.module'
184 hooks_module = mock.MagicMock()
184 hooks_module = mock.MagicMock()
185
185
186 import_patcher = mock.patch.object(
186 import_patcher = mock.patch.object(
187 hooks.importlib, 'import_module', return_value=hooks_module)
187 hooks.importlib, 'import_module', return_value=hooks_module)
188 with import_patcher as import_mock:
188 with import_patcher as import_mock:
189 client = hooks.HooksDummyClient(hooks_module_name)
189 client = hooks.HooksDummyClient(hooks_module_name)
190 import_mock.assert_called_once_with(hooks_module_name)
190 import_mock.assert_called_once_with(hooks_module_name)
191 assert client._hooks_module == hooks_module
191 assert client._hooks_module == hooks_module
192
192
193 def test_call_returns_hook_result(self):
193 def test_call_returns_hook_result(self):
194 hooks_module_name = 'rhodecode.fake.module'
194 hooks_module_name = 'rhodecode.fake.module'
195 hooks_module = mock.MagicMock()
195 hooks_module = mock.MagicMock()
196 import_patcher = mock.patch.object(
196 import_patcher = mock.patch.object(
197 hooks.importlib, 'import_module', return_value=hooks_module)
197 hooks.importlib, 'import_module', return_value=hooks_module)
198 with import_patcher:
198 with import_patcher:
199 client = hooks.HooksDummyClient(hooks_module_name)
199 client = hooks.HooksDummyClient(hooks_module_name)
200
200
201 result = client('post_push', {})
201 result = client('post_push', {})
202 hooks_module.Hooks.assert_called_once_with()
202 hooks_module.Hooks.assert_called_once_with()
203 assert result == hooks_module.Hooks().__enter__().post_push()
203 assert result == hooks_module.Hooks().__enter__().post_push()
204
204
205
205
206 @pytest.fixture
206 @pytest.fixture
207 def http_mirror(request):
207 def http_mirror(request):
208 server = MirrorHttpServer()
208 server = MirrorHttpServer()
209 request.addfinalizer(server.stop)
209 request.addfinalizer(server.stop)
210 return server
210 return server
211
211
212
212
213 class MirrorHttpHandler(BaseHTTPRequestHandler):
213 class MirrorHttpHandler(BaseHTTPRequestHandler):
214 def do_POST(self):
214 def do_POST(self):
215 length = int(self.headers['Content-Length'])
215 length = int(self.headers['Content-Length'])
216 body = self.rfile.read(length).decode('utf-8')
216 body = self.rfile.read(length).decode('utf-8')
217 self.send_response(200)
217 self.send_response(200)
218 self.end_headers()
218 self.end_headers()
219 self.wfile.write(body)
219 self.wfile.write(body)
220
220
221
221
222 class MirrorHttpServer(object):
222 class MirrorHttpServer(object):
223 ip_address = '127.0.0.1'
223 ip_address = '127.0.0.1'
224 port = 0
224 port = 0
225
225
226 def __init__(self):
226 def __init__(self):
227 self._daemon = TCPServer((self.ip_address, 0), MirrorHttpHandler)
227 self._daemon = TCPServer((self.ip_address, 0), MirrorHttpHandler)
228 _, self.port = self._daemon.server_address
228 _, self.port = self._daemon.server_address
229 self._thread = threading.Thread(target=self._daemon.serve_forever)
229 self._thread = threading.Thread(target=self._daemon.serve_forever)
230 self._thread.daemon = True
230 self._thread.daemon = True
231 self._thread.start()
231 self._thread.start()
232
232
233 def stop(self):
233 def stop(self):
234 self._daemon.shutdown()
234 self._daemon.shutdown()
235 self._thread.join()
235 self._thread.join()
236 self._daemon = None
236 self._daemon = None
237 self._thread = None
237 self._thread = None
238
238
239 @property
239 @property
240 def uri(self):
240 def uri(self):
241 return '{}:{}'.format(self.ip_address, self.port)
241 return '{}:{}'.format(self.ip_address, self.port)
General Comments 0
You need to be logged in to leave comments. Login now