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