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