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