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