##// END OF EJS Templates
protocol: introduced binaryEnvelope to return raw bytes via msgpack
super-admin -
r1089:d0d6adf3 python3
parent child Browse files
Show More
@@ -1,136 +1,142 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2020 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 os
18 18 import sys
19 19 import traceback
20 20 import logging
21 21 import urllib.parse
22 22
23 23 from vcsserver.lib.rc_cache import region_meta
24 24
25 25 from vcsserver import exceptions
26 26 from vcsserver.exceptions import NoContentException
27 27 from vcsserver.hgcompat import archival
28 28 from vcsserver.str_utils import safe_bytes
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 33 class RepoFactory(object):
34 34 """
35 35 Utility to create instances of repository
36 36
37 37 It provides internal caching of the `repo` object based on
38 38 the :term:`call context`.
39 39 """
40 40 repo_type = None
41 41
42 42 def __init__(self):
43 43 self._cache_region = region_meta.dogpile_cache_regions['repo_object']
44 44
45 45 def _create_config(self, path, config):
46 46 config = {}
47 47 return config
48 48
49 49 def _create_repo(self, wire, create):
50 50 raise NotImplementedError()
51 51
52 52 def repo(self, wire, create=False):
53 53 raise NotImplementedError()
54 54
55 55
56 56 def obfuscate_qs(query_string):
57 57 if query_string is None:
58 58 return None
59 59
60 60 parsed = []
61 61 for k, v in urllib.parse.parse_qsl(query_string, keep_blank_values=True):
62 62 if k in ['auth_token', 'api_key']:
63 63 v = "*****"
64 64 parsed.append((k, v))
65 65
66 66 return '&'.join('{}{}'.format(
67 67 k, '={}'.format(v) if v else '') for k, v in parsed)
68 68
69 69
70 70 def raise_from_original(new_type, org_exc: Exception):
71 71 """
72 72 Raise a new exception type with original args and traceback.
73 73 """
74 74
75 75 exc_type, exc_value, exc_traceback = sys.exc_info()
76 76 new_exc = new_type(*exc_value.args)
77 77
78 78 # store the original traceback into the new exc
79 79 new_exc._org_exc_tb = traceback.format_tb(exc_traceback)
80 80
81 81 try:
82 82 raise new_exc.with_traceback(exc_traceback)
83 83 finally:
84 84 del exc_traceback
85 85
86 86
87 87 class ArchiveNode(object):
88 88 def __init__(self, path, mode, is_link, raw_bytes):
89 89 self.path = path
90 90 self.mode = mode
91 91 self.is_link = is_link
92 92 self.raw_bytes = raw_bytes
93 93
94 94
95 95 def archive_repo(walker, archive_dest_path, kind, mtime, archive_at_path,
96 96 archive_dir_name, commit_id, write_metadata=True, extra_metadata=None):
97 97 """
98 98 walker should be a file walker, for example:
99 99 def walker():
100 100 for file_info in files:
101 101 yield ArchiveNode(fn, mode, is_link, ctx[fn].data)
102 102 """
103 103 extra_metadata = extra_metadata or {}
104 104 archive_dest_path = safe_bytes(archive_dest_path)
105 105
106 106 if kind == "tgz":
107 107 archiver = archival.tarit(archive_dest_path, mtime, b"gz")
108 108 elif kind == "tbz2":
109 109 archiver = archival.tarit(archive_dest_path, mtime, b"bz2")
110 110 elif kind == 'zip':
111 111 archiver = archival.zipit(archive_dest_path, mtime)
112 112 else:
113 113 raise exceptions.ArchiveException()(
114 114 f'Remote does not support: "{kind}" archive type.')
115 115
116 116 for f in walker(commit_id, archive_at_path):
117 117 f_path = os.path.join(safe_bytes(archive_dir_name), safe_bytes(f.path).lstrip(b'/'))
118 118 try:
119 119 archiver.addfile(f_path, f.mode, f.is_link, f.raw_bytes())
120 120 except NoContentException:
121 121 # NOTE(marcink): this is a special case for SVN so we can create "empty"
122 122 # directories which arent supported by archiver
123 123 archiver.addfile(os.path.join(f_path, b'.dir'), f.mode, f.is_link, b'')
124 124
125 125 if write_metadata:
126 126 metadata = dict([
127 127 ('commit_id', commit_id),
128 128 ('mtime', mtime),
129 129 ])
130 130 metadata.update(extra_metadata)
131 131
132 132 meta = [safe_bytes(f"{f_name}:{value}") for f_name, value in metadata.items()]
133 133 f_path = os.path.join(safe_bytes(archive_dir_name), b'.archival.txt')
134 134 archiver.addfile(f_path, 0o644, False, b'\n'.join(meta))
135 135
136 136 return archiver.done()
137
138
139 class BinaryEnvelope(object):
140 def __init__(self, value, bin_type=True):
141 self.value = value
142 self.bin_type = bin_type
@@ -1,762 +1,770 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2020 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 sys
21 21 import base64
22 22 import locale
23 23 import logging
24 24 import uuid
25 25 import time
26 26 import wsgiref.util
27 27 import traceback
28 28 import tempfile
29 29 import psutil
30 30
31 31 from itertools import chain
32 32
33 33 import msgpack
34 34 import configparser
35 35
36 36 from pyramid.config import Configurator
37 37 from pyramid.wsgi import wsgiapp
38 38 from pyramid.response import Response
39 39
40 from vcsserver.base import BinaryEnvelope
40 41 from vcsserver.lib.rc_json import json
41 42 from vcsserver.config.settings_maker import SettingsMaker
42 43 from vcsserver.str_utils import safe_int, safe_bytes, safe_str
43 44 from vcsserver.lib.statsd_client import StatsdClient
44 45
45 46 log = logging.getLogger(__name__)
46 47
47 48 # due to Mercurial/glibc2.27 problems we need to detect if locale settings are
48 49 # causing problems and "fix" it in case they do and fallback to LC_ALL = C
49 50
50 51 try:
51 52 locale.setlocale(locale.LC_ALL, '')
52 53 except locale.Error as e:
53 54 log.error(
54 55 'LOCALE ERROR: failed to set LC_ALL, fallback to LC_ALL=C, org error: %s', e)
55 56 os.environ['LC_ALL'] = 'C'
56 57
57 58
58 59 import vcsserver
59 60 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
60 61 from vcsserver.git_lfs.app import GIT_LFS_CONTENT_TYPE, GIT_LFS_PROTO_PAT
61 62 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
62 63 from vcsserver.echo_stub.echo_app import EchoApp
63 64 from vcsserver.exceptions import HTTPRepoLocked, HTTPRepoBranchProtected
64 65 from vcsserver.lib.exc_tracking import store_exception
65 66 from vcsserver.server import VcsServer
66 67
67 68 strict_vcs = True
68 69
69 70 git_import_err = None
70 71 try:
71 72 from vcsserver.remote.git import GitFactory, GitRemote
72 73 except ImportError as e:
73 74 GitFactory = None
74 75 GitRemote = None
75 76 git_import_err = e
76 77 if strict_vcs:
77 78 raise
78 79
79 80
80 81 hg_import_err = None
81 82 try:
82 83 from vcsserver.remote.hg import MercurialFactory, HgRemote
83 84 except ImportError as e:
84 85 MercurialFactory = None
85 86 HgRemote = None
86 87 hg_import_err = e
87 88 if strict_vcs:
88 89 raise
89 90
90 91
91 92 svn_import_err = None
92 93 try:
93 94 from vcsserver.remote.svn import SubversionFactory, SvnRemote
94 95 except ImportError as e:
95 96 SubversionFactory = None
96 97 SvnRemote = None
97 98 svn_import_err = e
98 99 if strict_vcs:
99 100 raise
100 101
101 102
102 103 def _is_request_chunked(environ):
103 104 stream = environ.get('HTTP_TRANSFER_ENCODING', '') == 'chunked'
104 105 return stream
105 106
106 107
107 108 def log_max_fd():
108 109 try:
109 110 maxfd = psutil.Process().rlimit(psutil.RLIMIT_NOFILE)[1]
110 111 log.info('Max file descriptors value: %s', maxfd)
111 112 except Exception:
112 113 pass
113 114
114 115
115 116 class VCS(object):
116 117 def __init__(self, locale_conf=None, cache_config=None):
117 118 self.locale = locale_conf
118 119 self.cache_config = cache_config
119 120 self._configure_locale()
120 121
121 122 log_max_fd()
122 123
123 124 if GitFactory and GitRemote:
124 125 git_factory = GitFactory()
125 126 self._git_remote = GitRemote(git_factory)
126 127 else:
127 128 log.error("Git client import failed: %s", git_import_err)
128 129
129 130 if MercurialFactory and HgRemote:
130 131 hg_factory = MercurialFactory()
131 132 self._hg_remote = HgRemote(hg_factory)
132 133 else:
133 134 log.error("Mercurial client import failed: %s", hg_import_err)
134 135
135 136 if SubversionFactory and SvnRemote:
136 137 svn_factory = SubversionFactory()
137 138
138 139 # hg factory is used for svn url validation
139 140 hg_factory = MercurialFactory()
140 141 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
141 142 else:
142 143 log.error("Subversion client import failed: %s", svn_import_err)
143 144
144 145 self._vcsserver = VcsServer()
145 146
146 147 def _configure_locale(self):
147 148 if self.locale:
148 149 log.info('Settings locale: `LC_ALL` to %s', self.locale)
149 150 else:
150 151 log.info('Configuring locale subsystem based on environment variables')
151 152 try:
152 153 # If self.locale is the empty string, then the locale
153 154 # module will use the environment variables. See the
154 155 # documentation of the package `locale`.
155 156 locale.setlocale(locale.LC_ALL, self.locale)
156 157
157 158 language_code, encoding = locale.getlocale()
158 159 log.info(
159 160 'Locale set to language code "%s" with encoding "%s".',
160 161 language_code, encoding)
161 162 except locale.Error:
162 163 log.exception('Cannot set locale, not configuring the locale system')
163 164
164 165
165 166 class WsgiProxy(object):
166 167 def __init__(self, wsgi):
167 168 self.wsgi = wsgi
168 169
169 170 def __call__(self, environ, start_response):
170 171 input_data = environ['wsgi.input'].read()
171 172 input_data = msgpack.unpackb(input_data)
172 173
173 174 error = None
174 175 try:
175 176 data, status, headers = self.wsgi.handle(
176 177 input_data['environment'], input_data['input_data'],
177 178 *input_data['args'], **input_data['kwargs'])
178 179 except Exception as e:
179 180 data, status, headers = [], None, None
180 181 error = {
181 182 'message': str(e),
182 183 '_vcs_kind': getattr(e, '_vcs_kind', None)
183 184 }
184 185
185 186 start_response(200, {})
186 187 return self._iterator(error, status, headers, data)
187 188
188 189 def _iterator(self, error, status, headers, data):
189 190 initial_data = [
190 191 error,
191 192 status,
192 193 headers,
193 194 ]
194 195
195 196 for d in chain(initial_data, data):
196 197 yield msgpack.packb(d)
197 198
198 199
199 200 def not_found(request):
200 201 return {'status': '404 NOT FOUND'}
201 202
202 203
203 204 class VCSViewPredicate(object):
204 205 def __init__(self, val, config):
205 206 self.remotes = val
206 207
207 208 def text(self):
208 209 return 'vcs view method = %s' % (list(self.remotes.keys()),)
209 210
210 211 phash = text
211 212
212 213 def __call__(self, context, request):
213 214 """
214 215 View predicate that returns true if given backend is supported by
215 216 defined remotes.
216 217 """
217 218 backend = request.matchdict.get('backend')
218 219 return backend in self.remotes
219 220
220 221
221 222 class HTTPApplication(object):
222 223 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
223 224
224 225 remote_wsgi = remote_wsgi
225 226 _use_echo_app = False
226 227
227 228 def __init__(self, settings=None, global_config=None):
228 229
229 230 self.config = Configurator(settings=settings)
230 231 # Init our statsd at very start
231 232 self.config.registry.statsd = StatsdClient.statsd
232 233 self.config.registry.vcs_call_context = {}
233 234
234 235 self.global_config = global_config
235 236 self.config.include('vcsserver.lib.rc_cache')
236 237
237 238 settings_locale = settings.get('locale', '') or 'en_US.UTF-8'
238 239 vcs = VCS(locale_conf=settings_locale, cache_config=settings)
239 240 self._remotes = {
240 241 'hg': vcs._hg_remote,
241 242 'git': vcs._git_remote,
242 243 'svn': vcs._svn_remote,
243 244 'server': vcs._vcsserver,
244 245 }
245 246 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
246 247 self._use_echo_app = True
247 248 log.warning("Using EchoApp for VCS operations.")
248 249 self.remote_wsgi = remote_wsgi_stub
249 250
250 251 self._configure_settings(global_config, settings)
251 252
252 253 self._configure()
253 254
254 255 def _configure_settings(self, global_config, app_settings):
255 256 """
256 257 Configure the settings module.
257 258 """
258 259 settings_merged = global_config.copy()
259 260 settings_merged.update(app_settings)
260 261
261 262 git_path = app_settings.get('git_path', None)
262 263 if git_path:
263 264 settings.GIT_EXECUTABLE = git_path
264 265 binary_dir = app_settings.get('core.binary_dir', None)
265 266 if binary_dir:
266 267 settings.BINARY_DIR = binary_dir
267 268
268 269 # Store the settings to make them available to other modules.
269 270 vcsserver.PYRAMID_SETTINGS = settings_merged
270 271 vcsserver.CONFIG = settings_merged
271 272
272 273 def _configure(self):
273 274 self.config.add_renderer(name='msgpack', factory=self._msgpack_renderer_factory)
274 275
275 276 self.config.add_route('service', '/_service')
276 277 self.config.add_route('status', '/status')
277 278 self.config.add_route('hg_proxy', '/proxy/hg')
278 279 self.config.add_route('git_proxy', '/proxy/git')
279 280
280 281 # rpc methods
281 282 self.config.add_route('vcs', '/{backend}')
282 283
283 284 # streaming rpc remote methods
284 285 self.config.add_route('vcs_stream', '/{backend}/stream')
285 286
286 287 # vcs operations clone/push as streaming
287 288 self.config.add_route('stream_git', '/stream/git/*repo_name')
288 289 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
289 290
290 291 self.config.add_view(self.status_view, route_name='status', renderer='json')
291 292 self.config.add_view(self.service_view, route_name='service', renderer='msgpack')
292 293
293 294 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
294 295 self.config.add_view(self.git_proxy(), route_name='git_proxy')
295 296 self.config.add_view(self.vcs_view, route_name='vcs', renderer='msgpack',
296 297 vcs_view=self._remotes)
297 298 self.config.add_view(self.vcs_stream_view, route_name='vcs_stream',
298 299 vcs_view=self._remotes)
299 300
300 301 self.config.add_view(self.hg_stream(), route_name='stream_hg')
301 302 self.config.add_view(self.git_stream(), route_name='stream_git')
302 303
303 304 self.config.add_view_predicate('vcs_view', VCSViewPredicate)
304 305
305 306 self.config.add_notfound_view(not_found, renderer='json')
306 307
307 308 self.config.add_view(self.handle_vcs_exception, context=Exception)
308 309
309 310 self.config.add_tween(
310 311 'vcsserver.tweens.request_wrapper.RequestWrapperTween',
311 312 )
312 313 self.config.add_request_method(
313 314 'vcsserver.lib.request_counter.get_request_counter',
314 315 'request_count')
315 316
316 317 def wsgi_app(self):
317 318 return self.config.make_wsgi_app()
318 319
319 320 def _vcs_view_params(self, request):
320 321 remote = self._remotes[request.matchdict['backend']]
321 322 payload = msgpack.unpackb(request.body, use_list=True)
322 323
323 324 method = payload.get('method')
324 325 params = payload['params']
325 326 wire = params.get('wire')
326 327 args = params.get('args')
327 328 kwargs = params.get('kwargs')
328 329 context_uid = None
329 330
330 331 request.registry.vcs_call_context = {
331 332 'method': method,
332 333 'repo_name': payload.get('_repo_name')
333 334 }
334 335
335 336 if wire:
336 337 try:
337 338 wire['context'] = context_uid = uuid.UUID(wire['context'])
338 339 except KeyError:
339 340 pass
340 341 args.insert(0, wire)
341 342 repo_state_uid = wire.get('repo_state_uid') if wire else None
342 343
343 344 # NOTE(marcink): trading complexity for slight performance
344 345 if log.isEnabledFor(logging.DEBUG):
345 346 # also we SKIP printing out any of those methods args since they maybe excessive
346 347 just_args_methods = {
347 348 'commitctx': ('content', 'removed', 'updated')
348 349 }
349 350 if method in just_args_methods:
350 351 skip_args = just_args_methods[method]
351 352 call_args = ''
352 353 call_kwargs = {}
353 354 for k in kwargs:
354 355 if k in skip_args:
355 356 # replace our skip key with dummy
356 357 call_kwargs[k] = f'RemovedParam({k})'
357 358 else:
358 359 call_kwargs[k] = kwargs[k]
359 360 else:
360 361 call_args = args[1:]
361 362 call_kwargs = kwargs
362 363
363 364 log.debug('Method requested:`%s` with args:%s kwargs:%s context_uid: %s, repo_state_uid:%s',
364 365 method, call_args, call_kwargs, context_uid, repo_state_uid)
365 366
366 367 statsd = request.registry.statsd
367 368 if statsd:
368 369 statsd.incr(
369 370 'vcsserver_method_total', tags=[
370 371 "method:{}".format(method),
371 372 ])
372 373 return payload, remote, method, args, kwargs
373 374
374 375 def vcs_view(self, request):
375 376
376 377 payload, remote, method, args, kwargs = self._vcs_view_params(request)
377 378 payload_id = payload.get('id')
378 379
379 380 try:
380 381 resp = getattr(remote, method)(*args, **kwargs)
381 382 except Exception as e:
382 383 exc_info = list(sys.exc_info())
383 384 exc_type, exc_value, exc_traceback = exc_info
384 385
385 386 org_exc = getattr(e, '_org_exc', None)
386 387 org_exc_name = None
387 388 org_exc_tb = ''
388 389 if org_exc:
389 390 org_exc_name = org_exc.__class__.__name__
390 391 org_exc_tb = getattr(e, '_org_exc_tb', '')
391 392 # replace our "faked" exception with our org
392 393 exc_info[0] = org_exc.__class__
393 394 exc_info[1] = org_exc
394 395
395 396 should_store_exc = True
396 397 if org_exc:
397 398 def get_exc_fqn(_exc_obj):
398 399 module_name = getattr(org_exc.__class__, '__module__', 'UNKNOWN')
399 400 return module_name + '.' + org_exc_name
400 401
401 402 exc_fqn = get_exc_fqn(org_exc)
402 403
403 404 if exc_fqn in ['mercurial.error.RepoLookupError',
404 405 'vcsserver.exceptions.RefNotFoundException']:
405 406 should_store_exc = False
406 407
407 408 if should_store_exc:
408 409 store_exception(id(exc_info), exc_info, request_path=request.path)
409 410
410 411 tb_info = ''.join(
411 412 traceback.format_exception(exc_type, exc_value, exc_traceback))
412 413
413 414 type_ = e.__class__.__name__
414 415 if type_ not in self.ALLOWED_EXCEPTIONS:
415 416 type_ = None
416 417
417 418 resp = {
418 419 'id': payload_id,
419 420 'error': {
420 421 'message': str(e),
421 422 'traceback': tb_info,
422 423 'org_exc': org_exc_name,
423 424 'org_exc_tb': org_exc_tb,
424 425 'type': type_
425 426 }
426 427 }
427 428
428 429 try:
429 430 resp['error']['_vcs_kind'] = getattr(e, '_vcs_kind', None)
430 431 except AttributeError:
431 432 pass
432 433 else:
433 434 resp = {
434 435 'id': payload_id,
435 436 'result': resp
436 437 }
437 438 log.debug('Serving data for method %s', method)
438 439 return resp
439 440
440 441 def vcs_stream_view(self, request):
441 442 payload, remote, method, args, kwargs = self._vcs_view_params(request)
442 443 # this method has a stream: marker we remove it here
443 444 method = method.split('stream:')[-1]
444 445 chunk_size = safe_int(payload.get('chunk_size')) or 4096
445 446
446 447 try:
447 448 resp = getattr(remote, method)(*args, **kwargs)
448 449 except Exception as e:
449 450 raise
450 451
451 452 def get_chunked_data(method_resp):
452 453 stream = io.BytesIO(method_resp)
453 454 while 1:
454 455 chunk = stream.read(chunk_size)
455 456 if not chunk:
456 457 break
457 458 yield chunk
458 459
459 460 response = Response(app_iter=get_chunked_data(resp))
460 461 response.content_type = 'application/octet-stream'
461 462
462 463 return response
463 464
464 465 def status_view(self, request):
465 466 import vcsserver
466 467 return {'status': 'OK', 'vcsserver_version': safe_str(vcsserver.__version__),
467 468 'pid': os.getpid()}
468 469
469 470 def service_view(self, request):
470 471 import vcsserver
471 472
472 473 payload = msgpack.unpackb(request.body, use_list=True)
473 474 server_config, app_config = {}, {}
474 475
475 476 try:
476 477 path = self.global_config['__file__']
477 478 config = configparser.RawConfigParser()
478 479
479 480 config.read(path)
480 481
481 482 if config.has_section('server:main'):
482 483 server_config = dict(config.items('server:main'))
483 484 if config.has_section('app:main'):
484 485 app_config = dict(config.items('app:main'))
485 486
486 487 except Exception:
487 488 log.exception('Failed to read .ini file for display')
488 489
489 490 environ = list(os.environ.items())
490 491
491 492 resp = {
492 493 'id': payload.get('id'),
493 494 'result': dict(
494 495 version=safe_str(vcsserver.__version__),
495 496 config=server_config,
496 497 app_config=app_config,
497 498 environ=environ,
498 499 payload=payload,
499 500 )
500 501 }
501 502 return resp
502 503
503 504 def _msgpack_renderer_factory(self, info):
505
504 506 def _render(value, system):
507 bin_type = False
508 res = value.get('result')
509 if res and isinstance(res, BinaryEnvelope):
510 value['result'] = res.value
511 bin_type = res.bin_type
512
505 513 request = system.get('request')
506 514 if request is not None:
507 515 response = request.response
508 516 ct = response.content_type
509 517 if ct == response.default_content_type:
510 518 response.content_type = 'application/x-msgpack'
511 519
512 return msgpack.packb(value, use_bin_type=False)
520 return msgpack.packb(value, use_bin_type=bin_type)
513 521 return _render
514 522
515 523 def set_env_from_config(self, environ, config):
516 524 dict_conf = {}
517 525 try:
518 526 for elem in config:
519 527 if elem[0] == 'rhodecode':
520 528 dict_conf = json.loads(elem[2])
521 529 break
522 530 except Exception:
523 531 log.exception('Failed to fetch SCM CONFIG')
524 532 return
525 533
526 534 username = dict_conf.get('username')
527 535 if username:
528 536 environ['REMOTE_USER'] = username
529 537 # mercurial specific, some extension api rely on this
530 538 environ['HGUSER'] = username
531 539
532 540 ip = dict_conf.get('ip')
533 541 if ip:
534 542 environ['REMOTE_HOST'] = ip
535 543
536 544 if _is_request_chunked(environ):
537 545 # set the compatibility flag for webob
538 546 environ['wsgi.input_terminated'] = True
539 547
540 548 def hg_proxy(self):
541 549 @wsgiapp
542 550 def _hg_proxy(environ, start_response):
543 551 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
544 552 return app(environ, start_response)
545 553 return _hg_proxy
546 554
547 555 def git_proxy(self):
548 556 @wsgiapp
549 557 def _git_proxy(environ, start_response):
550 558 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
551 559 return app(environ, start_response)
552 560 return _git_proxy
553 561
554 562 def hg_stream(self):
555 563 if self._use_echo_app:
556 564 @wsgiapp
557 565 def _hg_stream(environ, start_response):
558 566 app = EchoApp('fake_path', 'fake_name', None)
559 567 return app(environ, start_response)
560 568 return _hg_stream
561 569 else:
562 570 @wsgiapp
563 571 def _hg_stream(environ, start_response):
564 572 log.debug('http-app: handling hg stream')
565 573
566 574 packed_cc = base64.b64decode(environ['HTTP_X_RC_VCS_STREAM_CALL_CONTEXT'])
567 575 call_context = msgpack.unpackb(packed_cc)
568 576
569 577 repo_path = call_context['repo_path']
570 578 repo_name = call_context['repo_name']
571 579 config = call_context['repo_config']
572 580
573 581 app = scm_app.create_hg_wsgi_app(
574 582 repo_path, repo_name, config)
575 583
576 584 # Consistent path information for hgweb
577 585 environ['PATH_INFO'] = call_context['path_info']
578 586 environ['REPO_NAME'] = repo_name
579 587 self.set_env_from_config(environ, config)
580 588
581 589 log.debug('http-app: starting app handler '
582 590 'with %s and process request', app)
583 591 return app(environ, ResponseFilter(start_response))
584 592 return _hg_stream
585 593
586 594 def git_stream(self):
587 595 if self._use_echo_app:
588 596 @wsgiapp
589 597 def _git_stream(environ, start_response):
590 598 app = EchoApp('fake_path', 'fake_name', None)
591 599 return app(environ, start_response)
592 600 return _git_stream
593 601 else:
594 602 @wsgiapp
595 603 def _git_stream(environ, start_response):
596 604 log.debug('http-app: handling git stream')
597 605
598 606 packed_cc = base64.b64decode(environ['HTTP_X_RC_VCS_STREAM_CALL_CONTEXT'])
599 607 call_context = msgpack.unpackb(packed_cc)
600 608
601 609 repo_path = call_context['repo_path']
602 610 repo_name = call_context['repo_name']
603 611 config = call_context['repo_config']
604 612
605 613 environ['PATH_INFO'] = call_context['path_info']
606 614 self.set_env_from_config(environ, config)
607 615
608 616 content_type = environ.get('CONTENT_TYPE', '')
609 617
610 618 path = environ['PATH_INFO']
611 619 is_lfs_request = GIT_LFS_CONTENT_TYPE in content_type
612 620 log.debug(
613 621 'LFS: Detecting if request `%s` is LFS server path based '
614 622 'on content type:`%s`, is_lfs:%s',
615 623 path, content_type, is_lfs_request)
616 624
617 625 if not is_lfs_request:
618 626 # fallback detection by path
619 627 if GIT_LFS_PROTO_PAT.match(path):
620 628 is_lfs_request = True
621 629 log.debug(
622 630 'LFS: fallback detection by path of: `%s`, is_lfs:%s',
623 631 path, is_lfs_request)
624 632
625 633 if is_lfs_request:
626 634 app = scm_app.create_git_lfs_wsgi_app(
627 635 repo_path, repo_name, config)
628 636 else:
629 637 app = scm_app.create_git_wsgi_app(
630 638 repo_path, repo_name, config)
631 639
632 640 log.debug('http-app: starting app handler '
633 641 'with %s and process request', app)
634 642
635 643 return app(environ, start_response)
636 644
637 645 return _git_stream
638 646
639 647 def handle_vcs_exception(self, exception, request):
640 648 _vcs_kind = getattr(exception, '_vcs_kind', '')
641 649 if _vcs_kind == 'repo_locked':
642 650 # Get custom repo-locked status code if present.
643 651 status_code = request.headers.get('X-RC-Locked-Status-Code')
644 652 return HTTPRepoLocked(
645 653 title=exception.message, status_code=status_code)
646 654
647 655 elif _vcs_kind == 'repo_branch_protected':
648 656 # Get custom repo-branch-protected status code if present.
649 657 return HTTPRepoBranchProtected(title=exception.message)
650 658
651 659 exc_info = request.exc_info
652 660 store_exception(id(exc_info), exc_info)
653 661
654 662 traceback_info = 'unavailable'
655 663 if request.exc_info:
656 664 exc_type, exc_value, exc_tb = request.exc_info
657 665 traceback_info = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))
658 666
659 667 log.error(
660 668 'error occurred handling this request for path: %s, \n tb: %s',
661 669 request.path, traceback_info)
662 670
663 671 statsd = request.registry.statsd
664 672 if statsd:
665 673 exc_type = "{}.{}".format(exception.__class__.__module__, exception.__class__.__name__)
666 674 statsd.incr('vcsserver_exception_total',
667 675 tags=["type:{}".format(exc_type)])
668 676 raise exception
669 677
670 678
671 679 class ResponseFilter(object):
672 680
673 681 def __init__(self, start_response):
674 682 self._start_response = start_response
675 683
676 684 def __call__(self, status, response_headers, exc_info=None):
677 685 headers = tuple(
678 686 (h, v) for h, v in response_headers
679 687 if not wsgiref.util.is_hop_by_hop(h))
680 688 return self._start_response(status, headers, exc_info)
681 689
682 690
683 691 def sanitize_settings_and_apply_defaults(global_config, settings):
684 692 global_settings_maker = SettingsMaker(global_config)
685 693 settings_maker = SettingsMaker(settings)
686 694
687 695 settings_maker.make_setting('logging.autoconfigure', False, parser='bool')
688 696
689 697 logging_conf = os.path.join(os.path.dirname(global_config.get('__file__')), 'logging.ini')
690 698 settings_maker.enable_logging(logging_conf)
691 699
692 700 # Default includes, possible to change as a user
693 701 pyramid_includes = settings_maker.make_setting('pyramid.includes', [], parser='list:newline')
694 702 log.debug("Using the following pyramid.includes: %s", pyramid_includes)
695 703
696 704 settings_maker.make_setting('__file__', global_config.get('__file__'))
697 705
698 706 settings_maker.make_setting('pyramid.default_locale_name', 'en')
699 707 settings_maker.make_setting('locale', 'en_US.UTF-8')
700 708
701 709 settings_maker.make_setting('core.binary_dir', '')
702 710
703 711 temp_store = tempfile.gettempdir()
704 712 default_cache_dir = os.path.join(temp_store, 'rc_cache')
705 713 # save default, cache dir, and use it for all backends later.
706 714 default_cache_dir = settings_maker.make_setting(
707 715 'cache_dir',
708 716 default=default_cache_dir, default_when_empty=True,
709 717 parser='dir:ensured')
710 718
711 719 # exception store cache
712 720 settings_maker.make_setting(
713 721 'exception_tracker.store_path',
714 722 default=os.path.join(default_cache_dir, 'exc_store'), default_when_empty=True,
715 723 parser='dir:ensured'
716 724 )
717 725
718 726 # repo_object cache defaults
719 727 settings_maker.make_setting(
720 728 'rc_cache.repo_object.backend',
721 729 default='dogpile.cache.rc.file_namespace',
722 730 parser='string')
723 731 settings_maker.make_setting(
724 732 'rc_cache.repo_object.expiration_time',
725 733 default=30 * 24 * 60 * 60, # 30days
726 734 parser='int')
727 735 settings_maker.make_setting(
728 736 'rc_cache.repo_object.arguments.filename',
729 737 default=os.path.join(default_cache_dir, 'vcsserver_cache_repo_object.db'),
730 738 parser='string')
731 739
732 740 # statsd
733 741 settings_maker.make_setting('statsd.enabled', False, parser='bool')
734 742 settings_maker.make_setting('statsd.statsd_host', 'statsd-exporter', parser='string')
735 743 settings_maker.make_setting('statsd.statsd_port', 9125, parser='int')
736 744 settings_maker.make_setting('statsd.statsd_prefix', '')
737 745 settings_maker.make_setting('statsd.statsd_ipv6', False, parser='bool')
738 746
739 747 settings_maker.env_expand()
740 748
741 749
742 750 def main(global_config, **settings):
743 751 start_time = time.time()
744 752 log.info('Pyramid app config starting')
745 753
746 754 if MercurialFactory:
747 755 hgpatches.patch_largefiles_capabilities()
748 756 hgpatches.patch_subrepo_type_mapping()
749 757
750 758 # Fill in and sanitize the defaults & do ENV expansion
751 759 sanitize_settings_and_apply_defaults(global_config, settings)
752 760
753 761 # init and bootstrap StatsdClient
754 762 StatsdClient.setup(settings)
755 763
756 764 pyramid_app = HTTPApplication(settings=settings, global_config=global_config).wsgi_app()
757 765 total_time = time.time() - start_time
758 766 log.info('Pyramid app `%s` created and configured in %.2fs',
759 767 getattr(pyramid_app, 'func_name', 'pyramid_app'), total_time)
760 768 return pyramid_app
761 769
762 770
@@ -1,1367 +1,1368 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2020 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 collections
19 19 import logging
20 20 import os
21 21 import posixpath as vcspath
22 22 import re
23 23 import stat
24 24 import traceback
25 25 import urllib.request, urllib.parse, urllib.error
26 26 import urllib.request, urllib.error, urllib.parse
27 27 from functools import wraps
28 28
29 29 import more_itertools
30 30 import pygit2
31 31 from pygit2 import Repository as LibGit2Repo
32 32 from pygit2 import index as LibGit2Index
33 33 from dulwich import index, objects
34 34 from dulwich.client import HttpGitClient, LocalGitClient
35 35 from dulwich.errors import (
36 36 NotGitRepository, ChecksumMismatch, WrongObjectException,
37 37 MissingCommitError, ObjectMissing, HangupException,
38 38 UnexpectedCommandError)
39 39 from dulwich.repo import Repo as DulwichRepo
40 40 from dulwich.server import update_server_info
41 41
42 42 from vcsserver import exceptions, settings, subprocessio
43 43 from vcsserver.str_utils import safe_str, safe_int, safe_bytes, ascii_str, ascii_bytes
44 from vcsserver.base import RepoFactory, obfuscate_qs, ArchiveNode, archive_repo
44 from vcsserver.base import RepoFactory, obfuscate_qs, ArchiveNode, archive_repo, BinaryEnvelope
45 45 from vcsserver.hgcompat import (
46 46 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler)
47 47 from vcsserver.git_lfs.lib import LFSOidStore
48 48 from vcsserver.vcs_base import RemoteBase
49 49
50 50 DIR_STAT = stat.S_IFDIR
51 51 FILE_MODE = stat.S_IFMT
52 52 GIT_LINK = objects.S_IFGITLINK
53 53 PEELED_REF_MARKER = b'^{}'
54 54 HEAD_MARKER = b'HEAD'
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 def reraise_safe_exceptions(func):
60 60 """Converts Dulwich exceptions to something neutral."""
61 61
62 62 @wraps(func)
63 63 def wrapper(*args, **kwargs):
64 64 try:
65 65 return func(*args, **kwargs)
66 66 except (ChecksumMismatch, WrongObjectException, MissingCommitError, ObjectMissing,) as e:
67 67 exc = exceptions.LookupException(org_exc=e)
68 68 raise exc(safe_str(e))
69 69 except (HangupException, UnexpectedCommandError) as e:
70 70 exc = exceptions.VcsException(org_exc=e)
71 71 raise exc(safe_str(e))
72 72 except Exception as e:
73 73 # NOTE(marcink): becuase of how dulwich handles some exceptions
74 74 # (KeyError on empty repos), we cannot track this and catch all
75 75 # exceptions, it's an exceptions from other handlers
76 76 #if not hasattr(e, '_vcs_kind'):
77 77 #log.exception("Unhandled exception in git remote call")
78 78 #raise_from_original(exceptions.UnhandledException)
79 79 raise
80 80 return wrapper
81 81
82 82
83 83 class Repo(DulwichRepo):
84 84 """
85 85 A wrapper for dulwich Repo class.
86 86
87 87 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
88 88 "Too many open files" error. We need to close all opened file descriptors
89 89 once the repo object is destroyed.
90 90 """
91 91 def __del__(self):
92 92 if hasattr(self, 'object_store'):
93 93 self.close()
94 94
95 95
96 96 class Repository(LibGit2Repo):
97 97
98 98 def __enter__(self):
99 99 return self
100 100
101 101 def __exit__(self, exc_type, exc_val, exc_tb):
102 102 self.free()
103 103
104 104
105 105 class GitFactory(RepoFactory):
106 106 repo_type = 'git'
107 107
108 108 def _create_repo(self, wire, create, use_libgit2=False):
109 109 if use_libgit2:
110 110 return Repository(safe_bytes(wire['path']))
111 111 else:
112 112 # dulwich mode
113 113 repo_path = safe_str(wire['path'], to_encoding=settings.WIRE_ENCODING)
114 114 return Repo(repo_path)
115 115
116 116 def repo(self, wire, create=False, use_libgit2=False):
117 117 """
118 118 Get a repository instance for the given path.
119 119 """
120 120 return self._create_repo(wire, create, use_libgit2)
121 121
122 122 def repo_libgit2(self, wire):
123 123 return self.repo(wire, use_libgit2=True)
124 124
125 125
126 126 class GitRemote(RemoteBase):
127 127
128 128 def __init__(self, factory):
129 129 self._factory = factory
130 130 self._bulk_methods = {
131 131 "date": self.date,
132 132 "author": self.author,
133 133 "branch": self.branch,
134 134 "message": self.message,
135 135 "parents": self.parents,
136 136 "_commit": self.revision,
137 137 }
138 138
139 139 def _wire_to_config(self, wire):
140 140 if 'config' in wire:
141 141 return dict([(x[0] + '_' + x[1], x[2]) for x in wire['config']])
142 142 return {}
143 143
144 144 def _remote_conf(self, config):
145 145 params = [
146 146 '-c', 'core.askpass=""',
147 147 ]
148 148 ssl_cert_dir = config.get('vcs_ssl_dir')
149 149 if ssl_cert_dir:
150 150 params.extend(['-c', 'http.sslCAinfo={}'.format(ssl_cert_dir)])
151 151 return params
152 152
153 153 @reraise_safe_exceptions
154 154 def discover_git_version(self):
155 155 stdout, _ = self.run_git_command(
156 156 {}, ['--version'], _bare=True, _safe=True)
157 157 prefix = b'git version'
158 158 if stdout.startswith(prefix):
159 159 stdout = stdout[len(prefix):]
160 160 return safe_str(stdout.strip())
161 161
162 162 @reraise_safe_exceptions
163 163 def is_empty(self, wire):
164 164 repo_init = self._factory.repo_libgit2(wire)
165 165 with repo_init as repo:
166 166
167 167 try:
168 168 has_head = repo.head.name
169 169 if has_head:
170 170 return False
171 171
172 172 # NOTE(marcink): check again using more expensive method
173 173 return repo.is_empty
174 174 except Exception:
175 175 pass
176 176
177 177 return True
178 178
179 179 @reraise_safe_exceptions
180 180 def assert_correct_path(self, wire):
181 181 cache_on, context_uid, repo_id = self._cache_on(wire)
182 182 region = self._region(wire)
183 183
184 184 @region.conditional_cache_on_arguments(condition=cache_on)
185 185 def _assert_correct_path(_context_uid, _repo_id):
186 186 try:
187 187 repo_init = self._factory.repo_libgit2(wire)
188 188 with repo_init as repo:
189 189 pass
190 190 except pygit2.GitError:
191 191 path = wire.get('path')
192 192 tb = traceback.format_exc()
193 193 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
194 194 return False
195 195
196 196 return True
197 197 return _assert_correct_path(context_uid, repo_id)
198 198
199 199 @reraise_safe_exceptions
200 200 def bare(self, wire):
201 201 repo_init = self._factory.repo_libgit2(wire)
202 202 with repo_init as repo:
203 203 return repo.is_bare
204 204
205 205 @reraise_safe_exceptions
206 206 def blob_as_pretty_string(self, wire, sha):
207 207 repo_init = self._factory.repo_libgit2(wire)
208 208 with repo_init as repo:
209 209 blob_obj = repo[sha]
210 blob = blob_obj.data
211 return blob
210 return BinaryEnvelope(blob_obj.data)
212 211
213 212 @reraise_safe_exceptions
214 213 def blob_raw_length(self, wire, sha):
215 214 cache_on, context_uid, repo_id = self._cache_on(wire)
216 215 region = self._region(wire)
217 216
218 217 @region.conditional_cache_on_arguments(condition=cache_on)
219 218 def _blob_raw_length(_repo_id, _sha):
220 219
221 220 repo_init = self._factory.repo_libgit2(wire)
222 221 with repo_init as repo:
223 222 blob = repo[sha]
224 223 return blob.size
225 224
226 225 return _blob_raw_length(repo_id, sha)
227 226
228 227 def _parse_lfs_pointer(self, raw_content):
229 228 spec_string = b'version https://git-lfs.github.com/spec'
230 229 if raw_content and raw_content.startswith(spec_string):
231 230
232 231 pattern = re.compile(rb"""
233 232 (?:\n)?
234 233 ^version[ ]https://git-lfs\.github\.com/spec/(?P<spec_ver>v\d+)\n
235 234 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
236 235 ^size[ ](?P<oid_size>[0-9]+)\n
237 236 (?:\n)?
238 237 """, re.VERBOSE | re.MULTILINE)
239 238 match = pattern.match(raw_content)
240 239 if match:
241 240 return match.groupdict()
242 241
243 242 return {}
244 243
245 244 @reraise_safe_exceptions
246 245 def is_large_file(self, wire, commit_id):
247 246 cache_on, context_uid, repo_id = self._cache_on(wire)
248 247 region = self._region(wire)
249 248
250 249 @region.conditional_cache_on_arguments(condition=cache_on)
251 250 def _is_large_file(_repo_id, _sha):
252 251 repo_init = self._factory.repo_libgit2(wire)
253 252 with repo_init as repo:
254 253 blob = repo[commit_id]
255 254 if blob.is_binary:
256 255 return {}
257 256
258 257 return self._parse_lfs_pointer(blob.data)
259 258
260 259 return _is_large_file(repo_id, commit_id)
261 260
262 261 @reraise_safe_exceptions
263 262 def is_binary(self, wire, tree_id):
264 263 cache_on, context_uid, repo_id = self._cache_on(wire)
265 264 region = self._region(wire)
266 265
267 266 @region.conditional_cache_on_arguments(condition=cache_on)
268 267 def _is_binary(_repo_id, _tree_id):
269 268 repo_init = self._factory.repo_libgit2(wire)
270 269 with repo_init as repo:
271 270 blob_obj = repo[tree_id]
272 271 return blob_obj.is_binary
273 272
274 273 return _is_binary(repo_id, tree_id)
275 274
276 275 @reraise_safe_exceptions
277 276 def md5_hash(self, wire, tree_id):
278 277 cache_on, context_uid, repo_id = self._cache_on(wire)
279 278 region = self._region(wire)
280 279
281 280 @region.conditional_cache_on_arguments(condition=cache_on)
282 281 def _md5_hash(_repo_id, _tree_id):
283 282 return ''
284 283
285 284 return _md5_hash(repo_id, tree_id)
286 285
287 286 @reraise_safe_exceptions
288 287 def in_largefiles_store(self, wire, oid):
289 288 conf = self._wire_to_config(wire)
290 289 repo_init = self._factory.repo_libgit2(wire)
291 290 with repo_init as repo:
292 291 repo_name = repo.path
293 292
294 293 store_location = conf.get('vcs_git_lfs_store_location')
295 294 if store_location:
296 295
297 296 store = LFSOidStore(
298 297 oid=oid, repo=repo_name, store_location=store_location)
299 298 return store.has_oid()
300 299
301 300 return False
302 301
303 302 @reraise_safe_exceptions
304 303 def store_path(self, wire, oid):
305 304 conf = self._wire_to_config(wire)
306 305 repo_init = self._factory.repo_libgit2(wire)
307 306 with repo_init as repo:
308 307 repo_name = repo.path
309 308
310 309 store_location = conf.get('vcs_git_lfs_store_location')
311 310 if store_location:
312 311 store = LFSOidStore(
313 312 oid=oid, repo=repo_name, store_location=store_location)
314 313 return store.oid_path
315 314 raise ValueError('Unable to fetch oid with path {}'.format(oid))
316 315
317 316 @reraise_safe_exceptions
318 317 def bulk_request(self, wire, rev, pre_load):
319 318 cache_on, context_uid, repo_id = self._cache_on(wire)
320 319 region = self._region(wire)
321 320
322 321 @region.conditional_cache_on_arguments(condition=cache_on)
323 322 def _bulk_request(_repo_id, _rev, _pre_load):
324 323 result = {}
325 324 for attr in pre_load:
326 325 try:
327 326 method = self._bulk_methods[attr]
328 327 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
329 328 args = [wire, rev]
330 329 result[attr] = method(*args)
331 330 except KeyError as e:
332 331 raise exceptions.VcsException(e)(f"Unknown bulk attribute: {attr}")
333 332 return result
334 333
335 334 return _bulk_request(repo_id, rev, sorted(pre_load))
336 335
337 336 def _build_opener(self, url):
338 337 handlers = []
339 338 url_obj = url_parser(url)
340 339 _, authinfo = url_obj.authinfo()
341 340
342 341 if authinfo:
343 342 # create a password manager
344 343 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
345 344 passmgr.add_password(*authinfo)
346 345
347 346 handlers.extend((httpbasicauthhandler(passmgr),
348 347 httpdigestauthhandler(passmgr)))
349 348
350 349 return urllib.request.build_opener(*handlers)
351 350
352 351 def _type_id_to_name(self, type_id: int):
353 352 return {
354 353 1: 'commit',
355 354 2: 'tree',
356 355 3: 'blob',
357 356 4: 'tag'
358 357 }[type_id]
359 358
360 359 @reraise_safe_exceptions
361 360 def check_url(self, url, config):
362 361 url_obj = url_parser(safe_bytes(url))
363 362 test_uri, _ = url_obj.authinfo()
364 363 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
365 364 url_obj.query = obfuscate_qs(url_obj.query)
366 365 cleaned_uri = str(url_obj)
367 366 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
368 367
369 368 if not test_uri.endswith('info/refs'):
370 369 test_uri = test_uri.rstrip('/') + '/info/refs'
371 370
372 371 o = self._build_opener(url)
373 372 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
374 373
375 374 q = {"service": 'git-upload-pack'}
376 375 qs = '?%s' % urllib.parse.urlencode(q)
377 376 cu = "%s%s" % (test_uri, qs)
378 377 req = urllib.request.Request(cu, None, {})
379 378
380 379 try:
381 380 log.debug("Trying to open URL %s", cleaned_uri)
382 381 resp = o.open(req)
383 382 if resp.code != 200:
384 383 raise exceptions.URLError()('Return Code is not 200')
385 384 except Exception as e:
386 385 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
387 386 # means it cannot be cloned
388 387 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
389 388
390 389 # now detect if it's proper git repo
391 390 gitdata = resp.read()
392 391 if 'service=git-upload-pack' in gitdata:
393 392 pass
394 393 elif re.findall(r'[0-9a-fA-F]{40}\s+refs', gitdata):
395 394 # old style git can return some other format !
396 395 pass
397 396 else:
398 397 raise exceptions.URLError()(
399 398 "url [%s] does not look like an git" % (cleaned_uri,))
400 399
401 400 return True
402 401
403 402 @reraise_safe_exceptions
404 403 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
405 404 # TODO(marcink): deprecate this method. Last i checked we don't use it anymore
406 405 remote_refs = self.pull(wire, url, apply_refs=False)
407 406 repo = self._factory.repo(wire)
408 407 if isinstance(valid_refs, list):
409 408 valid_refs = tuple(valid_refs)
410 409
411 410 for k in remote_refs:
412 411 # only parse heads/tags and skip so called deferred tags
413 412 if k.startswith(valid_refs) and not k.endswith(deferred):
414 413 repo[k] = remote_refs[k]
415 414
416 415 if update_after_clone:
417 416 # we want to checkout HEAD
418 417 repo["HEAD"] = remote_refs["HEAD"]
419 418 index.build_index_from_tree(repo.path, repo.index_path(),
420 419 repo.object_store, repo["HEAD"].tree)
421 420
422 421 @reraise_safe_exceptions
423 422 def branch(self, wire, commit_id):
424 423 cache_on, context_uid, repo_id = self._cache_on(wire)
425 424 region = self._region(wire)
426 425 @region.conditional_cache_on_arguments(condition=cache_on)
427 426 def _branch(_context_uid, _repo_id, _commit_id):
428 427 regex = re.compile('^refs/heads')
429 428
430 429 def filter_with(ref):
431 430 return regex.match(ref[0]) and ref[1] == _commit_id
432 431
433 432 branches = list(filter(filter_with, list(self.get_refs(wire).items())))
434 433 return [x[0].split('refs/heads/')[-1] for x in branches]
435 434
436 435 return _branch(context_uid, repo_id, commit_id)
437 436
438 437 @reraise_safe_exceptions
439 438 def commit_branches(self, wire, commit_id):
440 439 cache_on, context_uid, repo_id = self._cache_on(wire)
441 440 region = self._region(wire)
442 441 @region.conditional_cache_on_arguments(condition=cache_on)
443 442 def _commit_branches(_context_uid, _repo_id, _commit_id):
444 443 repo_init = self._factory.repo_libgit2(wire)
445 444 with repo_init as repo:
446 445 branches = [x for x in repo.branches.with_commit(_commit_id)]
447 446 return branches
448 447
449 448 return _commit_branches(context_uid, repo_id, commit_id)
450 449
451 450 @reraise_safe_exceptions
452 451 def add_object(self, wire, content):
453 452 repo_init = self._factory.repo_libgit2(wire)
454 453 with repo_init as repo:
455 454 blob = objects.Blob()
456 455 blob.set_raw_string(content)
457 456 repo.object_store.add_object(blob)
458 457 return blob.id
459 458
460 459 # TODO: this is quite complex, check if that can be simplified
461 460 @reraise_safe_exceptions
462 461 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
463 462 # Defines the root tree
464 463 class _Root(object):
465 464 def __repr__(self):
466 465 return 'ROOT TREE'
467 466 ROOT = _Root()
468 467
469 468 repo = self._factory.repo(wire)
470 469 object_store = repo.object_store
471 470
472 471 # Create tree and populates it with blobs
473 472 if commit_tree:
474 473 commit_tree = safe_bytes(commit_tree)
475 474
476 475 if commit_tree and repo[commit_tree]:
477 476 git_commit = repo[safe_bytes(commit_data['parents'][0])]
478 477 commit_tree = repo[git_commit.tree] # root tree
479 478 else:
480 479 commit_tree = objects.Tree()
481 480
482 481 for node in updated:
483 482 # Compute subdirs if needed
484 483 dirpath, nodename = vcspath.split(node['path'])
485 484 dirnames = list(map(safe_str, dirpath and dirpath.split('/') or []))
486 485 parent = commit_tree
487 486 ancestors = [('', parent)]
488 487
489 488 # Tries to dig for the deepest existing tree
490 489 while dirnames:
491 490 curdir = dirnames.pop(0)
492 491 try:
493 492 dir_id = parent[curdir][1]
494 493 except KeyError:
495 494 # put curdir back into dirnames and stops
496 495 dirnames.insert(0, curdir)
497 496 break
498 497 else:
499 498 # If found, updates parent
500 499 parent = repo[dir_id]
501 500 ancestors.append((curdir, parent))
502 501 # Now parent is deepest existing tree and we need to create
503 502 # subtrees for dirnames (in reverse order)
504 503 # [this only applies for nodes from added]
505 504 new_trees = []
506 505
507 506 blob = objects.Blob.from_string(node['content'])
508 507
508 node_path = safe_bytes(node['node_path'])
509
509 510 if dirnames:
510 511 # If there are trees which should be created we need to build
511 512 # them now (in reverse order)
512 513 reversed_dirnames = list(reversed(dirnames))
513 514 curtree = objects.Tree()
514 curtree[node['node_path']] = node['mode'], blob.id
515 curtree[node_path] = node['mode'], blob.id
515 516 new_trees.append(curtree)
516 517 for dirname in reversed_dirnames[:-1]:
517 518 newtree = objects.Tree()
518 519 newtree[dirname] = (DIR_STAT, curtree.id)
519 520 new_trees.append(newtree)
520 521 curtree = newtree
521 522 parent[reversed_dirnames[-1]] = (DIR_STAT, curtree.id)
522 523 else:
523 parent.add(name=node['node_path'], mode=node['mode'], hexsha=blob.id)
524 parent.add(name=node_path, mode=node['mode'], hexsha=blob.id)
524 525
525 526 new_trees.append(parent)
526 527 # Update ancestors
527 528 reversed_ancestors = reversed(
528 529 [(a[1], b[1], b[0]) for a, b in zip(ancestors, ancestors[1:])])
529 530 for parent, tree, path in reversed_ancestors:
530 531 parent[path] = (DIR_STAT, tree.id)
531 532 object_store.add_object(tree)
532 533
533 534 object_store.add_object(blob)
534 535 for tree in new_trees:
535 536 object_store.add_object(tree)
536 537
537 538 for node_path in removed:
538 539 paths = node_path.split('/')
539 540 tree = commit_tree # start with top-level
540 541 trees = [{'tree': tree, 'path': ROOT}]
541 542 # Traverse deep into the forest...
542 543 # resolve final tree by iterating the path.
543 544 # e.g a/b/c.txt will get
544 545 # - root as tree then
545 546 # - 'a' as tree,
546 547 # - 'b' as tree,
547 548 # - stop at c as blob.
548 549 for path in paths:
549 550 try:
550 551 obj = repo[tree[path][1]]
551 552 if isinstance(obj, objects.Tree):
552 553 trees.append({'tree': obj, 'path': path})
553 554 tree = obj
554 555 except KeyError:
555 556 break
556 557 #PROBLEM:
557 558 """
558 559 We're not editing same reference tree object
559 560 """
560 561 # Cut down the blob and all rotten trees on the way back...
561 562 for path, tree_data in reversed(list(zip(paths, trees))):
562 563 tree = tree_data['tree']
563 564 tree.__delitem__(path)
564 565 # This operation edits the tree, we need to mark new commit back
565 566
566 567 if len(tree) > 0:
567 568 # This tree still has elements - don't remove it or any
568 569 # of it's parents
569 570 break
570 571
571 572 object_store.add_object(commit_tree)
572 573
573 574 # Create commit
574 575 commit = objects.Commit()
575 576 commit.tree = commit_tree.id
576 577 bytes_keys = [
577 578 'author',
578 579 'committer',
579 580 'message',
580 581 'encoding',
581 582 'parents'
582 583 ]
583 584
584 585 for k, v in commit_data.items():
585 586 if k in bytes_keys:
586 587 if k == 'parents':
587 588 v = [safe_bytes(x) for x in v]
588 589 else:
589 590 v = safe_bytes(v)
590 591 setattr(commit, k, v)
591 592
592 593 object_store.add_object(commit)
593 594
594 595 self.create_branch(wire, branch, safe_str(commit.id))
595 596
596 597 # dulwich set-ref
597 598 repo.refs[safe_bytes(f'refs/heads/{branch}')] = commit.id
598 599
599 600 return commit.id
600 601
601 602 @reraise_safe_exceptions
602 603 def pull(self, wire, url, apply_refs=True, refs=None, update_after=False):
603 604 if url != 'default' and '://' not in url:
604 605 client = LocalGitClient(url)
605 606 else:
606 607 url_obj = url_parser(url)
607 608 o = self._build_opener(url)
608 609 url, _ = url_obj.authinfo()
609 610 client = HttpGitClient(base_url=url, opener=o)
610 611 repo = self._factory.repo(wire)
611 612
612 613 determine_wants = repo.object_store.determine_wants_all
613 614 if refs:
614 615 refs = [ascii_bytes(x) for x in refs]
615 616
616 617 def determine_wants_requested(remote_refs):
617 618 determined = []
618 619 for ref_name, ref_hash in remote_refs.items():
619 620 bytes_ref_name = safe_bytes(ref_name)
620 621
621 622 if bytes_ref_name in refs:
622 623 bytes_ref_hash = safe_bytes(ref_hash)
623 624 determined.append(bytes_ref_hash)
624 625 return determined
625 626
626 627 # swap with our custom requested wants
627 628 determine_wants = determine_wants_requested
628 629
629 630 try:
630 631 remote_refs = client.fetch(
631 632 path=url, target=repo, determine_wants=determine_wants)
632 633
633 634 except NotGitRepository as e:
634 635 log.warning(
635 636 'Trying to fetch from "%s" failed, not a Git repository.', url)
636 637 # Exception can contain unicode which we convert
637 638 raise exceptions.AbortException(e)(repr(e))
638 639
639 640 # mikhail: client.fetch() returns all the remote refs, but fetches only
640 641 # refs filtered by `determine_wants` function. We need to filter result
641 642 # as well
642 643 if refs:
643 644 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
644 645
645 646 if apply_refs:
646 647 # TODO: johbo: Needs proper test coverage with a git repository
647 648 # that contains a tag object, so that we would end up with
648 649 # a peeled ref at this point.
649 650 for k in remote_refs:
650 651 if k.endswith(PEELED_REF_MARKER):
651 652 log.debug("Skipping peeled reference %s", k)
652 653 continue
653 654 repo[k] = remote_refs[k]
654 655
655 656 if refs and not update_after:
656 657 # mikhail: explicitly set the head to the last ref.
657 658 repo[HEAD_MARKER] = remote_refs[refs[-1]]
658 659
659 660 if update_after:
660 661 # we want to checkout HEAD
661 662 repo[HEAD_MARKER] = remote_refs[HEAD_MARKER]
662 663 index.build_index_from_tree(repo.path, repo.index_path(),
663 664 repo.object_store, repo[HEAD_MARKER].tree)
664 665 return remote_refs
665 666
666 667 @reraise_safe_exceptions
667 668 def sync_fetch(self, wire, url, refs=None, all_refs=False):
668 669 repo = self._factory.repo(wire)
669 670 if refs and not isinstance(refs, (list, tuple)):
670 671 refs = [refs]
671 672
672 673 config = self._wire_to_config(wire)
673 674 # get all remote refs we'll use to fetch later
674 675 cmd = ['ls-remote']
675 676 if not all_refs:
676 677 cmd += ['--heads', '--tags']
677 678 cmd += [url]
678 679 output, __ = self.run_git_command(
679 680 wire, cmd, fail_on_stderr=False,
680 681 _copts=self._remote_conf(config),
681 682 extra_env={'GIT_TERMINAL_PROMPT': '0'})
682 683
683 684 remote_refs = collections.OrderedDict()
684 685 fetch_refs = []
685 686
686 687 for ref_line in output.splitlines():
687 688 sha, ref = ref_line.split(b'\t')
688 689 sha = sha.strip()
689 690 if ref in remote_refs:
690 691 # duplicate, skip
691 692 continue
692 693 if ref.endswith(PEELED_REF_MARKER):
693 694 log.debug("Skipping peeled reference %s", ref)
694 695 continue
695 696 # don't sync HEAD
696 697 if ref in [HEAD_MARKER]:
697 698 continue
698 699
699 700 remote_refs[ref] = sha
700 701
701 702 if refs and sha in refs:
702 703 # we filter fetch using our specified refs
703 704 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
704 705 elif not refs:
705 706 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
706 707 log.debug('Finished obtaining fetch refs, total: %s', len(fetch_refs))
707 708
708 709 if fetch_refs:
709 710 for chunk in more_itertools.chunked(fetch_refs, 1024 * 4):
710 711 fetch_refs_chunks = list(chunk)
711 712 log.debug('Fetching %s refs from import url', len(fetch_refs_chunks))
712 713 self.run_git_command(
713 714 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs_chunks,
714 715 fail_on_stderr=False,
715 716 _copts=self._remote_conf(config),
716 717 extra_env={'GIT_TERMINAL_PROMPT': '0'})
717 718
718 719 return remote_refs
719 720
720 721 @reraise_safe_exceptions
721 722 def sync_push(self, wire, url, refs=None):
722 723 if not self.check_url(url, wire):
723 724 return
724 725 config = self._wire_to_config(wire)
725 726 self._factory.repo(wire)
726 727 self.run_git_command(
727 728 wire, ['push', url, '--mirror'], fail_on_stderr=False,
728 729 _copts=self._remote_conf(config),
729 730 extra_env={'GIT_TERMINAL_PROMPT': '0'})
730 731
731 732 @reraise_safe_exceptions
732 733 def get_remote_refs(self, wire, url):
733 734 repo = Repo(url)
734 735 return repo.get_refs()
735 736
736 737 @reraise_safe_exceptions
737 738 def get_description(self, wire):
738 739 repo = self._factory.repo(wire)
739 740 return repo.get_description()
740 741
741 742 @reraise_safe_exceptions
742 743 def get_missing_revs(self, wire, rev1, rev2, path2):
743 744 repo = self._factory.repo(wire)
744 745 LocalGitClient(thin_packs=False).fetch(path2, repo)
745 746
746 747 wire_remote = wire.copy()
747 748 wire_remote['path'] = path2
748 749 repo_remote = self._factory.repo(wire_remote)
749 750 LocalGitClient(thin_packs=False).fetch(wire["path"], repo_remote)
750 751
751 752 revs = [
752 753 x.commit.id
753 754 for x in repo_remote.get_walker(include=[rev2], exclude=[rev1])]
754 755 return revs
755 756
756 757 @reraise_safe_exceptions
757 758 def get_object(self, wire, sha, maybe_unreachable=False):
758 759 cache_on, context_uid, repo_id = self._cache_on(wire)
759 760 region = self._region(wire)
760 761
761 762 @region.conditional_cache_on_arguments(condition=cache_on)
762 763 def _get_object(_context_uid, _repo_id, _sha):
763 764 repo_init = self._factory.repo_libgit2(wire)
764 765 with repo_init as repo:
765 766
766 767 missing_commit_err = 'Commit {} does not exist for `{}`'.format(sha, wire['path'])
767 768 try:
768 769 commit = repo.revparse_single(sha)
769 770 except KeyError:
770 771 # NOTE(marcink): KeyError doesn't give us any meaningful information
771 772 # here, we instead give something more explicit
772 773 e = exceptions.RefNotFoundException('SHA: %s not found', sha)
773 774 raise exceptions.LookupException(e)(missing_commit_err)
774 775 except ValueError as e:
775 776 raise exceptions.LookupException(e)(missing_commit_err)
776 777
777 778 is_tag = False
778 779 if isinstance(commit, pygit2.Tag):
779 780 commit = repo.get(commit.target)
780 781 is_tag = True
781 782
782 783 check_dangling = True
783 784 if is_tag:
784 785 check_dangling = False
785 786
786 787 if check_dangling and maybe_unreachable:
787 788 check_dangling = False
788 789
789 790 # we used a reference and it parsed means we're not having a dangling commit
790 791 if sha != commit.hex:
791 792 check_dangling = False
792 793
793 794 if check_dangling:
794 795 # check for dangling commit
795 796 for branch in repo.branches.with_commit(commit.hex):
796 797 if branch:
797 798 break
798 799 else:
799 800 # NOTE(marcink): Empty error doesn't give us any meaningful information
800 801 # here, we instead give something more explicit
801 802 e = exceptions.RefNotFoundException('SHA: %s not found in branches', sha)
802 803 raise exceptions.LookupException(e)(missing_commit_err)
803 804
804 805 commit_id = commit.hex
805 806 type_id = commit.type
806 807
807 808 return {
808 809 'id': commit_id,
809 810 'type': self._type_id_to_name(type_id),
810 811 'commit_id': commit_id,
811 812 'idx': 0
812 813 }
813 814
814 815 return _get_object(context_uid, repo_id, sha)
815 816
816 817 @reraise_safe_exceptions
817 818 def get_refs(self, wire):
818 819 cache_on, context_uid, repo_id = self._cache_on(wire)
819 820 region = self._region(wire)
820 821
821 822 @region.conditional_cache_on_arguments(condition=cache_on)
822 823 def _get_refs(_context_uid, _repo_id):
823 824
824 825 repo_init = self._factory.repo_libgit2(wire)
825 826 with repo_init as repo:
826 827 regex = re.compile('^refs/(heads|tags)/')
827 828 return {x.name: x.target.hex for x in
828 829 [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]}
829 830
830 831 return _get_refs(context_uid, repo_id)
831 832
832 833 @reraise_safe_exceptions
833 834 def get_branch_pointers(self, wire):
834 835 cache_on, context_uid, repo_id = self._cache_on(wire)
835 836 region = self._region(wire)
836 837
837 838 @region.conditional_cache_on_arguments(condition=cache_on)
838 839 def _get_branch_pointers(_context_uid, _repo_id):
839 840
840 841 repo_init = self._factory.repo_libgit2(wire)
841 842 regex = re.compile('^refs/heads')
842 843 with repo_init as repo:
843 844 branches = [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]
844 845 return {x.target.hex: x.shorthand for x in branches}
845 846
846 847 return _get_branch_pointers(context_uid, repo_id)
847 848
848 849 @reraise_safe_exceptions
849 850 def head(self, wire, show_exc=True):
850 851 cache_on, context_uid, repo_id = self._cache_on(wire)
851 852 region = self._region(wire)
852 853
853 854 @region.conditional_cache_on_arguments(condition=cache_on)
854 855 def _head(_context_uid, _repo_id, _show_exc):
855 856 repo_init = self._factory.repo_libgit2(wire)
856 857 with repo_init as repo:
857 858 try:
858 859 return repo.head.peel().hex
859 860 except Exception:
860 861 if show_exc:
861 862 raise
862 863 return _head(context_uid, repo_id, show_exc)
863 864
864 865 @reraise_safe_exceptions
865 866 def init(self, wire):
866 867 repo_path = safe_str(wire['path'])
867 868 self.repo = Repo.init(repo_path)
868 869
869 870 @reraise_safe_exceptions
870 871 def init_bare(self, wire):
871 872 repo_path = safe_str(wire['path'])
872 873 self.repo = Repo.init_bare(repo_path)
873 874
874 875 @reraise_safe_exceptions
875 876 def revision(self, wire, rev):
876 877
877 878 cache_on, context_uid, repo_id = self._cache_on(wire)
878 879 region = self._region(wire)
879 880
880 881 @region.conditional_cache_on_arguments(condition=cache_on)
881 882 def _revision(_context_uid, _repo_id, _rev):
882 883 repo_init = self._factory.repo_libgit2(wire)
883 884 with repo_init as repo:
884 885 commit = repo[rev]
885 886 obj_data = {
886 887 'id': commit.id.hex,
887 888 }
888 889 # tree objects itself don't have tree_id attribute
889 890 if hasattr(commit, 'tree_id'):
890 891 obj_data['tree'] = commit.tree_id.hex
891 892
892 893 return obj_data
893 894 return _revision(context_uid, repo_id, rev)
894 895
895 896 @reraise_safe_exceptions
896 897 def date(self, wire, commit_id):
897 898 cache_on, context_uid, repo_id = self._cache_on(wire)
898 899 region = self._region(wire)
899 900
900 901 @region.conditional_cache_on_arguments(condition=cache_on)
901 902 def _date(_repo_id, _commit_id):
902 903 repo_init = self._factory.repo_libgit2(wire)
903 904 with repo_init as repo:
904 905 commit = repo[commit_id]
905 906
906 907 if hasattr(commit, 'commit_time'):
907 908 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
908 909 else:
909 910 commit = commit.get_object()
910 911 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
911 912
912 913 # TODO(marcink): check dulwich difference of offset vs timezone
913 914 return [commit_time, commit_time_offset]
914 915 return _date(repo_id, commit_id)
915 916
916 917 @reraise_safe_exceptions
917 918 def author(self, wire, commit_id):
918 919 cache_on, context_uid, repo_id = self._cache_on(wire)
919 920 region = self._region(wire)
920 921
921 922 @region.conditional_cache_on_arguments(condition=cache_on)
922 923 def _author(_repo_id, _commit_id):
923 924 repo_init = self._factory.repo_libgit2(wire)
924 925 with repo_init as repo:
925 926 commit = repo[commit_id]
926 927
927 928 if hasattr(commit, 'author'):
928 929 author = commit.author
929 930 else:
930 931 author = commit.get_object().author
931 932
932 933 if author.email:
933 934 return "{} <{}>".format(author.name, author.email)
934 935
935 936 try:
936 937 return "{}".format(author.name)
937 938 except Exception:
938 939 return "{}".format(safe_str(author.raw_name))
939 940
940 941 return _author(repo_id, commit_id)
941 942
942 943 @reraise_safe_exceptions
943 944 def message(self, wire, commit_id):
944 945 cache_on, context_uid, repo_id = self._cache_on(wire)
945 946 region = self._region(wire)
946 947 @region.conditional_cache_on_arguments(condition=cache_on)
947 948 def _message(_repo_id, _commit_id):
948 949 repo_init = self._factory.repo_libgit2(wire)
949 950 with repo_init as repo:
950 951 commit = repo[commit_id]
951 952 return commit.message
952 953 return _message(repo_id, commit_id)
953 954
954 955 @reraise_safe_exceptions
955 956 def parents(self, wire, commit_id):
956 957 cache_on, context_uid, repo_id = self._cache_on(wire)
957 958 region = self._region(wire)
958 959
959 960 @region.conditional_cache_on_arguments(condition=cache_on)
960 961 def _parents(_repo_id, _commit_id):
961 962 repo_init = self._factory.repo_libgit2(wire)
962 963 with repo_init as repo:
963 964 commit = repo[commit_id]
964 965 if hasattr(commit, 'parent_ids'):
965 966 parent_ids = commit.parent_ids
966 967 else:
967 968 parent_ids = commit.get_object().parent_ids
968 969
969 970 return [x.hex for x in parent_ids]
970 971 return _parents(repo_id, commit_id)
971 972
972 973 @reraise_safe_exceptions
973 974 def children(self, wire, commit_id):
974 975 cache_on, context_uid, repo_id = self._cache_on(wire)
975 976 region = self._region(wire)
976 977
977 978 head = self.head(wire)
978 979
979 980 @region.conditional_cache_on_arguments(condition=cache_on)
980 981 def _children(_repo_id, _commit_id):
981 982
982 983 output, __ = self.run_git_command(
983 984 wire, ['rev-list', '--all', '--children', f'{commit_id}^..{head}'])
984 985
985 986 child_ids = []
986 987 pat = re.compile(r'^{}'.format(commit_id))
987 988 for line in output.splitlines():
988 989 line = safe_str(line)
989 990 if pat.match(line):
990 991 found_ids = line.split(' ')[1:]
991 992 child_ids.extend(found_ids)
992 993 break
993 994
994 995 return child_ids
995 996 return _children(repo_id, commit_id)
996 997
997 998 @reraise_safe_exceptions
998 999 def set_refs(self, wire, key, value):
999 1000 repo_init = self._factory.repo_libgit2(wire)
1000 1001 with repo_init as repo:
1001 1002 repo.references.create(key, value, force=True)
1002 1003
1003 1004 @reraise_safe_exceptions
1004 1005 def create_branch(self, wire, branch_name, commit_id, force=False):
1005 1006 repo_init = self._factory.repo_libgit2(wire)
1006 1007 with repo_init as repo:
1007 1008 commit = repo[commit_id]
1008 1009
1009 1010 if force:
1010 1011 repo.branches.local.create(branch_name, commit, force=force)
1011 1012 elif not repo.branches.get(branch_name):
1012 1013 # create only if that branch isn't existing
1013 1014 repo.branches.local.create(branch_name, commit, force=force)
1014 1015
1015 1016 @reraise_safe_exceptions
1016 1017 def remove_ref(self, wire, key):
1017 1018 repo_init = self._factory.repo_libgit2(wire)
1018 1019 with repo_init as repo:
1019 1020 repo.references.delete(key)
1020 1021
1021 1022 @reraise_safe_exceptions
1022 1023 def tag_remove(self, wire, tag_name):
1023 1024 repo_init = self._factory.repo_libgit2(wire)
1024 1025 with repo_init as repo:
1025 1026 key = 'refs/tags/{}'.format(tag_name)
1026 1027 repo.references.delete(key)
1027 1028
1028 1029 @reraise_safe_exceptions
1029 1030 def tree_changes(self, wire, source_id, target_id):
1030 1031 # TODO(marcink): remove this seems it's only used by tests
1031 1032 repo = self._factory.repo(wire)
1032 1033 source = repo[source_id].tree if source_id else None
1033 1034 target = repo[target_id].tree
1034 1035 result = repo.object_store.tree_changes(source, target)
1035 1036 return list(result)
1036 1037
1037 1038 @reraise_safe_exceptions
1038 1039 def tree_and_type_for_path(self, wire, commit_id, path):
1039 1040
1040 1041 cache_on, context_uid, repo_id = self._cache_on(wire)
1041 1042 region = self._region(wire)
1042 1043
1043 1044 @region.conditional_cache_on_arguments(condition=cache_on)
1044 1045 def _tree_and_type_for_path(_context_uid, _repo_id, _commit_id, _path):
1045 1046 repo_init = self._factory.repo_libgit2(wire)
1046 1047
1047 1048 with repo_init as repo:
1048 1049 commit = repo[commit_id]
1049 1050 try:
1050 1051 tree = commit.tree[path]
1051 1052 except KeyError:
1052 1053 return None, None, None
1053 1054
1054 1055 return tree.id.hex, tree.type_str, tree.filemode
1055 1056 return _tree_and_type_for_path(context_uid, repo_id, commit_id, path)
1056 1057
1057 1058 @reraise_safe_exceptions
1058 1059 def tree_items(self, wire, tree_id):
1059 1060 cache_on, context_uid, repo_id = self._cache_on(wire)
1060 1061 region = self._region(wire)
1061 1062
1062 1063 @region.conditional_cache_on_arguments(condition=cache_on)
1063 1064 def _tree_items(_repo_id, _tree_id):
1064 1065
1065 1066 repo_init = self._factory.repo_libgit2(wire)
1066 1067 with repo_init as repo:
1067 1068 try:
1068 1069 tree = repo[tree_id]
1069 1070 except KeyError:
1070 1071 raise ObjectMissing('No tree with id: {}'.format(tree_id))
1071 1072
1072 1073 result = []
1073 1074 for item in tree:
1074 1075 item_sha = item.hex
1075 1076 item_mode = item.filemode
1076 1077 item_type = item.type_str
1077 1078
1078 1079 if item_type == 'commit':
1079 1080 # NOTE(marcink): submodules we translate to 'link' for backward compat
1080 1081 item_type = 'link'
1081 1082
1082 1083 result.append((item.name, item_mode, item_sha, item_type))
1083 1084 return result
1084 1085 return _tree_items(repo_id, tree_id)
1085 1086
1086 1087 @reraise_safe_exceptions
1087 1088 def diff_2(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1088 1089 """
1089 1090 Old version that uses subprocess to call diff
1090 1091 """
1091 1092
1092 1093 flags = [
1093 1094 '-U%s' % context, '--patch',
1094 1095 '--binary',
1095 1096 '--find-renames',
1096 1097 '--no-indent-heuristic',
1097 1098 # '--indent-heuristic',
1098 1099 #'--full-index',
1099 1100 #'--abbrev=40'
1100 1101 ]
1101 1102
1102 1103 if opt_ignorews:
1103 1104 flags.append('--ignore-all-space')
1104 1105
1105 1106 if commit_id_1 == self.EMPTY_COMMIT:
1106 1107 cmd = ['show'] + flags + [commit_id_2]
1107 1108 else:
1108 1109 cmd = ['diff'] + flags + [commit_id_1, commit_id_2]
1109 1110
1110 1111 if file_filter:
1111 1112 cmd.extend(['--', file_filter])
1112 1113
1113 1114 diff, __ = self.run_git_command(wire, cmd)
1114 1115 # If we used 'show' command, strip first few lines (until actual diff
1115 1116 # starts)
1116 1117 if commit_id_1 == self.EMPTY_COMMIT:
1117 1118 lines = diff.splitlines()
1118 1119 x = 0
1119 1120 for line in lines:
1120 1121 if line.startswith(b'diff'):
1121 1122 break
1122 1123 x += 1
1123 1124 # Append new line just like 'diff' command do
1124 1125 diff = '\n'.join(lines[x:]) + '\n'
1125 1126 return diff
1126 1127
1127 1128 @reraise_safe_exceptions
1128 1129 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1129 1130 repo_init = self._factory.repo_libgit2(wire)
1130 1131
1131 1132 with repo_init as repo:
1132 1133 swap = True
1133 1134 flags = 0
1134 1135 flags |= pygit2.GIT_DIFF_SHOW_BINARY
1135 1136
1136 1137 if opt_ignorews:
1137 1138 flags |= pygit2.GIT_DIFF_IGNORE_WHITESPACE
1138 1139
1139 1140 if commit_id_1 == self.EMPTY_COMMIT:
1140 1141 comm1 = repo[commit_id_2]
1141 1142 diff_obj = comm1.tree.diff_to_tree(
1142 1143 flags=flags, context_lines=context, swap=swap)
1143 1144
1144 1145 else:
1145 1146 comm1 = repo[commit_id_2]
1146 1147 comm2 = repo[commit_id_1]
1147 1148 diff_obj = comm1.tree.diff_to_tree(
1148 1149 comm2.tree, flags=flags, context_lines=context, swap=swap)
1149 1150 similar_flags = 0
1150 1151 similar_flags |= pygit2.GIT_DIFF_FIND_RENAMES
1151 1152 diff_obj.find_similar(flags=similar_flags)
1152 1153
1153 1154 if file_filter:
1154 1155 for p in diff_obj:
1155 1156 if p.delta.old_file.path == file_filter:
1156 return p.data or ''
1157 return BinaryEnvelope(p.data) or BinaryEnvelope(b'')
1157 1158 # fo matching path == no diff
1158 return ''
1159 return diff_obj.patch or ''
1159 return BinaryEnvelope(b'')
1160 return BinaryEnvelope(diff_obj.patch) or BinaryEnvelope(b'')
1160 1161
1161 1162 @reraise_safe_exceptions
1162 1163 def node_history(self, wire, commit_id, path, limit):
1163 1164 cache_on, context_uid, repo_id = self._cache_on(wire)
1164 1165 region = self._region(wire)
1165 1166
1166 1167 @region.conditional_cache_on_arguments(condition=cache_on)
1167 1168 def _node_history(_context_uid, _repo_id, _commit_id, _path, _limit):
1168 1169 # optimize for n==1, rev-list is much faster for that use-case
1169 1170 if limit == 1:
1170 1171 cmd = ['rev-list', '-1', commit_id, '--', path]
1171 1172 else:
1172 1173 cmd = ['log']
1173 1174 if limit:
1174 1175 cmd.extend(['-n', str(safe_int(limit, 0))])
1175 1176 cmd.extend(['--pretty=format: %H', '-s', commit_id, '--', path])
1176 1177
1177 1178 output, __ = self.run_git_command(wire, cmd)
1178 1179 commit_ids = re.findall(rb'[0-9a-fA-F]{40}', output)
1179 1180
1180 1181 return [x for x in commit_ids]
1181 1182 return _node_history(context_uid, repo_id, commit_id, path, limit)
1182 1183
1183 1184 @reraise_safe_exceptions
1184 1185 def node_annotate_legacy(self, wire, commit_id, path):
1185 1186 #note: replaced by pygit2 impelementation
1186 1187 cmd = ['blame', '-l', '--root', '-r', commit_id, '--', path]
1187 1188 # -l ==> outputs long shas (and we need all 40 characters)
1188 1189 # --root ==> doesn't put '^' character for boundaries
1189 1190 # -r commit_id ==> blames for the given commit
1190 1191 output, __ = self.run_git_command(wire, cmd)
1191 1192
1192 1193 result = []
1193 1194 for i, blame_line in enumerate(output.splitlines()[:-1]):
1194 1195 line_no = i + 1
1195 1196 blame_commit_id, line = re.split(rb' ', blame_line, 1)
1196 1197 result.append((line_no, blame_commit_id, line))
1197 1198
1198 1199 return result
1199 1200
1200 1201 @reraise_safe_exceptions
1201 1202 def node_annotate(self, wire, commit_id, path):
1202 1203
1203 1204 result_libgit = []
1204 1205 repo_init = self._factory.repo_libgit2(wire)
1205 1206 with repo_init as repo:
1206 1207 commit = repo[commit_id]
1207 1208 blame_obj = repo.blame(path, newest_commit=commit_id)
1208 1209 for i, line in enumerate(commit.tree[path].data.splitlines()):
1209 1210 line_no = i + 1
1210 1211 hunk = blame_obj.for_line(line_no)
1211 1212 blame_commit_id = hunk.final_commit_id.hex
1212 1213
1213 1214 result_libgit.append((line_no, blame_commit_id, line))
1214 1215
1215 1216 return result_libgit
1216 1217
1217 1218 @reraise_safe_exceptions
1218 1219 def update_server_info(self, wire):
1219 1220 repo = self._factory.repo(wire)
1220 1221 update_server_info(repo)
1221 1222
1222 1223 @reraise_safe_exceptions
1223 1224 def get_all_commit_ids(self, wire):
1224 1225
1225 1226 cache_on, context_uid, repo_id = self._cache_on(wire)
1226 1227 region = self._region(wire)
1227 1228
1228 1229 @region.conditional_cache_on_arguments(condition=cache_on)
1229 1230 def _get_all_commit_ids(_context_uid, _repo_id):
1230 1231
1231 1232 cmd = ['rev-list', '--reverse', '--date-order', '--branches', '--tags']
1232 1233 try:
1233 1234 output, __ = self.run_git_command(wire, cmd)
1234 1235 return output.splitlines()
1235 1236 except Exception:
1236 1237 # Can be raised for empty repositories
1237 1238 return []
1238 1239
1239 1240 @region.conditional_cache_on_arguments(condition=cache_on)
1240 1241 def _get_all_commit_ids_pygit2(_context_uid, _repo_id):
1241 1242 repo_init = self._factory.repo_libgit2(wire)
1242 1243 from pygit2 import GIT_SORT_REVERSE, GIT_SORT_TIME, GIT_BRANCH_ALL
1243 1244 results = []
1244 1245 with repo_init as repo:
1245 1246 for commit in repo.walk(repo.head.target, GIT_SORT_TIME | GIT_BRANCH_ALL | GIT_SORT_REVERSE):
1246 1247 results.append(commit.id.hex)
1247 1248
1248 1249 return _get_all_commit_ids(context_uid, repo_id)
1249 1250
1250 1251 @reraise_safe_exceptions
1251 1252 def run_git_command(self, wire, cmd, **opts):
1252 1253 path = wire.get('path', None)
1253 1254
1254 1255 if path and os.path.isdir(path):
1255 1256 opts['cwd'] = path
1256 1257
1257 1258 if '_bare' in opts:
1258 1259 _copts = []
1259 1260 del opts['_bare']
1260 1261 else:
1261 1262 _copts = ['-c', 'core.quotepath=false', ]
1262 1263 safe_call = False
1263 1264 if '_safe' in opts:
1264 1265 # no exc on failure
1265 1266 del opts['_safe']
1266 1267 safe_call = True
1267 1268
1268 1269 if '_copts' in opts:
1269 1270 _copts.extend(opts['_copts'] or [])
1270 1271 del opts['_copts']
1271 1272
1272 1273 gitenv = os.environ.copy()
1273 1274 gitenv.update(opts.pop('extra_env', {}))
1274 1275 # need to clean fix GIT_DIR !
1275 1276 if 'GIT_DIR' in gitenv:
1276 1277 del gitenv['GIT_DIR']
1277 1278 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
1278 1279 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
1279 1280
1280 1281 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
1281 1282 _opts = {'env': gitenv, 'shell': False}
1282 1283
1283 1284 proc = None
1284 1285 try:
1285 1286 _opts.update(opts)
1286 1287 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
1287 1288
1288 1289 return b''.join(proc), b''.join(proc.stderr)
1289 1290 except OSError as err:
1290 1291 cmd = ' '.join(map(safe_str, cmd)) # human friendly CMD
1291 1292 tb_err = ("Couldn't run git command (%s).\n"
1292 1293 "Original error was:%s\n"
1293 1294 "Call options:%s\n"
1294 1295 % (cmd, err, _opts))
1295 1296 log.exception(tb_err)
1296 1297 if safe_call:
1297 1298 return '', err
1298 1299 else:
1299 1300 raise exceptions.VcsException()(tb_err)
1300 1301 finally:
1301 1302 if proc:
1302 1303 proc.close()
1303 1304
1304 1305 @reraise_safe_exceptions
1305 1306 def install_hooks(self, wire, force=False):
1306 1307 from vcsserver.hook_utils import install_git_hooks
1307 1308 bare = self.bare(wire)
1308 1309 path = wire['path']
1309 1310 binary_dir = settings.BINARY_DIR
1310 1311 executable = None
1311 1312 if binary_dir:
1312 1313 executable = os.path.join(binary_dir, 'python3')
1313 1314 return install_git_hooks(path, bare, force_create=force)
1314 1315
1315 1316 @reraise_safe_exceptions
1316 1317 def get_hooks_info(self, wire):
1317 1318 from vcsserver.hook_utils import (
1318 1319 get_git_pre_hook_version, get_git_post_hook_version)
1319 1320 bare = self.bare(wire)
1320 1321 path = wire['path']
1321 1322 return {
1322 1323 'pre_version': get_git_pre_hook_version(path, bare),
1323 1324 'post_version': get_git_post_hook_version(path, bare),
1324 1325 }
1325 1326
1326 1327 @reraise_safe_exceptions
1327 1328 def set_head_ref(self, wire, head_name):
1328 1329 log.debug('Setting refs/head to `%s`', head_name)
1329 1330 cmd = ['symbolic-ref', '"HEAD"', '"refs/heads/%s"' % head_name]
1330 1331 output, __ = self.run_git_command(wire, cmd)
1331 1332 return [head_name] + output.splitlines()
1332 1333
1333 1334 @reraise_safe_exceptions
1334 1335 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
1335 1336 archive_dir_name, commit_id):
1336 1337
1337 1338 def file_walker(_commit_id, path):
1338 1339 repo_init = self._factory.repo_libgit2(wire)
1339 1340
1340 1341 with repo_init as repo:
1341 1342 commit = repo[commit_id]
1342 1343
1343 1344 if path in ['', '/']:
1344 1345 tree = commit.tree
1345 1346 else:
1346 1347 tree = commit.tree[path.rstrip('/')]
1347 1348 tree_id = tree.id.hex
1348 1349 try:
1349 1350 tree = repo[tree_id]
1350 1351 except KeyError:
1351 1352 raise ObjectMissing('No tree with id: {}'.format(tree_id))
1352 1353
1353 1354 index = LibGit2Index.Index()
1354 1355 index.read_tree(tree)
1355 1356 file_iter = index
1356 1357
1357 1358 for fn in file_iter:
1358 1359 file_path = fn.path
1359 1360 mode = fn.mode
1360 1361 is_link = stat.S_ISLNK(mode)
1361 1362 if mode == pygit2.GIT_FILEMODE_COMMIT:
1362 1363 log.debug('Skipping path %s as a commit node', file_path)
1363 1364 continue
1364 1365 yield ArchiveNode(file_path, mode, is_link, repo[fn.hex].read_raw)
1365 1366
1366 1367 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
1367 1368 archive_dir_name, commit_id)
@@ -1,1103 +1,1103 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2020 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 urllib.request
22 22 import urllib.parse
23 23 import traceback
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
33 33 import vcsserver
34 34 from vcsserver import exceptions
35 from vcsserver.base import RepoFactory, obfuscate_qs, raise_from_original, archive_repo, ArchiveNode
35 from vcsserver.base import RepoFactory, obfuscate_qs, raise_from_original, archive_repo, ArchiveNode, BinaryEnvelope
36 36 from vcsserver.hgcompat import (
37 37 archival, bin, clone, config as hgconfig, diffopts, hex, get_ctx,
38 38 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler,
39 39 makepeer, instance, match, memctx, exchange, memfilectx, nullrev, hg_merge,
40 40 patch, peer, revrange, ui, hg_tag, Abort, LookupError, RepoError,
41 41 RepoLookupError, InterventionRequired, RequirementError,
42 42 alwaysmatcher, patternmatcher, hgutil, hgext_strip)
43 43 from vcsserver.str_utils import ascii_bytes, ascii_str, safe_str, safe_bytes
44 44 from vcsserver.vcs_base import RemoteBase
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 def make_ui_from_config(repo_config):
50 50
51 51 class LoggingUI(ui.ui):
52 52
53 53 def status(self, *msg, **opts):
54 54 str_msg = map(safe_str, msg)
55 55 log.info(' '.join(str_msg).rstrip('\n'))
56 56 #super(LoggingUI, self).status(*msg, **opts)
57 57
58 58 def warn(self, *msg, **opts):
59 59 str_msg = map(safe_str, msg)
60 60 log.warning('ui_logger:'+' '.join(str_msg).rstrip('\n'))
61 61 #super(LoggingUI, self).warn(*msg, **opts)
62 62
63 63 def error(self, *msg, **opts):
64 64 str_msg = map(safe_str, msg)
65 65 log.error('ui_logger:'+' '.join(str_msg).rstrip('\n'))
66 66 #super(LoggingUI, self).error(*msg, **opts)
67 67
68 68 def note(self, *msg, **opts):
69 69 str_msg = map(safe_str, msg)
70 70 log.info('ui_logger:'+' '.join(str_msg).rstrip('\n'))
71 71 #super(LoggingUI, self).note(*msg, **opts)
72 72
73 73 def debug(self, *msg, **opts):
74 74 str_msg = map(safe_str, msg)
75 75 log.debug('ui_logger:'+' '.join(str_msg).rstrip('\n'))
76 76 #super(LoggingUI, self).debug(*msg, **opts)
77 77
78 78 baseui = LoggingUI()
79 79
80 80 # clean the baseui object
81 81 baseui._ocfg = hgconfig.config()
82 82 baseui._ucfg = hgconfig.config()
83 83 baseui._tcfg = hgconfig.config()
84 84
85 85 for section, option, value in repo_config:
86 86 baseui.setconfig(ascii_bytes(section), ascii_bytes(option), ascii_bytes(value))
87 87
88 88 # make our hgweb quiet so it doesn't print output
89 89 baseui.setconfig(b'ui', b'quiet', b'true')
90 90
91 91 baseui.setconfig(b'ui', b'paginate', b'never')
92 92 # for better Error reporting of Mercurial
93 93 baseui.setconfig(b'ui', b'message-output', b'stderr')
94 94
95 95 # force mercurial to only use 1 thread, otherwise it may try to set a
96 96 # signal in a non-main thread, thus generating a ValueError.
97 97 baseui.setconfig(b'worker', b'numcpus', 1)
98 98
99 99 # If there is no config for the largefiles extension, we explicitly disable
100 100 # it here. This overrides settings from repositories hgrc file. Recent
101 101 # mercurial versions enable largefiles in hgrc on clone from largefile
102 102 # repo.
103 103 if not baseui.hasconfig(b'extensions', b'largefiles'):
104 104 log.debug('Explicitly disable largefiles extension for repo.')
105 105 baseui.setconfig(b'extensions', b'largefiles', b'!')
106 106
107 107 return baseui
108 108
109 109
110 110 def reraise_safe_exceptions(func):
111 111 """Decorator for converting mercurial exceptions to something neutral."""
112 112
113 113 def wrapper(*args, **kwargs):
114 114 try:
115 115 return func(*args, **kwargs)
116 116 except (Abort, InterventionRequired) as e:
117 117 raise_from_original(exceptions.AbortException(e), e)
118 118 except RepoLookupError as e:
119 119 raise_from_original(exceptions.LookupException(e), e)
120 120 except RequirementError as e:
121 121 raise_from_original(exceptions.RequirementException(e), e)
122 122 except RepoError as e:
123 123 raise_from_original(exceptions.VcsException(e), e)
124 124 except LookupError as e:
125 125 raise_from_original(exceptions.LookupException(e), e)
126 126 except Exception as e:
127 127 if not hasattr(e, '_vcs_kind'):
128 128 log.exception("Unhandled exception in hg remote call")
129 129 raise_from_original(exceptions.UnhandledException(e), e)
130 130
131 131 raise
132 132 return wrapper
133 133
134 134
135 135 class MercurialFactory(RepoFactory):
136 136 repo_type = 'hg'
137 137
138 138 def _create_config(self, config, hooks=True):
139 139 if not hooks:
140 140 hooks_to_clean = frozenset((
141 141 'changegroup.repo_size', 'preoutgoing.pre_pull',
142 142 'outgoing.pull_logger', 'prechangegroup.pre_push'))
143 143 new_config = []
144 144 for section, option, value in config:
145 145 if section == 'hooks' and option in hooks_to_clean:
146 146 continue
147 147 new_config.append((section, option, value))
148 148 config = new_config
149 149
150 150 baseui = make_ui_from_config(config)
151 151 return baseui
152 152
153 153 def _create_repo(self, wire, create):
154 154 baseui = self._create_config(wire["config"])
155 155 return instance(baseui, safe_bytes(wire["path"]), create)
156 156
157 157 def repo(self, wire, create=False):
158 158 """
159 159 Get a repository instance for the given path.
160 160 """
161 161 return self._create_repo(wire, create)
162 162
163 163
164 164 def patch_ui_message_output(baseui):
165 165 baseui.setconfig(b'ui', b'quiet', b'false')
166 166 output = io.BytesIO()
167 167
168 168 def write(data, **unused_kwargs):
169 169 output.write(data)
170 170
171 171 baseui.status = write
172 172 baseui.write = write
173 173 baseui.warn = write
174 174 baseui.debug = write
175 175
176 176 return baseui, output
177 177
178 178
179 179 class HgRemote(RemoteBase):
180 180
181 181 def __init__(self, factory):
182 182 self._factory = factory
183 183 self._bulk_methods = {
184 184 "affected_files": self.ctx_files,
185 185 "author": self.ctx_user,
186 186 "branch": self.ctx_branch,
187 187 "children": self.ctx_children,
188 188 "date": self.ctx_date,
189 189 "message": self.ctx_description,
190 190 "parents": self.ctx_parents,
191 191 "status": self.ctx_status,
192 192 "obsolete": self.ctx_obsolete,
193 193 "phase": self.ctx_phase,
194 194 "hidden": self.ctx_hidden,
195 195 "_file_paths": self.ctx_list,
196 196 }
197 197
198 198 def _get_ctx(self, repo, ref):
199 199 return get_ctx(repo, ref)
200 200
201 201 @reraise_safe_exceptions
202 202 def discover_hg_version(self):
203 203 from mercurial import util
204 204 return safe_str(util.version())
205 205
206 206 @reraise_safe_exceptions
207 207 def is_empty(self, wire):
208 208 repo = self._factory.repo(wire)
209 209
210 210 try:
211 211 return len(repo) == 0
212 212 except Exception:
213 213 log.exception("failed to read object_store")
214 214 return False
215 215
216 216 @reraise_safe_exceptions
217 217 def bookmarks(self, wire):
218 218 cache_on, context_uid, repo_id = self._cache_on(wire)
219 219 region = self._region(wire)
220 220
221 221 @region.conditional_cache_on_arguments(condition=cache_on)
222 222 def _bookmarks(_context_uid, _repo_id):
223 223 repo = self._factory.repo(wire)
224 224 return {safe_str(name): ascii_str(hex(sha)) for name, sha in repo._bookmarks.items()}
225 225
226 226 return _bookmarks(context_uid, repo_id)
227 227
228 228 @reraise_safe_exceptions
229 229 def branches(self, wire, normal, closed):
230 230 cache_on, context_uid, repo_id = self._cache_on(wire)
231 231 region = self._region(wire)
232 232
233 233 @region.conditional_cache_on_arguments(condition=cache_on)
234 234 def _branches(_context_uid, _repo_id, _normal, _closed):
235 235 repo = self._factory.repo(wire)
236 236 iter_branches = repo.branchmap().iterbranches()
237 237 bt = {}
238 238 for branch_name, _heads, tip_node, is_closed in iter_branches:
239 239 if normal and not is_closed:
240 240 bt[safe_str(branch_name)] = ascii_str(hex(tip_node))
241 241 if closed and is_closed:
242 242 bt[safe_str(branch_name)] = ascii_str(hex(tip_node))
243 243
244 244 return bt
245 245
246 246 return _branches(context_uid, repo_id, normal, closed)
247 247
248 248 @reraise_safe_exceptions
249 249 def bulk_request(self, wire, commit_id, pre_load):
250 250 cache_on, context_uid, repo_id = self._cache_on(wire)
251 251 region = self._region(wire)
252 252
253 253 @region.conditional_cache_on_arguments(condition=cache_on)
254 254 def _bulk_request(_repo_id, _commit_id, _pre_load):
255 255 result = {}
256 256 for attr in pre_load:
257 257 try:
258 258 method = self._bulk_methods[attr]
259 259 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
260 260 result[attr] = method(wire, commit_id)
261 261 except KeyError as e:
262 262 raise exceptions.VcsException(e)(
263 263 'Unknown bulk attribute: "%s"' % attr)
264 264 return result
265 265
266 266 return _bulk_request(repo_id, commit_id, sorted(pre_load))
267 267
268 268 @reraise_safe_exceptions
269 269 def ctx_branch(self, wire, commit_id):
270 270 cache_on, context_uid, repo_id = self._cache_on(wire)
271 271 region = self._region(wire)
272 272
273 273 @region.conditional_cache_on_arguments(condition=cache_on)
274 274 def _ctx_branch(_repo_id, _commit_id):
275 275 repo = self._factory.repo(wire)
276 276 ctx = self._get_ctx(repo, commit_id)
277 277 return ctx.branch()
278 278 return _ctx_branch(repo_id, commit_id)
279 279
280 280 @reraise_safe_exceptions
281 281 def ctx_date(self, wire, commit_id):
282 282 cache_on, context_uid, repo_id = self._cache_on(wire)
283 283 region = self._region(wire)
284 284
285 285 @region.conditional_cache_on_arguments(condition=cache_on)
286 286 def _ctx_date(_repo_id, _commit_id):
287 287 repo = self._factory.repo(wire)
288 288 ctx = self._get_ctx(repo, commit_id)
289 289 return ctx.date()
290 290 return _ctx_date(repo_id, commit_id)
291 291
292 292 @reraise_safe_exceptions
293 293 def ctx_description(self, wire, revision):
294 294 repo = self._factory.repo(wire)
295 295 ctx = self._get_ctx(repo, revision)
296 296 return ctx.description()
297 297
298 298 @reraise_safe_exceptions
299 299 def ctx_files(self, wire, commit_id):
300 300 cache_on, context_uid, repo_id = self._cache_on(wire)
301 301 region = self._region(wire)
302 302
303 303 @region.conditional_cache_on_arguments(condition=cache_on)
304 304 def _ctx_files(_repo_id, _commit_id):
305 305 repo = self._factory.repo(wire)
306 306 ctx = self._get_ctx(repo, commit_id)
307 307 return ctx.files()
308 308
309 309 return _ctx_files(repo_id, commit_id)
310 310
311 311 @reraise_safe_exceptions
312 312 def ctx_list(self, path, revision):
313 313 repo = self._factory.repo(path)
314 314 ctx = self._get_ctx(repo, revision)
315 315 return list(ctx)
316 316
317 317 @reraise_safe_exceptions
318 318 def ctx_parents(self, wire, commit_id):
319 319 cache_on, context_uid, repo_id = self._cache_on(wire)
320 320 region = self._region(wire)
321 321
322 322 @region.conditional_cache_on_arguments(condition=cache_on)
323 323 def _ctx_parents(_repo_id, _commit_id):
324 324 repo = self._factory.repo(wire)
325 325 ctx = self._get_ctx(repo, commit_id)
326 326 return [parent.hex() for parent in ctx.parents()
327 327 if not (parent.hidden() or parent.obsolete())]
328 328
329 329 return _ctx_parents(repo_id, commit_id)
330 330
331 331 @reraise_safe_exceptions
332 332 def ctx_children(self, wire, commit_id):
333 333 cache_on, context_uid, repo_id = self._cache_on(wire)
334 334 region = self._region(wire)
335 335
336 336 @region.conditional_cache_on_arguments(condition=cache_on)
337 337 def _ctx_children(_repo_id, _commit_id):
338 338 repo = self._factory.repo(wire)
339 339 ctx = self._get_ctx(repo, commit_id)
340 340 return [child.hex() for child in ctx.children()
341 341 if not (child.hidden() or child.obsolete())]
342 342
343 343 return _ctx_children(repo_id, commit_id)
344 344
345 345 @reraise_safe_exceptions
346 346 def ctx_phase(self, wire, commit_id):
347 347 cache_on, context_uid, repo_id = self._cache_on(wire)
348 348 region = self._region(wire)
349 349
350 350 @region.conditional_cache_on_arguments(condition=cache_on)
351 351 def _ctx_phase(_context_uid, _repo_id, _commit_id):
352 352 repo = self._factory.repo(wire)
353 353 ctx = self._get_ctx(repo, commit_id)
354 354 # public=0, draft=1, secret=3
355 355 return ctx.phase()
356 356 return _ctx_phase(context_uid, repo_id, commit_id)
357 357
358 358 @reraise_safe_exceptions
359 359 def ctx_obsolete(self, wire, commit_id):
360 360 cache_on, context_uid, repo_id = self._cache_on(wire)
361 361 region = self._region(wire)
362 362
363 363 @region.conditional_cache_on_arguments(condition=cache_on)
364 364 def _ctx_obsolete(_context_uid, _repo_id, _commit_id):
365 365 repo = self._factory.repo(wire)
366 366 ctx = self._get_ctx(repo, commit_id)
367 367 return ctx.obsolete()
368 368 return _ctx_obsolete(context_uid, repo_id, commit_id)
369 369
370 370 @reraise_safe_exceptions
371 371 def ctx_hidden(self, wire, commit_id):
372 372 cache_on, context_uid, repo_id = self._cache_on(wire)
373 373 region = self._region(wire)
374 374
375 375 @region.conditional_cache_on_arguments(condition=cache_on)
376 376 def _ctx_hidden(_context_uid, _repo_id, _commit_id):
377 377 repo = self._factory.repo(wire)
378 378 ctx = self._get_ctx(repo, commit_id)
379 379 return ctx.hidden()
380 380 return _ctx_hidden(context_uid, repo_id, commit_id)
381 381
382 382 @reraise_safe_exceptions
383 383 def ctx_substate(self, wire, revision):
384 384 repo = self._factory.repo(wire)
385 385 ctx = self._get_ctx(repo, revision)
386 386 return ctx.substate
387 387
388 388 @reraise_safe_exceptions
389 389 def ctx_status(self, wire, revision):
390 390 repo = self._factory.repo(wire)
391 391 ctx = self._get_ctx(repo, revision)
392 392 status = repo[ctx.p1().node()].status(other=ctx.node())
393 393 # object of status (odd, custom named tuple in mercurial) is not
394 394 # correctly serializable, we make it a list, as the underling
395 395 # API expects this to be a list
396 396 return list(status)
397 397
398 398 @reraise_safe_exceptions
399 399 def ctx_user(self, wire, revision):
400 400 repo = self._factory.repo(wire)
401 401 ctx = self._get_ctx(repo, revision)
402 402 return ctx.user()
403 403
404 404 @reraise_safe_exceptions
405 405 def check_url(self, url, config):
406 406 _proto = None
407 407 if '+' in url[:url.find('://')]:
408 408 _proto = url[0:url.find('+')]
409 409 url = url[url.find('+') + 1:]
410 410 handlers = []
411 411 url_obj = url_parser(url)
412 412 test_uri, authinfo = url_obj.authinfo()
413 413 url_obj.passwd = '*****' if url_obj.passwd else url_obj.passwd
414 414 url_obj.query = obfuscate_qs(url_obj.query)
415 415
416 416 cleaned_uri = str(url_obj)
417 417 log.info("Checking URL for remote cloning/import: %s", cleaned_uri)
418 418
419 419 if authinfo:
420 420 # create a password manager
421 421 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
422 422 passmgr.add_password(*authinfo)
423 423
424 424 handlers.extend((httpbasicauthhandler(passmgr),
425 425 httpdigestauthhandler(passmgr)))
426 426
427 427 o = urllib.request.build_opener(*handlers)
428 428 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
429 429 ('Accept', 'application/mercurial-0.1')]
430 430
431 431 q = {"cmd": 'between'}
432 432 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
433 433 qs = '?%s' % urllib.parse.urlencode(q)
434 434 cu = "%s%s" % (test_uri, qs)
435 435 req = urllib.request.Request(cu, None, {})
436 436
437 437 try:
438 438 log.debug("Trying to open URL %s", cleaned_uri)
439 439 resp = o.open(req)
440 440 if resp.code != 200:
441 441 raise exceptions.URLError()('Return Code is not 200')
442 442 except Exception as e:
443 443 log.warning("URL cannot be opened: %s", cleaned_uri, exc_info=True)
444 444 # means it cannot be cloned
445 445 raise exceptions.URLError(e)("[%s] org_exc: %s" % (cleaned_uri, e))
446 446
447 447 # now check if it's a proper hg repo, but don't do it for svn
448 448 try:
449 449 if _proto == 'svn':
450 450 pass
451 451 else:
452 452 # check for pure hg repos
453 453 log.debug(
454 454 "Verifying if URL is a Mercurial repository: %s",
455 455 cleaned_uri)
456 456 ui = make_ui_from_config(config)
457 457 peer_checker = makepeer(ui, url)
458 458 peer_checker.lookup('tip')
459 459 except Exception as e:
460 460 log.warning("URL is not a valid Mercurial repository: %s",
461 461 cleaned_uri)
462 462 raise exceptions.URLError(e)(
463 463 "url [%s] does not look like an hg repo org_exc: %s"
464 464 % (cleaned_uri, e))
465 465
466 466 log.info("URL is a valid Mercurial repository: %s", cleaned_uri)
467 467 return True
468 468
469 469 @reraise_safe_exceptions
470 470 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_git, opt_ignorews, context):
471 471 repo = self._factory.repo(wire)
472 472
473 473 if file_filter:
474 474 # unpack the file-filter
475 475 repo_path, node_path = file_filter
476 476 match_filter = match(safe_bytes(repo_path), b'', [safe_bytes(node_path)])
477 477 else:
478 478 match_filter = file_filter
479 479 opts = diffopts(git=opt_git, ignorews=opt_ignorews, context=context, showfunc=1)
480 480
481 481 try:
482 482 diff_iter = patch.diff(
483 483 repo, node1=commit_id_1, node2=commit_id_2, match=match_filter, opts=opts)
484 return b"".join(diff_iter)
484 return BinaryEnvelope(b"".join(diff_iter))
485 485 except RepoLookupError as e:
486 486 raise exceptions.LookupException(e)()
487 487
488 488 @reraise_safe_exceptions
489 489 def node_history(self, wire, revision, path, limit):
490 490 cache_on, context_uid, repo_id = self._cache_on(wire)
491 491 region = self._region(wire)
492 492
493 493 @region.conditional_cache_on_arguments(condition=cache_on)
494 494 def _node_history(_context_uid, _repo_id, _revision, _path, _limit):
495 495 repo = self._factory.repo(wire)
496 496
497 497 ctx = self._get_ctx(repo, revision)
498 498 fctx = ctx.filectx(safe_bytes(path))
499 499
500 500 def history_iter():
501 501 limit_rev = fctx.rev()
502 502 for obj in reversed(list(fctx.filelog())):
503 503 obj = fctx.filectx(obj)
504 504 ctx = obj.changectx()
505 505 if ctx.hidden() or ctx.obsolete():
506 506 continue
507 507
508 508 if limit_rev >= obj.rev():
509 509 yield obj
510 510
511 511 history = []
512 512 for cnt, obj in enumerate(history_iter()):
513 513 if limit and cnt >= limit:
514 514 break
515 515 history.append(hex(obj.node()))
516 516
517 517 return [x for x in history]
518 518 return _node_history(context_uid, repo_id, revision, path, limit)
519 519
520 520 @reraise_safe_exceptions
521 521 def node_history_untill(self, wire, revision, path, limit):
522 522 cache_on, context_uid, repo_id = self._cache_on(wire)
523 523 region = self._region(wire)
524 524
525 525 @region.conditional_cache_on_arguments(condition=cache_on)
526 526 def _node_history_until(_context_uid, _repo_id):
527 527 repo = self._factory.repo(wire)
528 528 ctx = self._get_ctx(repo, revision)
529 529 fctx = ctx.filectx(safe_bytes(path))
530 530
531 531 file_log = list(fctx.filelog())
532 532 if limit:
533 533 # Limit to the last n items
534 534 file_log = file_log[-limit:]
535 535
536 536 return [hex(fctx.filectx(cs).node()) for cs in reversed(file_log)]
537 537 return _node_history_until(context_uid, repo_id, revision, path, limit)
538 538
539 539 @reraise_safe_exceptions
540 540 def fctx_annotate(self, wire, revision, path):
541 541 repo = self._factory.repo(wire)
542 542 ctx = self._get_ctx(repo, revision)
543 543 fctx = ctx.filectx(safe_bytes(path))
544 544
545 545 result = []
546 546 for i, annotate_obj in enumerate(fctx.annotate(), 1):
547 547 ln_no = i
548 548 sha = hex(annotate_obj.fctx.node())
549 549 content = annotate_obj.text
550 550 result.append((ln_no, sha, content))
551 551 return result
552 552
553 553 @reraise_safe_exceptions
554 554 def fctx_node_data(self, wire, revision, path):
555 555 repo = self._factory.repo(wire)
556 556 ctx = self._get_ctx(repo, revision)
557 557 fctx = ctx.filectx(safe_bytes(path))
558 return fctx.data()
558 return BinaryEnvelope(fctx.data())
559 559
560 560 @reraise_safe_exceptions
561 561 def fctx_flags(self, wire, commit_id, path):
562 562 cache_on, context_uid, repo_id = self._cache_on(wire)
563 563 region = self._region(wire)
564 564
565 565 @region.conditional_cache_on_arguments(condition=cache_on)
566 566 def _fctx_flags(_repo_id, _commit_id, _path):
567 567 repo = self._factory.repo(wire)
568 568 ctx = self._get_ctx(repo, commit_id)
569 569 fctx = ctx.filectx(safe_bytes(path))
570 570 return fctx.flags()
571 571
572 572 return _fctx_flags(repo_id, commit_id, path)
573 573
574 574 @reraise_safe_exceptions
575 575 def fctx_size(self, wire, commit_id, path):
576 576 cache_on, context_uid, repo_id = self._cache_on(wire)
577 577 region = self._region(wire)
578 578
579 579 @region.conditional_cache_on_arguments(condition=cache_on)
580 580 def _fctx_size(_repo_id, _revision, _path):
581 581 repo = self._factory.repo(wire)
582 582 ctx = self._get_ctx(repo, commit_id)
583 583 fctx = ctx.filectx(safe_bytes(path))
584 584 return fctx.size()
585 585 return _fctx_size(repo_id, commit_id, path)
586 586
587 587 @reraise_safe_exceptions
588 588 def get_all_commit_ids(self, wire, name):
589 589 cache_on, context_uid, repo_id = self._cache_on(wire)
590 590 region = self._region(wire)
591 591
592 592 @region.conditional_cache_on_arguments(condition=cache_on)
593 593 def _get_all_commit_ids(_context_uid, _repo_id, _name):
594 594 repo = self._factory.repo(wire)
595 595 revs = [ascii_str(repo[x].hex()) for x in repo.filtered(b'visible').changelog.revs()]
596 596 return revs
597 597 return _get_all_commit_ids(context_uid, repo_id, name)
598 598
599 599 @reraise_safe_exceptions
600 600 def get_config_value(self, wire, section, name, untrusted=False):
601 601 repo = self._factory.repo(wire)
602 602 return repo.ui.config(ascii_bytes(section), ascii_bytes(name), untrusted=untrusted)
603 603
604 604 @reraise_safe_exceptions
605 605 def is_large_file(self, wire, commit_id, path):
606 606 cache_on, context_uid, repo_id = self._cache_on(wire)
607 607 region = self._region(wire)
608 608
609 609 @region.conditional_cache_on_arguments(condition=cache_on)
610 610 def _is_large_file(_context_uid, _repo_id, _commit_id, _path):
611 611 return largefiles.lfutil.isstandin(safe_bytes(path))
612 612
613 613 return _is_large_file(context_uid, repo_id, commit_id, path)
614 614
615 615 @reraise_safe_exceptions
616 616 def is_binary(self, wire, revision, path):
617 617 cache_on, context_uid, repo_id = self._cache_on(wire)
618 618 region = self._region(wire)
619 619
620 620 @region.conditional_cache_on_arguments(condition=cache_on)
621 621 def _is_binary(_repo_id, _sha, _path):
622 622 repo = self._factory.repo(wire)
623 623 ctx = self._get_ctx(repo, revision)
624 624 fctx = ctx.filectx(safe_bytes(path))
625 625 return fctx.isbinary()
626 626
627 627 return _is_binary(repo_id, revision, path)
628 628
629 629 @reraise_safe_exceptions
630 630 def md5_hash(self, wire, revision, path):
631 631 cache_on, context_uid, repo_id = self._cache_on(wire)
632 632 region = self._region(wire)
633 633
634 634 @region.conditional_cache_on_arguments(condition=cache_on)
635 635 def _md5_hash(_repo_id, _sha, _path):
636 636 repo = self._factory.repo(wire)
637 637 ctx = self._get_ctx(repo, revision)
638 638 fctx = ctx.filectx(safe_bytes(path))
639 639 return hashlib.md5(fctx.data()).hexdigest()
640 640
641 641 return _md5_hash(repo_id, revision, path)
642 642
643 643 @reraise_safe_exceptions
644 644 def in_largefiles_store(self, wire, sha):
645 645 repo = self._factory.repo(wire)
646 646 return largefiles.lfutil.instore(repo, sha)
647 647
648 648 @reraise_safe_exceptions
649 649 def in_user_cache(self, wire, sha):
650 650 repo = self._factory.repo(wire)
651 651 return largefiles.lfutil.inusercache(repo.ui, sha)
652 652
653 653 @reraise_safe_exceptions
654 654 def store_path(self, wire, sha):
655 655 repo = self._factory.repo(wire)
656 656 return largefiles.lfutil.storepath(repo, sha)
657 657
658 658 @reraise_safe_exceptions
659 659 def link(self, wire, sha, path):
660 660 repo = self._factory.repo(wire)
661 661 largefiles.lfutil.link(
662 662 largefiles.lfutil.usercachepath(repo.ui, sha), path)
663 663
664 664 @reraise_safe_exceptions
665 665 def localrepository(self, wire, create=False):
666 666 self._factory.repo(wire, create=create)
667 667
668 668 @reraise_safe_exceptions
669 669 def lookup(self, wire, revision, both):
670 670 cache_on, context_uid, repo_id = self._cache_on(wire)
671 671 region = self._region(wire)
672 672
673 673 @region.conditional_cache_on_arguments(condition=cache_on)
674 674 def _lookup(_context_uid, _repo_id, _revision, _both):
675 675
676 676 repo = self._factory.repo(wire)
677 677 rev = _revision
678 678 if isinstance(rev, int):
679 679 # NOTE(marcink):
680 680 # since Mercurial doesn't support negative indexes properly
681 681 # we need to shift accordingly by one to get proper index, e.g
682 682 # repo[-1] => repo[-2]
683 683 # repo[0] => repo[-1]
684 684 if rev <= 0:
685 685 rev = rev + -1
686 686 try:
687 687 ctx = self._get_ctx(repo, rev)
688 688 except (TypeError, RepoLookupError, binascii.Error) as e:
689 689 e._org_exc_tb = traceback.format_exc()
690 690 raise exceptions.LookupException(e)(rev)
691 691 except LookupError as e:
692 692 e._org_exc_tb = traceback.format_exc()
693 693 raise exceptions.LookupException(e)(e.name)
694 694
695 695 if not both:
696 696 return ctx.hex()
697 697
698 698 ctx = repo[ctx.hex()]
699 699 return ctx.hex(), ctx.rev()
700 700
701 701 return _lookup(context_uid, repo_id, revision, both)
702 702
703 703 @reraise_safe_exceptions
704 704 def sync_push(self, wire, url):
705 705 if not self.check_url(url, wire['config']):
706 706 return
707 707
708 708 repo = self._factory.repo(wire)
709 709
710 710 # Disable any prompts for this repo
711 711 repo.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
712 712
713 713 bookmarks = list(dict(repo._bookmarks).keys())
714 714 remote = peer(repo, {}, safe_bytes(url))
715 715 # Disable any prompts for this remote
716 716 remote.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
717 717
718 718 return exchange.push(
719 719 repo, remote, newbranch=True, bookmarks=bookmarks).cgresult
720 720
721 721 @reraise_safe_exceptions
722 722 def revision(self, wire, rev):
723 723 repo = self._factory.repo(wire)
724 724 ctx = self._get_ctx(repo, rev)
725 725 return ctx.rev()
726 726
727 727 @reraise_safe_exceptions
728 728 def rev_range(self, wire, commit_filter):
729 729 cache_on, context_uid, repo_id = self._cache_on(wire)
730 730 region = self._region(wire)
731 731
732 732 @region.conditional_cache_on_arguments(condition=cache_on)
733 733 def _rev_range(_context_uid, _repo_id, _filter):
734 734 repo = self._factory.repo(wire)
735 735 revisions = [
736 736 ascii_str(repo[rev].hex())
737 737 for rev in revrange(repo, list(map(ascii_bytes, commit_filter)))
738 738 ]
739 739 return revisions
740 740
741 741 return _rev_range(context_uid, repo_id, sorted(commit_filter))
742 742
743 743 @reraise_safe_exceptions
744 744 def rev_range_hash(self, wire, node):
745 745 repo = self._factory.repo(wire)
746 746
747 747 def get_revs(repo, rev_opt):
748 748 if rev_opt:
749 749 revs = revrange(repo, rev_opt)
750 750 if len(revs) == 0:
751 751 return (nullrev, nullrev)
752 752 return max(revs), min(revs)
753 753 else:
754 754 return len(repo) - 1, 0
755 755
756 756 stop, start = get_revs(repo, [node + ':'])
757 757 revs = [ascii_str(repo[r].hex()) for r in range(start, stop + 1)]
758 758 return revs
759 759
760 760 @reraise_safe_exceptions
761 761 def revs_from_revspec(self, wire, rev_spec, *args, **kwargs):
762 762 org_path = safe_bytes(wire["path"])
763 763 other_path = safe_bytes(kwargs.pop('other_path', ''))
764 764
765 765 # case when we want to compare two independent repositories
766 766 if other_path and other_path != wire["path"]:
767 767 baseui = self._factory._create_config(wire["config"])
768 768 repo = unionrepo.makeunionrepository(baseui, other_path, org_path)
769 769 else:
770 770 repo = self._factory.repo(wire)
771 771 return list(repo.revs(rev_spec, *args))
772 772
773 773 @reraise_safe_exceptions
774 774 def verify(self, wire,):
775 775 repo = self._factory.repo(wire)
776 776 baseui = self._factory._create_config(wire['config'])
777 777
778 778 baseui, output = patch_ui_message_output(baseui)
779 779
780 780 repo.ui = baseui
781 781 verify.verify(repo)
782 782 return output.getvalue()
783 783
784 784 @reraise_safe_exceptions
785 785 def hg_update_cache(self, wire,):
786 786 repo = self._factory.repo(wire)
787 787 baseui = self._factory._create_config(wire['config'])
788 788 baseui, output = patch_ui_message_output(baseui)
789 789
790 790 repo.ui = baseui
791 791 with repo.wlock(), repo.lock():
792 792 repo.updatecaches(full=True)
793 793
794 794 return output.getvalue()
795 795
796 796 @reraise_safe_exceptions
797 797 def hg_rebuild_fn_cache(self, wire,):
798 798 repo = self._factory.repo(wire)
799 799 baseui = self._factory._create_config(wire['config'])
800 800 baseui, output = patch_ui_message_output(baseui)
801 801
802 802 repo.ui = baseui
803 803
804 804 repair.rebuildfncache(baseui, repo)
805 805
806 806 return output.getvalue()
807 807
808 808 @reraise_safe_exceptions
809 809 def tags(self, wire):
810 810 cache_on, context_uid, repo_id = self._cache_on(wire)
811 811 region = self._region(wire)
812 812
813 813 @region.conditional_cache_on_arguments(condition=cache_on)
814 814 def _tags(_context_uid, _repo_id):
815 815 repo = self._factory.repo(wire)
816 816 return {safe_str(name): ascii_str(hex(sha)) for name, sha in repo.tags().items()}
817 817
818 818 return _tags(context_uid, repo_id)
819 819
820 820 @reraise_safe_exceptions
821 821 def update(self, wire, node='', clean=False):
822 822 repo = self._factory.repo(wire)
823 823 baseui = self._factory._create_config(wire['config'])
824 824 node = safe_bytes(node)
825 825
826 826 commands.update(baseui, repo, node=node, clean=clean)
827 827
828 828 @reraise_safe_exceptions
829 829 def identify(self, wire):
830 830 repo = self._factory.repo(wire)
831 831 baseui = self._factory._create_config(wire['config'])
832 832 output = io.BytesIO()
833 833 baseui.write = output.write
834 834 # This is required to get a full node id
835 835 baseui.debugflag = True
836 836 commands.identify(baseui, repo, id=True)
837 837
838 838 return output.getvalue()
839 839
840 840 @reraise_safe_exceptions
841 841 def heads(self, wire, branch=None):
842 842 repo = self._factory.repo(wire)
843 843 baseui = self._factory._create_config(wire['config'])
844 844 output = io.BytesIO()
845 845
846 846 def write(data, **unused_kwargs):
847 847 output.write(data)
848 848
849 849 baseui.write = write
850 850 if branch:
851 851 args = [safe_bytes(branch)]
852 852 else:
853 853 args = []
854 854 commands.heads(baseui, repo, template=b'{node} ', *args)
855 855
856 856 return output.getvalue()
857 857
858 858 @reraise_safe_exceptions
859 859 def ancestor(self, wire, revision1, revision2):
860 860 repo = self._factory.repo(wire)
861 861 changelog = repo.changelog
862 862 lookup = repo.lookup
863 863 a = changelog.ancestor(lookup(safe_bytes(revision1)), lookup(safe_bytes(revision2)))
864 864 return hex(a)
865 865
866 866 @reraise_safe_exceptions
867 867 def clone(self, wire, source, dest, update_after_clone=False, hooks=True):
868 868 baseui = self._factory._create_config(wire["config"], hooks=hooks)
869 869 clone(baseui, safe_bytes(source), safe_bytes(dest), noupdate=not update_after_clone)
870 870
871 871 @reraise_safe_exceptions
872 872 def commitctx(self, wire, message, parents, commit_time, commit_timezone, user, files, extra, removed, updated):
873 873
874 874 repo = self._factory.repo(wire)
875 875 baseui = self._factory._create_config(wire['config'])
876 876 publishing = baseui.configbool(b'phases', b'publish')
877 877
878 878 def _filectxfn(_repo, ctx, path: bytes):
879 879 """
880 880 Marks given path as added/changed/removed in a given _repo. This is
881 881 for internal mercurial commit function.
882 882 """
883 883
884 884 # check if this path is removed
885 885 if safe_str(path) in removed:
886 886 # returning None is a way to mark node for removal
887 887 return None
888 888
889 889 # check if this path is added
890 890 for node in updated:
891 891 if safe_bytes(node['path']) == path:
892 892 return memfilectx(
893 893 _repo,
894 894 changectx=ctx,
895 895 path=safe_bytes(node['path']),
896 896 data=safe_bytes(node['content']),
897 897 islink=False,
898 898 isexec=bool(node['mode'] & stat.S_IXUSR),
899 899 copysource=False)
900 900 abort_exc = exceptions.AbortException()
901 901 raise abort_exc(f"Given path haven't been marked as added, changed or removed ({path})")
902 902
903 903 if publishing:
904 904 new_commit_phase = b'public'
905 905 else:
906 906 new_commit_phase = b'draft'
907 907 with repo.ui.configoverride({(b'phases', b'new-commit'): new_commit_phase}):
908 908 kwargs = {safe_bytes(k): safe_bytes(v) for k, v in extra.items()}
909 909 commit_ctx = memctx(
910 910 repo=repo,
911 911 parents=parents,
912 912 text=safe_bytes(message),
913 913 files=[safe_bytes(x) for x in files],
914 914 filectxfn=_filectxfn,
915 915 user=safe_bytes(user),
916 916 date=(commit_time, commit_timezone),
917 917 extra=kwargs)
918 918
919 919 n = repo.commitctx(commit_ctx)
920 920 new_id = hex(n)
921 921
922 922 return new_id
923 923
924 924 @reraise_safe_exceptions
925 925 def pull(self, wire, url, commit_ids=None):
926 926 repo = self._factory.repo(wire)
927 927 # Disable any prompts for this repo
928 928 repo.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
929 929
930 930 remote = peer(repo, {}, safe_bytes(url))
931 931 # Disable any prompts for this remote
932 932 remote.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
933 933
934 934 if commit_ids:
935 935 commit_ids = [bin(commit_id) for commit_id in commit_ids]
936 936
937 937 return exchange.pull(
938 938 repo, remote, heads=commit_ids, force=None).cgresult
939 939
940 940 @reraise_safe_exceptions
941 941 def pull_cmd(self, wire, source, bookmark='', branch='', revision='', hooks=True):
942 942 repo = self._factory.repo(wire)
943 943 baseui = self._factory._create_config(wire['config'], hooks=hooks)
944 944
945 945 source = safe_bytes(source)
946 946
947 947 # Mercurial internally has a lot of logic that checks ONLY if
948 948 # option is defined, we just pass those if they are defined then
949 949 opts = {}
950 950 if bookmark:
951 951 if isinstance(branch, list):
952 952 bookmark = [safe_bytes(x) for x in bookmark]
953 953 else:
954 954 bookmark = safe_bytes(bookmark)
955 955 opts['bookmark'] = bookmark
956 956 if branch:
957 957 if isinstance(branch, list):
958 958 branch = [safe_bytes(x) for x in branch]
959 959 else:
960 960 branch = safe_bytes(branch)
961 961 opts['branch'] = branch
962 962 if revision:
963 963 opts['rev'] = safe_bytes(revision)
964 964
965 965 commands.pull(baseui, repo, source, **opts)
966 966
967 967 @reraise_safe_exceptions
968 968 def push(self, wire, revisions, dest_path, hooks=True, push_branches=False):
969 969 repo = self._factory.repo(wire)
970 970 baseui = self._factory._create_config(wire['config'], hooks=hooks)
971 971 commands.push(baseui, repo, dest=dest_path, rev=revisions,
972 972 new_branch=push_branches)
973 973
974 974 @reraise_safe_exceptions
975 975 def strip(self, wire, revision, update, backup):
976 976 repo = self._factory.repo(wire)
977 977 ctx = self._get_ctx(repo, revision)
978 978 hgext_strip(
979 979 repo.baseui, repo, ctx.node(), update=update, backup=backup)
980 980
981 981 @reraise_safe_exceptions
982 982 def get_unresolved_files(self, wire):
983 983 repo = self._factory.repo(wire)
984 984
985 985 log.debug('Calculating unresolved files for repo: %s', repo)
986 986 output = io.BytesIO()
987 987
988 988 def write(data, **unused_kwargs):
989 989 output.write(data)
990 990
991 991 baseui = self._factory._create_config(wire['config'])
992 992 baseui.write = write
993 993
994 994 commands.resolve(baseui, repo, list=True)
995 995 unresolved = output.getvalue().splitlines(0)
996 996 return unresolved
997 997
998 998 @reraise_safe_exceptions
999 999 def merge(self, wire, revision):
1000 1000 repo = self._factory.repo(wire)
1001 1001 baseui = self._factory._create_config(wire['config'])
1002 1002 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
1003 1003
1004 1004 # In case of sub repositories are used mercurial prompts the user in
1005 1005 # case of merge conflicts or different sub repository sources. By
1006 1006 # setting the interactive flag to `False` mercurial doesn't prompt the
1007 1007 # used but instead uses a default value.
1008 1008 repo.ui.setconfig(b'ui', b'interactive', False)
1009 1009 commands.merge(baseui, repo, rev=revision)
1010 1010
1011 1011 @reraise_safe_exceptions
1012 1012 def merge_state(self, wire):
1013 1013 repo = self._factory.repo(wire)
1014 1014 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
1015 1015
1016 1016 # In case of sub repositories are used mercurial prompts the user in
1017 1017 # case of merge conflicts or different sub repository sources. By
1018 1018 # setting the interactive flag to `False` mercurial doesn't prompt the
1019 1019 # used but instead uses a default value.
1020 1020 repo.ui.setconfig(b'ui', b'interactive', False)
1021 1021 ms = hg_merge.mergestate(repo)
1022 1022 return [x for x in ms.unresolved()]
1023 1023
1024 1024 @reraise_safe_exceptions
1025 1025 def commit(self, wire, message, username, close_branch=False):
1026 1026 repo = self._factory.repo(wire)
1027 1027 baseui = self._factory._create_config(wire['config'])
1028 1028 repo.ui.setconfig(b'ui', b'username', username)
1029 1029 commands.commit(baseui, repo, message=message, close_branch=close_branch)
1030 1030
1031 1031 @reraise_safe_exceptions
1032 1032 def rebase(self, wire, source=None, dest=None, abort=False):
1033 1033 repo = self._factory.repo(wire)
1034 1034 baseui = self._factory._create_config(wire['config'])
1035 1035 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
1036 1036 # In case of sub repositories are used mercurial prompts the user in
1037 1037 # case of merge conflicts or different sub repository sources. By
1038 1038 # setting the interactive flag to `False` mercurial doesn't prompt the
1039 1039 # used but instead uses a default value.
1040 1040 repo.ui.setconfig(b'ui', b'interactive', False)
1041 1041 rebase.rebase(baseui, repo, base=source, dest=dest, abort=abort, keep=not abort)
1042 1042
1043 1043 @reraise_safe_exceptions
1044 1044 def tag(self, wire, name, revision, message, local, user, tag_time, tag_timezone):
1045 1045 repo = self._factory.repo(wire)
1046 1046 ctx = self._get_ctx(repo, revision)
1047 1047 node = ctx.node()
1048 1048
1049 1049 date = (tag_time, tag_timezone)
1050 1050 try:
1051 1051 hg_tag.tag(repo, name, node, message, local, user, date)
1052 1052 except Abort as e:
1053 1053 log.exception("Tag operation aborted")
1054 1054 # Exception can contain unicode which we convert
1055 1055 raise exceptions.AbortException(e)(repr(e))
1056 1056
1057 1057 @reraise_safe_exceptions
1058 1058 def bookmark(self, wire, bookmark, revision=''):
1059 1059 repo = self._factory.repo(wire)
1060 1060 baseui = self._factory._create_config(wire['config'])
1061 1061 commands.bookmark(baseui, repo, safe_bytes(bookmark), rev=safe_bytes(revision), force=True)
1062 1062
1063 1063 @reraise_safe_exceptions
1064 1064 def install_hooks(self, wire, force=False):
1065 1065 # we don't need any special hooks for Mercurial
1066 1066 pass
1067 1067
1068 1068 @reraise_safe_exceptions
1069 1069 def get_hooks_info(self, wire):
1070 1070 return {
1071 1071 'pre_version': vcsserver.__version__,
1072 1072 'post_version': vcsserver.__version__,
1073 1073 }
1074 1074
1075 1075 @reraise_safe_exceptions
1076 1076 def set_head_ref(self, wire, head_name):
1077 1077 pass
1078 1078
1079 1079 @reraise_safe_exceptions
1080 1080 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
1081 1081 archive_dir_name, commit_id):
1082 1082
1083 1083 def file_walker(_commit_id, path):
1084 1084 repo = self._factory.repo(wire)
1085 1085 ctx = repo[_commit_id]
1086 1086 is_root = path in ['', '/']
1087 1087 if is_root:
1088 1088 matcher = alwaysmatcher(badfn=None)
1089 1089 else:
1090 1090 matcher = patternmatcher('', [(b'glob', path+'/**', b'')], badfn=None)
1091 1091 file_iter = ctx.manifest().walk(matcher)
1092 1092
1093 1093 for fn in file_iter:
1094 1094 file_path = fn
1095 1095 flags = ctx.flags(fn)
1096 1096 mode = b'x' in flags and 0o755 or 0o644
1097 1097 is_link = b'l' in flags
1098 1098
1099 1099 yield ArchiveNode(file_path, mode, is_link, ctx[fn].data)
1100 1100
1101 1101 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
1102 1102 archive_dir_name, commit_id)
1103 1103
@@ -1,888 +1,888 b''
1 1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 2 # Copyright (C) 2014-2020 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
19 19 import os
20 20 import subprocess
21 21 from urllib.error import URLError
22 22 import urllib.parse
23 23 import logging
24 24 import posixpath as vcspath
25 25 import io
26 26 import urllib.request
27 27 import urllib.parse
28 28 import urllib.error
29 29 import traceback
30 30
31 31 import svn.client
32 32 import svn.core
33 33 import svn.delta
34 34 import svn.diff
35 35 import svn.fs
36 36 import svn.repos
37 37
38 38 from vcsserver import svn_diff, exceptions, subprocessio, settings
39 from vcsserver.base import RepoFactory, raise_from_original, ArchiveNode, archive_repo
39 from vcsserver.base import RepoFactory, raise_from_original, ArchiveNode, archive_repo, BinaryEnvelope
40 40 from vcsserver.exceptions import NoContentException
41 41 from vcsserver.str_utils import safe_str, safe_bytes
42 42 from vcsserver.vcs_base import RemoteBase
43 43 from vcsserver.lib.svnremoterepo import svnremoterepo
44 44 log = logging.getLogger(__name__)
45 45
46 46
47 47 svn_compatible_versions_map = {
48 48 'pre-1.4-compatible': '1.3',
49 49 'pre-1.5-compatible': '1.4',
50 50 'pre-1.6-compatible': '1.5',
51 51 'pre-1.8-compatible': '1.7',
52 52 'pre-1.9-compatible': '1.8',
53 53 }
54 54
55 55 current_compatible_version = '1.14'
56 56
57 57
58 58 def reraise_safe_exceptions(func):
59 59 """Decorator for converting svn exceptions to something neutral."""
60 60 def wrapper(*args, **kwargs):
61 61 try:
62 62 return func(*args, **kwargs)
63 63 except Exception as e:
64 64 if not hasattr(e, '_vcs_kind'):
65 65 log.exception("Unhandled exception in svn remote call")
66 66 raise_from_original(exceptions.UnhandledException(e), e)
67 67 raise
68 68 return wrapper
69 69
70 70
71 71 class SubversionFactory(RepoFactory):
72 72 repo_type = 'svn'
73 73
74 74 def _create_repo(self, wire, create, compatible_version):
75 75 path = svn.core.svn_path_canonicalize(wire['path'])
76 76 if create:
77 77 fs_config = {'compatible-version': current_compatible_version}
78 78 if compatible_version:
79 79
80 80 compatible_version_string = \
81 81 svn_compatible_versions_map.get(compatible_version) \
82 82 or compatible_version
83 83 fs_config['compatible-version'] = compatible_version_string
84 84
85 85 log.debug('Create SVN repo with config "%s"', fs_config)
86 86 repo = svn.repos.create(path, "", "", None, fs_config)
87 87 else:
88 88 repo = svn.repos.open(path)
89 89
90 90 log.debug('Got SVN object: %s', repo)
91 91 return repo
92 92
93 93 def repo(self, wire, create=False, compatible_version=None):
94 94 """
95 95 Get a repository instance for the given path.
96 96 """
97 97 return self._create_repo(wire, create, compatible_version)
98 98
99 99
100 100 NODE_TYPE_MAPPING = {
101 101 svn.core.svn_node_file: 'file',
102 102 svn.core.svn_node_dir: 'dir',
103 103 }
104 104
105 105
106 106 class SvnRemote(RemoteBase):
107 107
108 108 def __init__(self, factory, hg_factory=None):
109 109 self._factory = factory
110 110
111 111 @reraise_safe_exceptions
112 112 def discover_svn_version(self):
113 113 try:
114 114 import svn.core
115 115 svn_ver = svn.core.SVN_VERSION
116 116 except ImportError:
117 117 svn_ver = None
118 118 return safe_str(svn_ver)
119 119
120 120 @reraise_safe_exceptions
121 121 def is_empty(self, wire):
122 122
123 123 try:
124 124 return self.lookup(wire, -1) == 0
125 125 except Exception:
126 126 log.exception("failed to read object_store")
127 127 return False
128 128
129 129 def check_url(self, url):
130 130
131 131 # uuid function get's only valid UUID from proper repo, else
132 132 # throws exception
133 133 username, password, src_url = self.get_url_and_credentials(url)
134 134 try:
135 135 svnremoterepo(username, password, src_url).svn().uuid
136 136 except Exception:
137 137 tb = traceback.format_exc()
138 138 log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb)
139 139 raise URLError(
140 140 '"%s" is not a valid Subversion source url.' % (url, ))
141 141 return True
142 142
143 143 def is_path_valid_repository(self, wire, path):
144 144
145 145 # NOTE(marcink): short circuit the check for SVN repo
146 146 # the repos.open might be expensive to check, but we have one cheap
147 147 # pre condition that we can use, to check for 'format' file
148 148
149 149 if not os.path.isfile(os.path.join(path, 'format')):
150 150 return False
151 151
152 152 try:
153 153 svn.repos.open(path)
154 154 except svn.core.SubversionException:
155 155 tb = traceback.format_exc()
156 156 log.debug("Invalid Subversion path `%s`, tb: %s", path, tb)
157 157 return False
158 158 return True
159 159
160 160 @reraise_safe_exceptions
161 161 def verify(self, wire,):
162 162 repo_path = wire['path']
163 163 if not self.is_path_valid_repository(wire, repo_path):
164 164 raise Exception(
165 165 "Path %s is not a valid Subversion repository." % repo_path)
166 166
167 167 cmd = ['svnadmin', 'info', repo_path]
168 168 stdout, stderr = subprocessio.run_command(cmd)
169 169 return stdout
170 170
171 171 def lookup(self, wire, revision):
172 172 if revision not in [-1, None, 'HEAD']:
173 173 raise NotImplementedError
174 174 repo = self._factory.repo(wire)
175 175 fs_ptr = svn.repos.fs(repo)
176 176 head = svn.fs.youngest_rev(fs_ptr)
177 177 return head
178 178
179 179 def lookup_interval(self, wire, start_ts, end_ts):
180 180 repo = self._factory.repo(wire)
181 181 fsobj = svn.repos.fs(repo)
182 182 start_rev = None
183 183 end_rev = None
184 184 if start_ts:
185 185 start_ts_svn = apr_time_t(start_ts)
186 186 start_rev = svn.repos.dated_revision(repo, start_ts_svn) + 1
187 187 else:
188 188 start_rev = 1
189 189 if end_ts:
190 190 end_ts_svn = apr_time_t(end_ts)
191 191 end_rev = svn.repos.dated_revision(repo, end_ts_svn)
192 192 else:
193 193 end_rev = svn.fs.youngest_rev(fsobj)
194 194 return start_rev, end_rev
195 195
196 196 def revision_properties(self, wire, revision):
197 197
198 198 cache_on, context_uid, repo_id = self._cache_on(wire)
199 199 region = self._region(wire)
200 200 @region.conditional_cache_on_arguments(condition=cache_on)
201 201 def _revision_properties(_repo_id, _revision):
202 202 repo = self._factory.repo(wire)
203 203 fs_ptr = svn.repos.fs(repo)
204 204 return svn.fs.revision_proplist(fs_ptr, revision)
205 205 return _revision_properties(repo_id, revision)
206 206
207 207 def revision_changes(self, wire, revision):
208 208
209 209 repo = self._factory.repo(wire)
210 210 fsobj = svn.repos.fs(repo)
211 211 rev_root = svn.fs.revision_root(fsobj, revision)
212 212
213 213 editor = svn.repos.ChangeCollector(fsobj, rev_root)
214 214 editor_ptr, editor_baton = svn.delta.make_editor(editor)
215 215 base_dir = ""
216 216 send_deltas = False
217 217 svn.repos.replay2(
218 218 rev_root, base_dir, svn.core.SVN_INVALID_REVNUM, send_deltas,
219 219 editor_ptr, editor_baton, None)
220 220
221 221 added = []
222 222 changed = []
223 223 removed = []
224 224
225 225 # TODO: CHANGE_ACTION_REPLACE: Figure out where it belongs
226 226 for path, change in editor.changes.items():
227 227 # TODO: Decide what to do with directory nodes. Subversion can add
228 228 # empty directories.
229 229
230 230 if change.item_kind == svn.core.svn_node_dir:
231 231 continue
232 232 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
233 233 added.append(path)
234 234 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
235 235 svn.repos.CHANGE_ACTION_REPLACE]:
236 236 changed.append(path)
237 237 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
238 238 removed.append(path)
239 239 else:
240 240 raise NotImplementedError(
241 241 "Action %s not supported on path %s" % (
242 242 change.action, path))
243 243
244 244 changes = {
245 245 'added': added,
246 246 'changed': changed,
247 247 'removed': removed,
248 248 }
249 249 return changes
250 250
251 251 @reraise_safe_exceptions
252 252 def node_history(self, wire, path, revision, limit):
253 253 cache_on, context_uid, repo_id = self._cache_on(wire)
254 254 region = self._region(wire)
255 255 @region.conditional_cache_on_arguments(condition=cache_on)
256 256 def _assert_correct_path(_context_uid, _repo_id, _path, _revision, _limit):
257 257 cross_copies = False
258 258 repo = self._factory.repo(wire)
259 259 fsobj = svn.repos.fs(repo)
260 260 rev_root = svn.fs.revision_root(fsobj, revision)
261 261
262 262 history_revisions = []
263 263 history = svn.fs.node_history(rev_root, path)
264 264 history = svn.fs.history_prev(history, cross_copies)
265 265 while history:
266 266 __, node_revision = svn.fs.history_location(history)
267 267 history_revisions.append(node_revision)
268 268 if limit and len(history_revisions) >= limit:
269 269 break
270 270 history = svn.fs.history_prev(history, cross_copies)
271 271 return history_revisions
272 272 return _assert_correct_path(context_uid, repo_id, path, revision, limit)
273 273
274 274 def node_properties(self, wire, path, revision):
275 275 cache_on, context_uid, repo_id = self._cache_on(wire)
276 276 region = self._region(wire)
277 277 @region.conditional_cache_on_arguments(condition=cache_on)
278 278 def _node_properties(_repo_id, _path, _revision):
279 279 repo = self._factory.repo(wire)
280 280 fsobj = svn.repos.fs(repo)
281 281 rev_root = svn.fs.revision_root(fsobj, revision)
282 282 return svn.fs.node_proplist(rev_root, path)
283 283 return _node_properties(repo_id, path, revision)
284 284
285 285 def file_annotate(self, wire, path, revision):
286 286 abs_path = 'file://' + urllib.request.pathname2url(
287 287 vcspath.join(wire['path'], path))
288 288 file_uri = svn.core.svn_path_canonicalize(abs_path)
289 289
290 290 start_rev = svn_opt_revision_value_t(0)
291 291 peg_rev = svn_opt_revision_value_t(revision)
292 292 end_rev = peg_rev
293 293
294 294 annotations = []
295 295
296 296 def receiver(line_no, revision, author, date, line, pool):
297 297 annotations.append((line_no, revision, line))
298 298
299 299 # TODO: Cannot use blame5, missing typemap function in the swig code
300 300 try:
301 301 svn.client.blame2(
302 302 file_uri, peg_rev, start_rev, end_rev,
303 303 receiver, svn.client.create_context())
304 304 except svn.core.SubversionException as exc:
305 305 log.exception("Error during blame operation.")
306 306 raise Exception(
307 307 "Blame not supported or file does not exist at path %s. "
308 308 "Error %s." % (path, exc))
309 309
310 310 return annotations
311 311
312 312 def get_node_type(self, wire, path, revision=None):
313 313
314 314 cache_on, context_uid, repo_id = self._cache_on(wire)
315 315 region = self._region(wire)
316 316 @region.conditional_cache_on_arguments(condition=cache_on)
317 317 def _get_node_type(_repo_id, _path, _revision):
318 318 repo = self._factory.repo(wire)
319 319 fs_ptr = svn.repos.fs(repo)
320 320 if _revision is None:
321 321 _revision = svn.fs.youngest_rev(fs_ptr)
322 322 root = svn.fs.revision_root(fs_ptr, _revision)
323 323 node = svn.fs.check_path(root, path)
324 324 return NODE_TYPE_MAPPING.get(node, None)
325 325 return _get_node_type(repo_id, path, revision)
326 326
327 327 def get_nodes(self, wire, path, revision=None):
328 328
329 329 cache_on, context_uid, repo_id = self._cache_on(wire)
330 330 region = self._region(wire)
331 331
332 332 @region.conditional_cache_on_arguments(condition=cache_on)
333 333 def _get_nodes(_repo_id, _path, _revision):
334 334 repo = self._factory.repo(wire)
335 335 fsobj = svn.repos.fs(repo)
336 336 if _revision is None:
337 337 _revision = svn.fs.youngest_rev(fsobj)
338 338 root = svn.fs.revision_root(fsobj, _revision)
339 339 entries = svn.fs.dir_entries(root, path)
340 340 result = []
341 341 for entry_path, entry_info in entries.items():
342 342 result.append(
343 343 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
344 344 return result
345 345 return _get_nodes(repo_id, path, revision)
346 346
347 347 def get_file_content(self, wire, path, rev=None):
348 348 repo = self._factory.repo(wire)
349 349 fsobj = svn.repos.fs(repo)
350 350 if rev is None:
351 351 rev = svn.fs.youngest_revision(fsobj)
352 352 root = svn.fs.revision_root(fsobj, rev)
353 353 content = svn.core.Stream(svn.fs.file_contents(root, path))
354 return content.read()
354 return BinaryEnvelope(content.read())
355 355
356 356 def get_file_size(self, wire, path, revision=None):
357 357
358 358 cache_on, context_uid, repo_id = self._cache_on(wire)
359 359 region = self._region(wire)
360 360
361 361 @region.conditional_cache_on_arguments(condition=cache_on)
362 362 def _get_file_size(_repo_id, _path, _revision):
363 363 repo = self._factory.repo(wire)
364 364 fsobj = svn.repos.fs(repo)
365 365 if _revision is None:
366 366 _revision = svn.fs.youngest_revision(fsobj)
367 367 root = svn.fs.revision_root(fsobj, _revision)
368 368 size = svn.fs.file_length(root, path)
369 369 return size
370 370 return _get_file_size(repo_id, path, revision)
371 371
372 372 def create_repository(self, wire, compatible_version=None):
373 373 log.info('Creating Subversion repository in path "%s"', wire['path'])
374 374 self._factory.repo(wire, create=True,
375 375 compatible_version=compatible_version)
376 376
377 377 def get_url_and_credentials(self, src_url):
378 378 obj = urllib.parse.urlparse(src_url)
379 379 username = obj.username or None
380 380 password = obj.password or None
381 381 return username, password, src_url
382 382
383 383 def import_remote_repository(self, wire, src_url):
384 384 repo_path = wire['path']
385 385 if not self.is_path_valid_repository(wire, repo_path):
386 386 raise Exception(
387 387 "Path %s is not a valid Subversion repository." % repo_path)
388 388
389 389 username, password, src_url = self.get_url_and_credentials(src_url)
390 390 rdump_cmd = ['svnrdump', 'dump', '--non-interactive',
391 391 '--trust-server-cert-failures=unknown-ca']
392 392 if username and password:
393 393 rdump_cmd += ['--username', username, '--password', password]
394 394 rdump_cmd += [src_url]
395 395
396 396 rdump = subprocess.Popen(
397 397 rdump_cmd,
398 398 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
399 399 load = subprocess.Popen(
400 400 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
401 401
402 402 # TODO: johbo: This can be a very long operation, might be better
403 403 # to track some kind of status and provide an api to check if the
404 404 # import is done.
405 405 rdump.wait()
406 406 load.wait()
407 407
408 408 log.debug('Return process ended with code: %s', rdump.returncode)
409 409 if rdump.returncode != 0:
410 410 errors = rdump.stderr.read()
411 411 log.error('svnrdump dump failed: statuscode %s: message: %s', rdump.returncode, errors)
412 412
413 413 reason = 'UNKNOWN'
414 414 if b'svnrdump: E230001:' in errors:
415 415 reason = 'INVALID_CERTIFICATE'
416 416
417 417 if reason == 'UNKNOWN':
418 418 reason = 'UNKNOWN:{}'.format(safe_str(errors))
419 419
420 420 raise Exception(
421 421 'Failed to dump the remote repository from %s. Reason:%s' % (
422 422 src_url, reason))
423 423 if load.returncode != 0:
424 424 raise Exception(
425 425 'Failed to load the dump of remote repository from %s.' %
426 426 (src_url, ))
427 427
428 428 def commit(self, wire, message, author, timestamp, updated, removed):
429 429
430 430 updated = [{k: safe_bytes(v) for k, v in x.items() if isinstance(v, str)} for x in updated]
431 431
432 432 message = safe_bytes(message)
433 433 author = safe_bytes(author)
434 434
435 435 repo = self._factory.repo(wire)
436 436 fsobj = svn.repos.fs(repo)
437 437
438 438 rev = svn.fs.youngest_rev(fsobj)
439 439 txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
440 440 txn_root = svn.fs.txn_root(txn)
441 441
442 442 for node in updated:
443 443 TxnNodeProcessor(node, txn_root).update()
444 444 for node in removed:
445 445 TxnNodeProcessor(node, txn_root).remove()
446 446
447 447 commit_id = svn.repos.fs_commit_txn(repo, txn)
448 448
449 449 if timestamp:
450 450 apr_time = int(apr_time_t(timestamp))
451 451 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
452 452 svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
453 453
454 454 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
455 455 return commit_id
456 456
457 457 def diff(self, wire, rev1, rev2, path1=None, path2=None,
458 458 ignore_whitespace=False, context=3):
459 459
460 460 wire.update(cache=False)
461 461 repo = self._factory.repo(wire)
462 462 diff_creator = SvnDiffer(
463 463 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
464 464 try:
465 return diff_creator.generate_diff()
465 return BinaryEnvelope(diff_creator.generate_diff())
466 466 except svn.core.SubversionException as e:
467 467 log.exception(
468 468 "Error during diff operation operation. "
469 "Path might not exist %s, %s" % (path1, path2))
470 return ""
469 "Path might not exist %s, %s", path1, path2)
470 return BinaryEnvelope(b'')
471 471
472 472 @reraise_safe_exceptions
473 473 def is_large_file(self, wire, path):
474 474 return False
475 475
476 476 @reraise_safe_exceptions
477 477 def is_binary(self, wire, rev, path):
478 478 cache_on, context_uid, repo_id = self._cache_on(wire)
479 479 region = self._region(wire)
480 480
481 481 @region.conditional_cache_on_arguments(condition=cache_on)
482 482 def _is_binary(_repo_id, _rev, _path):
483 483 raw_bytes = self.get_file_content(wire, path, rev)
484 484 return raw_bytes and b'\0' in raw_bytes
485 485
486 486 return _is_binary(repo_id, rev, path)
487 487
488 488 @reraise_safe_exceptions
489 489 def md5_hash(self, wire, rev, path):
490 490 cache_on, context_uid, repo_id = self._cache_on(wire)
491 491 region = self._region(wire)
492 492
493 493 @region.conditional_cache_on_arguments(condition=cache_on)
494 494 def _md5_hash(_repo_id, _rev, _path):
495 495 return ''
496 496
497 497 return _md5_hash(repo_id, rev, path)
498 498
499 499 @reraise_safe_exceptions
500 500 def run_svn_command(self, wire, cmd, **opts):
501 501 path = wire.get('path', None)
502 502
503 503 if path and os.path.isdir(path):
504 504 opts['cwd'] = path
505 505
506 506 safe_call = opts.pop('_safe', False)
507 507
508 508 svnenv = os.environ.copy()
509 509 svnenv.update(opts.pop('extra_env', {}))
510 510
511 511 _opts = {'env': svnenv, 'shell': False}
512 512
513 513 try:
514 514 _opts.update(opts)
515 515 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
516 516
517 517 return b''.join(proc), b''.join(proc.stderr)
518 518 except OSError as err:
519 519 if safe_call:
520 520 return '', safe_str(err).strip()
521 521 else:
522 522 cmd = ' '.join(map(safe_str, cmd)) # human friendly CMD
523 523 tb_err = ("Couldn't run svn command (%s).\n"
524 524 "Original error was:%s\n"
525 525 "Call options:%s\n"
526 526 % (cmd, err, _opts))
527 527 log.exception(tb_err)
528 528 raise exceptions.VcsException()(tb_err)
529 529
530 530 @reraise_safe_exceptions
531 531 def install_hooks(self, wire, force=False):
532 532 from vcsserver.hook_utils import install_svn_hooks
533 533 repo_path = wire['path']
534 534 binary_dir = settings.BINARY_DIR
535 535 executable = None
536 536 if binary_dir:
537 537 executable = os.path.join(binary_dir, 'python3')
538 538 return install_svn_hooks(repo_path, force_create=force)
539 539
540 540 @reraise_safe_exceptions
541 541 def get_hooks_info(self, wire):
542 542 from vcsserver.hook_utils import (
543 543 get_svn_pre_hook_version, get_svn_post_hook_version)
544 544 repo_path = wire['path']
545 545 return {
546 546 'pre_version': get_svn_pre_hook_version(repo_path),
547 547 'post_version': get_svn_post_hook_version(repo_path),
548 548 }
549 549
550 550 @reraise_safe_exceptions
551 551 def set_head_ref(self, wire, head_name):
552 552 pass
553 553
554 554 @reraise_safe_exceptions
555 555 def archive_repo(self, wire, archive_dest_path, kind, mtime, archive_at_path,
556 556 archive_dir_name, commit_id):
557 557
558 558 def walk_tree(root, root_dir, _commit_id):
559 559 """
560 560 Special recursive svn repo walker
561 561 """
562 562 root_dir = safe_bytes(root_dir)
563 563
564 564 filemode_default = 0o100644
565 565 filemode_executable = 0o100755
566 566
567 567 file_iter = svn.fs.dir_entries(root, root_dir)
568 568 for f_name in file_iter:
569 569 f_type = NODE_TYPE_MAPPING.get(file_iter[f_name].kind, None)
570 570
571 571 if f_type == 'dir':
572 572 # return only DIR, and then all entries in that dir
573 573 yield os.path.join(root_dir, f_name), {'mode': filemode_default}, f_type
574 574 new_root = os.path.join(root_dir, f_name)
575 575 for _f_name, _f_data, _f_type in walk_tree(root, new_root, _commit_id):
576 576 yield _f_name, _f_data, _f_type
577 577 else:
578 578
579 579 f_path = os.path.join(root_dir, f_name).rstrip(b'/')
580 580 prop_list = svn.fs.node_proplist(root, f_path)
581 581
582 582 f_mode = filemode_default
583 583 if prop_list.get('svn:executable'):
584 584 f_mode = filemode_executable
585 585
586 586 f_is_link = False
587 587 if prop_list.get('svn:special'):
588 588 f_is_link = True
589 589
590 590 data = {
591 591 'is_link': f_is_link,
592 592 'mode': f_mode,
593 593 'content_stream': svn.core.Stream(svn.fs.file_contents(root, f_path)).read
594 594 }
595 595
596 596 yield f_path, data, f_type
597 597
598 598 def file_walker(_commit_id, path):
599 599 repo = self._factory.repo(wire)
600 600 root = svn.fs.revision_root(svn.repos.fs(repo), int(commit_id))
601 601
602 602 def no_content():
603 603 raise NoContentException()
604 604
605 605 for f_name, f_data, f_type in walk_tree(root, path, _commit_id):
606 606 file_path = f_name
607 607
608 608 if f_type == 'dir':
609 609 mode = f_data['mode']
610 610 yield ArchiveNode(file_path, mode, False, no_content)
611 611 else:
612 612 mode = f_data['mode']
613 613 is_link = f_data['is_link']
614 614 data_stream = f_data['content_stream']
615 615 yield ArchiveNode(file_path, mode, is_link, data_stream)
616 616
617 617 return archive_repo(file_walker, archive_dest_path, kind, mtime, archive_at_path,
618 618 archive_dir_name, commit_id)
619 619
620 620
621 621 class SvnDiffer(object):
622 622 """
623 623 Utility to create diffs based on difflib and the Subversion api
624 624 """
625 625
626 626 binary_content = False
627 627
628 628 def __init__(
629 629 self, repo, src_rev, src_path, tgt_rev, tgt_path,
630 630 ignore_whitespace, context):
631 631 self.repo = repo
632 632 self.ignore_whitespace = ignore_whitespace
633 633 self.context = context
634 634
635 635 fsobj = svn.repos.fs(repo)
636 636
637 637 self.tgt_rev = tgt_rev
638 638 self.tgt_path = tgt_path or ''
639 639 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
640 640 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
641 641
642 642 self.src_rev = src_rev
643 643 self.src_path = src_path or self.tgt_path
644 644 self.src_root = svn.fs.revision_root(fsobj, src_rev)
645 645 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
646 646
647 647 self._validate()
648 648
649 649 def _validate(self):
650 650 if (self.tgt_kind != svn.core.svn_node_none and
651 651 self.src_kind != svn.core.svn_node_none and
652 652 self.src_kind != self.tgt_kind):
653 653 # TODO: johbo: proper error handling
654 654 raise Exception(
655 655 "Source and target are not compatible for diff generation. "
656 656 "Source type: %s, target type: %s" %
657 657 (self.src_kind, self.tgt_kind))
658 658
659 659 def generate_diff(self):
660 660 buf = io.StringIO()
661 661 if self.tgt_kind == svn.core.svn_node_dir:
662 662 self._generate_dir_diff(buf)
663 663 else:
664 664 self._generate_file_diff(buf)
665 665 return buf.getvalue()
666 666
667 667 def _generate_dir_diff(self, buf):
668 668 editor = DiffChangeEditor()
669 669 editor_ptr, editor_baton = svn.delta.make_editor(editor)
670 670 svn.repos.dir_delta2(
671 671 self.src_root,
672 672 self.src_path,
673 673 '', # src_entry
674 674 self.tgt_root,
675 675 self.tgt_path,
676 676 editor_ptr, editor_baton,
677 677 authorization_callback_allow_all,
678 678 False, # text_deltas
679 679 svn.core.svn_depth_infinity, # depth
680 680 False, # entry_props
681 681 False, # ignore_ancestry
682 682 )
683 683
684 684 for path, __, change in sorted(editor.changes):
685 685 self._generate_node_diff(
686 686 buf, change, path, self.tgt_path, path, self.src_path)
687 687
688 688 def _generate_file_diff(self, buf):
689 689 change = None
690 690 if self.src_kind == svn.core.svn_node_none:
691 691 change = "add"
692 692 elif self.tgt_kind == svn.core.svn_node_none:
693 693 change = "delete"
694 694 tgt_base, tgt_path = vcspath.split(self.tgt_path)
695 695 src_base, src_path = vcspath.split(self.src_path)
696 696 self._generate_node_diff(
697 697 buf, change, tgt_path, tgt_base, src_path, src_base)
698 698
699 699 def _generate_node_diff(
700 700 self, buf, change, tgt_path, tgt_base, src_path, src_base):
701 701
702 702
703 703 tgt_path = safe_str(tgt_path)
704 704 src_path = safe_str(src_path)
705 705
706 706
707 707 if self.src_rev == self.tgt_rev and tgt_base == src_base:
708 708 # makes consistent behaviour with git/hg to return empty diff if
709 709 # we compare same revisions
710 710 return
711 711
712 712 tgt_full_path = vcspath.join(tgt_base, tgt_path)
713 713 src_full_path = vcspath.join(src_base, src_path)
714 714
715 715 self.binary_content = False
716 716 mime_type = self._get_mime_type(tgt_full_path)
717 717
718 718 if mime_type and not mime_type.startswith('text'):
719 719 self.binary_content = True
720 720 buf.write("=" * 67 + '\n')
721 721 buf.write("Cannot display: file marked as a binary type.\n")
722 722 buf.write("svn:mime-type = %s\n" % mime_type)
723 723 buf.write("Index: %s\n" % (tgt_path, ))
724 724 buf.write("=" * 67 + '\n')
725 725 buf.write("diff --git a/%(tgt_path)s b/%(tgt_path)s\n" % {
726 726 'tgt_path': tgt_path})
727 727
728 728 if change == 'add':
729 729 # TODO: johbo: SVN is missing a zero here compared to git
730 730 buf.write("new file mode 10644\n")
731 731
732 732 #TODO(marcink): intro to binary detection of svn patches
733 733 # if self.binary_content:
734 734 # buf.write('GIT binary patch\n')
735 735
736 736 buf.write("--- /dev/null\t(revision 0)\n")
737 737 src_lines = []
738 738 else:
739 739 if change == 'delete':
740 740 buf.write("deleted file mode 10644\n")
741 741
742 742 #TODO(marcink): intro to binary detection of svn patches
743 743 # if self.binary_content:
744 744 # buf.write('GIT binary patch\n')
745 745
746 746 buf.write("--- a/%s\t(revision %s)\n" % (
747 747 src_path, self.src_rev))
748 748 src_lines = self._svn_readlines(self.src_root, src_full_path)
749 749
750 750 if change == 'delete':
751 751 buf.write("+++ /dev/null\t(revision %s)\n" % (self.tgt_rev, ))
752 752 tgt_lines = []
753 753 else:
754 754 buf.write("+++ b/%s\t(revision %s)\n" % (
755 755 tgt_path, self.tgt_rev))
756 756 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
757 757
758 758 if not self.binary_content:
759 759 udiff = svn_diff.unified_diff(
760 760 src_lines, tgt_lines, context=self.context,
761 761 ignore_blank_lines=self.ignore_whitespace,
762 762 ignore_case=False,
763 763 ignore_space_changes=self.ignore_whitespace)
764 764
765 765 buf.writelines(udiff)
766 766
767 767 def _get_mime_type(self, path):
768 768 try:
769 769 mime_type = svn.fs.node_prop(
770 770 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
771 771 except svn.core.SubversionException:
772 772 mime_type = svn.fs.node_prop(
773 773 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
774 774 return mime_type
775 775
776 776 def _svn_readlines(self, fs_root, node_path):
777 777 if self.binary_content:
778 778 return []
779 779 node_kind = svn.fs.check_path(fs_root, node_path)
780 780 if node_kind not in (
781 781 svn.core.svn_node_file, svn.core.svn_node_symlink):
782 782 return []
783 783 content = svn.core.Stream(
784 784 svn.fs.file_contents(fs_root, node_path)).read()
785 785
786 786 return content.splitlines(True)
787 787
788 788
789 789 class DiffChangeEditor(svn.delta.Editor):
790 790 """
791 791 Records changes between two given revisions
792 792 """
793 793
794 794 def __init__(self):
795 795 self.changes = []
796 796
797 797 def delete_entry(self, path, revision, parent_baton, pool=None):
798 798 self.changes.append((path, None, 'delete'))
799 799
800 800 def add_file(
801 801 self, path, parent_baton, copyfrom_path, copyfrom_revision,
802 802 file_pool=None):
803 803 self.changes.append((path, 'file', 'add'))
804 804
805 805 def open_file(self, path, parent_baton, base_revision, file_pool=None):
806 806 self.changes.append((path, 'file', 'change'))
807 807
808 808
809 809 def authorization_callback_allow_all(root, path, pool):
810 810 return True
811 811
812 812
813 813 class TxnNodeProcessor(object):
814 814 """
815 815 Utility to process the change of one node within a transaction root.
816 816
817 817 It encapsulates the knowledge of how to add, update or remove
818 818 a node for a given transaction root. The purpose is to support the method
819 819 `SvnRemote.commit`.
820 820 """
821 821
822 822 def __init__(self, node, txn_root):
823 823 assert isinstance(node['path'], bytes)
824 824
825 825 self.node = node
826 826 self.txn_root = txn_root
827 827
828 828 def update(self):
829 829 self._ensure_parent_dirs()
830 830 self._add_file_if_node_does_not_exist()
831 831 self._update_file_content()
832 832 self._update_file_properties()
833 833
834 834 def remove(self):
835 835 svn.fs.delete(self.txn_root, self.node['path'])
836 836 # TODO: Clean up directory if empty
837 837
838 838 def _ensure_parent_dirs(self):
839 839 curdir = vcspath.dirname(self.node['path'])
840 840 dirs_to_create = []
841 841 while not self._svn_path_exists(curdir):
842 842 dirs_to_create.append(curdir)
843 843 curdir = vcspath.dirname(curdir)
844 844
845 845 for curdir in reversed(dirs_to_create):
846 846 log.debug('Creating missing directory "%s"', curdir)
847 847 svn.fs.make_dir(self.txn_root, curdir)
848 848
849 849 def _svn_path_exists(self, path):
850 850 path_status = svn.fs.check_path(self.txn_root, path)
851 851 return path_status != svn.core.svn_node_none
852 852
853 853 def _add_file_if_node_does_not_exist(self):
854 854 kind = svn.fs.check_path(self.txn_root, self.node['path'])
855 855 if kind == svn.core.svn_node_none:
856 856 svn.fs.make_file(self.txn_root, self.node['path'])
857 857
858 858 def _update_file_content(self):
859 859 assert isinstance(self.node['content'], bytes)
860 860
861 861 handler, baton = svn.fs.apply_textdelta(
862 862 self.txn_root, self.node['path'], None, None)
863 863 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
864 864
865 865 def _update_file_properties(self):
866 866 properties = self.node.get('properties', {})
867 867 for key, value in properties.items():
868 868 svn.fs.change_node_prop(
869 869 self.txn_root, self.node['path'], key, value)
870 870
871 871
872 872 def apr_time_t(timestamp):
873 873 """
874 874 Convert a Python timestamp into APR timestamp type apr_time_t
875 875 """
876 876 return timestamp * 1E6
877 877
878 878
879 879 def svn_opt_revision_value_t(num):
880 880 """
881 881 Put `num` into a `svn_opt_revision_value_t` structure.
882 882 """
883 883 value = svn.core.svn_opt_revision_value_t()
884 884 value.number = num
885 885 revision = svn.core.svn_opt_revision_t()
886 886 revision.kind = svn.core.svn_opt_revision_number
887 887 revision.value = value
888 888 return revision
General Comments 0
You need to be logged in to leave comments. Login now