##// END OF EJS Templates
hooks: expose pushed refs inside hooks....
marcink -
r223:a7b3535b default
parent child Browse files
Show More
@@ -1,396 +1,426 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-2017 RodeCode GmbH
4 # Copyright (C) 2014-2017 RodeCode 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 sys
21 import sys
22 import json
22 import json
23 import logging
23 import logging
24 import collections
24 import collections
25 import importlib
25 import importlib
26 import subprocess
26 import subprocess
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
35 from vcsserver import exceptions
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 connection.request('POST', '/', body)
49 connection.request('POST', '/', body)
50 response = connection.getresponse()
50 response = connection.getresponse()
51 return json.loads(response.read())
51 return json.loads(response.read())
52
52
53 def _serialize(self, hook_name, extras):
53 def _serialize(self, hook_name, extras):
54 data = {
54 data = {
55 'method': hook_name,
55 'method': hook_name,
56 'extras': extras
56 'extras': extras
57 }
57 }
58 return json.dumps(data)
58 return json.dumps(data)
59
59
60
60
61 class HooksDummyClient(object):
61 class HooksDummyClient(object):
62 def __init__(self, hooks_module):
62 def __init__(self, hooks_module):
63 self._hooks_module = importlib.import_module(hooks_module)
63 self._hooks_module = importlib.import_module(hooks_module)
64
64
65 def __call__(self, hook_name, extras):
65 def __call__(self, hook_name, extras):
66 with self._hooks_module.Hooks() as hooks:
66 with self._hooks_module.Hooks() as hooks:
67 return getattr(hooks, hook_name)(extras)
67 return getattr(hooks, hook_name)(extras)
68
68
69
69
70 class RemoteMessageWriter(object):
70 class RemoteMessageWriter(object):
71 """Writer base class."""
71 """Writer base class."""
72 def write(self, message):
72 def write(self, message):
73 raise NotImplementedError()
73 raise NotImplementedError()
74
74
75
75
76 class HgMessageWriter(RemoteMessageWriter):
76 class HgMessageWriter(RemoteMessageWriter):
77 """Writer that knows how to send messages to mercurial clients."""
77 """Writer that knows how to send messages to mercurial clients."""
78
78
79 def __init__(self, ui):
79 def __init__(self, ui):
80 self.ui = ui
80 self.ui = ui
81
81
82 def write(self, message):
82 def write(self, message):
83 # TODO: Check why the quiet flag is set by default.
83 # TODO: Check why the quiet flag is set by default.
84 old = self.ui.quiet
84 old = self.ui.quiet
85 self.ui.quiet = False
85 self.ui.quiet = False
86 self.ui.status(message.encode('utf-8'))
86 self.ui.status(message.encode('utf-8'))
87 self.ui.quiet = old
87 self.ui.quiet = old
88
88
89
89
90 class GitMessageWriter(RemoteMessageWriter):
90 class GitMessageWriter(RemoteMessageWriter):
91 """Writer that knows how to send messages to git clients."""
91 """Writer that knows how to send messages to git clients."""
92
92
93 def __init__(self, stdout=None):
93 def __init__(self, stdout=None):
94 self.stdout = stdout or sys.stdout
94 self.stdout = stdout or sys.stdout
95
95
96 def write(self, message):
96 def write(self, message):
97 self.stdout.write(message.encode('utf-8'))
97 self.stdout.write(message.encode('utf-8'))
98
98
99
99
100 def _handle_exception(result):
100 def _handle_exception(result):
101 exception_class = result.get('exception')
101 exception_class = result.get('exception')
102 exception_traceback = result.get('exception_traceback')
102 exception_traceback = result.get('exception_traceback')
103
103
104 if exception_traceback:
104 if exception_traceback:
105 log.error('Got traceback from remote call:%s', exception_traceback)
105 log.error('Got traceback from remote call:%s', exception_traceback)
106
106
107 if exception_class == 'HTTPLockedRC':
107 if exception_class == 'HTTPLockedRC':
108 raise exceptions.RepositoryLockedException(*result['exception_args'])
108 raise exceptions.RepositoryLockedException(*result['exception_args'])
109 elif exception_class == 'RepositoryError':
109 elif exception_class == 'RepositoryError':
110 raise exceptions.VcsException(*result['exception_args'])
110 raise exceptions.VcsException(*result['exception_args'])
111 elif exception_class:
111 elif exception_class:
112 raise Exception('Got remote exception "%s" with args "%s"' %
112 raise Exception('Got remote exception "%s" with args "%s"' %
113 (exception_class, result['exception_args']))
113 (exception_class, result['exception_args']))
114
114
115
115
116 def _get_hooks_client(extras):
116 def _get_hooks_client(extras):
117 if 'hooks_uri' in extras:
117 if 'hooks_uri' in extras:
118 protocol = extras.get('hooks_protocol')
118 protocol = extras.get('hooks_protocol')
119 return HooksHttpClient(extras['hooks_uri'])
119 return HooksHttpClient(extras['hooks_uri'])
120 else:
120 else:
121 return HooksDummyClient(extras['hooks_module'])
121 return HooksDummyClient(extras['hooks_module'])
122
122
123
123
124 def _call_hook(hook_name, extras, writer):
124 def _call_hook(hook_name, extras, writer):
125 hooks = _get_hooks_client(extras)
125 hooks = _get_hooks_client(extras)
126 result = hooks(hook_name, extras)
126 result = hooks(hook_name, extras)
127 writer.write(result['output'])
127 writer.write(result['output'])
128 _handle_exception(result)
128 _handle_exception(result)
129
129
130 return result['status']
130 return result['status']
131
131
132
132
133 def _extras_from_ui(ui):
133 def _extras_from_ui(ui):
134 extras = json.loads(ui.config('rhodecode', 'RC_SCM_DATA'))
134 extras = json.loads(ui.config('rhodecode', 'RC_SCM_DATA'))
135 return extras
135 return extras
136
136
137
137
138 def repo_size(ui, repo, **kwargs):
138 def repo_size(ui, repo, **kwargs):
139 return _call_hook('repo_size', _extras_from_ui(ui), HgMessageWriter(ui))
139 return _call_hook('repo_size', _extras_from_ui(ui), HgMessageWriter(ui))
140
140
141
141
142 def pre_pull(ui, repo, **kwargs):
142 def pre_pull(ui, repo, **kwargs):
143 return _call_hook('pre_pull', _extras_from_ui(ui), HgMessageWriter(ui))
143 return _call_hook('pre_pull', _extras_from_ui(ui), HgMessageWriter(ui))
144
144
145
145
146 def post_pull(ui, repo, **kwargs):
146 def post_pull(ui, repo, **kwargs):
147 return _call_hook('post_pull', _extras_from_ui(ui), HgMessageWriter(ui))
147 return _call_hook('post_pull', _extras_from_ui(ui), HgMessageWriter(ui))
148
148
149
149
150 def _rev_range_hash(repo, node):
151
152 commits = []
153 for rev in xrange(repo[node], len(repo)):
154 ctx = repo[rev]
155 commit_id = mercurial.node.hex(ctx.node())
156 branch = ctx.branch()
157 commits.append((commit_id, branch))
158
159 return commits
160
161
150 def pre_push(ui, repo, node=None, **kwargs):
162 def pre_push(ui, repo, node=None, **kwargs):
151 extras = _extras_from_ui(ui)
163 extras = _extras_from_ui(ui)
152
164
153 rev_data = []
165 rev_data = []
154 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
166 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
155 branches = collections.defaultdict(list)
167 branches = collections.defaultdict(list)
156 for commit_id, branch in _rev_range_hash(repo, node, with_branch=True):
168 for commit_id, branch in _rev_range_hash(repo, node):
157 branches[branch].append(commit_id)
169 branches[branch].append(commit_id)
158
170
159 for branch, commits in branches.iteritems():
171 for branch, commits in branches.iteritems():
160 old_rev = kwargs.get('node_last') or commits[0]
172 old_rev = kwargs.get('node_last') or commits[0]
161 rev_data.append({
173 rev_data.append({
162 'old_rev': old_rev,
174 'old_rev': old_rev,
163 'new_rev': commits[-1],
175 'new_rev': commits[-1],
164 'ref': '',
176 'ref': '',
165 'type': 'branch',
177 'type': 'branch',
166 'name': branch,
178 'name': branch,
167 })
179 })
168
180
169 extras['commit_ids'] = rev_data
181 extras['commit_ids'] = rev_data
170 return _call_hook('pre_push', extras, HgMessageWriter(ui))
182 return _call_hook('pre_push', extras, HgMessageWriter(ui))
171
183
172
184
173 def _rev_range_hash(repo, node, with_branch=False):
185 def post_push(ui, repo, node, **kwargs):
186 extras = _extras_from_ui(ui)
187
188 commit_ids = []
189 branches = []
190 bookmarks = []
191 tags = []
174
192
175 commits = []
193 for commit_id, branch in _rev_range_hash(repo, node):
176 for rev in xrange(repo[node], len(repo)):
194 commit_ids.append(commit_id)
177 ctx = repo[rev]
195 if branch not in branches:
178 commit_id = mercurial.node.hex(ctx.node())
196 branches.append(branch)
179 branch = ctx.branch()
180 if with_branch:
181 commits.append((commit_id, branch))
182 else:
183 commits.append(commit_id)
184
197
185 return commits
198 if hasattr(ui, '_rc_pushkey_branches'):
186
199 bookmarks = ui._rc_pushkey_branches
187
200
188 def post_push(ui, repo, node, **kwargs):
189 commit_ids = _rev_range_hash(repo, node)
190
191 extras = _extras_from_ui(ui)
192 extras['commit_ids'] = commit_ids
201 extras['commit_ids'] = commit_ids
202 extras['new_refs'] = {
203 'branches': branches,
204 'bookmarks': bookmarks,
205 'tags': tags
206 }
193
207
194 return _call_hook('post_push', extras, HgMessageWriter(ui))
208 return _call_hook('post_push', extras, HgMessageWriter(ui))
195
209
196
210
197 def key_push(ui, repo, **kwargs):
211 def key_push(ui, repo, **kwargs):
198 if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks':
212 if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks':
199 # store new bookmarks in our UI object propagated later to post_push
213 # store new bookmarks in our UI object propagated later to post_push
200 ui._rc_pushkey_branches = repo[kwargs['key']].bookmarks()
214 ui._rc_pushkey_branches = repo[kwargs['key']].bookmarks()
201 return
215 return
202
216
203 # backward compat
217 # backward compat
204 log_pull_action = post_pull
218 log_pull_action = post_pull
205
219
206 # backward compat
220 # backward compat
207 log_push_action = post_push
221 log_push_action = post_push
208
222
209
223
210 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
224 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
211 """
225 """
212 Old hook name: keep here for backward compatibility.
226 Old hook name: keep here for backward compatibility.
213
227
214 This is only required when the installed git hooks are not upgraded.
228 This is only required when the installed git hooks are not upgraded.
215 """
229 """
216 pass
230 pass
217
231
218
232
219 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
233 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
220 """
234 """
221 Old hook name: keep here for backward compatibility.
235 Old hook name: keep here for backward compatibility.
222
236
223 This is only required when the installed git hooks are not upgraded.
237 This is only required when the installed git hooks are not upgraded.
224 """
238 """
225 pass
239 pass
226
240
227
241
228 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
242 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
229
243
230
244
231 def git_pre_pull(extras):
245 def git_pre_pull(extras):
232 """
246 """
233 Pre pull hook.
247 Pre pull hook.
234
248
235 :param extras: dictionary containing the keys defined in simplevcs
249 :param extras: dictionary containing the keys defined in simplevcs
236 :type extras: dict
250 :type extras: dict
237
251
238 :return: status code of the hook. 0 for success.
252 :return: status code of the hook. 0 for success.
239 :rtype: int
253 :rtype: int
240 """
254 """
241 if 'pull' not in extras['hooks']:
255 if 'pull' not in extras['hooks']:
242 return HookResponse(0, '')
256 return HookResponse(0, '')
243
257
244 stdout = io.BytesIO()
258 stdout = io.BytesIO()
245 try:
259 try:
246 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
260 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
247 except Exception as error:
261 except Exception as error:
248 status = 128
262 status = 128
249 stdout.write('ERROR: %s\n' % str(error))
263 stdout.write('ERROR: %s\n' % str(error))
250
264
251 return HookResponse(status, stdout.getvalue())
265 return HookResponse(status, stdout.getvalue())
252
266
253
267
254 def git_post_pull(extras):
268 def git_post_pull(extras):
255 """
269 """
256 Post pull hook.
270 Post pull hook.
257
271
258 :param extras: dictionary containing the keys defined in simplevcs
272 :param extras: dictionary containing the keys defined in simplevcs
259 :type extras: dict
273 :type extras: dict
260
274
261 :return: status code of the hook. 0 for success.
275 :return: status code of the hook. 0 for success.
262 :rtype: int
276 :rtype: int
263 """
277 """
264 if 'pull' not in extras['hooks']:
278 if 'pull' not in extras['hooks']:
265 return HookResponse(0, '')
279 return HookResponse(0, '')
266
280
267 stdout = io.BytesIO()
281 stdout = io.BytesIO()
268 try:
282 try:
269 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
283 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
270 except Exception as error:
284 except Exception as error:
271 status = 128
285 status = 128
272 stdout.write('ERROR: %s\n' % error)
286 stdout.write('ERROR: %s\n' % error)
273
287
274 return HookResponse(status, stdout.getvalue())
288 return HookResponse(status, stdout.getvalue())
275
289
276
290
277 def _parse_git_ref_lines(revision_lines):
291 def _parse_git_ref_lines(revision_lines):
278 rev_data = []
292 rev_data = []
279 for revision_line in revision_lines or []:
293 for revision_line in revision_lines or []:
280 old_rev, new_rev, ref = revision_line.strip().split(' ')
294 old_rev, new_rev, ref = revision_line.strip().split(' ')
281 ref_data = ref.split('/', 2)
295 ref_data = ref.split('/', 2)
282 if ref_data[1] in ('tags', 'heads'):
296 if ref_data[1] in ('tags', 'heads'):
283 rev_data.append({
297 rev_data.append({
284 'old_rev': old_rev,
298 'old_rev': old_rev,
285 'new_rev': new_rev,
299 'new_rev': new_rev,
286 'ref': ref,
300 'ref': ref,
287 'type': ref_data[1],
301 'type': ref_data[1],
288 'name': ref_data[2],
302 'name': ref_data[2],
289 })
303 })
290 return rev_data
304 return rev_data
291
305
292
306
293 def git_pre_receive(unused_repo_path, revision_lines, env):
307 def git_pre_receive(unused_repo_path, revision_lines, env):
294 """
308 """
295 Pre push hook.
309 Pre push hook.
296
310
297 :param extras: dictionary containing the keys defined in simplevcs
311 :param extras: dictionary containing the keys defined in simplevcs
298 :type extras: dict
312 :type extras: dict
299
313
300 :return: status code of the hook. 0 for success.
314 :return: status code of the hook. 0 for success.
301 :rtype: int
315 :rtype: int
302 """
316 """
303 extras = json.loads(env['RC_SCM_DATA'])
317 extras = json.loads(env['RC_SCM_DATA'])
304 rev_data = _parse_git_ref_lines(revision_lines)
318 rev_data = _parse_git_ref_lines(revision_lines)
305 if 'push' not in extras['hooks']:
319 if 'push' not in extras['hooks']:
306 return 0
320 return 0
307 extras['commit_ids'] = rev_data
321 extras['commit_ids'] = rev_data
308 return _call_hook('pre_push', extras, GitMessageWriter())
322 return _call_hook('pre_push', extras, GitMessageWriter())
309
323
310
324
311 def _run_command(arguments):
325 def _run_command(arguments):
312 """
326 """
313 Run the specified command and return the stdout.
327 Run the specified command and return the stdout.
314
328
315 :param arguments: sequence of program arguments (including the program name)
329 :param arguments: sequence of program arguments (including the program name)
316 :type arguments: list[str]
330 :type arguments: list[str]
317 """
331 """
318 # TODO(skreft): refactor this method and all the other similar ones.
332 # TODO(skreft): refactor this method and all the other similar ones.
319 # Probably this should be using subprocessio.
333 # Probably this should be using subprocessio.
320 process = subprocess.Popen(
334 process = subprocess.Popen(
321 arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
335 arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
322 stdout, stderr = process.communicate()
336 stdout, stderr = process.communicate()
323
337
324 if process.returncode != 0:
338 if process.returncode != 0:
325 raise Exception(
339 raise Exception(
326 'Command %s exited with exit code %s: stderr:%s' % (
340 'Command %s exited with exit code %s: stderr:%s' % (
327 arguments, process.returncode, stderr))
341 arguments, process.returncode, stderr))
328
342
329 return stdout
343 return stdout
330
344
331
345
332 def git_post_receive(unused_repo_path, revision_lines, env):
346 def git_post_receive(unused_repo_path, revision_lines, env):
333 """
347 """
334 Post push hook.
348 Post push hook.
335
349
336 :param extras: dictionary containing the keys defined in simplevcs
350 :param extras: dictionary containing the keys defined in simplevcs
337 :type extras: dict
351 :type extras: dict
338
352
339 :return: status code of the hook. 0 for success.
353 :return: status code of the hook. 0 for success.
340 :rtype: int
354 :rtype: int
341 """
355 """
342 extras = json.loads(env['RC_SCM_DATA'])
356 extras = json.loads(env['RC_SCM_DATA'])
343 if 'push' not in extras['hooks']:
357 if 'push' not in extras['hooks']:
344 return 0
358 return 0
345
359
346 rev_data = _parse_git_ref_lines(revision_lines)
360 rev_data = _parse_git_ref_lines(revision_lines)
347
361
348 git_revs = []
362 git_revs = []
349
363
350 # N.B.(skreft): it is ok to just call git, as git before calling a
364 # N.B.(skreft): it is ok to just call git, as git before calling a
351 # subcommand sets the PATH environment variable so that it point to the
365 # subcommand sets the PATH environment variable so that it point to the
352 # correct version of the git executable.
366 # correct version of the git executable.
353 empty_commit_id = '0' * 40
367 empty_commit_id = '0' * 40
368 branches = []
369 tags = []
354 for push_ref in rev_data:
370 for push_ref in rev_data:
355 type_ = push_ref['type']
371 type_ = push_ref['type']
372
356 if type_ == 'heads':
373 if type_ == 'heads':
357 if push_ref['old_rev'] == empty_commit_id:
374 if push_ref['old_rev'] == empty_commit_id:
375 # starting new branch case
376 if push_ref['name'] not in branches:
377 branches.append(push_ref['name'])
358
378
359 # Fix up head revision if needed
379 # Fix up head revision if needed
360 cmd = ['git', 'show', 'HEAD']
380 cmd = ['git', 'show', 'HEAD']
361 try:
381 try:
362 _run_command(cmd)
382 _run_command(cmd)
363 except Exception:
383 except Exception:
364 cmd = ['git', 'symbolic-ref', 'HEAD',
384 cmd = ['git', 'symbolic-ref', 'HEAD',
365 'refs/heads/%s' % push_ref['name']]
385 'refs/heads/%s' % push_ref['name']]
366 print("Setting default branch to %s" % push_ref['name'])
386 print("Setting default branch to %s" % push_ref['name'])
367 _run_command(cmd)
387 _run_command(cmd)
368
388
369 cmd = ['git', 'for-each-ref', '--format=%(refname)',
389 cmd = ['git', 'for-each-ref', '--format=%(refname)',
370 'refs/heads/*']
390 'refs/heads/*']
371 heads = _run_command(cmd)
391 heads = _run_command(cmd)
372 heads = heads.replace(push_ref['ref'], '')
392 heads = heads.replace(push_ref['ref'], '')
373 heads = ' '.join(head for head in heads.splitlines() if head)
393 heads = ' '.join(head for head in heads.splitlines() if head)
374 cmd = ['git', 'log', '--reverse', '--pretty=format:%H',
394 cmd = ['git', 'log', '--reverse', '--pretty=format:%H',
375 '--', push_ref['new_rev'], '--not', heads]
395 '--', push_ref['new_rev'], '--not', heads]
376 git_revs.extend(_run_command(cmd).splitlines())
396 git_revs.extend(_run_command(cmd).splitlines())
377 elif push_ref['new_rev'] == empty_commit_id:
397 elif push_ref['new_rev'] == empty_commit_id:
378 # delete branch case
398 # delete branch case
379 git_revs.append('delete_branch=>%s' % push_ref['name'])
399 git_revs.append('delete_branch=>%s' % push_ref['name'])
380 else:
400 else:
401 if push_ref['name'] not in branches:
402 branches.append(push_ref['name'])
403
381 cmd = ['git', 'log',
404 cmd = ['git', 'log',
382 '{old_rev}..{new_rev}'.format(**push_ref),
405 '{old_rev}..{new_rev}'.format(**push_ref),
383 '--reverse', '--pretty=format:%H']
406 '--reverse', '--pretty=format:%H']
384 git_revs.extend(_run_command(cmd).splitlines())
407 git_revs.extend(_run_command(cmd).splitlines())
385 elif type_ == 'tags':
408 elif type_ == 'tags':
409 if push_ref['name'] not in tags:
410 tags.append(push_ref['name'])
386 git_revs.append('tag=>%s' % push_ref['name'])
411 git_revs.append('tag=>%s' % push_ref['name'])
387
412
388 extras['commit_ids'] = git_revs
413 extras['commit_ids'] = git_revs
414 extras['new_refs'] = {
415 'branches': branches,
416 'bookmarks': [],
417 'tags': tags,
418 }
389
419
390 if 'repo_size' in extras['hooks']:
420 if 'repo_size' in extras['hooks']:
391 try:
421 try:
392 _call_hook('repo_size', extras, GitMessageWriter())
422 _call_hook('repo_size', extras, GitMessageWriter())
393 except:
423 except:
394 pass
424 pass
395
425
396 return _call_hook('post_push', extras, GitMessageWriter())
426 return _call_hook('post_push', extras, GitMessageWriter())
@@ -1,239 +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-2017 RodeCode GmbH
2 # Copyright (C) 2014-2017 RodeCode 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': [],
74 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
74 expected_calls = [
75 expected_calls = [
75 mock.call('repo_size', extras, mock.ANY),
76 mock.call('repo_size', extras, mock.ANY),
76 mock.call('post_push', extras, mock.ANY),
77 mock.call('post_push', extras, mock.ANY),
77 ]
78 ]
78 assert call_hook_mock.call_args_list == expected_calls
79 assert call_hook_mock.call_args_list == expected_calls
79
80
80
81
81 def test_git_post_receive_does_not_call_disabled_repo_size():
82 def test_git_post_receive_does_not_call_disabled_repo_size():
82 extras = {'hooks': ['push']}
83 extras = {'hooks': ['push']}
83 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
84 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
84 hooks.git_post_receive(
85 hooks.git_post_receive(
85 None, '', {'RC_SCM_DATA': json.dumps(extras)})
86 None, '', {'RC_SCM_DATA': json.dumps(extras)})
86 extras.update({'commit_ids': []})
87 extras.update({'commit_ids': [],
88 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
87 expected_calls = [
89 expected_calls = [
88 mock.call('post_push', extras, mock.ANY)
90 mock.call('post_push', extras, mock.ANY)
89 ]
91 ]
90 assert call_hook_mock.call_args_list == expected_calls
92 assert call_hook_mock.call_args_list == expected_calls
91
93
92
94
93 def test_repo_size_exception_does_not_affect_git_post_receive():
95 def test_repo_size_exception_does_not_affect_git_post_receive():
94 extras = {'hooks': ['push', 'repo_size']}
96 extras = {'hooks': ['push', 'repo_size']}
95 status = 0
97 status = 0
96
98
97 def side_effect(name, *args, **kwargs):
99 def side_effect(name, *args, **kwargs):
98 if name == 'repo_size':
100 if name == 'repo_size':
99 raise Exception('Fake exception')
101 raise Exception('Fake exception')
100 else:
102 else:
101 return status
103 return status
102
104
103 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
105 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
104 call_hook_mock.side_effect = side_effect
106 call_hook_mock.side_effect = side_effect
105 result = hooks.git_post_receive(
107 result = hooks.git_post_receive(
106 None, '', {'RC_SCM_DATA': json.dumps(extras)})
108 None, '', {'RC_SCM_DATA': json.dumps(extras)})
107 assert result == status
109 assert result == status
108
110
109
111
110 def test_git_pre_pull_is_disabled():
112 def test_git_pre_pull_is_disabled():
111 assert hooks.git_pre_pull({'hooks': ['push']}) == hooks.HookResponse(0, '')
113 assert hooks.git_pre_pull({'hooks': ['push']}) == hooks.HookResponse(0, '')
112
114
113
115
114 def test_git_post_pull_is_disabled():
116 def test_git_post_pull_is_disabled():
115 assert (
117 assert (
116 hooks.git_post_pull({'hooks': ['push']}) == hooks.HookResponse(0, ''))
118 hooks.git_post_pull({'hooks': ['push']}) == hooks.HookResponse(0, ''))
117
119
118
120
119 class TestGetHooksClient(object):
121 class TestGetHooksClient(object):
120
122
121 def test_returns_http_client_when_protocol_matches(self):
123 def test_returns_http_client_when_protocol_matches(self):
122 hooks_uri = 'localhost:8000'
124 hooks_uri = 'localhost:8000'
123 result = hooks._get_hooks_client({
125 result = hooks._get_hooks_client({
124 'hooks_uri': hooks_uri,
126 'hooks_uri': hooks_uri,
125 'hooks_protocol': 'http'
127 'hooks_protocol': 'http'
126 })
128 })
127 assert isinstance(result, hooks.HooksHttpClient)
129 assert isinstance(result, hooks.HooksHttpClient)
128 assert result.hooks_uri == hooks_uri
130 assert result.hooks_uri == hooks_uri
129
131
130 def test_returns_dummy_client_when_hooks_uri_not_specified(self):
132 def test_returns_dummy_client_when_hooks_uri_not_specified(self):
131 fake_module = mock.Mock()
133 fake_module = mock.Mock()
132 import_patcher = mock.patch.object(
134 import_patcher = mock.patch.object(
133 hooks.importlib, 'import_module', return_value=fake_module)
135 hooks.importlib, 'import_module', return_value=fake_module)
134 fake_module_name = 'fake.module'
136 fake_module_name = 'fake.module'
135 with import_patcher as import_mock:
137 with import_patcher as import_mock:
136 result = hooks._get_hooks_client(
138 result = hooks._get_hooks_client(
137 {'hooks_module': fake_module_name})
139 {'hooks_module': fake_module_name})
138
140
139 import_mock.assert_called_once_with(fake_module_name)
141 import_mock.assert_called_once_with(fake_module_name)
140 assert isinstance(result, hooks.HooksDummyClient)
142 assert isinstance(result, hooks.HooksDummyClient)
141 assert result._hooks_module == fake_module
143 assert result._hooks_module == fake_module
142
144
143
145
144 class TestHooksHttpClient(object):
146 class TestHooksHttpClient(object):
145 def test_init_sets_hooks_uri(self):
147 def test_init_sets_hooks_uri(self):
146 uri = 'localhost:3000'
148 uri = 'localhost:3000'
147 client = hooks.HooksHttpClient(uri)
149 client = hooks.HooksHttpClient(uri)
148 assert client.hooks_uri == uri
150 assert client.hooks_uri == uri
149
151
150 def test_serialize_returns_json_string(self):
152 def test_serialize_returns_json_string(self):
151 client = hooks.HooksHttpClient('localhost:3000')
153 client = hooks.HooksHttpClient('localhost:3000')
152 hook_name = 'test'
154 hook_name = 'test'
153 extras = {
155 extras = {
154 'first': 1,
156 'first': 1,
155 'second': 'two'
157 'second': 'two'
156 }
158 }
157 result = client._serialize(hook_name, extras)
159 result = client._serialize(hook_name, extras)
158 expected_result = json.dumps({
160 expected_result = json.dumps({
159 'method': hook_name,
161 'method': hook_name,
160 'extras': extras
162 'extras': extras
161 })
163 })
162 assert result == expected_result
164 assert result == expected_result
163
165
164 def test_call_queries_http_server(self, http_mirror):
166 def test_call_queries_http_server(self, http_mirror):
165 client = hooks.HooksHttpClient(http_mirror.uri)
167 client = hooks.HooksHttpClient(http_mirror.uri)
166 hook_name = 'test'
168 hook_name = 'test'
167 extras = {
169 extras = {
168 'first': 1,
170 'first': 1,
169 'second': 'two'
171 'second': 'two'
170 }
172 }
171 result = client(hook_name, extras)
173 result = client(hook_name, extras)
172 expected_result = {
174 expected_result = {
173 'method': hook_name,
175 'method': hook_name,
174 'extras': extras
176 'extras': extras
175 }
177 }
176 assert result == expected_result
178 assert result == expected_result
177
179
178
180
179 class TestHooksDummyClient(object):
181 class TestHooksDummyClient(object):
180 def test_init_imports_hooks_module(self):
182 def test_init_imports_hooks_module(self):
181 hooks_module_name = 'rhodecode.fake.module'
183 hooks_module_name = 'rhodecode.fake.module'
182 hooks_module = mock.MagicMock()
184 hooks_module = mock.MagicMock()
183
185
184 import_patcher = mock.patch.object(
186 import_patcher = mock.patch.object(
185 hooks.importlib, 'import_module', return_value=hooks_module)
187 hooks.importlib, 'import_module', return_value=hooks_module)
186 with import_patcher as import_mock:
188 with import_patcher as import_mock:
187 client = hooks.HooksDummyClient(hooks_module_name)
189 client = hooks.HooksDummyClient(hooks_module_name)
188 import_mock.assert_called_once_with(hooks_module_name)
190 import_mock.assert_called_once_with(hooks_module_name)
189 assert client._hooks_module == hooks_module
191 assert client._hooks_module == hooks_module
190
192
191 def test_call_returns_hook_result(self):
193 def test_call_returns_hook_result(self):
192 hooks_module_name = 'rhodecode.fake.module'
194 hooks_module_name = 'rhodecode.fake.module'
193 hooks_module = mock.MagicMock()
195 hooks_module = mock.MagicMock()
194 import_patcher = mock.patch.object(
196 import_patcher = mock.patch.object(
195 hooks.importlib, 'import_module', return_value=hooks_module)
197 hooks.importlib, 'import_module', return_value=hooks_module)
196 with import_patcher:
198 with import_patcher:
197 client = hooks.HooksDummyClient(hooks_module_name)
199 client = hooks.HooksDummyClient(hooks_module_name)
198
200
199 result = client('post_push', {})
201 result = client('post_push', {})
200 hooks_module.Hooks.assert_called_once_with()
202 hooks_module.Hooks.assert_called_once_with()
201 assert result == hooks_module.Hooks().__enter__().post_push()
203 assert result == hooks_module.Hooks().__enter__().post_push()
202
204
203
205
204 @pytest.fixture
206 @pytest.fixture
205 def http_mirror(request):
207 def http_mirror(request):
206 server = MirrorHttpServer()
208 server = MirrorHttpServer()
207 request.addfinalizer(server.stop)
209 request.addfinalizer(server.stop)
208 return server
210 return server
209
211
210
212
211 class MirrorHttpHandler(BaseHTTPRequestHandler):
213 class MirrorHttpHandler(BaseHTTPRequestHandler):
212 def do_POST(self):
214 def do_POST(self):
213 length = int(self.headers['Content-Length'])
215 length = int(self.headers['Content-Length'])
214 body = self.rfile.read(length).decode('utf-8')
216 body = self.rfile.read(length).decode('utf-8')
215 self.send_response(200)
217 self.send_response(200)
216 self.end_headers()
218 self.end_headers()
217 self.wfile.write(body)
219 self.wfile.write(body)
218
220
219
221
220 class MirrorHttpServer(object):
222 class MirrorHttpServer(object):
221 ip_address = '127.0.0.1'
223 ip_address = '127.0.0.1'
222 port = 0
224 port = 0
223
225
224 def __init__(self):
226 def __init__(self):
225 self._daemon = TCPServer((self.ip_address, 0), MirrorHttpHandler)
227 self._daemon = TCPServer((self.ip_address, 0), MirrorHttpHandler)
226 _, self.port = self._daemon.server_address
228 _, self.port = self._daemon.server_address
227 self._thread = threading.Thread(target=self._daemon.serve_forever)
229 self._thread = threading.Thread(target=self._daemon.serve_forever)
228 self._thread.daemon = True
230 self._thread.daemon = True
229 self._thread.start()
231 self._thread.start()
230
232
231 def stop(self):
233 def stop(self):
232 self._daemon.shutdown()
234 self._daemon.shutdown()
233 self._thread.join()
235 self._thread.join()
234 self._daemon = None
236 self._daemon = None
235 self._thread = None
237 self._thread = None
236
238
237 @property
239 @property
238 def uri(self):
240 def uri(self):
239 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