##// 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 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 sys
22 22 import json
23 23 import logging
24 24 import collections
25 25 import importlib
26 26 import subprocess
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
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 connection.request('POST', '/', body)
50 50 response = connection.getresponse()
51 51 return json.loads(response.read())
52 52
53 53 def _serialize(self, hook_name, extras):
54 54 data = {
55 55 'method': hook_name,
56 56 'extras': extras
57 57 }
58 58 return json.dumps(data)
59 59
60 60
61 61 class HooksDummyClient(object):
62 62 def __init__(self, hooks_module):
63 63 self._hooks_module = importlib.import_module(hooks_module)
64 64
65 65 def __call__(self, hook_name, extras):
66 66 with self._hooks_module.Hooks() as hooks:
67 67 return getattr(hooks, hook_name)(extras)
68 68
69 69
70 70 class RemoteMessageWriter(object):
71 71 """Writer base class."""
72 72 def write(message):
73 73 raise NotImplementedError()
74 74
75 75
76 76 class HgMessageWriter(RemoteMessageWriter):
77 77 """Writer that knows how to send messages to mercurial clients."""
78 78
79 79 def __init__(self, ui):
80 80 self.ui = ui
81 81
82 82 def write(self, message):
83 83 # TODO: Check why the quiet flag is set by default.
84 84 old = self.ui.quiet
85 85 self.ui.quiet = False
86 86 self.ui.status(message.encode('utf-8'))
87 87 self.ui.quiet = old
88 88
89 89
90 90 class GitMessageWriter(RemoteMessageWriter):
91 91 """Writer that knows how to send messages to git clients."""
92 92
93 93 def __init__(self, stdout=None):
94 94 self.stdout = stdout or sys.stdout
95 95
96 96 def write(self, message):
97 97 self.stdout.write(message.encode('utf-8'))
98 98
99 99
100 100 def _handle_exception(result):
101 101 exception_class = result.get('exception')
102 102 exception_traceback = result.get('exception_traceback')
103 103
104 104 if exception_traceback:
105 105 log.error('Got traceback from remote call:%s', exception_traceback)
106 106
107 107 if exception_class == 'HTTPLockedRC':
108 108 raise exceptions.RepositoryLockedException(*result['exception_args'])
109 109 elif exception_class == 'RepositoryError':
110 110 raise exceptions.VcsException(*result['exception_args'])
111 111 elif exception_class:
112 112 raise Exception('Got remote exception "%s" with args "%s"' %
113 113 (exception_class, result['exception_args']))
114 114
115 115
116 116 def _get_hooks_client(extras):
117 117 if 'hooks_uri' in extras:
118 118 protocol = extras.get('hooks_protocol')
119 119 return HooksHttpClient(extras['hooks_uri'])
120 120 else:
121 121 return HooksDummyClient(extras['hooks_module'])
122 122
123 123
124 124 def _call_hook(hook_name, extras, writer):
125 125 hooks = _get_hooks_client(extras)
126 126 result = hooks(hook_name, extras)
127 127 writer.write(result['output'])
128 128 _handle_exception(result)
129 129
130 130 return result['status']
131 131
132 132
133 133 def _extras_from_ui(ui):
134 134 extras = json.loads(ui.config('rhodecode', 'RC_SCM_DATA'))
135 135 return extras
136 136
137 137
138 138 def repo_size(ui, repo, **kwargs):
139 139 return _call_hook('repo_size', _extras_from_ui(ui), HgMessageWriter(ui))
140 140
141 141
142 142 def pre_pull(ui, repo, **kwargs):
143 143 return _call_hook('pre_pull', _extras_from_ui(ui), HgMessageWriter(ui))
144 144
145 145
146 146 def post_pull(ui, repo, **kwargs):
147 147 return _call_hook('post_pull', _extras_from_ui(ui), HgMessageWriter(ui))
148 148
149 149
150 150 def pre_push(ui, repo, node=None, **kwargs):
151 151 extras = _extras_from_ui(ui)
152 152
153 153 rev_data = []
154 154 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
155 155 branches = collections.defaultdict(list)
156 156 for commit_id, branch in _rev_range_hash(repo, node, with_branch=True):
157 157 branches[branch].append(commit_id)
158 158
159 159 for branch, commits in branches.iteritems():
160 160 old_rev = kwargs.get('node_last') or commits[0]
161 161 rev_data.append({
162 162 'old_rev': old_rev,
163 163 'new_rev': commits[-1],
164 164 'ref': '',
165 165 'type': 'branch',
166 166 'name': branch,
167 167 })
168 168
169 169 extras['commit_ids'] = rev_data
170 170 return _call_hook('pre_push', extras, HgMessageWriter(ui))
171 171
172 172
173 173 def _rev_range_hash(repo, node, with_branch=False):
174 174
175 175 commits = []
176 176 for rev in xrange(repo[node], len(repo)):
177 177 ctx = repo[rev]
178 178 commit_id = mercurial.node.hex(ctx.node())
179 179 branch = ctx.branch()
180 180 if with_branch:
181 181 commits.append((commit_id, branch))
182 182 else:
183 183 commits.append(commit_id)
184 184
185 185 return commits
186 186
187 187
188 188 def post_push(ui, repo, node, **kwargs):
189 189 commit_ids = _rev_range_hash(repo, node)
190 190
191 191 extras = _extras_from_ui(ui)
192 192 extras['commit_ids'] = commit_ids
193 193
194 194 return _call_hook('post_push', extras, HgMessageWriter(ui))
195 195
196 196
197 197 # backward compat
198 198 log_pull_action = post_pull
199 199
200 200 # backward compat
201 201 log_push_action = post_push
202 202
203 203
204 204 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
205 205 """
206 206 Old hook name: keep here for backward compatibility.
207 207
208 208 This is only required when the installed git hooks are not upgraded.
209 209 """
210 210 pass
211 211
212 212
213 213 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
214 214 """
215 215 Old hook name: keep here for backward compatibility.
216 216
217 217 This is only required when the installed git hooks are not upgraded.
218 218 """
219 219 pass
220 220
221 221
222 222 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
223 223
224 224
225 225 def git_pre_pull(extras):
226 226 """
227 227 Pre pull hook.
228 228
229 229 :param extras: dictionary containing the keys defined in simplevcs
230 230 :type extras: dict
231 231
232 232 :return: status code of the hook. 0 for success.
233 233 :rtype: int
234 234 """
235 235 if 'pull' not in extras['hooks']:
236 236 return HookResponse(0, '')
237 237
238 238 stdout = io.BytesIO()
239 239 try:
240 240 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
241 241 except Exception as error:
242 242 status = 128
243 243 stdout.write('ERROR: %s\n' % str(error))
244 244
245 245 return HookResponse(status, stdout.getvalue())
246 246
247 247
248 248 def git_post_pull(extras):
249 249 """
250 250 Post pull hook.
251 251
252 252 :param extras: dictionary containing the keys defined in simplevcs
253 253 :type extras: dict
254 254
255 255 :return: status code of the hook. 0 for success.
256 256 :rtype: int
257 257 """
258 258 if 'pull' not in extras['hooks']:
259 259 return HookResponse(0, '')
260 260
261 261 stdout = io.BytesIO()
262 262 try:
263 263 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
264 264 except Exception as error:
265 265 status = 128
266 266 stdout.write('ERROR: %s\n' % error)
267 267
268 268 return HookResponse(status, stdout.getvalue())
269 269
270 270
271 271 def _parse_git_ref_lines(revision_lines):
272 272 rev_data = []
273 273 for revision_line in revision_lines or []:
274 274 old_rev, new_rev, ref = revision_line.strip().split(' ')
275 275 ref_data = ref.split('/', 2)
276 276 if ref_data[1] in ('tags', 'heads'):
277 277 rev_data.append({
278 278 'old_rev': old_rev,
279 279 'new_rev': new_rev,
280 280 'ref': ref,
281 281 'type': ref_data[1],
282 282 'name': ref_data[2],
283 283 })
284 284 return rev_data
285 285
286 286
287 287 def git_pre_receive(unused_repo_path, revision_lines, env):
288 288 """
289 289 Pre push hook.
290 290
291 291 :param extras: dictionary containing the keys defined in simplevcs
292 292 :type extras: dict
293 293
294 294 :return: status code of the hook. 0 for success.
295 295 :rtype: int
296 296 """
297 297 extras = json.loads(env['RC_SCM_DATA'])
298 298 rev_data = _parse_git_ref_lines(revision_lines)
299 299 if 'push' not in extras['hooks']:
300 300 return 0
301 301 extras['commit_ids'] = rev_data
302 302 return _call_hook('pre_push', extras, GitMessageWriter())
303 303
304 304
305 305 def _run_command(arguments):
306 306 """
307 307 Run the specified command and return the stdout.
308 308
309 309 :param arguments: sequence of program arguments (including the program name)
310 310 :type arguments: list[str]
311 311 """
312 312 # TODO(skreft): refactor this method and all the other similar ones.
313 313 # Probably this should be using subprocessio.
314 314 process = subprocess.Popen(
315 315 arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
316 stdout, _ = process.communicate()
316 stdout, stderr = process.communicate()
317 317
318 318 if process.returncode != 0:
319 319 raise Exception(
320 'Command %s exited with exit code %s' % (arguments,
321 process.returncode))
320 'Command %s exited with exit code %s: stderr:%s' % (
321 arguments, process.returncode, stderr))
322 322
323 323 return stdout
324 324
325 325
326 326 def git_post_receive(unused_repo_path, revision_lines, env):
327 327 """
328 328 Post push hook.
329 329
330 330 :param extras: dictionary containing the keys defined in simplevcs
331 331 :type extras: dict
332 332
333 333 :return: status code of the hook. 0 for success.
334 334 :rtype: int
335 335 """
336 336 extras = json.loads(env['RC_SCM_DATA'])
337 337 if 'push' not in extras['hooks']:
338 338 return 0
339 339
340 340 rev_data = _parse_git_ref_lines(revision_lines)
341 341
342 342 git_revs = []
343 343
344 344 # N.B.(skreft): it is ok to just call git, as git before calling a
345 345 # subcommand sets the PATH environment variable so that it point to the
346 346 # correct version of the git executable.
347 347 empty_commit_id = '0' * 40
348 348 for push_ref in rev_data:
349 349 type_ = push_ref['type']
350 350 if type_ == 'heads':
351 351 if push_ref['old_rev'] == empty_commit_id:
352 352
353 353 # Fix up head revision if needed
354 354 cmd = ['git', 'show', 'HEAD']
355 355 try:
356 356 _run_command(cmd)
357 357 except Exception:
358 358 cmd = ['git', 'symbolic-ref', 'HEAD',
359 359 'refs/heads/%s' % push_ref['name']]
360 360 print("Setting default branch to %s" % push_ref['name'])
361 361 _run_command(cmd)
362 362
363 363 cmd = ['git', 'for-each-ref', '--format=%(refname)',
364 364 'refs/heads/*']
365 365 heads = _run_command(cmd)
366 366 heads = heads.replace(push_ref['ref'], '')
367 367 heads = ' '.join(head for head in heads.splitlines() if head)
368 368 cmd = ['git', 'log', '--reverse', '--pretty=format:%H',
369 369 '--', push_ref['new_rev'], '--not', heads]
370 370 git_revs.extend(_run_command(cmd).splitlines())
371 371 elif push_ref['new_rev'] == empty_commit_id:
372 372 # delete branch case
373 373 git_revs.append('delete_branch=>%s' % push_ref['name'])
374 374 else:
375 375 cmd = ['git', 'log',
376 376 '{old_rev}..{new_rev}'.format(**push_ref),
377 377 '--reverse', '--pretty=format:%H']
378 378 git_revs.extend(_run_command(cmd).splitlines())
379 379 elif type_ == 'tags':
380 380 git_revs.append('tag=>%s' % push_ref['name'])
381 381
382 382 extras['commit_ids'] = git_revs
383 383
384 384 if 'repo_size' in extras['hooks']:
385 385 try:
386 386 _call_hook('repo_size', extras, GitMessageWriter())
387 387 except:
388 388 pass
389 389
390 390 return _call_hook('post_push', extras, GitMessageWriter())
General Comments 0
You need to be logged in to leave comments. Login now