##// END OF EJS Templates
hooks: use safer method of exporting the RC_SCM_DATA, prevents JSON decode errors in case of None value
marcink -
r345:a3726eeb default
parent child Browse files
Show More
@@ -1,475 +1,478 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 hook_data = os.environ.get('RC_SCM_DATA')
139 env_hook_data = os.environ.get('RC_SCM_DATA')
140 if env_hook_data:
141 hook_data = env_hook_data
142
140 143 extras = json.loads(hook_data)
141 144 return extras
142 145
143 146
144 147 def _rev_range_hash(repo, node):
145 148
146 149 commits = []
147 150 for rev in xrange(repo[node], len(repo)):
148 151 ctx = repo[rev]
149 152 commit_id = mercurial.node.hex(ctx.node())
150 153 branch = ctx.branch()
151 154 commits.append((commit_id, branch))
152 155
153 156 return commits
154 157
155 158
156 159 def repo_size(ui, repo, **kwargs):
157 160 extras = _extras_from_ui(ui)
158 161 return _call_hook('repo_size', extras, HgMessageWriter(ui))
159 162
160 163
161 164 def pre_pull(ui, repo, **kwargs):
162 165 extras = _extras_from_ui(ui)
163 166 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
164 167
165 168
166 169 def pre_pull_ssh(ui, repo, **kwargs):
167 170 if _extras_from_ui(ui).get('SSH'):
168 171 return pre_pull(ui, repo, **kwargs)
169 172 return 0
170 173
171 174
172 175 def post_pull(ui, repo, **kwargs):
173 176 extras = _extras_from_ui(ui)
174 177 return _call_hook('post_pull', extras, HgMessageWriter(ui))
175 178
176 179
177 180 def post_pull_ssh(ui, repo, **kwargs):
178 181 if _extras_from_ui(ui).get('SSH'):
179 182 return post_pull(ui, repo, **kwargs)
180 183 return 0
181 184
182 185
183 186 def pre_push(ui, repo, node=None, **kwargs):
184 187 extras = _extras_from_ui(ui)
185 188
186 189 rev_data = []
187 190 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
188 191 branches = collections.defaultdict(list)
189 192 for commit_id, branch in _rev_range_hash(repo, node):
190 193 branches[branch].append(commit_id)
191 194
192 195 for branch, commits in branches.iteritems():
193 196 old_rev = kwargs.get('node_last') or commits[0]
194 197 rev_data.append({
195 198 'old_rev': old_rev,
196 199 'new_rev': commits[-1],
197 200 'ref': '',
198 201 'type': 'branch',
199 202 'name': branch,
200 203 })
201 204
202 205 extras['commit_ids'] = rev_data
203 206 return _call_hook('pre_push', extras, HgMessageWriter(ui))
204 207
205 208
206 209 def pre_push_ssh(ui, repo, node=None, **kwargs):
207 210 if _extras_from_ui(ui).get('SSH'):
208 211 return pre_push(ui, repo, node, **kwargs)
209 212
210 213 return 0
211 214
212 215
213 216 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
214 217 extras = _extras_from_ui(ui)
215 218 if extras.get('SSH'):
216 219 permission = extras['SSH_PERMISSIONS']
217 220
218 221 if 'repository.write' == permission or 'repository.admin' == permission:
219 222 return 0
220 223
221 224 # non-zero ret code
222 225 return 1
223 226
224 227 return 0
225 228
226 229
227 230 def post_push(ui, repo, node, **kwargs):
228 231 extras = _extras_from_ui(ui)
229 232
230 233 commit_ids = []
231 234 branches = []
232 235 bookmarks = []
233 236 tags = []
234 237
235 238 for commit_id, branch in _rev_range_hash(repo, node):
236 239 commit_ids.append(commit_id)
237 240 if branch not in branches:
238 241 branches.append(branch)
239 242
240 243 if hasattr(ui, '_rc_pushkey_branches'):
241 244 bookmarks = ui._rc_pushkey_branches
242 245
243 246 extras['commit_ids'] = commit_ids
244 247 extras['new_refs'] = {
245 248 'branches': branches,
246 249 'bookmarks': bookmarks,
247 250 'tags': tags
248 251 }
249 252
250 253 return _call_hook('post_push', extras, HgMessageWriter(ui))
251 254
252 255
253 256 def post_push_ssh(ui, repo, node, **kwargs):
254 257 if _extras_from_ui(ui).get('SSH'):
255 258 return post_push(ui, repo, node, **kwargs)
256 259 return 0
257 260
258 261
259 262 def key_push(ui, repo, **kwargs):
260 263 if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks':
261 264 # store new bookmarks in our UI object propagated later to post_push
262 265 ui._rc_pushkey_branches = repo[kwargs['key']].bookmarks()
263 266 return
264 267
265 268
266 269 # backward compat
267 270 log_pull_action = post_pull
268 271
269 272 # backward compat
270 273 log_push_action = post_push
271 274
272 275
273 276 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
274 277 """
275 278 Old hook name: keep here for backward compatibility.
276 279
277 280 This is only required when the installed git hooks are not upgraded.
278 281 """
279 282 pass
280 283
281 284
282 285 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
283 286 """
284 287 Old hook name: keep here for backward compatibility.
285 288
286 289 This is only required when the installed git hooks are not upgraded.
287 290 """
288 291 pass
289 292
290 293
291 294 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
292 295
293 296
294 297 def git_pre_pull(extras):
295 298 """
296 299 Pre pull hook.
297 300
298 301 :param extras: dictionary containing the keys defined in simplevcs
299 302 :type extras: dict
300 303
301 304 :return: status code of the hook. 0 for success.
302 305 :rtype: int
303 306 """
304 307 if 'pull' not in extras['hooks']:
305 308 return HookResponse(0, '')
306 309
307 310 stdout = io.BytesIO()
308 311 try:
309 312 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
310 313 except Exception as error:
311 314 status = 128
312 315 stdout.write('ERROR: %s\n' % str(error))
313 316
314 317 return HookResponse(status, stdout.getvalue())
315 318
316 319
317 320 def git_post_pull(extras):
318 321 """
319 322 Post pull hook.
320 323
321 324 :param extras: dictionary containing the keys defined in simplevcs
322 325 :type extras: dict
323 326
324 327 :return: status code of the hook. 0 for success.
325 328 :rtype: int
326 329 """
327 330 if 'pull' not in extras['hooks']:
328 331 return HookResponse(0, '')
329 332
330 333 stdout = io.BytesIO()
331 334 try:
332 335 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
333 336 except Exception as error:
334 337 status = 128
335 338 stdout.write('ERROR: %s\n' % error)
336 339
337 340 return HookResponse(status, stdout.getvalue())
338 341
339 342
340 343 def _parse_git_ref_lines(revision_lines):
341 344 rev_data = []
342 345 for revision_line in revision_lines or []:
343 346 old_rev, new_rev, ref = revision_line.strip().split(' ')
344 347 ref_data = ref.split('/', 2)
345 348 if ref_data[1] in ('tags', 'heads'):
346 349 rev_data.append({
347 350 'old_rev': old_rev,
348 351 'new_rev': new_rev,
349 352 'ref': ref,
350 353 'type': ref_data[1],
351 354 'name': ref_data[2],
352 355 })
353 356 return rev_data
354 357
355 358
356 359 def git_pre_receive(unused_repo_path, revision_lines, env):
357 360 """
358 361 Pre push hook.
359 362
360 363 :param extras: dictionary containing the keys defined in simplevcs
361 364 :type extras: dict
362 365
363 366 :return: status code of the hook. 0 for success.
364 367 :rtype: int
365 368 """
366 369 extras = json.loads(env['RC_SCM_DATA'])
367 370 rev_data = _parse_git_ref_lines(revision_lines)
368 371 if 'push' not in extras['hooks']:
369 372 return 0
370 373 extras['commit_ids'] = rev_data
371 374 return _call_hook('pre_push', extras, GitMessageWriter())
372 375
373 376
374 377 def _run_command(arguments):
375 378 """
376 379 Run the specified command and return the stdout.
377 380
378 381 :param arguments: sequence of program arguments (including the program name)
379 382 :type arguments: list[str]
380 383 """
381 384 # TODO(skreft): refactor this method and all the other similar ones.
382 385 # Probably this should be using subprocessio.
383 386 process = subprocess.Popen(
384 387 arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
385 388 stdout, stderr = process.communicate()
386 389
387 390 if process.returncode != 0:
388 391 raise Exception(
389 392 'Command %s exited with exit code %s: stderr:%s' % (
390 393 arguments, process.returncode, stderr))
391 394
392 395 return stdout
393 396
394 397
395 398 def git_post_receive(unused_repo_path, revision_lines, env):
396 399 """
397 400 Post push hook.
398 401
399 402 :param extras: dictionary containing the keys defined in simplevcs
400 403 :type extras: dict
401 404
402 405 :return: status code of the hook. 0 for success.
403 406 :rtype: int
404 407 """
405 408 extras = json.loads(env['RC_SCM_DATA'])
406 409 if 'push' not in extras['hooks']:
407 410 return 0
408 411
409 412 rev_data = _parse_git_ref_lines(revision_lines)
410 413
411 414 git_revs = []
412 415
413 416 # N.B.(skreft): it is ok to just call git, as git before calling a
414 417 # subcommand sets the PATH environment variable so that it point to the
415 418 # correct version of the git executable.
416 419 empty_commit_id = '0' * 40
417 420 branches = []
418 421 tags = []
419 422 for push_ref in rev_data:
420 423 type_ = push_ref['type']
421 424
422 425 if type_ == 'heads':
423 426 if push_ref['old_rev'] == empty_commit_id:
424 427 # starting new branch case
425 428 if push_ref['name'] not in branches:
426 429 branches.append(push_ref['name'])
427 430
428 431 # Fix up head revision if needed
429 432 cmd = ['git', 'show', 'HEAD']
430 433 try:
431 434 _run_command(cmd)
432 435 except Exception:
433 436 cmd = ['git', 'symbolic-ref', 'HEAD',
434 437 'refs/heads/%s' % push_ref['name']]
435 438 print("Setting default branch to %s" % push_ref['name'])
436 439 _run_command(cmd)
437 440
438 441 cmd = ['git', 'for-each-ref', '--format=%(refname)',
439 442 'refs/heads/*']
440 443 heads = _run_command(cmd)
441 444 heads = heads.replace(push_ref['ref'], '')
442 445 heads = ' '.join(head for head in heads.splitlines() if head)
443 446 cmd = ['git', 'log', '--reverse', '--pretty=format:%H',
444 447 '--', push_ref['new_rev'], '--not', heads]
445 448 git_revs.extend(_run_command(cmd).splitlines())
446 449 elif push_ref['new_rev'] == empty_commit_id:
447 450 # delete branch case
448 451 git_revs.append('delete_branch=>%s' % push_ref['name'])
449 452 else:
450 453 if push_ref['name'] not in branches:
451 454 branches.append(push_ref['name'])
452 455
453 456 cmd = ['git', 'log',
454 457 '{old_rev}..{new_rev}'.format(**push_ref),
455 458 '--reverse', '--pretty=format:%H']
456 459 git_revs.extend(_run_command(cmd).splitlines())
457 460 elif type_ == 'tags':
458 461 if push_ref['name'] not in tags:
459 462 tags.append(push_ref['name'])
460 463 git_revs.append('tag=>%s' % push_ref['name'])
461 464
462 465 extras['commit_ids'] = git_revs
463 466 extras['new_refs'] = {
464 467 'branches': branches,
465 468 'bookmarks': [],
466 469 'tags': tags,
467 470 }
468 471
469 472 if 'repo_size' in extras['hooks']:
470 473 try:
471 474 _call_hook('repo_size', extras, GitMessageWriter())
472 475 except:
473 476 pass
474 477
475 478 return _call_hook('post_push', extras, GitMessageWriter())
General Comments 0
You need to be logged in to leave comments. Login now