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