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