##// END OF EJS Templates
hooks: show git stderr when git hook execution fails....
marcink -
r220:427ed58b default
parent child Browse files
Show More
@@ -1,390 +1,390 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(message):
72 def write(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 pre_push(ui, repo, node=None, **kwargs):
150 def pre_push(ui, repo, node=None, **kwargs):
151 extras = _extras_from_ui(ui)
151 extras = _extras_from_ui(ui)
152
152
153 rev_data = []
153 rev_data = []
154 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
154 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
155 branches = collections.defaultdict(list)
155 branches = collections.defaultdict(list)
156 for commit_id, branch in _rev_range_hash(repo, node, with_branch=True):
156 for commit_id, branch in _rev_range_hash(repo, node, with_branch=True):
157 branches[branch].append(commit_id)
157 branches[branch].append(commit_id)
158
158
159 for branch, commits in branches.iteritems():
159 for branch, commits in branches.iteritems():
160 old_rev = kwargs.get('node_last') or commits[0]
160 old_rev = kwargs.get('node_last') or commits[0]
161 rev_data.append({
161 rev_data.append({
162 'old_rev': old_rev,
162 'old_rev': old_rev,
163 'new_rev': commits[-1],
163 'new_rev': commits[-1],
164 'ref': '',
164 'ref': '',
165 'type': 'branch',
165 'type': 'branch',
166 'name': branch,
166 'name': branch,
167 })
167 })
168
168
169 extras['commit_ids'] = rev_data
169 extras['commit_ids'] = rev_data
170 return _call_hook('pre_push', extras, HgMessageWriter(ui))
170 return _call_hook('pre_push', extras, HgMessageWriter(ui))
171
171
172
172
173 def _rev_range_hash(repo, node, with_branch=False):
173 def _rev_range_hash(repo, node, with_branch=False):
174
174
175 commits = []
175 commits = []
176 for rev in xrange(repo[node], len(repo)):
176 for rev in xrange(repo[node], len(repo)):
177 ctx = repo[rev]
177 ctx = repo[rev]
178 commit_id = mercurial.node.hex(ctx.node())
178 commit_id = mercurial.node.hex(ctx.node())
179 branch = ctx.branch()
179 branch = ctx.branch()
180 if with_branch:
180 if with_branch:
181 commits.append((commit_id, branch))
181 commits.append((commit_id, branch))
182 else:
182 else:
183 commits.append(commit_id)
183 commits.append(commit_id)
184
184
185 return commits
185 return commits
186
186
187
187
188 def post_push(ui, repo, node, **kwargs):
188 def post_push(ui, repo, node, **kwargs):
189 commit_ids = _rev_range_hash(repo, node)
189 commit_ids = _rev_range_hash(repo, node)
190
190
191 extras = _extras_from_ui(ui)
191 extras = _extras_from_ui(ui)
192 extras['commit_ids'] = commit_ids
192 extras['commit_ids'] = commit_ids
193
193
194 return _call_hook('post_push', extras, HgMessageWriter(ui))
194 return _call_hook('post_push', extras, HgMessageWriter(ui))
195
195
196
196
197 # backward compat
197 # backward compat
198 log_pull_action = post_pull
198 log_pull_action = post_pull
199
199
200 # backward compat
200 # backward compat
201 log_push_action = post_push
201 log_push_action = post_push
202
202
203
203
204 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
204 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
205 """
205 """
206 Old hook name: keep here for backward compatibility.
206 Old hook name: keep here for backward compatibility.
207
207
208 This is only required when the installed git hooks are not upgraded.
208 This is only required when the installed git hooks are not upgraded.
209 """
209 """
210 pass
210 pass
211
211
212
212
213 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
213 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
214 """
214 """
215 Old hook name: keep here for backward compatibility.
215 Old hook name: keep here for backward compatibility.
216
216
217 This is only required when the installed git hooks are not upgraded.
217 This is only required when the installed git hooks are not upgraded.
218 """
218 """
219 pass
219 pass
220
220
221
221
222 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
222 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
223
223
224
224
225 def git_pre_pull(extras):
225 def git_pre_pull(extras):
226 """
226 """
227 Pre pull hook.
227 Pre pull hook.
228
228
229 :param extras: dictionary containing the keys defined in simplevcs
229 :param extras: dictionary containing the keys defined in simplevcs
230 :type extras: dict
230 :type extras: dict
231
231
232 :return: status code of the hook. 0 for success.
232 :return: status code of the hook. 0 for success.
233 :rtype: int
233 :rtype: int
234 """
234 """
235 if 'pull' not in extras['hooks']:
235 if 'pull' not in extras['hooks']:
236 return HookResponse(0, '')
236 return HookResponse(0, '')
237
237
238 stdout = io.BytesIO()
238 stdout = io.BytesIO()
239 try:
239 try:
240 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
240 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
241 except Exception as error:
241 except Exception as error:
242 status = 128
242 status = 128
243 stdout.write('ERROR: %s\n' % str(error))
243 stdout.write('ERROR: %s\n' % str(error))
244
244
245 return HookResponse(status, stdout.getvalue())
245 return HookResponse(status, stdout.getvalue())
246
246
247
247
248 def git_post_pull(extras):
248 def git_post_pull(extras):
249 """
249 """
250 Post pull hook.
250 Post pull hook.
251
251
252 :param extras: dictionary containing the keys defined in simplevcs
252 :param extras: dictionary containing the keys defined in simplevcs
253 :type extras: dict
253 :type extras: dict
254
254
255 :return: status code of the hook. 0 for success.
255 :return: status code of the hook. 0 for success.
256 :rtype: int
256 :rtype: int
257 """
257 """
258 if 'pull' not in extras['hooks']:
258 if 'pull' not in extras['hooks']:
259 return HookResponse(0, '')
259 return HookResponse(0, '')
260
260
261 stdout = io.BytesIO()
261 stdout = io.BytesIO()
262 try:
262 try:
263 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
263 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
264 except Exception as error:
264 except Exception as error:
265 status = 128
265 status = 128
266 stdout.write('ERROR: %s\n' % error)
266 stdout.write('ERROR: %s\n' % error)
267
267
268 return HookResponse(status, stdout.getvalue())
268 return HookResponse(status, stdout.getvalue())
269
269
270
270
271 def _parse_git_ref_lines(revision_lines):
271 def _parse_git_ref_lines(revision_lines):
272 rev_data = []
272 rev_data = []
273 for revision_line in revision_lines or []:
273 for revision_line in revision_lines or []:
274 old_rev, new_rev, ref = revision_line.strip().split(' ')
274 old_rev, new_rev, ref = revision_line.strip().split(' ')
275 ref_data = ref.split('/', 2)
275 ref_data = ref.split('/', 2)
276 if ref_data[1] in ('tags', 'heads'):
276 if ref_data[1] in ('tags', 'heads'):
277 rev_data.append({
277 rev_data.append({
278 'old_rev': old_rev,
278 'old_rev': old_rev,
279 'new_rev': new_rev,
279 'new_rev': new_rev,
280 'ref': ref,
280 'ref': ref,
281 'type': ref_data[1],
281 'type': ref_data[1],
282 'name': ref_data[2],
282 'name': ref_data[2],
283 })
283 })
284 return rev_data
284 return rev_data
285
285
286
286
287 def git_pre_receive(unused_repo_path, revision_lines, env):
287 def git_pre_receive(unused_repo_path, revision_lines, env):
288 """
288 """
289 Pre push hook.
289 Pre push hook.
290
290
291 :param extras: dictionary containing the keys defined in simplevcs
291 :param extras: dictionary containing the keys defined in simplevcs
292 :type extras: dict
292 :type extras: dict
293
293
294 :return: status code of the hook. 0 for success.
294 :return: status code of the hook. 0 for success.
295 :rtype: int
295 :rtype: int
296 """
296 """
297 extras = json.loads(env['RC_SCM_DATA'])
297 extras = json.loads(env['RC_SCM_DATA'])
298 rev_data = _parse_git_ref_lines(revision_lines)
298 rev_data = _parse_git_ref_lines(revision_lines)
299 if 'push' not in extras['hooks']:
299 if 'push' not in extras['hooks']:
300 return 0
300 return 0
301 extras['commit_ids'] = rev_data
301 extras['commit_ids'] = rev_data
302 return _call_hook('pre_push', extras, GitMessageWriter())
302 return _call_hook('pre_push', extras, GitMessageWriter())
303
303
304
304
305 def _run_command(arguments):
305 def _run_command(arguments):
306 """
306 """
307 Run the specified command and return the stdout.
307 Run the specified command and return the stdout.
308
308
309 :param arguments: sequence of program arguments (including the program name)
309 :param arguments: sequence of program arguments (including the program name)
310 :type arguments: list[str]
310 :type arguments: list[str]
311 """
311 """
312 # TODO(skreft): refactor this method and all the other similar ones.
312 # TODO(skreft): refactor this method and all the other similar ones.
313 # Probably this should be using subprocessio.
313 # Probably this should be using subprocessio.
314 process = subprocess.Popen(
314 process = subprocess.Popen(
315 arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
315 arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
316 stdout, _ = process.communicate()
316 stdout, stderr = process.communicate()
317
317
318 if process.returncode != 0:
318 if process.returncode != 0:
319 raise Exception(
319 raise Exception(
320 'Command %s exited with exit code %s' % (arguments,
320 'Command %s exited with exit code %s: stderr:%s' % (
321 process.returncode))
321 arguments, process.returncode, stderr))
322
322
323 return stdout
323 return stdout
324
324
325
325
326 def git_post_receive(unused_repo_path, revision_lines, env):
326 def git_post_receive(unused_repo_path, revision_lines, env):
327 """
327 """
328 Post push hook.
328 Post push hook.
329
329
330 :param extras: dictionary containing the keys defined in simplevcs
330 :param extras: dictionary containing the keys defined in simplevcs
331 :type extras: dict
331 :type extras: dict
332
332
333 :return: status code of the hook. 0 for success.
333 :return: status code of the hook. 0 for success.
334 :rtype: int
334 :rtype: int
335 """
335 """
336 extras = json.loads(env['RC_SCM_DATA'])
336 extras = json.loads(env['RC_SCM_DATA'])
337 if 'push' not in extras['hooks']:
337 if 'push' not in extras['hooks']:
338 return 0
338 return 0
339
339
340 rev_data = _parse_git_ref_lines(revision_lines)
340 rev_data = _parse_git_ref_lines(revision_lines)
341
341
342 git_revs = []
342 git_revs = []
343
343
344 # N.B.(skreft): it is ok to just call git, as git before calling a
344 # N.B.(skreft): it is ok to just call git, as git before calling a
345 # subcommand sets the PATH environment variable so that it point to the
345 # subcommand sets the PATH environment variable so that it point to the
346 # correct version of the git executable.
346 # correct version of the git executable.
347 empty_commit_id = '0' * 40
347 empty_commit_id = '0' * 40
348 for push_ref in rev_data:
348 for push_ref in rev_data:
349 type_ = push_ref['type']
349 type_ = push_ref['type']
350 if type_ == 'heads':
350 if type_ == 'heads':
351 if push_ref['old_rev'] == empty_commit_id:
351 if push_ref['old_rev'] == empty_commit_id:
352
352
353 # Fix up head revision if needed
353 # Fix up head revision if needed
354 cmd = ['git', 'show', 'HEAD']
354 cmd = ['git', 'show', 'HEAD']
355 try:
355 try:
356 _run_command(cmd)
356 _run_command(cmd)
357 except Exception:
357 except Exception:
358 cmd = ['git', 'symbolic-ref', 'HEAD',
358 cmd = ['git', 'symbolic-ref', 'HEAD',
359 'refs/heads/%s' % push_ref['name']]
359 'refs/heads/%s' % push_ref['name']]
360 print("Setting default branch to %s" % push_ref['name'])
360 print("Setting default branch to %s" % push_ref['name'])
361 _run_command(cmd)
361 _run_command(cmd)
362
362
363 cmd = ['git', 'for-each-ref', '--format=%(refname)',
363 cmd = ['git', 'for-each-ref', '--format=%(refname)',
364 'refs/heads/*']
364 'refs/heads/*']
365 heads = _run_command(cmd)
365 heads = _run_command(cmd)
366 heads = heads.replace(push_ref['ref'], '')
366 heads = heads.replace(push_ref['ref'], '')
367 heads = ' '.join(head for head in heads.splitlines() if head)
367 heads = ' '.join(head for head in heads.splitlines() if head)
368 cmd = ['git', 'log', '--reverse', '--pretty=format:%H',
368 cmd = ['git', 'log', '--reverse', '--pretty=format:%H',
369 '--', push_ref['new_rev'], '--not', heads]
369 '--', push_ref['new_rev'], '--not', heads]
370 git_revs.extend(_run_command(cmd).splitlines())
370 git_revs.extend(_run_command(cmd).splitlines())
371 elif push_ref['new_rev'] == empty_commit_id:
371 elif push_ref['new_rev'] == empty_commit_id:
372 # delete branch case
372 # delete branch case
373 git_revs.append('delete_branch=>%s' % push_ref['name'])
373 git_revs.append('delete_branch=>%s' % push_ref['name'])
374 else:
374 else:
375 cmd = ['git', 'log',
375 cmd = ['git', 'log',
376 '{old_rev}..{new_rev}'.format(**push_ref),
376 '{old_rev}..{new_rev}'.format(**push_ref),
377 '--reverse', '--pretty=format:%H']
377 '--reverse', '--pretty=format:%H']
378 git_revs.extend(_run_command(cmd).splitlines())
378 git_revs.extend(_run_command(cmd).splitlines())
379 elif type_ == 'tags':
379 elif type_ == 'tags':
380 git_revs.append('tag=>%s' % push_ref['name'])
380 git_revs.append('tag=>%s' % push_ref['name'])
381
381
382 extras['commit_ids'] = git_revs
382 extras['commit_ids'] = git_revs
383
383
384 if 'repo_size' in extras['hooks']:
384 if 'repo_size' in extras['hooks']:
385 try:
385 try:
386 _call_hook('repo_size', extras, GitMessageWriter())
386 _call_hook('repo_size', extras, GitMessageWriter())
387 except:
387 except:
388 pass
388 pass
389
389
390 return _call_hook('post_push', extras, GitMessageWriter())
390 return _call_hook('post_push', extras, GitMessageWriter())
General Comments 0
You need to be logged in to leave comments. Login now