##// END OF EJS Templates
hooks: fix case for SSH hooks executed in certain condition with errors....
marcink -
r347:ca693205 default
parent child Browse files
Show More
@@ -1,478 +1,482 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # RhodeCode VCSServer provides access to different vcs backends via network.
4 4 # Copyright (C) 2014-2017 RodeCode GmbH
5 5 #
6 6 # This program is free software; you can redistribute it and/or modify
7 7 # it under the terms of the GNU General Public License as published by
8 8 # the Free Software Foundation; either version 3 of the License, or
9 9 # (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software Foundation,
18 18 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19 19
20 20 import io
21 21 import os
22 22 import sys
23 23 import json
24 24 import logging
25 25 import collections
26 26 import importlib
27 27 import subprocess
28 28
29 29 from httplib import HTTPConnection
30 30
31 31
32 32 import mercurial.scmutil
33 33 import mercurial.node
34 34 import simplejson as json
35 35
36 36 from vcsserver import exceptions
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 class HooksHttpClient(object):
42 42 connection = None
43 43
44 44 def __init__(self, hooks_uri):
45 45 self.hooks_uri = hooks_uri
46 46
47 47 def __call__(self, method, extras):
48 48 connection = HTTPConnection(self.hooks_uri)
49 49 body = self._serialize(method, extras)
50 50 connection.request('POST', '/', body)
51 51 response = connection.getresponse()
52 52 return json.loads(response.read())
53 53
54 54 def _serialize(self, hook_name, extras):
55 55 data = {
56 56 'method': hook_name,
57 57 'extras': extras
58 58 }
59 59 return json.dumps(data)
60 60
61 61
62 62 class HooksDummyClient(object):
63 63 def __init__(self, hooks_module):
64 64 self._hooks_module = importlib.import_module(hooks_module)
65 65
66 66 def __call__(self, hook_name, extras):
67 67 with self._hooks_module.Hooks() as hooks:
68 68 return getattr(hooks, hook_name)(extras)
69 69
70 70
71 71 class RemoteMessageWriter(object):
72 72 """Writer base class."""
73 73 def write(self, message):
74 74 raise NotImplementedError()
75 75
76 76
77 77 class HgMessageWriter(RemoteMessageWriter):
78 78 """Writer that knows how to send messages to mercurial clients."""
79 79
80 80 def __init__(self, ui):
81 81 self.ui = ui
82 82
83 83 def write(self, message):
84 84 # TODO: Check why the quiet flag is set by default.
85 85 old = self.ui.quiet
86 86 self.ui.quiet = False
87 87 self.ui.status(message.encode('utf-8'))
88 88 self.ui.quiet = old
89 89
90 90
91 91 class GitMessageWriter(RemoteMessageWriter):
92 92 """Writer that knows how to send messages to git clients."""
93 93
94 94 def __init__(self, stdout=None):
95 95 self.stdout = stdout or sys.stdout
96 96
97 97 def write(self, message):
98 98 self.stdout.write(message.encode('utf-8'))
99 99
100 100
101 101 def _handle_exception(result):
102 102 exception_class = result.get('exception')
103 103 exception_traceback = result.get('exception_traceback')
104 104
105 105 if exception_traceback:
106 106 log.error('Got traceback from remote call:%s', exception_traceback)
107 107
108 108 if exception_class == 'HTTPLockedRC':
109 109 raise exceptions.RepositoryLockedException(*result['exception_args'])
110 110 elif exception_class == 'RepositoryError':
111 111 raise exceptions.VcsException(*result['exception_args'])
112 112 elif exception_class:
113 113 raise Exception('Got remote exception "%s" with args "%s"' %
114 114 (exception_class, result['exception_args']))
115 115
116 116
117 117 def _get_hooks_client(extras):
118 118 if 'hooks_uri' in extras:
119 119 protocol = extras.get('hooks_protocol')
120 120 return HooksHttpClient(extras['hooks_uri'])
121 121 else:
122 122 return HooksDummyClient(extras['hooks_module'])
123 123
124 124
125 125 def _call_hook(hook_name, extras, writer):
126 126 hooks = _get_hooks_client(extras)
127 127 result = hooks(hook_name, extras)
128 128 log.debug('Hooks got result: %s', result)
129 129 writer.write(result['output'])
130 130 _handle_exception(result)
131 131
132 132 return result['status']
133 133
134 134
135 135 def _extras_from_ui(ui):
136 136 hook_data = ui.config('rhodecode', 'RC_SCM_DATA')
137 137 if not hook_data:
138 138 # maybe it's inside environ ?
139 139 env_hook_data = os.environ.get('RC_SCM_DATA')
140 140 if env_hook_data:
141 141 hook_data = env_hook_data
142 142
143 extras = json.loads(hook_data)
143 extras = {}
144 if hook_data:
145 extras = json.loads(hook_data)
144 146 return extras
145 147
146 148
147 149 def _rev_range_hash(repo, node):
148 150
149 151 commits = []
150 152 for rev in xrange(repo[node], len(repo)):
151 153 ctx = repo[rev]
152 154 commit_id = mercurial.node.hex(ctx.node())
153 155 branch = ctx.branch()
154 156 commits.append((commit_id, branch))
155 157
156 158 return commits
157 159
158 160
159 161 def repo_size(ui, repo, **kwargs):
160 162 extras = _extras_from_ui(ui)
161 163 return _call_hook('repo_size', extras, HgMessageWriter(ui))
162 164
163 165
164 166 def pre_pull(ui, repo, **kwargs):
165 167 extras = _extras_from_ui(ui)
166 168 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
167 169
168 170
169 171 def pre_pull_ssh(ui, repo, **kwargs):
170 if _extras_from_ui(ui).get('SSH'):
172 extras = _extras_from_ui(ui)
173 if extras and extras.get('SSH'):
171 174 return pre_pull(ui, repo, **kwargs)
172 175 return 0
173 176
174 177
175 178 def post_pull(ui, repo, **kwargs):
176 179 extras = _extras_from_ui(ui)
177 180 return _call_hook('post_pull', extras, HgMessageWriter(ui))
178 181
179 182
180 183 def post_pull_ssh(ui, repo, **kwargs):
181 if _extras_from_ui(ui).get('SSH'):
184 extras = _extras_from_ui(ui)
185 if extras and extras.get('SSH'):
182 186 return post_pull(ui, repo, **kwargs)
183 187 return 0
184 188
185 189
186 190 def pre_push(ui, repo, node=None, **kwargs):
187 191 extras = _extras_from_ui(ui)
188 192
189 193 rev_data = []
190 194 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
191 195 branches = collections.defaultdict(list)
192 196 for commit_id, branch in _rev_range_hash(repo, node):
193 197 branches[branch].append(commit_id)
194 198
195 199 for branch, commits in branches.iteritems():
196 200 old_rev = kwargs.get('node_last') or commits[0]
197 201 rev_data.append({
198 202 'old_rev': old_rev,
199 203 'new_rev': commits[-1],
200 204 'ref': '',
201 205 'type': 'branch',
202 206 'name': branch,
203 207 })
204 208
205 209 extras['commit_ids'] = rev_data
206 210 return _call_hook('pre_push', extras, HgMessageWriter(ui))
207 211
208 212
209 213 def pre_push_ssh(ui, repo, node=None, **kwargs):
210 214 if _extras_from_ui(ui).get('SSH'):
211 215 return pre_push(ui, repo, node, **kwargs)
212 216
213 217 return 0
214 218
215 219
216 220 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
217 221 extras = _extras_from_ui(ui)
218 222 if extras.get('SSH'):
219 223 permission = extras['SSH_PERMISSIONS']
220 224
221 225 if 'repository.write' == permission or 'repository.admin' == permission:
222 226 return 0
223 227
224 228 # non-zero ret code
225 229 return 1
226 230
227 231 return 0
228 232
229 233
230 234 def post_push(ui, repo, node, **kwargs):
231 235 extras = _extras_from_ui(ui)
232 236
233 237 commit_ids = []
234 238 branches = []
235 239 bookmarks = []
236 240 tags = []
237 241
238 242 for commit_id, branch in _rev_range_hash(repo, node):
239 243 commit_ids.append(commit_id)
240 244 if branch not in branches:
241 245 branches.append(branch)
242 246
243 247 if hasattr(ui, '_rc_pushkey_branches'):
244 248 bookmarks = ui._rc_pushkey_branches
245 249
246 250 extras['commit_ids'] = commit_ids
247 251 extras['new_refs'] = {
248 252 'branches': branches,
249 253 'bookmarks': bookmarks,
250 254 'tags': tags
251 255 }
252 256
253 257 return _call_hook('post_push', extras, HgMessageWriter(ui))
254 258
255 259
256 260 def post_push_ssh(ui, repo, node, **kwargs):
257 261 if _extras_from_ui(ui).get('SSH'):
258 262 return post_push(ui, repo, node, **kwargs)
259 263 return 0
260 264
261 265
262 266 def key_push(ui, repo, **kwargs):
263 267 if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks':
264 268 # store new bookmarks in our UI object propagated later to post_push
265 269 ui._rc_pushkey_branches = repo[kwargs['key']].bookmarks()
266 270 return
267 271
268 272
269 273 # backward compat
270 274 log_pull_action = post_pull
271 275
272 276 # backward compat
273 277 log_push_action = post_push
274 278
275 279
276 280 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
277 281 """
278 282 Old hook name: keep here for backward compatibility.
279 283
280 284 This is only required when the installed git hooks are not upgraded.
281 285 """
282 286 pass
283 287
284 288
285 289 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
286 290 """
287 291 Old hook name: keep here for backward compatibility.
288 292
289 293 This is only required when the installed git hooks are not upgraded.
290 294 """
291 295 pass
292 296
293 297
294 298 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
295 299
296 300
297 301 def git_pre_pull(extras):
298 302 """
299 303 Pre pull hook.
300 304
301 305 :param extras: dictionary containing the keys defined in simplevcs
302 306 :type extras: dict
303 307
304 308 :return: status code of the hook. 0 for success.
305 309 :rtype: int
306 310 """
307 311 if 'pull' not in extras['hooks']:
308 312 return HookResponse(0, '')
309 313
310 314 stdout = io.BytesIO()
311 315 try:
312 316 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
313 317 except Exception as error:
314 318 status = 128
315 319 stdout.write('ERROR: %s\n' % str(error))
316 320
317 321 return HookResponse(status, stdout.getvalue())
318 322
319 323
320 324 def git_post_pull(extras):
321 325 """
322 326 Post pull hook.
323 327
324 328 :param extras: dictionary containing the keys defined in simplevcs
325 329 :type extras: dict
326 330
327 331 :return: status code of the hook. 0 for success.
328 332 :rtype: int
329 333 """
330 334 if 'pull' not in extras['hooks']:
331 335 return HookResponse(0, '')
332 336
333 337 stdout = io.BytesIO()
334 338 try:
335 339 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
336 340 except Exception as error:
337 341 status = 128
338 342 stdout.write('ERROR: %s\n' % error)
339 343
340 344 return HookResponse(status, stdout.getvalue())
341 345
342 346
343 347 def _parse_git_ref_lines(revision_lines):
344 348 rev_data = []
345 349 for revision_line in revision_lines or []:
346 350 old_rev, new_rev, ref = revision_line.strip().split(' ')
347 351 ref_data = ref.split('/', 2)
348 352 if ref_data[1] in ('tags', 'heads'):
349 353 rev_data.append({
350 354 'old_rev': old_rev,
351 355 'new_rev': new_rev,
352 356 'ref': ref,
353 357 'type': ref_data[1],
354 358 'name': ref_data[2],
355 359 })
356 360 return rev_data
357 361
358 362
359 363 def git_pre_receive(unused_repo_path, revision_lines, env):
360 364 """
361 365 Pre push hook.
362 366
363 367 :param extras: dictionary containing the keys defined in simplevcs
364 368 :type extras: dict
365 369
366 370 :return: status code of the hook. 0 for success.
367 371 :rtype: int
368 372 """
369 373 extras = json.loads(env['RC_SCM_DATA'])
370 374 rev_data = _parse_git_ref_lines(revision_lines)
371 375 if 'push' not in extras['hooks']:
372 376 return 0
373 377 extras['commit_ids'] = rev_data
374 378 return _call_hook('pre_push', extras, GitMessageWriter())
375 379
376 380
377 381 def _run_command(arguments):
378 382 """
379 383 Run the specified command and return the stdout.
380 384
381 385 :param arguments: sequence of program arguments (including the program name)
382 386 :type arguments: list[str]
383 387 """
384 388 # TODO(skreft): refactor this method and all the other similar ones.
385 389 # Probably this should be using subprocessio.
386 390 process = subprocess.Popen(
387 391 arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
388 392 stdout, stderr = process.communicate()
389 393
390 394 if process.returncode != 0:
391 395 raise Exception(
392 396 'Command %s exited with exit code %s: stderr:%s' % (
393 397 arguments, process.returncode, stderr))
394 398
395 399 return stdout
396 400
397 401
398 402 def git_post_receive(unused_repo_path, revision_lines, env):
399 403 """
400 404 Post push hook.
401 405
402 406 :param extras: dictionary containing the keys defined in simplevcs
403 407 :type extras: dict
404 408
405 409 :return: status code of the hook. 0 for success.
406 410 :rtype: int
407 411 """
408 412 extras = json.loads(env['RC_SCM_DATA'])
409 413 if 'push' not in extras['hooks']:
410 414 return 0
411 415
412 416 rev_data = _parse_git_ref_lines(revision_lines)
413 417
414 418 git_revs = []
415 419
416 420 # N.B.(skreft): it is ok to just call git, as git before calling a
417 421 # subcommand sets the PATH environment variable so that it point to the
418 422 # correct version of the git executable.
419 423 empty_commit_id = '0' * 40
420 424 branches = []
421 425 tags = []
422 426 for push_ref in rev_data:
423 427 type_ = push_ref['type']
424 428
425 429 if type_ == 'heads':
426 430 if push_ref['old_rev'] == empty_commit_id:
427 431 # starting new branch case
428 432 if push_ref['name'] not in branches:
429 433 branches.append(push_ref['name'])
430 434
431 435 # Fix up head revision if needed
432 436 cmd = ['git', 'show', 'HEAD']
433 437 try:
434 438 _run_command(cmd)
435 439 except Exception:
436 440 cmd = ['git', 'symbolic-ref', 'HEAD',
437 441 'refs/heads/%s' % push_ref['name']]
438 442 print("Setting default branch to %s" % push_ref['name'])
439 443 _run_command(cmd)
440 444
441 445 cmd = ['git', 'for-each-ref', '--format=%(refname)',
442 446 'refs/heads/*']
443 447 heads = _run_command(cmd)
444 448 heads = heads.replace(push_ref['ref'], '')
445 449 heads = ' '.join(head for head in heads.splitlines() if head)
446 450 cmd = ['git', 'log', '--reverse', '--pretty=format:%H',
447 451 '--', push_ref['new_rev'], '--not', heads]
448 452 git_revs.extend(_run_command(cmd).splitlines())
449 453 elif push_ref['new_rev'] == empty_commit_id:
450 454 # delete branch case
451 455 git_revs.append('delete_branch=>%s' % push_ref['name'])
452 456 else:
453 457 if push_ref['name'] not in branches:
454 458 branches.append(push_ref['name'])
455 459
456 460 cmd = ['git', 'log',
457 461 '{old_rev}..{new_rev}'.format(**push_ref),
458 462 '--reverse', '--pretty=format:%H']
459 463 git_revs.extend(_run_command(cmd).splitlines())
460 464 elif type_ == 'tags':
461 465 if push_ref['name'] not in tags:
462 466 tags.append(push_ref['name'])
463 467 git_revs.append('tag=>%s' % push_ref['name'])
464 468
465 469 extras['commit_ids'] = git_revs
466 470 extras['new_refs'] = {
467 471 'branches': branches,
468 472 'bookmarks': [],
469 473 'tags': tags,
470 474 }
471 475
472 476 if 'repo_size' in extras['hooks']:
473 477 try:
474 478 _call_hook('repo_size', extras, GitMessageWriter())
475 479 except:
476 480 pass
477 481
478 482 return _call_hook('post_push', extras, GitMessageWriter())
General Comments 0
You need to be logged in to leave comments. Login now