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