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