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