##// END OF EJS Templates
hooks: fixed function signature.
marcink -
r222:33f8414e default
parent child Browse files
Show More
@@ -1,396 +1,396 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-2017 RodeCode GmbH
4 # Copyright (C) 2014-2017 RodeCode 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 sys
21 import sys
22 import json
22 import json
23 import logging
23 import logging
24 import collections
24 import collections
25 import importlib
25 import importlib
26 import subprocess
26 import subprocess
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
35 from vcsserver import exceptions
36
36
37 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
38
38
39
39
40 class HooksHttpClient(object):
40 class HooksHttpClient(object):
41 connection = None
41 connection = None
42
42
43 def __init__(self, hooks_uri):
43 def __init__(self, hooks_uri):
44 self.hooks_uri = hooks_uri
44 self.hooks_uri = hooks_uri
45
45
46 def __call__(self, method, extras):
46 def __call__(self, method, extras):
47 connection = HTTPConnection(self.hooks_uri)
47 connection = HTTPConnection(self.hooks_uri)
48 body = self._serialize(method, extras)
48 body = self._serialize(method, extras)
49 connection.request('POST', '/', body)
49 connection.request('POST', '/', body)
50 response = connection.getresponse()
50 response = connection.getresponse()
51 return json.loads(response.read())
51 return json.loads(response.read())
52
52
53 def _serialize(self, hook_name, extras):
53 def _serialize(self, hook_name, extras):
54 data = {
54 data = {
55 'method': hook_name,
55 'method': hook_name,
56 'extras': extras
56 'extras': extras
57 }
57 }
58 return json.dumps(data)
58 return json.dumps(data)
59
59
60
60
61 class HooksDummyClient(object):
61 class HooksDummyClient(object):
62 def __init__(self, hooks_module):
62 def __init__(self, hooks_module):
63 self._hooks_module = importlib.import_module(hooks_module)
63 self._hooks_module = importlib.import_module(hooks_module)
64
64
65 def __call__(self, hook_name, extras):
65 def __call__(self, hook_name, extras):
66 with self._hooks_module.Hooks() as hooks:
66 with self._hooks_module.Hooks() as hooks:
67 return getattr(hooks, hook_name)(extras)
67 return getattr(hooks, hook_name)(extras)
68
68
69
69
70 class RemoteMessageWriter(object):
70 class RemoteMessageWriter(object):
71 """Writer base class."""
71 """Writer base class."""
72 def write(message):
72 def write(self, message):
73 raise NotImplementedError()
73 raise NotImplementedError()
74
74
75
75
76 class HgMessageWriter(RemoteMessageWriter):
76 class HgMessageWriter(RemoteMessageWriter):
77 """Writer that knows how to send messages to mercurial clients."""
77 """Writer that knows how to send messages to mercurial clients."""
78
78
79 def __init__(self, ui):
79 def __init__(self, ui):
80 self.ui = ui
80 self.ui = ui
81
81
82 def write(self, message):
82 def write(self, message):
83 # TODO: Check why the quiet flag is set by default.
83 # TODO: Check why the quiet flag is set by default.
84 old = self.ui.quiet
84 old = self.ui.quiet
85 self.ui.quiet = False
85 self.ui.quiet = False
86 self.ui.status(message.encode('utf-8'))
86 self.ui.status(message.encode('utf-8'))
87 self.ui.quiet = old
87 self.ui.quiet = old
88
88
89
89
90 class GitMessageWriter(RemoteMessageWriter):
90 class GitMessageWriter(RemoteMessageWriter):
91 """Writer that knows how to send messages to git clients."""
91 """Writer that knows how to send messages to git clients."""
92
92
93 def __init__(self, stdout=None):
93 def __init__(self, stdout=None):
94 self.stdout = stdout or sys.stdout
94 self.stdout = stdout or sys.stdout
95
95
96 def write(self, message):
96 def write(self, message):
97 self.stdout.write(message.encode('utf-8'))
97 self.stdout.write(message.encode('utf-8'))
98
98
99
99
100 def _handle_exception(result):
100 def _handle_exception(result):
101 exception_class = result.get('exception')
101 exception_class = result.get('exception')
102 exception_traceback = result.get('exception_traceback')
102 exception_traceback = result.get('exception_traceback')
103
103
104 if exception_traceback:
104 if exception_traceback:
105 log.error('Got traceback from remote call:%s', exception_traceback)
105 log.error('Got traceback from remote call:%s', exception_traceback)
106
106
107 if exception_class == 'HTTPLockedRC':
107 if exception_class == 'HTTPLockedRC':
108 raise exceptions.RepositoryLockedException(*result['exception_args'])
108 raise exceptions.RepositoryLockedException(*result['exception_args'])
109 elif exception_class == 'RepositoryError':
109 elif exception_class == 'RepositoryError':
110 raise exceptions.VcsException(*result['exception_args'])
110 raise exceptions.VcsException(*result['exception_args'])
111 elif exception_class:
111 elif exception_class:
112 raise Exception('Got remote exception "%s" with args "%s"' %
112 raise Exception('Got remote exception "%s" with args "%s"' %
113 (exception_class, result['exception_args']))
113 (exception_class, result['exception_args']))
114
114
115
115
116 def _get_hooks_client(extras):
116 def _get_hooks_client(extras):
117 if 'hooks_uri' in extras:
117 if 'hooks_uri' in extras:
118 protocol = extras.get('hooks_protocol')
118 protocol = extras.get('hooks_protocol')
119 return HooksHttpClient(extras['hooks_uri'])
119 return HooksHttpClient(extras['hooks_uri'])
120 else:
120 else:
121 return HooksDummyClient(extras['hooks_module'])
121 return HooksDummyClient(extras['hooks_module'])
122
122
123
123
124 def _call_hook(hook_name, extras, writer):
124 def _call_hook(hook_name, extras, writer):
125 hooks = _get_hooks_client(extras)
125 hooks = _get_hooks_client(extras)
126 result = hooks(hook_name, extras)
126 result = hooks(hook_name, extras)
127 writer.write(result['output'])
127 writer.write(result['output'])
128 _handle_exception(result)
128 _handle_exception(result)
129
129
130 return result['status']
130 return result['status']
131
131
132
132
133 def _extras_from_ui(ui):
133 def _extras_from_ui(ui):
134 extras = json.loads(ui.config('rhodecode', 'RC_SCM_DATA'))
134 extras = json.loads(ui.config('rhodecode', 'RC_SCM_DATA'))
135 return extras
135 return extras
136
136
137
137
138 def repo_size(ui, repo, **kwargs):
138 def repo_size(ui, repo, **kwargs):
139 return _call_hook('repo_size', _extras_from_ui(ui), HgMessageWriter(ui))
139 return _call_hook('repo_size', _extras_from_ui(ui), HgMessageWriter(ui))
140
140
141
141
142 def pre_pull(ui, repo, **kwargs):
142 def pre_pull(ui, repo, **kwargs):
143 return _call_hook('pre_pull', _extras_from_ui(ui), HgMessageWriter(ui))
143 return _call_hook('pre_pull', _extras_from_ui(ui), HgMessageWriter(ui))
144
144
145
145
146 def post_pull(ui, repo, **kwargs):
146 def post_pull(ui, repo, **kwargs):
147 return _call_hook('post_pull', _extras_from_ui(ui), HgMessageWriter(ui))
147 return _call_hook('post_pull', _extras_from_ui(ui), HgMessageWriter(ui))
148
148
149
149
150 def pre_push(ui, repo, node=None, **kwargs):
150 def pre_push(ui, repo, node=None, **kwargs):
151 extras = _extras_from_ui(ui)
151 extras = _extras_from_ui(ui)
152
152
153 rev_data = []
153 rev_data = []
154 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
154 if node and kwargs.get('hooktype') == 'pretxnchangegroup':
155 branches = collections.defaultdict(list)
155 branches = collections.defaultdict(list)
156 for commit_id, branch in _rev_range_hash(repo, node, with_branch=True):
156 for commit_id, branch in _rev_range_hash(repo, node, with_branch=True):
157 branches[branch].append(commit_id)
157 branches[branch].append(commit_id)
158
158
159 for branch, commits in branches.iteritems():
159 for branch, commits in branches.iteritems():
160 old_rev = kwargs.get('node_last') or commits[0]
160 old_rev = kwargs.get('node_last') or commits[0]
161 rev_data.append({
161 rev_data.append({
162 'old_rev': old_rev,
162 'old_rev': old_rev,
163 'new_rev': commits[-1],
163 'new_rev': commits[-1],
164 'ref': '',
164 'ref': '',
165 'type': 'branch',
165 'type': 'branch',
166 'name': branch,
166 'name': branch,
167 })
167 })
168
168
169 extras['commit_ids'] = rev_data
169 extras['commit_ids'] = rev_data
170 return _call_hook('pre_push', extras, HgMessageWriter(ui))
170 return _call_hook('pre_push', extras, HgMessageWriter(ui))
171
171
172
172
173 def _rev_range_hash(repo, node, with_branch=False):
173 def _rev_range_hash(repo, node, with_branch=False):
174
174
175 commits = []
175 commits = []
176 for rev in xrange(repo[node], len(repo)):
176 for rev in xrange(repo[node], len(repo)):
177 ctx = repo[rev]
177 ctx = repo[rev]
178 commit_id = mercurial.node.hex(ctx.node())
178 commit_id = mercurial.node.hex(ctx.node())
179 branch = ctx.branch()
179 branch = ctx.branch()
180 if with_branch:
180 if with_branch:
181 commits.append((commit_id, branch))
181 commits.append((commit_id, branch))
182 else:
182 else:
183 commits.append(commit_id)
183 commits.append(commit_id)
184
184
185 return commits
185 return commits
186
186
187
187
188 def post_push(ui, repo, node, **kwargs):
188 def post_push(ui, repo, node, **kwargs):
189 commit_ids = _rev_range_hash(repo, node)
189 commit_ids = _rev_range_hash(repo, node)
190
190
191 extras = _extras_from_ui(ui)
191 extras = _extras_from_ui(ui)
192 extras['commit_ids'] = commit_ids
192 extras['commit_ids'] = commit_ids
193
193
194 return _call_hook('post_push', extras, HgMessageWriter(ui))
194 return _call_hook('post_push', extras, HgMessageWriter(ui))
195
195
196
196
197 def key_push(ui, repo, **kwargs):
197 def key_push(ui, repo, **kwargs):
198 if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks':
198 if kwargs['new'] != '0' and kwargs['namespace'] == 'bookmarks':
199 # store new bookmarks in our UI object propagated later to post_push
199 # store new bookmarks in our UI object propagated later to post_push
200 ui._rc_pushkey_branches = repo[kwargs['key']].bookmarks()
200 ui._rc_pushkey_branches = repo[kwargs['key']].bookmarks()
201 return
201 return
202
202
203 # backward compat
203 # backward compat
204 log_pull_action = post_pull
204 log_pull_action = post_pull
205
205
206 # backward compat
206 # backward compat
207 log_push_action = post_push
207 log_push_action = post_push
208
208
209
209
210 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
210 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
211 """
211 """
212 Old hook name: keep here for backward compatibility.
212 Old hook name: keep here for backward compatibility.
213
213
214 This is only required when the installed git hooks are not upgraded.
214 This is only required when the installed git hooks are not upgraded.
215 """
215 """
216 pass
216 pass
217
217
218
218
219 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
219 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
220 """
220 """
221 Old hook name: keep here for backward compatibility.
221 Old hook name: keep here for backward compatibility.
222
222
223 This is only required when the installed git hooks are not upgraded.
223 This is only required when the installed git hooks are not upgraded.
224 """
224 """
225 pass
225 pass
226
226
227
227
228 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
228 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
229
229
230
230
231 def git_pre_pull(extras):
231 def git_pre_pull(extras):
232 """
232 """
233 Pre pull hook.
233 Pre pull hook.
234
234
235 :param extras: dictionary containing the keys defined in simplevcs
235 :param extras: dictionary containing the keys defined in simplevcs
236 :type extras: dict
236 :type extras: dict
237
237
238 :return: status code of the hook. 0 for success.
238 :return: status code of the hook. 0 for success.
239 :rtype: int
239 :rtype: int
240 """
240 """
241 if 'pull' not in extras['hooks']:
241 if 'pull' not in extras['hooks']:
242 return HookResponse(0, '')
242 return HookResponse(0, '')
243
243
244 stdout = io.BytesIO()
244 stdout = io.BytesIO()
245 try:
245 try:
246 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
246 status = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
247 except Exception as error:
247 except Exception as error:
248 status = 128
248 status = 128
249 stdout.write('ERROR: %s\n' % str(error))
249 stdout.write('ERROR: %s\n' % str(error))
250
250
251 return HookResponse(status, stdout.getvalue())
251 return HookResponse(status, stdout.getvalue())
252
252
253
253
254 def git_post_pull(extras):
254 def git_post_pull(extras):
255 """
255 """
256 Post pull hook.
256 Post pull hook.
257
257
258 :param extras: dictionary containing the keys defined in simplevcs
258 :param extras: dictionary containing the keys defined in simplevcs
259 :type extras: dict
259 :type extras: dict
260
260
261 :return: status code of the hook. 0 for success.
261 :return: status code of the hook. 0 for success.
262 :rtype: int
262 :rtype: int
263 """
263 """
264 if 'pull' not in extras['hooks']:
264 if 'pull' not in extras['hooks']:
265 return HookResponse(0, '')
265 return HookResponse(0, '')
266
266
267 stdout = io.BytesIO()
267 stdout = io.BytesIO()
268 try:
268 try:
269 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
269 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
270 except Exception as error:
270 except Exception as error:
271 status = 128
271 status = 128
272 stdout.write('ERROR: %s\n' % error)
272 stdout.write('ERROR: %s\n' % error)
273
273
274 return HookResponse(status, stdout.getvalue())
274 return HookResponse(status, stdout.getvalue())
275
275
276
276
277 def _parse_git_ref_lines(revision_lines):
277 def _parse_git_ref_lines(revision_lines):
278 rev_data = []
278 rev_data = []
279 for revision_line in revision_lines or []:
279 for revision_line in revision_lines or []:
280 old_rev, new_rev, ref = revision_line.strip().split(' ')
280 old_rev, new_rev, ref = revision_line.strip().split(' ')
281 ref_data = ref.split('/', 2)
281 ref_data = ref.split('/', 2)
282 if ref_data[1] in ('tags', 'heads'):
282 if ref_data[1] in ('tags', 'heads'):
283 rev_data.append({
283 rev_data.append({
284 'old_rev': old_rev,
284 'old_rev': old_rev,
285 'new_rev': new_rev,
285 'new_rev': new_rev,
286 'ref': ref,
286 'ref': ref,
287 'type': ref_data[1],
287 'type': ref_data[1],
288 'name': ref_data[2],
288 'name': ref_data[2],
289 })
289 })
290 return rev_data
290 return rev_data
291
291
292
292
293 def git_pre_receive(unused_repo_path, revision_lines, env):
293 def git_pre_receive(unused_repo_path, revision_lines, env):
294 """
294 """
295 Pre push hook.
295 Pre push hook.
296
296
297 :param extras: dictionary containing the keys defined in simplevcs
297 :param extras: dictionary containing the keys defined in simplevcs
298 :type extras: dict
298 :type extras: dict
299
299
300 :return: status code of the hook. 0 for success.
300 :return: status code of the hook. 0 for success.
301 :rtype: int
301 :rtype: int
302 """
302 """
303 extras = json.loads(env['RC_SCM_DATA'])
303 extras = json.loads(env['RC_SCM_DATA'])
304 rev_data = _parse_git_ref_lines(revision_lines)
304 rev_data = _parse_git_ref_lines(revision_lines)
305 if 'push' not in extras['hooks']:
305 if 'push' not in extras['hooks']:
306 return 0
306 return 0
307 extras['commit_ids'] = rev_data
307 extras['commit_ids'] = rev_data
308 return _call_hook('pre_push', extras, GitMessageWriter())
308 return _call_hook('pre_push', extras, GitMessageWriter())
309
309
310
310
311 def _run_command(arguments):
311 def _run_command(arguments):
312 """
312 """
313 Run the specified command and return the stdout.
313 Run the specified command and return the stdout.
314
314
315 :param arguments: sequence of program arguments (including the program name)
315 :param arguments: sequence of program arguments (including the program name)
316 :type arguments: list[str]
316 :type arguments: list[str]
317 """
317 """
318 # TODO(skreft): refactor this method and all the other similar ones.
318 # TODO(skreft): refactor this method and all the other similar ones.
319 # Probably this should be using subprocessio.
319 # Probably this should be using subprocessio.
320 process = subprocess.Popen(
320 process = subprocess.Popen(
321 arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
321 arguments, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
322 stdout, stderr = process.communicate()
322 stdout, stderr = process.communicate()
323
323
324 if process.returncode != 0:
324 if process.returncode != 0:
325 raise Exception(
325 raise Exception(
326 'Command %s exited with exit code %s: stderr:%s' % (
326 'Command %s exited with exit code %s: stderr:%s' % (
327 arguments, process.returncode, stderr))
327 arguments, process.returncode, stderr))
328
328
329 return stdout
329 return stdout
330
330
331
331
332 def git_post_receive(unused_repo_path, revision_lines, env):
332 def git_post_receive(unused_repo_path, revision_lines, env):
333 """
333 """
334 Post push hook.
334 Post push hook.
335
335
336 :param extras: dictionary containing the keys defined in simplevcs
336 :param extras: dictionary containing the keys defined in simplevcs
337 :type extras: dict
337 :type extras: dict
338
338
339 :return: status code of the hook. 0 for success.
339 :return: status code of the hook. 0 for success.
340 :rtype: int
340 :rtype: int
341 """
341 """
342 extras = json.loads(env['RC_SCM_DATA'])
342 extras = json.loads(env['RC_SCM_DATA'])
343 if 'push' not in extras['hooks']:
343 if 'push' not in extras['hooks']:
344 return 0
344 return 0
345
345
346 rev_data = _parse_git_ref_lines(revision_lines)
346 rev_data = _parse_git_ref_lines(revision_lines)
347
347
348 git_revs = []
348 git_revs = []
349
349
350 # N.B.(skreft): it is ok to just call git, as git before calling a
350 # N.B.(skreft): it is ok to just call git, as git before calling a
351 # subcommand sets the PATH environment variable so that it point to the
351 # subcommand sets the PATH environment variable so that it point to the
352 # correct version of the git executable.
352 # correct version of the git executable.
353 empty_commit_id = '0' * 40
353 empty_commit_id = '0' * 40
354 for push_ref in rev_data:
354 for push_ref in rev_data:
355 type_ = push_ref['type']
355 type_ = push_ref['type']
356 if type_ == 'heads':
356 if type_ == 'heads':
357 if push_ref['old_rev'] == empty_commit_id:
357 if push_ref['old_rev'] == empty_commit_id:
358
358
359 # Fix up head revision if needed
359 # Fix up head revision if needed
360 cmd = ['git', 'show', 'HEAD']
360 cmd = ['git', 'show', 'HEAD']
361 try:
361 try:
362 _run_command(cmd)
362 _run_command(cmd)
363 except Exception:
363 except Exception:
364 cmd = ['git', 'symbolic-ref', 'HEAD',
364 cmd = ['git', 'symbolic-ref', 'HEAD',
365 'refs/heads/%s' % push_ref['name']]
365 'refs/heads/%s' % push_ref['name']]
366 print("Setting default branch to %s" % push_ref['name'])
366 print("Setting default branch to %s" % push_ref['name'])
367 _run_command(cmd)
367 _run_command(cmd)
368
368
369 cmd = ['git', 'for-each-ref', '--format=%(refname)',
369 cmd = ['git', 'for-each-ref', '--format=%(refname)',
370 'refs/heads/*']
370 'refs/heads/*']
371 heads = _run_command(cmd)
371 heads = _run_command(cmd)
372 heads = heads.replace(push_ref['ref'], '')
372 heads = heads.replace(push_ref['ref'], '')
373 heads = ' '.join(head for head in heads.splitlines() if head)
373 heads = ' '.join(head for head in heads.splitlines() if head)
374 cmd = ['git', 'log', '--reverse', '--pretty=format:%H',
374 cmd = ['git', 'log', '--reverse', '--pretty=format:%H',
375 '--', push_ref['new_rev'], '--not', heads]
375 '--', push_ref['new_rev'], '--not', heads]
376 git_revs.extend(_run_command(cmd).splitlines())
376 git_revs.extend(_run_command(cmd).splitlines())
377 elif push_ref['new_rev'] == empty_commit_id:
377 elif push_ref['new_rev'] == empty_commit_id:
378 # delete branch case
378 # delete branch case
379 git_revs.append('delete_branch=>%s' % push_ref['name'])
379 git_revs.append('delete_branch=>%s' % push_ref['name'])
380 else:
380 else:
381 cmd = ['git', 'log',
381 cmd = ['git', 'log',
382 '{old_rev}..{new_rev}'.format(**push_ref),
382 '{old_rev}..{new_rev}'.format(**push_ref),
383 '--reverse', '--pretty=format:%H']
383 '--reverse', '--pretty=format:%H']
384 git_revs.extend(_run_command(cmd).splitlines())
384 git_revs.extend(_run_command(cmd).splitlines())
385 elif type_ == 'tags':
385 elif type_ == 'tags':
386 git_revs.append('tag=>%s' % push_ref['name'])
386 git_revs.append('tag=>%s' % push_ref['name'])
387
387
388 extras['commit_ids'] = git_revs
388 extras['commit_ids'] = git_revs
389
389
390 if 'repo_size' in extras['hooks']:
390 if 'repo_size' in extras['hooks']:
391 try:
391 try:
392 _call_hook('repo_size', extras, GitMessageWriter())
392 _call_hook('repo_size', extras, GitMessageWriter())
393 except:
393 except:
394 pass
394 pass
395
395
396 return _call_hook('post_push', extras, GitMessageWriter())
396 return _call_hook('post_push', extras, GitMessageWriter())
General Comments 0
You need to be logged in to leave comments. Login now