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