##// END OF EJS Templates
core: deprecated http based callback deamon....
super-admin -
r1315:910dabad default
parent child Browse files
Show More
@@ -1,131 +1,131 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 """
18 """
19 Special exception handling over the wire.
19 Special exception handling over the wire.
20
20
21 Since we cannot assume that our client is able to import our exception classes,
21 Since we cannot assume that our client is able to import our exception classes,
22 this module provides a "wrapping" mechanism to raise plain exceptions
22 this module provides a "wrapping" mechanism to raise plain exceptions
23 which contain an extra attribute `_vcs_kind` to allow a client to distinguish
23 which contain an extra attribute `_vcs_kind` to allow a client to distinguish
24 different error conditions.
24 different error conditions.
25 """
25 """
26
26
27 from pyramid.httpexceptions import HTTPLocked, HTTPForbidden
27 from pyramid.httpexceptions import HTTPLocked, HTTPForbidden
28
28
29
29
30 def _make_exception(kind, org_exc, *args):
30 def _make_exception(kind, org_exc, *args):
31 """
31 """
32 Prepares a base `Exception` instance to be sent over the wire.
32 Prepares a base `Exception` instance to be sent over the wire.
33
33
34 To give our caller a hint what this is about, it will attach an attribute
34 To give our caller a hint what this is about, it will attach an attribute
35 `_vcs_kind` to the exception.
35 `_vcs_kind` to the exception.
36 """
36 """
37 exc = Exception(*args)
37 exc = Exception(*args)
38 exc._vcs_kind = kind
38 exc._vcs_kind = kind
39 exc._org_exc = org_exc
39 exc._org_exc = org_exc
40 exc._org_exc_tb = getattr(org_exc, '_org_exc_tb', '')
40 exc._org_exc_tb = getattr(org_exc, '_org_exc_tb', '')
41 return exc
41 return exc
42
42
43
43
44 def AbortException(org_exc=None):
44 def AbortException(org_exc=None):
45 def _make_exception_wrapper(*args):
45 def _make_exception_wrapper(*args):
46 return _make_exception('abort', org_exc, *args)
46 return _make_exception('abort', org_exc, *args)
47 return _make_exception_wrapper
47 return _make_exception_wrapper
48
48
49
49
50 def ArchiveException(org_exc=None):
50 def ArchiveException(org_exc=None):
51 def _make_exception_wrapper(*args):
51 def _make_exception_wrapper(*args):
52 return _make_exception('archive', org_exc, *args)
52 return _make_exception('archive', org_exc, *args)
53 return _make_exception_wrapper
53 return _make_exception_wrapper
54
54
55
55
56 def ClientNotSupportedException(org_exc=None):
56 def ClientNotSupportedException(org_exc=None):
57 def _make_exception_wrapper(*args):
57 def _make_exception_wrapper(*args):
58 return _make_exception('client_not_supported', org_exc, *args)
58 return _make_exception('client_not_supported', org_exc, *args)
59 return _make_exception_wrapper
59 return _make_exception_wrapper
60
60
61
61
62 def LookupException(org_exc=None):
62 def LookupException(org_exc=None):
63 def _make_exception_wrapper(*args):
63 def _make_exception_wrapper(*args):
64 return _make_exception('lookup', org_exc, *args)
64 return _make_exception('lookup', org_exc, *args)
65 return _make_exception_wrapper
65 return _make_exception_wrapper
66
66
67
67
68 def VcsException(org_exc=None):
68 def VcsException(org_exc=None):
69 def _make_exception_wrapper(*args):
69 def _make_exception_wrapper(*args):
70 return _make_exception('error', org_exc, *args)
70 return _make_exception('error', org_exc, *args)
71 return _make_exception_wrapper
71 return _make_exception_wrapper
72
72
73
73
74 def RepositoryLockedException(org_exc=None):
74 def LockedRepoException(org_exc=None):
75 def _make_exception_wrapper(*args):
75 def _make_exception_wrapper(*args):
76 return _make_exception('repo_locked', org_exc, *args)
76 return _make_exception('repo_locked', org_exc, *args)
77 return _make_exception_wrapper
77 return _make_exception_wrapper
78
78
79
79
80 def RepositoryBranchProtectedException(org_exc=None):
80 def RepositoryBranchProtectedException(org_exc=None):
81 def _make_exception_wrapper(*args):
81 def _make_exception_wrapper(*args):
82 return _make_exception('repo_branch_protected', org_exc, *args)
82 return _make_exception('repo_branch_protected', org_exc, *args)
83 return _make_exception_wrapper
83 return _make_exception_wrapper
84
84
85
85
86 def RequirementException(org_exc=None):
86 def RequirementException(org_exc=None):
87 def _make_exception_wrapper(*args):
87 def _make_exception_wrapper(*args):
88 return _make_exception('requirement', org_exc, *args)
88 return _make_exception('requirement', org_exc, *args)
89 return _make_exception_wrapper
89 return _make_exception_wrapper
90
90
91
91
92 def UnhandledException(org_exc=None):
92 def UnhandledException(org_exc=None):
93 def _make_exception_wrapper(*args):
93 def _make_exception_wrapper(*args):
94 return _make_exception('unhandled', org_exc, *args)
94 return _make_exception('unhandled', org_exc, *args)
95 return _make_exception_wrapper
95 return _make_exception_wrapper
96
96
97
97
98 def URLError(org_exc=None):
98 def URLError(org_exc=None):
99 def _make_exception_wrapper(*args):
99 def _make_exception_wrapper(*args):
100 return _make_exception('url_error', org_exc, *args)
100 return _make_exception('url_error', org_exc, *args)
101 return _make_exception_wrapper
101 return _make_exception_wrapper
102
102
103
103
104 def SubrepoMergeException(org_exc=None):
104 def SubrepoMergeException(org_exc=None):
105 def _make_exception_wrapper(*args):
105 def _make_exception_wrapper(*args):
106 return _make_exception('subrepo_merge_error', org_exc, *args)
106 return _make_exception('subrepo_merge_error', org_exc, *args)
107 return _make_exception_wrapper
107 return _make_exception_wrapper
108
108
109
109
110 class HTTPRepoLocked(HTTPLocked):
110 class HTTPRepoLocked(HTTPLocked):
111 """
111 """
112 Subclass of HTTPLocked response that allows to set the title and status
112 Subclass of HTTPLocked response that allows to set the title and status
113 code via constructor arguments.
113 code via constructor arguments.
114 """
114 """
115 def __init__(self, title, status_code=None, **kwargs):
115 def __init__(self, title, status_code=None, **kwargs):
116 self.code = status_code or HTTPLocked.code
116 self.code = status_code or HTTPLocked.code
117 self.title = title
117 self.title = title
118 super().__init__(**kwargs)
118 super().__init__(**kwargs)
119
119
120
120
121 class HTTPRepoBranchProtected(HTTPForbidden):
121 class HTTPRepoBranchProtected(HTTPForbidden):
122 def __init__(self, *args, **kwargs):
122 def __init__(self, *args, **kwargs):
123 super(HTTPForbidden, self).__init__(*args, **kwargs)
123 super(HTTPForbidden, self).__init__(*args, **kwargs)
124
124
125
125
126 class RefNotFoundException(KeyError):
126 class RefNotFoundException(KeyError):
127 pass
127 pass
128
128
129
129
130 class NoContentException(ValueError):
130 class NoContentException(ValueError):
131 pass
131 pass
@@ -1,828 +1,780 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 base64
23 import base64
24 import msgpack
24 import msgpack
25 import dataclasses
25 import dataclasses
26 import pygit2
26 import pygit2
27
27
28 import http.client
28 import http.client
29 from celery import Celery
29 from celery import Celery
30
30
31 import mercurial.scmutil
31 import mercurial.scmutil
32 import mercurial.node
32 import mercurial.node
33
33
34 from vcsserver import exceptions, subprocessio, settings
34 from vcsserver import exceptions, subprocessio, settings
35 from vcsserver.lib.ext_json import json
35 from vcsserver.lib.ext_json import json
36 from vcsserver.lib.str_utils import ascii_str, safe_str
36 from vcsserver.lib.str_utils import ascii_str, safe_str
37 from vcsserver.lib.svn_txn_utils import get_txn_id_from_store
37 from vcsserver.lib.svn_txn_utils import get_txn_id_from_store
38 from vcsserver.remote.git_remote import Repository
38 from vcsserver.remote.git_remote import Repository
39
39
40 celery_app = Celery('__vcsserver__')
40 celery_app = Celery('__vcsserver__')
41 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
42
42
43
43
44 class HooksHttpClient:
45 proto = 'msgpack.v1'
46 connection = None
47
48 def __init__(self, hooks_uri):
49 self.hooks_uri = hooks_uri
50
51 def __repr__(self):
52 return f'{self.__class__}(hook_uri={self.hooks_uri}, proto={self.proto})'
53
54 def __call__(self, method, extras):
55 connection = http.client.HTTPConnection(self.hooks_uri)
56 # binary msgpack body
57 headers, body = self._serialize(method, extras)
58 log.debug('Doing a new hooks call using HTTPConnection to %s', self.hooks_uri)
59
60 try:
61 try:
62 connection.request('POST', '/', body, headers)
63 except Exception as error:
64 log.error('Hooks calling Connection failed on %s, org error: %s', connection.__dict__, error)
65 raise
66
67 response = connection.getresponse()
68 try:
69 return msgpack.load(response)
70 except Exception:
71 response_data = response.read()
72 log.exception('Failed to decode hook response json data. '
73 'response_code:%s, raw_data:%s',
74 response.status, response_data)
75 raise
76 finally:
77 connection.close()
78
79 @classmethod
80 def _serialize(cls, hook_name, extras):
81 data = {
82 'method': hook_name,
83 'extras': extras
84 }
85 headers = {
86 "rc-hooks-protocol": cls.proto,
87 "Connection": "keep-alive"
88 }
89 return headers, msgpack.packb(data)
90
91
92 class HooksCeleryClient:
44 class HooksCeleryClient:
93 TASK_TIMEOUT = 60 # time in seconds
45 TASK_TIMEOUT = 60 # time in seconds
94
46
95 def __init__(self, queue, backend):
47 def __init__(self, queue, backend):
96 celery_app.config_from_object({
48 celery_app.config_from_object({
97 'broker_url': queue, 'result_backend': backend,
49 'broker_url': queue, 'result_backend': backend,
98 'broker_connection_retry_on_startup': True,
50 'broker_connection_retry_on_startup': True,
99 'task_serializer': 'json',
51 'task_serializer': 'json',
100 'accept_content': ['json', 'msgpack'],
52 'accept_content': ['json', 'msgpack'],
101 'result_serializer': 'json',
53 'result_serializer': 'json',
102 'result_accept_content': ['json', 'msgpack']
54 'result_accept_content': ['json', 'msgpack']
103 })
55 })
104 self.celery_app = celery_app
56 self.celery_app = celery_app
105
57
106 def __call__(self, method, extras):
58 def __call__(self, method, extras):
107 inquired_task = self.celery_app.signature(
59 inquired_task = self.celery_app.signature(
108 f'rhodecode.lib.celerylib.tasks.{method}'
60 f'rhodecode.lib.celerylib.tasks.{method}'
109 )
61 )
110 result = inquired_task.delay(extras).get(timeout=self.TASK_TIMEOUT)
62 result = inquired_task.delay(extras).get(timeout=self.TASK_TIMEOUT)
111
63
112 return result
64 return result
113
65
114
66
115 class HooksShadowRepoClient:
67 class HooksShadowRepoClient:
116
68
117 def __call__(self, hook_name, extras):
69 def __call__(self, hook_name, extras):
118 return {'output': '', 'status': 0}
70 return {'output': '', 'status': 0}
119
71
120
72
121 class RemoteMessageWriter:
73 class RemoteMessageWriter:
122 """Writer base class."""
74 """Writer base class."""
123 def write(self, message):
75 def write(self, message):
124 raise NotImplementedError()
76 raise NotImplementedError()
125
77
126
78
127 class HgMessageWriter(RemoteMessageWriter):
79 class HgMessageWriter(RemoteMessageWriter):
128 """Writer that knows how to send messages to mercurial clients."""
80 """Writer that knows how to send messages to mercurial clients."""
129
81
130 def __init__(self, ui):
82 def __init__(self, ui):
131 self.ui = ui
83 self.ui = ui
132
84
133 def write(self, message: str):
85 def write(self, message: str):
134 # TODO: Check why the quiet flag is set by default.
86 # TODO: Check why the quiet flag is set by default.
135 old = self.ui.quiet
87 old = self.ui.quiet
136 self.ui.quiet = False
88 self.ui.quiet = False
137 self.ui.status(message.encode('utf-8'))
89 self.ui.status(message.encode('utf-8'))
138 self.ui.quiet = old
90 self.ui.quiet = old
139
91
140
92
141 class GitMessageWriter(RemoteMessageWriter):
93 class GitMessageWriter(RemoteMessageWriter):
142 """Writer that knows how to send messages to git clients."""
94 """Writer that knows how to send messages to git clients."""
143
95
144 def __init__(self, stdout=None):
96 def __init__(self, stdout=None):
145 self.stdout = stdout or sys.stdout
97 self.stdout = stdout or sys.stdout
146
98
147 def write(self, message: str):
99 def write(self, message: str):
148 self.stdout.write(message)
100 self.stdout.write(message)
149
101
150
102
151 class SvnMessageWriter(RemoteMessageWriter):
103 class SvnMessageWriter(RemoteMessageWriter):
152 """Writer that knows how to send messages to svn clients."""
104 """Writer that knows how to send messages to svn clients."""
153
105
154 def __init__(self, stderr=None):
106 def __init__(self, stderr=None):
155 # SVN needs data sent to stderr for back-to-client messaging
107 # SVN needs data sent to stderr for back-to-client messaging
156 self.stderr = stderr or sys.stderr
108 self.stderr = stderr or sys.stderr
157
109
158 def write(self, message):
110 def write(self, message):
159 self.stderr.write(message)
111 self.stderr.write(message)
160
112
161
113
162 def _maybe_handle_exception(result):
114 def _maybe_handle_exception(result):
115
116
163 exception_class = result.get('exception')
117 exception_class = result.get('exception')
164 exception_traceback = result.get('exception_traceback')
118 exception_traceback = result.get('exception_traceback')
165 if not (exception_class and exception_traceback):
119 if not exception_class:
166 return
120 return
121
167 log.debug('Handling hook-call exception: %s', exception_class)
122 log.debug('Handling hook-call exception: %s', exception_class)
168
123
169 if exception_traceback:
124 if exception_traceback:
170 log.error('Got traceback from remote call:%s', exception_traceback)
125 log.error('Got traceback from remote call:%s', exception_traceback)
171
126
172 if exception_class == 'HTTPLockedRC':
127 if exception_class == 'HTTPLockedRepo':
173 raise exceptions.RepositoryLockedException()(*result['exception_args'])
128 raise exceptions.LockedRepoException()(*result['exception_args'])
174 elif exception_class == 'ClientNotSupportedError':
129 elif exception_class == 'ClientNotSupportedError':
175 raise exceptions.ClientNotSupportedException()(*result['exception_args'])
130 raise exceptions.ClientNotSupportedException()(*result['exception_args'])
176 elif exception_class == 'HTTPBranchProtected':
131 elif exception_class == 'HTTPBranchProtected':
177 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
132 raise exceptions.RepositoryBranchProtectedException()(*result['exception_args'])
178 elif exception_class == 'RepositoryError':
133 elif exception_class == 'RepositoryError':
179 raise exceptions.VcsException()(*result['exception_args'])
134 raise exceptions.VcsException()(*result['exception_args'])
180 elif exception_class:
135 elif exception_class:
181 raise Exception(
136 raise Exception(
182 f"""Got remote exception "{exception_class}" with args "{result['exception_args']}" """
137 f"""Got remote exception "{exception_class}" with args "{result['exception_args']}" """
183 )
138 )
184
139
185
140
186 def _get_hooks_client(extras):
141 def _get_hooks_client(extras):
187 hooks_uri = extras.get('hooks_uri')
188 task_queue = extras.get('task_queue')
142 task_queue = extras.get('task_queue')
189 task_backend = extras.get('task_backend')
143 task_backend = extras.get('task_backend')
190 is_shadow_repo = extras.get('is_shadow_repo')
144 is_shadow_repo = extras.get('is_shadow_repo')
191
145
192 if hooks_uri:
146 if task_queue and task_backend:
193 return HooksHttpClient(hooks_uri)
194 elif task_queue and task_backend:
195 return HooksCeleryClient(task_queue, task_backend)
147 return HooksCeleryClient(task_queue, task_backend)
196 elif is_shadow_repo:
148 elif is_shadow_repo:
197 return HooksShadowRepoClient()
149 return HooksShadowRepoClient()
198 else:
150 else:
199 raise Exception("Hooks client not found!")
151 raise Exception("Hooks client not found!")
200
152
201
153
202 def _call_hook(hook_name, extras, writer):
154 def _call_hook(hook_name, extras, writer):
203 hooks_client = _get_hooks_client(extras)
155 hooks_client = _get_hooks_client(extras)
204 log.debug('Hooks, using client:%s', hooks_client)
156 log.debug('Hooks, using client:%s', hooks_client)
205 result = hooks_client(hook_name, extras)
157 result = hooks_client(hook_name, extras)
206 log.debug('Hooks got result: %s', result)
158 log.debug('Hooks got result: %s', result)
207 _maybe_handle_exception(result)
159 _maybe_handle_exception(result)
208 writer.write(result['output'])
160 writer.write(result['output'])
209
161
210 return result['status']
162 return result['status']
211
163
212
164
213 def _extras_from_ui(ui):
165 def _extras_from_ui(ui):
214 hook_data = ui.config(b'rhodecode', b'RC_SCM_DATA')
166 hook_data = ui.config(b'rhodecode', b'RC_SCM_DATA')
215 if not hook_data:
167 if not hook_data:
216 # maybe it's inside environ ?
168 # maybe it's inside environ ?
217 env_hook_data = os.environ.get('RC_SCM_DATA')
169 env_hook_data = os.environ.get('RC_SCM_DATA')
218 if env_hook_data:
170 if env_hook_data:
219 hook_data = env_hook_data
171 hook_data = env_hook_data
220
172
221 extras = {}
173 extras = {}
222 if hook_data:
174 if hook_data:
223 extras = json.loads(hook_data)
175 extras = json.loads(hook_data)
224 return extras
176 return extras
225
177
226
178
227 def _rev_range_hash(repo, node, check_heads=False):
179 def _rev_range_hash(repo, node, check_heads=False):
228 from vcsserver.hgcompat import get_ctx
180 from vcsserver.hgcompat import get_ctx
229
181
230 commits = []
182 commits = []
231 revs = []
183 revs = []
232 start = get_ctx(repo, node).rev()
184 start = get_ctx(repo, node).rev()
233 end = len(repo)
185 end = len(repo)
234 for rev in range(start, end):
186 for rev in range(start, end):
235 revs.append(rev)
187 revs.append(rev)
236 ctx = get_ctx(repo, rev)
188 ctx = get_ctx(repo, rev)
237 commit_id = ascii_str(mercurial.node.hex(ctx.node()))
189 commit_id = ascii_str(mercurial.node.hex(ctx.node()))
238 branch = safe_str(ctx.branch())
190 branch = safe_str(ctx.branch())
239 commits.append((commit_id, branch))
191 commits.append((commit_id, branch))
240
192
241 parent_heads = []
193 parent_heads = []
242 if check_heads:
194 if check_heads:
243 parent_heads = _check_heads(repo, start, end, revs)
195 parent_heads = _check_heads(repo, start, end, revs)
244 return commits, parent_heads
196 return commits, parent_heads
245
197
246
198
247 def _check_heads(repo, start, end, commits):
199 def _check_heads(repo, start, end, commits):
248 from vcsserver.hgcompat import get_ctx
200 from vcsserver.hgcompat import get_ctx
249 changelog = repo.changelog
201 changelog = repo.changelog
250 parents = set()
202 parents = set()
251
203
252 for new_rev in commits:
204 for new_rev in commits:
253 for p in changelog.parentrevs(new_rev):
205 for p in changelog.parentrevs(new_rev):
254 if p == mercurial.node.nullrev:
206 if p == mercurial.node.nullrev:
255 continue
207 continue
256 if p < start:
208 if p < start:
257 parents.add(p)
209 parents.add(p)
258
210
259 for p in parents:
211 for p in parents:
260 branch = get_ctx(repo, p).branch()
212 branch = get_ctx(repo, p).branch()
261 # The heads descending from that parent, on the same branch
213 # The heads descending from that parent, on the same branch
262 parent_heads = {p}
214 parent_heads = {p}
263 reachable = {p}
215 reachable = {p}
264 for x in range(p + 1, end):
216 for x in range(p + 1, end):
265 if get_ctx(repo, x).branch() != branch:
217 if get_ctx(repo, x).branch() != branch:
266 continue
218 continue
267 for pp in changelog.parentrevs(x):
219 for pp in changelog.parentrevs(x):
268 if pp in reachable:
220 if pp in reachable:
269 reachable.add(x)
221 reachable.add(x)
270 parent_heads.discard(pp)
222 parent_heads.discard(pp)
271 parent_heads.add(x)
223 parent_heads.add(x)
272 # More than one head? Suggest merging
224 # More than one head? Suggest merging
273 if len(parent_heads) > 1:
225 if len(parent_heads) > 1:
274 return list(parent_heads)
226 return list(parent_heads)
275
227
276 return []
228 return []
277
229
278
230
279 def _get_git_env():
231 def _get_git_env():
280 env = {}
232 env = {}
281 for k, v in os.environ.items():
233 for k, v in os.environ.items():
282 if k.startswith('GIT'):
234 if k.startswith('GIT'):
283 env[k] = v
235 env[k] = v
284
236
285 # serialized version
237 # serialized version
286 return [(k, v) for k, v in env.items()]
238 return [(k, v) for k, v in env.items()]
287
239
288
240
289 def _get_hg_env(old_rev, new_rev, txnid, repo_path):
241 def _get_hg_env(old_rev, new_rev, txnid, repo_path):
290 env = {}
242 env = {}
291 for k, v in os.environ.items():
243 for k, v in os.environ.items():
292 if k.startswith('HG'):
244 if k.startswith('HG'):
293 env[k] = v
245 env[k] = v
294
246
295 env['HG_NODE'] = old_rev
247 env['HG_NODE'] = old_rev
296 env['HG_NODE_LAST'] = new_rev
248 env['HG_NODE_LAST'] = new_rev
297 env['HG_TXNID'] = txnid
249 env['HG_TXNID'] = txnid
298 env['HG_PENDING'] = repo_path
250 env['HG_PENDING'] = repo_path
299
251
300 return [(k, v) for k, v in env.items()]
252 return [(k, v) for k, v in env.items()]
301
253
302
254
303 def _get_ini_settings(ini_file):
255 def _get_ini_settings(ini_file):
304 from vcsserver.http_main import sanitize_settings_and_apply_defaults
256 from vcsserver.http_main import sanitize_settings_and_apply_defaults
305 from vcsserver.lib.config_utils import get_app_config_lightweight, configure_and_store_settings
257 from vcsserver.lib.config_utils import get_app_config_lightweight, configure_and_store_settings
306
258
307 global_config = {'__file__': ini_file}
259 global_config = {'__file__': ini_file}
308 ini_settings = get_app_config_lightweight(ini_file)
260 ini_settings = get_app_config_lightweight(ini_file)
309 sanitize_settings_and_apply_defaults(global_config, ini_settings)
261 sanitize_settings_and_apply_defaults(global_config, ini_settings)
310 configure_and_store_settings(global_config, ini_settings)
262 configure_and_store_settings(global_config, ini_settings)
311
263
312 return ini_settings
264 return ini_settings
313
265
314
266
315 def _fix_hooks_executables(ini_path=''):
267 def _fix_hooks_executables(ini_path=''):
316 """
268 """
317 This is a trick to set proper settings.EXECUTABLE paths for certain execution patterns
269 This is a trick to set proper settings.EXECUTABLE paths for certain execution patterns
318 especially for subversion where hooks strip entire env, and calling just 'svn' command will most likely fail
270 especially for subversion where hooks strip entire env, and calling just 'svn' command will most likely fail
319 because svn is not on PATH
271 because svn is not on PATH
320 """
272 """
321 # set defaults, in case we can't read from ini_file
273 # set defaults, in case we can't read from ini_file
322 core_binary_dir = settings.BINARY_DIR or '/usr/local/bin/rhodecode_bin/vcs_bin'
274 core_binary_dir = settings.BINARY_DIR or '/usr/local/bin/rhodecode_bin/vcs_bin'
323 if ini_path:
275 if ini_path:
324 ini_settings = _get_ini_settings(ini_path)
276 ini_settings = _get_ini_settings(ini_path)
325 core_binary_dir = ini_settings['core.binary_dir']
277 core_binary_dir = ini_settings['core.binary_dir']
326
278
327 settings.BINARY_DIR = core_binary_dir
279 settings.BINARY_DIR = core_binary_dir
328
280
329
281
330 def repo_size(ui, repo, **kwargs):
282 def repo_size(ui, repo, **kwargs):
331 extras = _extras_from_ui(ui)
283 extras = _extras_from_ui(ui)
332 return _call_hook('repo_size', extras, HgMessageWriter(ui))
284 return _call_hook('repo_size', extras, HgMessageWriter(ui))
333
285
334
286
335 def pre_pull(ui, repo, **kwargs):
287 def pre_pull(ui, repo, **kwargs):
336 extras = _extras_from_ui(ui)
288 extras = _extras_from_ui(ui)
337 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
289 return _call_hook('pre_pull', extras, HgMessageWriter(ui))
338
290
339
291
340 def pre_pull_ssh(ui, repo, **kwargs):
292 def pre_pull_ssh(ui, repo, **kwargs):
341 extras = _extras_from_ui(ui)
293 extras = _extras_from_ui(ui)
342 if extras and extras.get('SSH'):
294 if extras and extras.get('SSH'):
343 return pre_pull(ui, repo, **kwargs)
295 return pre_pull(ui, repo, **kwargs)
344 return 0
296 return 0
345
297
346
298
347 def post_pull(ui, repo, **kwargs):
299 def post_pull(ui, repo, **kwargs):
348 extras = _extras_from_ui(ui)
300 extras = _extras_from_ui(ui)
349 return _call_hook('post_pull', extras, HgMessageWriter(ui))
301 return _call_hook('post_pull', extras, HgMessageWriter(ui))
350
302
351
303
352 def post_pull_ssh(ui, repo, **kwargs):
304 def post_pull_ssh(ui, repo, **kwargs):
353 extras = _extras_from_ui(ui)
305 extras = _extras_from_ui(ui)
354 if extras and extras.get('SSH'):
306 if extras and extras.get('SSH'):
355 return post_pull(ui, repo, **kwargs)
307 return post_pull(ui, repo, **kwargs)
356 return 0
308 return 0
357
309
358
310
359 def pre_push(ui, repo, node=None, **kwargs):
311 def pre_push(ui, repo, node=None, **kwargs):
360 """
312 """
361 Mercurial pre_push hook
313 Mercurial pre_push hook
362 """
314 """
363 extras = _extras_from_ui(ui)
315 extras = _extras_from_ui(ui)
364 detect_force_push = extras.get('detect_force_push')
316 detect_force_push = extras.get('detect_force_push')
365
317
366 rev_data = []
318 rev_data = []
367 hook_type: str = safe_str(kwargs.get('hooktype'))
319 hook_type: str = safe_str(kwargs.get('hooktype'))
368
320
369 if node and hook_type == 'pretxnchangegroup':
321 if node and hook_type == 'pretxnchangegroup':
370 branches = collections.defaultdict(list)
322 branches = collections.defaultdict(list)
371 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
323 commits, _heads = _rev_range_hash(repo, node, check_heads=detect_force_push)
372 for commit_id, branch in commits:
324 for commit_id, branch in commits:
373 branches[branch].append(commit_id)
325 branches[branch].append(commit_id)
374
326
375 for branch, commits in branches.items():
327 for branch, commits in branches.items():
376 old_rev = ascii_str(kwargs.get('node_last')) or commits[0]
328 old_rev = ascii_str(kwargs.get('node_last')) or commits[0]
377 rev_data.append({
329 rev_data.append({
378 'total_commits': len(commits),
330 'total_commits': len(commits),
379 'old_rev': old_rev,
331 'old_rev': old_rev,
380 'new_rev': commits[-1],
332 'new_rev': commits[-1],
381 'ref': '',
333 'ref': '',
382 'type': 'branch',
334 'type': 'branch',
383 'name': branch,
335 'name': branch,
384 })
336 })
385
337
386 for push_ref in rev_data:
338 for push_ref in rev_data:
387 push_ref['multiple_heads'] = _heads
339 push_ref['multiple_heads'] = _heads
388
340
389 repo_path = os.path.join(
341 repo_path = os.path.join(
390 extras.get('repo_store', ''), extras.get('repository', ''))
342 extras.get('repo_store', ''), extras.get('repository', ''))
391 push_ref['hg_env'] = _get_hg_env(
343 push_ref['hg_env'] = _get_hg_env(
392 old_rev=push_ref['old_rev'],
344 old_rev=push_ref['old_rev'],
393 new_rev=push_ref['new_rev'], txnid=ascii_str(kwargs.get('txnid')),
345 new_rev=push_ref['new_rev'], txnid=ascii_str(kwargs.get('txnid')),
394 repo_path=repo_path)
346 repo_path=repo_path)
395
347
396 extras['hook_type'] = hook_type or 'pre_push'
348 extras['hook_type'] = hook_type or 'pre_push'
397 extras['commit_ids'] = rev_data
349 extras['commit_ids'] = rev_data
398
350
399 return _call_hook('pre_push', extras, HgMessageWriter(ui))
351 return _call_hook('pre_push', extras, HgMessageWriter(ui))
400
352
401
353
402 def pre_push_ssh(ui, repo, node=None, **kwargs):
354 def pre_push_ssh(ui, repo, node=None, **kwargs):
403 extras = _extras_from_ui(ui)
355 extras = _extras_from_ui(ui)
404 if extras.get('SSH'):
356 if extras.get('SSH'):
405 return pre_push(ui, repo, node, **kwargs)
357 return pre_push(ui, repo, node, **kwargs)
406
358
407 return 0
359 return 0
408
360
409
361
410 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
362 def pre_push_ssh_auth(ui, repo, node=None, **kwargs):
411 """
363 """
412 Mercurial pre_push hook for SSH
364 Mercurial pre_push hook for SSH
413 """
365 """
414 extras = _extras_from_ui(ui)
366 extras = _extras_from_ui(ui)
415 if extras.get('SSH'):
367 if extras.get('SSH'):
416 permission = extras['SSH_PERMISSIONS']
368 permission = extras['SSH_PERMISSIONS']
417
369
418 if 'repository.write' == permission or 'repository.admin' == permission:
370 if 'repository.write' == permission or 'repository.admin' == permission:
419 return 0
371 return 0
420
372
421 # non-zero ret code
373 # non-zero ret code
422 return 1
374 return 1
423
375
424 return 0
376 return 0
425
377
426
378
427 def post_push(ui, repo, node, **kwargs):
379 def post_push(ui, repo, node, **kwargs):
428 """
380 """
429 Mercurial post_push hook
381 Mercurial post_push hook
430 """
382 """
431 extras = _extras_from_ui(ui)
383 extras = _extras_from_ui(ui)
432
384
433 commit_ids = []
385 commit_ids = []
434 branches = []
386 branches = []
435 bookmarks = []
387 bookmarks = []
436 tags = []
388 tags = []
437 hook_type: str = safe_str(kwargs.get('hooktype'))
389 hook_type: str = safe_str(kwargs.get('hooktype'))
438
390
439 commits, _heads = _rev_range_hash(repo, node)
391 commits, _heads = _rev_range_hash(repo, node)
440 for commit_id, branch in commits:
392 for commit_id, branch in commits:
441 commit_ids.append(commit_id)
393 commit_ids.append(commit_id)
442 if branch not in branches:
394 if branch not in branches:
443 branches.append(branch)
395 branches.append(branch)
444
396
445 if hasattr(ui, '_rc_pushkey_bookmarks'):
397 if hasattr(ui, '_rc_pushkey_bookmarks'):
446 bookmarks = ui._rc_pushkey_bookmarks
398 bookmarks = ui._rc_pushkey_bookmarks
447
399
448 extras['hook_type'] = hook_type or 'post_push'
400 extras['hook_type'] = hook_type or 'post_push'
449 extras['commit_ids'] = commit_ids
401 extras['commit_ids'] = commit_ids
450
402
451 extras['new_refs'] = {
403 extras['new_refs'] = {
452 'branches': branches,
404 'branches': branches,
453 'bookmarks': bookmarks,
405 'bookmarks': bookmarks,
454 'tags': tags
406 'tags': tags
455 }
407 }
456
408
457 return _call_hook('post_push', extras, HgMessageWriter(ui))
409 return _call_hook('post_push', extras, HgMessageWriter(ui))
458
410
459
411
460 def post_push_ssh(ui, repo, node, **kwargs):
412 def post_push_ssh(ui, repo, node, **kwargs):
461 """
413 """
462 Mercurial post_push hook for SSH
414 Mercurial post_push hook for SSH
463 """
415 """
464 if _extras_from_ui(ui).get('SSH'):
416 if _extras_from_ui(ui).get('SSH'):
465 return post_push(ui, repo, node, **kwargs)
417 return post_push(ui, repo, node, **kwargs)
466 return 0
418 return 0
467
419
468
420
469 def key_push(ui, repo, **kwargs):
421 def key_push(ui, repo, **kwargs):
470 from vcsserver.hgcompat import get_ctx
422 from vcsserver.hgcompat import get_ctx
471
423
472 if kwargs['new'] != b'0' and kwargs['namespace'] == b'bookmarks':
424 if kwargs['new'] != b'0' and kwargs['namespace'] == b'bookmarks':
473 # store new bookmarks in our UI object propagated later to post_push
425 # store new bookmarks in our UI object propagated later to post_push
474 ui._rc_pushkey_bookmarks = get_ctx(repo, kwargs['key']).bookmarks()
426 ui._rc_pushkey_bookmarks = get_ctx(repo, kwargs['key']).bookmarks()
475 return
427 return
476
428
477
429
478 # backward compat
430 # backward compat
479 log_pull_action = post_pull
431 log_pull_action = post_pull
480
432
481 # backward compat
433 # backward compat
482 log_push_action = post_push
434 log_push_action = post_push
483
435
484
436
485 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
437 def handle_git_pre_receive(unused_repo_path, unused_revs, unused_env):
486 """
438 """
487 Old hook name: keep here for backward compatibility.
439 Old hook name: keep here for backward compatibility.
488
440
489 This is only required when the installed git hooks are not upgraded.
441 This is only required when the installed git hooks are not upgraded.
490 """
442 """
491 pass
443 pass
492
444
493
445
494 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
446 def handle_git_post_receive(unused_repo_path, unused_revs, unused_env):
495 """
447 """
496 Old hook name: keep here for backward compatibility.
448 Old hook name: keep here for backward compatibility.
497
449
498 This is only required when the installed git hooks are not upgraded.
450 This is only required when the installed git hooks are not upgraded.
499 """
451 """
500 pass
452 pass
501
453
502
454
503 @dataclasses.dataclass
455 @dataclasses.dataclass
504 class HookResponse:
456 class HookResponse:
505 status: int
457 status: int
506 output: str
458 output: str
507
459
508
460
509 def git_pre_pull(extras) -> HookResponse:
461 def git_pre_pull(extras) -> HookResponse:
510 """
462 """
511 Pre pull hook.
463 Pre pull hook.
512
464
513 :param extras: dictionary containing the keys defined in simplevcs
465 :param extras: dictionary containing the keys defined in simplevcs
514 :type extras: dict
466 :type extras: dict
515
467
516 :return: status code of the hook. 0 for success.
468 :return: status code of the hook. 0 for success.
517 :rtype: int
469 :rtype: int
518 """
470 """
519
471
520 if 'pull' not in extras['hooks']:
472 if 'pull' not in extras['hooks']:
521 return HookResponse(0, '')
473 return HookResponse(0, '')
522
474
523 stdout = io.StringIO()
475 stdout = io.StringIO()
524 try:
476 try:
525 status_code = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
477 status_code = _call_hook('pre_pull', extras, GitMessageWriter(stdout))
526
478
527 except Exception as error:
479 except Exception as error:
528 log.exception('Failed to call pre_pull hook')
480 log.exception('Failed to call pre_pull hook')
529 status_code = 128
481 status_code = 128
530 stdout.write(f'ERROR: {error}\n')
482 stdout.write(f'ERROR: {error}\n')
531
483
532 return HookResponse(status_code, stdout.getvalue())
484 return HookResponse(status_code, stdout.getvalue())
533
485
534
486
535 def git_post_pull(extras) -> HookResponse:
487 def git_post_pull(extras) -> HookResponse:
536 """
488 """
537 Post pull hook.
489 Post pull hook.
538
490
539 :param extras: dictionary containing the keys defined in simplevcs
491 :param extras: dictionary containing the keys defined in simplevcs
540 :type extras: dict
492 :type extras: dict
541
493
542 :return: status code of the hook. 0 for success.
494 :return: status code of the hook. 0 for success.
543 :rtype: int
495 :rtype: int
544 """
496 """
545 if 'pull' not in extras['hooks']:
497 if 'pull' not in extras['hooks']:
546 return HookResponse(0, '')
498 return HookResponse(0, '')
547
499
548 stdout = io.StringIO()
500 stdout = io.StringIO()
549 try:
501 try:
550 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
502 status = _call_hook('post_pull', extras, GitMessageWriter(stdout))
551 except Exception as error:
503 except Exception as error:
552 status = 128
504 status = 128
553 stdout.write(f'ERROR: {error}\n')
505 stdout.write(f'ERROR: {error}\n')
554
506
555 return HookResponse(status, stdout.getvalue())
507 return HookResponse(status, stdout.getvalue())
556
508
557
509
558 def _parse_git_ref_lines(revision_lines):
510 def _parse_git_ref_lines(revision_lines):
559 rev_data = []
511 rev_data = []
560 for revision_line in revision_lines or []:
512 for revision_line in revision_lines or []:
561 old_rev, new_rev, ref = revision_line.strip().split(' ')
513 old_rev, new_rev, ref = revision_line.strip().split(' ')
562 ref_data = ref.split('/', 2)
514 ref_data = ref.split('/', 2)
563 if ref_data[1] in ('tags', 'heads'):
515 if ref_data[1] in ('tags', 'heads'):
564 rev_data.append({
516 rev_data.append({
565 # NOTE(marcink):
517 # NOTE(marcink):
566 # we're unable to tell total_commits for git at this point
518 # we're unable to tell total_commits for git at this point
567 # but we set the variable for consistency with GIT
519 # but we set the variable for consistency with GIT
568 'total_commits': -1,
520 'total_commits': -1,
569 'old_rev': old_rev,
521 'old_rev': old_rev,
570 'new_rev': new_rev,
522 'new_rev': new_rev,
571 'ref': ref,
523 'ref': ref,
572 'type': ref_data[1],
524 'type': ref_data[1],
573 'name': ref_data[2],
525 'name': ref_data[2],
574 })
526 })
575 return rev_data
527 return rev_data
576
528
577
529
578 def git_pre_receive(unused_repo_path, revision_lines, env) -> int:
530 def git_pre_receive(unused_repo_path, revision_lines, env) -> int:
579 """
531 """
580 Pre push hook.
532 Pre push hook.
581
533
582 :return: status code of the hook. 0 for success.
534 :return: status code of the hook. 0 for success.
583 """
535 """
584 extras = json.loads(env['RC_SCM_DATA'])
536 extras = json.loads(env['RC_SCM_DATA'])
585 rev_data = _parse_git_ref_lines(revision_lines)
537 rev_data = _parse_git_ref_lines(revision_lines)
586 if 'push' not in extras['hooks']:
538 if 'push' not in extras['hooks']:
587 return 0
539 return 0
588 _fix_hooks_executables(env.get('RC_INI_FILE'))
540 _fix_hooks_executables(env.get('RC_INI_FILE'))
589
541
590 empty_commit_id = '0' * 40
542 empty_commit_id = '0' * 40
591
543
592 detect_force_push = extras.get('detect_force_push')
544 detect_force_push = extras.get('detect_force_push')
593
545
594 for push_ref in rev_data:
546 for push_ref in rev_data:
595 # store our git-env which holds the temp store
547 # store our git-env which holds the temp store
596 push_ref['git_env'] = _get_git_env()
548 push_ref['git_env'] = _get_git_env()
597 push_ref['pruned_sha'] = ''
549 push_ref['pruned_sha'] = ''
598 if not detect_force_push:
550 if not detect_force_push:
599 # don't check for forced-push when we don't need to
551 # don't check for forced-push when we don't need to
600 continue
552 continue
601
553
602 type_ = push_ref['type']
554 type_ = push_ref['type']
603 new_branch = push_ref['old_rev'] == empty_commit_id
555 new_branch = push_ref['old_rev'] == empty_commit_id
604 delete_branch = push_ref['new_rev'] == empty_commit_id
556 delete_branch = push_ref['new_rev'] == empty_commit_id
605 if type_ == 'heads' and not (new_branch or delete_branch):
557 if type_ == 'heads' and not (new_branch or delete_branch):
606 old_rev = push_ref['old_rev']
558 old_rev = push_ref['old_rev']
607 new_rev = push_ref['new_rev']
559 new_rev = push_ref['new_rev']
608 cmd = [settings.GIT_EXECUTABLE(), 'rev-list', old_rev, f'^{new_rev}']
560 cmd = [settings.GIT_EXECUTABLE(), 'rev-list', old_rev, f'^{new_rev}']
609 stdout, stderr = subprocessio.run_command(
561 stdout, stderr = subprocessio.run_command(
610 cmd, env=os.environ.copy())
562 cmd, env=os.environ.copy())
611 # means we're having some non-reachable objects, this forced push was used
563 # means we're having some non-reachable objects, this forced push was used
612 if stdout:
564 if stdout:
613 push_ref['pruned_sha'] = stdout.splitlines()
565 push_ref['pruned_sha'] = stdout.splitlines()
614
566
615 extras['hook_type'] = 'pre_receive'
567 extras['hook_type'] = 'pre_receive'
616 extras['commit_ids'] = rev_data
568 extras['commit_ids'] = rev_data
617
569
618 stdout = sys.stdout
570 stdout = sys.stdout
619 status_code = _call_hook('pre_push', extras, GitMessageWriter(stdout))
571 status_code = _call_hook('pre_push', extras, GitMessageWriter(stdout))
620
572
621 return status_code
573 return status_code
622
574
623
575
624 def git_post_receive(unused_repo_path, revision_lines, env) -> int:
576 def git_post_receive(unused_repo_path, revision_lines, env) -> int:
625 """
577 """
626 Post push hook.
578 Post push hook.
627
579
628 :return: status code of the hook. 0 for success.
580 :return: status code of the hook. 0 for success.
629 """
581 """
630 extras = json.loads(env['RC_SCM_DATA'])
582 extras = json.loads(env['RC_SCM_DATA'])
631 if 'push' not in extras['hooks']:
583 if 'push' not in extras['hooks']:
632 return 0
584 return 0
633
585
634 _fix_hooks_executables(env.get('RC_INI_FILE'))
586 _fix_hooks_executables(env.get('RC_INI_FILE'))
635
587
636 rev_data = _parse_git_ref_lines(revision_lines)
588 rev_data = _parse_git_ref_lines(revision_lines)
637
589
638 git_revs = []
590 git_revs = []
639
591
640 # 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
641 # 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
642 # correct version of the git executable.
594 # correct version of the git executable.
643 empty_commit_id = '0' * 40
595 empty_commit_id = '0' * 40
644 branches = []
596 branches = []
645 tags = []
597 tags = []
646 for push_ref in rev_data:
598 for push_ref in rev_data:
647 type_ = push_ref['type']
599 type_ = push_ref['type']
648
600
649 if type_ == 'heads':
601 if type_ == 'heads':
650 # starting new branch case
602 # starting new branch case
651 if push_ref['old_rev'] == empty_commit_id:
603 if push_ref['old_rev'] == empty_commit_id:
652 push_ref_name = push_ref['name']
604 push_ref_name = push_ref['name']
653
605
654 if push_ref_name not in branches:
606 if push_ref_name not in branches:
655 branches.append(push_ref_name)
607 branches.append(push_ref_name)
656
608
657 need_head_set = ''
609 need_head_set = ''
658 with Repository(os.getcwd()) as repo:
610 with Repository(os.getcwd()) as repo:
659 try:
611 try:
660 repo.head
612 repo.head
661 except pygit2.GitError:
613 except pygit2.GitError:
662 need_head_set = f'refs/heads/{push_ref_name}'
614 need_head_set = f'refs/heads/{push_ref_name}'
663
615
664 if need_head_set:
616 if need_head_set:
665 repo.set_head(need_head_set)
617 repo.set_head(need_head_set)
666 print(f"Setting default branch to {push_ref_name}")
618 print(f"Setting default branch to {push_ref_name}")
667
619
668 cmd = [settings.GIT_EXECUTABLE(), 'for-each-ref', '--format=%(refname)', 'refs/heads/*']
620 cmd = [settings.GIT_EXECUTABLE(), 'for-each-ref', '--format=%(refname)', 'refs/heads/*']
669 stdout, stderr = subprocessio.run_command(
621 stdout, stderr = subprocessio.run_command(
670 cmd, env=os.environ.copy())
622 cmd, env=os.environ.copy())
671 heads = safe_str(stdout)
623 heads = safe_str(stdout)
672 heads = heads.replace(push_ref['ref'], '')
624 heads = heads.replace(push_ref['ref'], '')
673 heads = ' '.join(head for head
625 heads = ' '.join(head for head
674 in heads.splitlines() if head) or '.'
626 in heads.splitlines() if head) or '.'
675 cmd = [settings.GIT_EXECUTABLE(), 'log', '--reverse',
627 cmd = [settings.GIT_EXECUTABLE(), 'log', '--reverse',
676 '--pretty=format:%H', '--', push_ref['new_rev'],
628 '--pretty=format:%H', '--', push_ref['new_rev'],
677 '--not', heads]
629 '--not', heads]
678 stdout, stderr = subprocessio.run_command(
630 stdout, stderr = subprocessio.run_command(
679 cmd, env=os.environ.copy())
631 cmd, env=os.environ.copy())
680 git_revs.extend(list(map(ascii_str, stdout.splitlines())))
632 git_revs.extend(list(map(ascii_str, stdout.splitlines())))
681
633
682 # delete branch case
634 # delete branch case
683 elif push_ref['new_rev'] == empty_commit_id:
635 elif push_ref['new_rev'] == empty_commit_id:
684 git_revs.append(f'delete_branch=>{push_ref["name"]}')
636 git_revs.append(f'delete_branch=>{push_ref["name"]}')
685 else:
637 else:
686 if push_ref['name'] not in branches:
638 if push_ref['name'] not in branches:
687 branches.append(push_ref['name'])
639 branches.append(push_ref['name'])
688
640
689 cmd = [settings.GIT_EXECUTABLE(), 'log',
641 cmd = [settings.GIT_EXECUTABLE(), 'log',
690 f'{push_ref["old_rev"]}..{push_ref["new_rev"]}',
642 f'{push_ref["old_rev"]}..{push_ref["new_rev"]}',
691 '--reverse', '--pretty=format:%H']
643 '--reverse', '--pretty=format:%H']
692 stdout, stderr = subprocessio.run_command(
644 stdout, stderr = subprocessio.run_command(
693 cmd, env=os.environ.copy())
645 cmd, env=os.environ.copy())
694 # we get bytes from stdout, we need str to be consistent
646 # we get bytes from stdout, we need str to be consistent
695 log_revs = list(map(ascii_str, stdout.splitlines()))
647 log_revs = list(map(ascii_str, stdout.splitlines()))
696 git_revs.extend(log_revs)
648 git_revs.extend(log_revs)
697
649
698 # Pure pygit2 impl. but still 2-3x slower :/
650 # Pure pygit2 impl. but still 2-3x slower :/
699 # results = []
651 # results = []
700 #
652 #
701 # with Repository(os.getcwd()) as repo:
653 # with Repository(os.getcwd()) as repo:
702 # repo_new_rev = repo[push_ref['new_rev']]
654 # repo_new_rev = repo[push_ref['new_rev']]
703 # repo_old_rev = repo[push_ref['old_rev']]
655 # repo_old_rev = repo[push_ref['old_rev']]
704 # walker = repo.walk(repo_new_rev.id, pygit2.GIT_SORT_TOPOLOGICAL)
656 # walker = repo.walk(repo_new_rev.id, pygit2.GIT_SORT_TOPOLOGICAL)
705 #
657 #
706 # for commit in walker:
658 # for commit in walker:
707 # if commit.id == repo_old_rev.id:
659 # if commit.id == repo_old_rev.id:
708 # break
660 # break
709 # results.append(commit.id.hex)
661 # results.append(commit.id.hex)
710 # # reverse the order, can't use GIT_SORT_REVERSE
662 # # reverse the order, can't use GIT_SORT_REVERSE
711 # log_revs = results[::-1]
663 # log_revs = results[::-1]
712
664
713 elif type_ == 'tags':
665 elif type_ == 'tags':
714 if push_ref['name'] not in tags:
666 if push_ref['name'] not in tags:
715 tags.append(push_ref['name'])
667 tags.append(push_ref['name'])
716 git_revs.append(f'tag=>{push_ref["name"]}')
668 git_revs.append(f'tag=>{push_ref["name"]}')
717
669
718 extras['hook_type'] = 'post_receive'
670 extras['hook_type'] = 'post_receive'
719 extras['commit_ids'] = git_revs
671 extras['commit_ids'] = git_revs
720 extras['new_refs'] = {
672 extras['new_refs'] = {
721 'branches': branches,
673 'branches': branches,
722 'bookmarks': [],
674 'bookmarks': [],
723 'tags': tags,
675 'tags': tags,
724 }
676 }
725
677
726 stdout = sys.stdout
678 stdout = sys.stdout
727
679
728 if 'repo_size' in extras['hooks']:
680 if 'repo_size' in extras['hooks']:
729 try:
681 try:
730 _call_hook('repo_size', extras, GitMessageWriter(stdout))
682 _call_hook('repo_size', extras, GitMessageWriter(stdout))
731 except Exception:
683 except Exception:
732 pass
684 pass
733
685
734 status_code = _call_hook('post_push', extras, GitMessageWriter(stdout))
686 status_code = _call_hook('post_push', extras, GitMessageWriter(stdout))
735 return status_code
687 return status_code
736
688
737
689
738 def get_extras_from_txn_id(repo_path, txn_id):
690 def get_extras_from_txn_id(repo_path, txn_id):
739 extras = get_txn_id_from_store(repo_path, txn_id)
691 extras = get_txn_id_from_store(repo_path, txn_id)
740 return extras
692 return extras
741
693
742
694
743 def svn_pre_commit(repo_path, commit_data, env):
695 def svn_pre_commit(repo_path, commit_data, env):
744
696
745 path, txn_id = commit_data
697 path, txn_id = commit_data
746 branches = []
698 branches = []
747 tags = []
699 tags = []
748
700
749 if env.get('RC_SCM_DATA'):
701 if env.get('RC_SCM_DATA'):
750 extras = json.loads(env['RC_SCM_DATA'])
702 extras = json.loads(env['RC_SCM_DATA'])
751 else:
703 else:
752 ini_path = env.get('RC_INI_FILE')
704 ini_path = env.get('RC_INI_FILE')
753 if ini_path:
705 if ini_path:
754 _get_ini_settings(ini_path)
706 _get_ini_settings(ini_path)
755 # fallback method to read from TXN-ID stored data
707 # fallback method to read from TXN-ID stored data
756 extras = get_extras_from_txn_id(path, txn_id)
708 extras = get_extras_from_txn_id(path, txn_id)
757
709
758 if not extras:
710 if not extras:
759 raise ValueError('SVN-PRE-COMMIT: Failed to extract context data in called extras for hook execution')
711 raise ValueError('SVN-PRE-COMMIT: Failed to extract context data in called extras for hook execution')
760
712
761 if extras.get('rc_internal_commit'):
713 if extras.get('rc_internal_commit'):
762 # special marker for internal commit, we don't call hooks client
714 # special marker for internal commit, we don't call hooks client
763 return 0
715 return 0
764
716
765 extras['hook_type'] = 'pre_commit'
717 extras['hook_type'] = 'pre_commit'
766 extras['commit_ids'] = [txn_id]
718 extras['commit_ids'] = [txn_id]
767 extras['txn_id'] = txn_id
719 extras['txn_id'] = txn_id
768 extras['new_refs'] = {
720 extras['new_refs'] = {
769 'total_commits': 1,
721 'total_commits': 1,
770 'branches': branches,
722 'branches': branches,
771 'bookmarks': [],
723 'bookmarks': [],
772 'tags': tags,
724 'tags': tags,
773 }
725 }
774
726
775 return _call_hook('pre_push', extras, SvnMessageWriter())
727 return _call_hook('pre_push', extras, SvnMessageWriter())
776
728
777
729
778 def svn_post_commit(repo_path, commit_data, env):
730 def svn_post_commit(repo_path, commit_data, env):
779 """
731 """
780 commit_data is path, rev, txn_id
732 commit_data is path, rev, txn_id
781 """
733 """
782
734
783 if len(commit_data) == 3:
735 if len(commit_data) == 3:
784 path, commit_id, txn_id = commit_data
736 path, commit_id, txn_id = commit_data
785 elif len(commit_data) == 2:
737 elif len(commit_data) == 2:
786 log.error('Failed to extract txn_id from commit_data using legacy method. '
738 log.error('Failed to extract txn_id from commit_data using legacy method. '
787 'Some functionality might be limited')
739 'Some functionality might be limited')
788 path, commit_id = commit_data
740 path, commit_id = commit_data
789 txn_id = None
741 txn_id = None
790 else:
742 else:
791 return 0
743 return 0
792
744
793 branches = []
745 branches = []
794 tags = []
746 tags = []
795
747
796 if env.get('RC_SCM_DATA'):
748 if env.get('RC_SCM_DATA'):
797 extras = json.loads(env['RC_SCM_DATA'])
749 extras = json.loads(env['RC_SCM_DATA'])
798 else:
750 else:
799 ini_path = env.get('RC_INI_FILE')
751 ini_path = env.get('RC_INI_FILE')
800 if ini_path:
752 if ini_path:
801 _get_ini_settings(ini_path)
753 _get_ini_settings(ini_path)
802 # fallback method to read from TXN-ID stored data
754 # fallback method to read from TXN-ID stored data
803 extras = get_extras_from_txn_id(path, txn_id)
755 extras = get_extras_from_txn_id(path, txn_id)
804
756
805 if not extras and txn_id:
757 if not extras and txn_id:
806 raise ValueError('SVN-POST-COMMIT: Failed to extract context data in called extras for hook execution')
758 raise ValueError('SVN-POST-COMMIT: Failed to extract context data in called extras for hook execution')
807
759
808 if extras.get('rc_internal_commit'):
760 if extras.get('rc_internal_commit'):
809 # special marker for internal commit, we don't call hooks client
761 # special marker for internal commit, we don't call hooks client
810 return 0
762 return 0
811
763
812 extras['hook_type'] = 'post_commit'
764 extras['hook_type'] = 'post_commit'
813 extras['commit_ids'] = [commit_id]
765 extras['commit_ids'] = [commit_id]
814 extras['txn_id'] = txn_id
766 extras['txn_id'] = txn_id
815 extras['new_refs'] = {
767 extras['new_refs'] = {
816 'branches': branches,
768 'branches': branches,
817 'bookmarks': [],
769 'bookmarks': [],
818 'tags': tags,
770 'tags': tags,
819 'total_commits': 1,
771 'total_commits': 1,
820 }
772 }
821
773
822 if 'repo_size' in extras['hooks']:
774 if 'repo_size' in extras['hooks']:
823 try:
775 try:
824 _call_hook('repo_size', extras, SvnMessageWriter())
776 _call_hook('repo_size', extras, SvnMessageWriter())
825 except Exception:
777 except Exception:
826 pass
778 pass
827
779
828 return _call_hook('post_push', extras, SvnMessageWriter())
780 return _call_hook('post_push', extras, SvnMessageWriter())
@@ -1,257 +1,135 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 threading
18 import pytest
19 import msgpack
20
21 from http.server import BaseHTTPRequestHandler
22 from socketserver import TCPServer
23
19
24 import mercurial.ui
20 import mercurial.ui
25 import mock
21 import mock
26 import pytest
27
22
28 from vcsserver.hooks import HooksHttpClient
29 from vcsserver.lib.ext_json import json
23 from vcsserver.lib.ext_json import json
30 from vcsserver import hooks
24 from vcsserver import hooks
31
25
32
26
33 def get_hg_ui(extras=None):
27 def get_hg_ui(extras=None):
34 """Create a Config object with a valid RC_SCM_DATA entry."""
28 """Create a Config object with a valid RC_SCM_DATA entry."""
35 extras = extras or {}
29 extras = extras or {}
36 required_extras = {
30 required_extras = {
37 'username': '',
31 'username': '',
38 'repository': '',
32 'repository': '',
39 'locked_by': '',
33 'locked_by': '',
40 'scm': '',
34 'scm': '',
41 'make_lock': '',
35 'make_lock': '',
42 'action': '',
36 'action': '',
43 'ip': '',
37 'ip': '',
44 'hooks_uri': 'fake_hooks_uri',
45 }
38 }
46 required_extras.update(extras)
39 required_extras.update(extras)
47 hg_ui = mercurial.ui.ui()
40 hg_ui = mercurial.ui.ui()
48 hg_ui.setconfig(b'rhodecode', b'RC_SCM_DATA', json.dumps(required_extras))
41 hg_ui.setconfig(b'rhodecode', b'RC_SCM_DATA', json.dumps(required_extras))
49
42
50 return hg_ui
43 return hg_ui
51
44
52
45
53 def test_git_pre_receive_is_disabled():
46 def test_git_pre_receive_is_disabled():
54 extras = {'hooks': ['pull']}
47 extras = {'hooks': ['pull']}
55 response = hooks.git_pre_receive(None, None,
48 response = hooks.git_pre_receive(None, None,
56 {'RC_SCM_DATA': json.dumps(extras)})
49 {'RC_SCM_DATA': json.dumps(extras)})
57
50
58 assert response == 0
51 assert response == 0
59
52
60
53
61 def test_git_post_receive_is_disabled():
54 def test_git_post_receive_is_disabled():
62 extras = {'hooks': ['pull']}
55 extras = {'hooks': ['pull']}
63 response = hooks.git_post_receive(None, '',
56 response = hooks.git_post_receive(None, '',
64 {'RC_SCM_DATA': json.dumps(extras)})
57 {'RC_SCM_DATA': json.dumps(extras)})
65
58
66 assert response == 0
59 assert response == 0
67
60
68
61
69 def test_git_post_receive_calls_repo_size():
62 def test_git_post_receive_calls_repo_size():
70 extras = {'hooks': ['push', 'repo_size']}
63 extras = {'hooks': ['push', 'repo_size']}
71
64
72 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
65 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
73 hooks.git_post_receive(
66 hooks.git_post_receive(
74 None, '', {'RC_SCM_DATA': json.dumps(extras)})
67 None, '', {'RC_SCM_DATA': json.dumps(extras)})
75 extras.update({'commit_ids': [], 'hook_type': 'post_receive',
68 extras.update({'commit_ids': [], 'hook_type': 'post_receive',
76 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
69 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
77 expected_calls = [
70 expected_calls = [
78 mock.call('repo_size', extras, mock.ANY),
71 mock.call('repo_size', extras, mock.ANY),
79 mock.call('post_push', extras, mock.ANY),
72 mock.call('post_push', extras, mock.ANY),
80 ]
73 ]
81 assert call_hook_mock.call_args_list == expected_calls
74 assert call_hook_mock.call_args_list == expected_calls
82
75
83
76
84 def test_git_post_receive_does_not_call_disabled_repo_size():
77 def test_git_post_receive_does_not_call_disabled_repo_size():
85 extras = {'hooks': ['push']}
78 extras = {'hooks': ['push']}
86
79
87 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
80 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
88 hooks.git_post_receive(
81 hooks.git_post_receive(
89 None, '', {'RC_SCM_DATA': json.dumps(extras)})
82 None, '', {'RC_SCM_DATA': json.dumps(extras)})
90 extras.update({'commit_ids': [], 'hook_type': 'post_receive',
83 extras.update({'commit_ids': [], 'hook_type': 'post_receive',
91 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
84 'new_refs': {'bookmarks': [], 'branches': [], 'tags': []}})
92 expected_calls = [
85 expected_calls = [
93 mock.call('post_push', extras, mock.ANY)
86 mock.call('post_push', extras, mock.ANY)
94 ]
87 ]
95 assert call_hook_mock.call_args_list == expected_calls
88 assert call_hook_mock.call_args_list == expected_calls
96
89
97
90
98 def test_repo_size_exception_does_not_affect_git_post_receive():
91 def test_repo_size_exception_does_not_affect_git_post_receive():
99 extras = {'hooks': ['push', 'repo_size']}
92 extras = {'hooks': ['push', 'repo_size']}
100 status = 0
93 status = 0
101
94
102 def side_effect(name, *args, **kwargs):
95 def side_effect(name, *args, **kwargs):
103 if name == 'repo_size':
96 if name == 'repo_size':
104 raise Exception('Fake exception')
97 raise Exception('Fake exception')
105 else:
98 else:
106 return status
99 return status
107
100
108 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
101 with mock.patch.object(hooks, '_call_hook') as call_hook_mock:
109 call_hook_mock.side_effect = side_effect
102 call_hook_mock.side_effect = side_effect
110 result = hooks.git_post_receive(
103 result = hooks.git_post_receive(
111 None, '', {'RC_SCM_DATA': json.dumps(extras)})
104 None, '', {'RC_SCM_DATA': json.dumps(extras)})
112 assert result == status
105 assert result == status
113
106
114
107
115 def test_git_pre_pull_is_disabled():
108 def test_git_pre_pull_is_disabled():
116 assert hooks.git_pre_pull({'hooks': ['push']}) == hooks.HookResponse(0, '')
109 assert hooks.git_pre_pull({'hooks': ['push']}) == hooks.HookResponse(0, '')
117
110
118
111
119 def test_git_post_pull_is_disabled():
112 def test_git_post_pull_is_disabled():
120 assert (
113 assert (
121 hooks.git_post_pull({'hooks': ['push']}) == hooks.HookResponse(0, ''))
114 hooks.git_post_pull({'hooks': ['push']}) == hooks.HookResponse(0, ''))
122
115
123
116
124 class TestGetHooksClient:
117 class TestGetHooksClient:
125
118
126 def test_returns_http_client_when_protocol_matches(self):
127 hooks_uri = 'localhost:8000'
128 result = hooks._get_hooks_client({
129 'hooks_uri': hooks_uri,
130 'hooks_protocol': 'http'
131 })
132 assert isinstance(result, hooks.HooksHttpClient)
133 assert result.hooks_uri == hooks_uri
134
135 def test_return_celery_client_when_queue_and_backend_provided(self):
119 def test_return_celery_client_when_queue_and_backend_provided(self):
136 task_queue = 'redis://task_queue:0'
120 task_queue = 'redis://task_queue:0'
137 task_backend = task_queue
121 task_backend = task_queue
138 result = hooks._get_hooks_client({
122 result = hooks._get_hooks_client({
139 'task_queue': task_queue,
123 'task_queue': task_queue,
140 'task_backend': task_backend
124 'task_backend': task_backend
141 })
125 })
142 assert isinstance(result, hooks.HooksCeleryClient)
126 assert isinstance(result, hooks.HooksCeleryClient)
143
127
144
128
145 class TestHooksHttpClient:
129 class TestHooksCeleryClient:
146 def test_init_sets_hooks_uri(self):
147 uri = 'localhost:3000'
148 client = hooks.HooksHttpClient(uri)
149 assert client.hooks_uri == uri
150
151 def test_serialize_returns_serialized_string(self):
152 client = hooks.HooksHttpClient('localhost:3000')
153 hook_name = 'test'
154 extras = {
155 'first': 1,
156 'second': 'two'
157 }
158 hooks_proto, result = client._serialize(hook_name, extras)
159 expected_result = msgpack.packb({
160 'method': hook_name,
161 'extras': extras,
162 })
163 assert hooks_proto == {'rc-hooks-protocol': 'msgpack.v1', 'Connection': 'keep-alive'}
164 assert result == expected_result
165
166 def test_call_queries_http_server(self, http_mirror):
167 client = hooks.HooksHttpClient(http_mirror.uri)
168 hook_name = 'test'
169 extras = {
170 'first': 1,
171 'second': 'two'
172 }
173 result = client(hook_name, extras)
174 expected_result = msgpack.unpackb(msgpack.packb({
175 'method': hook_name,
176 'extras': extras
177 }), raw=False)
178 assert result == expected_result
179
180
181 @pytest.fixture
182 def http_mirror(request):
183 server = MirrorHttpServer()
184 request.addfinalizer(server.stop)
185 return server
186
187
188 class MirrorHttpHandler(BaseHTTPRequestHandler):
189
190 def do_POST(self):
191 length = int(self.headers['Content-Length'])
192 body = self.rfile.read(length)
193 self.send_response(200)
194 self.end_headers()
195 self.wfile.write(body)
196
197
198 class MirrorHttpServer:
199 ip_address = '127.0.0.1'
200 port = 0
201
130
202 def __init__(self):
131 def test_hooks_http_client_init(self):
203 self._daemon = TCPServer((self.ip_address, 0), MirrorHttpHandler)
132 queue = 'redis://redis:6379/0'
204 _, self.port = self._daemon.server_address
133 backend = 'redis://redis:6379/0'
205 self._thread = threading.Thread(target=self._daemon.serve_forever)
134 client = hooks.HooksCeleryClient(queue, backend)
206 self._thread.daemon = True
135 assert client.celery_app.conf.broker_url == queue
207 self._thread.start()
208
209 def stop(self):
210 self._daemon.shutdown()
211 self._thread.join()
212 self._daemon = None
213 self._thread = None
214
215 @property
216 def uri(self):
217 return '{}:{}'.format(self.ip_address, self.port)
218
219
220 def test_hooks_http_client_init():
221 hooks_uri = 'http://localhost:8000'
222 client = HooksHttpClient(hooks_uri)
223 assert client.hooks_uri == hooks_uri
224
225
226 def test_hooks_http_client_call():
227 hooks_uri = 'http://localhost:8000'
228
229 method = 'test_method'
230 extras = {'key': 'value'}
231
232 with \
233 mock.patch('http.client.HTTPConnection') as mock_connection,\
234 mock.patch('msgpack.load') as mock_load:
235
236 client = HooksHttpClient(hooks_uri)
237
238 mock_load.return_value = {'result': 'success'}
239 response = mock.MagicMock()
240 response.status = 200
241 mock_connection.request.side_effect = None
242 mock_connection.getresponse.return_value = response
243
244 result = client(method, extras)
245
246 mock_connection.assert_called_with(hooks_uri)
247 mock_connection.return_value.request.assert_called_once()
248 assert result == {'result': 'success'}
249
250
251 def test_hooks_http_client_serialize():
252 method = 'test_method'
253 extras = {'key': 'value'}
254 headers, body = HooksHttpClient._serialize(method, extras)
255
256 assert headers == {'rc-hooks-protocol': HooksHttpClient.proto, 'Connection': 'keep-alive'}
257 assert msgpack.unpackb(body) == {'method': method, 'extras': extras}
General Comments 0
You need to be logged in to leave comments. Login now