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