##// END OF EJS Templates
fix(core): don't rely on pkgutil to extract the version info of project
super-admin -
r1188:dc28b397 default
parent child Browse files
Show More
@@ -1,28 +1,34 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 import pkgutil
18 import os
19 19
20 20
21 __version__ = pkgutil.get_data('vcsserver', 'VERSION').strip().decode()
21 def get_version():
22 here = os.path.abspath(os.path.dirname(__file__))
23 ver_file = os.path.join(here, "VERSION")
24 with open(ver_file, "rt") as f:
25 version = f.read().strip()
26
27 return version
22 28
23 29 # link to config for pyramid
24 30 CONFIG = {}
25 31
26 32 # Populated with the settings dictionary from application init in
27 33 #
28 34 PYRAMID_SETTINGS = {}
@@ -1,220 +1,220 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import re
19 19 import os
20 20 import sys
21 21 import datetime
22 22 import logging
23 23 import pkg_resources
24 24
25 25 import vcsserver
26 26 from vcsserver.str_utils import safe_bytes
27 27
28 28 log = logging.getLogger(__name__)
29 29
30 30 HOOKS_DIR_MODE = 0o755
31 31 HOOKS_FILE_MODE = 0o755
32 32
33 33
34 34 def set_permissions_if_needed(path_to_check, perms: oct):
35 35 # Get current permissions
36 36 current_permissions = os.stat(path_to_check).st_mode & 0o777 # Extract permission bits
37 37
38 38 # Check if current permissions are lower than required
39 39 if current_permissions < int(perms):
40 40 # Change the permissions if they are lower than required
41 41 os.chmod(path_to_check, perms)
42 42
43 43
44 44 def get_git_hooks_path(repo_path, bare):
45 45 hooks_path = os.path.join(repo_path, 'hooks')
46 46 if not bare:
47 47 hooks_path = os.path.join(repo_path, '.git', 'hooks')
48 48
49 49 return hooks_path
50 50
51 51
52 52 def install_git_hooks(repo_path, bare, executable=None, force_create=False):
53 53 """
54 54 Creates a RhodeCode hook inside a git repository
55 55
56 56 :param repo_path: path to repository
57 57 :param bare: defines if repository is considered a bare git repo
58 58 :param executable: binary executable to put in the hooks
59 59 :param force_create: Creates even if the same name hook exists
60 60 """
61 61 executable = executable or sys.executable
62 62 hooks_path = get_git_hooks_path(repo_path, bare)
63 63
64 64 # we always call it to ensure dir exists and it has a proper mode
65 65 if not os.path.exists(hooks_path):
66 66 # If it doesn't exist, create a new directory with the specified mode
67 67 os.makedirs(hooks_path, mode=HOOKS_DIR_MODE, exist_ok=True)
68 68 # If it exists, change the directory's mode to the specified mode
69 69 set_permissions_if_needed(hooks_path, perms=HOOKS_DIR_MODE)
70 70
71 71 tmpl_post = pkg_resources.resource_string(
72 72 'vcsserver', '/'.join(
73 73 ('hook_utils', 'hook_templates', 'git_post_receive.py.tmpl')))
74 74 tmpl_pre = pkg_resources.resource_string(
75 75 'vcsserver', '/'.join(
76 76 ('hook_utils', 'hook_templates', 'git_pre_receive.py.tmpl')))
77 77
78 78 path = '' # not used for now
79 79 timestamp = datetime.datetime.utcnow().isoformat()
80 80
81 81 for h_type, template in [('pre', tmpl_pre), ('post', tmpl_post)]:
82 82 log.debug('Installing git hook in repo %s', repo_path)
83 83 _hook_file = os.path.join(hooks_path, f'{h_type}-receive')
84 84 _rhodecode_hook = check_rhodecode_hook(_hook_file)
85 85
86 86 if _rhodecode_hook or force_create:
87 87 log.debug('writing git %s hook file at %s !', h_type, _hook_file)
88 88 try:
89 89 with open(_hook_file, 'wb') as f:
90 template = template.replace(b'_TMPL_', safe_bytes(vcsserver.__version__))
90 template = template.replace(b'_TMPL_', safe_bytes(vcsserver.get_version()))
91 91 template = template.replace(b'_DATE_', safe_bytes(timestamp))
92 92 template = template.replace(b'_ENV_', safe_bytes(executable))
93 93 template = template.replace(b'_PATH_', safe_bytes(path))
94 94 f.write(template)
95 95 set_permissions_if_needed(_hook_file, perms=HOOKS_FILE_MODE)
96 96 except OSError:
97 97 log.exception('error writing hook file %s', _hook_file)
98 98 else:
99 99 log.debug('skipping writing hook file')
100 100
101 101 return True
102 102
103 103
104 104 def get_svn_hooks_path(repo_path):
105 105 hooks_path = os.path.join(repo_path, 'hooks')
106 106
107 107 return hooks_path
108 108
109 109
110 110 def install_svn_hooks(repo_path, executable=None, force_create=False):
111 111 """
112 112 Creates RhodeCode hooks inside a svn repository
113 113
114 114 :param repo_path: path to repository
115 115 :param executable: binary executable to put in the hooks
116 116 :param force_create: Create even if same name hook exists
117 117 """
118 118 executable = executable or sys.executable
119 119 hooks_path = get_svn_hooks_path(repo_path)
120 120 if not os.path.isdir(hooks_path):
121 121 os.makedirs(hooks_path, mode=0o777, exist_ok=True)
122 122
123 123 tmpl_post = pkg_resources.resource_string(
124 124 'vcsserver', '/'.join(
125 125 ('hook_utils', 'hook_templates', 'svn_post_commit_hook.py.tmpl')))
126 126 tmpl_pre = pkg_resources.resource_string(
127 127 'vcsserver', '/'.join(
128 128 ('hook_utils', 'hook_templates', 'svn_pre_commit_hook.py.tmpl')))
129 129
130 130 path = '' # not used for now
131 131 timestamp = datetime.datetime.utcnow().isoformat()
132 132
133 133 for h_type, template in [('pre', tmpl_pre), ('post', tmpl_post)]:
134 134 log.debug('Installing svn hook in repo %s', repo_path)
135 135 _hook_file = os.path.join(hooks_path, f'{h_type}-commit')
136 136 _rhodecode_hook = check_rhodecode_hook(_hook_file)
137 137
138 138 if _rhodecode_hook or force_create:
139 139 log.debug('writing svn %s hook file at %s !', h_type, _hook_file)
140 140
141 141 try:
142 142 with open(_hook_file, 'wb') as f:
143 template = template.replace(b'_TMPL_', safe_bytes(vcsserver.__version__))
143 template = template.replace(b'_TMPL_', safe_bytes(vcsserver.get_version()))
144 144 template = template.replace(b'_DATE_', safe_bytes(timestamp))
145 145 template = template.replace(b'_ENV_', safe_bytes(executable))
146 146 template = template.replace(b'_PATH_', safe_bytes(path))
147 147
148 148 f.write(template)
149 149 os.chmod(_hook_file, 0o755)
150 150 except OSError:
151 151 log.exception('error writing hook file %s', _hook_file)
152 152 else:
153 153 log.debug('skipping writing hook file')
154 154
155 155 return True
156 156
157 157
158 158 def get_version_from_hook(hook_path):
159 159 version = b''
160 160 hook_content = read_hook_content(hook_path)
161 161 matches = re.search(rb'RC_HOOK_VER\s*=\s*(.*)', hook_content)
162 162 if matches:
163 163 try:
164 164 version = matches.groups()[0]
165 165 log.debug('got version %s from hooks.', version)
166 166 except Exception:
167 167 log.exception("Exception while reading the hook version.")
168 168 return version.replace(b"'", b"")
169 169
170 170
171 171 def check_rhodecode_hook(hook_path):
172 172 """
173 173 Check if the hook was created by RhodeCode
174 174 """
175 175 if not os.path.exists(hook_path):
176 176 return True
177 177
178 178 log.debug('hook exists, checking if it is from RhodeCode')
179 179
180 180 version = get_version_from_hook(hook_path)
181 181 if version:
182 182 return True
183 183
184 184 return False
185 185
186 186
187 187 def read_hook_content(hook_path) -> bytes:
188 188 content = b''
189 189 if os.path.isfile(hook_path):
190 190 with open(hook_path, 'rb') as f:
191 191 content = f.read()
192 192 return content
193 193
194 194
195 195 def get_git_pre_hook_version(repo_path, bare):
196 196 hooks_path = get_git_hooks_path(repo_path, bare)
197 197 _hook_file = os.path.join(hooks_path, 'pre-receive')
198 198 version = get_version_from_hook(_hook_file)
199 199 return version
200 200
201 201
202 202 def get_git_post_hook_version(repo_path, bare):
203 203 hooks_path = get_git_hooks_path(repo_path, bare)
204 204 _hook_file = os.path.join(hooks_path, 'post-receive')
205 205 version = get_version_from_hook(_hook_file)
206 206 return version
207 207
208 208
209 209 def get_svn_pre_hook_version(repo_path):
210 210 hooks_path = get_svn_hooks_path(repo_path)
211 211 _hook_file = os.path.join(hooks_path, 'pre-commit')
212 212 version = get_version_from_hook(_hook_file)
213 213 return version
214 214
215 215
216 216 def get_svn_post_hook_version(repo_path):
217 217 hooks_path = get_svn_hooks_path(repo_path)
218 218 _hook_file = os.path.join(hooks_path, 'post-commit')
219 219 version = get_version_from_hook(_hook_file)
220 220 return version
@@ -1,775 +1,775 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import io
19 19 import os
20 20 import platform
21 21 import sys
22 22 import locale
23 23 import logging
24 24 import uuid
25 25 import time
26 26 import wsgiref.util
27 27 import tempfile
28 28 import psutil
29 29
30 30 from itertools import chain
31 31
32 32 import msgpack
33 33 import configparser
34 34
35 35 from pyramid.config import Configurator
36 36 from pyramid.wsgi import wsgiapp
37 37 from pyramid.response import Response
38 38
39 39 from vcsserver.base import BytesEnvelope, BinaryEnvelope
40 40 from vcsserver.lib.rc_json import json
41 41 from vcsserver.config.settings_maker import SettingsMaker
42 42 from vcsserver.str_utils import safe_int
43 43 from vcsserver.lib.statsd_client import StatsdClient
44 44 from vcsserver.tweens.request_wrapper import get_headers_call_context
45 45
46 46 import vcsserver
47 47 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
48 48 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
49 49 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
50 50 from vcsserver.echo_stub.echo_app import EchoApp
51 51 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
52 52 from vcsserver.lib.exc_tracking import store_exception, format_exc
53 53 from vcsserver.server import VcsServer
54 54
55 55 strict_vcs = True
56 56
57 57 git_import_err = None
58 58 try:
59 59 from vcsserver.remote.git_remote import GitFactory, GitRemote
60 60 except ImportError as e:
61 61 GitFactory = None
62 62 GitRemote = None
63 63 git_import_err = e
64 64 if strict_vcs:
65 65 raise
66 66
67 67
68 68 hg_import_err = None
69 69 try:
70 70 from vcsserver.remote.hg_remote import MercurialFactory, HgRemote
71 71 except ImportError as e:
72 72 MercurialFactory = None
73 73 HgRemote = None
74 74 hg_import_err = e
75 75 if strict_vcs:
76 76 raise
77 77
78 78
79 79 svn_import_err = None
80 80 try:
81 81 from vcsserver.remote.svn_remote import SubversionFactory, SvnRemote
82 82 except ImportError as e:
83 83 SubversionFactory = None
84 84 SvnRemote = None
85 85 svn_import_err = e
86 86 if strict_vcs:
87 87 raise
88 88
89 89 log = logging.getLogger(__name__)
90 90
91 91 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
92 92 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
93 93
94 94 try:
95 95 locale.setlocale(locale.LC_ALL, '')
96 96 except locale.Error as e:
97 97 log.error(
98 98 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
99 99 os.environ['LC_ALL'] = 'C'
100 100
101 101
102 102 def _is_request_chunked(environ):
103 103 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
104 104 return stream
105 105
106 106
107 107 def log_max_fd():
108 108 try:
109 109 maxfd = psutil.Process().rlimit(psutil.RLIMIT_NOFILE)[1]
110 110 log.info('Max file descriptors value: %s', maxfd)
111 111 except Exception:
112 112 pass
113 113
114 114
115 115 class VCS:
116 116 def __init__(self, locale_conf=None, cache_config=None):
117 117 self.locale = locale_conf
118 118 self.cache_config = cache_config
119 119 self._configure_locale()
120 120
121 121 log_max_fd()
122 122
123 123 if GitFactory and GitRemote:
124 124 git_factory = GitFactory()
125 125 self._git_remote = GitRemote(git_factory)
126 126 else:
127 127 log.error("Git client import failed: %s", git_import_err)
128 128
129 129 if MercurialFactory and HgRemote:
130 130 hg_factory = MercurialFactory()
131 131 self._hg_remote = HgRemote(hg_factory)
132 132 else:
133 133 log.error("Mercurial client import failed: %s", hg_import_err)
134 134
135 135 if SubversionFactory and SvnRemote:
136 136 svn_factory = SubversionFactory()
137 137
138 138 # hg factory is used for svn url validation
139 139 hg_factory = MercurialFactory()
140 140 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
141 141 else:
142 142 log.error("Subversion client import failed: %s", svn_import_err)
143 143
144 144 self._vcsserver = VcsServer()
145 145
146 146 def _configure_locale(self):
147 147 if self.locale:
148 148 log.info('Settings locale: `LC_ALL` to %s', self.locale)
149 149 else:
150 150 log.info('Configuring locale subsystem based on environment variables')
151 151 try:
152 152 # If self.locale is the empty string, then the locale
153 153 # module will use the environment variables. See the
154 154 # documentation of the package `locale`.
155 155 locale.setlocale(locale.LC_ALL, self.locale)
156 156
157 157 language_code, encoding = locale.getlocale()
158 158 log.info(
159 159 'Locale set to language code "%s" with encoding "%s".',
160 160 language_code, encoding)
161 161 except locale.Error:
162 162 log.exception('Cannot set locale, not configuring the locale system')
163 163
164 164
165 165 class WsgiProxy:
166 166 def __init__(self, wsgi):
167 167 self.wsgi = wsgi
168 168
169 169 def __call__(self, environ, start_response):
170 170 input_data = environ['wsgi.input'].read()
171 171 input_data = msgpack.unpackb(input_data)
172 172
173 173 error = None
174 174 try:
175 175 data, status, headers = self.wsgi.handle(
176 176 input_data['environment'], input_data['input_data'],
177 177 *input_data['args'], **input_data['kwargs'])
178 178 except Exception as e:
179 179 data, status, headers = [], None, None
180 180 error = {
181 181 'message': str(e),
182 182 '_vcs_kind': getattr(e, '_vcs_kind', None)
183 183 }
184 184
185 185 start_response(200, {})
186 186 return self._iterator(error, status, headers, data)
187 187
188 188 def _iterator(self, error, status, headers, data):
189 189 initial_data = [
190 190 error,
191 191 status,
192 192 headers,
193 193 ]
194 194
195 195 for d in chain(initial_data, data):
196 196 yield msgpack.packb(d)
197 197
198 198
199 199 def not_found(request):
200 200 return {'status': '404 NOT FOUND'}
201 201
202 202
203 203 class VCSViewPredicate:
204 204 def __init__(self, val, config):
205 205 self.remotes = val
206 206
207 207 def text(self):
208 208 return f'vcs view method = {list(self.remotes.keys())}'
209 209
210 210 phash = text
211 211
212 212 def __call__(self, context, request):
213 213 """
214 214 View predicate that returns true if given backend is supported by
215 215 defined remotes.
216 216 """
217 217 backend = request.matchdict.get('backend')
218 218 return backend in self.remotes
219 219
220 220
221 221 class HTTPApplication:
222 222 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
223 223
224 224 remote_wsgi = remote_wsgi
225 225 _use_echo_app = False
226 226
227 227 def __init__(self, settings=None, global_config=None):
228 228
229 229 self.config = Configurator(settings=settings)
230 230 # Init our statsd at very start
231 231 self.config.registry.statsd = StatsdClient.statsd
232 232 self.config.registry.vcs_call_context = {}
233 233
234 234 self.global_config = global_config
235 235 self.config.include('vcsserver.lib.rc_cache')
236 236 self.config.include('vcsserver.lib.rc_cache.archive_cache')
237 237
238 238 settings_locale = settings.get('locale', '') or 'en_US.UTF-8'
239 239 vcs = VCS(locale_conf=settings_locale, cache_config=settings)
240 240 self._remotes = {
241 241 'hg': vcs._hg_remote,
242 242 'git': vcs._git_remote,
243 243 'svn': vcs._svn_remote,
244 244 'server': vcs._vcsserver,
245 245 }
246 246 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
247 247 self._use_echo_app = True
248 248 log.warning("Using EchoApp for VCS operations.")
249 249 self.remote_wsgi = remote_wsgi_stub
250 250
251 251 self._configure_settings(global_config, settings)
252 252
253 253 self._configure()
254 254
255 255 def _configure_settings(self, global_config, app_settings):
256 256 """
257 257 Configure the settings module.
258 258 """
259 259 settings_merged = global_config.copy()
260 260 settings_merged.update(app_settings)
261 261
262 262 git_path = app_settings.get('git_path', None)
263 263 if git_path:
264 264 settings.GIT_EXECUTABLE = git_path
265 265 binary_dir = app_settings.get('core.binary_dir', None)
266 266 if binary_dir:
267 267 settings.BINARY_DIR = binary_dir
268 268
269 269 # Store the settings to make them available to other modules.
270 270 vcsserver.PYRAMID_SETTINGS = settings_merged
271 271 vcsserver.CONFIG = settings_merged
272 272
273 273 def _configure(self):
274 274 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
275 275
276 276 self.config.add_route('service', '/_service')
277 277 self.config.add_route('status', '/status')
278 278 self.config.add_route('hg_proxy', '/proxy/hg')
279 279 self.config.add_route('git_proxy', '/proxy/git')
280 280
281 281 # rpc methods
282 282 self.config.add_route('vcs', '/{backend}')
283 283
284 284 # streaming rpc remote methods
285 285 self.config.add_route('vcs_stream', '/{backend}/stream')
286 286
287 287 # vcs operations clone/push as streaming
288 288 self.config.add_route('stream_git', '/stream/git/*repo_name')
289 289 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
290 290
291 291 self.config.add_view(self.status_view, route_name='status', renderer='json')
292 292 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
293 293
294 294 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
295 295 self.config.add_view(self.git_proxy(), route_name='git_proxy')
296 296 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
297 297 vcs_view=self._remotes)
298 298 self.config.add_view(self.vcs_stream_view, route_name='vcs_stream',
299 299 vcs_view=self._remotes)
300 300
301 301 self.config.add_view(self.hg_stream(), route_name='stream_hg')
302 302 self.config.add_view(self.git_stream(), route_name='stream_git')
303 303
304 304 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
305 305
306 306 self.config.add_notfound_view(not_found, renderer='json')
307 307
308 308 self.config.add_view(self.handle_vcs_exception, context=Exception)
309 309
310 310 self.config.add_tween(
311 311 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
312 312 )
313 313 self.config.add_request_method(
314 314 'vcsserver.lib.request_counter.get_request_counter',
315 315 'request_count')
316 316
317 317 def wsgi_app(self):
318 318 return self.config.make_wsgi_app()
319 319
320 320 def _vcs_view_params(self, request):
321 321 remote = self._remotes[request.matchdict['backend']]
322 322 payload = msgpack.unpackb(request.body, use_list=True)
323 323
324 324 method = payload.get('method')
325 325 params = payload['params']
326 326 wire = params.get('wire')
327 327 args = params.get('args')
328 328 kwargs = params.get('kwargs')
329 329 context_uid = None
330 330
331 331 request.registry.vcs_call_context = {
332 332 'method': method,
333 333 'repo_name': payload.get('_repo_name'),
334 334 }
335 335
336 336 if wire:
337 337 try:
338 338 wire['context'] = context_uid = uuid.UUID(wire['context'])
339 339 except KeyError:
340 340 pass
341 341 args.insert(0, wire)
342 342 repo_state_uid = wire.get('repo_state_uid') if wire else None
343 343
344 344 # NOTE(marcink): trading complexity for slight performance
345 345 if log.isEnabledFor(logging.DEBUG):
346 346 # also we SKIP printing out any of those methods args since they maybe excessive
347 347 just_args_methods = {
348 348 'commitctx': ('content', 'removed', 'updated'),
349 349 'commit': ('content', 'removed', 'updated')
350 350 }
351 351 if method in just_args_methods:
352 352 skip_args = just_args_methods[method]
353 353 call_args = ''
354 354 call_kwargs = {}
355 355 for k in kwargs:
356 356 if k in skip_args:
357 357 # replace our skip key with dummy
358 358 call_kwargs[k] = f'RemovedParam({k})'
359 359 else:
360 360 call_kwargs[k] = kwargs[k]
361 361 else:
362 362 call_args = args[1:]
363 363 call_kwargs = kwargs
364 364
365 365 log.debug('Method requested:`%s` with args:%s kwargs:%s context_uid: %s, repo_state_uid:%s',
366 366 method, call_args, call_kwargs, context_uid, repo_state_uid)
367 367
368 368 statsd = request.registry.statsd
369 369 if statsd:
370 370 statsd.incr(
371 371 'vcsserver_method_total', tags=[
372 372 f"method:{method}",
373 373 ])
374 374 return payload, remote, method, args, kwargs
375 375
376 376 def vcs_view(self, request):
377 377
378 378 payload, remote, method, args, kwargs = self._vcs_view_params(request)
379 379 payload_id = payload.get('id')
380 380
381 381 try:
382 382 resp = getattr(remote, method)(*args, **kwargs)
383 383 except Exception as e:
384 384 exc_info = list(sys.exc_info())
385 385 exc_type, exc_value, exc_traceback = exc_info
386 386
387 387 org_exc = getattr(e, '_org_exc', None)
388 388 org_exc_name = None
389 389 org_exc_tb = ''
390 390 if org_exc:
391 391 org_exc_name = org_exc.__class__.__name__
392 392 org_exc_tb = getattr(e, '_org_exc_tb', '')
393 393 # replace our "faked" exception with our org
394 394 exc_info[0] = org_exc.__class__
395 395 exc_info[1] = org_exc
396 396
397 397 should_store_exc = True
398 398 if org_exc:
399 399 def get_exc_fqn(_exc_obj):
400 400 module_name = getattr(org_exc.__class__, '__module__', 'UNKNOWN')
401 401 return module_name + '.' + org_exc_name
402 402
403 403 exc_fqn = get_exc_fqn(org_exc)
404 404
405 405 if exc_fqn in ['mercurial.error.RepoLookupError',
406 406 'vcsserver.exceptions.RefNotFoundException']:
407 407 should_store_exc = False
408 408
409 409 if should_store_exc:
410 410 store_exception(id(exc_info), exc_info, request_path=request.path)
411 411
412 412 tb_info = format_exc(exc_info)
413 413
414 414 type_ = e.__class__.__name__
415 415 if type_ not in self.ALLOWED_EXCEPTIONS:
416 416 type_ = None
417 417
418 418 resp = {
419 419 'id': payload_id,
420 420 'error': {
421 421 'message': str(e),
422 422 'traceback': tb_info,
423 423 'org_exc': org_exc_name,
424 424 'org_exc_tb': org_exc_tb,
425 425 'type': type_
426 426 }
427 427 }
428 428
429 429 try:
430 430 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
431 431 except AttributeError:
432 432 pass
433 433 else:
434 434 resp = {
435 435 'id': payload_id,
436 436 'result': resp
437 437 }
438 438 log.debug('Serving data for method %s', method)
439 439 return resp
440 440
441 441 def vcs_stream_view(self, request):
442 442 payload, remote, method, args, kwargs = self._vcs_view_params(request)
443 443 # this method has a stream: marker we remove it here
444 444 method = method.split('stream:')[-1]
445 445 chunk_size = safe_int(payload.get('chunk_size')) or 4096
446 446
447 447 resp = getattr(remote, method)(*args, **kwargs)
448 448
449 449 def get_chunked_data(method_resp):
450 450 stream = io.BytesIO(method_resp)
451 451 while 1:
452 452 chunk = stream.read(chunk_size)
453 453 if not chunk:
454 454 break
455 455 yield chunk
456 456
457 457 response = Response(app_iter=get_chunked_data(resp))
458 458 response.content_type = 'application/octet-stream'
459 459
460 460 return response
461 461
462 462 def status_view(self, request):
463 463 import vcsserver
464 464 _platform_id = platform.uname()[1] or 'instance'
465 465
466 466 return {
467 467 "status": "OK",
468 "vcsserver_version": vcsserver.__version__,
468 "vcsserver_version": vcsserver.get_version(),
469 469 "platform": _platform_id,
470 470 "pid": os.getpid(),
471 471 }
472 472
473 473 def service_view(self, request):
474 474 import vcsserver
475 475
476 476 payload = msgpack.unpackb(request.body, use_list=True)
477 477 server_config, app_config = {}, {}
478 478
479 479 try:
480 480 path = self.global_config['__file__']
481 481 config = configparser.RawConfigParser()
482 482
483 483 config.read(path)
484 484
485 485 if config.has_section('server:main'):
486 486 server_config = dict(config.items('server:main'))
487 487 if config.has_section('app:main'):
488 488 app_config = dict(config.items('app:main'))
489 489
490 490 except Exception:
491 491 log.exception('Failed to read .ini file for display')
492 492
493 493 environ = list(os.environ.items())
494 494
495 495 resp = {
496 496 'id': payload.get('id'),
497 497 'result': dict(
498 version=vcsserver.__version__,
498 version=vcsserver.get_version(),
499 499 config=server_config,
500 500 app_config=app_config,
501 501 environ=environ,
502 502 payload=payload,
503 503 )
504 504 }
505 505 return resp
506 506
507 507 def _msgpack_renderer_factory(self, info):
508 508
509 509 def _render(value, system):
510 510 bin_type = False
511 511 res = value.get('result')
512 512 if isinstance(res, BytesEnvelope):
513 513 log.debug('Result is wrapped in BytesEnvelope type')
514 514 bin_type = True
515 515 elif isinstance(res, BinaryEnvelope):
516 516 log.debug('Result is wrapped in BinaryEnvelope type')
517 517 value['result'] = res.val
518 518 bin_type = True
519 519
520 520 request = system.get('request')
521 521 if request is not None:
522 522 response = request.response
523 523 ct = response.content_type
524 524 if ct == response.default_content_type:
525 525 response.content_type = 'application/x-msgpack'
526 526 if bin_type:
527 527 response.content_type = 'application/x-msgpack-bin'
528 528
529 529 return msgpack.packb(value, use_bin_type=bin_type)
530 530 return _render
531 531
532 532 def set_env_from_config(self, environ, config):
533 533 dict_conf = {}
534 534 try:
535 535 for elem in config:
536 536 if elem[0] == 'rhodecode':
537 537 dict_conf = json.loads(elem[2])
538 538 break
539 539 except Exception:
540 540 log.exception('Failed to fetch SCM CONFIG')
541 541 return
542 542
543 543 username = dict_conf.get('username')
544 544 if username:
545 545 environ['REMOTE_USER'] = username
546 546 # mercurial specific, some extension api rely on this
547 547 environ['HGUSER'] = username
548 548
549 549 ip = dict_conf.get('ip')
550 550 if ip:
551 551 environ['REMOTE_HOST'] = ip
552 552
553 553 if _is_request_chunked(environ):
554 554 # set the compatibility flag for webob
555 555 environ['wsgi.input_terminated'] = True
556 556
557 557 def hg_proxy(self):
558 558 @wsgiapp
559 559 def _hg_proxy(environ, start_response):
560 560 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
561 561 return app(environ, start_response)
562 562 return _hg_proxy
563 563
564 564 def git_proxy(self):
565 565 @wsgiapp
566 566 def _git_proxy(environ, start_response):
567 567 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
568 568 return app(environ, start_response)
569 569 return _git_proxy
570 570
571 571 def hg_stream(self):
572 572 if self._use_echo_app:
573 573 @wsgiapp
574 574 def _hg_stream(environ, start_response):
575 575 app = EchoApp('fake_path', 'fake_name', None)
576 576 return app(environ, start_response)
577 577 return _hg_stream
578 578 else:
579 579 @wsgiapp
580 580 def _hg_stream(environ, start_response):
581 581 log.debug('http-app: handling hg stream')
582 582 call_context = get_headers_call_context(environ)
583 583
584 584 repo_path = call_context['repo_path']
585 585 repo_name = call_context['repo_name']
586 586 config = call_context['repo_config']
587 587
588 588 app = scm_app.create_hg_wsgi_app(
589 589 repo_path, repo_name, config)
590 590
591 591 # Consistent path information for hgweb
592 592 environ['PATH_INFO'] = call_context['path_info']
593 593 environ['REPO_NAME'] = repo_name
594 594 self.set_env_from_config(environ, config)
595 595
596 596 log.debug('http-app: starting app handler '
597 597 'with %s and process request', app)
598 598 return app(environ, ResponseFilter(start_response))
599 599 return _hg_stream
600 600
601 601 def git_stream(self):
602 602 if self._use_echo_app:
603 603 @wsgiapp
604 604 def _git_stream(environ, start_response):
605 605 app = EchoApp('fake_path', 'fake_name', None)
606 606 return app(environ, start_response)
607 607 return _git_stream
608 608 else:
609 609 @wsgiapp
610 610 def _git_stream(environ, start_response):
611 611 log.debug('http-app: handling git stream')
612 612
613 613 call_context = get_headers_call_context(environ)
614 614
615 615 repo_path = call_context['repo_path']
616 616 repo_name = call_context['repo_name']
617 617 config = call_context['repo_config']
618 618
619 619 environ['PATH_INFO'] = call_context['path_info']
620 620 self.set_env_from_config(environ, config)
621 621
622 622 content_type = environ.get('CONTENT_TYPE', '')
623 623
624 624 path = environ['PATH_INFO']
625 625 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
626 626 log.debug(
627 627 'LFS: Detecting if request `%s` is LFS server path based '
628 628 'on content type:`%s`, is_lfs:%s',
629 629 path, content_type, is_lfs_request)
630 630
631 631 if not is_lfs_request:
632 632 # fallback detection by path
633 633 if GIT_LFS_PROTO_PAT.match(path):
634 634 is_lfs_request = True
635 635 log.debug(
636 636 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
637 637 path, is_lfs_request)
638 638
639 639 if is_lfs_request:
640 640 app = scm_app.create_git_lfs_wsgi_app(
641 641 repo_path, repo_name, config)
642 642 else:
643 643 app = scm_app.create_git_wsgi_app(
644 644 repo_path, repo_name, config)
645 645
646 646 log.debug('http-app: starting app handler '
647 647 'with %s and process request', app)
648 648
649 649 return app(environ, start_response)
650 650
651 651 return _git_stream
652 652
653 653 def handle_vcs_exception(self, exception, request):
654 654 _vcs_kind = getattr(exception, '_vcs_kind', '')
655 655
656 656 if _vcs_kind == 'repo_locked':
657 657 headers_call_context = get_headers_call_context(request.environ)
658 658 status_code = safe_int(headers_call_context['locked_status_code'])
659 659
660 660 return HTTPRepoLocked(
661 661 title=str(exception), status_code=status_code, headers=[('X-Rc-Locked', '1')])
662 662
663 663 elif _vcs_kind == 'repo_branch_protected':
664 664 # Get custom repo-branch-protected status code if present.
665 665 return HTTPRepoBranchProtected(
666 666 title=str(exception), headers=[('X-Rc-Branch-Protection', '1')])
667 667
668 668 exc_info = request.exc_info
669 669 store_exception(id(exc_info), exc_info)
670 670
671 671 traceback_info = 'unavailable'
672 672 if request.exc_info:
673 673 traceback_info = format_exc(request.exc_info)
674 674
675 675 log.error(
676 676 'error occurred handling this request for path: %s, \n%s',
677 677 request.path, traceback_info)
678 678
679 679 statsd = request.registry.statsd
680 680 if statsd:
681 681 exc_type = f"{exception.__class__.__module__}.{exception.__class__.__name__}"
682 682 statsd.incr('vcsserver_exception_total',
683 683 tags=[f"type:{exc_type}"])
684 684 raise exception
685 685
686 686
687 687 class ResponseFilter:
688 688
689 689 def __init__(self, start_response):
690 690 self._start_response = start_response
691 691
692 692 def __call__(self, status, response_headers, exc_info=None):
693 693 headers = tuple(
694 694 (h, v) for h, v in response_headers
695 695 if not wsgiref.util.is_hop_by_hop(h))
696 696 return self._start_response(status, headers, exc_info)
697 697
698 698
699 699 def sanitize_settings_and_apply_defaults(global_config, settings):
700 700 _global_settings_maker = SettingsMaker(global_config)
701 701 settings_maker = SettingsMaker(settings)
702 702
703 703 settings_maker.make_setting('logging.autoconfigure', False, parser='bool')
704 704
705 705 logging_conf = os.path.join(os.path.dirname(global_config.get('__file__')), 'logging.ini')
706 706 settings_maker.enable_logging(logging_conf)
707 707
708 708 # Default includes, possible to change as a user
709 709 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
710 710 log.debug("Using the following pyramid.includes: %s", pyramid_includes)
711 711
712 712 settings_maker.make_setting('__file__', global_config.get('__file__'))
713 713
714 714 settings_maker.make_setting('pyramid.default_locale_name', 'en')
715 715 settings_maker.make_setting('locale', 'en_US.UTF-8')
716 716
717 717 settings_maker.make_setting('core.binary_dir', '')
718 718
719 719 temp_store = tempfile.gettempdir()
720 720 default_cache_dir = os.path.join(temp_store, 'rc_cache')
721 721 # save default, cache dir, and use it for all backends later.
722 722 default_cache_dir = settings_maker.make_setting(
723 723 'cache_dir',
724 724 default=default_cache_dir, default_when_empty=True,
725 725 parser='dir:ensured')
726 726
727 727 # exception store cache
728 728 settings_maker.make_setting(
729 729 'exception_tracker.store_path',
730 730 default=os.path.join(default_cache_dir, 'exc_store'), default_when_empty=True,
731 731 parser='dir:ensured'
732 732 )
733 733
734 734 # repo_object cache defaults
735 735 settings_maker.make_setting(
736 736 'rc_cache.repo_object.backend',
737 737 default='dogpile.cache.rc.file_namespace',
738 738 parser='string')
739 739 settings_maker.make_setting(
740 740 'rc_cache.repo_object.expiration_time',
741 741 default=30 * 24 * 60 * 60, # 30days
742 742 parser='int')
743 743 settings_maker.make_setting(
744 744 'rc_cache.repo_object.arguments.filename',
745 745 default=os.path.join(default_cache_dir, 'vcsserver_cache_repo_object.db'),
746 746 parser='string')
747 747
748 748 # statsd
749 749 settings_maker.make_setting('statsd.enabled', False, parser='bool')
750 750 settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string')
751 751 settings_maker.make_setting('statsd.statsd_port', 9125, parser='int')
752 752 settings_maker.make_setting('statsd.statsd_prefix', '')
753 753 settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool')
754 754
755 755 settings_maker.env_expand()
756 756
757 757
758 758 def main(global_config, **settings):
759 759 start_time = time.time()
760 760 log.info('Pyramid app config starting')
761 761
762 762 if MercurialFactory:
763 763 hgpatches.patch_largefiles_capabilities()
764 764 hgpatches.patch_subrepo_type_mapping()
765 765
766 766 # Fill in and sanitize the defaults & do ENV expansion
767 767 sanitize_settings_and_apply_defaults(global_config, settings)
768 768
769 769 # init and bootstrap StatsdClient
770 770 StatsdClient.setup(settings)
771 771
772 772 pyramid_app = HTTPApplication(settings=settings, global_config=global_config).wsgi_app()
773 773 total_time = time.time() - start_time
774 774 log.info('Pyramid app created and configured in %.2fs', total_time)
775 775 return pyramid_app
@@ -1,1213 +1,1213 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17 import binascii
18 18 import io
19 19 import logging
20 20 import stat
21 21 import sys
22 22 import urllib.request
23 23 import urllib.parse
24 24 import hashlib
25 25
26 26 from hgext import largefiles, rebase, purge
27 27
28 28 from mercurial import commands
29 29 from mercurial import unionrepo
30 30 from mercurial import verify
31 31 from mercurial import repair
32 32 from mercurial.error import AmbiguousPrefixLookupError
33 33
34 34 import vcsserver
35 35 from vcsserver import exceptions
36 36 from vcsserver.base import (
37 37 RepoFactory,
38 38 obfuscate_qs,
39 39 raise_from_original,
40 40 store_archive_in_cache,
41 41 ArchiveNode,
42 42 BytesEnvelope,
43 43 BinaryEnvelope,
44 44 )
45 45 from vcsserver.hgcompat import (
46 46 archival,
47 47 bin,
48 48 clone,
49 49 config as hgconfig,
50 50 diffopts,
51 51 hex,
52 52 get_ctx,
53 53 hg_url as url_parser,
54 54 httpbasicauthhandler,
55 55 httpdigestauthhandler,
56 56 makepeer,
57 57 instance,
58 58 match,
59 59 memctx,
60 60 exchange,
61 61 memfilectx,
62 62 nullrev,
63 63 hg_merge,
64 64 patch,
65 65 peer,
66 66 revrange,
67 67 ui,
68 68 hg_tag,
69 69 Abort,
70 70 LookupError,
71 71 RepoError,
72 72 RepoLookupError,
73 73 InterventionRequired,
74 74 RequirementError,
75 75 alwaysmatcher,
76 76 patternmatcher,
77 77 hgutil,
78 78 hgext_strip,
79 79 )
80 80 from vcsserver.str_utils import ascii_bytes, ascii_str, safe_str, safe_bytes
81 81 from vcsserver.vcs_base import RemoteBase
82 82 from vcsserver.config import hooks as hooks_config
83 83 from vcsserver.lib.exc_tracking import format_exc
84 84
85 85 log = logging.getLogger(__name__)
86 86
87 87
88 88 def make_ui_from_config(repo_config):
89 89
90 90 class LoggingUI(ui.ui):
91 91
92 92 def status(self, *msg, **opts):
93 93 str_msg = map(safe_str, msg)
94 94 log.info(' '.join(str_msg).rstrip('\n'))
95 95 #super(LoggingUI, self).status(*msg, **opts)
96 96
97 97 def warn(self, *msg, **opts):
98 98 str_msg = map(safe_str, msg)
99 99 log.warning('ui_logger:'+' '.join(str_msg).rstrip('\n'))
100 100 #super(LoggingUI, self).warn(*msg, **opts)
101 101
102 102 def error(self, *msg, **opts):
103 103 str_msg = map(safe_str, msg)
104 104 log.error('ui_logger:'+' '.join(str_msg).rstrip('\n'))
105 105 #super(LoggingUI, self).error(*msg, **opts)
106 106
107 107 def note(self, *msg, **opts):
108 108 str_msg = map(safe_str, msg)
109 109 log.info('ui_logger:'+' '.join(str_msg).rstrip('\n'))
110 110 #super(LoggingUI, self).note(*msg, **opts)
111 111
112 112 def debug(self, *msg, **opts):
113 113 str_msg = map(safe_str, msg)
114 114 log.debug('ui_logger:'+' '.join(str_msg).rstrip('\n'))
115 115 #super(LoggingUI, self).debug(*msg, **opts)
116 116
117 117 baseui = LoggingUI()
118 118
119 119 # clean the baseui object
120 120 baseui._ocfg = hgconfig.config()
121 121 baseui._ucfg = hgconfig.config()
122 122 baseui._tcfg = hgconfig.config()
123 123
124 124 for section, option, value in repo_config:
125 125 baseui.setconfig(ascii_bytes(section), ascii_bytes(option), ascii_bytes(value))
126 126
127 127 # make our hgweb quiet so it doesn't print output
128 128 baseui.setconfig(b'ui', b'quiet', b'true')
129 129
130 130 baseui.setconfig(b'ui', b'paginate', b'never')
131 131 # for better Error reporting of Mercurial
132 132 baseui.setconfig(b'ui', b'message-output', b'stderr')
133 133
134 134 # force mercurial to only use 1 thread, otherwise it may try to set a
135 135 # signal in a non-main thread, thus generating a ValueError.
136 136 baseui.setconfig(b'worker', b'numcpus', 1)
137 137
138 138 # If there is no config for the largefiles extension, we explicitly disable
139 139 # it here. This overrides settings from repositories hgrc file. Recent
140 140 # mercurial versions enable largefiles in hgrc on clone from largefile
141 141 # repo.
142 142 if not baseui.hasconfig(b'extensions', b'largefiles'):
143 143 log.debug('Explicitly disable largefiles extension for repo.')
144 144 baseui.setconfig(b'extensions', b'largefiles', b'!')
145 145
146 146 return baseui
147 147
148 148
149 149 def reraise_safe_exceptions(func):
150 150 """Decorator for converting mercurial exceptions to something neutral."""
151 151
152 152 def wrapper(*args, **kwargs):
153 153 try:
154 154 return func(*args, **kwargs)
155 155 except (Abort, InterventionRequired) as e:
156 156 raise_from_original(exceptions.AbortException(e), e)
157 157 except RepoLookupError as e:
158 158 raise_from_original(exceptions.LookupException(e), e)
159 159 except RequirementError as e:
160 160 raise_from_original(exceptions.RequirementException(e), e)
161 161 except RepoError as e:
162 162 raise_from_original(exceptions.VcsException(e), e)
163 163 except LookupError as e:
164 164 raise_from_original(exceptions.LookupException(e), e)
165 165 except Exception as e:
166 166 if not hasattr(e, '_vcs_kind'):
167 167 log.exception("Unhandled exception in hg remote call")
168 168 raise_from_original(exceptions.UnhandledException(e), e)
169 169
170 170 raise
171 171 return wrapper
172 172
173 173
174 174 class MercurialFactory(RepoFactory):
175 175 repo_type = 'hg'
176 176
177 177 def _create_config(self, config, hooks=True):
178 178 if not hooks:
179 179
180 180 hooks_to_clean = {
181 181
182 182 hooks_config.HOOK_REPO_SIZE,
183 183 hooks_config.HOOK_PRE_PULL,
184 184 hooks_config.HOOK_PULL,
185 185
186 186 hooks_config.HOOK_PRE_PUSH,
187 187 # TODO: what about PRETXT, this was disabled in pre 5.0.0
188 188 hooks_config.HOOK_PRETX_PUSH,
189 189
190 190 }
191 191 new_config = []
192 192 for section, option, value in config:
193 193 if section == 'hooks' and option in hooks_to_clean:
194 194 continue
195 195 new_config.append((section, option, value))
196 196 config = new_config
197 197
198 198 baseui = make_ui_from_config(config)
199 199 return baseui
200 200
201 201 def _create_repo(self, wire, create):
202 202 baseui = self._create_config(wire["config"])
203 203 repo = instance(baseui, safe_bytes(wire["path"]), create)
204 204 log.debug('repository created: got HG object: %s', repo)
205 205 return repo
206 206
207 207 def repo(self, wire, create=False):
208 208 """
209 209 Get a repository instance for the given path.
210 210 """
211 211 return self._create_repo(wire, create)
212 212
213 213
214 214 def patch_ui_message_output(baseui):
215 215 baseui.setconfig(b'ui', b'quiet', b'false')
216 216 output = io.BytesIO()
217 217
218 218 def write(data, **unused_kwargs):
219 219 output.write(data)
220 220
221 221 baseui.status = write
222 222 baseui.write = write
223 223 baseui.warn = write
224 224 baseui.debug = write
225 225
226 226 return baseui, output
227 227
228 228
229 229 def get_obfuscated_url(url_obj):
230 230 url_obj.passwd = b'*****' if url_obj.passwd else url_obj.passwd
231 231 url_obj.query = obfuscate_qs(url_obj.query)
232 232 obfuscated_uri = str(url_obj)
233 233 return obfuscated_uri
234 234
235 235
236 236 def normalize_url_for_hg(url: str):
237 237 _proto = None
238 238
239 239 if '+' in url[:url.find('://')]:
240 240 _proto = url[0:url.find('+')]
241 241 url = url[url.find('+') + 1:]
242 242 return url, _proto
243 243
244 244
245 245 class HgRemote(RemoteBase):
246 246
247 247 def __init__(self, factory):
248 248 self._factory = factory
249 249 self._bulk_methods = {
250 250 "affected_files": self.ctx_files,
251 251 "author": self.ctx_user,
252 252 "branch": self.ctx_branch,
253 253 "children": self.ctx_children,
254 254 "date": self.ctx_date,
255 255 "message": self.ctx_description,
256 256 "parents": self.ctx_parents,
257 257 "status": self.ctx_status,
258 258 "obsolete": self.ctx_obsolete,
259 259 "phase": self.ctx_phase,
260 260 "hidden": self.ctx_hidden,
261 261 "_file_paths": self.ctx_list,
262 262 }
263 263 self._bulk_file_methods = {
264 264 "size": self.fctx_size,
265 265 "data": self.fctx_node_data,
266 266 "flags": self.fctx_flags,
267 267 "is_binary": self.is_binary,
268 268 "md5": self.md5_hash,
269 269 }
270 270
271 271 def _get_ctx(self, repo, ref):
272 272 return get_ctx(repo, ref)
273 273
274 274 @reraise_safe_exceptions
275 275 def discover_hg_version(self):
276 276 from mercurial import util
277 277 return safe_str(util.version())
278 278
279 279 @reraise_safe_exceptions
280 280 def is_empty(self, wire):
281 281 repo = self._factory.repo(wire)
282 282
283 283 try:
284 284 return len(repo) == 0
285 285 except Exception:
286 286 log.exception("failed to read object_store")
287 287 return False
288 288
289 289 @reraise_safe_exceptions
290 290 def bookmarks(self, wire):
291 291 cache_on, context_uid, repo_id = self._cache_on(wire)
292 292 region = self._region(wire)
293 293
294 294 @region.conditional_cache_on_arguments(condition=cache_on)
295 295 def _bookmarks(_context_uid, _repo_id):
296 296 repo = self._factory.repo(wire)
297 297 return {safe_str(name): ascii_str(hex(sha)) for name, sha in repo._bookmarks.items()}
298 298
299 299 return _bookmarks(context_uid, repo_id)
300 300
301 301 @reraise_safe_exceptions
302 302 def branches(self, wire, normal, closed):
303 303 cache_on, context_uid, repo_id = self._cache_on(wire)
304 304 region = self._region(wire)
305 305
306 306 @region.conditional_cache_on_arguments(condition=cache_on)
307 307 def _branches(_context_uid, _repo_id, _normal, _closed):
308 308 repo = self._factory.repo(wire)
309 309 iter_branches = repo.branchmap().iterbranches()
310 310 bt = {}
311 311 for branch_name, _heads, tip_node, is_closed in iter_branches:
312 312 if normal and not is_closed:
313 313 bt[safe_str(branch_name)] = ascii_str(hex(tip_node))
314 314 if closed and is_closed:
315 315 bt[safe_str(branch_name)] = ascii_str(hex(tip_node))
316 316
317 317 return bt
318 318
319 319 return _branches(context_uid, repo_id, normal, closed)
320 320
321 321 @reraise_safe_exceptions
322 322 def bulk_request(self, wire, commit_id, pre_load):
323 323 cache_on, context_uid, repo_id = self._cache_on(wire)
324 324 region = self._region(wire)
325 325
326 326 @region.conditional_cache_on_arguments(condition=cache_on)
327 327 def _bulk_request(_repo_id, _commit_id, _pre_load):
328 328 result = {}
329 329 for attr in pre_load:
330 330 try:
331 331 method = self._bulk_methods[attr]
332 332 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
333 333 result[attr] = method(wire, commit_id)
334 334 except KeyError as e:
335 335 raise exceptions.VcsException(e)(
336 336 f'Unknown bulk attribute: "{attr}"')
337 337 return result
338 338
339 339 return _bulk_request(repo_id, commit_id, sorted(pre_load))
340 340
341 341 @reraise_safe_exceptions
342 342 def ctx_branch(self, wire, commit_id):
343 343 cache_on, context_uid, repo_id = self._cache_on(wire)
344 344 region = self._region(wire)
345 345
346 346 @region.conditional_cache_on_arguments(condition=cache_on)
347 347 def _ctx_branch(_repo_id, _commit_id):
348 348 repo = self._factory.repo(wire)
349 349 ctx = self._get_ctx(repo, commit_id)
350 350 return ctx.branch()
351 351 return _ctx_branch(repo_id, commit_id)
352 352
353 353 @reraise_safe_exceptions
354 354 def ctx_date(self, wire, commit_id):
355 355 cache_on, context_uid, repo_id = self._cache_on(wire)
356 356 region = self._region(wire)
357 357
358 358 @region.conditional_cache_on_arguments(condition=cache_on)
359 359 def _ctx_date(_repo_id, _commit_id):
360 360 repo = self._factory.repo(wire)
361 361 ctx = self._get_ctx(repo, commit_id)
362 362 return ctx.date()
363 363 return _ctx_date(repo_id, commit_id)
364 364
365 365 @reraise_safe_exceptions
366 366 def ctx_description(self, wire, revision):
367 367 repo = self._factory.repo(wire)
368 368 ctx = self._get_ctx(repo, revision)
369 369 return ctx.description()
370 370
371 371 @reraise_safe_exceptions
372 372 def ctx_files(self, wire, commit_id):
373 373 cache_on, context_uid, repo_id = self._cache_on(wire)
374 374 region = self._region(wire)
375 375
376 376 @region.conditional_cache_on_arguments(condition=cache_on)
377 377 def _ctx_files(_repo_id, _commit_id):
378 378 repo = self._factory.repo(wire)
379 379 ctx = self._get_ctx(repo, commit_id)
380 380 return ctx.files()
381 381
382 382 return _ctx_files(repo_id, commit_id)
383 383
384 384 @reraise_safe_exceptions
385 385 def ctx_list(self, path, revision):
386 386 repo = self._factory.repo(path)
387 387 ctx = self._get_ctx(repo, revision)
388 388 return list(ctx)
389 389
390 390 @reraise_safe_exceptions
391 391 def ctx_parents(self, wire, commit_id):
392 392 cache_on, context_uid, repo_id = self._cache_on(wire)
393 393 region = self._region(wire)
394 394
395 395 @region.conditional_cache_on_arguments(condition=cache_on)
396 396 def _ctx_parents(_repo_id, _commit_id):
397 397 repo = self._factory.repo(wire)
398 398 ctx = self._get_ctx(repo, commit_id)
399 399 return [parent.hex() for parent in ctx.parents()
400 400 if not (parent.hidden() or parent.obsolete())]
401 401
402 402 return _ctx_parents(repo_id, commit_id)
403 403
404 404 @reraise_safe_exceptions
405 405 def ctx_children(self, wire, commit_id):
406 406 cache_on, context_uid, repo_id = self._cache_on(wire)
407 407 region = self._region(wire)
408 408
409 409 @region.conditional_cache_on_arguments(condition=cache_on)
410 410 def _ctx_children(_repo_id, _commit_id):
411 411 repo = self._factory.repo(wire)
412 412 ctx = self._get_ctx(repo, commit_id)
413 413 return [child.hex() for child in ctx.children()
414 414 if not (child.hidden() or child.obsolete())]
415 415
416 416 return _ctx_children(repo_id, commit_id)
417 417
418 418 @reraise_safe_exceptions
419 419 def ctx_phase(self, wire, commit_id):
420 420 cache_on, context_uid, repo_id = self._cache_on(wire)
421 421 region = self._region(wire)
422 422
423 423 @region.conditional_cache_on_arguments(condition=cache_on)
424 424 def _ctx_phase(_context_uid, _repo_id, _commit_id):
425 425 repo = self._factory.repo(wire)
426 426 ctx = self._get_ctx(repo, commit_id)
427 427 # public=0, draft=1, secret=3
428 428 return ctx.phase()
429 429 return _ctx_phase(context_uid, repo_id, commit_id)
430 430
431 431 @reraise_safe_exceptions
432 432 def ctx_obsolete(self, wire, commit_id):
433 433 cache_on, context_uid, repo_id = self._cache_on(wire)
434 434 region = self._region(wire)
435 435
436 436 @region.conditional_cache_on_arguments(condition=cache_on)
437 437 def _ctx_obsolete(_context_uid, _repo_id, _commit_id):
438 438 repo = self._factory.repo(wire)
439 439 ctx = self._get_ctx(repo, commit_id)
440 440 return ctx.obsolete()
441 441 return _ctx_obsolete(context_uid, repo_id, commit_id)
442 442
443 443 @reraise_safe_exceptions
444 444 def ctx_hidden(self, wire, commit_id):
445 445 cache_on, context_uid, repo_id = self._cache_on(wire)
446 446 region = self._region(wire)
447 447
448 448 @region.conditional_cache_on_arguments(condition=cache_on)
449 449 def _ctx_hidden(_context_uid, _repo_id, _commit_id):
450 450 repo = self._factory.repo(wire)
451 451 ctx = self._get_ctx(repo, commit_id)
452 452 return ctx.hidden()
453 453 return _ctx_hidden(context_uid, repo_id, commit_id)
454 454
455 455 @reraise_safe_exceptions
456 456 def ctx_substate(self, wire, revision):
457 457 repo = self._factory.repo(wire)
458 458 ctx = self._get_ctx(repo, revision)
459 459 return ctx.substate
460 460
461 461 @reraise_safe_exceptions
462 462 def ctx_status(self, wire, revision):
463 463 repo = self._factory.repo(wire)
464 464 ctx = self._get_ctx(repo, revision)
465 465 status = repo[ctx.p1().node()].status(other=ctx.node())
466 466 # object of status (odd, custom named tuple in mercurial) is not
467 467 # correctly serializable, we make it a list, as the underling
468 468 # API expects this to be a list
469 469 return list(status)
470 470
471 471 @reraise_safe_exceptions
472 472 def ctx_user(self, wire, revision):
473 473 repo = self._factory.repo(wire)
474 474 ctx = self._get_ctx(repo, revision)
475 475 return ctx.user()
476 476
477 477 @reraise_safe_exceptions
478 478 def check_url(self, url, config):
479 479 url, _proto = normalize_url_for_hg(url)
480 480 url_obj = url_parser(safe_bytes(url))
481 481
482 482 test_uri = safe_str(url_obj.authinfo()[0])
483 483 authinfo = url_obj.authinfo()[1]
484 484 obfuscated_uri = get_obfuscated_url(url_obj)
485 485 log.info("Checking URL for remote cloning/import: %s", obfuscated_uri)
486 486
487 487 handlers = []
488 488 if authinfo:
489 489 # create a password manager
490 490 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
491 491 passmgr.add_password(*authinfo)
492 492
493 493 handlers.extend((httpbasicauthhandler(passmgr),
494 494 httpdigestauthhandler(passmgr)))
495 495
496 496 o = urllib.request.build_opener(*handlers)
497 497 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
498 498 ('Accept', 'application/mercurial-0.1')]
499 499
500 500 q = {"cmd": 'between'}
501 501 q.update({'pairs': "{}-{}".format('0' * 40, '0' * 40)})
502 502 qs = f'?{urllib.parse.urlencode(q)}'
503 503 cu = f"{test_uri}{qs}"
504 504
505 505 try:
506 506 req = urllib.request.Request(cu, None, {})
507 507 log.debug("Trying to open URL %s", obfuscated_uri)
508 508 resp = o.open(req)
509 509 if resp.code != 200:
510 510 raise exceptions.URLError()('Return Code is not 200')
511 511 except Exception as e:
512 512 log.warning("URL cannot be opened: %s", obfuscated_uri, exc_info=True)
513 513 # means it cannot be cloned
514 514 raise exceptions.URLError(e)(f"[{obfuscated_uri}] org_exc: {e}")
515 515
516 516 # now check if it's a proper hg repo, but don't do it for svn
517 517 try:
518 518 if _proto == 'svn':
519 519 pass
520 520 else:
521 521 # check for pure hg repos
522 522 log.debug(
523 523 "Verifying if URL is a Mercurial repository: %s", obfuscated_uri)
524 524 ui = make_ui_from_config(config)
525 525 peer_checker = makepeer(ui, safe_bytes(url))
526 526 peer_checker.lookup(b'tip')
527 527 except Exception as e:
528 528 log.warning("URL is not a valid Mercurial repository: %s",
529 529 obfuscated_uri)
530 530 raise exceptions.URLError(e)(
531 531 f"url [{obfuscated_uri}] does not look like an hg repo org_exc: {e}")
532 532
533 533 log.info("URL is a valid Mercurial repository: %s", obfuscated_uri)
534 534 return True
535 535
536 536 @reraise_safe_exceptions
537 537 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_git, opt_ignorews, context):
538 538 repo = self._factory.repo(wire)
539 539
540 540 if file_filter:
541 541 # unpack the file-filter
542 542 repo_path, node_path = file_filter
543 543 match_filter = match(safe_bytes(repo_path), b'', [safe_bytes(node_path)])
544 544 else:
545 545 match_filter = file_filter
546 546 opts = diffopts(git=opt_git, ignorews=opt_ignorews, context=context, showfunc=1)
547 547
548 548 try:
549 549 diff_iter = patch.diff(
550 550 repo, node1=commit_id_1, node2=commit_id_2, match=match_filter, opts=opts)
551 551 return BytesEnvelope(b"".join(diff_iter))
552 552 except RepoLookupError as e:
553 553 raise exceptions.LookupException(e)()
554 554
555 555 @reraise_safe_exceptions
556 556 def node_history(self, wire, revision, path, limit):
557 557 cache_on, context_uid, repo_id = self._cache_on(wire)
558 558 region = self._region(wire)
559 559
560 560 @region.conditional_cache_on_arguments(condition=cache_on)
561 561 def _node_history(_context_uid, _repo_id, _revision, _path, _limit):
562 562 repo = self._factory.repo(wire)
563 563
564 564 ctx = self._get_ctx(repo, revision)
565 565 fctx = ctx.filectx(safe_bytes(path))
566 566
567 567 def history_iter():
568 568 limit_rev = fctx.rev()
569 569
570 570 for fctx_candidate in reversed(list(fctx.filelog())):
571 571 f_obj = fctx.filectx(fctx_candidate)
572 572
573 573 # NOTE: This can be problematic...we can hide ONLY history node resulting in empty history
574 574 _ctx = f_obj.changectx()
575 575 if _ctx.hidden() or _ctx.obsolete():
576 576 continue
577 577
578 578 if limit_rev >= f_obj.rev():
579 579 yield f_obj
580 580
581 581 history = []
582 582 for cnt, obj in enumerate(history_iter()):
583 583 if limit and cnt >= limit:
584 584 break
585 585 history.append(hex(obj.node()))
586 586
587 587 return [x for x in history]
588 588 return _node_history(context_uid, repo_id, revision, path, limit)
589 589
590 590 @reraise_safe_exceptions
591 591 def node_history_until(self, wire, revision, path, limit):
592 592 cache_on, context_uid, repo_id = self._cache_on(wire)
593 593 region = self._region(wire)
594 594
595 595 @region.conditional_cache_on_arguments(condition=cache_on)
596 596 def _node_history_until(_context_uid, _repo_id):
597 597 repo = self._factory.repo(wire)
598 598 ctx = self._get_ctx(repo, revision)
599 599 fctx = ctx.filectx(safe_bytes(path))
600 600
601 601 file_log = list(fctx.filelog())
602 602 if limit:
603 603 # Limit to the last n items
604 604 file_log = file_log[-limit:]
605 605
606 606 return [hex(fctx.filectx(cs).node()) for cs in reversed(file_log)]
607 607 return _node_history_until(context_uid, repo_id, revision, path, limit)
608 608
609 609 @reraise_safe_exceptions
610 610 def bulk_file_request(self, wire, commit_id, path, pre_load):
611 611 cache_on, context_uid, repo_id = self._cache_on(wire)
612 612 region = self._region(wire)
613 613
614 614 @region.conditional_cache_on_arguments(condition=cache_on)
615 615 def _bulk_file_request(_repo_id, _commit_id, _path, _pre_load):
616 616 result = {}
617 617 for attr in pre_load:
618 618 try:
619 619 method = self._bulk_file_methods[attr]
620 620 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
621 621 result[attr] = method(wire, _commit_id, _path)
622 622 except KeyError as e:
623 623 raise exceptions.VcsException(e)(f'Unknown bulk attribute: "{attr}"')
624 624 return result
625 625
626 626 return BinaryEnvelope(_bulk_file_request(repo_id, commit_id, path, sorted(pre_load)))
627 627
628 628 @reraise_safe_exceptions
629 629 def fctx_annotate(self, wire, revision, path):
630 630 repo = self._factory.repo(wire)
631 631 ctx = self._get_ctx(repo, revision)
632 632 fctx = ctx.filectx(safe_bytes(path))
633 633
634 634 result = []
635 635 for i, annotate_obj in enumerate(fctx.annotate(), 1):
636 636 ln_no = i
637 637 sha = hex(annotate_obj.fctx.node())
638 638 content = annotate_obj.text
639 639 result.append((ln_no, ascii_str(sha), content))
640 640 return BinaryEnvelope(result)
641 641
642 642 @reraise_safe_exceptions
643 643 def fctx_node_data(self, wire, revision, path):
644 644 repo = self._factory.repo(wire)
645 645 ctx = self._get_ctx(repo, revision)
646 646 fctx = ctx.filectx(safe_bytes(path))
647 647 return BytesEnvelope(fctx.data())
648 648
649 649 @reraise_safe_exceptions
650 650 def fctx_flags(self, wire, commit_id, path):
651 651 cache_on, context_uid, repo_id = self._cache_on(wire)
652 652 region = self._region(wire)
653 653
654 654 @region.conditional_cache_on_arguments(condition=cache_on)
655 655 def _fctx_flags(_repo_id, _commit_id, _path):
656 656 repo = self._factory.repo(wire)
657 657 ctx = self._get_ctx(repo, commit_id)
658 658 fctx = ctx.filectx(safe_bytes(path))
659 659 return fctx.flags()
660 660
661 661 return _fctx_flags(repo_id, commit_id, path)
662 662
663 663 @reraise_safe_exceptions
664 664 def fctx_size(self, wire, commit_id, path):
665 665 cache_on, context_uid, repo_id = self._cache_on(wire)
666 666 region = self._region(wire)
667 667
668 668 @region.conditional_cache_on_arguments(condition=cache_on)
669 669 def _fctx_size(_repo_id, _revision, _path):
670 670 repo = self._factory.repo(wire)
671 671 ctx = self._get_ctx(repo, commit_id)
672 672 fctx = ctx.filectx(safe_bytes(path))
673 673 return fctx.size()
674 674 return _fctx_size(repo_id, commit_id, path)
675 675
676 676 @reraise_safe_exceptions
677 677 def get_all_commit_ids(self, wire, name):
678 678 cache_on, context_uid, repo_id = self._cache_on(wire)
679 679 region = self._region(wire)
680 680
681 681 @region.conditional_cache_on_arguments(condition=cache_on)
682 682 def _get_all_commit_ids(_context_uid, _repo_id, _name):
683 683 repo = self._factory.repo(wire)
684 684 revs = [ascii_str(repo[x].hex()) for x in repo.filtered(b'visible').changelog.revs()]
685 685 return revs
686 686 return _get_all_commit_ids(context_uid, repo_id, name)
687 687
688 688 @reraise_safe_exceptions
689 689 def get_config_value(self, wire, section, name, untrusted=False):
690 690 repo = self._factory.repo(wire)
691 691 return repo.ui.config(ascii_bytes(section), ascii_bytes(name), untrusted=untrusted)
692 692
693 693 @reraise_safe_exceptions
694 694 def is_large_file(self, wire, commit_id, path):
695 695 cache_on, context_uid, repo_id = self._cache_on(wire)
696 696 region = self._region(wire)
697 697
698 698 @region.conditional_cache_on_arguments(condition=cache_on)
699 699 def _is_large_file(_context_uid, _repo_id, _commit_id, _path):
700 700 return largefiles.lfutil.isstandin(safe_bytes(path))
701 701
702 702 return _is_large_file(context_uid, repo_id, commit_id, path)
703 703
704 704 @reraise_safe_exceptions
705 705 def is_binary(self, wire, revision, path):
706 706 cache_on, context_uid, repo_id = self._cache_on(wire)
707 707 region = self._region(wire)
708 708
709 709 @region.conditional_cache_on_arguments(condition=cache_on)
710 710 def _is_binary(_repo_id, _sha, _path):
711 711 repo = self._factory.repo(wire)
712 712 ctx = self._get_ctx(repo, revision)
713 713 fctx = ctx.filectx(safe_bytes(path))
714 714 return fctx.isbinary()
715 715
716 716 return _is_binary(repo_id, revision, path)
717 717
718 718 @reraise_safe_exceptions
719 719 def md5_hash(self, wire, revision, path):
720 720 cache_on, context_uid, repo_id = self._cache_on(wire)
721 721 region = self._region(wire)
722 722
723 723 @region.conditional_cache_on_arguments(condition=cache_on)
724 724 def _md5_hash(_repo_id, _sha, _path):
725 725 repo = self._factory.repo(wire)
726 726 ctx = self._get_ctx(repo, revision)
727 727 fctx = ctx.filectx(safe_bytes(path))
728 728 return hashlib.md5(fctx.data()).hexdigest()
729 729
730 730 return _md5_hash(repo_id, revision, path)
731 731
732 732 @reraise_safe_exceptions
733 733 def in_largefiles_store(self, wire, sha):
734 734 repo = self._factory.repo(wire)
735 735 return largefiles.lfutil.instore(repo, sha)
736 736
737 737 @reraise_safe_exceptions
738 738 def in_user_cache(self, wire, sha):
739 739 repo = self._factory.repo(wire)
740 740 return largefiles.lfutil.inusercache(repo.ui, sha)
741 741
742 742 @reraise_safe_exceptions
743 743 def store_path(self, wire, sha):
744 744 repo = self._factory.repo(wire)
745 745 return largefiles.lfutil.storepath(repo, sha)
746 746
747 747 @reraise_safe_exceptions
748 748 def link(self, wire, sha, path):
749 749 repo = self._factory.repo(wire)
750 750 largefiles.lfutil.link(
751 751 largefiles.lfutil.usercachepath(repo.ui, sha), path)
752 752
753 753 @reraise_safe_exceptions
754 754 def localrepository(self, wire, create=False):
755 755 self._factory.repo(wire, create=create)
756 756
757 757 @reraise_safe_exceptions
758 758 def lookup(self, wire, revision, both):
759 759 cache_on, context_uid, repo_id = self._cache_on(wire)
760 760 region = self._region(wire)
761 761
762 762 @region.conditional_cache_on_arguments(condition=cache_on)
763 763 def _lookup(_context_uid, _repo_id, _revision, _both):
764 764 repo = self._factory.repo(wire)
765 765 rev = _revision
766 766 if isinstance(rev, int):
767 767 # NOTE(marcink):
768 768 # since Mercurial doesn't support negative indexes properly
769 769 # we need to shift accordingly by one to get proper index, e.g
770 770 # repo[-1] => repo[-2]
771 771 # repo[0] => repo[-1]
772 772 if rev <= 0:
773 773 rev = rev + -1
774 774 try:
775 775 ctx = self._get_ctx(repo, rev)
776 776 except AmbiguousPrefixLookupError:
777 777 e = RepoLookupError(rev)
778 778 e._org_exc_tb = format_exc(sys.exc_info())
779 779 raise exceptions.LookupException(e)(rev)
780 780 except (TypeError, RepoLookupError, binascii.Error) as e:
781 781 e._org_exc_tb = format_exc(sys.exc_info())
782 782 raise exceptions.LookupException(e)(rev)
783 783 except LookupError as e:
784 784 e._org_exc_tb = format_exc(sys.exc_info())
785 785 raise exceptions.LookupException(e)(e.name)
786 786
787 787 if not both:
788 788 return ctx.hex()
789 789
790 790 ctx = repo[ctx.hex()]
791 791 return ctx.hex(), ctx.rev()
792 792
793 793 return _lookup(context_uid, repo_id, revision, both)
794 794
795 795 @reraise_safe_exceptions
796 796 def sync_push(self, wire, url):
797 797 if not self.check_url(url, wire['config']):
798 798 return
799 799
800 800 repo = self._factory.repo(wire)
801 801
802 802 # Disable any prompts for this repo
803 803 repo.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
804 804
805 805 bookmarks = list(dict(repo._bookmarks).keys())
806 806 remote = peer(repo, {}, safe_bytes(url))
807 807 # Disable any prompts for this remote
808 808 remote.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
809 809
810 810 return exchange.push(
811 811 repo, remote, newbranch=True, bookmarks=bookmarks).cgresult
812 812
813 813 @reraise_safe_exceptions
814 814 def revision(self, wire, rev):
815 815 repo = self._factory.repo(wire)
816 816 ctx = self._get_ctx(repo, rev)
817 817 return ctx.rev()
818 818
819 819 @reraise_safe_exceptions
820 820 def rev_range(self, wire, commit_filter):
821 821 cache_on, context_uid, repo_id = self._cache_on(wire)
822 822 region = self._region(wire)
823 823
824 824 @region.conditional_cache_on_arguments(condition=cache_on)
825 825 def _rev_range(_context_uid, _repo_id, _filter):
826 826 repo = self._factory.repo(wire)
827 827 revisions = [
828 828 ascii_str(repo[rev].hex())
829 829 for rev in revrange(repo, list(map(ascii_bytes, commit_filter)))
830 830 ]
831 831 return revisions
832 832
833 833 return _rev_range(context_uid, repo_id, sorted(commit_filter))
834 834
835 835 @reraise_safe_exceptions
836 836 def rev_range_hash(self, wire, node):
837 837 repo = self._factory.repo(wire)
838 838
839 839 def get_revs(repo, rev_opt):
840 840 if rev_opt:
841 841 revs = revrange(repo, rev_opt)
842 842 if len(revs) == 0:
843 843 return (nullrev, nullrev)
844 844 return max(revs), min(revs)
845 845 else:
846 846 return len(repo) - 1, 0
847 847
848 848 stop, start = get_revs(repo, [node + ':'])
849 849 revs = [ascii_str(repo[r].hex()) for r in range(start, stop + 1)]
850 850 return revs
851 851
852 852 @reraise_safe_exceptions
853 853 def revs_from_revspec(self, wire, rev_spec, *args, **kwargs):
854 854 org_path = safe_bytes(wire["path"])
855 855 other_path = safe_bytes(kwargs.pop('other_path', ''))
856 856
857 857 # case when we want to compare two independent repositories
858 858 if other_path and other_path != wire["path"]:
859 859 baseui = self._factory._create_config(wire["config"])
860 860 repo = unionrepo.makeunionrepository(baseui, other_path, org_path)
861 861 else:
862 862 repo = self._factory.repo(wire)
863 863 return list(repo.revs(rev_spec, *args))
864 864
865 865 @reraise_safe_exceptions
866 866 def verify(self, wire,):
867 867 repo = self._factory.repo(wire)
868 868 baseui = self._factory._create_config(wire['config'])
869 869
870 870 baseui, output = patch_ui_message_output(baseui)
871 871
872 872 repo.ui = baseui
873 873 verify.verify(repo)
874 874 return output.getvalue()
875 875
876 876 @reraise_safe_exceptions
877 877 def hg_update_cache(self, wire,):
878 878 repo = self._factory.repo(wire)
879 879 baseui = self._factory._create_config(wire['config'])
880 880 baseui, output = patch_ui_message_output(baseui)
881 881
882 882 repo.ui = baseui
883 883 with repo.wlock(), repo.lock():
884 884 repo.updatecaches(full=True)
885 885
886 886 return output.getvalue()
887 887
888 888 @reraise_safe_exceptions
889 889 def hg_rebuild_fn_cache(self, wire,):
890 890 repo = self._factory.repo(wire)
891 891 baseui = self._factory._create_config(wire['config'])
892 892 baseui, output = patch_ui_message_output(baseui)
893 893
894 894 repo.ui = baseui
895 895
896 896 repair.rebuildfncache(baseui, repo)
897 897
898 898 return output.getvalue()
899 899
900 900 @reraise_safe_exceptions
901 901 def tags(self, wire):
902 902 cache_on, context_uid, repo_id = self._cache_on(wire)
903 903 region = self._region(wire)
904 904
905 905 @region.conditional_cache_on_arguments(condition=cache_on)
906 906 def _tags(_context_uid, _repo_id):
907 907 repo = self._factory.repo(wire)
908 908 return {safe_str(name): ascii_str(hex(sha)) for name, sha in repo.tags().items()}
909 909
910 910 return _tags(context_uid, repo_id)
911 911
912 912 @reraise_safe_exceptions
913 913 def update(self, wire, node='', clean=False):
914 914 repo = self._factory.repo(wire)
915 915 baseui = self._factory._create_config(wire['config'])
916 916 node = safe_bytes(node)
917 917
918 918 commands.update(baseui, repo, node=node, clean=clean)
919 919
920 920 @reraise_safe_exceptions
921 921 def identify(self, wire):
922 922 repo = self._factory.repo(wire)
923 923 baseui = self._factory._create_config(wire['config'])
924 924 output = io.BytesIO()
925 925 baseui.write = output.write
926 926 # This is required to get a full node id
927 927 baseui.debugflag = True
928 928 commands.identify(baseui, repo, id=True)
929 929
930 930 return output.getvalue()
931 931
932 932 @reraise_safe_exceptions
933 933 def heads(self, wire, branch=None):
934 934 repo = self._factory.repo(wire)
935 935 baseui = self._factory._create_config(wire['config'])
936 936 output = io.BytesIO()
937 937
938 938 def write(data, **unused_kwargs):
939 939 output.write(data)
940 940
941 941 baseui.write = write
942 942 if branch:
943 943 args = [safe_bytes(branch)]
944 944 else:
945 945 args = []
946 946 commands.heads(baseui, repo, template=b'{node} ', *args)
947 947
948 948 return output.getvalue()
949 949
950 950 @reraise_safe_exceptions
951 951 def ancestor(self, wire, revision1, revision2):
952 952 repo = self._factory.repo(wire)
953 953 changelog = repo.changelog
954 954 lookup = repo.lookup
955 955 a = changelog.ancestor(lookup(safe_bytes(revision1)), lookup(safe_bytes(revision2)))
956 956 return hex(a)
957 957
958 958 @reraise_safe_exceptions
959 959 def clone(self, wire, source, dest, update_after_clone=False, hooks=True):
960 960 baseui = self._factory._create_config(wire["config"], hooks=hooks)
961 961 clone(baseui, safe_bytes(source), safe_bytes(dest), noupdate=not update_after_clone)
962 962
963 963 @reraise_safe_exceptions
964 964 def commitctx(self, wire, message, parents, commit_time, commit_timezone, user, files, extra, removed, updated):
965 965
966 966 repo = self._factory.repo(wire)
967 967 baseui = self._factory._create_config(wire['config'])
968 968 publishing = baseui.configbool(b'phases', b'publish')
969 969
970 970 def _filectxfn(_repo, ctx, path: bytes):
971 971 """
972 972 Marks given path as added/changed/removed in a given _repo. This is
973 973 for internal mercurial commit function.
974 974 """
975 975
976 976 # check if this path is removed
977 977 if safe_str(path) in removed:
978 978 # returning None is a way to mark node for removal
979 979 return None
980 980
981 981 # check if this path is added
982 982 for node in updated:
983 983 if safe_bytes(node['path']) == path:
984 984 return memfilectx(
985 985 _repo,
986 986 changectx=ctx,
987 987 path=safe_bytes(node['path']),
988 988 data=safe_bytes(node['content']),
989 989 islink=False,
990 990 isexec=bool(node['mode'] & stat.S_IXUSR),
991 991 copysource=False)
992 992 abort_exc = exceptions.AbortException()
993 993 raise abort_exc(f"Given path haven't been marked as added, changed or removed ({path})")
994 994
995 995 if publishing:
996 996 new_commit_phase = b'public'
997 997 else:
998 998 new_commit_phase = b'draft'
999 999 with repo.ui.configoverride({(b'phases', b'new-commit'): new_commit_phase}):
1000 1000 kwargs = {safe_bytes(k): safe_bytes(v) for k, v in extra.items()}
1001 1001 commit_ctx = memctx(
1002 1002 repo=repo,
1003 1003 parents=parents,
1004 1004 text=safe_bytes(message),
1005 1005 files=[safe_bytes(x) for x in files],
1006 1006 filectxfn=_filectxfn,
1007 1007 user=safe_bytes(user),
1008 1008 date=(commit_time, commit_timezone),
1009 1009 extra=kwargs)
1010 1010
1011 1011 n = repo.commitctx(commit_ctx)
1012 1012 new_id = hex(n)
1013 1013
1014 1014 return new_id
1015 1015
1016 1016 @reraise_safe_exceptions
1017 1017 def pull(self, wire, url, commit_ids=None):
1018 1018 repo = self._factory.repo(wire)
1019 1019 # Disable any prompts for this repo
1020 1020 repo.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
1021 1021
1022 1022 remote = peer(repo, {}, safe_bytes(url))
1023 1023 # Disable any prompts for this remote
1024 1024 remote.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
1025 1025
1026 1026 if commit_ids:
1027 1027 commit_ids = [bin(commit_id) for commit_id in commit_ids]
1028 1028
1029 1029 return exchange.pull(
1030 1030 repo, remote, heads=commit_ids, force=None).cgresult
1031 1031
1032 1032 @reraise_safe_exceptions
1033 1033 def pull_cmd(self, wire, source, bookmark='', branch='', revision='', hooks=True):
1034 1034 repo = self._factory.repo(wire)
1035 1035 baseui = self._factory._create_config(wire['config'], hooks=hooks)
1036 1036
1037 1037 source = safe_bytes(source)
1038 1038
1039 1039 # Mercurial internally has a lot of logic that checks ONLY if
1040 1040 # option is defined, we just pass those if they are defined then
1041 1041 opts = {}
1042 1042
1043 1043 if bookmark:
1044 1044 opts['bookmark'] = [safe_bytes(x) for x in bookmark] \
1045 1045 if isinstance(bookmark, list) else safe_bytes(bookmark)
1046 1046
1047 1047 if branch:
1048 1048 opts['branch'] = [safe_bytes(x) for x in branch] \
1049 1049 if isinstance(branch, list) else safe_bytes(branch)
1050 1050
1051 1051 if revision:
1052 1052 opts['rev'] = [safe_bytes(x) for x in revision] \
1053 1053 if isinstance(revision, list) else safe_bytes(revision)
1054 1054
1055 1055 commands.pull(baseui, repo, source, **opts)
1056 1056
1057 1057 @reraise_safe_exceptions
1058 1058 def push(self, wire, revisions, dest_path, hooks: bool = True, push_branches: bool = False):
1059 1059 repo = self._factory.repo(wire)
1060 1060 baseui = self._factory._create_config(wire['config'], hooks=hooks)
1061 1061
1062 1062 revisions = [safe_bytes(x) for x in revisions] \
1063 1063 if isinstance(revisions, list) else safe_bytes(revisions)
1064 1064
1065 1065 commands.push(baseui, repo, safe_bytes(dest_path),
1066 1066 rev=revisions,
1067 1067 new_branch=push_branches)
1068 1068
1069 1069 @reraise_safe_exceptions
1070 1070 def strip(self, wire, revision, update, backup):
1071 1071 repo = self._factory.repo(wire)
1072 1072 ctx = self._get_ctx(repo, revision)
1073 1073 hgext_strip.strip(
1074 1074 repo.baseui, repo, ctx.node(), update=update, backup=backup)
1075 1075
1076 1076 @reraise_safe_exceptions
1077 1077 def get_unresolved_files(self, wire):
1078 1078 repo = self._factory.repo(wire)
1079 1079
1080 1080 log.debug('Calculating unresolved files for repo: %s', repo)
1081 1081 output = io.BytesIO()
1082 1082
1083 1083 def write(data, **unused_kwargs):
1084 1084 output.write(data)
1085 1085
1086 1086 baseui = self._factory._create_config(wire['config'])
1087 1087 baseui.write = write
1088 1088
1089 1089 commands.resolve(baseui, repo, list=True)
1090 1090 unresolved = output.getvalue().splitlines(0)
1091 1091 return unresolved
1092 1092
1093 1093 @reraise_safe_exceptions
1094 1094 def merge(self, wire, revision):
1095 1095 repo = self._factory.repo(wire)
1096 1096 baseui = self._factory._create_config(wire['config'])
1097 1097 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
1098 1098
1099 1099 # In case of sub repositories are used mercurial prompts the user in
1100 1100 # case of merge conflicts or different sub repository sources. By
1101 1101 # setting the interactive flag to `False` mercurial doesn't prompt the
1102 1102 # used but instead uses a default value.
1103 1103 repo.ui.setconfig(b'ui', b'interactive', False)
1104 1104 commands.merge(baseui, repo, rev=safe_bytes(revision))
1105 1105
1106 1106 @reraise_safe_exceptions
1107 1107 def merge_state(self, wire):
1108 1108 repo = self._factory.repo(wire)
1109 1109 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
1110 1110
1111 1111 # In case of sub repositories are used mercurial prompts the user in
1112 1112 # case of merge conflicts or different sub repository sources. By
1113 1113 # setting the interactive flag to `False` mercurial doesn't prompt the
1114 1114 # used but instead uses a default value.
1115 1115 repo.ui.setconfig(b'ui', b'interactive', False)
1116 1116 ms = hg_merge.mergestate(repo)
1117 1117 return [x for x in ms.unresolved()]
1118 1118
1119 1119 @reraise_safe_exceptions
1120 1120 def commit(self, wire, message, username, close_branch=False):
1121 1121 repo = self._factory.repo(wire)
1122 1122 baseui = self._factory._create_config(wire['config'])
1123 1123 repo.ui.setconfig(b'ui', b'username', safe_bytes(username))
1124 1124 commands.commit(baseui, repo, message=safe_bytes(message), close_branch=close_branch)
1125 1125
1126 1126 @reraise_safe_exceptions
1127 1127 def rebase(self, wire, source='', dest='', abort=False):
1128 1128
1129 1129 repo = self._factory.repo(wire)
1130 1130 baseui = self._factory._create_config(wire['config'])
1131 1131 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
1132 1132 # In case of sub repositories are used mercurial prompts the user in
1133 1133 # case of merge conflicts or different sub repository sources. By
1134 1134 # setting the interactive flag to `False` mercurial doesn't prompt the
1135 1135 # used but instead uses a default value.
1136 1136 repo.ui.setconfig(b'ui', b'interactive', False)
1137 1137
1138 1138 rebase_kws = dict(
1139 1139 keep=not abort,
1140 1140 abort=abort
1141 1141 )
1142 1142
1143 1143 if source:
1144 1144 source = repo[source]
1145 1145 rebase_kws['base'] = [source.hex()]
1146 1146 if dest:
1147 1147 dest = repo[dest]
1148 1148 rebase_kws['dest'] = dest.hex()
1149 1149
1150 1150 rebase.rebase(baseui, repo, **rebase_kws)
1151 1151
1152 1152 @reraise_safe_exceptions
1153 1153 def tag(self, wire, name, revision, message, local, user, tag_time, tag_timezone):
1154 1154 repo = self._factory.repo(wire)
1155 1155 ctx = self._get_ctx(repo, revision)
1156 1156 node = ctx.node()
1157 1157
1158 1158 date = (tag_time, tag_timezone)
1159 1159 try:
1160 1160 hg_tag.tag(repo, safe_bytes(name), node, safe_bytes(message), local, safe_bytes(user), date)
1161 1161 except Abort as e:
1162 1162 log.exception("Tag operation aborted")
1163 1163 # Exception can contain unicode which we convert
1164 1164 raise exceptions.AbortException(e)(repr(e))
1165 1165
1166 1166 @reraise_safe_exceptions
1167 1167 def bookmark(self, wire, bookmark, revision=''):
1168 1168 repo = self._factory.repo(wire)
1169 1169 baseui = self._factory._create_config(wire['config'])
1170 1170 revision = revision or ''
1171 1171 commands.bookmark(baseui, repo, safe_bytes(bookmark), rev=safe_bytes(revision), force=True)
1172 1172
1173 1173 @reraise_safe_exceptions
1174 1174 def install_hooks(self, wire, force=False):
1175 1175 # we don't need any special hooks for Mercurial
1176 1176 pass
1177 1177
1178 1178 @reraise_safe_exceptions
1179 1179 def get_hooks_info(self, wire):
1180 1180 return {
1181 'pre_version': vcsserver.__version__,
1182 'post_version': vcsserver.__version__,
1181 'pre_version': vcsserver.get_version(),
1182 'post_version': vcsserver.get_version(),
1183 1183 }
1184 1184
1185 1185 @reraise_safe_exceptions
1186 1186 def set_head_ref(self, wire, head_name):
1187 1187 pass
1188 1188
1189 1189 @reraise_safe_exceptions
1190 1190 def archive_repo(self, wire, archive_name_key, kind, mtime, archive_at_path,
1191 1191 archive_dir_name, commit_id, cache_config):
1192 1192
1193 1193 def file_walker(_commit_id, path):
1194 1194 repo = self._factory.repo(wire)
1195 1195 ctx = repo[_commit_id]
1196 1196 is_root = path in ['', '/']
1197 1197 if is_root:
1198 1198 matcher = alwaysmatcher(badfn=None)
1199 1199 else:
1200 1200 matcher = patternmatcher('', [(b'glob', safe_bytes(path)+b'/**', b'')], badfn=None)
1201 1201 file_iter = ctx.manifest().walk(matcher)
1202 1202
1203 1203 for fn in file_iter:
1204 1204 file_path = fn
1205 1205 flags = ctx.flags(fn)
1206 1206 mode = b'x' in flags and 0o755 or 0o644
1207 1207 is_link = b'l' in flags
1208 1208
1209 1209 yield ArchiveNode(file_path, mode, is_link, ctx[fn].data)
1210 1210
1211 1211 return store_archive_in_cache(
1212 1212 file_walker, archive_name_key, kind, mtime, archive_at_path, archive_dir_name, commit_id, cache_config=cache_config)
1213 1213
@@ -1,289 +1,289 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17
18 18 import os
19 19 import sys
20 20 import stat
21 21 import pytest
22 22 import vcsserver
23 23 import tempfile
24 24 from vcsserver import hook_utils
25 25 from vcsserver.hook_utils import set_permissions_if_needed, HOOKS_DIR_MODE, HOOKS_FILE_MODE
26 26 from vcsserver.tests.fixture import no_newline_id_generator
27 27 from vcsserver.str_utils import safe_bytes
28 28 from vcsserver.utils import AttributeDict
29 29
30 30
31 31 class TestCheckRhodecodeHook:
32 32
33 33 def test_returns_false_when_hook_file_is_wrong_found(self, tmpdir):
34 34 hook = os.path.join(str(tmpdir), 'fake_hook_file.py')
35 35 with open(hook, 'wb') as f:
36 36 f.write(b'dummy test')
37 37 result = hook_utils.check_rhodecode_hook(hook)
38 38 assert result is False
39 39
40 40 def test_returns_true_when_no_hook_file_found(self, tmpdir):
41 41 hook = os.path.join(str(tmpdir), 'fake_hook_file_not_existing.py')
42 42 result = hook_utils.check_rhodecode_hook(hook)
43 43 assert result
44 44
45 45 @pytest.mark.parametrize("file_content, expected_result", [
46 46 ("RC_HOOK_VER = '3.3.3'\n", True),
47 47 ("RC_HOOK = '3.3.3'\n", False),
48 48 ], ids=no_newline_id_generator)
49 49 def test_signatures(self, file_content, expected_result, tmpdir):
50 50 hook = os.path.join(str(tmpdir), 'fake_hook_file_1.py')
51 51 with open(hook, 'wb') as f:
52 52 f.write(safe_bytes(file_content))
53 53
54 54 result = hook_utils.check_rhodecode_hook(hook)
55 55
56 56 assert result is expected_result
57 57
58 58
59 59 class BaseInstallHooks:
60 60 HOOK_FILES = ()
61 61
62 62 def _check_hook_file_dir_mode(self, file_path):
63 63 dir_path = os.path.dirname(file_path)
64 64 assert os.path.exists(dir_path), f'dir {file_path} missing'
65 65 stat_info = os.stat(dir_path)
66 66
67 67 file_mode = stat.S_IMODE(stat_info.st_mode)
68 68 expected_mode = int(HOOKS_DIR_MODE)
69 69 assert expected_mode == file_mode, f'expected mode: {oct(expected_mode)} got: {oct(file_mode)} for {dir_path}'
70 70
71 71 def _check_hook_file_mode(self, file_path):
72 72 assert os.path.exists(file_path), f'path {file_path} missing'
73 73 stat_info = os.stat(file_path)
74 74
75 75 file_mode = stat.S_IMODE(stat_info.st_mode)
76 76 expected_mode = int(HOOKS_FILE_MODE)
77 77 assert expected_mode == file_mode, f'expected mode: {oct(expected_mode)} got: {oct(file_mode)} for {file_path}'
78 78
79 79 def _check_hook_file_content(self, file_path, executable):
80 80 executable = executable or sys.executable
81 81 with open(file_path, 'rt') as hook_file:
82 82 content = hook_file.read()
83 83
84 84 expected_env = '#!{}'.format(executable)
85 expected_rc_version = "\nRC_HOOK_VER = '{}'\n".format(vcsserver.__version__)
85 expected_rc_version = "\nRC_HOOK_VER = '{}'\n".format(vcsserver.get_version())
86 86 assert content.strip().startswith(expected_env)
87 87 assert expected_rc_version in content
88 88
89 89 def _create_fake_hook(self, file_path, content):
90 90 with open(file_path, 'w') as hook_file:
91 91 hook_file.write(content)
92 92
93 93 def create_dummy_repo(self, repo_type):
94 94 tmpdir = tempfile.mkdtemp()
95 95 repo = AttributeDict()
96 96 if repo_type == 'git':
97 97 repo.path = os.path.join(tmpdir, 'test_git_hooks_installation_repo')
98 98 os.makedirs(repo.path)
99 99 os.makedirs(os.path.join(repo.path, 'hooks'))
100 100 repo.bare = True
101 101
102 102 elif repo_type == 'svn':
103 103 repo.path = os.path.join(tmpdir, 'test_svn_hooks_installation_repo')
104 104 os.makedirs(repo.path)
105 105 os.makedirs(os.path.join(repo.path, 'hooks'))
106 106
107 107 return repo
108 108
109 109 def check_hooks(self, repo_path, repo_bare=True):
110 110 for file_name in self.HOOK_FILES:
111 111 if repo_bare:
112 112 file_path = os.path.join(repo_path, 'hooks', file_name)
113 113 else:
114 114 file_path = os.path.join(repo_path, '.git', 'hooks', file_name)
115 115
116 116 self._check_hook_file_dir_mode(file_path)
117 117 self._check_hook_file_mode(file_path)
118 118 self._check_hook_file_content(file_path, sys.executable)
119 119
120 120
121 121 class TestInstallGitHooks(BaseInstallHooks):
122 122 HOOK_FILES = ('pre-receive', 'post-receive')
123 123
124 124 def test_hooks_are_installed(self):
125 125 repo = self.create_dummy_repo('git')
126 126 result = hook_utils.install_git_hooks(repo.path, repo.bare)
127 127 assert result
128 128 self.check_hooks(repo.path, repo.bare)
129 129
130 130 def test_hooks_are_replaced(self):
131 131 repo = self.create_dummy_repo('git')
132 132 hooks_path = os.path.join(repo.path, 'hooks')
133 133 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
134 134 self._create_fake_hook(
135 135 file_path, content="RC_HOOK_VER = 'abcde'\n")
136 136
137 137 result = hook_utils.install_git_hooks(repo.path, repo.bare)
138 138 assert result
139 139 self.check_hooks(repo.path, repo.bare)
140 140
141 141 def test_non_rc_hooks_are_not_replaced(self):
142 142 repo = self.create_dummy_repo('git')
143 143 hooks_path = os.path.join(repo.path, 'hooks')
144 144 non_rc_content = 'echo "non rc hook"\n'
145 145 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
146 146 self._create_fake_hook(
147 147 file_path, content=non_rc_content)
148 148
149 149 result = hook_utils.install_git_hooks(repo.path, repo.bare)
150 150 assert result
151 151
152 152 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
153 153 with open(file_path, 'rt') as hook_file:
154 154 content = hook_file.read()
155 155 assert content == non_rc_content
156 156
157 157 def test_non_rc_hooks_are_replaced_with_force_flag(self):
158 158 repo = self.create_dummy_repo('git')
159 159 hooks_path = os.path.join(repo.path, 'hooks')
160 160 non_rc_content = 'echo "non rc hook"\n'
161 161 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
162 162 self._create_fake_hook(
163 163 file_path, content=non_rc_content)
164 164
165 165 result = hook_utils.install_git_hooks(
166 166 repo.path, repo.bare, force_create=True)
167 167 assert result
168 168 self.check_hooks(repo.path, repo.bare)
169 169
170 170
171 171 class TestInstallSvnHooks(BaseInstallHooks):
172 172 HOOK_FILES = ('pre-commit', 'post-commit')
173 173
174 174 def test_hooks_are_installed(self):
175 175 repo = self.create_dummy_repo('svn')
176 176 result = hook_utils.install_svn_hooks(repo.path)
177 177 assert result
178 178 self.check_hooks(repo.path)
179 179
180 180 def test_hooks_are_replaced(self):
181 181 repo = self.create_dummy_repo('svn')
182 182 hooks_path = os.path.join(repo.path, 'hooks')
183 183 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
184 184 self._create_fake_hook(
185 185 file_path, content="RC_HOOK_VER = 'abcde'\n")
186 186
187 187 result = hook_utils.install_svn_hooks(repo.path)
188 188 assert result
189 189 self.check_hooks(repo.path)
190 190
191 191 def test_non_rc_hooks_are_not_replaced(self):
192 192 repo = self.create_dummy_repo('svn')
193 193 hooks_path = os.path.join(repo.path, 'hooks')
194 194 non_rc_content = 'echo "non rc hook"\n'
195 195 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
196 196 self._create_fake_hook(
197 197 file_path, content=non_rc_content)
198 198
199 199 result = hook_utils.install_svn_hooks(repo.path)
200 200 assert result
201 201
202 202 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
203 203 with open(file_path, 'rt') as hook_file:
204 204 content = hook_file.read()
205 205 assert content == non_rc_content
206 206
207 207 def test_non_rc_hooks_are_replaced_with_force_flag(self):
208 208 repo = self.create_dummy_repo('svn')
209 209 hooks_path = os.path.join(repo.path, 'hooks')
210 210 non_rc_content = 'echo "non rc hook"\n'
211 211 for file_path in [os.path.join(hooks_path, f) for f in self.HOOK_FILES]:
212 212 self._create_fake_hook(
213 213 file_path, content=non_rc_content)
214 214
215 215 result = hook_utils.install_svn_hooks(
216 216 repo.path, force_create=True)
217 217 assert result
218 218 self.check_hooks(repo.path, )
219 219
220 220
221 221 def create_test_file(filename):
222 222 """Utility function to create a test file."""
223 223 with open(filename, 'w') as f:
224 224 f.write("Test file")
225 225
226 226
227 227 def remove_test_file(filename):
228 228 """Utility function to remove a test file."""
229 229 if os.path.exists(filename):
230 230 os.remove(filename)
231 231
232 232
233 233 @pytest.fixture
234 234 def test_file():
235 235 filename = 'test_file.txt'
236 236 create_test_file(filename)
237 237 yield filename
238 238 remove_test_file(filename)
239 239
240 240
241 241 def test_increase_permissions(test_file):
242 242 # Set initial lower permissions
243 243 initial_perms = 0o644
244 244 os.chmod(test_file, initial_perms)
245 245
246 246 # Set higher permissions
247 247 new_perms = 0o666
248 248 set_permissions_if_needed(test_file, new_perms)
249 249
250 250 # Check if permissions were updated
251 251 assert (os.stat(test_file).st_mode & 0o777) == new_perms
252 252
253 253
254 254 def test_no_permission_change_needed(test_file):
255 255 # Set initial permissions
256 256 initial_perms = 0o666
257 257 os.chmod(test_file, initial_perms)
258 258
259 259 # Attempt to set the same permissions
260 260 set_permissions_if_needed(test_file, initial_perms)
261 261
262 262 # Check if permissions were unchanged
263 263 assert (os.stat(test_file).st_mode & 0o777) == initial_perms
264 264
265 265
266 266 def test_no_permission_reduction(test_file):
267 267 # Set initial higher permissions
268 268 initial_perms = 0o666
269 269 os.chmod(test_file, initial_perms)
270 270
271 271 # Attempt to set lower permissions
272 272 lower_perms = 0o644
273 273 set_permissions_if_needed(test_file, lower_perms)
274 274
275 275 # Check if permissions were not reduced
276 276 assert (os.stat(test_file).st_mode & 0o777) == initial_perms
277 277
278 278
279 279 def test_no_permission_reduction_when_on_777(test_file):
280 280 # Set initial higher permissions
281 281 initial_perms = 0o777
282 282 os.chmod(test_file, initial_perms)
283 283
284 284 # Attempt to set lower permissions
285 285 lower_perms = 0o755
286 286 set_permissions_if_needed(test_file, lower_perms)
287 287
288 288 # Check if permissions were not reduced
289 289 assert (os.stat(test_file).st_mode & 0o777) == initial_perms
@@ -1,123 +1,123 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software; you can redistribute it and/or modify
5 5 # it under the terms of the GNU General Public License as published by
6 6 # the Free Software Foundation; either version 3 of the License, or
7 7 # (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 17 import base64
18 18 import logging
19 19 import time
20 20
21 21 import msgpack
22 22
23 23 import vcsserver
24 24 from vcsserver.str_utils import safe_str
25 25
26 26 log = logging.getLogger(__name__)
27 27
28 28
29 29 def get_access_path(environ):
30 30 path = environ.get('PATH_INFO')
31 31 return path
32 32
33 33
34 34 def get_user_agent(environ):
35 35 return environ.get('HTTP_USER_AGENT')
36 36
37 37
38 38 def get_call_context(request) -> dict:
39 39 cc = {}
40 40 registry = request.registry
41 41 if hasattr(registry, 'vcs_call_context'):
42 42 cc.update({
43 43 'X-RC-Method': registry.vcs_call_context.get('method'),
44 44 'X-RC-Repo-Name': registry.vcs_call_context.get('repo_name')
45 45 })
46 46
47 47 return cc
48 48
49 49
50 50 def get_headers_call_context(environ, strict=True):
51 51 if 'HTTP_X_RC_VCS_STREAM_CALL_CONTEXT' in environ:
52 52 packed_cc = base64.b64decode(environ['HTTP_X_RC_VCS_STREAM_CALL_CONTEXT'])
53 53 return msgpack.unpackb(packed_cc)
54 54 elif strict:
55 55 raise ValueError('Expected header HTTP_X_RC_VCS_STREAM_CALL_CONTEXT not found')
56 56
57 57
58 58 class RequestWrapperTween:
59 59 def __init__(self, handler, registry):
60 60 self.handler = handler
61 61 self.registry = registry
62 62
63 63 # one-time configuration code goes here
64 64
65 65 def __call__(self, request):
66 66 start = time.time()
67 67 log.debug('Starting request time measurement')
68 68 response = None
69 69
70 70 try:
71 71 response = self.handler(request)
72 72 finally:
73 73 ua = get_user_agent(request.environ)
74 74 call_context = get_call_context(request)
75 75 vcs_method = call_context.get('X-RC-Method', '_NO_VCS_METHOD')
76 76 repo_name = call_context.get('X-RC-Repo-Name', '')
77 77
78 78 count = request.request_count()
79 _ver_ = vcsserver.__version__
79 _ver_ = vcsserver.get_version()
80 80 _path = safe_str(get_access_path(request.environ))
81 81
82 82 ip = '127.0.0.1'
83 83 match_route = request.matched_route.name if request.matched_route else "NOT_FOUND"
84 84 resp_code = getattr(response, 'status_code', 'UNDEFINED')
85 85
86 86 _view_path = f"{repo_name}@{_path}/{vcs_method}"
87 87
88 88 total = time.time() - start
89 89
90 90 log.info(
91 91 'Req[%4s] IP: %s %s Request to %s time: %.4fs [%s], VCSServer %s',
92 92 count, ip, request.environ.get('REQUEST_METHOD'),
93 93 _view_path, total, ua, _ver_,
94 94 extra={"time": total, "ver": _ver_, "code": resp_code,
95 95 "path": _path, "view_name": match_route, "user_agent": ua,
96 96 "vcs_method": vcs_method, "repo_name": repo_name}
97 97 )
98 98
99 99 statsd = request.registry.statsd
100 100 if statsd:
101 101 match_route = request.matched_route.name if request.matched_route else _path
102 102 elapsed_time_ms = round(1000.0 * total) # use ms only
103 103 statsd.timing(
104 104 "vcsserver_req_timing.histogram", elapsed_time_ms,
105 105 tags=[
106 106 f"view_name:{match_route}",
107 107 f"code:{resp_code}"
108 108 ],
109 109 use_decimals=False
110 110 )
111 111 statsd.incr(
112 112 "vcsserver_req_total", tags=[
113 113 f"view_name:{match_route}",
114 114 f"code:{resp_code}"
115 115 ])
116 116
117 117 return response
118 118
119 119
120 120 def includeme(config):
121 121 config.add_tween(
122 122 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
123 123 )
General Comments 0
You need to be logged in to leave comments. Login now