##// END OF EJS Templates
svn: make hooks safe and fully backward compatible....
marcink -
r436:74eb96f2 stable
parent child Browse files
Show More
@@ -1,541 +1,570 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-2018 RhodeCode 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 logging
24 24 import collections
25 25 import importlib
26 26 import base64
27 27
28 28 from httplib import HTTPConnection
29 29
30 30
31 31 import mercurial.scmutil
32 32 import mercurial.node
33 33 import simplejson as json
34 34
35 35 from vcsserver import exceptions, subprocessio, settings
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 class HooksHttpClient(object):
41 41 connection = None
42 42
43 43 def __init__(self, hooks_uri):
44 44 self.hooks_uri = hooks_uri
45 45
46 46 def __call__(self, method, extras):
47 47 connection = HTTPConnection(self.hooks_uri)
48 48 body = self._serialize(method, extras)
49 49 try:
50 50 connection.request('POST', '/', body)
51 51 except Exception:
52 52 log.error('Connection failed on %s', connection)
53 53 raise
54 54 response = connection.getresponse()
55 55 return json.loads(response.read())
56 56
57 57 def _serialize(self, hook_name, extras):
58 58 data = {
59 59 'method': hook_name,
60 60 'extras': extras
61 61 }
62 62 return json.dumps(data)
63 63
64 64
65 65 class HooksDummyClient(object):
66 66 def __init__(self, hooks_module):
67 67 self._hooks_module = importlib.import_module(hooks_module)
68 68
69 69 def __call__(self, hook_name, extras):
70 70 with self._hooks_module.Hooks() as hooks:
71 71 return getattr(hooks, hook_name)(extras)
72 72
73 73
74 74 class RemoteMessageWriter(object):
75 75 """Writer base class."""
76 76 def write(self, message):
77 77 raise NotImplementedError()
78 78
79 79
80 80 class HgMessageWriter(RemoteMessageWriter):
81 81 """Writer that knows how to send messages to mercurial clients."""
82 82
83 83 def __init__(self, ui):
84 84 self.ui = ui
85 85
86 86 def write(self, message):
87 87 # TODO: Check why the quiet flag is set by default.
88 88 old = self.ui.quiet
89 89 self.ui.quiet = False
90 90 self.ui.status(message.encode('utf-8'))
91 91 self.ui.quiet = old
92 92
93 93
94 94 class GitMessageWriter(RemoteMessageWriter):
95 95 """Writer that knows how to send messages to git clients."""
96 96
97 97 def __init__(self, stdout=None):
98 98 self.stdout = stdout or sys.stdout
99 99
100 100 def write(self, message):
101 101 self.stdout.write(message.encode('utf-8'))
102 102
103 103
104 104 class SvnMessageWriter(RemoteMessageWriter):
105 105 """Writer that knows how to send messages to svn clients."""
106 106
107 107 def __init__(self, stderr=None):
108 108 # SVN needs data sent to stderr for back-to-client messaging
109 109 self.stderr = stderr or sys.stderr
110 110
111 111 def write(self, message):
112 112 self.stderr.write(message.encode('utf-8'))
113 113
114 114
115 115 def _handle_exception(result):
116 116 exception_class = result.get('exception')
117 117 exception_traceback = result.get('exception_traceback')
118 118
119 119 if exception_traceback:
120 120 log.error('Got traceback from remote call:%s', exception_traceback)
121 121
122 122 if exception_class == 'HTTPLockedRC':
123 123 raise exceptions.RepositoryLockedException(*result['exception_args'])
124 124 elif exception_class == 'RepositoryError':
125 125 raise exceptions.VcsException(*result['exception_args'])
126 126 elif exception_class:
127 127 raise Exception('Got remote exception "%s" with args "%s"' %
128 128 (exception_class, result['exception_args']))
129 129
130 130
131 131 def _get_hooks_client(extras):
132 132 if 'hooks_uri' in extras:
133 133 protocol = extras.get('hooks_protocol')
134 134 return HooksHttpClient(extras['hooks_uri'])
135 135 else:
136 136 return HooksDummyClient(extras['hooks_module'])
137 137
138 138
139 139 def _call_hook(hook_name, extras, writer):
140 140 hooks_client = _get_hooks_client(extras)
141 141 log.debug('Hooks, using client:%s', hooks_client)
142 142 result = hooks_client(hook_name, extras)
143 143 log.debug('Hooks got result: %s', result)
144 144 writer.write(result['output'])
145 145 _handle_exception(result)
146 146
147 147 return result['status']
148 148
149 149
150 150 def _extras_from_ui(ui):
151 151 hook_data = ui.config('rhodecode', 'RC_SCM_DATA')
152 152 if not hook_data:
153 153 # maybe it's inside environ ?
154 154 env_hook_data = os.environ.get('RC_SCM_DATA')
155 155 if env_hook_data:
156 156 hook_data = env_hook_data
157 157
158 158 extras = {}
159 159 if hook_data:
160 160 extras = json.loads(hook_data)
161 161 return extras
162 162
163 163
164 164 def _rev_range_hash(repo, node):
165 165
166 166 commits = []
167 167 for rev in xrange(repo[node], len(repo)):
168 168 ctx = repo[rev]
169 169 commit_id = mercurial.node.hex(ctx.node())
170 170 branch = ctx.branch()
171 171 commits.append((commit_id, branch))
172 172
173 173 return commits
174 174
175 175
176 176 def repo_size(ui, repo, **kwargs):
177 177 extras = _extras_from_ui(ui)
178 178 return _call_hook('repo_size', extras, HgMessageWriter(ui))
179 179
180 180
181 181 def pre_pull(ui, repo, **kwargs):
182 182 extras = _extras_from_ui(ui)
183 183 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
184 184
185 185
186 186 def pre_pull_ssh(ui, repo, **kwargs):
187 187 extras = _extras_from_ui(ui)
188 188 if extras and extras.get('SSH'):
189 189 return pre_pull(ui, repo, **kwargs)
190 190 return 0
191 191
192 192
193 193 def post_pull(ui, repo, **kwargs):
194 194 extras = _extras_from_ui(ui)
195 195 return _call_hook('post_pull', extras, HgMessageWriter(ui))
196 196
197 197
198 198 def post_pull_ssh(ui, repo, **kwargs):
199 199 extras = _extras_from_ui(ui)
200 200 if extras and extras.get('SSH'):
201 201 return post_pull(ui, repo, **kwargs)
202 202 return 0
203 203
204 204
205 205 def pre_push(ui, repo, node=None, **kwargs):
206 206 extras = _extras_from_ui(ui)
207 207
208 208 rev_data = []
209 209 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
210 210 branches = collections.defaultdict(list)
211 211 for commit_id, branch in _rev_range_hash(repo, node):
212 212 branches[branch].append(commit_id)
213 213
214 214 for branch, commits in branches.iteritems():
215 215 old_rev = kwargs.get('node_last') or commits[0]
216 216 rev_data.append({
217 217 'old_rev': old_rev,
218 218 'new_rev': commits[-1],
219 219 'ref': '',
220 220 'type': 'branch',
221 221 'name': branch,
222 222 })
223 223
224 224 extras['commit_ids'] = rev_data
225 225 return _call_hook('pre_push', extras, HgMessageWriter(ui))
226 226
227 227
228 228 def pre_push_ssh(ui, repo, node=None, **kwargs):
229 229 if _extras_from_ui(ui).get('SSH'):
230 230 return pre_push(ui, repo, node, **kwargs)
231 231
232 232 return 0
233 233
234 234
235 235 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
236 236 extras = _extras_from_ui(ui)
237 237 if extras.get('SSH'):
238 238 permission = extras['SSH_PERMISSIONS']
239 239
240 240 if 'repository.write' == permission or 'repository.admin' == permission:
241 241 return 0
242 242
243 243 # non-zero ret code
244 244 return 1
245 245
246 246 return 0
247 247
248 248
249 249 def post_push(ui, repo, node, **kwargs):
250 250 extras = _extras_from_ui(ui)
251 251
252 252 commit_ids = []
253 253 branches = []
254 254 bookmarks = []
255 255 tags = []
256 256
257 257 for commit_id, branch in _rev_range_hash(repo, node):
258 258 commit_ids.append(commit_id)
259 259 if branch not in branches:
260 260 branches.append(branch)
261 261
262 262 if hasattr(ui, '_rc_pushkey_branches'):
263 263 bookmarks = ui._rc_pushkey_branches
264 264
265 265 extras['commit_ids'] = commit_ids
266 266 extras['new_refs'] = {
267 267 'branches': branches,
268 268 'bookmarks': bookmarks,
269 269 'tags': tags
270 270 }
271 271
272 272 return _call_hook('post_push', extras, HgMessageWriter(ui))
273 273
274 274
275 275 def post_push_ssh(ui, repo, node, **kwargs):
276 276 if _extras_from_ui(ui).get('SSH'):
277 277 return post_push(ui, repo, node, **kwargs)
278 278 return 0
279 279
280 280
281 281 def key_push(ui, repo, **kwargs):
282 282 if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks':
283 283 # store new bookmarks in our UI object propagated later to post_push
284 284 ui._rc_pushkey_branches = repo[kwargs['key']].bookmarks()
285 285 return
286 286
287 287
288 288 # backward compat
289 289 log_pull_action = post_pull
290 290
291 291 # backward compat
292 292 log_push_action = post_push
293 293
294 294
295 295 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
296 296 """
297 297 Old hook name: keep here for backward compatibility.
298 298
299 299 This is only required when the installed git hooks are not upgraded.
300 300 """
301 301 pass
302 302
303 303
304 304 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
305 305 """
306 306 Old hook name: keep here for backward compatibility.
307 307
308 308 This is only required when the installed git hooks are not upgraded.
309 309 """
310 310 pass
311 311
312 312
313 313 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
314 314
315 315
316 316 def git_pre_pull(extras):
317 317 """
318 318 Pre pull hook.
319 319
320 320 :param extras: dictionary containing the keys defined in simplevcs
321 321 :type extras: dict
322 322
323 323 :return: status code of the hook. 0 for success.
324 324 :rtype: int
325 325 """
326 326 if 'pull' not in extras['hooks']:
327 327 return HookResponse(0, '')
328 328
329 329 stdout = io.BytesIO()
330 330 try:
331 331 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
332 332 except Exception as error:
333 333 status = 128
334 334 stdout.write('ERROR: %s\n' % str(error))
335 335
336 336 return HookResponse(status, stdout.getvalue())
337 337
338 338
339 339 def git_post_pull(extras):
340 340 """
341 341 Post pull hook.
342 342
343 343 :param extras: dictionary containing the keys defined in simplevcs
344 344 :type extras: dict
345 345
346 346 :return: status code of the hook. 0 for success.
347 347 :rtype: int
348 348 """
349 349 if 'pull' not in extras['hooks']:
350 350 return HookResponse(0, '')
351 351
352 352 stdout = io.BytesIO()
353 353 try:
354 354 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
355 355 except Exception as error:
356 356 status = 128
357 357 stdout.write('ERROR: %s\n' % error)
358 358
359 359 return HookResponse(status, stdout.getvalue())
360 360
361 361
362 362 def _parse_git_ref_lines(revision_lines):
363 363 rev_data = []
364 364 for revision_line in revision_lines or []:
365 365 old_rev, new_rev, ref = revision_line.strip().split(' ')
366 366 ref_data = ref.split('/', 2)
367 367 if ref_data[1] in ('tags', 'heads'):
368 368 rev_data.append({
369 369 'old_rev': old_rev,
370 370 'new_rev': new_rev,
371 371 'ref': ref,
372 372 'type': ref_data[1],
373 373 'name': ref_data[2],
374 374 })
375 375 return rev_data
376 376
377 377
378 378 def git_pre_receive(unused_repo_path, revision_lines, env):
379 379 """
380 380 Pre push hook.
381 381
382 382 :param extras: dictionary containing the keys defined in simplevcs
383 383 :type extras: dict
384 384
385 385 :return: status code of the hook. 0 for success.
386 386 :rtype: int
387 387 """
388 388 extras = json.loads(env['RC_SCM_DATA'])
389 389 rev_data = _parse_git_ref_lines(revision_lines)
390 390 if 'push' not in extras['hooks']:
391 391 return 0
392 392 extras['commit_ids'] = rev_data
393 393 return _call_hook('pre_push', extras, GitMessageWriter())
394 394
395 395
396 396 def git_post_receive(unused_repo_path, revision_lines, env):
397 397 """
398 398 Post push hook.
399 399
400 400 :param extras: dictionary containing the keys defined in simplevcs
401 401 :type extras: dict
402 402
403 403 :return: status code of the hook. 0 for success.
404 404 :rtype: int
405 405 """
406 406 extras = json.loads(env['RC_SCM_DATA'])
407 407 if 'push' not in extras['hooks']:
408 408 return 0
409 409
410 410 rev_data = _parse_git_ref_lines(revision_lines)
411 411
412 412 git_revs = []
413 413
414 414 # N.B.(skreft): it is ok to just call git, as git before calling a
415 415 # subcommand sets the PATH environment variable so that it point to the
416 416 # correct version of the git executable.
417 417 empty_commit_id = '0' * 40
418 418 branches = []
419 419 tags = []
420 420 for push_ref in rev_data:
421 421 type_ = push_ref['type']
422 422
423 423 if type_ == 'heads':
424 424 if push_ref['old_rev'] == empty_commit_id:
425 425 # starting new branch case
426 426 if push_ref['name'] not in branches:
427 427 branches.append(push_ref['name'])
428 428
429 429 # Fix up head revision if needed
430 430 cmd = [settings.GIT_EXECUTABLE, 'show', 'HEAD']
431 431 try:
432 432 subprocessio.run_command(cmd, env=os.environ.copy())
433 433 except Exception:
434 434 cmd = [settings.GIT_EXECUTABLE, 'symbolic-ref', 'HEAD',
435 435 'refs/heads/%s' % push_ref['name']]
436 436 print("Setting default branch to %s" % push_ref['name'])
437 437 subprocessio.run_command(cmd, env=os.environ.copy())
438 438
439 439 cmd = [settings.GIT_EXECUTABLE, 'for-each-ref',
440 440 '--format=%(refname)', 'refs/heads/*']
441 441 stdout, stderr = subprocessio.run_command(
442 442 cmd, env=os.environ.copy())
443 443 heads = stdout
444 444 heads = heads.replace(push_ref['ref'], '')
445 445 heads = ' '.join(head for head in heads.splitlines() if head)
446 446 cmd = [settings.GIT_EXECUTABLE, 'log', '--reverse',
447 447 '--pretty=format:%H', '--', push_ref['new_rev'],
448 448 '--not', heads]
449 449 stdout, stderr = subprocessio.run_command(
450 450 cmd, env=os.environ.copy())
451 451 git_revs.extend(stdout.splitlines())
452 452 elif push_ref['new_rev'] == empty_commit_id:
453 453 # delete branch case
454 454 git_revs.append('delete_branch=>%s' % push_ref['name'])
455 455 else:
456 456 if push_ref['name'] not in branches:
457 457 branches.append(push_ref['name'])
458 458
459 459 cmd = [settings.GIT_EXECUTABLE, 'log',
460 460 '{old_rev}..{new_rev}'.format(**push_ref),
461 461 '--reverse', '--pretty=format:%H']
462 462 stdout, stderr = subprocessio.run_command(
463 463 cmd, env=os.environ.copy())
464 464 git_revs.extend(stdout.splitlines())
465 465 elif type_ == 'tags':
466 466 if push_ref['name'] not in tags:
467 467 tags.append(push_ref['name'])
468 468 git_revs.append('tag=>%s' % push_ref['name'])
469 469
470 470 extras['commit_ids'] = git_revs
471 471 extras['new_refs'] = {
472 472 'branches': branches,
473 473 'bookmarks': [],
474 474 'tags': tags,
475 475 }
476 476
477 477 if 'repo_size' in extras['hooks']:
478 478 try:
479 479 _call_hook('repo_size', extras, GitMessageWriter())
480 480 except:
481 481 pass
482 482
483 483 return _call_hook('post_push', extras, GitMessageWriter())
484 484
485 485
486 def _get_extras_from_txn_id(path, txn_id):
487 extras = {}
488 try:
489 cmd = ['svnlook', 'pget',
490 '-t', txn_id,
491 '--revprop', path, 'rc-scm-extras']
492 stdout, stderr = subprocessio.run_command(
493 cmd, env=os.environ.copy())
494 extras = json.loads(base64.urlsafe_b64decode(stdout))
495 except Exception:
496 log.exception('Failed to extract extras info from txn_id')
497
498 return extras
499
500
486 501 def svn_pre_commit(repo_path, commit_data, env):
487 502 path, txn_id = commit_data
488 503 branches = []
489 504 tags = []
490 505
491 cmd = ['svnlook', 'pget',
492 '-t', txn_id,
493 '--revprop', path, 'rc-scm-extras']
494 stdout, stderr = subprocessio.run_command(
495 cmd, env=os.environ.copy())
496 extras = json.loads(base64.urlsafe_b64decode(stdout))
506 if env.get('RC_SCM_DATA'):
507 extras = json.loads(env['RC_SCM_DATA'])
508 else:
509 # fallback method to read from TXN-ID stored data
510 extras = _get_extras_from_txn_id(path, txn_id)
511 if not extras:
512 return 0
497 513
498 514 extras['commit_ids'] = []
499 515 extras['txn_id'] = txn_id
500 516 extras['new_refs'] = {
501 517 'branches': branches,
502 518 'bookmarks': [],
503 519 'tags': tags,
504 520 }
505 sys.stderr.write(str(extras))
521
506 522 return _call_hook('pre_push', extras, SvnMessageWriter())
507 523
508 524
525 def _get_extras_from_commit_id(commit_id, path):
526 extras = {}
527 try:
528 cmd = ['svnlook', 'pget',
529 '-r', commit_id,
530 '--revprop', path, 'rc-scm-extras']
531 stdout, stderr = subprocessio.run_command(
532 cmd, env=os.environ.copy())
533 extras = json.loads(base64.urlsafe_b64decode(stdout))
534 except Exception:
535 log.exception('Failed to extract extras info from commit_id')
536
537 return extras
538
539
509 540 def svn_post_commit(repo_path, commit_data, env):
510 541 """
511 542 commit_data is path, rev, txn_id
512 543 """
513 544 path, commit_id, txn_id = commit_data
514 545 branches = []
515 546 tags = []
516 547
517 cmd = ['svnlook', 'pget',
518 '-r', commit_id,
519 '--revprop', path, 'rc-scm-extras']
520 stdout, stderr = subprocessio.run_command(
521 cmd, env=os.environ.copy())
522
523 extras = json.loads(base64.urlsafe_b64decode(stdout))
548 if env.get('RC_SCM_DATA'):
549 extras = json.loads(env['RC_SCM_DATA'])
550 else:
551 # fallback method to read from TXN-ID stored data
552 extras = _get_extras_from_commit_id(commit_id, path)
553 if not extras:
554 return 0
524 555
525 556 extras['commit_ids'] = [commit_id]
526 557 extras['txn_id'] = txn_id
527 558 extras['new_refs'] = {
528 559 'branches': branches,
529 560 'bookmarks': [],
530 561 'tags': tags,
531 562 }
532 563
533 564 if 'repo_size' in extras['hooks']:
534 565 try:
535 566 _call_hook('repo_size', extras, SvnMessageWriter())
536 except:
567 except Exception:
537 568 pass
538 569
539 570 return _call_hook('post_push', extras, SvnMessageWriter())
540
541
General Comments 0
You need to be logged in to leave comments. Login now