##// END OF EJS Templates
hooks: handle delete branch permission case with branch protection
dan -
r643:d42f7af5 default
parent child Browse files
Show More
@@ -1,711 +1,710 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-2019 RhodeCode GmbH
4 # Copyright (C) 2014-2019 RhodeCode 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 os
21 import os
22 import sys
22 import sys
23 import logging
23 import logging
24 import collections
24 import collections
25 import importlib
25 import importlib
26 import base64
26 import base64
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, subprocessio, settings
35 from vcsserver import exceptions, subprocessio, settings
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 try:
49 try:
50 connection.request('POST', '/', body)
50 connection.request('POST', '/', body)
51 except Exception:
51 except Exception:
52 log.error('Connection failed on %s', connection)
52 log.error('Connection failed on %s', connection)
53 raise
53 raise
54 response = connection.getresponse()
54 response = connection.getresponse()
55
55
56 response_data = response.read()
56 response_data = response.read()
57
57
58 try:
58 try:
59 return json.loads(response_data)
59 return json.loads(response_data)
60 except Exception:
60 except Exception:
61 log.exception('Failed to decode hook response json data. '
61 log.exception('Failed to decode hook response json data. '
62 'response_code:%s, raw_data:%s',
62 'response_code:%s, raw_data:%s',
63 response.status, response_data)
63 response.status, response_data)
64 raise
64 raise
65
65
66 def _serialize(self, hook_name, extras):
66 def _serialize(self, hook_name, extras):
67 data = {
67 data = {
68 'method': hook_name,
68 'method': hook_name,
69 'extras': extras
69 'extras': extras
70 }
70 }
71 return json.dumps(data)
71 return json.dumps(data)
72
72
73
73
74 class HooksDummyClient(object):
74 class HooksDummyClient(object):
75 def __init__(self, hooks_module):
75 def __init__(self, hooks_module):
76 self._hooks_module = importlib.import_module(hooks_module)
76 self._hooks_module = importlib.import_module(hooks_module)
77
77
78 def __call__(self, hook_name, extras):
78 def __call__(self, hook_name, extras):
79 with self._hooks_module.Hooks() as hooks:
79 with self._hooks_module.Hooks() as hooks:
80 return getattr(hooks, hook_name)(extras)
80 return getattr(hooks, hook_name)(extras)
81
81
82
82
83 class RemoteMessageWriter(object):
83 class RemoteMessageWriter(object):
84 """Writer base class."""
84 """Writer base class."""
85 def write(self, message):
85 def write(self, message):
86 raise NotImplementedError()
86 raise NotImplementedError()
87
87
88
88
89 class HgMessageWriter(RemoteMessageWriter):
89 class HgMessageWriter(RemoteMessageWriter):
90 """Writer that knows how to send messages to mercurial clients."""
90 """Writer that knows how to send messages to mercurial clients."""
91
91
92 def __init__(self, ui):
92 def __init__(self, ui):
93 self.ui = ui
93 self.ui = ui
94
94
95 def write(self, message):
95 def write(self, message):
96 # TODO: Check why the quiet flag is set by default.
96 # TODO: Check why the quiet flag is set by default.
97 old = self.ui.quiet
97 old = self.ui.quiet
98 self.ui.quiet = False
98 self.ui.quiet = False
99 self.ui.status(message.encode('utf-8'))
99 self.ui.status(message.encode('utf-8'))
100 self.ui.quiet = old
100 self.ui.quiet = old
101
101
102
102
103 class GitMessageWriter(RemoteMessageWriter):
103 class GitMessageWriter(RemoteMessageWriter):
104 """Writer that knows how to send messages to git clients."""
104 """Writer that knows how to send messages to git clients."""
105
105
106 def __init__(self, stdout=None):
106 def __init__(self, stdout=None):
107 self.stdout = stdout or sys.stdout
107 self.stdout = stdout or sys.stdout
108
108
109 def write(self, message):
109 def write(self, message):
110 self.stdout.write(message.encode('utf-8'))
110 self.stdout.write(message.encode('utf-8'))
111
111
112
112
113 class SvnMessageWriter(RemoteMessageWriter):
113 class SvnMessageWriter(RemoteMessageWriter):
114 """Writer that knows how to send messages to svn clients."""
114 """Writer that knows how to send messages to svn clients."""
115
115
116 def __init__(self, stderr=None):
116 def __init__(self, stderr=None):
117 # SVN needs data sent to stderr for back-to-client messaging
117 # SVN needs data sent to stderr for back-to-client messaging
118 self.stderr = stderr or sys.stderr
118 self.stderr = stderr or sys.stderr
119
119
120 def write(self, message):
120 def write(self, message):
121 self.stderr.write(message.encode('utf-8'))
121 self.stderr.write(message.encode('utf-8'))
122
122
123
123
124 def _handle_exception(result):
124 def _handle_exception(result):
125 exception_class = result.get('exception')
125 exception_class = result.get('exception')
126 exception_traceback = result.get('exception_traceback')
126 exception_traceback = result.get('exception_traceback')
127
127
128 if exception_traceback:
128 if exception_traceback:
129 log.error('Got traceback from remote call:%s', exception_traceback)
129 log.error('Got traceback from remote call:%s', exception_traceback)
130
130
131 if exception_class == 'HTTPLockedRC':
131 if exception_class == 'HTTPLockedRC':
132 raise exceptions.RepositoryLockedException()(*result['exception_args'])
132 raise exceptions.RepositoryLockedException()(*result['exception_args'])
133 elif exception_class == 'HTTPBranchProtected':
133 elif exception_class == 'HTTPBranchProtected':
134 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
134 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
135 elif exception_class == 'RepositoryError':
135 elif exception_class == 'RepositoryError':
136 raise exceptions.VcsException()(*result['exception_args'])
136 raise exceptions.VcsException()(*result['exception_args'])
137 elif exception_class:
137 elif exception_class:
138 raise Exception('Got remote exception "%s" with args "%s"' %
138 raise Exception('Got remote exception "%s" with args "%s"' %
139 (exception_class, result['exception_args']))
139 (exception_class, result['exception_args']))
140
140
141
141
142 def _get_hooks_client(extras):
142 def _get_hooks_client(extras):
143 if 'hooks_uri' in extras:
143 if 'hooks_uri' in extras:
144 protocol = extras.get('hooks_protocol')
144 protocol = extras.get('hooks_protocol')
145 return HooksHttpClient(extras['hooks_uri'])
145 return HooksHttpClient(extras['hooks_uri'])
146 else:
146 else:
147 return HooksDummyClient(extras['hooks_module'])
147 return HooksDummyClient(extras['hooks_module'])
148
148
149
149
150 def _call_hook(hook_name, extras, writer):
150 def _call_hook(hook_name, extras, writer):
151 hooks_client = _get_hooks_client(extras)
151 hooks_client = _get_hooks_client(extras)
152 log.debug('Hooks, using client:%s', hooks_client)
152 log.debug('Hooks, using client:%s', hooks_client)
153 result = hooks_client(hook_name, extras)
153 result = hooks_client(hook_name, extras)
154 log.debug('Hooks got result: %s', result)
154 log.debug('Hooks got result: %s', result)
155
155
156 _handle_exception(result)
156 _handle_exception(result)
157 writer.write(result['output'])
157 writer.write(result['output'])
158
158
159 return result['status']
159 return result['status']
160
160
161
161
162 def _extras_from_ui(ui):
162 def _extras_from_ui(ui):
163 hook_data = ui.config('rhodecode', 'RC_SCM_DATA')
163 hook_data = ui.config('rhodecode', 'RC_SCM_DATA')
164 if not hook_data:
164 if not hook_data:
165 # maybe it's inside environ ?
165 # maybe it's inside environ ?
166 env_hook_data = os.environ.get('RC_SCM_DATA')
166 env_hook_data = os.environ.get('RC_SCM_DATA')
167 if env_hook_data:
167 if env_hook_data:
168 hook_data = env_hook_data
168 hook_data = env_hook_data
169
169
170 extras = {}
170 extras = {}
171 if hook_data:
171 if hook_data:
172 extras = json.loads(hook_data)
172 extras = json.loads(hook_data)
173 return extras
173 return extras
174
174
175
175
176 def _rev_range_hash(repo, node, check_heads=False):
176 def _rev_range_hash(repo, node, check_heads=False):
177
177
178 commits = []
178 commits = []
179 revs = []
179 revs = []
180 start = repo[node].rev()
180 start = repo[node].rev()
181 end = len(repo)
181 end = len(repo)
182 for rev in range(start, end):
182 for rev in range(start, end):
183 revs.append(rev)
183 revs.append(rev)
184 ctx = repo[rev]
184 ctx = repo[rev]
185 commit_id = mercurial.node.hex(ctx.node())
185 commit_id = mercurial.node.hex(ctx.node())
186 branch = ctx.branch()
186 branch = ctx.branch()
187 commits.append((commit_id, branch))
187 commits.append((commit_id, branch))
188
188
189 parent_heads = []
189 parent_heads = []
190 if check_heads:
190 if check_heads:
191 parent_heads = _check_heads(repo, start, end, revs)
191 parent_heads = _check_heads(repo, start, end, revs)
192 return commits, parent_heads
192 return commits, parent_heads
193
193
194
194
195 def _check_heads(repo, start, end, commits):
195 def _check_heads(repo, start, end, commits):
196 changelog = repo.changelog
196 changelog = repo.changelog
197 parents = set()
197 parents = set()
198
198
199 for new_rev in commits:
199 for new_rev in commits:
200 for p in changelog.parentrevs(new_rev):
200 for p in changelog.parentrevs(new_rev):
201 if p == mercurial.node.nullrev:
201 if p == mercurial.node.nullrev:
202 continue
202 continue
203 if p < start:
203 if p < start:
204 parents.add(p)
204 parents.add(p)
205
205
206 for p in parents:
206 for p in parents:
207 branch = repo[p].branch()
207 branch = repo[p].branch()
208 # The heads descending from that parent, on the same branch
208 # The heads descending from that parent, on the same branch
209 parent_heads = set([p])
209 parent_heads = set([p])
210 reachable = set([p])
210 reachable = set([p])
211 for x in xrange(p + 1, end):
211 for x in xrange(p + 1, end):
212 if repo[x].branch() != branch:
212 if repo[x].branch() != branch:
213 continue
213 continue
214 for pp in changelog.parentrevs(x):
214 for pp in changelog.parentrevs(x):
215 if pp in reachable:
215 if pp in reachable:
216 reachable.add(x)
216 reachable.add(x)
217 parent_heads.discard(pp)
217 parent_heads.discard(pp)
218 parent_heads.add(x)
218 parent_heads.add(x)
219 # More than one head? Suggest merging
219 # More than one head? Suggest merging
220 if len(parent_heads) > 1:
220 if len(parent_heads) > 1:
221 return list(parent_heads)
221 return list(parent_heads)
222
222
223 return []
223 return []
224
224
225
225
226 def _get_git_env():
226 def _get_git_env():
227 env = {}
227 env = {}
228 for k, v in os.environ.items():
228 for k, v in os.environ.items():
229 if k.startswith('GIT'):
229 if k.startswith('GIT'):
230 env[k] = v
230 env[k] = v
231
231
232 # serialized version
232 # serialized version
233 return [(k, v) for k, v in env.items()]
233 return [(k, v) for k, v in env.items()]
234
234
235
235
236 def _get_hg_env(old_rev, new_rev, txnid, repo_path):
236 def _get_hg_env(old_rev, new_rev, txnid, repo_path):
237 env = {}
237 env = {}
238 for k, v in os.environ.items():
238 for k, v in os.environ.items():
239 if k.startswith('HG'):
239 if k.startswith('HG'):
240 env[k] = v
240 env[k] = v
241
241
242 env['HG_NODE'] = old_rev
242 env['HG_NODE'] = old_rev
243 env['HG_NODE_LAST'] = new_rev
243 env['HG_NODE_LAST'] = new_rev
244 env['HG_TXNID'] = txnid
244 env['HG_TXNID'] = txnid
245 env['HG_PENDING'] = repo_path
245 env['HG_PENDING'] = repo_path
246
246
247 return [(k, v) for k, v in env.items()]
247 return [(k, v) for k, v in env.items()]
248
248
249
249
250 def repo_size(ui, repo, **kwargs):
250 def repo_size(ui, repo, **kwargs):
251 extras = _extras_from_ui(ui)
251 extras = _extras_from_ui(ui)
252 return _call_hook('repo_size', extras, HgMessageWriter(ui))
252 return _call_hook('repo_size', extras, HgMessageWriter(ui))
253
253
254
254
255 def pre_pull(ui, repo, **kwargs):
255 def pre_pull(ui, repo, **kwargs):
256 extras = _extras_from_ui(ui)
256 extras = _extras_from_ui(ui)
257 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
257 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
258
258
259
259
260 def pre_pull_ssh(ui, repo, **kwargs):
260 def pre_pull_ssh(ui, repo, **kwargs):
261 extras = _extras_from_ui(ui)
261 extras = _extras_from_ui(ui)
262 if extras and extras.get('SSH'):
262 if extras and extras.get('SSH'):
263 return pre_pull(ui, repo, **kwargs)
263 return pre_pull(ui, repo, **kwargs)
264 return 0
264 return 0
265
265
266
266
267 def post_pull(ui, repo, **kwargs):
267 def post_pull(ui, repo, **kwargs):
268 extras = _extras_from_ui(ui)
268 extras = _extras_from_ui(ui)
269 return _call_hook('post_pull', extras, HgMessageWriter(ui))
269 return _call_hook('post_pull', extras, HgMessageWriter(ui))
270
270
271
271
272 def post_pull_ssh(ui, repo, **kwargs):
272 def post_pull_ssh(ui, repo, **kwargs):
273 extras = _extras_from_ui(ui)
273 extras = _extras_from_ui(ui)
274 if extras and extras.get('SSH'):
274 if extras and extras.get('SSH'):
275 return post_pull(ui, repo, **kwargs)
275 return post_pull(ui, repo, **kwargs)
276 return 0
276 return 0
277
277
278
278
279 def pre_push(ui, repo, node=None, **kwargs):
279 def pre_push(ui, repo, node=None, **kwargs):
280 """
280 """
281 Mercurial pre_push hook
281 Mercurial pre_push hook
282 """
282 """
283 extras = _extras_from_ui(ui)
283 extras = _extras_from_ui(ui)
284 detect_force_push = extras.get('detect_force_push')
284 detect_force_push = extras.get('detect_force_push')
285
285
286 rev_data = []
286 rev_data = []
287 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
287 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
288 branches = collections.defaultdict(list)
288 branches = collections.defaultdict(list)
289 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
289 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
290 for commit_id, branch in commits:
290 for commit_id, branch in commits:
291 branches[branch].append(commit_id)
291 branches[branch].append(commit_id)
292
292
293 for branch, commits in branches.items():
293 for branch, commits in branches.items():
294 old_rev = kwargs.get('node_last') or commits[0]
294 old_rev = kwargs.get('node_last') or commits[0]
295 rev_data.append({
295 rev_data.append({
296 'total_commits': len(commits),
296 'total_commits': len(commits),
297 'old_rev': old_rev,
297 'old_rev': old_rev,
298 'new_rev': commits[-1],
298 'new_rev': commits[-1],
299 'ref': '',
299 'ref': '',
300 'type': 'branch',
300 'type': 'branch',
301 'name': branch,
301 'name': branch,
302 })
302 })
303
303
304 for push_ref in rev_data:
304 for push_ref in rev_data:
305 push_ref['multiple_heads'] = _heads
305 push_ref['multiple_heads'] = _heads
306
306
307 repo_path = os.path.join(
307 repo_path = os.path.join(
308 extras.get('repo_store', ''), extras.get('repository', ''))
308 extras.get('repo_store', ''), extras.get('repository', ''))
309 push_ref['hg_env'] = _get_hg_env(
309 push_ref['hg_env'] = _get_hg_env(
310 old_rev=push_ref['old_rev'],
310 old_rev=push_ref['old_rev'],
311 new_rev=push_ref['new_rev'], txnid=kwargs.get('txnid'),
311 new_rev=push_ref['new_rev'], txnid=kwargs.get('txnid'),
312 repo_path=repo_path)
312 repo_path=repo_path)
313
313
314 extras['hook_type'] = kwargs.get('hooktype', 'pre_push')
314 extras['hook_type'] = kwargs.get('hooktype', 'pre_push')
315 extras['commit_ids'] = rev_data
315 extras['commit_ids'] = rev_data
316
316
317 return _call_hook('pre_push', extras, HgMessageWriter(ui))
317 return _call_hook('pre_push', extras, HgMessageWriter(ui))
318
318
319
319
320 def pre_push_ssh(ui, repo, node=None, **kwargs):
320 def pre_push_ssh(ui, repo, node=None, **kwargs):
321 extras = _extras_from_ui(ui)
321 extras = _extras_from_ui(ui)
322 if extras.get('SSH'):
322 if extras.get('SSH'):
323 return pre_push(ui, repo, node, **kwargs)
323 return pre_push(ui, repo, node, **kwargs)
324
324
325 return 0
325 return 0
326
326
327
327
328 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
328 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
329 """
329 """
330 Mercurial pre_push hook for SSH
330 Mercurial pre_push hook for SSH
331 """
331 """
332 extras = _extras_from_ui(ui)
332 extras = _extras_from_ui(ui)
333 if extras.get('SSH'):
333 if extras.get('SSH'):
334 permission = extras['SSH_PERMISSIONS']
334 permission = extras['SSH_PERMISSIONS']
335
335
336 if 'repository.write' == permission or 'repository.admin' == permission:
336 if 'repository.write' == permission or 'repository.admin' == permission:
337 return 0
337 return 0
338
338
339 # non-zero ret code
339 # non-zero ret code
340 return 1
340 return 1
341
341
342 return 0
342 return 0
343
343
344
344
345 def post_push(ui, repo, node, **kwargs):
345 def post_push(ui, repo, node, **kwargs):
346 """
346 """
347 Mercurial post_push hook
347 Mercurial post_push hook
348 """
348 """
349 extras = _extras_from_ui(ui)
349 extras = _extras_from_ui(ui)
350
350
351 commit_ids = []
351 commit_ids = []
352 branches = []
352 branches = []
353 bookmarks = []
353 bookmarks = []
354 tags = []
354 tags = []
355
355
356 commits, _heads = _rev_range_hash(repo, node)
356 commits, _heads = _rev_range_hash(repo, node)
357 for commit_id, branch in commits:
357 for commit_id, branch in commits:
358 commit_ids.append(commit_id)
358 commit_ids.append(commit_id)
359 if branch not in branches:
359 if branch not in branches:
360 branches.append(branch)
360 branches.append(branch)
361
361
362 if hasattr(ui, '_rc_pushkey_branches'):
362 if hasattr(ui, '_rc_pushkey_branches'):
363 bookmarks = ui._rc_pushkey_branches
363 bookmarks = ui._rc_pushkey_branches
364
364
365 extras['hook_type'] = kwargs.get('hooktype', 'post_push')
365 extras['hook_type'] = kwargs.get('hooktype', 'post_push')
366 extras['commit_ids'] = commit_ids
366 extras['commit_ids'] = commit_ids
367 extras['new_refs'] = {
367 extras['new_refs'] = {
368 'branches': branches,
368 'branches': branches,
369 'bookmarks': bookmarks,
369 'bookmarks': bookmarks,
370 'tags': tags
370 'tags': tags
371 }
371 }
372
372
373 return _call_hook('post_push', extras, HgMessageWriter(ui))
373 return _call_hook('post_push', extras, HgMessageWriter(ui))
374
374
375
375
376 def post_push_ssh(ui, repo, node, **kwargs):
376 def post_push_ssh(ui, repo, node, **kwargs):
377 """
377 """
378 Mercurial post_push hook for SSH
378 Mercurial post_push hook for SSH
379 """
379 """
380 if _extras_from_ui(ui).get('SSH'):
380 if _extras_from_ui(ui).get('SSH'):
381 return post_push(ui, repo, node, **kwargs)
381 return post_push(ui, repo, node, **kwargs)
382 return 0
382 return 0
383
383
384
384
385 def key_push(ui, repo, **kwargs):
385 def key_push(ui, repo, **kwargs):
386 if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks':
386 if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks':
387 # store new bookmarks in our UI object propagated later to post_push
387 # store new bookmarks in our UI object propagated later to post_push
388 ui._rc_pushkey_branches = repo[kwargs['key']].bookmarks()
388 ui._rc_pushkey_branches = repo[kwargs['key']].bookmarks()
389 return
389 return
390
390
391
391
392 # backward compat
392 # backward compat
393 log_pull_action = post_pull
393 log_pull_action = post_pull
394
394
395 # backward compat
395 # backward compat
396 log_push_action = post_push
396 log_push_action = post_push
397
397
398
398
399 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
399 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
400 """
400 """
401 Old hook name: keep here for backward compatibility.
401 Old hook name: keep here for backward compatibility.
402
402
403 This is only required when the installed git hooks are not upgraded.
403 This is only required when the installed git hooks are not upgraded.
404 """
404 """
405 pass
405 pass
406
406
407
407
408 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
408 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
409 """
409 """
410 Old hook name: keep here for backward compatibility.
410 Old hook name: keep here for backward compatibility.
411
411
412 This is only required when the installed git hooks are not upgraded.
412 This is only required when the installed git hooks are not upgraded.
413 """
413 """
414 pass
414 pass
415
415
416
416
417 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
417 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
418
418
419
419
420 def git_pre_pull(extras):
420 def git_pre_pull(extras):
421 """
421 """
422 Pre pull hook.
422 Pre pull hook.
423
423
424 :param extras: dictionary containing the keys defined in simplevcs
424 :param extras: dictionary containing the keys defined in simplevcs
425 :type extras: dict
425 :type extras: dict
426
426
427 :return: status code of the hook. 0 for success.
427 :return: status code of the hook. 0 for success.
428 :rtype: int
428 :rtype: int
429 """
429 """
430 if 'pull' not in extras['hooks']:
430 if 'pull' not in extras['hooks']:
431 return HookResponse(0, '')
431 return HookResponse(0, '')
432
432
433 stdout = io.BytesIO()
433 stdout = io.BytesIO()
434 try:
434 try:
435 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
435 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
436 except Exception as error:
436 except Exception as error:
437 status = 128
437 status = 128
438 stdout.write('ERROR: %s\n' % str(error))
438 stdout.write('ERROR: %s\n' % str(error))
439
439
440 return HookResponse(status, stdout.getvalue())
440 return HookResponse(status, stdout.getvalue())
441
441
442
442
443 def git_post_pull(extras):
443 def git_post_pull(extras):
444 """
444 """
445 Post pull hook.
445 Post pull hook.
446
446
447 :param extras: dictionary containing the keys defined in simplevcs
447 :param extras: dictionary containing the keys defined in simplevcs
448 :type extras: dict
448 :type extras: dict
449
449
450 :return: status code of the hook. 0 for success.
450 :return: status code of the hook. 0 for success.
451 :rtype: int
451 :rtype: int
452 """
452 """
453 if 'pull' not in extras['hooks']:
453 if 'pull' not in extras['hooks']:
454 return HookResponse(0, '')
454 return HookResponse(0, '')
455
455
456 stdout = io.BytesIO()
456 stdout = io.BytesIO()
457 try:
457 try:
458 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
458 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
459 except Exception as error:
459 except Exception as error:
460 status = 128
460 status = 128
461 stdout.write('ERROR: %s\n' % error)
461 stdout.write('ERROR: %s\n' % error)
462
462
463 return HookResponse(status, stdout.getvalue())
463 return HookResponse(status, stdout.getvalue())
464
464
465
465
466 def _parse_git_ref_lines(revision_lines):
466 def _parse_git_ref_lines(revision_lines):
467 rev_data = []
467 rev_data = []
468 for revision_line in revision_lines or []:
468 for revision_line in revision_lines or []:
469 old_rev, new_rev, ref = revision_line.strip().split(' ')
469 old_rev, new_rev, ref = revision_line.strip().split(' ')
470 ref_data = ref.split('/', 2)
470 ref_data = ref.split('/', 2)
471 if ref_data[1] in ('tags', 'heads'):
471 if ref_data[1] in ('tags', 'heads'):
472 rev_data.append({
472 rev_data.append({
473 # NOTE(marcink):
473 # NOTE(marcink):
474 # we're unable to tell total_commits for git at this point
474 # we're unable to tell total_commits for git at this point
475 # but we set the variable for consistency with GIT
475 # but we set the variable for consistency with GIT
476 'total_commits': -1,
476 'total_commits': -1,
477 'old_rev': old_rev,
477 'old_rev': old_rev,
478 'new_rev': new_rev,
478 'new_rev': new_rev,
479 'ref': ref,
479 'ref': ref,
480 'type': ref_data[1],
480 'type': ref_data[1],
481 'name': ref_data[2],
481 'name': ref_data[2],
482 })
482 })
483 return rev_data
483 return rev_data
484
484
485
485
486 def git_pre_receive(unused_repo_path, revision_lines, env):
486 def git_pre_receive(unused_repo_path, revision_lines, env):
487 """
487 """
488 Pre push hook.
488 Pre push hook.
489
489
490 :param extras: dictionary containing the keys defined in simplevcs
490 :param extras: dictionary containing the keys defined in simplevcs
491 :type extras: dict
491 :type extras: dict
492
492
493 :return: status code of the hook. 0 for success.
493 :return: status code of the hook. 0 for success.
494 :rtype: int
494 :rtype: int
495 """
495 """
496 extras = json.loads(env['RC_SCM_DATA'])
496 extras = json.loads(env['RC_SCM_DATA'])
497 rev_data = _parse_git_ref_lines(revision_lines)
497 rev_data = _parse_git_ref_lines(revision_lines)
498 if 'push' not in extras['hooks']:
498 if 'push' not in extras['hooks']:
499 return 0
499 return 0
500 empty_commit_id = '0' * 40
500 empty_commit_id = '0' * 40
501
501
502 detect_force_push = extras.get('detect_force_push')
502 detect_force_push = extras.get('detect_force_push')
503
503
504 for push_ref in rev_data:
504 for push_ref in rev_data:
505 # store our git-env which holds the temp store
505 # store our git-env which holds the temp store
506 push_ref['git_env'] = _get_git_env()
506 push_ref['git_env'] = _get_git_env()
507 push_ref['pruned_sha'] = ''
507 push_ref['pruned_sha'] = ''
508 if not detect_force_push:
508 if not detect_force_push:
509 # don't check for forced-push when we don't need to
509 # don't check for forced-push when we don't need to
510 continue
510 continue
511
511
512 type_ = push_ref['type']
512 type_ = push_ref['type']
513 new_branch = push_ref['old_rev'] == empty_commit_id
513 new_branch = push_ref['old_rev'] == empty_commit_id
514 if type_ == 'heads' and not new_branch:
514 delete_branch = push_ref['new_rev'] == empty_commit_id
515 if type_ == 'heads' and not (new_branch or delete_branch):
515 old_rev = push_ref['old_rev']
516 old_rev = push_ref['old_rev']
516 new_rev = push_ref['new_rev']
517 new_rev = push_ref['new_rev']
517 cmd = [settings.GIT_EXECUTABLE, 'rev-list',
518 cmd = [settings.GIT_EXECUTABLE, 'rev-list', old_rev, '^{}'.format(new_rev)]
518 old_rev, '^{}'.format(new_rev)]
519 stdout, stderr = subprocessio.run_command(
519 stdout, stderr = subprocessio.run_command(
520 cmd, env=os.environ.copy())
520 cmd, env=os.environ.copy())
521 # means we're having some non-reachable objects, this forced push
521 # means we're having some non-reachable objects, this forced push was used
522 # was used
523 if stdout:
522 if stdout:
524 push_ref['pruned_sha'] = stdout.splitlines()
523 push_ref['pruned_sha'] = stdout.splitlines()
525
524
526 extras['hook_type'] = 'pre_receive'
525 extras['hook_type'] = 'pre_receive'
527 extras['commit_ids'] = rev_data
526 extras['commit_ids'] = rev_data
528 return _call_hook('pre_push', extras, GitMessageWriter())
527 return _call_hook('pre_push', extras, GitMessageWriter())
529
528
530
529
531 def git_post_receive(unused_repo_path, revision_lines, env):
530 def git_post_receive(unused_repo_path, revision_lines, env):
532 """
531 """
533 Post push hook.
532 Post push hook.
534
533
535 :param extras: dictionary containing the keys defined in simplevcs
534 :param extras: dictionary containing the keys defined in simplevcs
536 :type extras: dict
535 :type extras: dict
537
536
538 :return: status code of the hook. 0 for success.
537 :return: status code of the hook. 0 for success.
539 :rtype: int
538 :rtype: int
540 """
539 """
541 extras = json.loads(env['RC_SCM_DATA'])
540 extras = json.loads(env['RC_SCM_DATA'])
542 if 'push' not in extras['hooks']:
541 if 'push' not in extras['hooks']:
543 return 0
542 return 0
544
543
545 rev_data = _parse_git_ref_lines(revision_lines)
544 rev_data = _parse_git_ref_lines(revision_lines)
546
545
547 git_revs = []
546 git_revs = []
548
547
549 # N.B.(skreft): it is ok to just call git, as git before calling a
548 # N.B.(skreft): it is ok to just call git, as git before calling a
550 # subcommand sets the PATH environment variable so that it point to the
549 # subcommand sets the PATH environment variable so that it point to the
551 # correct version of the git executable.
550 # correct version of the git executable.
552 empty_commit_id = '0' * 40
551 empty_commit_id = '0' * 40
553 branches = []
552 branches = []
554 tags = []
553 tags = []
555 for push_ref in rev_data:
554 for push_ref in rev_data:
556 type_ = push_ref['type']
555 type_ = push_ref['type']
557
556
558 if type_ == 'heads':
557 if type_ == 'heads':
559 if push_ref['old_rev'] == empty_commit_id:
558 if push_ref['old_rev'] == empty_commit_id:
560 # starting new branch case
559 # starting new branch case
561 if push_ref['name'] not in branches:
560 if push_ref['name'] not in branches:
562 branches.append(push_ref['name'])
561 branches.append(push_ref['name'])
563
562
564 # Fix up head revision if needed
563 # Fix up head revision if needed
565 cmd = [settings.GIT_EXECUTABLE, 'show', 'HEAD']
564 cmd = [settings.GIT_EXECUTABLE, 'show', 'HEAD']
566 try:
565 try:
567 subprocessio.run_command(cmd, env=os.environ.copy())
566 subprocessio.run_command(cmd, env=os.environ.copy())
568 except Exception:
567 except Exception:
569 cmd = [settings.GIT_EXECUTABLE, 'symbolic-ref', 'HEAD',
568 cmd = [settings.GIT_EXECUTABLE, 'symbolic-ref', 'HEAD',
570 'refs/heads/%s' % push_ref['name']]
569 'refs/heads/%s' % push_ref['name']]
571 print("Setting default branch to %s" % push_ref['name'])
570 print("Setting default branch to %s" % push_ref['name'])
572 subprocessio.run_command(cmd, env=os.environ.copy())
571 subprocessio.run_command(cmd, env=os.environ.copy())
573
572
574 cmd = [settings.GIT_EXECUTABLE, 'for-each-ref',
573 cmd = [settings.GIT_EXECUTABLE, 'for-each-ref',
575 '--format=%(refname)', 'refs/heads/*']
574 '--format=%(refname)', 'refs/heads/*']
576 stdout, stderr = subprocessio.run_command(
575 stdout, stderr = subprocessio.run_command(
577 cmd, env=os.environ.copy())
576 cmd, env=os.environ.copy())
578 heads = stdout
577 heads = stdout
579 heads = heads.replace(push_ref['ref'], '')
578 heads = heads.replace(push_ref['ref'], '')
580 heads = ' '.join(head for head
579 heads = ' '.join(head for head
581 in heads.splitlines() if head) or '.'
580 in heads.splitlines() if head) or '.'
582 cmd = [settings.GIT_EXECUTABLE, 'log', '--reverse',
581 cmd = [settings.GIT_EXECUTABLE, 'log', '--reverse',
583 '--pretty=format:%H', '--', push_ref['new_rev'],
582 '--pretty=format:%H', '--', push_ref['new_rev'],
584 '--not', heads]
583 '--not', heads]
585 stdout, stderr = subprocessio.run_command(
584 stdout, stderr = subprocessio.run_command(
586 cmd, env=os.environ.copy())
585 cmd, env=os.environ.copy())
587 git_revs.extend(stdout.splitlines())
586 git_revs.extend(stdout.splitlines())
588 elif push_ref['new_rev'] == empty_commit_id:
587 elif push_ref['new_rev'] == empty_commit_id:
589 # delete branch case
588 # delete branch case
590 git_revs.append('delete_branch=>%s' % push_ref['name'])
589 git_revs.append('delete_branch=>%s' % push_ref['name'])
591 else:
590 else:
592 if push_ref['name'] not in branches:
591 if push_ref['name'] not in branches:
593 branches.append(push_ref['name'])
592 branches.append(push_ref['name'])
594
593
595 cmd = [settings.GIT_EXECUTABLE, 'log',
594 cmd = [settings.GIT_EXECUTABLE, 'log',
596 '{old_rev}..{new_rev}'.format(**push_ref),
595 '{old_rev}..{new_rev}'.format(**push_ref),
597 '--reverse', '--pretty=format:%H']
596 '--reverse', '--pretty=format:%H']
598 stdout, stderr = subprocessio.run_command(
597 stdout, stderr = subprocessio.run_command(
599 cmd, env=os.environ.copy())
598 cmd, env=os.environ.copy())
600 git_revs.extend(stdout.splitlines())
599 git_revs.extend(stdout.splitlines())
601 elif type_ == 'tags':
600 elif type_ == 'tags':
602 if push_ref['name'] not in tags:
601 if push_ref['name'] not in tags:
603 tags.append(push_ref['name'])
602 tags.append(push_ref['name'])
604 git_revs.append('tag=>%s' % push_ref['name'])
603 git_revs.append('tag=>%s' % push_ref['name'])
605
604
606 extras['hook_type'] = 'post_receive'
605 extras['hook_type'] = 'post_receive'
607 extras['commit_ids'] = git_revs
606 extras['commit_ids'] = git_revs
608 extras['new_refs'] = {
607 extras['new_refs'] = {
609 'branches': branches,
608 'branches': branches,
610 'bookmarks': [],
609 'bookmarks': [],
611 'tags': tags,
610 'tags': tags,
612 }
611 }
613
612
614 if 'repo_size' in extras['hooks']:
613 if 'repo_size' in extras['hooks']:
615 try:
614 try:
616 _call_hook('repo_size', extras, GitMessageWriter())
615 _call_hook('repo_size', extras, GitMessageWriter())
617 except:
616 except:
618 pass
617 pass
619
618
620 return _call_hook('post_push', extras, GitMessageWriter())
619 return _call_hook('post_push', extras, GitMessageWriter())
621
620
622
621
623 def _get_extras_from_txn_id(path, txn_id):
622 def _get_extras_from_txn_id(path, txn_id):
624 extras = {}
623 extras = {}
625 try:
624 try:
626 cmd = ['svnlook', 'pget',
625 cmd = ['svnlook', 'pget',
627 '-t', txn_id,
626 '-t', txn_id,
628 '--revprop', path, 'rc-scm-extras']
627 '--revprop', path, 'rc-scm-extras']
629 stdout, stderr = subprocessio.run_command(
628 stdout, stderr = subprocessio.run_command(
630 cmd, env=os.environ.copy())
629 cmd, env=os.environ.copy())
631 extras = json.loads(base64.urlsafe_b64decode(stdout))
630 extras = json.loads(base64.urlsafe_b64decode(stdout))
632 except Exception:
631 except Exception:
633 log.exception('Failed to extract extras info from txn_id')
632 log.exception('Failed to extract extras info from txn_id')
634
633
635 return extras
634 return extras
636
635
637
636
638 def _get_extras_from_commit_id(commit_id, path):
637 def _get_extras_from_commit_id(commit_id, path):
639 extras = {}
638 extras = {}
640 try:
639 try:
641 cmd = ['svnlook', 'pget',
640 cmd = ['svnlook', 'pget',
642 '-r', commit_id,
641 '-r', commit_id,
643 '--revprop', path, 'rc-scm-extras']
642 '--revprop', path, 'rc-scm-extras']
644 stdout, stderr = subprocessio.run_command(
643 stdout, stderr = subprocessio.run_command(
645 cmd, env=os.environ.copy())
644 cmd, env=os.environ.copy())
646 extras = json.loads(base64.urlsafe_b64decode(stdout))
645 extras = json.loads(base64.urlsafe_b64decode(stdout))
647 except Exception:
646 except Exception:
648 log.exception('Failed to extract extras info from commit_id')
647 log.exception('Failed to extract extras info from commit_id')
649
648
650 return extras
649 return extras
651
650
652
651
653 def svn_pre_commit(repo_path, commit_data, env):
652 def svn_pre_commit(repo_path, commit_data, env):
654 path, txn_id = commit_data
653 path, txn_id = commit_data
655 branches = []
654 branches = []
656 tags = []
655 tags = []
657
656
658 if env.get('RC_SCM_DATA'):
657 if env.get('RC_SCM_DATA'):
659 extras = json.loads(env['RC_SCM_DATA'])
658 extras = json.loads(env['RC_SCM_DATA'])
660 else:
659 else:
661 # fallback method to read from TXN-ID stored data
660 # fallback method to read from TXN-ID stored data
662 extras = _get_extras_from_txn_id(path, txn_id)
661 extras = _get_extras_from_txn_id(path, txn_id)
663 if not extras:
662 if not extras:
664 return 0
663 return 0
665
664
666 extras['hook_type'] = 'pre_commit'
665 extras['hook_type'] = 'pre_commit'
667 extras['commit_ids'] = []
666 extras['commit_ids'] = []
668 extras['txn_id'] = txn_id
667 extras['txn_id'] = txn_id
669 extras['new_refs'] = {
668 extras['new_refs'] = {
670 'total_commits': 1,
669 'total_commits': 1,
671 'branches': branches,
670 'branches': branches,
672 'bookmarks': [],
671 'bookmarks': [],
673 'tags': tags,
672 'tags': tags,
674 }
673 }
675
674
676 return _call_hook('pre_push', extras, SvnMessageWriter())
675 return _call_hook('pre_push', extras, SvnMessageWriter())
677
676
678
677
679 def svn_post_commit(repo_path, commit_data, env):
678 def svn_post_commit(repo_path, commit_data, env):
680 """
679 """
681 commit_data is path, rev, txn_id
680 commit_data is path, rev, txn_id
682 """
681 """
683 path, commit_id, txn_id = commit_data
682 path, commit_id, txn_id = commit_data
684 branches = []
683 branches = []
685 tags = []
684 tags = []
686
685
687 if env.get('RC_SCM_DATA'):
686 if env.get('RC_SCM_DATA'):
688 extras = json.loads(env['RC_SCM_DATA'])
687 extras = json.loads(env['RC_SCM_DATA'])
689 else:
688 else:
690 # fallback method to read from TXN-ID stored data
689 # fallback method to read from TXN-ID stored data
691 extras = _get_extras_from_commit_id(commit_id, path)
690 extras = _get_extras_from_commit_id(commit_id, path)
692 if not extras:
691 if not extras:
693 return 0
692 return 0
694
693
695 extras['hook_type'] = 'post_commit'
694 extras['hook_type'] = 'post_commit'
696 extras['commit_ids'] = [commit_id]
695 extras['commit_ids'] = [commit_id]
697 extras['txn_id'] = txn_id
696 extras['txn_id'] = txn_id
698 extras['new_refs'] = {
697 extras['new_refs'] = {
699 'branches': branches,
698 'branches': branches,
700 'bookmarks': [],
699 'bookmarks': [],
701 'tags': tags,
700 'tags': tags,
702 'total_commits': 1,
701 'total_commits': 1,
703 }
702 }
704
703
705 if 'repo_size' in extras['hooks']:
704 if 'repo_size' in extras['hooks']:
706 try:
705 try:
707 _call_hook('repo_size', extras, SvnMessageWriter())
706 _call_hook('repo_size', extras, SvnMessageWriter())
708 except Exception:
707 except Exception:
709 pass
708 pass
710
709
711 return _call_hook('post_push', extras, SvnMessageWriter())
710 return _call_hook('post_push', extras, SvnMessageWriter())
General Comments 0
You need to be logged in to leave comments. Login now