##// END OF EJS Templates
libs: more python3 reformats
super-admin -
r5091:9ce86a18 default
parent child Browse files
Show More
@@ -1,190 +1,188 b''
1
2
3 1 # Copyright (C) 2014-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 """
22 20 Various version Control System version lib (vcs) management abstraction layer
23 21 for Python. Build with server client architecture.
24 22 """
25 23 import io
26 24 import atexit
27 25 import logging
28 26
29 27 import rhodecode
30 28 from rhodecode.lib.str_utils import safe_bytes
31 29 from rhodecode.lib.vcs.conf import settings
32 30 from rhodecode.lib.vcs.backends import get_vcs_instance, get_backend
33 31 from rhodecode.lib.vcs.exceptions import (
34 32 VCSError, RepositoryError, CommitError, VCSCommunicationError)
35 33
36 34 __all__ = [
37 35 'get_vcs_instance', 'get_backend',
38 36 'VCSError', 'RepositoryError', 'CommitError', 'VCSCommunicationError'
39 37 ]
40 38
41 39 log = logging.getLogger(__name__)
42 40
43 41 # The pycurl library directly accesses C API functions and is not patched by
44 42 # gevent. This will potentially lead to deadlocks due to incompatibility to
45 43 # gevent. Therefore we check if gevent is active and import a gevent compatible
46 44 # wrapper in that case.
47 45 try:
48 46 from gevent import monkey
49 47 if monkey.is_module_patched('__builtin__'):
50 48 import geventcurl as pycurl
51 49 log.debug('Using gevent comapatible pycurl: %s', pycurl)
52 50 else:
53 51 import pycurl
54 52 except ImportError:
55 53 import pycurl
56 54
57 55
58 56 def connect_http(server_and_port):
59 57 log.debug('Initialized VCSServer connections to %s.', server_and_port)
60 58
61 59 from rhodecode.lib.vcs import connection, client_http
62 60 from rhodecode.lib.middleware.utils import scm_app
63 61
64 62 session_factory = client_http.ThreadlocalSessionFactory()
65 63
66 64 connection.Git = client_http.RemoteVCSMaker(
67 65 server_and_port, '/git', 'git', session_factory)
68 66 connection.Hg = client_http.RemoteVCSMaker(
69 67 server_and_port, '/hg', 'hg', session_factory)
70 68 connection.Svn = client_http.RemoteVCSMaker(
71 69 server_and_port, '/svn', 'svn', session_factory)
72 70 connection.Service = client_http.ServiceConnection(
73 71 server_and_port, '/_service', session_factory)
74 72
75 73 scm_app.HG_REMOTE_WSGI = client_http.VcsHttpProxy(
76 74 server_and_port, '/proxy/hg')
77 75 scm_app.GIT_REMOTE_WSGI = client_http.VcsHttpProxy(
78 76 server_and_port, '/proxy/git')
79 77
80 78 @atexit.register
81 79 def free_connection_resources():
82 80 connection.Git = None
83 81 connection.Hg = None
84 82 connection.Svn = None
85 83 connection.Service = None
86 84
87 85
88 86 def connect_vcs(server_and_port, protocol):
89 87 """
90 88 Initializes the connection to the vcs server.
91 89
92 90 :param server_and_port: str, e.g. "localhost:9900"
93 91 :param protocol: str or "http"
94 92 """
95 93 if protocol == 'http':
96 94 connect_http(server_and_port)
97 95 else:
98 raise Exception('Invalid vcs server protocol "{}"'.format(protocol))
96 raise Exception(f'Invalid vcs server protocol "{protocol}"')
99 97
100 98
101 99 class CurlSession(object):
102 100 """
103 101 Modeled so that it provides a subset of the requests interface.
104 102
105 103 This has been created so that it does only provide a minimal API for our
106 104 needs. The parts which it provides are based on the API of the library
107 105 `requests` which allows us to easily benchmark against it.
108 106
109 107 Please have a look at the class :class:`requests.Session` when you extend
110 108 it.
111 109 """
112 110 CURL_UA = f'RhodeCode HTTP {rhodecode.__version__}'
113 111
114 112 def __init__(self):
115 113 curl = pycurl.Curl()
116 114 # TODO: johbo: I did test with 7.19 of libcurl. This version has
117 115 # trouble with 100 - continue being set in the expect header. This
118 116 # can lead to massive performance drops, switching it off here.
119 117
120 118 curl.setopt(curl.TCP_NODELAY, True)
121 119 curl.setopt(curl.PROTOCOLS, curl.PROTO_HTTP)
122 120 curl.setopt(curl.USERAGENT, safe_bytes(self.CURL_UA))
123 121 curl.setopt(curl.SSL_VERIFYPEER, 0)
124 122 curl.setopt(curl.SSL_VERIFYHOST, 0)
125 123 self._curl = curl
126 124
127 125 def post(self, url, data, allow_redirects=False, headers=None):
128 126 headers = headers or {}
129 127 # format is ['header_name1: header_value1', 'header_name2: header_value2'])
130 128 headers_list = [b"Expect:"] + [safe_bytes('{}: {}'.format(k, v)) for k, v in headers.items()]
131 129 response_buffer = io.BytesIO()
132 130
133 131 curl = self._curl
134 132 curl.setopt(curl.URL, url)
135 133 curl.setopt(curl.POST, True)
136 134 curl.setopt(curl.POSTFIELDS, data)
137 135 curl.setopt(curl.FOLLOWLOCATION, allow_redirects)
138 136 curl.setopt(curl.WRITEDATA, response_buffer)
139 137 curl.setopt(curl.HTTPHEADER, headers_list)
140 138 curl.perform()
141 139
142 140 status_code = curl.getinfo(pycurl.HTTP_CODE)
143 141 content_type = curl.getinfo(pycurl.CONTENT_TYPE)
144 142 return CurlResponse(response_buffer, status_code, content_type)
145 143
146 144
147 145 class CurlResponse(object):
148 146 """
149 147 The response of a request, modeled after the requests API.
150 148
151 149 This class provides a subset of the response interface known from the
152 150 library `requests`. It is intentionally kept similar, so that we can use
153 151 `requests` as a drop in replacement for benchmarking purposes.
154 152 """
155 153
156 154 def __init__(self, response_buffer, status_code, content_type=''):
157 155 self._response_buffer = response_buffer
158 156 self._status_code = status_code
159 157 self._content_type = content_type
160 158
161 159 def __repr__(self):
162 160 return f'CurlResponse(code={self._status_code}, content_type={self._content_type})'
163 161
164 162 @property
165 163 def content(self):
166 164 try:
167 165 return self._response_buffer.getvalue()
168 166 finally:
169 167 self._response_buffer.close()
170 168
171 169 @property
172 170 def status_code(self):
173 171 return self._status_code
174 172
175 173 @property
176 174 def content_type(self):
177 175 return self._content_type
178 176
179 177 def iter_content(self, chunk_size):
180 178 self._response_buffer.seek(0)
181 179 while 1:
182 180 chunk = self._response_buffer.read(chunk_size)
183 181 if not chunk:
184 182 break
185 183 yield chunk
186 184
187 185
188 186 def _create_http_rpc_session():
189 187 session = CurlSession()
190 188 return session
@@ -1,95 +1,93 b''
1
2
3 1 # Copyright (C) 2014-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 """
22 20 VCS Backends module
23 21 """
24 22
25 23 import os
26 24 import logging
27 25
28 26 from rhodecode import typing
29 27
30 28 from rhodecode.lib.vcs.conf import settings
31 29 from rhodecode.lib.vcs.exceptions import VCSError
32 30 from rhodecode.lib.vcs.utils.helpers import get_scm
33 31 from rhodecode.lib.vcs.utils.imports import import_class
34 32
35 33
36 34 log = logging.getLogger(__name__)
37 35
38 36
39 37 def get_vcs_instance(repo_path, *args, **kwargs) -> typing.VCSRepo | None:
40 38 """
41 39 Given a path to a repository an instance of the corresponding vcs backend
42 40 repository class is created and returned. If no repository can be found
43 41 for the path it returns None. Arguments and keyword arguments are passed
44 42 to the vcs backend repository class.
45 43 """
46 44 from rhodecode.lib.utils2 import safe_str
47 45
48 46 explicit_vcs_alias = kwargs.pop('_vcs_alias', None)
49 47 try:
50 48 vcs_alias = safe_str(explicit_vcs_alias or get_scm(repo_path)[0])
51 49 log.debug(
52 50 'Creating instance of %s repository from %s', vcs_alias,
53 51 safe_str(repo_path))
54 52 backend = get_backend(vcs_alias)
55 53
56 54 if explicit_vcs_alias:
57 55 # do final verification of existence of the path, this does the
58 56 # same as get_scm() call which we skip in explicit_vcs_alias
59 57 if not os.path.isdir(repo_path):
60 58 raise VCSError(f"Given path {repo_path} is not a directory")
61 59 except VCSError:
62 60 log.exception(
63 61 'Perhaps this repository is in db and not in '
64 62 'filesystem run rescan repositories with '
65 63 '"destroy old data" option from admin panel')
66 64 return None
67 65
68 66 return backend(repo_path=repo_path, *args, **kwargs)
69 67
70 68
71 69 def get_backend(alias) -> typing.VCSRepoClass:
72 70 """
73 71 Returns ``Repository`` class identified by the given alias or raises
74 72 VCSError if alias is not recognized or backend class cannot be imported.
75 73 """
76 74 if alias not in settings.BACKENDS:
77 75 raise VCSError(
78 76 f"Given alias '{alias}' is not recognized! "
79 77 f"Allowed aliases:{settings.BACKENDS.keys()}")
80 78 backend_path = settings.BACKENDS[alias]
81 79 klass = import_class(backend_path)
82 80 return klass
83 81
84 82
85 83 def get_supported_backends():
86 84 """
87 85 Returns list of aliases of supported backends.
88 86 """
89 87 return settings.BACKENDS.keys()
90 88
91 89
92 90 def get_vcsserver_service_data():
93 91 from rhodecode.lib.vcs import connection
94 92 return connection.Service.get_vcsserver_service_data()
95 93
@@ -1,1989 +1,1987 b''
1 1
2 2
3 3 # Copyright (C) 2014-2023 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Base module for all VCS systems
23 23 """
24 24 import os
25 25 import re
26 26 import time
27 27 import shutil
28 28 import datetime
29 29 import fnmatch
30 30 import itertools
31 31 import logging
32 32 import dataclasses
33 33 import warnings
34 34
35 35 from zope.cachedescriptors.property import Lazy as LazyProperty
36 36
37 37
38 38 import rhodecode
39 39 from rhodecode.translation import lazy_ugettext
40 40 from rhodecode.lib.utils2 import safe_str, CachedProperty
41 41 from rhodecode.lib.vcs.utils import author_name, author_email
42 42 from rhodecode.lib.vcs.conf import settings
43 43 from rhodecode.lib.vcs.exceptions import (
44 44 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
45 45 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
46 46 NodeDoesNotExistError, NodeNotChangedError, VCSError,
47 47 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
48 48 RepositoryError)
49 49
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 FILEMODE_DEFAULT = 0o100644
55 55 FILEMODE_EXECUTABLE = 0o100755
56 56 EMPTY_COMMIT_ID = '0' * 40
57 57
58 58
59 59 @dataclasses.dataclass
60 60 class Reference:
61 61 type: str
62 62 name: str
63 63 commit_id: str
64 64
65 65 def __iter__(self):
66 66 yield self.type
67 67 yield self.name
68 68 yield self.commit_id
69 69
70 70 @property
71 71 def branch(self):
72 72 if self.type == 'branch':
73 73 return self.name
74 74
75 75 @property
76 76 def bookmark(self):
77 77 if self.type == 'book':
78 78 return self.name
79 79
80 80 @property
81 81 def to_str(self):
82 82 return reference_to_unicode(self)
83 83
84 84 def asdict(self):
85 85 return dict(
86 86 type=self.type,
87 87 name=self.name,
88 88 commit_id=self.commit_id
89 89 )
90 90
91 91
92 92 def unicode_to_reference(raw: str):
93 93 """
94 94 Convert a unicode (or string) to a reference object.
95 95 If unicode evaluates to False it returns None.
96 96 """
97 97 if raw:
98 98 refs = raw.split(':')
99 99 return Reference(*refs)
100 100 else:
101 101 return None
102 102
103 103
104 104 def reference_to_unicode(ref: Reference):
105 105 """
106 106 Convert a reference object to unicode.
107 107 If reference is None it returns None.
108 108 """
109 109 if ref:
110 110 return ':'.join(ref)
111 111 else:
112 112 return None
113 113
114 114
115 115 class MergeFailureReason(object):
116 116 """
117 117 Enumeration with all the reasons why the server side merge could fail.
118 118
119 119 DO NOT change the number of the reasons, as they may be stored in the
120 120 database.
121 121
122 122 Changing the name of a reason is acceptable and encouraged to deprecate old
123 123 reasons.
124 124 """
125 125
126 126 # Everything went well.
127 127 NONE = 0
128 128
129 129 # An unexpected exception was raised. Check the logs for more details.
130 130 UNKNOWN = 1
131 131
132 132 # The merge was not successful, there are conflicts.
133 133 MERGE_FAILED = 2
134 134
135 135 # The merge succeeded but we could not push it to the target repository.
136 136 PUSH_FAILED = 3
137 137
138 138 # The specified target is not a head in the target repository.
139 139 TARGET_IS_NOT_HEAD = 4
140 140
141 141 # The source repository contains more branches than the target. Pushing
142 142 # the merge will create additional branches in the target.
143 143 HG_SOURCE_HAS_MORE_BRANCHES = 5
144 144
145 145 # The target reference has multiple heads. That does not allow to correctly
146 146 # identify the target location. This could only happen for mercurial
147 147 # branches.
148 148 HG_TARGET_HAS_MULTIPLE_HEADS = 6
149 149
150 150 # The target repository is locked
151 151 TARGET_IS_LOCKED = 7
152 152
153 153 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
154 154 # A involved commit could not be found.
155 155 _DEPRECATED_MISSING_COMMIT = 8
156 156
157 157 # The target repo reference is missing.
158 158 MISSING_TARGET_REF = 9
159 159
160 160 # The source repo reference is missing.
161 161 MISSING_SOURCE_REF = 10
162 162
163 163 # The merge was not successful, there are conflicts related to sub
164 164 # repositories.
165 165 SUBREPO_MERGE_FAILED = 11
166 166
167 167
168 168 class UpdateFailureReason(object):
169 169 """
170 170 Enumeration with all the reasons why the pull request update could fail.
171 171
172 172 DO NOT change the number of the reasons, as they may be stored in the
173 173 database.
174 174
175 175 Changing the name of a reason is acceptable and encouraged to deprecate old
176 176 reasons.
177 177 """
178 178
179 179 # Everything went well.
180 180 NONE = 0
181 181
182 182 # An unexpected exception was raised. Check the logs for more details.
183 183 UNKNOWN = 1
184 184
185 185 # The pull request is up to date.
186 186 NO_CHANGE = 2
187 187
188 188 # The pull request has a reference type that is not supported for update.
189 189 WRONG_REF_TYPE = 3
190 190
191 191 # Update failed because the target reference is missing.
192 192 MISSING_TARGET_REF = 4
193 193
194 194 # Update failed because the source reference is missing.
195 195 MISSING_SOURCE_REF = 5
196 196
197 197
198 198 class MergeResponse(object):
199 199
200 200 # uses .format(**metadata) for variables
201 201 MERGE_STATUS_MESSAGES = {
202 202 MergeFailureReason.NONE: lazy_ugettext(
203 203 'This pull request can be automatically merged.'),
204 204 MergeFailureReason.UNKNOWN: lazy_ugettext(
205 205 'This pull request cannot be merged because of an unhandled exception. '
206 206 '{exception}'),
207 207 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
208 208 'This pull request cannot be merged because of merge conflicts. {unresolved_files}'),
209 209 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
210 210 'This pull request could not be merged because push to '
211 211 'target:`{target}@{merge_commit}` failed.'),
212 212 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
213 213 'This pull request cannot be merged because the target '
214 214 '`{target_ref.name}` is not a head.'),
215 215 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
216 216 'This pull request cannot be merged because the source contains '
217 217 'more branches than the target.'),
218 218 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
219 219 'This pull request cannot be merged because the target `{target_ref.name}` '
220 220 'has multiple heads: `{heads}`.'),
221 221 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
222 222 'This pull request cannot be merged because the target repository is '
223 223 'locked by {locked_by}.'),
224 224
225 225 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
226 226 'This pull request cannot be merged because the target '
227 227 'reference `{target_ref.name}` is missing.'),
228 228 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
229 229 'This pull request cannot be merged because the source '
230 230 'reference `{source_ref.name}` is missing.'),
231 231 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
232 232 'This pull request cannot be merged because of conflicts related '
233 233 'to sub repositories.'),
234 234
235 235 # Deprecations
236 236 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
237 237 'This pull request cannot be merged because the target or the '
238 238 'source reference is missing.'),
239 239
240 240 }
241 241
242 242 def __init__(self, possible, executed, merge_ref, failure_reason, metadata=None):
243 243 self.possible = possible
244 244 self.executed = executed
245 245 self.merge_ref = merge_ref
246 246 self.failure_reason = failure_reason
247 247 self.metadata = metadata or {}
248 248
249 249 def __repr__(self):
250 return '<MergeResponse:{} {}>'.format(self.label, self.failure_reason)
250 return f'<MergeResponse:{self.label} {self.failure_reason}>'
251 251
252 252 def __eq__(self, other):
253 253 same_instance = isinstance(other, self.__class__)
254 254 return same_instance \
255 255 and self.possible == other.possible \
256 256 and self.executed == other.executed \
257 257 and self.failure_reason == other.failure_reason
258 258
259 259 @property
260 260 def label(self):
261 261 label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if
262 262 not k.startswith('_'))
263 263 return label_dict.get(self.failure_reason)
264 264
265 265 @property
266 266 def merge_status_message(self):
267 267 """
268 268 Return a human friendly error message for the given merge status code.
269 269 """
270 270 msg = safe_str(self.MERGE_STATUS_MESSAGES[self.failure_reason])
271 271
272 272 try:
273 273 return msg.format(**self.metadata)
274 274 except Exception:
275 275 log.exception('Failed to format %s message', self)
276 276 return msg
277 277
278 278 def asdict(self):
279 279 data = {}
280 280 for k in ['possible', 'executed', 'merge_ref', 'failure_reason',
281 281 'merge_status_message']:
282 282 data[k] = getattr(self, k)
283 283 return data
284 284
285 285
286 286 class TargetRefMissing(ValueError):
287 287 pass
288 288
289 289
290 290 class SourceRefMissing(ValueError):
291 291 pass
292 292
293 293
294 294 class BaseRepository(object):
295 295 """
296 296 Base Repository for final backends
297 297
298 298 .. attribute:: DEFAULT_BRANCH_NAME
299 299
300 300 name of default branch (i.e. "trunk" for svn, "master" for git etc.
301 301
302 302 .. attribute:: commit_ids
303 303
304 304 list of all available commit ids, in ascending order
305 305
306 306 .. attribute:: path
307 307
308 308 absolute path to the repository
309 309
310 310 .. attribute:: bookmarks
311 311
312 312 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
313 313 there are no bookmarks or the backend implementation does not support
314 314 bookmarks.
315 315
316 316 .. attribute:: tags
317 317
318 318 Mapping from name to :term:`Commit ID` of the tag.
319 319
320 320 """
321 321
322 322 DEFAULT_BRANCH_NAME = None
323 DEFAULT_CONTACT = u"Unknown"
324 DEFAULT_DESCRIPTION = u"unknown"
323 DEFAULT_CONTACT = "Unknown"
324 DEFAULT_DESCRIPTION = "unknown"
325 325 EMPTY_COMMIT_ID = '0' * 40
326 326 COMMIT_ID_PAT = re.compile(r'[0-9a-fA-F]{40}')
327 327
328 328 path = None
329 329
330 330 _is_empty = None
331 331 _commit_ids = {}
332 332
333 333 def __init__(self, repo_path, config=None, create=False, **kwargs):
334 334 """
335 335 Initializes repository. Raises RepositoryError if repository could
336 336 not be find at the given ``repo_path`` or directory at ``repo_path``
337 337 exists and ``create`` is set to True.
338 338
339 339 :param repo_path: local path of the repository
340 340 :param config: repository configuration
341 341 :param create=False: if set to True, would try to create repository.
342 342 :param src_url=None: if set, should be proper url from which repository
343 343 would be cloned; requires ``create`` parameter to be set to True -
344 344 raises RepositoryError if src_url is set and create evaluates to
345 345 False
346 346 """
347 347 raise NotImplementedError
348 348
349 349 def __repr__(self):
350 return '<%s at %s>' % (self.__class__.__name__, self.path)
350 return '<{} at {}>'.format(self.__class__.__name__, self.path)
351 351
352 352 def __len__(self):
353 353 return self.count()
354 354
355 355 def __eq__(self, other):
356 356 same_instance = isinstance(other, self.__class__)
357 357 return same_instance and other.path == self.path
358 358
359 359 def __ne__(self, other):
360 360 return not self.__eq__(other)
361 361
362 362 def get_create_shadow_cache_pr_path(self, db_repo):
363 363 path = db_repo.cached_diffs_dir
364 364 if not os.path.exists(path):
365 365 os.makedirs(path, 0o755)
366 366 return path
367 367
368 368 @classmethod
369 369 def get_default_config(cls, default=None):
370 370 config = Config()
371 371 if default and isinstance(default, list):
372 372 for section, key, val in default:
373 373 config.set(section, key, val)
374 374 return config
375 375
376 376 @LazyProperty
377 377 def _remote(self):
378 378 raise NotImplementedError
379 379
380 380 def _heads(self, branch=None):
381 381 return []
382 382
383 383 @LazyProperty
384 384 def EMPTY_COMMIT(self):
385 385 return EmptyCommit(self.EMPTY_COMMIT_ID)
386 386
387 387 @LazyProperty
388 388 def alias(self):
389 389 for k, v in settings.BACKENDS.items():
390 390 if v.split('.')[-1] == str(self.__class__.__name__):
391 391 return k
392 392
393 393 @LazyProperty
394 394 def name(self):
395 395 return safe_str(os.path.basename(self.path))
396 396
397 397 @LazyProperty
398 398 def description(self):
399 399 raise NotImplementedError
400 400
401 401 def refs(self):
402 402 """
403 403 returns a `dict` with branches, bookmarks, tags, and closed_branches
404 404 for this repository
405 405 """
406 406 return dict(
407 407 branches=self.branches,
408 408 branches_closed=self.branches_closed,
409 409 tags=self.tags,
410 410 bookmarks=self.bookmarks
411 411 )
412 412
413 413 @LazyProperty
414 414 def branches(self):
415 415 """
416 416 A `dict` which maps branch names to commit ids.
417 417 """
418 418 raise NotImplementedError
419 419
420 420 @LazyProperty
421 421 def branches_closed(self):
422 422 """
423 423 A `dict` which maps tags names to commit ids.
424 424 """
425 425 raise NotImplementedError
426 426
427 427 @LazyProperty
428 428 def bookmarks(self):
429 429 """
430 430 A `dict` which maps tags names to commit ids.
431 431 """
432 432 raise NotImplementedError
433 433
434 434 @LazyProperty
435 435 def tags(self):
436 436 """
437 437 A `dict` which maps tags names to commit ids.
438 438 """
439 439 raise NotImplementedError
440 440
441 441 @LazyProperty
442 442 def size(self):
443 443 """
444 444 Returns combined size in bytes for all repository files
445 445 """
446 446 tip = self.get_commit()
447 447 return tip.size
448 448
449 449 def size_at_commit(self, commit_id):
450 450 commit = self.get_commit(commit_id)
451 451 return commit.size
452 452
453 453 def _check_for_empty(self):
454 454 no_commits = len(self._commit_ids) == 0
455 455 if no_commits:
456 456 # check on remote to be sure
457 457 return self._remote.is_empty()
458 458 else:
459 459 return False
460 460
461 461 def is_empty(self):
462 462 if rhodecode.is_test:
463 463 return self._check_for_empty()
464 464
465 465 if self._is_empty is None:
466 466 # cache empty for production, but not tests
467 467 self._is_empty = self._check_for_empty()
468 468
469 469 return self._is_empty
470 470
471 471 @staticmethod
472 472 def check_url(url, config):
473 473 """
474 474 Function will check given url and try to verify if it's a valid
475 475 link.
476 476 """
477 477 raise NotImplementedError
478 478
479 479 @staticmethod
480 480 def is_valid_repository(path):
481 481 """
482 482 Check if given `path` contains a valid repository of this backend
483 483 """
484 484 raise NotImplementedError
485 485
486 486 # ==========================================================================
487 487 # COMMITS
488 488 # ==========================================================================
489 489
490 490 @CachedProperty
491 491 def commit_ids(self):
492 492 raise NotImplementedError
493 493
494 494 def append_commit_id(self, commit_id):
495 495 if commit_id not in self.commit_ids:
496 496 self._rebuild_cache(self.commit_ids + [commit_id])
497 497
498 498 # clear cache
499 499 self._invalidate_prop_cache('commit_ids')
500 500 self._is_empty = False
501 501
502 502 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
503 503 translate_tag=None, maybe_unreachable=False, reference_obj=None):
504 504 """
505 505 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
506 506 are both None, most recent commit is returned.
507 507
508 508 :param pre_load: Optional. List of commit attributes to load.
509 509
510 510 :raises ``EmptyRepositoryError``: if there are no commits
511 511 """
512 512 raise NotImplementedError
513 513
514 514 def __iter__(self):
515 515 for commit_id in self.commit_ids:
516 516 yield self.get_commit(commit_id=commit_id)
517 517
518 518 def get_commits(
519 519 self, start_id=None, end_id=None, start_date=None, end_date=None,
520 520 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
521 521 """
522 522 Returns iterator of `BaseCommit` objects from start to end
523 523 not inclusive. This should behave just like a list, ie. end is not
524 524 inclusive.
525 525
526 526 :param start_id: None or str, must be a valid commit id
527 527 :param end_id: None or str, must be a valid commit id
528 528 :param start_date:
529 529 :param end_date:
530 530 :param branch_name:
531 531 :param show_hidden:
532 532 :param pre_load:
533 533 :param translate_tags:
534 534 """
535 535 raise NotImplementedError
536 536
537 537 def __getitem__(self, key):
538 538 """
539 539 Allows index based access to the commit objects of this repository.
540 540 """
541 541 pre_load = ["author", "branch", "date", "message", "parents"]
542 542 if isinstance(key, slice):
543 543 return self._get_range(key, pre_load)
544 544 return self.get_commit(commit_idx=key, pre_load=pre_load)
545 545
546 546 def _get_range(self, slice_obj, pre_load):
547 547 for commit_id in self.commit_ids.__getitem__(slice_obj):
548 548 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
549 549
550 550 def count(self):
551 551 return len(self.commit_ids)
552 552
553 553 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
554 554 """
555 555 Creates and returns a tag for the given ``commit_id``.
556 556
557 557 :param name: name for new tag
558 558 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
559 559 :param commit_id: commit id for which new tag would be created
560 560 :param message: message of the tag's commit
561 561 :param date: date of tag's commit
562 562
563 563 :raises TagAlreadyExistError: if tag with same name already exists
564 564 """
565 565 raise NotImplementedError
566 566
567 567 def remove_tag(self, name, user, message=None, date=None):
568 568 """
569 569 Removes tag with the given ``name``.
570 570
571 571 :param name: name of the tag to be removed
572 572 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
573 573 :param message: message of the tag's removal commit
574 574 :param date: date of tag's removal commit
575 575
576 576 :raises TagDoesNotExistError: if tag with given name does not exists
577 577 """
578 578 raise NotImplementedError
579 579
580 580 def get_diff(
581 581 self, commit1, commit2, path=None, ignore_whitespace=False,
582 582 context=3, path1=None):
583 583 """
584 584 Returns (git like) *diff*, as plain text. Shows changes introduced by
585 585 `commit2` since `commit1`.
586 586
587 587 :param commit1: Entry point from which diff is shown. Can be
588 588 ``self.EMPTY_COMMIT`` - in this case, patch showing all
589 589 the changes since empty state of the repository until `commit2`
590 590 :param commit2: Until which commit changes should be shown.
591 591 :param path: Can be set to a path of a file to create a diff of that
592 592 file. If `path1` is also set, this value is only associated to
593 593 `commit2`.
594 594 :param ignore_whitespace: If set to ``True``, would not show whitespace
595 595 changes. Defaults to ``False``.
596 596 :param context: How many lines before/after changed lines should be
597 597 shown. Defaults to ``3``.
598 598 :param path1: Can be set to a path to associate with `commit1`. This
599 599 parameter works only for backends which support diff generation for
600 600 different paths. Other backends will raise a `ValueError` if `path1`
601 601 is set and has a different value than `path`.
602 602 :param file_path: filter this diff by given path pattern
603 603 """
604 604 raise NotImplementedError
605 605
606 606 def strip(self, commit_id, branch=None):
607 607 """
608 608 Strip given commit_id from the repository
609 609 """
610 610 raise NotImplementedError
611 611
612 612 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
613 613 """
614 614 Return a latest common ancestor commit if one exists for this repo
615 615 `commit_id1` vs `commit_id2` from `repo2`.
616 616
617 617 :param commit_id1: Commit it from this repository to use as a
618 618 target for the comparison.
619 619 :param commit_id2: Source commit id to use for comparison.
620 620 :param repo2: Source repository to use for comparison.
621 621 """
622 622 raise NotImplementedError
623 623
624 624 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
625 625 """
626 626 Compare this repository's revision `commit_id1` with `commit_id2`.
627 627
628 628 Returns a tuple(commits, ancestor) that would be merged from
629 629 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
630 630 will be returned as ancestor.
631 631
632 632 :param commit_id1: Commit it from this repository to use as a
633 633 target for the comparison.
634 634 :param commit_id2: Source commit id to use for comparison.
635 635 :param repo2: Source repository to use for comparison.
636 636 :param merge: If set to ``True`` will do a merge compare which also
637 637 returns the common ancestor.
638 638 :param pre_load: Optional. List of commit attributes to load.
639 639 """
640 640 raise NotImplementedError
641 641
642 642 def merge(self, repo_id, workspace_id, target_ref, source_repo, source_ref,
643 643 user_name='', user_email='', message='', dry_run=False,
644 644 use_rebase=False, close_branch=False):
645 645 """
646 646 Merge the revisions specified in `source_ref` from `source_repo`
647 647 onto the `target_ref` of this repository.
648 648
649 649 `source_ref` and `target_ref` are named tupls with the following
650 650 fields `type`, `name` and `commit_id`.
651 651
652 652 Returns a MergeResponse named tuple with the following fields
653 653 'possible', 'executed', 'source_commit', 'target_commit',
654 654 'merge_commit'.
655 655
656 656 :param repo_id: `repo_id` target repo id.
657 657 :param workspace_id: `workspace_id` unique identifier.
658 658 :param target_ref: `target_ref` points to the commit on top of which
659 659 the `source_ref` should be merged.
660 660 :param source_repo: The repository that contains the commits to be
661 661 merged.
662 662 :param source_ref: `source_ref` points to the topmost commit from
663 663 the `source_repo` which should be merged.
664 664 :param user_name: Merge commit `user_name`.
665 665 :param user_email: Merge commit `user_email`.
666 666 :param message: Merge commit `message`.
667 667 :param dry_run: If `True` the merge will not take place.
668 668 :param use_rebase: If `True` commits from the source will be rebased
669 669 on top of the target instead of being merged.
670 670 :param close_branch: If `True` branch will be close before merging it
671 671 """
672 672 if dry_run:
673 673 message = message or settings.MERGE_DRY_RUN_MESSAGE
674 674 user_email = user_email or settings.MERGE_DRY_RUN_EMAIL
675 675 user_name = user_name or settings.MERGE_DRY_RUN_USER
676 676 else:
677 677 if not user_name:
678 678 raise ValueError('user_name cannot be empty')
679 679 if not user_email:
680 680 raise ValueError('user_email cannot be empty')
681 681 if not message:
682 682 raise ValueError('message cannot be empty')
683 683
684 684 try:
685 685 return self._merge_repo(
686 686 repo_id, workspace_id, target_ref, source_repo,
687 687 source_ref, message, user_name, user_email, dry_run=dry_run,
688 688 use_rebase=use_rebase, close_branch=close_branch)
689 689 except RepositoryError as exc:
690 690 log.exception('Unexpected failure when running merge, dry-run=%s', dry_run)
691 691 return MergeResponse(
692 692 False, False, None, MergeFailureReason.UNKNOWN,
693 693 metadata={'exception': str(exc)})
694 694
695 695 def _merge_repo(self, repo_id, workspace_id, target_ref,
696 696 source_repo, source_ref, merge_message,
697 697 merger_name, merger_email, dry_run=False,
698 698 use_rebase=False, close_branch=False):
699 699 """Internal implementation of merge."""
700 700 raise NotImplementedError
701 701
702 702 def _maybe_prepare_merge_workspace(
703 703 self, repo_id, workspace_id, target_ref, source_ref):
704 704 """
705 705 Create the merge workspace.
706 706
707 707 :param workspace_id: `workspace_id` unique identifier.
708 708 """
709 709 raise NotImplementedError
710 710
711 711 @classmethod
712 712 def _get_legacy_shadow_repository_path(cls, repo_path, workspace_id):
713 713 """
714 714 Legacy version that was used before. We still need it for
715 715 backward compat
716 716 """
717 717 return os.path.join(
718 718 os.path.dirname(repo_path),
719 '.__shadow_%s_%s' % (os.path.basename(repo_path), workspace_id))
719 '.__shadow_{}_{}'.format(os.path.basename(repo_path), workspace_id))
720 720
721 721 @classmethod
722 722 def _get_shadow_repository_path(cls, repo_path, repo_id, workspace_id):
723 723 # The name of the shadow repository must start with '.', so it is
724 724 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
725 725 legacy_repository_path = cls._get_legacy_shadow_repository_path(repo_path, workspace_id)
726 726 if os.path.exists(legacy_repository_path):
727 727 return legacy_repository_path
728 728 else:
729 729 return os.path.join(
730 730 os.path.dirname(repo_path),
731 '.__shadow_repo_%s_%s' % (repo_id, workspace_id))
731 '.__shadow_repo_{}_{}'.format(repo_id, workspace_id))
732 732
733 733 def cleanup_merge_workspace(self, repo_id, workspace_id):
734 734 """
735 735 Remove merge workspace.
736 736
737 737 This function MUST not fail in case there is no workspace associated to
738 738 the given `workspace_id`.
739 739
740 740 :param workspace_id: `workspace_id` unique identifier.
741 741 """
742 742 shadow_repository_path = self._get_shadow_repository_path(
743 743 self.path, repo_id, workspace_id)
744 744 shadow_repository_path_del = '{}.{}.delete'.format(
745 745 shadow_repository_path, time.time())
746 746
747 747 # move the shadow repo, so it never conflicts with the one used.
748 748 # we use this method because shutil.rmtree had some edge case problems
749 749 # removing symlinked repositories
750 750 if not os.path.isdir(shadow_repository_path):
751 751 return
752 752
753 753 shutil.move(shadow_repository_path, shadow_repository_path_del)
754 754 try:
755 755 shutil.rmtree(shadow_repository_path_del, ignore_errors=False)
756 756 except Exception:
757 757 log.exception('Failed to gracefully remove shadow repo under %s',
758 758 shadow_repository_path_del)
759 759 shutil.rmtree(shadow_repository_path_del, ignore_errors=True)
760 760
761 761 # ========== #
762 762 # COMMIT API #
763 763 # ========== #
764 764
765 765 @LazyProperty
766 766 def in_memory_commit(self):
767 767 """
768 768 Returns :class:`InMemoryCommit` object for this repository.
769 769 """
770 770 raise NotImplementedError
771 771
772 772 # ======================== #
773 773 # UTILITIES FOR SUBCLASSES #
774 774 # ======================== #
775 775
776 776 def _validate_diff_commits(self, commit1, commit2):
777 777 """
778 778 Validates that the given commits are related to this repository.
779 779
780 780 Intended as a utility for sub classes to have a consistent validation
781 781 of input parameters in methods like :meth:`get_diff`.
782 782 """
783 783 self._validate_commit(commit1)
784 784 self._validate_commit(commit2)
785 785 if (isinstance(commit1, EmptyCommit) and
786 786 isinstance(commit2, EmptyCommit)):
787 787 raise ValueError("Cannot compare two empty commits")
788 788
789 789 def _validate_commit(self, commit):
790 790 if not isinstance(commit, BaseCommit):
791 791 raise TypeError(
792 792 "%s is not of type BaseCommit" % repr(commit))
793 793 if commit.repository != self and not isinstance(commit, EmptyCommit):
794 794 raise ValueError(
795 795 "Commit %s must be a valid commit from this repository %s, "
796 796 "related to this repository instead %s." %
797 797 (commit, self, commit.repository))
798 798
799 799 def _validate_commit_id(self, commit_id):
800 800 if not isinstance(commit_id, str):
801 801 raise TypeError(f"commit_id must be a string value got {type(commit_id)} instead")
802 802
803 803 def _validate_commit_idx(self, commit_idx):
804 804 if not isinstance(commit_idx, int):
805 805 raise TypeError(f"commit_idx must be a numeric value, got {type(commit_idx)}")
806 806
807 807 def _validate_branch_name(self, branch_name):
808 808 if branch_name and branch_name not in self.branches_all:
809 msg = ("Branch %s not found in %s" % (branch_name, self))
809 msg = ("Branch {} not found in {}".format(branch_name, self))
810 810 raise BranchDoesNotExistError(msg)
811 811
812 812 #
813 813 # Supporting deprecated API parts
814 814 # TODO: johbo: consider to move this into a mixin
815 815 #
816 816
817 817 @property
818 818 def EMPTY_CHANGESET(self):
819 819 warnings.warn(
820 820 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
821 821 return self.EMPTY_COMMIT_ID
822 822
823 823 @property
824 824 def revisions(self):
825 825 warnings.warn("Use commits attribute instead", DeprecationWarning)
826 826 return self.commit_ids
827 827
828 828 @revisions.setter
829 829 def revisions(self, value):
830 830 warnings.warn("Use commits attribute instead", DeprecationWarning)
831 831 self.commit_ids = value
832 832
833 833 def get_changeset(self, revision=None, pre_load=None):
834 834 warnings.warn("Use get_commit instead", DeprecationWarning)
835 835 commit_id = None
836 836 commit_idx = None
837 837 if isinstance(revision, str):
838 838 commit_id = revision
839 839 else:
840 840 commit_idx = revision
841 841 return self.get_commit(
842 842 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
843 843
844 844 def get_changesets(
845 845 self, start=None, end=None, start_date=None, end_date=None,
846 846 branch_name=None, pre_load=None):
847 847 warnings.warn("Use get_commits instead", DeprecationWarning)
848 848 start_id = self._revision_to_commit(start)
849 849 end_id = self._revision_to_commit(end)
850 850 return self.get_commits(
851 851 start_id=start_id, end_id=end_id, start_date=start_date,
852 852 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
853 853
854 854 def _revision_to_commit(self, revision):
855 855 """
856 856 Translates a revision to a commit_id
857 857
858 858 Helps to support the old changeset based API which allows to use
859 859 commit ids and commit indices interchangeable.
860 860 """
861 861 if revision is None:
862 862 return revision
863 863
864 864 if isinstance(revision, str):
865 865 commit_id = revision
866 866 else:
867 867 commit_id = self.commit_ids[revision]
868 868 return commit_id
869 869
870 870 @property
871 871 def in_memory_changeset(self):
872 872 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
873 873 return self.in_memory_commit
874 874
875 875 def get_path_permissions(self, username):
876 876 """
877 877 Returns a path permission checker or None if not supported
878 878
879 879 :param username: session user name
880 880 :return: an instance of BasePathPermissionChecker or None
881 881 """
882 882 return None
883 883
884 884 def install_hooks(self, force=False):
885 885 return self._remote.install_hooks(force)
886 886
887 887 def get_hooks_info(self):
888 888 return self._remote.get_hooks_info()
889 889
890 890 def vcsserver_invalidate_cache(self, delete=False):
891 891 return self._remote.vcsserver_invalidate_cache(delete)
892 892
893 893
894 894 class BaseCommit(object):
895 895 """
896 896 Each backend should implement it's commit representation.
897 897
898 898 **Attributes**
899 899
900 900 ``repository``
901 901 repository object within which commit exists
902 902
903 903 ``id``
904 904 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
905 905 just ``tip``.
906 906
907 907 ``raw_id``
908 908 raw commit representation (i.e. full 40 length sha for git
909 909 backend)
910 910
911 911 ``short_id``
912 912 shortened (if apply) version of ``raw_id``; it would be simple
913 913 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
914 914 as ``raw_id`` for subversion
915 915
916 916 ``idx``
917 917 commit index
918 918
919 919 ``files``
920 920 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
921 921
922 922 ``dirs``
923 923 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
924 924
925 925 ``nodes``
926 926 combined list of ``Node`` objects
927 927
928 928 ``author``
929 929 author of the commit, as unicode
930 930
931 931 ``message``
932 932 message of the commit, as unicode
933 933
934 934 ``parents``
935 935 list of parent commits
936 936
937 937 """
938 938 repository = None
939 939 branch = None
940 940
941 941 """
942 942 Depending on the backend this should be set to the branch name of the
943 943 commit. Backends not supporting branches on commits should leave this
944 944 value as ``None``.
945 945 """
946 946
947 947 _ARCHIVE_PREFIX_TEMPLATE = '{repo_name}-{short_id}'
948 948 """
949 949 This template is used to generate a default prefix for repository archives
950 950 if no prefix has been specified.
951 951 """
952 952
953 953 def __repr__(self):
954 954 return self.__str__()
955 955
956 956 def __str__(self):
957 957 return f'<{self.__class__.__name__} at {self.idx}:{self.short_id}>'
958 958
959 959 def __eq__(self, other):
960 960 same_instance = isinstance(other, self.__class__)
961 961 return same_instance and self.raw_id == other.raw_id
962 962
963 963 def __json__(self):
964 964 parents = []
965 965 try:
966 966 for parent in self.parents:
967 967 parents.append({'raw_id': parent.raw_id})
968 968 except NotImplementedError:
969 969 # empty commit doesn't have parents implemented
970 970 pass
971 971
972 972 return {
973 973 'short_id': self.short_id,
974 974 'raw_id': self.raw_id,
975 975 'revision': self.idx,
976 976 'message': self.message,
977 977 'date': self.date,
978 978 'author': self.author,
979 979 'parents': parents,
980 980 'branch': self.branch
981 981 }
982 982
983 983 def __getstate__(self):
984 984 d = self.__dict__.copy()
985 985 d.pop('_remote', None)
986 986 d.pop('repository', None)
987 987 return d
988 988
989 989 def get_remote(self):
990 990 return self._remote
991 991
992 992 def serialize(self):
993 993 return self.__json__()
994 994
995 995 def _get_refs(self):
996 996 return {
997 997 'branches': [self.branch] if self.branch else [],
998 998 'bookmarks': getattr(self, 'bookmarks', []),
999 999 'tags': self.tags
1000 1000 }
1001 1001
1002 1002 @LazyProperty
1003 1003 def last(self):
1004 1004 """
1005 1005 ``True`` if this is last commit in repository, ``False``
1006 1006 otherwise; trying to access this attribute while there is no
1007 1007 commits would raise `EmptyRepositoryError`
1008 1008 """
1009 1009 if self.repository is None:
1010 1010 raise CommitError("Cannot check if it's most recent commit")
1011 1011 return self.raw_id == self.repository.commit_ids[-1]
1012 1012
1013 1013 @LazyProperty
1014 1014 def parents(self):
1015 1015 """
1016 1016 Returns list of parent commits.
1017 1017 """
1018 1018 raise NotImplementedError
1019 1019
1020 1020 @LazyProperty
1021 1021 def first_parent(self):
1022 1022 """
1023 1023 Returns list of parent commits.
1024 1024 """
1025 1025 return self.parents[0] if self.parents else EmptyCommit()
1026 1026
1027 1027 @property
1028 1028 def merge(self):
1029 1029 """
1030 1030 Returns boolean if commit is a merge.
1031 1031 """
1032 1032 return len(self.parents) > 1
1033 1033
1034 1034 @LazyProperty
1035 1035 def children(self):
1036 1036 """
1037 1037 Returns list of child commits.
1038 1038 """
1039 1039 raise NotImplementedError
1040 1040
1041 1041 @LazyProperty
1042 1042 def id(self):
1043 1043 """
1044 1044 Returns string identifying this commit.
1045 1045 """
1046 1046 raise NotImplementedError
1047 1047
1048 1048 @LazyProperty
1049 1049 def raw_id(self):
1050 1050 """
1051 1051 Returns raw string identifying this commit.
1052 1052 """
1053 1053 raise NotImplementedError
1054 1054
1055 1055 @LazyProperty
1056 1056 def short_id(self):
1057 1057 """
1058 1058 Returns shortened version of ``raw_id`` attribute, as string,
1059 1059 identifying this commit, useful for presentation to users.
1060 1060 """
1061 1061 raise NotImplementedError
1062 1062
1063 1063 @LazyProperty
1064 1064 def idx(self):
1065 1065 """
1066 1066 Returns integer identifying this commit.
1067 1067 """
1068 1068 raise NotImplementedError
1069 1069
1070 1070 @LazyProperty
1071 1071 def committer(self):
1072 1072 """
1073 1073 Returns committer for this commit
1074 1074 """
1075 1075 raise NotImplementedError
1076 1076
1077 1077 @LazyProperty
1078 1078 def committer_name(self):
1079 1079 """
1080 1080 Returns committer name for this commit
1081 1081 """
1082 1082
1083 1083 return author_name(self.committer)
1084 1084
1085 1085 @LazyProperty
1086 1086 def committer_email(self):
1087 1087 """
1088 1088 Returns committer email address for this commit
1089 1089 """
1090 1090
1091 1091 return author_email(self.committer)
1092 1092
1093 1093 @LazyProperty
1094 1094 def author(self):
1095 1095 """
1096 1096 Returns author for this commit
1097 1097 """
1098 1098
1099 1099 raise NotImplementedError
1100 1100
1101 1101 @LazyProperty
1102 1102 def author_name(self):
1103 1103 """
1104 1104 Returns author name for this commit
1105 1105 """
1106 1106
1107 1107 return author_name(self.author)
1108 1108
1109 1109 @LazyProperty
1110 1110 def author_email(self):
1111 1111 """
1112 1112 Returns author email address for this commit
1113 1113 """
1114 1114
1115 1115 return author_email(self.author)
1116 1116
1117 1117 def get_file_mode(self, path: bytes):
1118 1118 """
1119 1119 Returns stat mode of the file at `path`.
1120 1120 """
1121 1121 raise NotImplementedError
1122 1122
1123 1123 def is_link(self, path):
1124 1124 """
1125 1125 Returns ``True`` if given `path` is a symlink
1126 1126 """
1127 1127 raise NotImplementedError
1128 1128
1129 1129 def is_node_binary(self, path):
1130 1130 """
1131 1131 Returns ``True`` is given path is a binary file
1132 1132 """
1133 1133 raise NotImplementedError
1134 1134
1135 1135 def node_md5_hash(self, path):
1136 1136 """
1137 1137 Returns md5 hash of a node data
1138 1138 """
1139 1139 raise NotImplementedError
1140 1140
1141 1141 def get_file_content(self, path) -> bytes:
1142 1142 """
1143 1143 Returns content of the file at the given `path`.
1144 1144 """
1145 1145 raise NotImplementedError
1146 1146
1147 1147 def get_file_content_streamed(self, path):
1148 1148 """
1149 1149 returns a streaming response from vcsserver with file content
1150 1150 """
1151 1151 raise NotImplementedError
1152 1152
1153 1153 def get_file_size(self, path):
1154 1154 """
1155 1155 Returns size of the file at the given `path`.
1156 1156 """
1157 1157 raise NotImplementedError
1158 1158
1159 1159 def get_path_commit(self, path, pre_load=None):
1160 1160 """
1161 1161 Returns last commit of the file at the given `path`.
1162 1162
1163 1163 :param pre_load: Optional. List of commit attributes to load.
1164 1164 """
1165 1165 commits = self.get_path_history(path, limit=1, pre_load=pre_load)
1166 1166 if not commits:
1167 1167 raise RepositoryError(
1168 1168 'Failed to fetch history for path {}. '
1169 1169 'Please check if such path exists in your repository'.format(
1170 1170 path))
1171 1171 return commits[0]
1172 1172
1173 1173 def get_path_history(self, path, limit=None, pre_load=None):
1174 1174 """
1175 1175 Returns history of file as reversed list of :class:`BaseCommit`
1176 1176 objects for which file at given `path` has been modified.
1177 1177
1178 1178 :param limit: Optional. Allows to limit the size of the returned
1179 1179 history. This is intended as a hint to the underlying backend, so
1180 1180 that it can apply optimizations depending on the limit.
1181 1181 :param pre_load: Optional. List of commit attributes to load.
1182 1182 """
1183 1183 raise NotImplementedError
1184 1184
1185 1185 def get_file_annotate(self, path, pre_load=None):
1186 1186 """
1187 1187 Returns a generator of four element tuples with
1188 1188 lineno, sha, commit lazy loader and line
1189 1189
1190 1190 :param pre_load: Optional. List of commit attributes to load.
1191 1191 """
1192 1192 raise NotImplementedError
1193 1193
1194 1194 def get_nodes(self, path, pre_load=None):
1195 1195 """
1196 1196 Returns combined ``DirNode`` and ``FileNode`` objects list representing
1197 1197 state of commit at the given ``path``.
1198 1198
1199 1199 :raises ``CommitError``: if node at the given ``path`` is not
1200 1200 instance of ``DirNode``
1201 1201 """
1202 1202 raise NotImplementedError
1203 1203
1204 1204 def get_node(self, path):
1205 1205 """
1206 1206 Returns ``Node`` object from the given ``path``.
1207 1207
1208 1208 :raises ``NodeDoesNotExistError``: if there is no node at the given
1209 1209 ``path``
1210 1210 """
1211 1211 raise NotImplementedError
1212 1212
1213 1213 def get_largefile_node(self, path):
1214 1214 """
1215 1215 Returns the path to largefile from Mercurial/Git-lfs storage.
1216 1216 or None if it's not a largefile node
1217 1217 """
1218 1218 return None
1219 1219
1220 1220 def archive_repo(self, archive_name_key, kind='tgz', subrepos=None,
1221 1221 archive_dir_name=None, write_metadata=False, mtime=None,
1222 1222 archive_at_path='/', cache_config=None):
1223 1223 """
1224 1224 Creates an archive containing the contents of the repository.
1225 1225
1226 1226 :param archive_name_key: unique key under this archive should be generated
1227 1227 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1228 1228 :param archive_dir_name: name of root directory in archive.
1229 1229 Default is repository name and commit's short_id joined with dash:
1230 1230 ``"{repo_name}-{short_id}"``.
1231 1231 :param write_metadata: write a metadata file into archive.
1232 1232 :param mtime: custom modification time for archive creation, defaults
1233 1233 to time.time() if not given.
1234 1234 :param archive_at_path: pack files at this path (default '/')
1235 1235 :param cache_config: config spec to send to vcsserver to configure the backend to store files
1236 1236
1237 1237 :raise VCSError: If prefix has a problem.
1238 1238 """
1239 1239 cache_config = cache_config or {}
1240 1240 allowed_kinds = [x[0] for x in settings.ARCHIVE_SPECS]
1241 1241 if kind not in allowed_kinds:
1242 1242 raise ImproperArchiveTypeError(
1243 1243 'Archive kind (%s) not supported use one of %s' %
1244 1244 (kind, allowed_kinds))
1245 1245
1246 1246 archive_dir_name = self._validate_archive_prefix(archive_dir_name)
1247 1247 mtime = mtime is not None or time.mktime(self.date.timetuple())
1248 1248 commit_id = self.raw_id
1249 1249
1250 1250 return self.repository._remote.archive_repo(
1251 1251 archive_name_key, kind, mtime, archive_at_path,
1252 1252 archive_dir_name, commit_id, cache_config)
1253 1253
1254 1254 def _validate_archive_prefix(self, archive_dir_name):
1255 1255 if archive_dir_name is None:
1256 1256 archive_dir_name = self._ARCHIVE_PREFIX_TEMPLATE.format(
1257 1257 repo_name=safe_str(self.repository.name),
1258 1258 short_id=self.short_id)
1259 1259 elif not isinstance(archive_dir_name, str):
1260 1260 raise ValueError(f"archive_dir_name is not str object but: {type(archive_dir_name)}")
1261 1261 elif archive_dir_name.startswith('/'):
1262 1262 raise VCSError("Prefix cannot start with leading slash")
1263 1263 elif archive_dir_name.strip() == '':
1264 1264 raise VCSError("Prefix cannot be empty")
1265 1265 elif not archive_dir_name.isascii():
1266 1266 raise VCSError("Prefix cannot contain non ascii characters")
1267 1267 return archive_dir_name
1268 1268
1269 1269 @LazyProperty
1270 1270 def root(self):
1271 1271 """
1272 1272 Returns ``RootNode`` object for this commit.
1273 1273 """
1274 1274 return self.get_node('')
1275 1275
1276 1276 def next(self, branch=None):
1277 1277 """
1278 1278 Returns next commit from current, if branch is gives it will return
1279 1279 next commit belonging to this branch
1280 1280
1281 1281 :param branch: show commits within the given named branch
1282 1282 """
1283 1283 indexes = range(self.idx + 1, self.repository.count())
1284 1284 return self._find_next(indexes, branch)
1285 1285
1286 1286 def prev(self, branch=None):
1287 1287 """
1288 1288 Returns previous commit from current, if branch is gives it will
1289 1289 return previous commit belonging to this branch
1290 1290
1291 1291 :param branch: show commit within the given named branch
1292 1292 """
1293 1293 indexes = range(self.idx - 1, -1, -1)
1294 1294 return self._find_next(indexes, branch)
1295 1295
1296 1296 def _find_next(self, indexes, branch=None):
1297 1297 if branch and self.branch != branch:
1298 1298 raise VCSError('Branch option used on commit not belonging '
1299 1299 'to that branch')
1300 1300
1301 1301 for next_idx in indexes:
1302 1302 commit = self.repository.get_commit(commit_idx=next_idx)
1303 1303 if branch and branch != commit.branch:
1304 1304 continue
1305 1305 return commit
1306 1306 raise CommitDoesNotExistError
1307 1307
1308 1308 def diff(self, ignore_whitespace=True, context=3):
1309 1309 """
1310 1310 Returns a `Diff` object representing the change made by this commit.
1311 1311 """
1312 1312 parent = self.first_parent
1313 1313 diff = self.repository.get_diff(
1314 1314 parent, self,
1315 1315 ignore_whitespace=ignore_whitespace,
1316 1316 context=context)
1317 1317 return diff
1318 1318
1319 1319 @LazyProperty
1320 1320 def added(self):
1321 1321 """
1322 1322 Returns list of added ``FileNode`` objects.
1323 1323 """
1324 1324 raise NotImplementedError
1325 1325
1326 1326 @LazyProperty
1327 1327 def changed(self):
1328 1328 """
1329 1329 Returns list of modified ``FileNode`` objects.
1330 1330 """
1331 1331 raise NotImplementedError
1332 1332
1333 1333 @LazyProperty
1334 1334 def removed(self):
1335 1335 """
1336 1336 Returns list of removed ``FileNode`` objects.
1337 1337 """
1338 1338 raise NotImplementedError
1339 1339
1340 1340 @LazyProperty
1341 1341 def size(self):
1342 1342 """
1343 1343 Returns total number of bytes from contents of all filenodes.
1344 1344 """
1345 return sum((node.size for node in self.get_filenodes_generator()))
1345 return sum(node.size for node in self.get_filenodes_generator())
1346 1346
1347 1347 def walk(self, topurl=''):
1348 1348 """
1349 1349 Similar to os.walk method. Insted of filesystem it walks through
1350 1350 commit starting at given ``topurl``. Returns generator of tuples
1351 1351 (top_node, dirnodes, filenodes).
1352 1352 """
1353 1353 from rhodecode.lib.vcs.nodes import DirNode
1354 1354
1355 1355 if isinstance(topurl, DirNode):
1356 1356 top_node = topurl
1357 1357 else:
1358 1358 top_node = self.get_node(topurl)
1359 1359
1360 1360 has_default_pre_load = False
1361 1361 if isinstance(top_node, DirNode):
1362 1362 # used to inject as we walk same defaults as given top_node
1363 1363 default_pre_load = top_node.default_pre_load
1364 1364 has_default_pre_load = True
1365 1365
1366 1366 if not top_node.is_dir():
1367 1367 return
1368 1368 yield top_node, top_node.dirs, top_node.files
1369 1369 for dir_node in top_node.dirs:
1370 1370 if has_default_pre_load:
1371 1371 dir_node.default_pre_load = default_pre_load
1372 for tup in self.walk(dir_node):
1373 yield tup
1372 yield from self.walk(dir_node)
1374 1373
1375 1374 def get_filenodes_generator(self):
1376 1375 """
1377 1376 Returns generator that yields *all* file nodes.
1378 1377 """
1379 1378 for topnode, dirs, files in self.walk():
1380 for node in files:
1381 yield node
1379 yield from files
1382 1380
1383 1381 #
1384 1382 # Utilities for sub classes to support consistent behavior
1385 1383 #
1386 1384
1387 1385 def no_node_at_path(self, path):
1388 1386 return NodeDoesNotExistError(
1389 1387 f"There is no file nor directory at the given path: "
1390 1388 f"`{safe_str(path)}` at commit {self.short_id}")
1391 1389
1392 1390 def _fix_path(self, path: str) -> str:
1393 1391 """
1394 1392 Paths are stored without trailing slash so we need to get rid off it if
1395 1393 needed.
1396 1394 """
1397 1395 return safe_str(path).rstrip('/')
1398 1396
1399 1397 #
1400 1398 # Deprecated API based on changesets
1401 1399 #
1402 1400
1403 1401 @property
1404 1402 def revision(self):
1405 1403 warnings.warn("Use idx instead", DeprecationWarning)
1406 1404 return self.idx
1407 1405
1408 1406 @revision.setter
1409 1407 def revision(self, value):
1410 1408 warnings.warn("Use idx instead", DeprecationWarning)
1411 1409 self.idx = value
1412 1410
1413 1411 def get_file_changeset(self, path):
1414 1412 warnings.warn("Use get_path_commit instead", DeprecationWarning)
1415 1413 return self.get_path_commit(path)
1416 1414
1417 1415
1418 1416 class BaseChangesetClass(type):
1419 1417
1420 1418 def __instancecheck__(self, instance):
1421 1419 return isinstance(instance, BaseCommit)
1422 1420
1423 1421
1424 1422 class BaseChangeset(BaseCommit, metaclass=BaseChangesetClass):
1425 1423
1426 1424 def __new__(cls, *args, **kwargs):
1427 1425 warnings.warn(
1428 1426 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1429 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1427 return super().__new__(cls, *args, **kwargs)
1430 1428
1431 1429
1432 1430 class BaseInMemoryCommit(object):
1433 1431 """
1434 1432 Represents differences between repository's state (most recent head) and
1435 1433 changes made *in place*.
1436 1434
1437 1435 **Attributes**
1438 1436
1439 1437 ``repository``
1440 1438 repository object for this in-memory-commit
1441 1439
1442 1440 ``added``
1443 1441 list of ``FileNode`` objects marked as *added*
1444 1442
1445 1443 ``changed``
1446 1444 list of ``FileNode`` objects marked as *changed*
1447 1445
1448 1446 ``removed``
1449 1447 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1450 1448 *removed*
1451 1449
1452 1450 ``parents``
1453 1451 list of :class:`BaseCommit` instances representing parents of
1454 1452 in-memory commit. Should always be 2-element sequence.
1455 1453
1456 1454 """
1457 1455
1458 1456 def __init__(self, repository):
1459 1457 self.repository = repository
1460 1458 self.added = []
1461 1459 self.changed = []
1462 1460 self.removed = []
1463 1461 self.parents = []
1464 1462
1465 1463 def add(self, *filenodes):
1466 1464 """
1467 1465 Marks given ``FileNode`` objects as *to be committed*.
1468 1466
1469 1467 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1470 1468 latest commit
1471 1469 :raises ``NodeAlreadyAddedError``: if node with same path is already
1472 1470 marked as *added*
1473 1471 """
1474 1472 # Check if not already marked as *added* first
1475 1473 for node in filenodes:
1476 1474 if node.path in (n.path for n in self.added):
1477 1475 raise NodeAlreadyAddedError(
1478 1476 "Such FileNode %s is already marked for addition"
1479 1477 % node.path)
1480 1478 for node in filenodes:
1481 1479 self.added.append(node)
1482 1480
1483 1481 def change(self, *filenodes):
1484 1482 """
1485 1483 Marks given ``FileNode`` objects to be *changed* in next commit.
1486 1484
1487 1485 :raises ``EmptyRepositoryError``: if there are no commits yet
1488 1486 :raises ``NodeAlreadyExistsError``: if node with same path is already
1489 1487 marked to be *changed*
1490 1488 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1491 1489 marked to be *removed*
1492 1490 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1493 1491 commit
1494 1492 :raises ``NodeNotChangedError``: if node hasn't really be changed
1495 1493 """
1496 1494 for node in filenodes:
1497 1495 if node.path in (n.path for n in self.removed):
1498 1496 raise NodeAlreadyRemovedError(
1499 1497 "Node at %s is already marked as removed" % node.path)
1500 1498 try:
1501 1499 self.repository.get_commit()
1502 1500 except EmptyRepositoryError:
1503 1501 raise EmptyRepositoryError(
1504 1502 "Nothing to change - try to *add* new nodes rather than "
1505 1503 "changing them")
1506 1504 for node in filenodes:
1507 1505 if node.path in (n.path for n in self.changed):
1508 1506 raise NodeAlreadyChangedError(
1509 1507 "Node at '%s' is already marked as changed" % node.path)
1510 1508 self.changed.append(node)
1511 1509
1512 1510 def remove(self, *filenodes):
1513 1511 """
1514 1512 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1515 1513 *removed* in next commit.
1516 1514
1517 1515 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1518 1516 be *removed*
1519 1517 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1520 1518 be *changed*
1521 1519 """
1522 1520 for node in filenodes:
1523 1521 if node.path in (n.path for n in self.removed):
1524 1522 raise NodeAlreadyRemovedError(
1525 1523 "Node is already marked to for removal at %s" % node.path)
1526 1524 if node.path in (n.path for n in self.changed):
1527 1525 raise NodeAlreadyChangedError(
1528 1526 "Node is already marked to be changed at %s" % node.path)
1529 1527 # We only mark node as *removed* - real removal is done by
1530 1528 # commit method
1531 1529 self.removed.append(node)
1532 1530
1533 1531 def reset(self):
1534 1532 """
1535 1533 Resets this instance to initial state (cleans ``added``, ``changed``
1536 1534 and ``removed`` lists).
1537 1535 """
1538 1536 self.added = []
1539 1537 self.changed = []
1540 1538 self.removed = []
1541 1539 self.parents = []
1542 1540
1543 1541 def get_ipaths(self):
1544 1542 """
1545 1543 Returns generator of paths from nodes marked as added, changed or
1546 1544 removed.
1547 1545 """
1548 1546 for node in itertools.chain(self.added, self.changed, self.removed):
1549 1547 yield node.path
1550 1548
1551 1549 def get_paths(self):
1552 1550 """
1553 1551 Returns list of paths from nodes marked as added, changed or removed.
1554 1552 """
1555 1553 return list(self.get_ipaths())
1556 1554
1557 1555 def check_integrity(self, parents=None):
1558 1556 """
1559 1557 Checks in-memory commit's integrity. Also, sets parents if not
1560 1558 already set.
1561 1559
1562 1560 :raises CommitError: if any error occurs (i.e.
1563 1561 ``NodeDoesNotExistError``).
1564 1562 """
1565 1563 if not self.parents:
1566 1564 parents = parents or []
1567 1565 if len(parents) == 0:
1568 1566 try:
1569 1567 parents = [self.repository.get_commit(), None]
1570 1568 except EmptyRepositoryError:
1571 1569 parents = [None, None]
1572 1570 elif len(parents) == 1:
1573 1571 parents += [None]
1574 1572 self.parents = parents
1575 1573
1576 1574 # Local parents, only if not None
1577 1575 parents = [p for p in self.parents if p]
1578 1576
1579 1577 # Check nodes marked as added
1580 1578 for p in parents:
1581 1579 for node in self.added:
1582 1580 try:
1583 1581 p.get_node(node.path)
1584 1582 except NodeDoesNotExistError:
1585 1583 pass
1586 1584 else:
1587 1585 raise NodeAlreadyExistsError(
1588 "Node `%s` already exists at %s" % (node.path, p))
1586 "Node `{}` already exists at {}".format(node.path, p))
1589 1587
1590 1588 # Check nodes marked as changed
1591 1589 missing = set(self.changed)
1592 1590 not_changed = set(self.changed)
1593 1591 if self.changed and not parents:
1594 1592 raise NodeDoesNotExistError(str(self.changed[0].path))
1595 1593 for p in parents:
1596 1594 for node in self.changed:
1597 1595 try:
1598 1596 old = p.get_node(node.path)
1599 1597 missing.remove(node)
1600 1598 # if content actually changed, remove node from not_changed
1601 1599 if old.content != node.content:
1602 1600 not_changed.remove(node)
1603 1601 except NodeDoesNotExistError:
1604 1602 pass
1605 1603 if self.changed and missing:
1606 1604 raise NodeDoesNotExistError(
1607 1605 "Node `%s` marked as modified but missing in parents: %s"
1608 1606 % (node.path, parents))
1609 1607
1610 1608 if self.changed and not_changed:
1611 1609 raise NodeNotChangedError(
1612 1610 "Node `%s` wasn't actually changed (parents: %s)"
1613 1611 % (not_changed.pop().path, parents))
1614 1612
1615 1613 # Check nodes marked as removed
1616 1614 if self.removed and not parents:
1617 1615 raise NodeDoesNotExistError(
1618 1616 "Cannot remove node at %s as there "
1619 1617 "were no parents specified" % self.removed[0].path)
1620 1618 really_removed = set()
1621 1619 for p in parents:
1622 1620 for node in self.removed:
1623 1621 try:
1624 1622 p.get_node(node.path)
1625 1623 really_removed.add(node)
1626 1624 except CommitError:
1627 1625 pass
1628 1626 not_removed = set(self.removed) - really_removed
1629 1627 if not_removed:
1630 1628 # TODO: johbo: This code branch does not seem to be covered
1631 1629 raise NodeDoesNotExistError(
1632 1630 "Cannot remove node at %s from "
1633 1631 "following parents: %s" % (not_removed, parents))
1634 1632
1635 1633 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
1636 1634 """
1637 1635 Performs in-memory commit (doesn't check workdir in any way) and
1638 1636 returns newly created :class:`BaseCommit`. Updates repository's
1639 1637 attribute `commits`.
1640 1638
1641 1639 .. note::
1642 1640
1643 1641 While overriding this method each backend's should call
1644 1642 ``self.check_integrity(parents)`` in the first place.
1645 1643
1646 1644 :param message: message of the commit
1647 1645 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1648 1646 :param parents: single parent or sequence of parents from which commit
1649 1647 would be derived
1650 1648 :param date: ``datetime.datetime`` instance. Defaults to
1651 1649 ``datetime.datetime.now()``.
1652 1650 :param branch: branch name, as string. If none given, default backend's
1653 1651 branch would be used.
1654 1652
1655 1653 :raises ``CommitError``: if any error occurs while committing
1656 1654 """
1657 1655 raise NotImplementedError
1658 1656
1659 1657
1660 1658 class BaseInMemoryChangesetClass(type):
1661 1659
1662 1660 def __instancecheck__(self, instance):
1663 1661 return isinstance(instance, BaseInMemoryCommit)
1664 1662
1665 1663
1666 1664 class BaseInMemoryChangeset(BaseInMemoryCommit, metaclass=BaseInMemoryChangesetClass):
1667 1665
1668 1666 def __new__(cls, *args, **kwargs):
1669 1667 warnings.warn(
1670 1668 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1671 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1669 return super().__new__(cls, *args, **kwargs)
1672 1670
1673 1671
1674 1672 class EmptyCommit(BaseCommit):
1675 1673 """
1676 1674 An dummy empty commit. It's possible to pass hash when creating
1677 1675 an EmptyCommit
1678 1676 """
1679 1677
1680 1678 def __init__(
1681 1679 self, commit_id=EMPTY_COMMIT_ID, repo=None, alias=None, idx=-1,
1682 1680 message='', author='', date=None):
1683 1681 self._empty_commit_id = commit_id
1684 1682 # TODO: johbo: Solve idx parameter, default value does not make
1685 1683 # too much sense
1686 1684 self.idx = idx
1687 1685 self.message = message
1688 1686 self.author = author
1689 1687 self.date = date or datetime.datetime.fromtimestamp(0)
1690 1688 self.repository = repo
1691 1689 self.alias = alias
1692 1690
1693 1691 @LazyProperty
1694 1692 def raw_id(self):
1695 1693 """
1696 1694 Returns raw string identifying this commit, useful for web
1697 1695 representation.
1698 1696 """
1699 1697
1700 1698 return self._empty_commit_id
1701 1699
1702 1700 @LazyProperty
1703 1701 def branch(self):
1704 1702 if self.alias:
1705 1703 from rhodecode.lib.vcs.backends import get_backend
1706 1704 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1707 1705
1708 1706 @LazyProperty
1709 1707 def short_id(self):
1710 1708 return self.raw_id[:12]
1711 1709
1712 1710 @LazyProperty
1713 1711 def id(self):
1714 1712 return self.raw_id
1715 1713
1716 1714 def get_path_commit(self, path, pre_load=None):
1717 1715 return self
1718 1716
1719 1717 def get_file_content(self, path) -> bytes:
1720 1718 return b''
1721 1719
1722 1720 def get_file_content_streamed(self, path):
1723 1721 yield self.get_file_content(path)
1724 1722
1725 1723 def get_file_size(self, path):
1726 1724 return 0
1727 1725
1728 1726
1729 1727 class EmptyChangesetClass(type):
1730 1728
1731 1729 def __instancecheck__(self, instance):
1732 1730 return isinstance(instance, EmptyCommit)
1733 1731
1734 1732
1735 1733 class EmptyChangeset(EmptyCommit, metaclass=EmptyChangesetClass):
1736 1734
1737 1735 def __new__(cls, *args, **kwargs):
1738 1736 warnings.warn(
1739 1737 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1740 1738 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1741 1739
1742 1740 def __init__(self, cs=EMPTY_COMMIT_ID, repo=None, requested_revision=None,
1743 1741 alias=None, revision=-1, message='', author='', date=None):
1744 1742 if requested_revision is not None:
1745 1743 warnings.warn(
1746 1744 "Parameter requested_revision not supported anymore",
1747 1745 DeprecationWarning)
1748 super(EmptyChangeset, self).__init__(
1746 super().__init__(
1749 1747 commit_id=cs, repo=repo, alias=alias, idx=revision,
1750 1748 message=message, author=author, date=date)
1751 1749
1752 1750 @property
1753 1751 def revision(self):
1754 1752 warnings.warn("Use idx instead", DeprecationWarning)
1755 1753 return self.idx
1756 1754
1757 1755 @revision.setter
1758 1756 def revision(self, value):
1759 1757 warnings.warn("Use idx instead", DeprecationWarning)
1760 1758 self.idx = value
1761 1759
1762 1760
1763 1761 class EmptyRepository(BaseRepository):
1764 1762 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1765 1763 pass
1766 1764
1767 1765 def get_diff(self, *args, **kwargs):
1768 1766 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1769 1767 return GitDiff(b'')
1770 1768
1771 1769
1772 1770 class CollectionGenerator(object):
1773 1771
1774 1772 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None, translate_tag=None):
1775 1773 self.repo = repo
1776 1774 self.commit_ids = commit_ids
1777 1775 self.collection_size = collection_size
1778 1776 self.pre_load = pre_load
1779 1777 self.translate_tag = translate_tag
1780 1778
1781 1779 def __len__(self):
1782 1780 if self.collection_size is not None:
1783 1781 return self.collection_size
1784 1782 return self.commit_ids.__len__()
1785 1783
1786 1784 def __iter__(self):
1787 1785 for commit_id in self.commit_ids:
1788 1786 # TODO: johbo: Mercurial passes in commit indices or commit ids
1789 1787 yield self._commit_factory(commit_id)
1790 1788
1791 1789 def _commit_factory(self, commit_id):
1792 1790 """
1793 1791 Allows backends to override the way commits are generated.
1794 1792 """
1795 1793 return self.repo.get_commit(
1796 1794 commit_id=commit_id, pre_load=self.pre_load,
1797 1795 translate_tag=self.translate_tag)
1798 1796
1799 1797 def __getitem__(self, key):
1800 1798 """Return either a single element by index, or a sliced collection."""
1801 1799
1802 1800 if isinstance(key, slice):
1803 1801 commit_ids = self.commit_ids[key.start:key.stop]
1804 1802
1805 1803 else:
1806 1804 # single item
1807 1805 commit_ids = self.commit_ids[key]
1808 1806
1809 1807 return self.__class__(
1810 1808 self.repo, commit_ids, pre_load=self.pre_load,
1811 1809 translate_tag=self.translate_tag)
1812 1810
1813 1811 def __repr__(self):
1814 1812 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1815 1813
1816 1814
1817 1815 class Config(object):
1818 1816 """
1819 1817 Represents the configuration for a repository.
1820 1818
1821 1819 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1822 1820 standard library. It implements only the needed subset.
1823 1821 """
1824 1822
1825 1823 def __init__(self):
1826 1824 self._values = {}
1827 1825
1828 1826 def copy(self):
1829 1827 clone = Config()
1830 1828 for section, values in self._values.items():
1831 1829 clone._values[section] = values.copy()
1832 1830 return clone
1833 1831
1834 1832 def __repr__(self):
1835 return '<Config(%s sections) at %s>' % (
1833 return '<Config({} sections) at {}>'.format(
1836 1834 len(self._values), hex(id(self)))
1837 1835
1838 1836 def items(self, section):
1839 1837 return self._values.get(section, {}).items()
1840 1838
1841 1839 def get(self, section, option):
1842 1840 return self._values.get(section, {}).get(option)
1843 1841
1844 1842 def set(self, section, option, value):
1845 1843 section_values = self._values.setdefault(section, {})
1846 1844 section_values[option] = value
1847 1845
1848 1846 def clear_section(self, section):
1849 1847 self._values[section] = {}
1850 1848
1851 1849 def serialize(self):
1852 1850 """
1853 1851 Creates a list of three tuples (section, key, value) representing
1854 1852 this config object.
1855 1853 """
1856 1854 items = []
1857 1855 for section in self._values:
1858 1856 for option, value in self._values[section].items():
1859 1857 items.append(
1860 1858 (safe_str(section), safe_str(option), safe_str(value)))
1861 1859 return items
1862 1860
1863 1861
1864 1862 class Diff(object):
1865 1863 """
1866 1864 Represents a diff result from a repository backend.
1867 1865
1868 1866 Subclasses have to provide a backend specific value for
1869 1867 :attr:`_header_re` and :attr:`_meta_re`.
1870 1868 """
1871 1869 _meta_re = None
1872 1870 _header_re: bytes = re.compile(br"")
1873 1871
1874 1872 def __init__(self, raw_diff: bytes):
1875 1873 if not isinstance(raw_diff, bytes):
1876 1874 raise Exception(f'raw_diff must be bytes - got {type(raw_diff)}')
1877 1875
1878 1876 self.raw = memoryview(raw_diff)
1879 1877
1880 1878 def get_header_re(self):
1881 1879 return self._header_re
1882 1880
1883 1881 def chunks(self):
1884 1882 """
1885 1883 split the diff in chunks of separate --git a/file b/file chunks
1886 1884 to make diffs consistent we must prepend with \n, and make sure
1887 1885 we can detect last chunk as this was also has special rule
1888 1886 """
1889 1887
1890 1888 diff_parts = (b'\n' + bytes(self.raw)).split(b'\ndiff --git')
1891 1889
1892 1890 chunks = diff_parts[1:]
1893 1891 total_chunks = len(chunks)
1894 1892
1895 1893 def diff_iter(_chunks):
1896 1894 for cur_chunk, chunk in enumerate(_chunks, start=1):
1897 1895 yield DiffChunk(chunk, self, cur_chunk == total_chunks)
1898 1896 return diff_iter(chunks)
1899 1897
1900 1898
1901 1899 class DiffChunk(object):
1902 1900
1903 1901 def __init__(self, chunk: bytes, diff_obj: Diff, is_last_chunk: bool):
1904 1902 self.diff_obj = diff_obj
1905 1903
1906 1904 # since we split by \ndiff --git that part is lost from original diff
1907 1905 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1908 1906 if not is_last_chunk:
1909 1907 chunk += b'\n'
1910 1908 header_re = self.diff_obj.get_header_re()
1911 1909 match = header_re.match(chunk)
1912 1910 self.header = match.groupdict()
1913 1911 self.diff = chunk[match.end():]
1914 1912 self.raw = chunk
1915 1913
1916 1914 @property
1917 1915 def header_as_str(self):
1918 1916 if self.header:
1919 1917 def safe_str_on_bytes(val):
1920 1918 if isinstance(val, bytes):
1921 1919 return safe_str(val)
1922 1920 return val
1923 1921 return {safe_str(k): safe_str_on_bytes(v) for k, v in self.header.items()}
1924 1922
1925 1923 def __repr__(self):
1926 1924 return f'DiffChunk({self.header_as_str})'
1927 1925
1928 1926
1929 1927 class BasePathPermissionChecker(object):
1930 1928
1931 1929 @staticmethod
1932 1930 def create_from_patterns(includes, excludes):
1933 1931 if includes and '*' in includes and not excludes:
1934 1932 return AllPathPermissionChecker()
1935 1933 elif excludes and '*' in excludes:
1936 1934 return NonePathPermissionChecker()
1937 1935 else:
1938 1936 return PatternPathPermissionChecker(includes, excludes)
1939 1937
1940 1938 @property
1941 1939 def has_full_access(self):
1942 1940 raise NotImplementedError()
1943 1941
1944 1942 def has_access(self, path):
1945 1943 raise NotImplementedError()
1946 1944
1947 1945
1948 1946 class AllPathPermissionChecker(BasePathPermissionChecker):
1949 1947
1950 1948 @property
1951 1949 def has_full_access(self):
1952 1950 return True
1953 1951
1954 1952 def has_access(self, path):
1955 1953 return True
1956 1954
1957 1955
1958 1956 class NonePathPermissionChecker(BasePathPermissionChecker):
1959 1957
1960 1958 @property
1961 1959 def has_full_access(self):
1962 1960 return False
1963 1961
1964 1962 def has_access(self, path):
1965 1963 return False
1966 1964
1967 1965
1968 1966 class PatternPathPermissionChecker(BasePathPermissionChecker):
1969 1967
1970 1968 def __init__(self, includes, excludes):
1971 1969 self.includes = includes
1972 1970 self.excludes = excludes
1973 1971 self.includes_re = [] if not includes else [
1974 1972 re.compile(fnmatch.translate(pattern)) for pattern in includes]
1975 1973 self.excludes_re = [] if not excludes else [
1976 1974 re.compile(fnmatch.translate(pattern)) for pattern in excludes]
1977 1975
1978 1976 @property
1979 1977 def has_full_access(self):
1980 1978 return '*' in self.includes and not self.excludes
1981 1979
1982 1980 def has_access(self, path):
1983 1981 for regex in self.excludes_re:
1984 1982 if regex.match(path):
1985 1983 return False
1986 1984 for regex in self.includes_re:
1987 1985 if regex.match(path):
1988 1986 return True
1989 1987 return False
@@ -1,56 +1,54 b''
1
2
3 1 # Copyright (C) 2014-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 """
22 20 GIT module
23 21 """
24 22 import os
25 23 import logging
26 24
27 25 from rhodecode.lib.vcs import connection
28 26 from rhodecode.lib.vcs.backends.git.repository import GitRepository
29 27 from rhodecode.lib.vcs.backends.git.commit import GitCommit
30 28 from rhodecode.lib.vcs.backends.git.inmemory import GitInMemoryCommit
31 29
32 30
33 31 log = logging.getLogger(__name__)
34 32
35 33
36 34 def discover_git_version(raise_on_exc=False):
37 35 """
38 36 Returns the string as it was returned by running 'git --version'
39 37
40 38 It will return an empty string in case the connection is not initialized
41 39 or no vcsserver is available.
42 40 """
43 41 try:
44 42 return connection.Git.discover_git_version()
45 43 except Exception:
46 44 log.warning("Failed to discover the Git version", exc_info=True)
47 45 if raise_on_exc:
48 46 raise
49 47 return ''
50 48
51 49
52 50 def lfs_store(base_location):
53 51 """
54 52 Return a lfs store relative to base_location
55 53 """
56 54 return os.path.join(base_location, '.cache', 'lfs_store')
@@ -1,491 +1,488 b''
1
2
3 1 # Copyright (C) 2014-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 """
22 20 GIT commit module
23 21 """
24 22
25 23 import stat
26 24 import configparser
27 25 from itertools import chain
28 26
29 27 from zope.cachedescriptors.property import Lazy as LazyProperty
30 28
31 29 from rhodecode.lib.datelib import utcdate_fromtimestamp
32 30 from rhodecode.lib.str_utils import safe_bytes, safe_str
33 31 from rhodecode.lib.vcs.backends import base
34 32 from rhodecode.lib.vcs.exceptions import CommitError, NodeDoesNotExistError
35 33 from rhodecode.lib.vcs.nodes import (
36 34 FileNode, DirNode, NodeKind, RootNode, SubModuleNode,
37 35 ChangedFileNodesGenerator, AddedFileNodesGenerator,
38 36 RemovedFileNodesGenerator, LargeFileNode)
39 37
40 38
41 39 class GitCommit(base.BaseCommit):
42 40 """
43 41 Represents state of the repository at single commit id.
44 42 """
45 43
46 44 _filter_pre_load = [
47 45 # done through a more complex tree walk on parents
48 46 "affected_files",
49 47 # done through subprocess not remote call
50 48 "children",
51 49 # done through a more complex tree walk on parents
52 50 "status",
53 51 # mercurial specific property not supported here
54 52 "_file_paths",
55 53 # mercurial specific property not supported here
56 54 'obsolete',
57 55 # mercurial specific property not supported here
58 56 'phase',
59 57 # mercurial specific property not supported here
60 58 'hidden'
61 59 ]
62 60
63 61 def __init__(self, repository, raw_id, idx, pre_load=None):
64 62 self.repository = repository
65 63 self._remote = repository._remote
66 64 # TODO: johbo: Tweak of raw_id should not be necessary
67 65 self.raw_id = safe_str(raw_id)
68 66 self.idx = idx
69 67
70 68 self._set_bulk_properties(pre_load)
71 69
72 70 # caches
73 71 self._stat_modes = {} # stat info for paths
74 72 self._paths = {} # path processed with parse_tree
75 73 self.nodes = {}
76 74 self._submodules = None
77 75
78 76 def _set_bulk_properties(self, pre_load):
79 77
80 78 if not pre_load:
81 79 return
82 80 pre_load = [entry for entry in pre_load
83 81 if entry not in self._filter_pre_load]
84 82 if not pre_load:
85 83 return
86 84
87 85 result = self._remote.bulk_request(self.raw_id, pre_load)
88 86 for attr, value in result.items():
89 87 if attr in ["author", "message"]:
90 88 if value:
91 89 value = safe_str(value)
92 90 elif attr == "date":
93 91 value = utcdate_fromtimestamp(*value)
94 92 elif attr == "parents":
95 93 value = self._make_commits(value)
96 94 elif attr == "branch":
97 95 value = self._set_branch(value)
98 96 self.__dict__[attr] = value
99 97
100 98 @LazyProperty
101 99 def _commit(self):
102 100 return self._remote[self.raw_id]
103 101
104 102 @LazyProperty
105 103 def _tree_id(self):
106 104 return self._remote[self._commit['tree']]['id']
107 105
108 106 @LazyProperty
109 107 def id(self):
110 108 return self.raw_id
111 109
112 110 @LazyProperty
113 111 def short_id(self):
114 112 return self.raw_id[:12]
115 113
116 114 @LazyProperty
117 115 def message(self):
118 116 return safe_str(self._remote.message(self.id))
119 117
120 118 @LazyProperty
121 119 def committer(self):
122 120 return safe_str(self._remote.author(self.id))
123 121
124 122 @LazyProperty
125 123 def author(self):
126 124 return safe_str(self._remote.author(self.id))
127 125
128 126 @LazyProperty
129 127 def date(self):
130 128 unix_ts, tz = self._remote.date(self.raw_id)
131 129 return utcdate_fromtimestamp(unix_ts, tz)
132 130
133 131 @LazyProperty
134 132 def status(self):
135 133 """
136 134 Returns modified, added, removed, deleted files for current commit
137 135 """
138 136 return self.changed, self.added, self.removed
139 137
140 138 @LazyProperty
141 139 def tags(self):
142 140 tags = [safe_str(name) for name,
143 141 commit_id in self.repository.tags.items()
144 142 if commit_id == self.raw_id]
145 143 return tags
146 144
147 145 @LazyProperty
148 146 def commit_branches(self):
149 147 branches = []
150 148 for name, commit_id in self.repository.branches.items():
151 149 if commit_id == self.raw_id:
152 150 branches.append(name)
153 151 return branches
154 152
155 153 def _set_branch(self, branches):
156 154 if branches:
157 155 # actually commit can have multiple branches in git
158 156 return safe_str(branches[0])
159 157
160 158 @LazyProperty
161 159 def branch(self):
162 160 branches = self._remote.branch(self.raw_id)
163 161 return self._set_branch(branches)
164 162
165 163 def _get_tree_id_for_path(self, path):
166 164
167 165 path = safe_str(path)
168 166 if path in self._paths:
169 167 return self._paths[path]
170 168
171 169 tree_id = self._tree_id
172 170
173 171 path = path.strip('/')
174 172 if path == '':
175 173 data = [tree_id, "tree"]
176 174 self._paths[''] = data
177 175 return data
178 176
179 177 tree_id, tree_type, tree_mode = \
180 178 self._remote.tree_and_type_for_path(self.raw_id, path)
181 179 if tree_id is None:
182 180 raise self.no_node_at_path(path)
183 181
184 182 self._paths[path] = [tree_id, tree_type]
185 183 self._stat_modes[path] = tree_mode
186 184
187 185 if path not in self._paths:
188 186 raise self.no_node_at_path(path)
189 187
190 188 return self._paths[path]
191 189
192 190 def _get_kind(self, path):
193 191 tree_id, type_ = self._get_tree_id_for_path(path)
194 192 if type_ == 'blob':
195 193 return NodeKind.FILE
196 194 elif type_ == 'tree':
197 195 return NodeKind.DIR
198 196 elif type_ == 'link':
199 197 return NodeKind.SUBMODULE
200 198 return None
201 199
202 200 def _assert_is_path(self, path):
203 201 path = self._fix_path(path)
204 202 if self._get_kind(path) != NodeKind.FILE:
205 203 raise CommitError(f"File does not exist for commit {self.raw_id} at '{path}'")
206 204 return path
207 205
208 206 def _get_file_nodes(self):
209 207 return chain(*(t[2] for t in self.walk()))
210 208
211 209 @LazyProperty
212 210 def parents(self):
213 211 """
214 212 Returns list of parent commits.
215 213 """
216 214 parent_ids = self._remote.parents(self.id)
217 215 return self._make_commits(parent_ids)
218 216
219 217 @LazyProperty
220 218 def children(self):
221 219 """
222 220 Returns list of child commits.
223 221 """
224 222
225 223 children = self._remote.children(self.raw_id)
226 224 return self._make_commits(children)
227 225
228 226 def _make_commits(self, commit_ids):
229 227 def commit_maker(_commit_id):
230 228 return self.repository.get_commit(commit_id=_commit_id)
231 229
232 230 return [commit_maker(commit_id) for commit_id in commit_ids]
233 231
234 232 def get_file_mode(self, path: bytes):
235 233 """
236 234 Returns stat mode of the file at the given `path`.
237 235 """
238 236 path = self._assert_is_path(path)
239 237
240 238 # ensure path is traversed
241 239 self._get_tree_id_for_path(path)
242 240
243 241 return self._stat_modes[path]
244 242
245 243 def is_link(self, path):
246 244 return stat.S_ISLNK(self.get_file_mode(path))
247 245
248 246 def is_node_binary(self, path):
249 247 tree_id, _ = self._get_tree_id_for_path(path)
250 248 return self._remote.is_binary(tree_id)
251 249
252 250 def node_md5_hash(self, path):
253 251 path = self._assert_is_path(path)
254 252 return self._remote.md5_hash(self.raw_id, path)
255 253
256 254 def get_file_content(self, path):
257 255 """
258 256 Returns content of the file at given `path`.
259 257 """
260 258 tree_id, _ = self._get_tree_id_for_path(path)
261 259 return self._remote.blob_as_pretty_string(tree_id)
262 260
263 261 def get_file_content_streamed(self, path):
264 262 tree_id, _ = self._get_tree_id_for_path(path)
265 263 stream_method = getattr(self._remote, 'stream:blob_as_pretty_string')
266 264 return stream_method(tree_id)
267 265
268 266 def get_file_size(self, path):
269 267 """
270 268 Returns size of the file at given `path`.
271 269 """
272 270 tree_id, _ = self._get_tree_id_for_path(path)
273 271 return self._remote.blob_raw_length(tree_id)
274 272
275 273 def get_path_history(self, path, limit=None, pre_load=None):
276 274 """
277 275 Returns history of file as reversed list of `GitCommit` objects for
278 276 which file at given `path` has been modified.
279 277 """
280 278
281 279 path = self._assert_is_path(path)
282 280 hist = self._remote.node_history(self.raw_id, path, limit)
283 281 return [
284 282 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
285 283 for commit_id in hist]
286 284
287 285 def get_file_annotate(self, path, pre_load=None):
288 286 """
289 287 Returns a generator of four element tuples with
290 288 lineno, commit_id, commit lazy loader and line
291 289 """
292 290
293 291 result = self._remote.node_annotate(self.raw_id, path)
294 292
295 293 for ln_no, commit_id, content in result:
296 294 yield (
297 295 ln_no, commit_id,
298 296 lambda: self.repository.get_commit(commit_id=commit_id, pre_load=pre_load),
299 297 content)
300 298
301 299 def get_nodes(self, path, pre_load=None):
302 300
303 301 if self._get_kind(path) != NodeKind.DIR:
304 302 raise CommitError(
305 303 f"Directory does not exist for commit {self.raw_id} at '{path}'")
306 304 path = self._fix_path(path)
307 305
308 306 tree_id, _ = self._get_tree_id_for_path(path)
309 307
310 308 dirnodes = []
311 309 filenodes = []
312 310
313 311 # extracted tree ID gives us our files...
314 312 bytes_path = safe_str(path) # libgit operates on bytes
315 313 for name, stat_, id_, type_ in self._remote.tree_items(tree_id):
316 314 if type_ == 'link':
317 315 url = self._get_submodule_url('/'.join((bytes_path, name)))
318 316 dirnodes.append(SubModuleNode(
319 317 name, url=url, commit=id_, alias=self.repository.alias))
320 318 continue
321 319
322 320 if bytes_path != '':
323 321 obj_path = '/'.join((bytes_path, name))
324 322 else:
325 323 obj_path = name
326 324 if obj_path not in self._stat_modes:
327 325 self._stat_modes[obj_path] = stat_
328 326
329 327 if type_ == 'tree':
330 328 dirnodes.append(DirNode(safe_bytes(obj_path), commit=self))
331 329 elif type_ == 'blob':
332 330 filenodes.append(FileNode(safe_bytes(obj_path), commit=self, mode=stat_, pre_load=pre_load))
333 331 else:
334 332 raise CommitError(f"Requested object should be Tree or Blob, is {type_}")
335 333
336 334 nodes = dirnodes + filenodes
337 335 for node in nodes:
338 336 if node.path not in self.nodes:
339 337 self.nodes[node.path] = node
340 338 nodes.sort()
341 339 return nodes
342 340
343 341 def get_node(self, path, pre_load=None):
344 342 path = self._fix_path(path)
345 343 if path not in self.nodes:
346 344 try:
347 345 tree_id, type_ = self._get_tree_id_for_path(path)
348 346 except CommitError:
349 347 raise NodeDoesNotExistError(
350 348 f"Cannot find one of parents' directories for a given "
351 349 f"path: {path}")
352 350
353 351 if type_ in ['link', 'commit']:
354 352 url = self._get_submodule_url(path)
355 353 node = SubModuleNode(path, url=url, commit=tree_id,
356 354 alias=self.repository.alias)
357 355 elif type_ == 'tree':
358 356 if path == '':
359 357 node = RootNode(commit=self)
360 358 else:
361 359 node = DirNode(safe_bytes(path), commit=self)
362 360 elif type_ == 'blob':
363 361 node = FileNode(safe_bytes(path), commit=self, pre_load=pre_load)
364 362 self._stat_modes[path] = node.mode
365 363 else:
366 364 raise self.no_node_at_path(path)
367 365
368 366 # cache node
369 367 self.nodes[path] = node
370 368
371 369 return self.nodes[path]
372 370
373 371 def get_largefile_node(self, path):
374 372 tree_id, _ = self._get_tree_id_for_path(path)
375 373 pointer_spec = self._remote.is_large_file(tree_id)
376 374
377 375 if pointer_spec:
378 376 # content of that file regular FileNode is the hash of largefile
379 377 file_id = pointer_spec.get('oid_hash')
380 378 if self._remote.in_largefiles_store(file_id):
381 379 lf_path = self._remote.store_path(file_id)
382 380 return LargeFileNode(safe_bytes(lf_path), commit=self, org_path=path)
383 381
384 382 @LazyProperty
385 383 def affected_files(self):
386 384 """
387 385 Gets a fast accessible file changes for given commit
388 386 """
389 387 added, modified, deleted = self._changes_cache
390 388 return list(added.union(modified).union(deleted))
391 389
392 390 @LazyProperty
393 391 def _changes_cache(self):
394 392 added = set()
395 393 modified = set()
396 394 deleted = set()
397 395
398 396 parents = self.parents
399 397 if not self.parents:
400 398 parents = [base.EmptyCommit()]
401 399 for parent in parents:
402 400 if isinstance(parent, base.EmptyCommit):
403 401 oid = None
404 402 else:
405 403 oid = parent.raw_id
406 404 _added, _modified, _deleted = self._remote.tree_changes(oid, self.raw_id)
407 405 added = added | set(_added)
408 406 modified = modified | set(_modified)
409 407 deleted = deleted | set(_deleted)
410 408
411 409 return added, modified, deleted
412 410
413 411 def _get_paths_for_status(self, status):
414 412 """
415 413 Returns sorted list of paths for given ``status``.
416 414
417 415 :param status: one of: *added*, *modified* or *deleted*
418 416 """
419 417 added, modified, deleted = self._changes_cache
420 418 return sorted({
421 419 'added': list(added),
422 420 'modified': list(modified),
423 421 'deleted': list(deleted)}[status]
424 422 )
425 423
426 424 @LazyProperty
427 425 def added(self):
428 426 """
429 427 Returns list of added ``FileNode`` objects.
430 428 """
431 429 if not self.parents:
432 430 return list(self._get_file_nodes())
433 431 return AddedFileNodesGenerator(self.added_paths, self)
434 432
435 433 @LazyProperty
436 434 def added_paths(self):
437 435 return [n for n in self._get_paths_for_status('added')]
438 436
439 437 @LazyProperty
440 438 def changed(self):
441 439 """
442 440 Returns list of modified ``FileNode`` objects.
443 441 """
444 442 if not self.parents:
445 443 return []
446 444 return ChangedFileNodesGenerator(self.changed_paths, self)
447 445
448 446 @LazyProperty
449 447 def changed_paths(self):
450 448 return [n for n in self._get_paths_for_status('modified')]
451 449
452 450 @LazyProperty
453 451 def removed(self):
454 452 """
455 453 Returns list of removed ``FileNode`` objects.
456 454 """
457 455 if not self.parents:
458 456 return []
459 457 return RemovedFileNodesGenerator(self.removed_paths, self)
460 458
461 459 @LazyProperty
462 460 def removed_paths(self):
463 461 return [n for n in self._get_paths_for_status('deleted')]
464 462
465 463 def _get_submodule_url(self, submodule_path):
466 464 git_modules_path = '.gitmodules'
467 465
468 466 if self._submodules is None:
469 467 self._submodules = {}
470 468
471 469 try:
472 470 submodules_node = self.get_node(git_modules_path)
473 471 except NodeDoesNotExistError:
474 472 return None
475 473
476 474 # ConfigParser fails if there are whitespaces, also it needs an iterable
477 475 # file like content
478 476 def iter_content(_content):
479 for line in _content.splitlines():
480 yield line
477 yield from _content.splitlines()
481 478
482 479 parser = configparser.RawConfigParser()
483 480 parser.read_file(iter_content(submodules_node.content))
484 481
485 482 for section in parser.sections():
486 483 path = parser.get(section, 'path')
487 484 url = parser.get(section, 'url')
488 485 if path and url:
489 486 self._submodules[path.strip('/')] = url
490 487
491 488 return self._submodules.get(submodule_path.strip('/'))
@@ -1,49 +1,47 b''
1
2
3 1 # Copyright (C) 2014-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 """
22 20 GIT diff module
23 21 """
24 22
25 23 import re
26 24
27 25 from rhodecode.lib.vcs.backends import base
28 26
29 27
30 28 class GitDiff(base.Diff):
31 29
32 30 _header_re = re.compile(br"""
33 31 #^diff[ ]--git
34 32 [ ]"?a/(?P<a_path>.+?)"?[ ]"?b/(?P<b_path>.+?)"?\n
35 33 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
36 34 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
37 35 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))?
38 36 (?:^rename[ ]from[ ](?P<rename_from>[^\r\n]+)\n
39 37 ^rename[ ]to[ ](?P<rename_to>[^\r\n]+)(?:\n|$))?
40 38 (?:^copy[ ]from[ ](?P<copy_from>[^\r\n]+)\n
41 39 ^copy[ ]to[ ](?P<copy_to>[^\r\n]+)(?:\n|$))?
42 40 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
43 41 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
44 42 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
45 43 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
46 44 (?:^(?P<bin_patch>GIT[ ]binary[ ]patch)(?:\n|$))?
47 45 (?:^---[ ]("?a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
48 46 (?:^\+\+\+[ ]("?b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
49 47 """, re.VERBOSE | re.MULTILINE)
@@ -1,107 +1,105 b''
1
2
3 1 # Copyright (C) 2014-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 """
22 20 GIT inmemory module
23 21 """
24 22
25 23 from rhodecode.lib.datelib import date_to_timestamp_plus_offset
26 24 from rhodecode.lib.str_utils import safe_str, get_default_encodings
27 25 from rhodecode.lib.vcs.backends import base
28 26
29 27
30 28 class GitInMemoryCommit(base.BaseInMemoryCommit):
31 29
32 30 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
33 31 """
34 32 Performs in-memory commit (doesn't check workdir in any way) and
35 33 returns newly created `GitCommit`. Updates repository's
36 34 `commit_ids`.
37 35
38 36 :param message: message of the commit
39 37 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
40 38 :param parents: single parent or sequence of parents from which commit
41 39 would be derived
42 40 :param date: `datetime.datetime` instance. Defaults to
43 41 ``datetime.datetime.now()``.
44 42 :param branch: branch name, as string. If none given, default backend's
45 43 branch would be used.
46 44
47 45 :raises `CommitError`: if any error occurs while committing
48 46 """
49 47 self.check_integrity(parents)
50 48 if branch is None:
51 49 branch = self.repository.DEFAULT_BRANCH_NAME
52 50
53 51 commit_tree = None
54 52 if self.parents[0]:
55 53 commit_tree = self.parents[0]._commit['tree']
56 54
57 55 encoding = get_default_encodings()[0]
58 56 updated = []
59 57 for node in self.added + self.changed:
60 58 content = node.content
61 59 # TODO: left for reference pre py3 migration, probably need to be removed
62 60 # if node.is_binary:
63 61 # content = node.content
64 62 # else:
65 63 # content = node.content.encode(ENCODING)
66 64
67 65 updated.append({
68 66 'path': node.path,
69 67 'node_path': node.name,
70 68 'content': content,
71 69 'mode': node.mode,
72 70 })
73 71
74 72 removed = [node.path for node in self.removed]
75 73
76 74 date, tz = date_to_timestamp_plus_offset(date)
77 75
78 76 author_time = kwargs.pop('author_time', date)
79 77 author_tz = kwargs.pop('author_timezone', tz)
80 78
81 79 commit_data = {
82 80 'parents': [p._commit['id'] for p in self.parents if p],
83 81 'author': safe_str(author),
84 82 'committer': safe_str(author),
85 83 'encoding': encoding,
86 84 'message': safe_str(message),
87 85
88 86 'commit_time': int(date),
89 87 'commit_timezone': tz,
90 88
91 89 'author_time': int(author_time),
92 90 'author_timezone': author_tz,
93 91 }
94 92
95 93 commit_id = self.repository._remote.commit(
96 94 commit_data, branch, commit_tree, updated, removed)
97 95
98 96 # Update vcs repository object
99 97 self.repository.append_commit_id(commit_id)
100 98
101 99 # invalidate parsed refs after commit
102 100 self.repository._refs = self.repository._get_refs()
103 101 self.repository.branches = self.repository._get_branches()
104 102 tip = self.repository.get_commit(commit_id)
105 103
106 104 self.reset()
107 105 return tip
@@ -1,1055 +1,1053 b''
1
2
3 1 # Copyright (C) 2014-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 """
22 20 GIT repository module
23 21 """
24 22
25 23 import logging
26 24 import os
27 25 import re
28 26
29 27 from zope.cachedescriptors.property import Lazy as LazyProperty
30 28
31 29 from collections import OrderedDict
32 30 from rhodecode.lib.datelib import (
33 31 utcdate_fromtimestamp, makedate, date_astimestamp)
34 32 from rhodecode.lib.hash_utils import safe_str
35 33 from rhodecode.lib.utils2 import CachedProperty
36 34 from rhodecode.lib.vcs import connection, path as vcspath
37 35 from rhodecode.lib.vcs.backends.base import (
38 36 BaseRepository, CollectionGenerator, Config, MergeResponse,
39 37 MergeFailureReason, Reference)
40 38 from rhodecode.lib.vcs.backends.git.commit import GitCommit
41 39 from rhodecode.lib.vcs.backends.git.diff import GitDiff
42 40 from rhodecode.lib.vcs.backends.git.inmemory import GitInMemoryCommit
43 41 from rhodecode.lib.vcs.exceptions import (
44 42 CommitDoesNotExistError, EmptyRepositoryError,
45 43 RepositoryError, TagAlreadyExistError, TagDoesNotExistError, VCSError, UnresolvedFilesInRepo)
46 44
47 45
48 46 SHA_PATTERN = re.compile(r'^([0-9a-fA-F]{12}|[0-9a-fA-F]{40})$')
49 47
50 48 log = logging.getLogger(__name__)
51 49
52 50
53 51 class GitRepository(BaseRepository):
54 52 """
55 53 Git repository backend.
56 54 """
57 55 DEFAULT_BRANCH_NAME = os.environ.get('GIT_DEFAULT_BRANCH_NAME') or 'master'
58 DEFAULT_REF = 'branch:{}'.format(DEFAULT_BRANCH_NAME)
56 DEFAULT_REF = f'branch:{DEFAULT_BRANCH_NAME}'
59 57
60 58 contact = BaseRepository.DEFAULT_CONTACT
61 59
62 60 def __init__(self, repo_path, config=None, create=False, src_url=None,
63 61 do_workspace_checkout=False, with_wire=None, bare=False):
64 62
65 63 self.path = safe_str(os.path.abspath(repo_path))
66 64 self.config = config if config else self.get_default_config()
67 65 self.with_wire = with_wire or {"cache": False} # default should not use cache
68 66
69 67 self._init_repo(create, src_url, do_workspace_checkout, bare)
70 68
71 69 # caches
72 70 self._commit_ids = {}
73 71
74 72 @LazyProperty
75 73 def _remote(self):
76 74 repo_id = self.path
77 75 return connection.Git(self.path, repo_id, self.config, with_wire=self.with_wire)
78 76
79 77 @LazyProperty
80 78 def bare(self):
81 79 return self._remote.bare()
82 80
83 81 @LazyProperty
84 82 def head(self):
85 83 return self._remote.head()
86 84
87 85 @CachedProperty
88 86 def commit_ids(self):
89 87 """
90 88 Returns list of commit ids, in ascending order. Being lazy
91 89 attribute allows external tools to inject commit ids from cache.
92 90 """
93 91 commit_ids = self._get_all_commit_ids()
94 92 self._rebuild_cache(commit_ids)
95 93 return commit_ids
96 94
97 95 def _rebuild_cache(self, commit_ids):
98 self._commit_ids = dict((commit_id, index)
99 for index, commit_id in enumerate(commit_ids))
96 self._commit_ids = {commit_id: index
97 for index, commit_id in enumerate(commit_ids)}
100 98
101 99 def run_git_command(self, cmd, **opts):
102 100 """
103 101 Runs given ``cmd`` as git command and returns tuple
104 102 (stdout, stderr).
105 103
106 104 :param cmd: git command to be executed
107 105 :param opts: env options to pass into Subprocess command
108 106 """
109 107 if not isinstance(cmd, list):
110 108 raise ValueError(f'cmd must be a list, got {type(cmd)} instead')
111 109
112 110 skip_stderr_log = opts.pop('skip_stderr_log', False)
113 111 out, err = self._remote.run_git_command(cmd, **opts)
114 112 if err and not skip_stderr_log:
115 113 log.debug('Stderr output of git command "%s":\n%s', cmd, err)
116 114 return out, err
117 115
118 116 @staticmethod
119 117 def check_url(url, config):
120 118 """
121 119 Function will check given url and try to verify if it's a valid
122 120 link. Sometimes it may happened that git will issue basic
123 121 auth request that can cause whole API to hang when used from python
124 122 or other external calls.
125 123
126 124 On failures it'll raise urllib2.HTTPError, exception is also thrown
127 125 when the return code is non 200
128 126 """
129 127 # check first if it's not an url
130 128 if os.path.isdir(url) or url.startswith('file:'):
131 129 return True
132 130
133 131 if '+' in url.split('://', 1)[0]:
134 132 url = url.split('+', 1)[1]
135 133
136 134 # Request the _remote to verify the url
137 135 return connection.Git.check_url(url, config.serialize())
138 136
139 137 @staticmethod
140 138 def is_valid_repository(path):
141 139 if os.path.isdir(os.path.join(path, '.git')):
142 140 return True
143 141 # check case of bare repository
144 142 try:
145 143 GitRepository(path)
146 144 return True
147 145 except VCSError:
148 146 pass
149 147 return False
150 148
151 149 def _init_repo(self, create, src_url=None, do_workspace_checkout=False,
152 150 bare=False):
153 151 if create and os.path.exists(self.path):
154 152 raise RepositoryError(
155 153 "Cannot create repository at %s, location already exist"
156 154 % self.path)
157 155
158 156 if bare and do_workspace_checkout:
159 157 raise RepositoryError("Cannot update a bare repository")
160 158 try:
161 159
162 160 if src_url:
163 161 # check URL before any actions
164 162 GitRepository.check_url(src_url, self.config)
165 163
166 164 if create:
167 165 os.makedirs(self.path, mode=0o755)
168 166
169 167 if bare:
170 168 self._remote.init_bare()
171 169 else:
172 170 self._remote.init()
173 171
174 172 if src_url and bare:
175 173 # bare repository only allows a fetch and checkout is not allowed
176 174 self.fetch(src_url, commit_ids=None)
177 175 elif src_url:
178 176 self.pull(src_url, commit_ids=None,
179 177 update_after=do_workspace_checkout)
180 178
181 179 else:
182 180 if not self._remote.assert_correct_path():
183 181 raise RepositoryError(
184 182 'Path "%s" does not contain a Git repository' %
185 183 (self.path,))
186 184
187 185 # TODO: johbo: check if we have to translate the OSError here
188 186 except OSError as err:
189 187 raise RepositoryError(err)
190 188
191 189 def _get_all_commit_ids(self):
192 190 return self._remote.get_all_commit_ids()
193 191
194 192 def _get_commit_ids(self, filters=None):
195 193 # we must check if this repo is not empty, since later command
196 194 # fails if it is. And it's cheaper to ask than throw the subprocess
197 195 # errors
198 196
199 197 head = self._remote.head(show_exc=False)
200 198
201 199 if not head:
202 200 return []
203 201
204 202 rev_filter = ['--branches', '--tags']
205 203 extra_filter = []
206 204
207 205 if filters:
208 206 if filters.get('since'):
209 207 extra_filter.append('--since=%s' % (filters['since']))
210 208 if filters.get('until'):
211 209 extra_filter.append('--until=%s' % (filters['until']))
212 210 if filters.get('branch_name'):
213 211 rev_filter = []
214 212 extra_filter.append(filters['branch_name'])
215 213 rev_filter.extend(extra_filter)
216 214
217 215 # if filters.get('start') or filters.get('end'):
218 216 # # skip is offset, max-count is limit
219 217 # if filters.get('start'):
220 218 # extra_filter += ' --skip=%s' % filters['start']
221 219 # if filters.get('end'):
222 220 # extra_filter += ' --max-count=%s' % (filters['end'] - (filters['start'] or 0))
223 221
224 222 cmd = ['rev-list', '--reverse', '--date-order'] + rev_filter
225 223 try:
226 224 output, __ = self.run_git_command(cmd)
227 225 except RepositoryError:
228 226 # Can be raised for empty repositories
229 227 return []
230 228 return output.splitlines()
231 229
232 230 def _lookup_commit(self, commit_id_or_idx, translate_tag=True, maybe_unreachable=False, reference_obj=None):
233 231
234 232 def is_null(value):
235 233 return len(value) == commit_id_or_idx.count('0')
236 234
237 235 if commit_id_or_idx in (None, '', 'tip', 'HEAD', 'head', -1):
238 236 return self.commit_ids[-1]
239 237
240 238 commit_missing_err = "Commit {} does not exist for `{}`".format(
241 239 *map(safe_str, [commit_id_or_idx, self.name]))
242 240
243 241 is_bstr = isinstance(commit_id_or_idx, str)
244 242 is_branch = reference_obj and reference_obj.branch
245 243
246 244 lookup_ok = False
247 245 if is_bstr:
248 246 # Need to call remote to translate id for tagging scenarios,
249 247 # or branch that are numeric
250 248 try:
251 249 remote_data = self._remote.get_object(commit_id_or_idx,
252 250 maybe_unreachable=maybe_unreachable)
253 251 commit_id_or_idx = remote_data["commit_id"]
254 252 lookup_ok = True
255 253 except (CommitDoesNotExistError,):
256 254 lookup_ok = False
257 255
258 256 if lookup_ok is False:
259 257 is_numeric_idx = \
260 258 (is_bstr and commit_id_or_idx.isdigit() and len(commit_id_or_idx) < 12) \
261 259 or isinstance(commit_id_or_idx, int)
262 260 if not is_branch and (is_numeric_idx or is_null(commit_id_or_idx)):
263 261 try:
264 262 commit_id_or_idx = self.commit_ids[int(commit_id_or_idx)]
265 263 lookup_ok = True
266 264 except Exception:
267 265 raise CommitDoesNotExistError(commit_missing_err)
268 266
269 267 # we failed regular lookup, and by integer number lookup
270 268 if lookup_ok is False:
271 269 raise CommitDoesNotExistError(commit_missing_err)
272 270
273 271 # Ensure we return full id
274 272 if not SHA_PATTERN.match(str(commit_id_or_idx)):
275 273 raise CommitDoesNotExistError(
276 274 "Given commit id %s not recognized" % commit_id_or_idx)
277 275 return commit_id_or_idx
278 276
279 277 def get_hook_location(self):
280 278 """
281 279 returns absolute path to location where hooks are stored
282 280 """
283 281 loc = os.path.join(self.path, 'hooks')
284 282 if not self.bare:
285 283 loc = os.path.join(self.path, '.git', 'hooks')
286 284 return loc
287 285
288 286 @LazyProperty
289 287 def last_change(self):
290 288 """
291 289 Returns last change made on this repository as
292 290 `datetime.datetime` object.
293 291 """
294 292 try:
295 293 return self.get_commit().date
296 294 except RepositoryError:
297 295 tzoffset = makedate()[1]
298 296 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
299 297
300 298 def _get_fs_mtime(self):
301 299 idx_loc = '' if self.bare else '.git'
302 300 # fallback to filesystem
303 301 in_path = os.path.join(self.path, idx_loc, "index")
304 302 he_path = os.path.join(self.path, idx_loc, "HEAD")
305 303 if os.path.exists(in_path):
306 304 return os.stat(in_path).st_mtime
307 305 else:
308 306 return os.stat(he_path).st_mtime
309 307
310 308 @LazyProperty
311 309 def description(self):
312 310 description = self._remote.get_description()
313 311 return safe_str(description or self.DEFAULT_DESCRIPTION)
314 312
315 313 def _get_refs_entries(self, prefix='', reverse=False, strip_prefix=True):
316 314 if self.is_empty():
317 315 return OrderedDict()
318 316
319 317 result = []
320 318 for ref, sha in self._refs.items():
321 319 if ref.startswith(prefix):
322 320 ref_name = ref
323 321 if strip_prefix:
324 322 ref_name = ref[len(prefix):]
325 323 result.append((safe_str(ref_name), sha))
326 324
327 325 def get_name(entry):
328 326 return entry[0]
329 327
330 328 return OrderedDict(sorted(result, key=get_name, reverse=reverse))
331 329
332 330 def _get_branches(self):
333 331 return self._get_refs_entries(prefix='refs/heads/', strip_prefix=True)
334 332
335 333 @CachedProperty
336 334 def branches(self):
337 335 return self._get_branches()
338 336
339 337 @CachedProperty
340 338 def branches_closed(self):
341 339 return {}
342 340
343 341 @CachedProperty
344 342 def bookmarks(self):
345 343 return {}
346 344
347 345 @CachedProperty
348 346 def branches_all(self):
349 347 all_branches = {}
350 348 all_branches.update(self.branches)
351 349 all_branches.update(self.branches_closed)
352 350 return all_branches
353 351
354 352 @CachedProperty
355 353 def tags(self):
356 354 return self._get_tags()
357 355
358 356 def _get_tags(self):
359 357 return self._get_refs_entries(prefix='refs/tags/', strip_prefix=True, reverse=True)
360 358
361 359 def tag(self, name, user, commit_id=None, message=None, date=None,
362 360 **kwargs):
363 361 # TODO: fix this method to apply annotated tags correct with message
364 362 """
365 363 Creates and returns a tag for the given ``commit_id``.
366 364
367 365 :param name: name for new tag
368 366 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
369 367 :param commit_id: commit id for which new tag would be created
370 368 :param message: message of the tag's commit
371 369 :param date: date of tag's commit
372 370
373 371 :raises TagAlreadyExistError: if tag with same name already exists
374 372 """
375 373 if name in self.tags:
376 374 raise TagAlreadyExistError("Tag %s already exists" % name)
377 375 commit = self.get_commit(commit_id=commit_id)
378 message = message or "Added tag %s for commit %s" % (name, commit.raw_id)
376 message = message or "Added tag {} for commit {}".format(name, commit.raw_id)
379 377
380 378 self._remote.set_refs('refs/tags/%s' % name, commit.raw_id)
381 379
382 380 self._invalidate_prop_cache('tags')
383 381 self._invalidate_prop_cache('_refs')
384 382
385 383 return commit
386 384
387 385 def remove_tag(self, name, user, message=None, date=None):
388 386 """
389 387 Removes tag with the given ``name``.
390 388
391 389 :param name: name of the tag to be removed
392 390 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
393 391 :param message: message of the tag's removal commit
394 392 :param date: date of tag's removal commit
395 393
396 394 :raises TagDoesNotExistError: if tag with given name does not exists
397 395 """
398 396 if name not in self.tags:
399 397 raise TagDoesNotExistError("Tag %s does not exist" % name)
400 398
401 399 self._remote.tag_remove(name)
402 400 self._invalidate_prop_cache('tags')
403 401 self._invalidate_prop_cache('_refs')
404 402
405 403 def _get_refs(self):
406 404 return self._remote.get_refs()
407 405
408 406 @CachedProperty
409 407 def _refs(self):
410 408 return self._get_refs()
411 409
412 410 @property
413 411 def _ref_tree(self):
414 412 node = tree = {}
415 413 for ref, sha in self._refs.items():
416 414 path = ref.split('/')
417 415 for bit in path[:-1]:
418 416 node = node.setdefault(bit, {})
419 417 node[path[-1]] = sha
420 418 node = tree
421 419 return tree
422 420
423 421 def get_remote_ref(self, ref_name):
424 ref_key = 'refs/remotes/origin/{}'.format(safe_str(ref_name))
422 ref_key = f'refs/remotes/origin/{safe_str(ref_name)}'
425 423 try:
426 424 return self._refs[ref_key]
427 425 except Exception:
428 426 return
429 427
430 428 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
431 429 translate_tag=True, maybe_unreachable=False, reference_obj=None):
432 430 """
433 431 Returns `GitCommit` object representing commit from git repository
434 432 at the given `commit_id` or head (most recent commit) if None given.
435 433 """
436 434
437 435 if self.is_empty():
438 436 raise EmptyRepositoryError("There are no commits yet")
439 437
440 438 if commit_id is not None:
441 439 self._validate_commit_id(commit_id)
442 440 try:
443 441 # we have cached idx, use it without contacting the remote
444 442 idx = self._commit_ids[commit_id]
445 443 return GitCommit(self, commit_id, idx, pre_load=pre_load)
446 444 except KeyError:
447 445 pass
448 446
449 447 elif commit_idx is not None:
450 448 self._validate_commit_idx(commit_idx)
451 449 try:
452 450 _commit_id = self.commit_ids[commit_idx]
453 451 if commit_idx < 0:
454 452 commit_idx = self.commit_ids.index(_commit_id)
455 453 return GitCommit(self, _commit_id, commit_idx, pre_load=pre_load)
456 454 except IndexError:
457 455 commit_id = commit_idx
458 456 else:
459 457 commit_id = "tip"
460 458
461 459 if translate_tag:
462 460 commit_id = self._lookup_commit(
463 461 commit_id, maybe_unreachable=maybe_unreachable,
464 462 reference_obj=reference_obj)
465 463
466 464 try:
467 465 idx = self._commit_ids[commit_id]
468 466 except KeyError:
469 467 idx = -1
470 468
471 469 return GitCommit(self, commit_id, idx, pre_load=pre_load)
472 470
473 471 def get_commits(
474 472 self, start_id=None, end_id=None, start_date=None, end_date=None,
475 473 branch_name=None, show_hidden=False, pre_load=None, translate_tags=True):
476 474 """
477 475 Returns generator of `GitCommit` objects from start to end (both
478 476 are inclusive), in ascending date order.
479 477
480 478 :param start_id: None, str(commit_id)
481 479 :param end_id: None, str(commit_id)
482 480 :param start_date: if specified, commits with commit date less than
483 481 ``start_date`` would be filtered out from returned set
484 482 :param end_date: if specified, commits with commit date greater than
485 483 ``end_date`` would be filtered out from returned set
486 484 :param branch_name: if specified, commits not reachable from given
487 485 branch would be filtered out from returned set
488 486 :param show_hidden: Show hidden commits such as obsolete or hidden from
489 487 Mercurial evolve
490 488 :raise BranchDoesNotExistError: If given `branch_name` does not
491 489 exist.
492 490 :raise CommitDoesNotExistError: If commits for given `start` or
493 491 `end` could not be found.
494 492
495 493 """
496 494 if self.is_empty():
497 495 raise EmptyRepositoryError("There are no commits yet")
498 496
499 497 self._validate_branch_name(branch_name)
500 498
501 499 if start_id is not None:
502 500 self._validate_commit_id(start_id)
503 501 if end_id is not None:
504 502 self._validate_commit_id(end_id)
505 503
506 504 start_raw_id = self._lookup_commit(start_id)
507 505 start_pos = self._commit_ids[start_raw_id] if start_id else None
508 506 end_raw_id = self._lookup_commit(end_id)
509 507 end_pos = max(0, self._commit_ids[end_raw_id]) if end_id else None
510 508
511 509 if None not in [start_id, end_id] and start_pos > end_pos:
512 510 raise RepositoryError(
513 511 "Start commit '%s' cannot be after end commit '%s'" %
514 512 (start_id, end_id))
515 513
516 514 if end_pos is not None:
517 515 end_pos += 1
518 516
519 517 filter_ = []
520 518 if branch_name:
521 519 filter_.append({'branch_name': branch_name})
522 520 if start_date and not end_date:
523 521 filter_.append({'since': start_date})
524 522 if end_date and not start_date:
525 523 filter_.append({'until': end_date})
526 524 if start_date and end_date:
527 525 filter_.append({'since': start_date})
528 526 filter_.append({'until': end_date})
529 527
530 528 # if start_pos or end_pos:
531 529 # filter_.append({'start': start_pos})
532 530 # filter_.append({'end': end_pos})
533 531
534 532 if filter_:
535 533 revfilters = {
536 534 'branch_name': branch_name,
537 535 'since': start_date.strftime('%m/%d/%y %H:%M:%S') if start_date else None,
538 536 'until': end_date.strftime('%m/%d/%y %H:%M:%S') if end_date else None,
539 537 'start': start_pos,
540 538 'end': end_pos,
541 539 }
542 540 commit_ids = self._get_commit_ids(filters=revfilters)
543 541
544 542 else:
545 543 commit_ids = self.commit_ids
546 544
547 545 if start_pos or end_pos:
548 546 commit_ids = commit_ids[start_pos: end_pos]
549 547
550 548 return CollectionGenerator(self, commit_ids, pre_load=pre_load,
551 549 translate_tag=translate_tags)
552 550
553 551 def get_diff(
554 552 self, commit1, commit2, path='', ignore_whitespace=False,
555 553 context=3, path1=None):
556 554 """
557 555 Returns (git like) *diff*, as plain text. Shows changes introduced by
558 556 ``commit2`` since ``commit1``.
559 557
560 558 :param commit1: Entry point from which diff is shown. Can be
561 559 ``self.EMPTY_COMMIT`` - in this case, patch showing all
562 560 the changes since empty state of the repository until ``commit2``
563 561 :param commit2: Until which commits changes should be shown.
564 562 :param path:
565 563 :param ignore_whitespace: If set to ``True``, would not show whitespace
566 564 changes. Defaults to ``False``.
567 565 :param context: How many lines before/after changed lines should be
568 566 shown. Defaults to ``3``.
569 567 :param path1:
570 568 """
571 569 self._validate_diff_commits(commit1, commit2)
572 570 if path1 is not None and path1 != path:
573 571 raise ValueError("Diff of two different paths not supported.")
574 572
575 573 if path:
576 574 file_filter = path
577 575 else:
578 576 file_filter = None
579 577
580 578 diff = self._remote.diff(
581 579 commit1.raw_id, commit2.raw_id, file_filter=file_filter,
582 580 opt_ignorews=ignore_whitespace,
583 581 context=context)
584 582
585 583 return GitDiff(diff)
586 584
587 585 def strip(self, commit_id, branch_name):
588 586 commit = self.get_commit(commit_id=commit_id)
589 587 if commit.merge:
590 588 raise Exception('Cannot reset to merge commit')
591 589
592 590 # parent is going to be the new head now
593 591 commit = commit.parents[0]
594 592 self._remote.set_refs('refs/heads/%s' % branch_name, commit.raw_id)
595 593
596 594 # clear cached properties
597 595 self._invalidate_prop_cache('commit_ids')
598 596 self._invalidate_prop_cache('_refs')
599 597 self._invalidate_prop_cache('branches')
600 598
601 599 return len(self.commit_ids)
602 600
603 601 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
604 602 log.debug('Calculating common ancestor between %sc1:%s and %sc2:%s',
605 603 self, commit_id1, repo2, commit_id2)
606 604
607 605 if commit_id1 == commit_id2:
608 606 return commit_id1
609 607
610 608 if self != repo2:
611 609 commits = self._remote.get_missing_revs(
612 610 commit_id1, commit_id2, repo2.path)
613 611 if commits:
614 612 commit = repo2.get_commit(commits[-1])
615 613 if commit.parents:
616 614 ancestor_id = commit.parents[0].raw_id
617 615 else:
618 616 ancestor_id = None
619 617 else:
620 618 # no commits from other repo, ancestor_id is the commit_id2
621 619 ancestor_id = commit_id2
622 620 else:
623 621 output, __ = self.run_git_command(
624 622 ['merge-base', commit_id1, commit_id2])
625 623 ancestor_id = self.COMMIT_ID_PAT.findall(output)[0]
626 624
627 625 log.debug('Found common ancestor with sha: %s', ancestor_id)
628 626
629 627 return ancestor_id
630 628
631 629 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
632 630 repo1 = self
633 631 ancestor_id = None
634 632
635 633 if commit_id1 == commit_id2:
636 634 commits = []
637 635 elif repo1 != repo2:
638 636 missing_ids = self._remote.get_missing_revs(commit_id1, commit_id2,
639 637 repo2.path)
640 638 commits = [
641 639 repo2.get_commit(commit_id=commit_id, pre_load=pre_load)
642 640 for commit_id in reversed(missing_ids)]
643 641 else:
644 642 output, __ = repo1.run_git_command(
645 643 ['log', '--reverse', '--pretty=format: %H', '-s',
646 '%s..%s' % (commit_id1, commit_id2)])
644 '{}..{}'.format(commit_id1, commit_id2)])
647 645 commits = [
648 646 repo1.get_commit(commit_id=commit_id, pre_load=pre_load)
649 647 for commit_id in self.COMMIT_ID_PAT.findall(output)]
650 648
651 649 return commits
652 650
653 651 @LazyProperty
654 652 def in_memory_commit(self):
655 653 """
656 654 Returns ``GitInMemoryCommit`` object for this repository.
657 655 """
658 656 return GitInMemoryCommit(self)
659 657
660 658 def pull(self, url, commit_ids=None, update_after=False):
661 659 """
662 660 Pull changes from external location. Pull is different in GIT
663 661 that fetch since it's doing a checkout
664 662
665 663 :param commit_ids: Optional. Can be set to a list of commit ids
666 664 which shall be pulled from the other repository.
667 665 """
668 666 refs = None
669 667 if commit_ids is not None:
670 668 remote_refs = self._remote.get_remote_refs(url)
671 669 refs = [ref for ref in remote_refs if remote_refs[ref] in commit_ids]
672 670 self._remote.pull(url, refs=refs, update_after=update_after)
673 671 self._remote.invalidate_vcs_cache()
674 672
675 673 def fetch(self, url, commit_ids=None):
676 674 """
677 675 Fetch all git objects from external location.
678 676 """
679 677 self._remote.sync_fetch(url, refs=commit_ids)
680 678 self._remote.invalidate_vcs_cache()
681 679
682 680 def push(self, url):
683 681 refs = None
684 682 self._remote.sync_push(url, refs=refs)
685 683
686 684 def set_refs(self, ref_name, commit_id):
687 685 self._remote.set_refs(ref_name, commit_id)
688 686 self._invalidate_prop_cache('_refs')
689 687
690 688 def remove_ref(self, ref_name):
691 689 self._remote.remove_ref(ref_name)
692 690 self._invalidate_prop_cache('_refs')
693 691
694 692 def run_gc(self, prune=True):
695 693 cmd = ['gc', '--aggressive']
696 694 if prune:
697 695 cmd += ['--prune=now']
698 696 _stdout, stderr = self.run_git_command(cmd, fail_on_stderr=False)
699 697 return stderr
700 698
701 699 def _update_server_info(self):
702 700 """
703 701 runs gits update-server-info command in this repo instance
704 702 """
705 703 self._remote.update_server_info()
706 704
707 705 def _current_branch(self):
708 706 """
709 707 Return the name of the current branch.
710 708
711 709 It only works for non bare repositories (i.e. repositories with a
712 710 working copy)
713 711 """
714 712 if self.bare:
715 713 raise RepositoryError('Bare git repos do not have active branches')
716 714
717 715 if self.is_empty():
718 716 return None
719 717
720 718 stdout, _ = self.run_git_command(['rev-parse', '--abbrev-ref', 'HEAD'])
721 719 return stdout.strip()
722 720
723 721 def _checkout(self, branch_name, create=False, force=False):
724 722 """
725 723 Checkout a branch in the working directory.
726 724
727 725 It tries to create the branch if create is True, failing if the branch
728 726 already exists.
729 727
730 728 It only works for non bare repositories (i.e. repositories with a
731 729 working copy)
732 730 """
733 731 if self.bare:
734 732 raise RepositoryError('Cannot checkout branches in a bare git repo')
735 733
736 734 cmd = ['checkout']
737 735 if force:
738 736 cmd.append('-f')
739 737 if create:
740 738 cmd.append('-b')
741 739 cmd.append(branch_name)
742 740 self.run_git_command(cmd, fail_on_stderr=False)
743 741
744 742 def _create_branch(self, branch_name, commit_id):
745 743 """
746 744 creates a branch in a GIT repo
747 745 """
748 746 self._remote.create_branch(branch_name, commit_id)
749 747
750 748 def _identify(self):
751 749 """
752 750 Return the current state of the working directory.
753 751 """
754 752 if self.bare:
755 753 raise RepositoryError('Bare git repos do not have active branches')
756 754
757 755 if self.is_empty():
758 756 return None
759 757
760 758 stdout, _ = self.run_git_command(['rev-parse', 'HEAD'])
761 759 return stdout.strip()
762 760
763 761 def _local_clone(self, clone_path, branch_name, source_branch=None):
764 762 """
765 763 Create a local clone of the current repo.
766 764 """
767 765 # N.B.(skreft): the --branch option is required as otherwise the shallow
768 766 # clone will only fetch the active branch.
769 767 cmd = ['clone', '--branch', branch_name,
770 768 self.path, os.path.abspath(clone_path)]
771 769
772 770 self.run_git_command(cmd, fail_on_stderr=False)
773 771
774 772 # if we get the different source branch, make sure we also fetch it for
775 773 # merge conditions
776 774 if source_branch and source_branch != branch_name:
777 775 # check if the ref exists.
778 776 shadow_repo = GitRepository(os.path.abspath(clone_path))
779 777 if shadow_repo.get_remote_ref(source_branch):
780 778 cmd = ['fetch', self.path, source_branch]
781 779 self.run_git_command(cmd, fail_on_stderr=False)
782 780
783 781 def _local_fetch(self, repository_path, branch_name, use_origin=False):
784 782 """
785 783 Fetch a branch from a local repository.
786 784 """
787 785 repository_path = os.path.abspath(repository_path)
788 786 if repository_path == self.path:
789 787 raise ValueError('Cannot fetch from the same repository')
790 788
791 789 if use_origin:
792 790 branch_name = '+{branch}:refs/heads/{branch}'.format(
793 791 branch=branch_name)
794 792
795 793 cmd = ['fetch', '--no-tags', '--update-head-ok',
796 794 repository_path, branch_name]
797 795 self.run_git_command(cmd, fail_on_stderr=False)
798 796
799 797 def _local_reset(self, branch_name):
800 branch_name = '{}'.format(branch_name)
798 branch_name = f'{branch_name}'
801 799 cmd = ['reset', '--hard', branch_name, '--']
802 800 self.run_git_command(cmd, fail_on_stderr=False)
803 801
804 802 def _last_fetch_heads(self):
805 803 """
806 804 Return the last fetched heads that need merging.
807 805
808 806 The algorithm is defined at
809 807 https://github.com/git/git/blob/v2.1.3/git-pull.sh#L283
810 808 """
811 809 if not self.bare:
812 810 fetch_heads_path = os.path.join(self.path, '.git', 'FETCH_HEAD')
813 811 else:
814 812 fetch_heads_path = os.path.join(self.path, 'FETCH_HEAD')
815 813
816 814 heads = []
817 815 with open(fetch_heads_path) as f:
818 816 for line in f:
819 817 if ' not-for-merge ' in line:
820 818 continue
821 819 line = re.sub('\t.*', '', line, flags=re.DOTALL)
822 820 heads.append(line)
823 821
824 822 return heads
825 823
826 824 def get_shadow_instance(self, shadow_repository_path, enable_hooks=False, cache=False):
827 825 return GitRepository(shadow_repository_path, with_wire={"cache": cache})
828 826
829 827 def _local_pull(self, repository_path, branch_name, ff_only=True):
830 828 """
831 829 Pull a branch from a local repository.
832 830 """
833 831 if self.bare:
834 832 raise RepositoryError('Cannot pull into a bare git repository')
835 833 # N.B.(skreft): The --ff-only option is to make sure this is a
836 834 # fast-forward (i.e., we are only pulling new changes and there are no
837 835 # conflicts with our current branch)
838 836 # Additionally, that option needs to go before --no-tags, otherwise git
839 837 # pull complains about it being an unknown flag.
840 838 cmd = ['pull']
841 839 if ff_only:
842 840 cmd.append('--ff-only')
843 841 cmd.extend(['--no-tags', repository_path, branch_name])
844 842 self.run_git_command(cmd, fail_on_stderr=False)
845 843
846 844 def _local_merge(self, merge_message, user_name, user_email, heads):
847 845 """
848 846 Merge the given head into the checked out branch.
849 847
850 848 It will force a merge commit.
851 849
852 850 Currently it raises an error if the repo is empty, as it is not possible
853 851 to create a merge commit in an empty repo.
854 852
855 853 :param merge_message: The message to use for the merge commit.
856 854 :param heads: the heads to merge.
857 855 """
858 856 if self.bare:
859 857 raise RepositoryError('Cannot merge into a bare git repository')
860 858
861 859 if not heads:
862 860 return
863 861
864 862 if self.is_empty():
865 863 # TODO(skreft): do something more robust in this case.
866 864 raise RepositoryError('Do not know how to merge into empty repositories yet')
867 865 unresolved = None
868 866
869 867 # N.B.(skreft): the --no-ff option is used to enforce the creation of a
870 868 # commit message. We also specify the user who is doing the merge.
871 869 cmd = ['-c', f'user.name="{user_name}"',
872 870 '-c', f'user.email={user_email}',
873 871 'merge', '--no-ff', '-m', safe_str(merge_message)]
874 872
875 873 merge_cmd = cmd + heads
876 874
877 875 try:
878 876 self.run_git_command(merge_cmd, fail_on_stderr=False)
879 877 except RepositoryError:
880 878 files = self.run_git_command(['diff', '--name-only', '--diff-filter', 'U'],
881 879 fail_on_stderr=False)[0].splitlines()
882 880 # NOTE(marcink): we add U notation for consistent with HG backend output
883 unresolved = ['U {}'.format(f) for f in files]
881 unresolved = [f'U {f}' for f in files]
884 882
885 883 # Cleanup any merge leftovers
886 884 self._remote.invalidate_vcs_cache()
887 885 self.run_git_command(['merge', '--abort'], fail_on_stderr=False)
888 886
889 887 if unresolved:
890 888 raise UnresolvedFilesInRepo(unresolved)
891 889 else:
892 890 raise
893 891
894 892 def _local_push(
895 893 self, source_branch, repository_path, target_branch,
896 894 enable_hooks=False, rc_scm_data=None):
897 895 """
898 896 Push the source_branch to the given repository and target_branch.
899 897
900 898 Currently it if the target_branch is not master and the target repo is
901 899 empty, the push will work, but then GitRepository won't be able to find
902 900 the pushed branch or the commits. As the HEAD will be corrupted (i.e.,
903 901 pointing to master, which does not exist).
904 902
905 903 It does not run the hooks in the target repo.
906 904 """
907 905 # TODO(skreft): deal with the case in which the target repo is empty,
908 906 # and the target_branch is not master.
909 907 target_repo = GitRepository(repository_path)
910 908 if (not target_repo.bare and
911 909 target_repo._current_branch() == target_branch):
912 910 # Git prevents pushing to the checked out branch, so simulate it by
913 911 # pulling into the target repository.
914 912 target_repo._local_pull(self.path, source_branch)
915 913 else:
916 914 cmd = ['push', os.path.abspath(repository_path),
917 '%s:%s' % (source_branch, target_branch)]
915 '{}:{}'.format(source_branch, target_branch)]
918 916 gitenv = {}
919 917 if rc_scm_data:
920 918 gitenv.update({'RC_SCM_DATA': rc_scm_data})
921 919
922 920 if not enable_hooks:
923 921 gitenv['RC_SKIP_HOOKS'] = '1'
924 922 self.run_git_command(cmd, fail_on_stderr=False, extra_env=gitenv)
925 923
926 924 def _get_new_pr_branch(self, source_branch, target_branch):
927 prefix = 'pr_%s-%s_' % (source_branch, target_branch)
925 prefix = 'pr_{}-{}_'.format(source_branch, target_branch)
928 926 pr_branches = []
929 927 for branch in self.branches:
930 928 if branch.startswith(prefix):
931 929 pr_branches.append(int(branch[len(prefix):]))
932 930
933 931 if not pr_branches:
934 932 branch_id = 0
935 933 else:
936 934 branch_id = max(pr_branches) + 1
937 935
938 936 return '%s%d' % (prefix, branch_id)
939 937
940 938 def _maybe_prepare_merge_workspace(
941 939 self, repo_id, workspace_id, target_ref, source_ref):
942 940 shadow_repository_path = self._get_shadow_repository_path(
943 941 self.path, repo_id, workspace_id)
944 942 if not os.path.exists(shadow_repository_path):
945 943 self._local_clone(
946 944 shadow_repository_path, target_ref.name, source_ref.name)
947 945 log.debug('Prepared %s shadow repository in %s',
948 946 self.alias, shadow_repository_path)
949 947
950 948 return shadow_repository_path
951 949
952 950 def _merge_repo(self, repo_id, workspace_id, target_ref,
953 951 source_repo, source_ref, merge_message,
954 952 merger_name, merger_email, dry_run=False,
955 953 use_rebase=False, close_branch=False):
956 954
957 955 log.debug('Executing merge_repo with %s strategy, dry_run mode:%s',
958 956 'rebase' if use_rebase else 'merge', dry_run)
959 957 if target_ref.commit_id != self.branches[target_ref.name]:
960 958 log.warning('Target ref %s commit mismatch %s vs %s', target_ref,
961 959 target_ref.commit_id, self.branches[target_ref.name])
962 960 return MergeResponse(
963 961 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD,
964 962 metadata={'target_ref': target_ref})
965 963
966 964 shadow_repository_path = self._maybe_prepare_merge_workspace(
967 965 repo_id, workspace_id, target_ref, source_ref)
968 966 shadow_repo = self.get_shadow_instance(shadow_repository_path)
969 967
970 968 # checkout source, if it's different. Otherwise we could not
971 969 # fetch proper commits for merge testing
972 970 if source_ref.name != target_ref.name:
973 971 if shadow_repo.get_remote_ref(source_ref.name):
974 972 shadow_repo._checkout(source_ref.name, force=True)
975 973
976 974 # checkout target, and fetch changes
977 975 shadow_repo._checkout(target_ref.name, force=True)
978 976
979 977 # fetch/reset pull the target, in case it is changed
980 978 # this handles even force changes
981 979 shadow_repo._local_fetch(self.path, target_ref.name, use_origin=True)
982 980 shadow_repo._local_reset(target_ref.name)
983 981
984 982 # Need to reload repo to invalidate the cache, or otherwise we cannot
985 983 # retrieve the last target commit.
986 984 shadow_repo = self.get_shadow_instance(shadow_repository_path)
987 985 if target_ref.commit_id != shadow_repo.branches[target_ref.name]:
988 986 log.warning('Shadow Target ref %s commit mismatch %s vs %s',
989 987 target_ref, target_ref.commit_id,
990 988 shadow_repo.branches[target_ref.name])
991 989 return MergeResponse(
992 990 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD,
993 991 metadata={'target_ref': target_ref})
994 992
995 993 # calculate new branch
996 994 pr_branch = shadow_repo._get_new_pr_branch(
997 995 source_ref.name, target_ref.name)
998 996 log.debug('using pull-request merge branch: `%s`', pr_branch)
999 997 # checkout to temp branch, and fetch changes
1000 998 shadow_repo._checkout(pr_branch, create=True)
1001 999 try:
1002 1000 shadow_repo._local_fetch(source_repo.path, source_ref.name)
1003 1001 except RepositoryError:
1004 1002 log.exception('Failure when doing local fetch on '
1005 1003 'shadow repo: %s', shadow_repo)
1006 1004 return MergeResponse(
1007 1005 False, False, None, MergeFailureReason.MISSING_SOURCE_REF,
1008 1006 metadata={'source_ref': source_ref})
1009 1007
1010 1008 merge_ref = None
1011 1009 merge_failure_reason = MergeFailureReason.NONE
1012 1010 metadata = {}
1013 1011 try:
1014 1012 shadow_repo._local_merge(merge_message, merger_name, merger_email,
1015 1013 [source_ref.commit_id])
1016 1014 merge_possible = True
1017 1015
1018 1016 # Need to invalidate the cache, or otherwise we
1019 1017 # cannot retrieve the merge commit.
1020 1018 shadow_repo = shadow_repo.get_shadow_instance(shadow_repository_path)
1021 1019 merge_commit_id = shadow_repo.branches[pr_branch]
1022 1020
1023 1021 # Set a reference pointing to the merge commit. This reference may
1024 1022 # be used to easily identify the last successful merge commit in
1025 1023 # the shadow repository.
1026 1024 shadow_repo.set_refs('refs/heads/pr-merge', merge_commit_id)
1027 1025 merge_ref = Reference('branch', 'pr-merge', merge_commit_id)
1028 1026 except RepositoryError as e:
1029 1027 log.exception('Failure when doing local merge on git shadow repo')
1030 1028 if isinstance(e, UnresolvedFilesInRepo):
1031 1029 metadata['unresolved_files'] = '\n* conflict: ' + ('\n * conflict: '.join(e.args[0]))
1032 1030
1033 1031 merge_possible = False
1034 1032 merge_failure_reason = MergeFailureReason.MERGE_FAILED
1035 1033
1036 1034 if merge_possible and not dry_run:
1037 1035 try:
1038 1036 shadow_repo._local_push(
1039 1037 pr_branch, self.path, target_ref.name, enable_hooks=True,
1040 1038 rc_scm_data=self.config.get('rhodecode', 'RC_SCM_DATA'))
1041 1039 merge_succeeded = True
1042 1040 except RepositoryError:
1043 1041 log.exception(
1044 1042 'Failure when doing local push from the shadow '
1045 1043 'repository to the target repository at %s.', self.path)
1046 1044 merge_succeeded = False
1047 1045 merge_failure_reason = MergeFailureReason.PUSH_FAILED
1048 1046 metadata['target'] = 'git shadow repo'
1049 1047 metadata['merge_commit'] = pr_branch
1050 1048 else:
1051 1049 merge_succeeded = False
1052 1050
1053 1051 return MergeResponse(
1054 1052 merge_possible, merge_succeeded, merge_ref, merge_failure_reason,
1055 1053 metadata=metadata)
@@ -1,56 +1,54 b''
1
2
3 1 # Copyright (C) 2014-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 """
22 20 HG module
23 21 """
24 22 import os
25 23 import logging
26 24
27 25 from rhodecode.lib.vcs import connection
28 26 from rhodecode.lib.vcs.backends.hg.commit import MercurialCommit
29 27 from rhodecode.lib.vcs.backends.hg.inmemory import MercurialInMemoryCommit
30 28 from rhodecode.lib.vcs.backends.hg.repository import MercurialRepository
31 29
32 30
33 31 log = logging.getLogger(__name__)
34 32
35 33
36 34 def discover_hg_version(raise_on_exc=False):
37 35 """
38 36 Returns the string as it was returned by running 'git --version'
39 37
40 38 It will return an empty string in case the connection is not initialized
41 39 or no vcsserver is available.
42 40 """
43 41 try:
44 42 return connection.Hg.discover_hg_version()
45 43 except Exception:
46 44 log.warning("Failed to discover the HG version", exc_info=True)
47 45 if raise_on_exc:
48 46 raise
49 47 return ''
50 48
51 49
52 50 def largefiles_store(base_location):
53 51 """
54 52 Return a largefile store relative to base_location
55 53 """
56 54 return os.path.join(base_location, '.cache', 'largefiles')
@@ -1,405 +1,403 b''
1
2
3 1 # Copyright (C) 2014-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 """
22 20 HG commit module
23 21 """
24 22
25 23 import os
26 24
27 25 from zope.cachedescriptors.property import Lazy as LazyProperty
28 26
29 27 from rhodecode.lib.datelib import utcdate_fromtimestamp
30 28 from rhodecode.lib.str_utils import safe_bytes, safe_str
31 29 from rhodecode.lib.vcs import path as vcspath
32 30 from rhodecode.lib.vcs.backends import base
33 31 from rhodecode.lib.vcs.exceptions import CommitError
34 32 from rhodecode.lib.vcs.nodes import (
35 33 AddedFileNodesGenerator, ChangedFileNodesGenerator, DirNode, FileNode,
36 34 NodeKind, RemovedFileNodesGenerator, RootNode, SubModuleNode,
37 35 LargeFileNode)
38 36 from rhodecode.lib.vcs.utils.paths import get_dirs_for_path
39 37
40 38
41 39 class MercurialCommit(base.BaseCommit):
42 40 """
43 41 Represents state of the repository at the single commit.
44 42 """
45 43
46 44 _filter_pre_load = [
47 45 # git specific property not supported here
48 46 "_commit",
49 47 ]
50 48
51 49 def __init__(self, repository, raw_id, idx, pre_load=None):
52 50 raw_id = safe_str(raw_id)
53 51
54 52 self.repository = repository
55 53 self._remote = repository._remote
56 54
57 55 self.raw_id = raw_id
58 56 self.idx = idx
59 57
60 58 self._set_bulk_properties(pre_load)
61 59
62 60 # caches
63 61 self.nodes = {}
64 62 self._stat_modes = {} # stat info for paths
65 63
66 64 def _set_bulk_properties(self, pre_load):
67 65 if not pre_load:
68 66 return
69 67 pre_load = [entry for entry in pre_load
70 68 if entry not in self._filter_pre_load]
71 69 if not pre_load:
72 70 return
73 71
74 72 result = self._remote.bulk_request(self.raw_id, pre_load)
75 73
76 74 for attr, value in result.items():
77 75 if attr in ["author", "branch", "message"]:
78 76 value = safe_str(value)
79 77 elif attr == "affected_files":
80 78 value = list(map(safe_str, value))
81 79 elif attr == "date":
82 80 value = utcdate_fromtimestamp(*value)
83 81 elif attr in ["children", "parents"]:
84 82 value = self._make_commits(value)
85 83 elif attr in ["phase"]:
86 84 value = self._get_phase_text(value)
87 85 self.__dict__[attr] = value
88 86
89 87 @LazyProperty
90 88 def tags(self):
91 89 tags = [name for name, commit_id in self.repository.tags.items()
92 90 if commit_id == self.raw_id]
93 91 return tags
94 92
95 93 @LazyProperty
96 94 def branch(self):
97 95 return safe_str(self._remote.ctx_branch(self.raw_id))
98 96
99 97 @LazyProperty
100 98 def bookmarks(self):
101 99 bookmarks = [
102 100 name for name, commit_id in self.repository.bookmarks.items()
103 101 if commit_id == self.raw_id]
104 102 return bookmarks
105 103
106 104 @LazyProperty
107 105 def message(self):
108 106 return safe_str(self._remote.ctx_description(self.raw_id))
109 107
110 108 @LazyProperty
111 109 def committer(self):
112 110 return safe_str(self.author)
113 111
114 112 @LazyProperty
115 113 def author(self):
116 114 return safe_str(self._remote.ctx_user(self.raw_id))
117 115
118 116 @LazyProperty
119 117 def date(self):
120 118 return utcdate_fromtimestamp(*self._remote.ctx_date(self.raw_id))
121 119
122 120 @LazyProperty
123 121 def status(self):
124 122 """
125 123 Returns modified, added, removed, deleted files for current commit
126 124 """
127 125 return self._remote.ctx_status(self.raw_id)
128 126
129 127 @LazyProperty
130 128 def _file_paths(self):
131 129 return self._remote.ctx_list(self.raw_id)
132 130
133 131 @LazyProperty
134 132 def _dir_paths(self):
135 133 dir_paths = ['']
136 134 dir_paths.extend(list(set(get_dirs_for_path(*self._file_paths))))
137 135
138 136 return dir_paths
139 137
140 138 @LazyProperty
141 139 def _paths(self):
142 140 return self._dir_paths + self._file_paths
143 141
144 142 @LazyProperty
145 143 def id(self):
146 144 if self.last:
147 145 return 'tip'
148 146 return self.short_id
149 147
150 148 @LazyProperty
151 149 def short_id(self):
152 150 return self.raw_id[:12]
153 151
154 152 def _make_commits(self, commit_ids, pre_load=None):
155 153 return [self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
156 154 for commit_id in commit_ids]
157 155
158 156 @LazyProperty
159 157 def parents(self):
160 158 """
161 159 Returns list of parent commits.
162 160 """
163 161 parents = self._remote.ctx_parents(self.raw_id)
164 162 return self._make_commits(parents)
165 163
166 164 def _get_phase_text(self, phase_id):
167 165 return {
168 166 0: 'public',
169 167 1: 'draft',
170 168 2: 'secret',
171 169 }.get(phase_id) or ''
172 170
173 171 @LazyProperty
174 172 def phase(self):
175 173 phase_id = self._remote.ctx_phase(self.raw_id)
176 174 phase_text = self._get_phase_text(phase_id)
177 175
178 176 return safe_str(phase_text)
179 177
180 178 @LazyProperty
181 179 def obsolete(self):
182 180 obsolete = self._remote.ctx_obsolete(self.raw_id)
183 181 return obsolete
184 182
185 183 @LazyProperty
186 184 def hidden(self):
187 185 hidden = self._remote.ctx_hidden(self.raw_id)
188 186 return hidden
189 187
190 188 @LazyProperty
191 189 def children(self):
192 190 """
193 191 Returns list of child commits.
194 192 """
195 193 children = self._remote.ctx_children(self.raw_id)
196 194 return self._make_commits(children)
197 195
198 196 def _get_kind(self, path):
199 197 path = self._fix_path(path)
200 198 if path in self._file_paths:
201 199 return NodeKind.FILE
202 200 elif path in self._dir_paths:
203 201 return NodeKind.DIR
204 202 else:
205 203 raise CommitError(f"Node does not exist at the given path '{path}'")
206 204
207 205 def _assert_is_path(self, path) -> str:
208 206 path = self._fix_path(path)
209 207 if self._get_kind(path) != NodeKind.FILE:
210 208 raise CommitError(f"File does not exist for commit {self.raw_id} at '{path}'")
211 209
212 210 return path
213 211
214 212 def get_file_mode(self, path: bytes):
215 213 """
216 214 Returns stat mode of the file at the given ``path``.
217 215 """
218 216 path = self._assert_is_path(path)
219 217
220 218 if path not in self._stat_modes:
221 219 self._stat_modes[path] = self._remote.fctx_flags(self.raw_id, path)
222 220
223 221 if 'x' in self._stat_modes[path]:
224 222 return base.FILEMODE_EXECUTABLE
225 223 return base.FILEMODE_DEFAULT
226 224
227 225 def is_link(self, path):
228 226 path = self._assert_is_path(path)
229 227 if path not in self._stat_modes:
230 228 self._stat_modes[path] = self._remote.fctx_flags(self.raw_id, path)
231 229
232 230 return 'l' in self._stat_modes[path]
233 231
234 232 def is_node_binary(self, path):
235 233 path = self._assert_is_path(path)
236 234 return self._remote.is_binary(self.raw_id, path)
237 235
238 236 def node_md5_hash(self, path):
239 237 path = self._assert_is_path(path)
240 238 return self._remote.md5_hash(self.raw_id, path)
241 239
242 240 def get_file_content(self, path):
243 241 """
244 242 Returns content of the file at given ``path``.
245 243 """
246 244 path = self._assert_is_path(path)
247 245 return self._remote.fctx_node_data(self.raw_id, path)
248 246
249 247 def get_file_content_streamed(self, path):
250 248 path = self._assert_is_path(path)
251 249 stream_method = getattr(self._remote, 'stream:fctx_node_data')
252 250 return stream_method(self.raw_id, path)
253 251
254 252 def get_file_size(self, path):
255 253 """
256 254 Returns size of the file at given ``path``.
257 255 """
258 256 path = self._assert_is_path(path)
259 257 return self._remote.fctx_size(self.raw_id, path)
260 258
261 259 def get_path_history(self, path, limit=None, pre_load=None):
262 260 """
263 261 Returns history of file as reversed list of `MercurialCommit` objects
264 262 for which file at given ``path`` has been modified.
265 263 """
266 264 path = self._assert_is_path(path)
267 265 hist = self._remote.node_history(self.raw_id, path, limit)
268 266 return [
269 267 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
270 268 for commit_id in hist]
271 269
272 270 def get_file_annotate(self, path, pre_load=None):
273 271 """
274 272 Returns a generator of four element tuples with
275 273 lineno, commit_id, commit lazy loader and line
276 274 """
277 275 result = self._remote.fctx_annotate(self.raw_id, path)
278 276
279 277 for ln_no, commit_id, content in result:
280 278 yield (
281 279 ln_no, commit_id,
282 280 lambda: self.repository.get_commit(commit_id=commit_id, pre_load=pre_load),
283 281 content)
284 282
285 283 def get_nodes(self, path, pre_load=None):
286 284 """
287 285 Returns combined ``DirNode`` and ``FileNode`` objects list representing
288 286 state of commit at the given ``path``. If node at the given ``path``
289 287 is not instance of ``DirNode``, CommitError would be raised.
290 288 """
291 289
292 290 if self._get_kind(path) != NodeKind.DIR:
293 291 raise CommitError(
294 "Directory does not exist for idx %s at '%s'" % (self.raw_id, path))
292 "Directory does not exist for idx {} at '{}'".format(self.raw_id, path))
295 293 path = self._fix_path(path)
296 294
297 295 filenodes = [
298 296 FileNode(safe_bytes(f), commit=self, pre_load=pre_load) for f in self._file_paths
299 297 if os.path.dirname(f) == path]
300 298 # TODO: johbo: Check if this can be done in a more obvious way
301 299 dirs = path == '' and '' or [
302 300 d for d in self._dir_paths
303 301 if d and vcspath.dirname(d) == path]
304 302 dirnodes = [
305 303 DirNode(safe_bytes(d), commit=self) for d in dirs
306 304 if os.path.dirname(d) == path]
307 305
308 306 alias = self.repository.alias
309 307 for k, vals in self._submodules.items():
310 308 if vcspath.dirname(k) == path:
311 309 loc = vals[0]
312 310 commit = vals[1]
313 311 dirnodes.append(SubModuleNode(k, url=loc, commit=commit, alias=alias))
314 312
315 313 nodes = dirnodes + filenodes
316 314 for node in nodes:
317 315 if node.path not in self.nodes:
318 316 self.nodes[node.path] = node
319 317 nodes.sort()
320 318
321 319 return nodes
322 320
323 321 def get_node(self, path, pre_load=None):
324 322 """
325 323 Returns `Node` object from the given `path`. If there is no node at
326 324 the given `path`, `NodeDoesNotExistError` would be raised.
327 325 """
328 326 path = self._fix_path(path)
329 327
330 328 if path not in self.nodes:
331 329 if path in self._file_paths:
332 330 node = FileNode(safe_bytes(path), commit=self, pre_load=pre_load)
333 331 elif path in self._dir_paths:
334 332 if path == '':
335 333 node = RootNode(commit=self)
336 334 else:
337 335 node = DirNode(safe_bytes(path), commit=self)
338 336 else:
339 337 raise self.no_node_at_path(path)
340 338
341 339 # cache node
342 340 self.nodes[path] = node
343 341 return self.nodes[path]
344 342
345 343 def get_largefile_node(self, path):
346 344 pointer_spec = self._remote.is_large_file(self.raw_id, path)
347 345 if pointer_spec:
348 346 # content of that file regular FileNode is the hash of largefile
349 347 file_id = self.get_file_content(path).strip()
350 348
351 349 if self._remote.in_largefiles_store(file_id):
352 350 lf_path = self._remote.store_path(file_id)
353 351 return LargeFileNode(safe_bytes(lf_path), commit=self, org_path=path)
354 352 elif self._remote.in_user_cache(file_id):
355 353 lf_path = self._remote.store_path(file_id)
356 354 self._remote.link(file_id, path)
357 355 return LargeFileNode(safe_bytes(lf_path), commit=self, org_path=path)
358 356
359 357 @LazyProperty
360 358 def _submodules(self):
361 359 """
362 360 Returns a dictionary with submodule information from substate file
363 361 of hg repository.
364 362 """
365 363 return self._remote.ctx_substate(self.raw_id)
366 364
367 365 @LazyProperty
368 366 def affected_files(self):
369 367 """
370 368 Gets a fast accessible file changes for given commit
371 369 """
372 370 return self._remote.ctx_files(self.raw_id)
373 371
374 372 @property
375 373 def added(self):
376 374 """
377 375 Returns list of added ``FileNode`` objects.
378 376 """
379 377 return AddedFileNodesGenerator(self.added_paths, self)
380 378
381 379 @LazyProperty
382 380 def added_paths(self):
383 381 return [n for n in self.status[1]]
384 382
385 383 @property
386 384 def changed(self):
387 385 """
388 386 Returns list of modified ``FileNode`` objects.
389 387 """
390 388 return ChangedFileNodesGenerator(self.changed_paths, self)
391 389
392 390 @LazyProperty
393 391 def changed_paths(self):
394 392 return [n for n in self.status[0]]
395 393
396 394 @property
397 395 def removed(self):
398 396 """
399 397 Returns list of removed ``FileNode`` objects.
400 398 """
401 399 return RemovedFileNodesGenerator(self.removed_paths, self)
402 400
403 401 @LazyProperty
404 402 def removed_paths(self):
405 403 return [n for n in self.status[2]]
@@ -1,49 +1,47 b''
1
2
3 1 # Copyright (C) 2014-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 """
22 20 HG diff module
23 21 """
24 22
25 23 import re
26 24
27 25 from rhodecode.lib.vcs.backends import base
28 26
29 27
30 28 class MercurialDiff(base.Diff):
31 29
32 30 _header_re = re.compile(br"""
33 31 #^diff[ ]--git
34 32 [ ]"?a/(?P<a_path>.+?)"?[ ]"?b/(?P<b_path>.+?)"?\n
35 33 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
36 34 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
37 35 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))?
38 36 (?:^rename[ ]from[ ](?P<rename_from>[^\r\n]+)\n
39 37 ^rename[ ]to[ ](?P<rename_to>[^\r\n]+)(?:\n|$))?
40 38 (?:^copy[ ]from[ ](?P<copy_from>[^\r\n]+)\n
41 39 ^copy[ ]to[ ](?P<copy_to>[^\r\n]+)(?:\n|$))?
42 40 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
43 41 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
44 42 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
45 43 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
46 44 (?:^(?P<bin_patch>GIT[ ]binary[ ]patch)(?:\n|$))?
47 45 (?:^---[ ]("?a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
48 46 (?:^\+\+\+[ ]("?b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
49 47 """, re.VERBOSE | re.MULTILINE)
@@ -1,96 +1,94 b''
1
2
3 1 # Copyright (C) 2014-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 """
22 20 HG inmemory module
23 21 """
24 22
25 23 from rhodecode.lib.datelib import date_to_timestamp_plus_offset
26 24 from rhodecode.lib.str_utils import safe_str
27 25 from rhodecode.lib.vcs.backends.base import BaseInMemoryCommit
28 26 from rhodecode.lib.vcs.exceptions import RepositoryError
29 27
30 28
31 29 class MercurialInMemoryCommit(BaseInMemoryCommit):
32 30
33 31 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
34 32 """
35 33 Performs in-memory commit (doesn't check workdir in any way) and
36 34 returns newly created `MercurialCommit`. Updates repository's
37 35 `commit_ids`.
38 36
39 37 :param message: message of the commit
40 38 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
41 39 :param parents: single parent or sequence of parents from which commit
42 40 would be derived
43 41 :param date: `datetime.datetime` instance. Defaults to
44 42 ``datetime.datetime.now()``.
45 43 :param branch: Optional. Branch name as unicode. Will use the backend's
46 44 default if not given.
47 45
48 46 :raises `RepositoryError`: if any error occurs while committing
49 47 """
50 48 self.check_integrity(parents)
51 49
52 50 if not isinstance(message, str) or not isinstance(author, str):
53 51 # TODO: johbo: Should be a TypeError
54 52 raise RepositoryError(
55 53 f'Given message and author needs to be '
56 54 f'an <str> instance got {type(message)} & {type(author)} instead'
57 55 )
58 56
59 57 if branch is None:
60 58 branch = self.repository.DEFAULT_BRANCH_NAME
61 59 kwargs['branch'] = safe_str(branch)
62 60
63 61 message = safe_str(message)
64 62 author = safe_str(author)
65 63
66 64 parent_ids = [p.raw_id if p else None for p in self.parents]
67 65
68 66 updated = []
69 67 for node in self.added + self.changed:
70 68 content = node.content
71 69 # TODO: left for reference pre py3 migration, probably need to be removed
72 70 # if node.is_binary:
73 71 # content = node.content
74 72 # else:
75 73 # content = node.content.encode(ENCODING)
76 74 updated.append({
77 75 'path': node.path,
78 76 'content': content,
79 77 'mode': node.mode,
80 78 })
81 79
82 80 removed = [node.path for node in self.removed]
83 81
84 82 date, tz = date_to_timestamp_plus_offset(date)
85 83
86 84 commit_id = self.repository._remote.commitctx(
87 85 message=message, parents=parent_ids,
88 86 commit_time=date, commit_timezone=tz, user=author,
89 87 files=self.get_paths(), extra=kwargs, removed=removed,
90 88 updated=updated)
91 89 self.repository.append_commit_id(commit_id)
92 90
93 91 self.repository.branches = self.repository._get_branches()
94 92 tip = self.repository.get_commit(commit_id)
95 93 self.reset()
96 94 return tip
@@ -1,1015 +1,1013 b''
1
2
3 1 # Copyright (C) 2014-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 """
22 20 HG repository module
23 21 """
24 22 import os
25 23 import logging
26 24 import binascii
27 25 import configparser
28 26 import urllib.request
29 27 import urllib.parse
30 28 import urllib.error
31 29
32 30 from zope.cachedescriptors.property import Lazy as LazyProperty
33 31
34 32 from collections import OrderedDict
35 33 from rhodecode.lib.datelib import (
36 34 date_to_timestamp_plus_offset, utcdate_fromtimestamp, makedate)
37 35 from rhodecode.lib.str_utils import safe_str
38 36 from rhodecode.lib.utils2 import CachedProperty
39 37 from rhodecode.lib.vcs import connection, exceptions
40 38 from rhodecode.lib.vcs.backends.base import (
41 39 BaseRepository, CollectionGenerator, Config, MergeResponse,
42 40 MergeFailureReason, Reference, BasePathPermissionChecker)
43 41 from rhodecode.lib.vcs.backends.hg.commit import MercurialCommit
44 42 from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
45 43 from rhodecode.lib.vcs.backends.hg.inmemory import MercurialInMemoryCommit
46 44 from rhodecode.lib.vcs.exceptions import (
47 45 EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
48 46 TagDoesNotExistError, CommitDoesNotExistError, SubrepoMergeError, UnresolvedFilesInRepo)
49 47
50 48 hexlify = binascii.hexlify
51 49 nullid = "\0" * 20
52 50
53 51 log = logging.getLogger(__name__)
54 52
55 53
56 54 class MercurialRepository(BaseRepository):
57 55 """
58 56 Mercurial repository backend
59 57 """
60 58 DEFAULT_BRANCH_NAME = 'default'
61 59
62 60 def __init__(self, repo_path, config=None, create=False, src_url=None,
63 61 do_workspace_checkout=False, with_wire=None, bare=False):
64 62 """
65 63 Raises RepositoryError if repository could not be find at the given
66 64 ``repo_path``.
67 65
68 66 :param repo_path: local path of the repository
69 67 :param config: config object containing the repo configuration
70 68 :param create=False: if set to True, would try to create repository if
71 69 it does not exist rather than raising exception
72 70 :param src_url=None: would try to clone repository from given location
73 71 :param do_workspace_checkout=False: sets update of working copy after
74 72 making a clone
75 73 :param bare: not used, compatible with other VCS
76 74 """
77 75
78 76 self.path = safe_str(os.path.abspath(repo_path))
79 77 # mercurial since 4.4.X requires certain configuration to be present
80 78 # because sometimes we init the repos with config we need to meet
81 79 # special requirements
82 80 self.config = config if config else self.get_default_config(
83 81 default=[('extensions', 'largefiles', '1')])
84 82 self.with_wire = with_wire or {"cache": False} # default should not use cache
85 83
86 84 self._init_repo(create, src_url, do_workspace_checkout)
87 85
88 86 # caches
89 87 self._commit_ids = {}
90 88
91 89 @LazyProperty
92 90 def _remote(self):
93 91 repo_id = self.path
94 92 return connection.Hg(self.path, repo_id, self.config, with_wire=self.with_wire)
95 93
96 94 @CachedProperty
97 95 def commit_ids(self):
98 96 """
99 97 Returns list of commit ids, in ascending order. Being lazy
100 98 attribute allows external tools to inject shas from cache.
101 99 """
102 100 commit_ids = self._get_all_commit_ids()
103 101 self._rebuild_cache(commit_ids)
104 102 return commit_ids
105 103
106 104 def _rebuild_cache(self, commit_ids):
107 self._commit_ids = dict((commit_id, index)
108 for index, commit_id in enumerate(commit_ids))
105 self._commit_ids = {commit_id: index
106 for index, commit_id in enumerate(commit_ids)}
109 107
110 108 @CachedProperty
111 109 def branches(self):
112 110 return self._get_branches()
113 111
114 112 @CachedProperty
115 113 def branches_closed(self):
116 114 return self._get_branches(active=False, closed=True)
117 115
118 116 @CachedProperty
119 117 def branches_all(self):
120 118 all_branches = {}
121 119 all_branches.update(self.branches)
122 120 all_branches.update(self.branches_closed)
123 121 return all_branches
124 122
125 123 def _get_branches(self, active=True, closed=False):
126 124 """
127 125 Gets branches for this repository
128 126 Returns only not closed active branches by default
129 127
130 128 :param active: return also active branches
131 129 :param closed: return also closed branches
132 130
133 131 """
134 132 if self.is_empty():
135 133 return {}
136 134
137 135 def get_name(ctx):
138 136 return ctx[0]
139 137
140 138 _branches = [(n, h,) for n, h in
141 139 self._remote.branches(active, closed).items()]
142 140
143 141 return OrderedDict(sorted(_branches, key=get_name, reverse=False))
144 142
145 143 @CachedProperty
146 144 def tags(self):
147 145 """
148 146 Gets tags for this repository
149 147 """
150 148 return self._get_tags()
151 149
152 150 def _get_tags(self):
153 151 if self.is_empty():
154 152 return {}
155 153
156 154 def get_name(ctx):
157 155 return ctx[0]
158 156
159 157 _tags = [(n, h,) for n, h in
160 158 self._remote.tags().items()]
161 159
162 160 return OrderedDict(sorted(_tags, key=get_name, reverse=True))
163 161
164 162 def tag(self, name, user, commit_id=None, message=None, date=None, **kwargs):
165 163 """
166 164 Creates and returns a tag for the given ``commit_id``.
167 165
168 166 :param name: name for new tag
169 167 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
170 168 :param commit_id: commit id for which new tag would be created
171 169 :param message: message of the tag's commit
172 170 :param date: date of tag's commit
173 171
174 172 :raises TagAlreadyExistError: if tag with same name already exists
175 173 """
176 174 if name in self.tags:
177 175 raise TagAlreadyExistError("Tag %s already exists" % name)
178 176
179 177 commit = self.get_commit(commit_id=commit_id)
180 178 local = kwargs.setdefault('local', False)
181 179
182 180 if message is None:
183 message = "Added tag %s for commit %s" % (name, commit.short_id)
181 message = "Added tag {} for commit {}".format(name, commit.short_id)
184 182
185 183 date, tz = date_to_timestamp_plus_offset(date)
186 184
187 185 self._remote.tag(name, commit.raw_id, message, local, user, date, tz)
188 186 self._remote.invalidate_vcs_cache()
189 187
190 188 # Reinitialize tags
191 189 self._invalidate_prop_cache('tags')
192 190 tag_id = self.tags[name]
193 191
194 192 return self.get_commit(commit_id=tag_id)
195 193
196 194 def remove_tag(self, name, user, message=None, date=None):
197 195 """
198 196 Removes tag with the given `name`.
199 197
200 198 :param name: name of the tag to be removed
201 199 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
202 200 :param message: message of the tag's removal commit
203 201 :param date: date of tag's removal commit
204 202
205 203 :raises TagDoesNotExistError: if tag with given name does not exists
206 204 """
207 205 if name not in self.tags:
208 206 raise TagDoesNotExistError("Tag %s does not exist" % name)
209 207
210 208 if message is None:
211 209 message = "Removed tag %s" % name
212 210 local = False
213 211
214 212 date, tz = date_to_timestamp_plus_offset(date)
215 213
216 214 self._remote.tag(name, nullid, message, local, user, date, tz)
217 215 self._remote.invalidate_vcs_cache()
218 216 self._invalidate_prop_cache('tags')
219 217
220 218 @LazyProperty
221 219 def bookmarks(self):
222 220 """
223 221 Gets bookmarks for this repository
224 222 """
225 223 return self._get_bookmarks()
226 224
227 225 def _get_bookmarks(self):
228 226 if self.is_empty():
229 227 return {}
230 228
231 229 def get_name(ctx):
232 230 return ctx[0]
233 231
234 232 _bookmarks = [
235 233 (n, h) for n, h in
236 234 self._remote.bookmarks().items()]
237 235
238 236 return OrderedDict(sorted(_bookmarks, key=get_name))
239 237
240 238 def _get_all_commit_ids(self):
241 239 return self._remote.get_all_commit_ids('visible')
242 240
243 241 def get_diff(
244 242 self, commit1, commit2, path='', ignore_whitespace=False,
245 243 context=3, path1=None):
246 244 """
247 245 Returns (git like) *diff*, as plain text. Shows changes introduced by
248 246 `commit2` since `commit1`.
249 247
250 248 :param commit1: Entry point from which diff is shown. Can be
251 249 ``self.EMPTY_COMMIT`` - in this case, patch showing all
252 250 the changes since empty state of the repository until `commit2`
253 251 :param commit2: Until which commit changes should be shown.
254 252 :param ignore_whitespace: If set to ``True``, would not show whitespace
255 253 changes. Defaults to ``False``.
256 254 :param context: How many lines before/after changed lines should be
257 255 shown. Defaults to ``3``.
258 256 """
259 257 self._validate_diff_commits(commit1, commit2)
260 258 if path1 is not None and path1 != path:
261 259 raise ValueError("Diff of two different paths not supported.")
262 260
263 261 if path:
264 262 file_filter = [self.path, path]
265 263 else:
266 264 file_filter = None
267 265
268 266 diff = self._remote.diff(
269 267 commit1.raw_id, commit2.raw_id, file_filter=file_filter,
270 268 opt_git=True, opt_ignorews=ignore_whitespace,
271 269 context=context)
272 270 return MercurialDiff(diff)
273 271
274 272 def strip(self, commit_id, branch=None):
275 273 self._remote.strip(commit_id, update=False, backup="none")
276 274
277 275 self._remote.invalidate_vcs_cache()
278 276 # clear cache
279 277 self._invalidate_prop_cache('commit_ids')
280 278
281 279 return len(self.commit_ids)
282 280
283 281 def verify(self):
284 282 verify = self._remote.verify()
285 283
286 284 self._remote.invalidate_vcs_cache()
287 285 return verify
288 286
289 287 def hg_update_cache(self):
290 288 update_cache = self._remote.hg_update_cache()
291 289
292 290 self._remote.invalidate_vcs_cache()
293 291 return update_cache
294 292
295 293 def hg_rebuild_fn_cache(self):
296 294 update_cache = self._remote.hg_rebuild_fn_cache()
297 295
298 296 self._remote.invalidate_vcs_cache()
299 297 return update_cache
300 298
301 299 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
302 300 log.debug('Calculating common ancestor between %sc1:%s and %sc2:%s',
303 301 self, commit_id1, repo2, commit_id2)
304 302
305 303 if commit_id1 == commit_id2:
306 304 return commit_id1
307 305
308 306 ancestors = self._remote.revs_from_revspec(
309 307 "ancestor(id(%s), id(%s))", commit_id1, commit_id2,
310 308 other_path=repo2.path)
311 309
312 310 ancestor_id = repo2[ancestors[0]].raw_id if ancestors else None
313 311
314 312 log.debug('Found common ancestor with sha: %s', ancestor_id)
315 313 return ancestor_id
316 314
317 315 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
318 316 if commit_id1 == commit_id2:
319 317 commits = []
320 318 else:
321 319 if merge:
322 320 indexes = self._remote.revs_from_revspec(
323 321 "ancestors(id(%s)) - ancestors(id(%s)) - id(%s)",
324 322 commit_id2, commit_id1, commit_id1, other_path=repo2.path)
325 323 else:
326 324 indexes = self._remote.revs_from_revspec(
327 325 "id(%s)..id(%s) - id(%s)", commit_id1, commit_id2,
328 326 commit_id1, other_path=repo2.path)
329 327
330 328 commits = [repo2.get_commit(commit_idx=idx, pre_load=pre_load)
331 329 for idx in indexes]
332 330
333 331 return commits
334 332
335 333 @staticmethod
336 334 def check_url(url, config):
337 335 """
338 336 Function will check given url and try to verify if it's a valid
339 337 link. Sometimes it may happened that mercurial will issue basic
340 338 auth request that can cause whole API to hang when used from python
341 339 or other external calls.
342 340
343 341 On failures it'll raise urllib2.HTTPError, exception is also thrown
344 342 when the return code is non 200
345 343 """
346 344 # check first if it's not an local url
347 345 if os.path.isdir(url) or url.startswith('file:'):
348 346 return True
349 347
350 348 # Request the _remote to verify the url
351 349 return connection.Hg.check_url(url, config.serialize())
352 350
353 351 @staticmethod
354 352 def is_valid_repository(path):
355 353 return os.path.isdir(os.path.join(path, '.hg'))
356 354
357 355 def _init_repo(self, create, src_url=None, do_workspace_checkout=False):
358 356 """
359 357 Function will check for mercurial repository in given path. If there
360 358 is no repository in that path it will raise an exception unless
361 359 `create` parameter is set to True - in that case repository would
362 360 be created.
363 361
364 362 If `src_url` is given, would try to clone repository from the
365 363 location at given clone_point. Additionally it'll make update to
366 364 working copy accordingly to `do_workspace_checkout` flag.
367 365 """
368 366 if create and os.path.exists(self.path):
369 367 raise RepositoryError(
370 368 f"Cannot create repository at {self.path}, location already exist")
371 369
372 370 if src_url:
373 371 url = str(self._get_url(src_url))
374 372 MercurialRepository.check_url(url, self.config)
375 373
376 374 self._remote.clone(url, self.path, do_workspace_checkout)
377 375
378 376 # Don't try to create if we've already cloned repo
379 377 create = False
380 378
381 379 if create:
382 380 os.makedirs(self.path, mode=0o755)
383 381
384 382 self._remote.localrepository(create)
385 383
386 384 @LazyProperty
387 385 def in_memory_commit(self):
388 386 return MercurialInMemoryCommit(self)
389 387
390 388 @LazyProperty
391 389 def description(self):
392 390 description = self._remote.get_config_value(
393 391 'web', 'description', untrusted=True)
394 392 return safe_str(description or self.DEFAULT_DESCRIPTION)
395 393
396 394 @LazyProperty
397 395 def contact(self):
398 396 contact = (
399 397 self._remote.get_config_value("web", "contact") or
400 398 self._remote.get_config_value("ui", "username"))
401 399 return safe_str(contact or self.DEFAULT_CONTACT)
402 400
403 401 @LazyProperty
404 402 def last_change(self):
405 403 """
406 404 Returns last change made on this repository as
407 405 `datetime.datetime` object.
408 406 """
409 407 try:
410 408 return self.get_commit().date
411 409 except RepositoryError:
412 410 tzoffset = makedate()[1]
413 411 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
414 412
415 413 def _get_fs_mtime(self):
416 414 # fallback to filesystem
417 415 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
418 416 st_path = os.path.join(self.path, '.hg', "store")
419 417 if os.path.exists(cl_path):
420 418 return os.stat(cl_path).st_mtime
421 419 else:
422 420 return os.stat(st_path).st_mtime
423 421
424 422 def _get_url(self, url):
425 423 """
426 424 Returns normalized url. If schema is not given, would fall
427 425 to filesystem
428 426 (``file:///``) schema.
429 427 """
430 428 if url != 'default' and '://' not in url:
431 429 url = "file:" + urllib.request.pathname2url(url)
432 430 return url
433 431
434 432 def get_hook_location(self):
435 433 """
436 434 returns absolute path to location where hooks are stored
437 435 """
438 436 return os.path.join(self.path, '.hg', '.hgrc')
439 437
440 438 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
441 439 translate_tag=None, maybe_unreachable=False, reference_obj=None):
442 440 """
443 441 Returns ``MercurialCommit`` object representing repository's
444 442 commit at the given `commit_id` or `commit_idx`.
445 443 """
446 444 if self.is_empty():
447 445 raise EmptyRepositoryError("There are no commits yet")
448 446
449 447 if commit_id is not None:
450 448 self._validate_commit_id(commit_id)
451 449 try:
452 450 # we have cached idx, use it without contacting the remote
453 451 idx = self._commit_ids[commit_id]
454 452 return MercurialCommit(self, commit_id, idx, pre_load=pre_load)
455 453 except KeyError:
456 454 pass
457 455
458 456 elif commit_idx is not None:
459 457 self._validate_commit_idx(commit_idx)
460 458 try:
461 459 _commit_id = self.commit_ids[commit_idx]
462 460 if commit_idx < 0:
463 461 commit_idx = self.commit_ids.index(_commit_id)
464 462
465 463 return MercurialCommit(self, _commit_id, commit_idx, pre_load=pre_load)
466 464 except IndexError:
467 465 commit_id = commit_idx
468 466 else:
469 467 commit_id = "tip"
470 468
471 469 # case here is no cached version, do an actual lookup instead
472 470 try:
473 471 raw_id, idx = self._remote.lookup(commit_id, both=True)
474 472 except CommitDoesNotExistError:
475 473 msg = "Commit {} does not exist for `{}`".format(
476 474 *map(safe_str, [commit_id, self.name]))
477 475 raise CommitDoesNotExistError(msg)
478 476
479 477 return MercurialCommit(self, raw_id, idx, pre_load=pre_load)
480 478
481 479 def get_commits(
482 480 self, start_id=None, end_id=None, start_date=None, end_date=None,
483 481 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
484 482 """
485 483 Returns generator of ``MercurialCommit`` objects from start to end
486 484 (both are inclusive)
487 485
488 486 :param start_id: None, str(commit_id)
489 487 :param end_id: None, str(commit_id)
490 488 :param start_date: if specified, commits with commit date less than
491 489 ``start_date`` would be filtered out from returned set
492 490 :param end_date: if specified, commits with commit date greater than
493 491 ``end_date`` would be filtered out from returned set
494 492 :param branch_name: if specified, commits not reachable from given
495 493 branch would be filtered out from returned set
496 494 :param show_hidden: Show hidden commits such as obsolete or hidden from
497 495 Mercurial evolve
498 496 :raise BranchDoesNotExistError: If given ``branch_name`` does not
499 497 exist.
500 498 :raise CommitDoesNotExistError: If commit for given ``start`` or
501 499 ``end`` could not be found.
502 500 """
503 501 # actually we should check now if it's not an empty repo
504 502 if self.is_empty():
505 503 raise EmptyRepositoryError("There are no commits yet")
506 504 self._validate_branch_name(branch_name)
507 505
508 506 branch_ancestors = False
509 507 if start_id is not None:
510 508 self._validate_commit_id(start_id)
511 509 c_start = self.get_commit(commit_id=start_id)
512 510 start_pos = self._commit_ids[c_start.raw_id]
513 511 else:
514 512 start_pos = None
515 513
516 514 if end_id is not None:
517 515 self._validate_commit_id(end_id)
518 516 c_end = self.get_commit(commit_id=end_id)
519 517 end_pos = max(0, self._commit_ids[c_end.raw_id])
520 518 else:
521 519 end_pos = None
522 520
523 521 if None not in [start_id, end_id] and start_pos > end_pos:
524 522 raise RepositoryError(
525 523 "Start commit '%s' cannot be after end commit '%s'" %
526 524 (start_id, end_id))
527 525
528 526 if end_pos is not None:
529 527 end_pos += 1
530 528
531 529 commit_filter = []
532 530
533 531 if branch_name and not branch_ancestors:
534 commit_filter.append('branch("%s")' % (branch_name,))
532 commit_filter.append('branch("{}")'.format(branch_name))
535 533 elif branch_name and branch_ancestors:
536 commit_filter.append('ancestors(branch("%s"))' % (branch_name,))
534 commit_filter.append('ancestors(branch("{}"))'.format(branch_name))
537 535
538 536 if start_date and not end_date:
539 commit_filter.append('date(">%s")' % (start_date,))
537 commit_filter.append('date(">{}")'.format(start_date))
540 538 if end_date and not start_date:
541 commit_filter.append('date("<%s")' % (end_date,))
539 commit_filter.append('date("<{}")'.format(end_date))
542 540 if start_date and end_date:
543 541 commit_filter.append(
544 'date(">%s") and date("<%s")' % (start_date, end_date))
542 'date(">{}") and date("<{}")'.format(start_date, end_date))
545 543
546 544 if not show_hidden:
547 545 commit_filter.append('not obsolete()')
548 546 commit_filter.append('not hidden()')
549 547
550 548 # TODO: johbo: Figure out a simpler way for this solution
551 549 collection_generator = CollectionGenerator
552 550 if commit_filter:
553 551 commit_filter = ' and '.join(map(safe_str, commit_filter))
554 552 revisions = self._remote.rev_range([commit_filter])
555 553 collection_generator = MercurialIndexBasedCollectionGenerator
556 554 else:
557 555 revisions = self.commit_ids
558 556
559 557 if start_pos or end_pos:
560 558 revisions = revisions[start_pos:end_pos]
561 559
562 560 return collection_generator(self, revisions, pre_load=pre_load)
563 561
564 562 def pull(self, url, commit_ids=None):
565 563 """
566 564 Pull changes from external location.
567 565
568 566 :param commit_ids: Optional. Can be set to a list of commit ids
569 567 which shall be pulled from the other repository.
570 568 """
571 569 url = self._get_url(url)
572 570 self._remote.pull(url, commit_ids=commit_ids)
573 571 self._remote.invalidate_vcs_cache()
574 572
575 573 def fetch(self, url, commit_ids=None):
576 574 """
577 575 Backward compatibility with GIT fetch==pull
578 576 """
579 577 return self.pull(url, commit_ids=commit_ids)
580 578
581 579 def push(self, url):
582 580 url = self._get_url(url)
583 581 self._remote.sync_push(url)
584 582
585 583 def _local_clone(self, clone_path):
586 584 """
587 585 Create a local clone of the current repo.
588 586 """
589 587 self._remote.clone(self.path, clone_path, update_after_clone=True,
590 588 hooks=False)
591 589
592 590 def _update(self, revision, clean=False):
593 591 """
594 592 Update the working copy to the specified revision.
595 593 """
596 594 log.debug('Doing checkout to commit: `%s` for %s', revision, self)
597 595 self._remote.update(revision, clean=clean)
598 596
599 597 def _identify(self):
600 598 """
601 599 Return the current state of the working directory.
602 600 """
603 601 return self._remote.identify().strip().rstrip('+')
604 602
605 603 def _heads(self, branch=None):
606 604 """
607 605 Return the commit ids of the repository heads.
608 606 """
609 607 return self._remote.heads(branch=branch).strip().split(' ')
610 608
611 609 def _ancestor(self, revision1, revision2):
612 610 """
613 611 Return the common ancestor of the two revisions.
614 612 """
615 613 return self._remote.ancestor(revision1, revision2)
616 614
617 615 def _local_push(
618 616 self, revision, repository_path, push_branches=False,
619 617 enable_hooks=False):
620 618 """
621 619 Push the given revision to the specified repository.
622 620
623 621 :param push_branches: allow to create branches in the target repo.
624 622 """
625 623 self._remote.push(
626 624 [revision], repository_path, hooks=enable_hooks,
627 625 push_branches=push_branches)
628 626
629 627 def _local_merge(self, target_ref, merge_message, user_name, user_email,
630 628 source_ref, use_rebase=False, close_commit_id=None, dry_run=False):
631 629 """
632 630 Merge the given source_revision into the checked out revision.
633 631
634 632 Returns the commit id of the merge and a boolean indicating if the
635 633 commit needs to be pushed.
636 634 """
637 635 source_ref_commit_id = source_ref.commit_id
638 636 target_ref_commit_id = target_ref.commit_id
639 637
640 638 # update our workdir to target ref, for proper merge
641 639 self._update(target_ref_commit_id, clean=True)
642 640
643 641 ancestor = self._ancestor(target_ref_commit_id, source_ref_commit_id)
644 642 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
645 643
646 644 if close_commit_id:
647 645 # NOTE(marcink): if we get the close commit, this is our new source
648 646 # which will include the close commit itself.
649 647 source_ref_commit_id = close_commit_id
650 648
651 649 if ancestor == source_ref_commit_id:
652 650 # Nothing to do, the changes were already integrated
653 651 return target_ref_commit_id, False
654 652
655 653 elif ancestor == target_ref_commit_id and is_the_same_branch:
656 654 # In this case we should force a commit message
657 655 return source_ref_commit_id, True
658 656
659 657 unresolved = None
660 658 if use_rebase:
661 659 try:
662 bookmark_name = 'rcbook%s%s' % (source_ref_commit_id, target_ref_commit_id)
660 bookmark_name = 'rcbook{}{}'.format(source_ref_commit_id, target_ref_commit_id)
663 661 self.bookmark(bookmark_name, revision=source_ref.commit_id)
664 662 self._remote.rebase(
665 663 source=source_ref_commit_id, dest=target_ref_commit_id)
666 664 self._remote.invalidate_vcs_cache()
667 665 self._update(bookmark_name, clean=True)
668 666 return self._identify(), True
669 667 except RepositoryError as e:
670 668 # The rebase-abort may raise another exception which 'hides'
671 669 # the original one, therefore we log it here.
672 670 log.exception('Error while rebasing shadow repo during merge.')
673 671 if 'unresolved conflicts' in safe_str(e):
674 672 unresolved = self._remote.get_unresolved_files()
675 673 log.debug('unresolved files: %s', unresolved)
676 674
677 675 # Cleanup any rebase leftovers
678 676 self._remote.invalidate_vcs_cache()
679 677 self._remote.rebase(abort=True)
680 678 self._remote.invalidate_vcs_cache()
681 679 self._remote.update(clean=True)
682 680 if unresolved:
683 681 raise UnresolvedFilesInRepo(unresolved)
684 682 else:
685 683 raise
686 684 else:
687 685 try:
688 686 self._remote.merge(source_ref_commit_id)
689 687 self._remote.invalidate_vcs_cache()
690 688 self._remote.commit(
691 689 message=safe_str(merge_message),
692 username=safe_str('%s <%s>' % (user_name, user_email)))
690 username=safe_str('{} <{}>'.format(user_name, user_email)))
693 691 self._remote.invalidate_vcs_cache()
694 692 return self._identify(), True
695 693 except RepositoryError as e:
696 694 # The merge-abort may raise another exception which 'hides'
697 695 # the original one, therefore we log it here.
698 696 log.exception('Error while merging shadow repo during merge.')
699 697 if 'unresolved merge conflicts' in safe_str(e):
700 698 unresolved = self._remote.get_unresolved_files()
701 699 log.debug('unresolved files: %s', unresolved)
702 700
703 701 # Cleanup any merge leftovers
704 702 self._remote.update(clean=True)
705 703 if unresolved:
706 704 raise UnresolvedFilesInRepo(unresolved)
707 705 else:
708 706 raise
709 707
710 708 def _local_close(self, target_ref, user_name, user_email,
711 709 source_ref, close_message=''):
712 710 """
713 711 Close the branch of the given source_revision
714 712
715 713 Returns the commit id of the close and a boolean indicating if the
716 714 commit needs to be pushed.
717 715 """
718 716 self._update(source_ref.commit_id)
719 message = close_message or "Closing branch: `{}`".format(source_ref.name)
717 message = close_message or f"Closing branch: `{source_ref.name}`"
720 718 try:
721 719 self._remote.commit(
722 720 message=safe_str(message),
723 username=safe_str('%s <%s>' % (user_name, user_email)),
721 username=safe_str('{} <{}>'.format(user_name, user_email)),
724 722 close_branch=True)
725 723 self._remote.invalidate_vcs_cache()
726 724 return self._identify(), True
727 725 except RepositoryError:
728 726 # Cleanup any commit leftovers
729 727 self._remote.update(clean=True)
730 728 raise
731 729
732 730 def _is_the_same_branch(self, target_ref, source_ref):
733 731 return (
734 732 self._get_branch_name(target_ref) ==
735 733 self._get_branch_name(source_ref))
736 734
737 735 def _get_branch_name(self, ref):
738 736 if ref.type == 'branch':
739 737 return ref.name
740 738 return self._remote.ctx_branch(ref.commit_id)
741 739
742 740 def _maybe_prepare_merge_workspace(
743 741 self, repo_id, workspace_id, unused_target_ref, unused_source_ref):
744 742 shadow_repository_path = self._get_shadow_repository_path(
745 743 self.path, repo_id, workspace_id)
746 744 if not os.path.exists(shadow_repository_path):
747 745 self._local_clone(shadow_repository_path)
748 746 log.debug(
749 747 'Prepared shadow repository in %s', shadow_repository_path)
750 748
751 749 return shadow_repository_path
752 750
753 751 def _merge_repo(self, repo_id, workspace_id, target_ref,
754 752 source_repo, source_ref, merge_message,
755 753 merger_name, merger_email, dry_run=False,
756 754 use_rebase=False, close_branch=False):
757 755
758 756 log.debug('Executing merge_repo with %s strategy, dry_run mode:%s',
759 757 'rebase' if use_rebase else 'merge', dry_run)
760 758 if target_ref.commit_id not in self._heads():
761 759 return MergeResponse(
762 760 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD,
763 761 metadata={'target_ref': target_ref})
764 762
765 763 try:
766 764 if target_ref.type == 'branch' and len(self._heads(target_ref.name)) != 1:
767 765 heads_all = self._heads(target_ref.name)
768 766 max_heads = 10
769 767 if len(heads_all) > max_heads:
770 768 heads = '\n,'.join(
771 769 heads_all[:max_heads] +
772 770 ['and {} more.'.format(len(heads_all)-max_heads)])
773 771 else:
774 772 heads = '\n,'.join(heads_all)
775 773 metadata = {
776 774 'target_ref': target_ref,
777 775 'source_ref': source_ref,
778 776 'heads': heads
779 777 }
780 778 return MergeResponse(
781 779 False, False, None,
782 780 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS,
783 781 metadata=metadata)
784 782 except CommitDoesNotExistError:
785 783 log.exception('Failure when looking up branch heads on hg target')
786 784 return MergeResponse(
787 785 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
788 786 metadata={'target_ref': target_ref})
789 787
790 788 shadow_repository_path = self._maybe_prepare_merge_workspace(
791 789 repo_id, workspace_id, target_ref, source_ref)
792 790 shadow_repo = self.get_shadow_instance(shadow_repository_path)
793 791
794 792 log.debug('Pulling in target reference %s', target_ref)
795 793 self._validate_pull_reference(target_ref)
796 794 shadow_repo._local_pull(self.path, target_ref)
797 795
798 796 try:
799 797 log.debug('Pulling in source reference %s', source_ref)
800 798 source_repo._validate_pull_reference(source_ref)
801 799 shadow_repo._local_pull(source_repo.path, source_ref)
802 800 except CommitDoesNotExistError:
803 801 log.exception('Failure when doing local pull on hg shadow repo')
804 802 return MergeResponse(
805 803 False, False, None, MergeFailureReason.MISSING_SOURCE_REF,
806 804 metadata={'source_ref': source_ref})
807 805
808 806 merge_ref = None
809 807 merge_commit_id = None
810 808 close_commit_id = None
811 809 merge_failure_reason = MergeFailureReason.NONE
812 810 metadata = {}
813 811
814 812 # enforce that close branch should be used only in case we source from
815 813 # an actual Branch
816 814 close_branch = close_branch and source_ref.type == 'branch'
817 815
818 816 # don't allow to close branch if source and target are the same
819 817 close_branch = close_branch and source_ref.name != target_ref.name
820 818
821 819 needs_push_on_close = False
822 820 if close_branch and not use_rebase and not dry_run:
823 821 try:
824 822 close_commit_id, needs_push_on_close = shadow_repo._local_close(
825 823 target_ref, merger_name, merger_email, source_ref)
826 824 merge_possible = True
827 825 except RepositoryError:
828 826 log.exception('Failure when doing close branch on '
829 827 'shadow repo: %s', shadow_repo)
830 828 merge_possible = False
831 829 merge_failure_reason = MergeFailureReason.MERGE_FAILED
832 830 else:
833 831 merge_possible = True
834 832
835 833 needs_push = False
836 834 if merge_possible:
837 835
838 836 try:
839 837 merge_commit_id, needs_push = shadow_repo._local_merge(
840 838 target_ref, merge_message, merger_name, merger_email,
841 839 source_ref, use_rebase=use_rebase,
842 840 close_commit_id=close_commit_id, dry_run=dry_run)
843 841 merge_possible = True
844 842
845 843 # read the state of the close action, if it
846 844 # maybe required a push
847 845 needs_push = needs_push or needs_push_on_close
848 846
849 847 # Set a bookmark pointing to the merge commit. This bookmark
850 848 # may be used to easily identify the last successful merge
851 849 # commit in the shadow repository.
852 850 shadow_repo.bookmark('pr-merge', revision=merge_commit_id)
853 851 merge_ref = Reference('book', 'pr-merge', merge_commit_id)
854 852 except SubrepoMergeError:
855 853 log.exception(
856 854 'Subrepo merge error during local merge on hg shadow repo.')
857 855 merge_possible = False
858 856 merge_failure_reason = MergeFailureReason.SUBREPO_MERGE_FAILED
859 857 needs_push = False
860 858 except RepositoryError as e:
861 859 log.exception('Failure when doing local merge on hg shadow repo')
862 860 if isinstance(e, UnresolvedFilesInRepo):
863 861 all_conflicts = list(e.args[0])
864 862 max_conflicts = 20
865 863 if len(all_conflicts) > max_conflicts:
866 864 conflicts = all_conflicts[:max_conflicts] \
867 865 + ['and {} more.'.format(len(all_conflicts)-max_conflicts)]
868 866 else:
869 867 conflicts = all_conflicts
870 868 metadata['unresolved_files'] = \
871 869 '\n* conflict: ' + \
872 870 ('\n * conflict: '.join(conflicts))
873 871
874 872 merge_possible = False
875 873 merge_failure_reason = MergeFailureReason.MERGE_FAILED
876 874 needs_push = False
877 875
878 876 if merge_possible and not dry_run:
879 877 if needs_push:
880 878 # In case the target is a bookmark, update it, so after pushing
881 879 # the bookmarks is also updated in the target.
882 880 if target_ref.type == 'book':
883 881 shadow_repo.bookmark(
884 882 target_ref.name, revision=merge_commit_id)
885 883 try:
886 884 shadow_repo_with_hooks = self.get_shadow_instance(
887 885 shadow_repository_path,
888 886 enable_hooks=True)
889 887 # This is the actual merge action, we push from shadow
890 888 # into origin.
891 889 # Note: the push_branches option will push any new branch
892 890 # defined in the source repository to the target. This may
893 891 # be dangerous as branches are permanent in Mercurial.
894 892 # This feature was requested in issue #441.
895 893 shadow_repo_with_hooks._local_push(
896 894 merge_commit_id, self.path, push_branches=True,
897 895 enable_hooks=True)
898 896
899 897 # maybe we also need to push the close_commit_id
900 898 if close_commit_id:
901 899 shadow_repo_with_hooks._local_push(
902 900 close_commit_id, self.path, push_branches=True,
903 901 enable_hooks=True)
904 902 merge_succeeded = True
905 903 except RepositoryError:
906 904 log.exception(
907 905 'Failure when doing local push from the shadow '
908 906 'repository to the target repository at %s.', self.path)
909 907 merge_succeeded = False
910 908 merge_failure_reason = MergeFailureReason.PUSH_FAILED
911 909 metadata['target'] = 'hg shadow repo'
912 910 metadata['merge_commit'] = merge_commit_id
913 911 else:
914 912 merge_succeeded = True
915 913 else:
916 914 merge_succeeded = False
917 915
918 916 return MergeResponse(
919 917 merge_possible, merge_succeeded, merge_ref, merge_failure_reason,
920 918 metadata=metadata)
921 919
922 920 def get_shadow_instance(self, shadow_repository_path, enable_hooks=False, cache=False):
923 921 config = self.config.copy()
924 922 if not enable_hooks:
925 923 config.clear_section('hooks')
926 924 return MercurialRepository(shadow_repository_path, config, with_wire={"cache": cache})
927 925
928 926 def _validate_pull_reference(self, reference):
929 927 if not (reference.name in self.bookmarks or
930 928 reference.name in self.branches or
931 929 self.get_commit(reference.commit_id)):
932 930 raise CommitDoesNotExistError(
933 931 'Unknown branch, bookmark or commit id')
934 932
935 933 def _local_pull(self, repository_path, reference):
936 934 """
937 935 Fetch a branch, bookmark or commit from a local repository.
938 936 """
939 937 repository_path = os.path.abspath(repository_path)
940 938 if repository_path == self.path:
941 939 raise ValueError('Cannot pull from the same repository')
942 940
943 941 reference_type_to_option_name = {
944 942 'book': 'bookmark',
945 943 'branch': 'branch',
946 944 }
947 945 option_name = reference_type_to_option_name.get(
948 946 reference.type, 'revision')
949 947
950 948 if option_name == 'revision':
951 949 ref = reference.commit_id
952 950 else:
953 951 ref = reference.name
954 952
955 953 options = {option_name: [ref]}
956 954 self._remote.pull_cmd(repository_path, hooks=False, **options)
957 955 self._remote.invalidate_vcs_cache()
958 956
959 957 def bookmark(self, bookmark, revision=None):
960 958 if isinstance(bookmark, str):
961 959 bookmark = safe_str(bookmark)
962 960 self._remote.bookmark(bookmark, revision=revision)
963 961 self._remote.invalidate_vcs_cache()
964 962
965 963 def get_path_permissions(self, username):
966 964 hgacl_file = os.path.join(self.path, '.hg/hgacl')
967 965
968 966 def read_patterns(suffix):
969 967 svalue = None
970 968 for section, option in [
971 969 ('narrowacl', username + suffix),
972 970 ('narrowacl', 'default' + suffix),
973 971 ('narrowhgacl', username + suffix),
974 972 ('narrowhgacl', 'default' + suffix)
975 973 ]:
976 974 try:
977 975 svalue = hgacl.get(section, option)
978 976 break # stop at the first value we find
979 977 except configparser.NoOptionError:
980 978 pass
981 979 if not svalue:
982 980 return None
983 981 result = ['/']
984 982 for pattern in svalue.split():
985 983 result.append(pattern)
986 984 if '*' not in pattern and '?' not in pattern:
987 985 result.append(pattern + '/*')
988 986 return result
989 987
990 988 if os.path.exists(hgacl_file):
991 989 try:
992 990 hgacl = configparser.RawConfigParser()
993 991 hgacl.read(hgacl_file)
994 992
995 993 includes = read_patterns('.includes')
996 994 excludes = read_patterns('.excludes')
997 995 return BasePathPermissionChecker.create_from_patterns(
998 996 includes, excludes)
999 997 except BaseException as e:
1000 998 msg = 'Cannot read ACL settings from {} on {}: {}'.format(
1001 999 hgacl_file, self.name, e)
1002 1000 raise exceptions.RepositoryRequirementError(msg)
1003 1001 else:
1004 1002 return None
1005 1003
1006 1004
1007 1005 class MercurialIndexBasedCollectionGenerator(CollectionGenerator):
1008 1006
1009 1007 def _commit_factory(self, commit_id):
1010 1008 if isinstance(commit_id, int):
1011 1009 return self.repo.get_commit(
1012 1010 commit_idx=commit_id, pre_load=self.pre_load)
1013 1011 else:
1014 1012 return self.repo.get_commit(
1015 1013 commit_id=commit_id, pre_load=self.pre_load)
@@ -1,47 +1,45 b''
1
2
3 1 # Copyright (C) 2014-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 """
22 20 SVN module
23 21 """
24 22 import logging
25 23
26 24 from rhodecode.lib.vcs import connection
27 25 from rhodecode.lib.vcs.backends.svn.commit import SubversionCommit
28 26 from rhodecode.lib.vcs.backends.svn.repository import SubversionRepository
29 27
30 28
31 29 log = logging.getLogger(__name__)
32 30
33 31
34 32 def discover_svn_version(raise_on_exc=False):
35 33 """
36 34 Returns the string as it was returned by running 'git --version'
37 35
38 36 It will return an empty string in case the connection is not initialized
39 37 or no vcsserver is available.
40 38 """
41 39 try:
42 40 return connection.Svn.discover_svn_version()
43 41 except Exception:
44 42 log.warning("Failed to discover the SVN version", exc_info=True)
45 43 if raise_on_exc:
46 44 raise
47 45 return ''
@@ -1,256 +1,254 b''
1
2
3 1 # Copyright (C) 2014-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 """
22 20 SVN commit module
23 21 """
24 22
25 23
26 24 import dateutil.parser
27 25 from zope.cachedescriptors.property import Lazy as LazyProperty
28 26
29 27 from rhodecode.lib.str_utils import safe_bytes, safe_str
30 28 from rhodecode.lib.vcs import nodes, path as vcspath
31 29 from rhodecode.lib.vcs.backends import base
32 30 from rhodecode.lib.vcs.exceptions import CommitError
33 31
34 32
35 33 _SVN_PROP_TRUE = '*'
36 34
37 35
38 36 class SubversionCommit(base.BaseCommit):
39 37 """
40 38 Subversion specific implementation of commits
41 39
42 40 .. attribute:: branch
43 41
44 42 The Subversion backend does not support to assign branches to
45 43 specific commits. This attribute has always the value `None`.
46 44
47 45 """
48 46
49 47 def __init__(self, repository, commit_id):
50 48 self.repository = repository
51 49 self.idx = self.repository._get_commit_idx(commit_id)
52 50 self._svn_rev = self.idx + 1
53 51 self._remote = repository._remote
54 52 # TODO: handling of raw_id should be a method on repository itself,
55 53 # which knows how to translate commit index and commit id
56 54 self.raw_id = commit_id
57 55 self.short_id = commit_id
58 self.id = 'r%s' % (commit_id, )
56 self.id = 'r{}'.format(commit_id)
59 57
60 58 # TODO: Implement the following placeholder attributes
61 59 self.nodes = {}
62 60 self.tags = []
63 61
64 62 @property
65 63 def author(self):
66 64 return safe_str(self._properties.get('svn:author'))
67 65
68 66 @property
69 67 def date(self):
70 68 return _date_from_svn_properties(self._properties)
71 69
72 70 @property
73 71 def message(self):
74 72 return safe_str(self._properties.get('svn:log'))
75 73
76 74 @LazyProperty
77 75 def _properties(self):
78 76 return self._remote.revision_properties(self._svn_rev)
79 77
80 78 @LazyProperty
81 79 def parents(self):
82 80 parent_idx = self.idx - 1
83 81 if parent_idx >= 0:
84 82 parent = self.repository.get_commit(commit_idx=parent_idx)
85 83 return [parent]
86 84 return []
87 85
88 86 @LazyProperty
89 87 def children(self):
90 88 child_idx = self.idx + 1
91 89 if child_idx < len(self.repository.commit_ids):
92 90 child = self.repository.get_commit(commit_idx=child_idx)
93 91 return [child]
94 92 return []
95 93
96 94 def get_file_mode(self, path: bytes):
97 95 # Note: Subversion flags files which are executable with a special
98 96 # property `svn:executable` which is set to the value ``"*"``.
99 97 if self._get_file_property(path, 'svn:executable') == _SVN_PROP_TRUE:
100 98 return base.FILEMODE_EXECUTABLE
101 99 else:
102 100 return base.FILEMODE_DEFAULT
103 101
104 102 def is_link(self, path):
105 103 # Note: Subversion has a flag for special files, the content of the
106 104 # file contains the type of that file.
107 105 if self._get_file_property(path, 'svn:special') == _SVN_PROP_TRUE:
108 106 return self.get_file_content(path).startswith(b'link')
109 107 return False
110 108
111 109 def is_node_binary(self, path):
112 110 path = self._fix_path(path)
113 111 return self._remote.is_binary(self._svn_rev, safe_str(path))
114 112
115 113 def node_md5_hash(self, path):
116 114 path = self._fix_path(path)
117 115 return self._remote.md5_hash(self._svn_rev, safe_str(path))
118 116
119 117 def _get_file_property(self, path, name):
120 118 file_properties = self._remote.node_properties(
121 119 safe_str(path), self._svn_rev)
122 120 return file_properties.get(name)
123 121
124 122 def get_file_content(self, path):
125 123 path = self._fix_path(path)
126 124 return self._remote.get_file_content(self._svn_rev, safe_str(path))
127 125
128 126 def get_file_content_streamed(self, path):
129 127 path = self._fix_path(path)
130 128
131 129 stream_method = getattr(self._remote, 'stream:get_file_content')
132 130 return stream_method(self._svn_rev, safe_str(path))
133 131
134 132 def get_file_size(self, path):
135 133 path = self._fix_path(path)
136 134 return self._remote.get_file_size(self._svn_rev, safe_str(path))
137 135
138 136 def get_path_history(self, path, limit=None, pre_load=None):
139 137 path = safe_str(self._fix_path(path))
140 138 history = self._remote.node_history(path, self._svn_rev, limit)
141 139 return [
142 140 self.repository.get_commit(commit_id=str(svn_rev))
143 141 for svn_rev in history]
144 142
145 143 def get_file_annotate(self, path, pre_load=None):
146 144 result = self._remote.file_annotate(safe_str(path), self._svn_rev)
147 145
148 146 for zero_based_line_no, svn_rev, content in result:
149 147 commit_id = str(svn_rev)
150 148 line_no = zero_based_line_no + 1
151 149 yield (
152 150 line_no,
153 151 commit_id,
154 152 lambda: self.repository.get_commit(commit_id=commit_id),
155 153 content)
156 154
157 155 def get_node(self, path, pre_load=None):
158 156 path = self._fix_path(path)
159 157 if path not in self.nodes:
160 158
161 159 if path == '':
162 160 node = nodes.RootNode(commit=self)
163 161 else:
164 162 node_type = self._remote.get_node_type(self._svn_rev, safe_str(path))
165 163 if node_type == 'dir':
166 164 node = nodes.DirNode(safe_bytes(path), commit=self)
167 165 elif node_type == 'file':
168 166 node = nodes.FileNode(safe_bytes(path), commit=self, pre_load=pre_load)
169 167 else:
170 168 raise self.no_node_at_path(path)
171 169
172 170 self.nodes[path] = node
173 171 return self.nodes[path]
174 172
175 173 def get_nodes(self, path, pre_load=None):
176 174 if self._get_kind(path) != nodes.NodeKind.DIR:
177 175 raise CommitError(
178 176 f"Directory does not exist for commit {self.raw_id} at '{path}'")
179 177 path = safe_str(self._fix_path(path))
180 178
181 179 path_nodes = []
182 180 for name, kind in self._remote.get_nodes(self._svn_rev, path):
183 181 node_path = vcspath.join(path, name)
184 182 if kind == 'dir':
185 183 node = nodes.DirNode(safe_bytes(node_path), commit=self)
186 184 elif kind == 'file':
187 185 node = nodes.FileNode(safe_bytes(node_path), commit=self, pre_load=pre_load)
188 186 else:
189 187 raise ValueError(f"Node kind {kind} not supported.")
190 188 self.nodes[node_path] = node
191 189 path_nodes.append(node)
192 190
193 191 return path_nodes
194 192
195 193 def _get_kind(self, path):
196 194 path = self._fix_path(path)
197 195 kind = self._remote.get_node_type(self._svn_rev, path)
198 196 if kind == 'file':
199 197 return nodes.NodeKind.FILE
200 198 elif kind == 'dir':
201 199 return nodes.NodeKind.DIR
202 200 else:
203 201 raise CommitError(
204 "Node does not exist at the given path '%s'" % (path, ))
202 "Node does not exist at the given path '{}'".format(path))
205 203
206 204 @LazyProperty
207 205 def _changes_cache(self):
208 206 return self._remote.revision_changes(self._svn_rev)
209 207
210 208 @LazyProperty
211 209 def affected_files(self):
212 210 changed_files = set()
213 211 for files in self._changes_cache.values():
214 212 changed_files.update(files)
215 213 return list(changed_files)
216 214
217 215 @LazyProperty
218 216 def id(self):
219 217 return self.raw_id
220 218
221 219 @property
222 220 def added(self):
223 221 return nodes.AddedFileNodesGenerator(self.added_paths, self)
224 222
225 223 @LazyProperty
226 224 def added_paths(self):
227 225 return [n for n in self._changes_cache['added']]
228 226
229 227 @property
230 228 def changed(self):
231 229 return nodes.ChangedFileNodesGenerator(self.changed_paths, self)
232 230
233 231 @LazyProperty
234 232 def changed_paths(self):
235 233 return [n for n in self._changes_cache['changed']]
236 234
237 235 @property
238 236 def removed(self):
239 237 return nodes.RemovedFileNodesGenerator(self.removed_paths, self)
240 238
241 239 @LazyProperty
242 240 def removed_paths(self):
243 241 return [n for n in self._changes_cache['removed']]
244 242
245 243
246 244 def _date_from_svn_properties(properties):
247 245 """
248 246 Parses the date out of given svn properties.
249 247
250 248 :return: :class:`datetime.datetime` instance. The object is naive.
251 249 """
252 250
253 251 aware_date = dateutil.parser.parse(properties.get('svn:date'))
254 252 # final_date = aware_date.astimezone(dateutil.tz.tzlocal())
255 253 final_date = aware_date
256 254 return final_date.replace(tzinfo=None)
@@ -1,51 +1,49 b''
1
2
3 1 # Copyright (C) 2014-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 """
22 20 SVN diff module
23 21 """
24 22
25 23 import re
26 24
27 25 from rhodecode.lib.vcs.backends import base
28 26
29 27
30 28 class SubversionDiff(base.Diff):
31 29
32 30 _meta_re = re.compile(br"""
33 31 (?:^(?P<svn_bin_patch>Cannot[ ]display:[ ]file[ ]marked[ ]as[ ]a[ ]binary[ ]type.)(?:\n|$))?
34 32 """, re.VERBOSE | re.MULTILINE)
35 33
36 34 _header_re = re.compile(br"""
37 35 #^diff[ ]--git
38 36 [ ]"?a/(?P<a_path>.+?)"?[ ]"?b/(?P<b_path>.+?)"?\n
39 37 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n
40 38 ^rename[ ]from[ ](?P<rename_from>[^\r\n]+)\n
41 39 ^rename[ ]to[ ](?P<rename_to>[^\r\n]+)(?:\n|$))?
42 40 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
43 41 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
44 42 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
45 43 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
46 44 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
47 45 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
48 46 (?:^(?P<bin_patch>GIT[ ]binary[ ]patch)(?:\n|$))?
49 47 (?:^---[ ]("?a/(?P<a_file>.+)|/dev/null)\t\(revision[ ]\d+\)(?:\n|$))?
50 48 (?:^\+\+\+[ ]("?b/(?P<b_file>.+)|/dev/null)\t\(revision[ ]\d+\)(?:\n|$))?
51 49 """, re.VERBOSE | re.MULTILINE)
@@ -1,79 +1,77 b''
1
2
3 1 # Copyright (C) 2014-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19
22 20 """
23 21 SVN inmemory module
24 22 """
25 23
26 24 from rhodecode.lib.datelib import date_astimestamp
27 25 from rhodecode.lib.str_utils import safe_str, safe_bytes
28 26 from rhodecode.lib.vcs.backends import base
29 27
30 28
31 29 class SubversionInMemoryCommit(base.BaseInMemoryCommit):
32 30
33 31 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
34 32 if branch not in (None, self.repository.DEFAULT_BRANCH_NAME):
35 33 raise NotImplementedError("Branches are not yet supported")
36 34
37 35 self.check_integrity(parents)
38 36
39 37 message = safe_str(message)
40 38 author = safe_str(author)
41 39
42 40 updated = []
43 41 for node in self.added:
44 42 node_data = {
45 43 'path': safe_bytes(node.path),
46 44 'content': node.content,
47 45 'mode': node.mode,
48 46 }
49 47 if node.is_binary:
50 48 node_data['properties'] = {
51 49 'svn:mime-type': 'application/octet-stream'
52 50 }
53 51 updated.append(node_data)
54 52 for node in self.changed:
55 53 updated.append({
56 54 'path': safe_bytes(node.path),
57 55 'content': node.content,
58 56 'mode': node.mode,
59 57 })
60 58
61 59 removed = []
62 60 for node in self.removed:
63 61 removed.append({
64 62 'path': safe_bytes(node.path),
65 63 })
66 64
67 65 timestamp = date_astimestamp(date) if date else None
68 66 svn_rev = self.repository._remote.commit(
69 67 message=message, author=author, timestamp=timestamp,
70 68 updated=updated, removed=removed)
71 69
72 70 # TODO: Find a nicer way. If commit_ids is not yet evaluated, then
73 71 # we should not add the commit_id, if it is already evaluated, it
74 72 # will not be evaluated again.
75 73 commit_id = str(svn_rev)
76 74 self.repository.append_commit_id(commit_id)
77 75 tip = self.repository.get_commit()
78 76 self.reset()
79 77 return tip
@@ -1,369 +1,367 b''
1
2
3 1 # Copyright (C) 2014-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 """
22 20 SVN repository module
23 21 """
24 22
25 23 import logging
26 24 import os
27 25 import urllib.request
28 26 import urllib.parse
29 27 import urllib.error
30 28
31 29 from zope.cachedescriptors.property import Lazy as LazyProperty
32 30
33 31 from collections import OrderedDict
34 32 from rhodecode.lib.datelib import date_astimestamp
35 33 from rhodecode.lib.str_utils import safe_str
36 34 from rhodecode.lib.utils2 import CachedProperty
37 35 from rhodecode.lib.vcs import connection, path as vcspath
38 36 from rhodecode.lib.vcs.backends import base
39 37 from rhodecode.lib.vcs.backends.svn.commit import (
40 38 SubversionCommit, _date_from_svn_properties)
41 39 from rhodecode.lib.vcs.backends.svn.diff import SubversionDiff
42 40 from rhodecode.lib.vcs.backends.svn.inmemory import SubversionInMemoryCommit
43 41 from rhodecode.lib.vcs.conf import settings
44 42 from rhodecode.lib.vcs.exceptions import (
45 43 CommitDoesNotExistError, EmptyRepositoryError, RepositoryError,
46 44 VCSError, NodeDoesNotExistError)
47 45
48 46
49 47 log = logging.getLogger(__name__)
50 48
51 49
52 50 class SubversionRepository(base.BaseRepository):
53 51 """
54 52 Subversion backend implementation
55 53
56 54 .. important::
57 55
58 56 It is very important to distinguish the commit index and the commit id
59 57 which is assigned by Subversion. The first one is always handled as an
60 58 `int` by this implementation. The commit id assigned by Subversion on
61 59 the other side will always be a `str`.
62 60
63 61 There is a specific trap since the first commit will have the index
64 62 ``0`` but the svn id will be ``"1"``.
65 63
66 64 """
67 65
68 66 # Note: Subversion does not really have a default branch name.
69 67 DEFAULT_BRANCH_NAME = None
70 68
71 69 contact = base.BaseRepository.DEFAULT_CONTACT
72 70 description = base.BaseRepository.DEFAULT_DESCRIPTION
73 71
74 72 def __init__(self, repo_path, config=None, create=False, src_url=None, with_wire=None,
75 73 bare=False, **kwargs):
76 74 self.path = safe_str(os.path.abspath(repo_path))
77 75 self.config = config if config else self.get_default_config()
78 76 self.with_wire = with_wire or {"cache": False} # default should not use cache
79 77
80 78 self._init_repo(create, src_url)
81 79
82 80 # caches
83 81 self._commit_ids = {}
84 82
85 83 @LazyProperty
86 84 def _remote(self):
87 85 repo_id = self.path
88 86 return connection.Svn(self.path, repo_id, self.config, with_wire=self.with_wire)
89 87
90 88 def _init_repo(self, create, src_url):
91 89 if create and os.path.exists(self.path):
92 90 raise RepositoryError(
93 91 f"Cannot create repository at {self.path}, location already exist"
94 92 )
95 93
96 94 if create:
97 95 self._remote.create_repository(settings.SVN_COMPATIBLE_VERSION)
98 96 if src_url:
99 97 src_url = _sanitize_url(src_url)
100 98 self._remote.import_remote_repository(src_url)
101 99 else:
102 100 self._check_path()
103 101
104 102 @CachedProperty
105 103 def commit_ids(self):
106 104 head = self._remote.lookup(None)
107 105 return [str(r) for r in range(1, head + 1)]
108 106
109 107 def _rebuild_cache(self, commit_ids):
110 108 pass
111 109
112 110 def run_svn_command(self, cmd, **opts):
113 111 """
114 112 Runs given ``cmd`` as svn command and returns tuple
115 113 (stdout, stderr).
116 114
117 115 :param cmd: full svn command to be executed
118 116 :param opts: env options to pass into Subprocess command
119 117 """
120 118 if not isinstance(cmd, list):
121 119 raise ValueError(f'cmd must be a list, got {type(cmd)} instead')
122 120
123 121 skip_stderr_log = opts.pop('skip_stderr_log', False)
124 122 out, err = self._remote.run_svn_command(cmd, **opts)
125 123 if err and not skip_stderr_log:
126 124 log.debug('Stderr output of svn command "%s":\n%s', cmd, err)
127 125 return out, err
128 126
129 127 @LazyProperty
130 128 def branches(self):
131 129 return self._tags_or_branches('vcs_svn_branch')
132 130
133 131 @LazyProperty
134 132 def branches_closed(self):
135 133 return {}
136 134
137 135 @LazyProperty
138 136 def bookmarks(self):
139 137 return {}
140 138
141 139 @LazyProperty
142 140 def branches_all(self):
143 141 # TODO: johbo: Implement proper branch support
144 142 all_branches = {}
145 143 all_branches.update(self.branches)
146 144 all_branches.update(self.branches_closed)
147 145 return all_branches
148 146
149 147 @LazyProperty
150 148 def tags(self):
151 149 return self._tags_or_branches('vcs_svn_tag')
152 150
153 151 def _tags_or_branches(self, config_section):
154 152 found_items = {}
155 153
156 154 if self.is_empty():
157 155 return {}
158 156
159 157 for pattern in self._patterns_from_section(config_section):
160 158 pattern = vcspath.sanitize(pattern)
161 159 tip = self.get_commit()
162 160 try:
163 161 if pattern.endswith('*'):
164 162 basedir = tip.get_node(vcspath.dirname(pattern))
165 163 directories = basedir.dirs
166 164 else:
167 165 directories = (tip.get_node(pattern), )
168 166 except NodeDoesNotExistError:
169 167 continue
170 168 found_items.update((safe_str(n.path), self.commit_ids[-1]) for n in directories)
171 169
172 170 def get_name(item):
173 171 return item[0]
174 172
175 173 return OrderedDict(sorted(found_items.items(), key=get_name))
176 174
177 175 def _patterns_from_section(self, section):
178 176 return (pattern for key, pattern in self.config.items(section))
179 177
180 178 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
181 179 if self != repo2:
182 180 raise ValueError(
183 181 "Subversion does not support getting common ancestor of"
184 182 " different repositories.")
185 183
186 184 if int(commit_id1) < int(commit_id2):
187 185 return commit_id1
188 186 return commit_id2
189 187
190 188 def verify(self):
191 189 verify = self._remote.verify()
192 190
193 191 self._remote.invalidate_vcs_cache()
194 192 return verify
195 193
196 194 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
197 195 # TODO: johbo: Implement better comparison, this is a very naive
198 196 # version which does not allow to compare branches, tags or folders
199 197 # at all.
200 198 if repo2 != self:
201 199 raise ValueError(
202 200 "Subversion does not support comparison of of different "
203 201 "repositories.")
204 202
205 203 if commit_id1 == commit_id2:
206 204 return []
207 205
208 206 commit_idx1 = self._get_commit_idx(commit_id1)
209 207 commit_idx2 = self._get_commit_idx(commit_id2)
210 208
211 209 commits = [
212 210 self.get_commit(commit_idx=idx)
213 211 for idx in range(commit_idx1 + 1, commit_idx2 + 1)]
214 212
215 213 return commits
216 214
217 215 def _get_commit_idx(self, commit_id):
218 216 try:
219 217 svn_rev = int(commit_id)
220 218 except:
221 219 # TODO: johbo: this might be only one case, HEAD, check this
222 220 svn_rev = self._remote.lookup(commit_id)
223 221 commit_idx = svn_rev - 1
224 222 if commit_idx >= len(self.commit_ids):
225 223 raise CommitDoesNotExistError(
226 "Commit at index %s does not exist." % (commit_idx, ))
224 "Commit at index {} does not exist.".format(commit_idx))
227 225 return commit_idx
228 226
229 227 @staticmethod
230 228 def check_url(url, config):
231 229 """
232 230 Check if `url` is a valid source to import a Subversion repository.
233 231 """
234 232 # convert to URL if it's a local directory
235 233 if os.path.isdir(url):
236 234 url = 'file://' + urllib.request.pathname2url(url)
237 235 return connection.Svn.check_url(url, config.serialize())
238 236
239 237 @staticmethod
240 238 def is_valid_repository(path):
241 239 try:
242 240 SubversionRepository(path)
243 241 return True
244 242 except VCSError:
245 243 pass
246 244 return False
247 245
248 246 def _check_path(self):
249 247 if not os.path.exists(self.path):
250 raise VCSError('Path "%s" does not exist!' % (self.path, ))
248 raise VCSError('Path "{}" does not exist!'.format(self.path))
251 249 if not self._remote.is_path_valid_repository(self.path):
252 250 raise VCSError(
253 251 'Path "%s" does not contain a Subversion repository' %
254 252 (self.path, ))
255 253
256 254 @LazyProperty
257 255 def last_change(self):
258 256 """
259 257 Returns last change made on this repository as
260 258 `datetime.datetime` object.
261 259 """
262 260 # Subversion always has a first commit which has id "0" and contains
263 261 # what we are looking for.
264 262 last_id = len(self.commit_ids)
265 263 properties = self._remote.revision_properties(last_id)
266 264 return _date_from_svn_properties(properties)
267 265
268 266 @LazyProperty
269 267 def in_memory_commit(self):
270 268 return SubversionInMemoryCommit(self)
271 269
272 270 def get_hook_location(self):
273 271 """
274 272 returns absolute path to location where hooks are stored
275 273 """
276 274 return os.path.join(self.path, 'hooks')
277 275
278 276 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
279 277 translate_tag=None, maybe_unreachable=False, reference_obj=None):
280 278 if self.is_empty():
281 279 raise EmptyRepositoryError("There are no commits yet")
282 280 if commit_id is not None:
283 281 self._validate_commit_id(commit_id)
284 282 elif commit_idx is not None:
285 283 self._validate_commit_idx(commit_idx)
286 284 try:
287 285 commit_id = self.commit_ids[commit_idx]
288 286 except IndexError:
289 raise CommitDoesNotExistError('No commit with idx: {}'.format(commit_idx))
287 raise CommitDoesNotExistError(f'No commit with idx: {commit_idx}')
290 288
291 289 commit_id = self._sanitize_commit_id(commit_id)
292 290 commit = SubversionCommit(repository=self, commit_id=commit_id)
293 291 return commit
294 292
295 293 def get_commits(
296 294 self, start_id=None, end_id=None, start_date=None, end_date=None,
297 295 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
298 296 if self.is_empty():
299 297 raise EmptyRepositoryError("There are no commit_ids yet")
300 298 self._validate_branch_name(branch_name)
301 299
302 300 if start_id is not None:
303 301 self._validate_commit_id(start_id)
304 302 if end_id is not None:
305 303 self._validate_commit_id(end_id)
306 304
307 305 start_raw_id = self._sanitize_commit_id(start_id)
308 306 start_pos = self.commit_ids.index(start_raw_id) if start_id else None
309 307 end_raw_id = self._sanitize_commit_id(end_id)
310 308 end_pos = max(0, self.commit_ids.index(end_raw_id)) if end_id else None
311 309
312 310 if None not in [start_id, end_id] and start_pos > end_pos:
313 311 raise RepositoryError(
314 312 "Start commit '%s' cannot be after end commit '%s'" %
315 313 (start_id, end_id))
316 314 if end_pos is not None:
317 315 end_pos += 1
318 316
319 317 # Date based filtering
320 318 if start_date or end_date:
321 319 start_raw_id, end_raw_id = self._remote.lookup_interval(
322 320 date_astimestamp(start_date) if start_date else None,
323 321 date_astimestamp(end_date) if end_date else None)
324 322 start_pos = start_raw_id - 1
325 323 end_pos = end_raw_id
326 324
327 325 commit_ids = self.commit_ids
328 326
329 327 # TODO: johbo: Reconsider impact of DEFAULT_BRANCH_NAME here
330 328 if branch_name not in [None, self.DEFAULT_BRANCH_NAME]:
331 329 svn_rev = int(self.commit_ids[-1])
332 330 commit_ids = self._remote.node_history(
333 331 path=branch_name, revision=svn_rev, limit=None)
334 332 commit_ids = [str(i) for i in reversed(commit_ids)]
335 333
336 334 if start_pos or end_pos:
337 335 commit_ids = commit_ids[start_pos:end_pos]
338 336 return base.CollectionGenerator(self, commit_ids, pre_load=pre_load)
339 337
340 338 def _sanitize_commit_id(self, commit_id):
341 339 if commit_id and commit_id.isdigit():
342 340 if int(commit_id) <= len(self.commit_ids):
343 341 return commit_id
344 342 else:
345 343 raise CommitDoesNotExistError(
346 "Commit %s does not exist." % (commit_id, ))
344 "Commit {} does not exist.".format(commit_id))
347 345 if commit_id not in [
348 346 None, 'HEAD', 'tip', self.DEFAULT_BRANCH_NAME]:
349 347 raise CommitDoesNotExistError(
350 "Commit id %s not understood." % (commit_id, ))
348 "Commit id {} not understood.".format(commit_id))
351 349 svn_rev = self._remote.lookup('HEAD')
352 350 return str(svn_rev)
353 351
354 352 def get_diff(
355 353 self, commit1, commit2, path=None, ignore_whitespace=False,
356 354 context=3, path1=None):
357 355 self._validate_diff_commits(commit1, commit2)
358 356 svn_rev1 = int(commit1.raw_id)
359 357 svn_rev2 = int(commit2.raw_id)
360 358 diff = self._remote.diff(
361 359 svn_rev1, svn_rev2, path1=path1, path2=path,
362 360 ignore_whitespace=ignore_whitespace, context=context)
363 361 return SubversionDiff(diff)
364 362
365 363
366 364 def _sanitize_url(url):
367 365 if '://' not in url:
368 366 url = 'file://' + urllib.request.pathname2url(url)
369 367 return url
@@ -1,432 +1,429 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 """
22 20 Client for the VCSServer implemented based on HTTP.
23 21 """
24 22
25 23 import copy
26 24 import logging
27 25 import threading
28 26 import time
29 27 import urllib.request
30 28 import urllib.error
31 29 import urllib.parse
32 30 import urllib.parse
33 31 import uuid
34 32 import traceback
35 33
36 34 import pycurl
37 35 import msgpack
38 36 import requests
39 37 from requests.packages.urllib3.util.retry import Retry
40 38
41 39 import rhodecode
42 40 from rhodecode.lib import rc_cache
43 41 from rhodecode.lib.rc_cache.utils import compute_key_from_params
44 42 from rhodecode.lib.system_info import get_cert_path
45 43 from rhodecode.lib.vcs import exceptions, CurlSession
46 44 from rhodecode.lib.utils2 import str2bool
47 45
48 46 log = logging.getLogger(__name__)
49 47
50 48
51 49 # TODO: mikhail: Keep it in sync with vcsserver's
52 50 # HTTPApplication.ALLOWED_EXCEPTIONS
53 51 EXCEPTIONS_MAP = {
54 52 'KeyError': KeyError,
55 53 'URLError': urllib.error.URLError,
56 54 }
57 55
58 56
59 57 def _remote_call(url, payload, exceptions_map, session, retries=3):
60 58
61 59 for attempt in range(retries):
62 60 try:
63 61 response = session.post(url, data=msgpack.packb(payload))
64 62 break
65 63 except pycurl.error as e:
66 64 error_code, error_message = e.args
67 65 if error_code == pycurl.E_RECV_ERROR:
68 66 log.warning(f'Received a "Connection reset by peer" error. '
69 67 f'Retrying... ({attempt + 1}/{retries})')
70 68 continue # Retry if connection reset error.
71 msg = '{}. \npycurl traceback: {}'.format(e, traceback.format_exc())
69 msg = f'{e}. \npycurl traceback: {traceback.format_exc()}'
72 70 raise exceptions.HttpVCSCommunicationError(msg)
73 71 except Exception as e:
74 72 message = getattr(e, 'message', '')
75 73 if 'Failed to connect' in message:
76 74 # gevent doesn't return proper pycurl errors
77 75 raise exceptions.HttpVCSCommunicationError(e)
78 76 else:
79 77 raise
80 78
81 79 if response.status_code >= 400:
82 80 content_type = response.content_type
83 81 log.error('Call to %s returned non 200 HTTP code: %s [%s]',
84 82 url, response.status_code, content_type)
85 83 raise exceptions.HttpVCSCommunicationError(repr(response.content))
86 84
87 85 try:
88 86 response = msgpack.unpackb(response.content)
89 87 except Exception:
90 88 log.exception('Failed to decode response from msgpack')
91 89 raise
92 90
93 91 error = response.get('error')
94 92 if error:
95 93 type_ = error.get('type', 'Exception')
96 94 exc = exceptions_map.get(type_, Exception)
97 95 exc = exc(error.get('message'))
98 96 try:
99 97 exc._vcs_kind = error['_vcs_kind']
100 98 except KeyError:
101 99 pass
102 100
103 101 try:
104 102 exc._vcs_server_traceback = error['traceback']
105 103 exc._vcs_server_org_exc_name = error['org_exc']
106 104 exc._vcs_server_org_exc_tb = error['org_exc_tb']
107 105 except KeyError:
108 106 pass
109 107
110 108 exc.add_note(attach_exc_details(error))
111 109 raise exc # raising the org exception from vcsserver
112 110 return response.get('result')
113 111
114 112
115 113 def attach_exc_details(error):
116 114 note = '-- EXC NOTE -- :\n'
117 115 note += f'vcs_kind: {error.get("_vcs_kind")}\n'
118 116 note += f'org_exc: {error.get("_vcs_kind")}\n'
119 117 note += f'tb: {error.get("traceback")}\n'
120 118 note += '-- END EXC NOTE --'
121 119 return note
122 120
123 121
124 122 def _streaming_remote_call(url, payload, exceptions_map, session, chunk_size):
125 123 try:
126 124 headers = {
127 125 'X-RC-Method': payload.get('method'),
128 126 'X-RC-Repo-Name': payload.get('_repo_name')
129 127 }
130 128 response = session.post(url, data=msgpack.packb(payload), headers=headers)
131 129 except pycurl.error as e:
132 130 error_code, error_message = e.args
133 msg = '{}. \npycurl traceback: {}'.format(e, traceback.format_exc())
131 msg = f'{e}. \npycurl traceback: {traceback.format_exc()}'
134 132 raise exceptions.HttpVCSCommunicationError(msg)
135 133 except Exception as e:
136 134 message = getattr(e, 'message', '')
137 135 if 'Failed to connect' in message:
138 136 # gevent doesn't return proper pycurl errors
139 137 raise exceptions.HttpVCSCommunicationError(e)
140 138 else:
141 139 raise
142 140
143 141 if response.status_code >= 400:
144 142 log.error('Call to %s returned non 200 HTTP code: %s',
145 143 url, response.status_code)
146 144 raise exceptions.HttpVCSCommunicationError(repr(response.content))
147 145
148 146 return response.iter_content(chunk_size=chunk_size)
149 147
150 148
151 149 class ServiceConnection(object):
152 150 def __init__(self, server_and_port, backend_endpoint, session_factory):
153 151 self.url = urllib.parse.urljoin('http://%s' % server_and_port, backend_endpoint)
154 152 self._session_factory = session_factory
155 153
156 154 def __getattr__(self, name):
157 155 def f(*args, **kwargs):
158 156 return self._call(name, *args, **kwargs)
159 157 return f
160 158
161 159 @exceptions.map_vcs_exceptions
162 160 def _call(self, name, *args, **kwargs):
163 161 payload = {
164 162 'id': str(uuid.uuid4()),
165 163 'method': name,
166 164 'params': {'args': args, 'kwargs': kwargs}
167 165 }
168 166 return _remote_call(
169 167 self.url, payload, EXCEPTIONS_MAP, self._session_factory())
170 168
171 169
172 170 class RemoteVCSMaker(object):
173 171
174 172 def __init__(self, server_and_port, backend_endpoint, backend_type, session_factory):
175 173 self.url = urllib.parse.urljoin('http://%s' % server_and_port, backend_endpoint)
176 174 self.stream_url = urllib.parse.urljoin('http://%s' % server_and_port, backend_endpoint+'/stream')
177 175
178 176 self._session_factory = session_factory
179 177 self.backend_type = backend_type
180 178
181 179 @classmethod
182 180 def init_cache_region(cls, repo_id):
183 cache_namespace_uid = 'repo.{}'.format(repo_id)
181 cache_namespace_uid = f'repo.{repo_id}'
184 182 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
185 183 return region, cache_namespace_uid
186 184
187 185 def __call__(self, path, repo_id, config, with_wire=None):
188 186 log.debug('%s RepoMaker call on %s', self.backend_type.upper(), path)
189 187 return RemoteRepo(path, repo_id, config, self, with_wire=with_wire)
190 188
191 189 def __getattr__(self, name):
192 190 def remote_attr(*args, **kwargs):
193 191 return self._call(name, *args, **kwargs)
194 192 return remote_attr
195 193
196 194 @exceptions.map_vcs_exceptions
197 195 def _call(self, func_name, *args, **kwargs):
198 196 payload = {
199 197 'id': str(uuid.uuid4()),
200 198 'method': func_name,
201 199 'backend': self.backend_type,
202 200 'params': {'args': args, 'kwargs': kwargs}
203 201 }
204 202 url = self.url
205 203 return _remote_call(url, payload, EXCEPTIONS_MAP, self._session_factory())
206 204
207 205
208 206 class RemoteRepo(object):
209 207 CHUNK_SIZE = 16384
210 208
211 209 def __init__(self, path, repo_id, config, remote_maker, with_wire=None):
212 210 self.url = remote_maker.url
213 211 self.stream_url = remote_maker.stream_url
214 212 self._session = remote_maker._session_factory()
215 213
216 214 cache_repo_id = self._repo_id_sanitizer(repo_id)
217 215 _repo_name = self._get_repo_name(config, path)
218 216 self._cache_region, self._cache_namespace = \
219 217 remote_maker.init_cache_region(cache_repo_id)
220 218
221 219 with_wire = with_wire or {}
222 220
223 221 repo_state_uid = with_wire.get('repo_state_uid') or 'state'
224 222
225 223 self._wire = {
226 224 "_repo_name": _repo_name,
227 225 "path": path, # repo path
228 226 "repo_id": repo_id,
229 227 "cache_repo_id": cache_repo_id,
230 228 "config": config,
231 229 "repo_state_uid": repo_state_uid,
232 230 "context": self._create_vcs_cache_context(path, repo_state_uid)
233 231 }
234 232
235 233 if with_wire:
236 234 self._wire.update(with_wire)
237 235
238 236 # NOTE(johbo): Trading complexity for performance. Avoiding the call to
239 237 # log.debug brings a few percent gain even if is is not active.
240 238 if log.isEnabledFor(logging.DEBUG):
241 239 self._call_with_logging = True
242 240
243 241 self.cert_dir = get_cert_path(rhodecode.CONFIG.get('__file__'))
244 242
245 243 def _get_repo_name(self, config, path):
246 244 repo_store = config.get('paths', '/')
247 245 return path.split(repo_store)[-1].lstrip('/')
248 246
249 247 def _repo_id_sanitizer(self, repo_id):
250 248 pathless = repo_id.replace('/', '__').replace('-', '_')
251 249 return ''.join(char if ord(char) < 128 else '_{}_'.format(ord(char)) for char in pathless)
252 250
253 251 def __getattr__(self, name):
254 252
255 253 if name.startswith('stream:'):
256 254 def repo_remote_attr(*args, **kwargs):
257 255 return self._call_stream(name, *args, **kwargs)
258 256 else:
259 257 def repo_remote_attr(*args, **kwargs):
260 258 return self._call(name, *args, **kwargs)
261 259
262 260 return repo_remote_attr
263 261
264 262 def _base_call(self, name, *args, **kwargs):
265 263 # TODO: oliver: This is currently necessary pre-call since the
266 264 # config object is being changed for hooking scenarios
267 265 wire = copy.deepcopy(self._wire)
268 266 wire["config"] = wire["config"].serialize()
269 267 wire["config"].append(('vcs', 'ssl_dir', self.cert_dir))
270 268
271 269 payload = {
272 270 'id': str(uuid.uuid4()),
273 271 'method': name,
274 272 "_repo_name": wire['_repo_name'],
275 273 'params': {'wire': wire, 'args': args, 'kwargs': kwargs}
276 274 }
277 275
278 276 context_uid = wire.get('context')
279 277 return context_uid, payload
280 278
281 279 def get_local_cache(self, name, args):
282 280 cache_on = False
283 281 cache_key = ''
284 282 local_cache_on = rhodecode.ConfigGet().get_bool('vcs.methods.cache')
285 283
286 284 cache_methods = [
287 285 'branches', 'tags', 'bookmarks',
288 286 'is_large_file', 'is_binary',
289 287 'fctx_size', 'stream:fctx_node_data', 'blob_raw_length',
290 288 'node_history',
291 289 'revision', 'tree_items',
292 290 'ctx_list', 'ctx_branch', 'ctx_description',
293 291 'bulk_request',
294 292 'assert_correct_path'
295 293 ]
296 294
297 295 if local_cache_on and name in cache_methods:
298 296 cache_on = True
299 297 repo_state_uid = self._wire['repo_state_uid']
300 298 call_args = [a for a in args]
301 299 cache_key = compute_key_from_params(repo_state_uid, name, *call_args)
302 300
303 301 return cache_on, cache_key
304 302
305 303 @exceptions.map_vcs_exceptions
306 304 def _call(self, name, *args, **kwargs):
307 305 context_uid, payload = self._base_call(name, *args, **kwargs)
308 306 url = self.url
309 307
310 308 start = time.time()
311 309 cache_on, cache_key = self.get_local_cache(name, args)
312 310
313 311 @self._cache_region.conditional_cache_on_arguments(
314 312 namespace=self._cache_namespace, condition=cache_on and cache_key)
315 313 def remote_call(_cache_key):
316 314 if self._call_with_logging:
317 315 args_repr = f'ARG: {str(args):.512}|KW: {str(kwargs):.512}'
318 316 log.debug('Calling %s@%s with args:%r. wire_context: %s cache_on: %s',
319 317 url, name, args_repr, context_uid, cache_on)
320 318 return _remote_call(url, payload, EXCEPTIONS_MAP, self._session)
321 319
322 320 result = remote_call(cache_key)
323 321 if self._call_with_logging:
324 322 log.debug('Call %s@%s took: %.4fs. wire_context: %s',
325 323 url, name, time.time()-start, context_uid)
326 324 return result
327 325
328 326 @exceptions.map_vcs_exceptions
329 327 def _call_stream(self, name, *args, **kwargs):
330 328 context_uid, payload = self._base_call(name, *args, **kwargs)
331 329 payload['chunk_size'] = self.CHUNK_SIZE
332 330 url = self.stream_url
333 331
334 332 start = time.time()
335 333 cache_on, cache_key = self.get_local_cache(name, args)
336 334
337 335 # Cache is a problem because this is a stream
338 336 def streaming_remote_call(_cache_key):
339 337 if self._call_with_logging:
340 338 args_repr = f'ARG: {str(args):.512}|KW: {str(kwargs):.512}'
341 339 log.debug('Calling %s@%s with args:%r. wire_context: %s cache_on: %s',
342 340 url, name, args_repr, context_uid, cache_on)
343 341 return _streaming_remote_call(url, payload, EXCEPTIONS_MAP, self._session, self.CHUNK_SIZE)
344 342
345 343 result = streaming_remote_call(cache_key)
346 344 if self._call_with_logging:
347 345 log.debug('Call %s@%s took: %.4fs. wire_context: %s',
348 346 url, name, time.time()-start, context_uid)
349 347 return result
350 348
351 349 def __getitem__(self, key):
352 350 return self.revision(key)
353 351
354 352 def _create_vcs_cache_context(self, *args):
355 353 """
356 354 Creates a unique string which is passed to the VCSServer on every
357 355 remote call. It is used as cache key in the VCSServer.
358 356 """
359 357 hash_key = '-'.join(map(str, args))
360 358 return str(uuid.uuid5(uuid.NAMESPACE_URL, hash_key))
361 359
362 360 def invalidate_vcs_cache(self):
363 361 """
364 362 This invalidates the context which is sent to the VCSServer on every
365 363 call to a remote method. It forces the VCSServer to create a fresh
366 364 repository instance on the next call to a remote method.
367 365 """
368 366 self._wire['context'] = str(uuid.uuid4())
369 367
370 368
371 369 class VcsHttpProxy(object):
372 370
373 371 CHUNK_SIZE = 16384
374 372
375 373 def __init__(self, server_and_port, backend_endpoint):
376 374 retries = Retry(total=5, connect=None, read=None, redirect=None)
377 375
378 376 adapter = requests.adapters.HTTPAdapter(max_retries=retries)
379 377 self.base_url = urllib.parse.urljoin('http://%s' % server_and_port, backend_endpoint)
380 378 self.session = requests.Session()
381 379 self.session.mount('http://', adapter)
382 380
383 381 def handle(self, environment, input_data, *args, **kwargs):
384 382 data = {
385 383 'environment': environment,
386 384 'input_data': input_data,
387 385 'args': args,
388 386 'kwargs': kwargs
389 387 }
390 388 result = self.session.post(
391 389 self.base_url, msgpack.packb(data), stream=True)
392 390 return self._get_result(result)
393 391
394 392 def _deserialize_and_raise(self, error):
395 393 exception = Exception(error['message'])
396 394 try:
397 395 exception._vcs_kind = error['_vcs_kind']
398 396 except KeyError:
399 397 pass
400 398 raise exception
401 399
402 400 def _iterate(self, result):
403 401 unpacker = msgpack.Unpacker()
404 402 for line in result.iter_content(chunk_size=self.CHUNK_SIZE):
405 403 unpacker.feed(line)
406 for chunk in unpacker:
407 yield chunk
404 yield from unpacker
408 405
409 406 def _get_result(self, result):
410 407 iterator = self._iterate(result)
411 408 error = next(iterator)
412 409 if error:
413 410 self._deserialize_and_raise(error)
414 411
415 412 status = next(iterator)
416 413 headers = next(iterator)
417 414
418 415 return iterator, status, headers
419 416
420 417
421 418 class ThreadlocalSessionFactory(object):
422 419 """
423 420 Creates one CurlSession per thread on demand.
424 421 """
425 422
426 423 def __init__(self):
427 424 self._thread_local = threading.local()
428 425
429 426 def __call__(self):
430 427 if not hasattr(self._thread_local, 'curl_session'):
431 428 self._thread_local.curl_session = CurlSession()
432 429 return self._thread_local.curl_session
@@ -1,18 +1,17 b''
1
2 1 # Copyright (C) 2010-2023 RhodeCode GmbH
3 2 #
4 3 # This program is free software: you can redistribute it and/or modify
5 4 # it under the terms of the GNU Affero General Public License, version 3
6 5 # (only), as published by the Free Software Foundation.
7 6 #
8 7 # This program is distributed in the hope that it will be useful,
9 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 10 # GNU General Public License for more details.
12 11 #
13 12 # You should have received a copy of the GNU Affero General Public License
14 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 14 #
16 15 # This program is dual-licensed. If you wish to learn more about the
17 16 # RhodeCode Enterprise Edition, including its added features, Support services,
18 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,1221 +1,1219 b''
1
2
3 1 # Copyright (C) 2014-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 DEFAULTS = {
22 20 'encodings_map': {'.gz': 'gzip',
23 21 '.Z': 'compress',
24 22 '.bz2': 'bzip2',
25 23 '.xz': 'xz'},
26 24 'suffix_map': {'.svgz': '.svg.gz',
27 25 '.tgz': '.tar.gz',
28 26 '.taz': '.tar.gz',
29 27 '.tz': '.tar.gz',
30 28 '.tbz2': '.tar.bz2',
31 29 '.txz': '.tar.xz'},
32 30 }
33 31
34 32 TYPES_MAP = [
35 33 {'.jpg': 'image/jpg',
36 34 '.mid': 'audio/midi',
37 35 '.midi': 'audio/midi',
38 36 '.pct': 'image/pict',
39 37 '.pic': 'image/pict',
40 38 '.pict': 'image/pict',
41 39 '.rtf': 'application/rtf',
42 40 '.xul': 'text/xul'},
43 41 {'.123': 'application/vnd.lotus-1-2-3',
44 42 '.3dml': 'text/vnd.in3d.3dml',
45 43 '.3g2': 'video/3gpp2',
46 44 '.3gp': 'video/3gpp',
47 45 '.7z': 'application/x-7z-compressed',
48 46 '.ASM': 'text/x-nasm',
49 47 '.C': 'text/x-c++hdr',
50 48 '.COB': 'text/x-cobol',
51 49 '.CPP': 'text/x-c++hdr',
52 50 '.CPY': 'text/x-cobol',
53 51 '.F': 'text/x-fortran',
54 52 '.F90': 'text/x-fortran',
55 53 '.H': 'text/x-c++hdr',
56 54 '.R': 'text/S-plus',
57 55 '.Rd': 'text/x-r-doc',
58 56 '.S': 'text/S-plus',
59 57 '.[1234567]': 'application/x-troff',
60 58 '.a': 'application/octet-stream',
61 59 '.aab': 'application/x-authorware-bin',
62 60 '.aac': 'audio/x-aac',
63 61 '.aam': 'application/x-authorware-map',
64 62 '.aas': 'application/x-authorware-seg',
65 63 '.abap': 'text/x-abap',
66 64 '.abw': 'application/x-abiword',
67 65 '.ac': 'application/pkix-attr-cert',
68 66 '.acc': 'application/vnd.americandynamics.acc',
69 67 '.ace': 'application/x-ace-compressed',
70 68 '.acu': 'application/vnd.acucobol',
71 69 '.acutc': 'application/vnd.acucorp',
72 70 '.ada': 'text/x-ada',
73 71 '.adb': 'text/x-ada',
74 72 '.adp': 'audio/adpcm',
75 73 '.ads': 'text/x-ada',
76 74 '.aep': 'application/vnd.audiograph',
77 75 '.afm': 'application/x-font-type1',
78 76 '.afp': 'application/vnd.ibm.modcap',
79 77 '.ahead': 'application/vnd.ahead.space',
80 78 '.ahk': 'text/x-autohotkey',
81 79 '.ahkl': 'text/x-autohotkey',
82 80 '.ai': 'application/postscript',
83 81 '.aif': 'audio/x-aiff',
84 82 '.aifc': 'audio/x-aiff',
85 83 '.aiff': 'audio/x-aiff',
86 84 '.air': 'application/vnd.adobe.air-application-installer-package+zip',
87 85 '.ait': 'application/vnd.dvb.ait',
88 86 '.aj': 'text/x-aspectj',
89 87 '.ami': 'application/vnd.amiga.ami',
90 88 '.apk': 'application/vnd.android.package-archive',
91 89 '.application': 'application/x-ms-application',
92 90 '.apr': 'application/vnd.lotus-approach',
93 91 '.as': 'application/x-actionscript3',
94 92 '.asc': 'application/pgp-signature',
95 93 '.asf': 'video/x-ms-asf',
96 94 '.asm': 'text/x-asm',
97 95 '.aso': 'application/vnd.accpac.simply.aso',
98 96 '.aspx': 'application/x-aspx',
99 97 '.asx': 'video/x-ms-asf',
100 98 '.asy': 'text/x-asymptote',
101 99 '.atc': 'application/vnd.acucorp',
102 100 '.atom': 'application/atom+xml',
103 101 '.atomcat': 'application/atomcat+xml',
104 102 '.atomsvc': 'application/atomsvc+xml',
105 103 '.atx': 'application/vnd.antix.game-component',
106 104 '.au': 'audio/basic',
107 105 '.au3': 'text/x-autoit',
108 106 '.aux': 'text/x-tex',
109 107 '.avi': 'video/x-msvideo',
110 108 '.aw': 'application/applixware',
111 109 '.awk': 'application/x-awk',
112 110 '.azf': 'application/vnd.airzip.filesecure.azf',
113 111 '.azs': 'application/vnd.airzip.filesecure.azs',
114 112 '.azw': 'application/vnd.amazon.ebook',
115 113 '.b': 'application/x-brainfuck',
116 114 '.bas': 'text/x-vbnet',
117 115 '.bash': 'text/x-sh',
118 116 '.bat': 'application/x-msdownload',
119 117 '.bcpio': 'application/x-bcpio',
120 118 '.bdf': 'application/x-font-bdf',
121 119 '.bdm': 'application/vnd.syncml.dm+wbxml',
122 120 '.bed': 'application/vnd.realvnc.bed',
123 121 '.befunge': 'application/x-befunge',
124 122 '.bf': 'application/x-brainfuck',
125 123 '.bh2': 'application/vnd.fujitsu.oasysprs',
126 124 '.bin': 'application/octet-stream',
127 125 '.bmi': 'application/vnd.bmi',
128 126 '.bmp': 'image/bmp',
129 127 '.bmx': 'text/x-bmx',
130 128 '.boo': 'text/x-boo',
131 129 '.book': 'application/vnd.framemaker',
132 130 '.box': 'application/vnd.previewsystems.box',
133 131 '.boz': 'application/x-bzip2',
134 132 '.bpk': 'application/octet-stream',
135 133 '.btif': 'image/prs.btif',
136 134 '.bz': 'application/x-bzip',
137 135 '.bz2': 'application/x-bzip2',
138 136 '.c': 'text/x-c',
139 137 '.c++': 'text/x-c++hdr',
140 138 '.c++-objdump': 'text/x-cpp-objdump',
141 139 '.c-objdump': 'text/x-c-objdump',
142 140 '.c11amc': 'application/vnd.cluetrust.cartomobile-config',
143 141 '.c11amz': 'application/vnd.cluetrust.cartomobile-config-pkg',
144 142 '.c4d': 'application/vnd.clonk.c4group',
145 143 '.c4f': 'application/vnd.clonk.c4group',
146 144 '.c4g': 'application/vnd.clonk.c4group',
147 145 '.c4p': 'application/vnd.clonk.c4group',
148 146 '.c4u': 'application/vnd.clonk.c4group',
149 147 '.cab': 'application/vnd.ms-cab-compressed',
150 148 '.car': 'application/vnd.curl.car',
151 149 '.cat': 'application/vnd.ms-pki.seccat',
152 150 '.cc': 'text/x-c',
153 151 '.cct': 'application/x-director',
154 152 '.ccxml': 'application/ccxml+xml',
155 153 '.cdbcmsg': 'application/vnd.contact.cmsg',
156 154 '.cdf': 'application/x-netcdf',
157 155 '.cdkey': 'application/vnd.mediastation.cdkey',
158 156 '.cdmia': 'application/cdmi-capability',
159 157 '.cdmic': 'application/cdmi-container',
160 158 '.cdmid': 'application/cdmi-domain',
161 159 '.cdmio': 'application/cdmi-object',
162 160 '.cdmiq': 'application/cdmi-queue',
163 161 '.cdx': 'chemical/x-cdx',
164 162 '.cdxml': 'application/vnd.chemdraw+xml',
165 163 '.cdy': 'application/vnd.cinderella',
166 164 '.cer': 'application/pkix-cert',
167 165 '.ceylon': 'text/x-ceylon',
168 166 '.cfc': 'application/x-coldfusion',
169 167 '.cfg': 'text/x-ini',
170 168 '.cfm': 'application/x-coldfusion',
171 169 '.cfml': 'application/x-coldfusion',
172 170 '.cgm': 'image/cgm',
173 171 '.chat': 'application/x-chat',
174 172 '.chm': 'application/vnd.ms-htmlhelp',
175 173 '.chrt': 'application/vnd.kde.kchart',
176 174 '.cif': 'chemical/x-cif',
177 175 '.cii': 'application/vnd.anser-web-certificate-issue-initiation',
178 176 '.cil': 'application/vnd.ms-artgalry',
179 177 '.cl': 'text/x-common-lisp',
180 178 '.cla': 'application/vnd.claymore',
181 179 '.class': 'application/java-vm',
182 180 '.clj': 'text/x-clojure',
183 181 '.clkk': 'application/vnd.crick.clicker.keyboard',
184 182 '.clkp': 'application/vnd.crick.clicker.palette',
185 183 '.clkt': 'application/vnd.crick.clicker.template',
186 184 '.clkw': 'application/vnd.crick.clicker.wordbank',
187 185 '.clkx': 'application/vnd.crick.clicker',
188 186 '.clp': 'application/x-msclip',
189 187 '.cls': 'text/x-openedge',
190 188 '.cmake': 'text/x-cmake',
191 189 '.cmc': 'application/vnd.cosmocaller',
192 190 '.cmd': 'application/x-dos-batch',
193 191 '.cmdf': 'chemical/x-cmdf',
194 192 '.cml': 'chemical/x-cml',
195 193 '.cmp': 'application/vnd.yellowriver-custom-menu',
196 194 '.cmx': 'image/x-cmx',
197 195 '.cob': 'text/x-cobol',
198 196 '.cod': 'application/vnd.rim.cod',
199 197 '.coffee': 'text/coffeescript',
200 198 '.com': 'application/x-msdownload',
201 199 '.conf': 'text/plain',
202 200 '.cp': 'text/x-c++hdr',
203 201 '.cpio': 'application/x-cpio',
204 202 '.cpp': 'text/x-c',
205 203 '.cpp-objdump': 'text/x-cpp-objdump',
206 204 '.cpt': 'application/mac-compactpro',
207 205 '.cpy': 'text/x-cobol',
208 206 '.crd': 'application/x-mscardfile',
209 207 '.crl': 'application/pkix-crl',
210 208 '.croc': 'text/x-crocsrc',
211 209 '.crt': 'application/x-x509-ca-cert',
212 210 '.cryptonote': 'application/vnd.rig.cryptonote',
213 211 '.cs': 'text/x-csharp',
214 212 '.csh': 'application/x-csh',
215 213 '.csml': 'chemical/x-csml',
216 214 '.csp': 'application/vnd.commonspace',
217 215 '.css': 'text/css',
218 216 '.cst': 'application/x-director',
219 217 '.csv': 'text/csv',
220 218 '.cu': 'application/cu-seeme',
221 219 '.cuh': 'text/x-cuda',
222 220 '.curl': 'text/vnd.curl',
223 221 '.cww': 'application/prs.cww',
224 222 '.cxt': 'application/x-director',
225 223 '.cxx': 'text/x-c',
226 224 '.cxx-objdump': 'text/x-cpp-objdump',
227 225 '.d': 'text/x-dsrc',
228 226 '.d-objdump': 'text/x-d-objdump',
229 227 '.dae': 'model/vnd.collada+xml',
230 228 '.daf': 'application/vnd.mobius.daf',
231 229 '.dart': 'text/x-dart',
232 230 '.dataless': 'application/vnd.fdsn.seed',
233 231 '.davmount': 'application/davmount+xml',
234 232 '.dcr': 'application/x-director',
235 233 '.dcurl': 'text/vnd.curl.dcurl',
236 234 '.dd2': 'application/vnd.oma.dd2+xml',
237 235 '.ddd': 'application/vnd.fujixerox.ddd',
238 236 '.deb': 'application/x-debian-package',
239 237 '.def': 'text/plain',
240 238 '.deploy': 'application/octet-stream',
241 239 '.der': 'application/x-x509-ca-cert',
242 240 '.dfac': 'application/vnd.dreamfactory',
243 241 '.dg': 'text/x-dg',
244 242 '.di': 'text/x-dsrc',
245 243 '.dic': 'text/x-c',
246 244 '.dif': 'video/x-dv',
247 245 '.diff': 'text/x-diff',
248 246 '.dir': 'application/x-director',
249 247 '.dis': 'application/vnd.mobius.dis',
250 248 '.dist': 'application/octet-stream',
251 249 '.distz': 'application/octet-stream',
252 250 '.djv': 'image/vnd.djvu',
253 251 '.djvu': 'image/vnd.djvu',
254 252 '.dll': 'application/x-msdownload',
255 253 '.dmg': 'application/octet-stream',
256 254 '.dms': 'application/octet-stream',
257 255 '.dna': 'application/vnd.dna',
258 256 '.doc': 'application/msword',
259 257 '.docm': 'application/vnd.ms-word.document.macroenabled.12',
260 258 '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
261 259 '.dot': 'application/msword',
262 260 '.dotm': 'application/vnd.ms-word.template.macroenabled.12',
263 261 '.dotx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
264 262 '.dp': 'application/vnd.osgi.dp',
265 263 '.dpg': 'application/vnd.dpgraph',
266 264 '.dra': 'audio/vnd.dra',
267 265 '.dsc': 'text/prs.lines.tag',
268 266 '.dssc': 'application/dssc+der',
269 267 '.dtb': 'application/x-dtbook+xml',
270 268 '.dtd': 'application/xml-dtd',
271 269 '.dts': 'audio/vnd.dts',
272 270 '.dtshd': 'audio/vnd.dts.hd',
273 271 '.duby': 'text/x-ruby',
274 272 '.duel': 'text/x-duel',
275 273 '.dump': 'application/octet-stream',
276 274 '.dv': 'video/x-dv',
277 275 '.dvi': 'application/x-dvi',
278 276 '.dwf': 'model/vnd.dwf',
279 277 '.dwg': 'image/vnd.dwg',
280 278 '.dxf': 'image/vnd.dxf',
281 279 '.dxp': 'application/vnd.spotfire.dxp',
282 280 '.dxr': 'application/x-director',
283 281 '.dyl': 'text/x-dylan',
284 282 '.dylan': 'text/x-dylan',
285 283 '.dylan-console': 'text/x-dylan-console',
286 284 '.ebuild': 'text/x-sh',
287 285 '.ec': 'text/x-echdr',
288 286 '.ecelp4800': 'audio/vnd.nuera.ecelp4800',
289 287 '.ecelp7470': 'audio/vnd.nuera.ecelp7470',
290 288 '.ecelp9600': 'audio/vnd.nuera.ecelp9600',
291 289 '.ecl': 'text/x-ecl',
292 290 '.eclass': 'text/x-sh',
293 291 '.ecma': 'application/ecmascript',
294 292 '.edm': 'application/vnd.novadigm.edm',
295 293 '.edx': 'application/vnd.novadigm.edx',
296 294 '.efif': 'application/vnd.picsel',
297 295 '.eh': 'text/x-echdr',
298 296 '.ei6': 'application/vnd.pg.osasli',
299 297 '.el': 'text/x-common-lisp',
300 298 '.elc': 'application/octet-stream',
301 299 '.eml': 'message/rfc822',
302 300 '.emma': 'application/emma+xml',
303 301 '.eol': 'audio/vnd.digital-winds',
304 302 '.eot': 'application/vnd.ms-fontobject',
305 303 '.eps': 'application/postscript',
306 304 '.epub': 'application/epub+zip',
307 305 '.erl': 'text/x-erlang',
308 306 '.erl-sh': 'text/x-erl-shellsession',
309 307 '.es': 'text/x-erlang',
310 308 '.es3': 'application/vnd.eszigno3+xml',
311 309 '.escript': 'text/x-erlang',
312 310 '.esf': 'application/vnd.epson.esf',
313 311 '.et3': 'application/vnd.eszigno3+xml',
314 312 '.etx': 'text/x-setext',
315 313 '.evoque': 'application/x-evoque',
316 314 '.ex': 'text/x-elixir',
317 315 '.exe': 'application/x-msdownload',
318 316 '.exi': 'application/exi',
319 317 '.exs': 'text/x-elixir',
320 318 '.ext': 'application/vnd.novadigm.ext',
321 319 '.ez': 'application/andrew-inset',
322 320 '.ez2': 'application/vnd.ezpix-album',
323 321 '.ez3': 'application/vnd.ezpix-package',
324 322 '.f': 'text/x-fortran',
325 323 '.f4v': 'video/x-f4v',
326 324 '.f77': 'text/x-fortran',
327 325 '.f90': 'text/x-fortran',
328 326 '.factor': 'text/x-factor',
329 327 '.fan': 'application/x-fantom',
330 328 '.fancypack': 'text/x-fancysrc',
331 329 '.fbs': 'image/vnd.fastbidsheet',
332 330 '.fcs': 'application/vnd.isac.fcs',
333 331 '.fdf': 'application/vnd.fdf',
334 332 '.fe_launch': 'application/vnd.denovo.fcselayout-link',
335 333 '.feature': 'text/x-gherkin',
336 334 '.fg5': 'application/vnd.fujitsu.oasysgp',
337 335 '.fgd': 'application/x-director',
338 336 '.fh': 'image/x-freehand',
339 337 '.fh4': 'image/x-freehand',
340 338 '.fh5': 'image/x-freehand',
341 339 '.fh7': 'image/x-freehand',
342 340 '.fhc': 'image/x-freehand',
343 341 '.fig': 'application/x-xfig',
344 342 '.fli': 'video/x-fli',
345 343 '.flo': 'application/vnd.micrografx.flo',
346 344 '.flv': 'video/x-flv',
347 345 '.flw': 'application/vnd.kde.kivio',
348 346 '.flx': 'text/vnd.fmi.flexstor',
349 347 '.flxh': 'text/x-felix',
350 348 '.fly': 'text/vnd.fly',
351 349 '.fm': 'application/vnd.framemaker',
352 350 '.fnc': 'application/vnd.frogans.fnc',
353 351 '.for': 'text/x-fortran',
354 352 '.fpx': 'image/vnd.fpx',
355 353 '.frag': 'text/x-glslsrc',
356 354 '.frame': 'application/vnd.framemaker',
357 355 '.fs': 'text/x-fsharp',
358 356 '.fsc': 'application/vnd.fsc.weblaunch',
359 357 '.fsi': 'text/x-fsharp',
360 358 '.fst': 'image/vnd.fst',
361 359 '.ftc': 'application/vnd.fluxtime.clip',
362 360 '.fti': 'application/vnd.anser-web-funds-transfer-initiation',
363 361 '.fun': 'text/x-standardml',
364 362 '.fvt': 'video/vnd.fvt',
365 363 '.fxp': 'application/vnd.adobe.fxp',
366 364 '.fxpl': 'application/vnd.adobe.fxp',
367 365 '.fy': 'text/x-fancysrc',
368 366 '.fzs': 'application/vnd.fuzzysheet',
369 367 '.g2w': 'application/vnd.geoplan',
370 368 '.g3': 'image/g3fax',
371 369 '.g3w': 'application/vnd.geospace',
372 370 '.gac': 'application/vnd.groove-account',
373 371 '.gdc': 'text/x-gooddata-cl',
374 372 '.gdl': 'model/vnd.gdl',
375 373 '.gemspec': 'text/x-ruby',
376 374 '.geo': 'application/vnd.dynageo',
377 375 '.gex': 'application/vnd.geometry-explorer',
378 376 '.ggb': 'application/vnd.geogebra.file',
379 377 '.ggt': 'application/vnd.geogebra.tool',
380 378 '.ghf': 'application/vnd.groove-help',
381 379 '.gif': 'image/gif',
382 380 '.gim': 'application/vnd.groove-identity-message',
383 381 '.gmx': 'application/vnd.gmx',
384 382 '.gnumeric': 'application/x-gnumeric',
385 383 '.go': 'text/x-gosrc',
386 384 '.gph': 'application/vnd.flographit',
387 385 '.gqf': 'application/vnd.grafeq',
388 386 '.gqs': 'application/vnd.grafeq',
389 387 '.gram': 'application/srgs',
390 388 '.gre': 'application/vnd.geometry-explorer',
391 389 '.groovy': 'text/x-groovy',
392 390 '.grv': 'application/vnd.groove-injector',
393 391 '.grxml': 'application/srgs+xml',
394 392 '.gs': 'text/x-gosu',
395 393 '.gsf': 'application/x-font-ghostscript',
396 394 '.gsp': 'text/x-gosu',
397 395 '.gst': 'text/x-gosu-template',
398 396 '.gsx': 'text/x-gosu',
399 397 '.gtar': 'application/x-gtar',
400 398 '.gtm': 'application/vnd.groove-tool-message',
401 399 '.gtw': 'model/vnd.gtw',
402 400 '.gv': 'text/vnd.graphviz',
403 401 '.gxt': 'application/vnd.geonext',
404 402 '.h': 'text/x-c',
405 403 '.h++': 'text/x-c++hdr',
406 404 '.h261': 'video/h261',
407 405 '.h263': 'video/h263',
408 406 '.h264': 'video/h264',
409 407 '.hal': 'application/vnd.hal+xml',
410 408 '.haml': 'text/x-haml',
411 409 '.hbci': 'application/vnd.hbci',
412 410 '.hdf': 'application/x-hdf',
413 411 '.hdp': 'text/x-dylan-lid',
414 412 '.hh': 'text/x-c',
415 413 '.hlp': 'application/winhlp',
416 414 '.hpgl': 'application/vnd.hp-hpgl',
417 415 '.hpid': 'application/vnd.hp-hpid',
418 416 '.hpp': 'text/x-c++hdr',
419 417 '.hps': 'application/vnd.hp-hps',
420 418 '.hqx': 'application/mac-binhex40',
421 419 '.hrl': 'text/x-erlang',
422 420 '.hs': 'text/x-haskell',
423 421 '.htke': 'application/vnd.kenameaapp',
424 422 '.htm': 'text/html',
425 423 '.html': 'text/html',
426 424 '.hvd': 'application/vnd.yamaha.hv-dic',
427 425 '.hvp': 'application/vnd.yamaha.hv-voice',
428 426 '.hvs': 'application/vnd.yamaha.hv-script',
429 427 '.hx': 'text/haxe',
430 428 '.hxx': 'text/x-c++hdr',
431 429 '.hy': 'text/x-hybris',
432 430 '.hyb': 'text/x-hybris',
433 431 '.i2g': 'application/vnd.intergeo',
434 432 '.icc': 'application/vnd.iccprofile',
435 433 '.ice': 'x-conference/x-cooltalk',
436 434 '.icm': 'application/vnd.iccprofile',
437 435 '.ico': 'image/x-icon',
438 436 '.ics': 'text/calendar',
439 437 '.idc': 'text/x-chdr',
440 438 '.ief': 'image/ief',
441 439 '.ifb': 'text/calendar',
442 440 '.ifm': 'application/vnd.shana.informed.formdata',
443 441 '.iges': 'model/iges',
444 442 '.igl': 'application/vnd.igloader',
445 443 '.igm': 'application/vnd.insors.igm',
446 444 '.igs': 'model/iges',
447 445 '.igx': 'application/vnd.micrografx.igx',
448 446 '.iif': 'application/vnd.shana.informed.interchange',
449 447 '.ik': 'text/x-iokesrc',
450 448 '.imp': 'application/vnd.accpac.simply.imp',
451 449 '.ims': 'application/vnd.ms-ims',
452 450 '.in': 'text/plain',
453 451 '.inc': 'text/x-povray',
454 452 '.ini': 'text/x-ini',
455 453 '.intr': 'text/x-dylan',
456 454 '.io': 'text/x-iosrc',
457 455 '.ipfix': 'application/ipfix',
458 456 '.ipk': 'application/vnd.shana.informed.package',
459 457 '.irm': 'application/vnd.ibm.rights-management',
460 458 '.irp': 'application/vnd.irepository.package+xml',
461 459 '.iso': 'application/octet-stream',
462 460 '.itp': 'application/vnd.shana.informed.formtemplate',
463 461 '.ivp': 'application/vnd.immervision-ivp',
464 462 '.ivu': 'application/vnd.immervision-ivu',
465 463 '.j': 'text/x-objective-j',
466 464 '.jad': 'text/vnd.sun.j2me.app-descriptor',
467 465 '.jade': 'text/x-jade',
468 466 '.jam': 'application/vnd.jam',
469 467 '.jar': 'application/java-archive',
470 468 '.java': 'text/x-java-source',
471 469 '.jbst': 'text/x-duel',
472 470 '.jisp': 'application/vnd.jisp',
473 471 '.jl': 'text/x-julia',
474 472 '.jlt': 'application/vnd.hp-jlyt',
475 473 '.jnlp': 'application/x-java-jnlp-file',
476 474 '.joda': 'application/vnd.joost.joda-archive',
477 475 '.jp2': 'image/jp2',
478 476 '.jpe': 'image/jpeg',
479 477 '.jpeg': 'image/jpeg',
480 478 '.jpg': 'image/jpeg',
481 479 '.jpgm': 'video/jpm',
482 480 '.jpgv': 'video/jpeg',
483 481 '.jpm': 'video/jpm',
484 482 '.js': 'application/javascript',
485 483 '.json': 'application/json',
486 484 '.jsp': 'application/x-jsp',
487 485 '.kar': 'audio/midi',
488 486 '.karbon': 'application/vnd.kde.karbon',
489 487 '.kfo': 'application/vnd.kde.kformula',
490 488 '.kia': 'application/vnd.kidspiration',
491 489 '.kid': 'application/x-genshi',
492 490 '.kk': 'text/x-koka',
493 491 '.kki': 'text/x-koka',
494 492 '.kml': 'application/vnd.google-earth.kml+xml',
495 493 '.kmz': 'application/vnd.google-earth.kmz',
496 494 '.kne': 'application/vnd.kinar',
497 495 '.knp': 'application/vnd.kinar',
498 496 '.kon': 'application/vnd.kde.kontour',
499 497 '.kpr': 'application/vnd.kde.kpresenter',
500 498 '.kpt': 'application/vnd.kde.kpresenter',
501 499 '.ksh': 'text/plain',
502 500 '.ksp': 'application/vnd.kde.kspread',
503 501 '.kt': 'text/x-kotlin',
504 502 '.ktr': 'application/vnd.kahootz',
505 503 '.ktx': 'image/ktx',
506 504 '.ktz': 'application/vnd.kahootz',
507 505 '.kwd': 'application/vnd.kde.kword',
508 506 '.kwt': 'application/vnd.kde.kword',
509 507 '.lasso': 'text/x-lasso',
510 508 '.lasso[89]': 'text/x-lasso',
511 509 '.lasxml': 'application/vnd.las.las+xml',
512 510 '.latex': 'application/x-latex',
513 511 '.lbd': 'application/vnd.llamagraphics.life-balance.desktop',
514 512 '.lbe': 'application/vnd.llamagraphics.life-balance.exchange+xml',
515 513 '.les': 'application/vnd.hhe.lesson-player',
516 514 '.less': 'text/x-less',
517 515 '.lgt': 'text/x-logtalk',
518 516 '.lha': 'application/octet-stream',
519 517 '.lhs': 'text/x-literate-haskell',
520 518 '.lid': 'text/x-dylan-lid',
521 519 '.link66': 'application/vnd.route66.link66+xml',
522 520 '.lisp': 'text/x-common-lisp',
523 521 '.list': 'text/plain',
524 522 '.list3820': 'application/vnd.ibm.modcap',
525 523 '.listafp': 'application/vnd.ibm.modcap',
526 524 '.ll': 'text/x-llvm',
527 525 '.log': 'text/plain',
528 526 '.lostxml': 'application/lost+xml',
529 527 '.lrf': 'application/octet-stream',
530 528 '.lrm': 'application/vnd.ms-lrm',
531 529 '.ls': 'text/x-livescript',
532 530 '.lsp': 'text/x-newlisp',
533 531 '.ltf': 'application/vnd.frogans.ltf',
534 532 '.ltx': 'text/x-latex',
535 533 '.lua': 'text/x-lua',
536 534 '.lvp': 'audio/vnd.lucent.voice',
537 535 '.lwp': 'application/vnd.lotus-wordpro',
538 536 '.lzh': 'application/octet-stream',
539 537 '.m': 'text/octave',
540 538 '.m13': 'application/x-msmediaview',
541 539 '.m14': 'application/x-msmediaview',
542 540 '.m1v': 'video/mpeg',
543 541 '.m21': 'application/mp21',
544 542 '.m2a': 'audio/mpeg',
545 543 '.m2v': 'video/mpeg',
546 544 '.m3a': 'audio/mpeg',
547 545 '.m3u': 'audio/x-mpegurl',
548 546 '.m3u8': 'application/x-mpegurl',
549 547 '.m4a': 'audio/mp4a-latm',
550 548 '.m4p': 'audio/mp4a-latm',
551 549 '.m4u': 'video/vnd.mpegurl',
552 550 '.m4v': 'video/x-m4v',
553 551 '.ma': 'application/mathematica',
554 552 '.mac': 'image/x-macpaint',
555 553 '.mads': 'application/mads+xml',
556 554 '.mag': 'application/vnd.ecowin.chart',
557 555 '.mak': 'text/x-makefile',
558 556 '.maker': 'application/vnd.framemaker',
559 557 '.mako': 'application/x-mako',
560 558 '.man': 'text/troff',
561 559 '.manifest': 'text/cache-manifest',
562 560 '.maql': 'text/x-gooddata-maql',
563 561 '.markdown': 'text/x-markdown',
564 562 '.mathml': 'application/mathml+xml',
565 563 '.mb': 'application/mathematica',
566 564 '.mbk': 'application/vnd.mobius.mbk',
567 565 '.mbox': 'application/mbox',
568 566 '.mc': 'application/x-mason',
569 567 '.mc1': 'application/vnd.medcalcdata',
570 568 '.mcd': 'application/vnd.mcd',
571 569 '.mcurl': 'text/vnd.curl.mcurl',
572 570 '.md': 'text/x-minidsrc',
573 571 '.mdb': 'application/x-msaccess',
574 572 '.mdi': 'image/vnd.ms-modi',
575 573 '.mdown': 'text/x-markdown',
576 574 '.me': 'text/troff',
577 575 '.mesh': 'model/mesh',
578 576 '.meta4': 'application/metalink4+xml',
579 577 '.mets': 'application/mets+xml',
580 578 '.mfm': 'application/vnd.mfmp',
581 579 '.mgp': 'application/vnd.osgeo.mapguide.package',
582 580 '.mgz': 'application/vnd.proteus.magazine',
583 581 '.mht': 'message/rfc822',
584 582 '.mhtml': 'message/rfc822',
585 583 '.mi': 'application/x-mason',
586 584 '.mid': 'audio/midi',
587 585 '.midi': 'audio/midi',
588 586 '.mif': 'application/vnd.mif',
589 587 '.mime': 'message/rfc822',
590 588 '.mj2': 'video/mj2',
591 589 '.mjp2': 'video/mj2',
592 590 '.ml': 'text/x-ocaml',
593 591 '.mli': 'text/x-ocaml',
594 592 '.mll': 'text/x-ocaml',
595 593 '.mlp': 'application/vnd.dolby.mlp',
596 594 '.mly': 'text/x-ocaml',
597 595 '.mm': 'text/x-objective-c++',
598 596 '.mmd': 'application/vnd.chipnuts.karaoke-mmd',
599 597 '.mmf': 'application/vnd.smaf',
600 598 '.mmr': 'image/vnd.fujixerox.edmics-mmr',
601 599 '.mny': 'application/x-msmoney',
602 600 '.mo': 'text/x-modelica',
603 601 '.mobi': 'application/x-mobipocket-ebook',
604 602 '.mobipocket-ebook': 'application/octet-stream',
605 603 '.mod': 'text/x-modula2',
606 604 '.mods': 'application/mods+xml',
607 605 '.monkey': 'text/x-monkey',
608 606 '.moo': 'text/x-moocode',
609 607 '.moon': 'text/x-moonscript',
610 608 '.mov': 'video/quicktime',
611 609 '.movie': 'video/x-sgi-movie',
612 610 '.mp2': 'audio/mpeg',
613 611 '.mp21': 'application/mp21',
614 612 '.mp2a': 'audio/mpeg',
615 613 '.mp3': 'audio/mpeg',
616 614 '.mp4': 'video/mp4',
617 615 '.mp4a': 'audio/mp4',
618 616 '.mp4s': 'application/mp4',
619 617 '.mp4v': 'video/mp4',
620 618 '.mpa': 'video/mpeg',
621 619 '.mpc': 'application/vnd.mophun.certificate',
622 620 '.mpe': 'video/mpeg',
623 621 '.mpeg': 'video/mpeg',
624 622 '.mpg': 'video/mpeg',
625 623 '.mpg4': 'video/mp4',
626 624 '.mpga': 'audio/mpeg',
627 625 '.mpkg': 'application/vnd.apple.installer+xml',
628 626 '.mpm': 'application/vnd.blueice.multipass',
629 627 '.mpn': 'application/vnd.mophun.application',
630 628 '.mpp': 'application/vnd.ms-project',
631 629 '.mpt': 'application/vnd.ms-project',
632 630 '.mpy': 'application/vnd.ibm.minipay',
633 631 '.mqy': 'application/vnd.mobius.mqy',
634 632 '.mrc': 'application/marc',
635 633 '.mrcx': 'application/marcxml+xml',
636 634 '.ms': 'text/troff',
637 635 '.mscml': 'application/mediaservercontrol+xml',
638 636 '.mseed': 'application/vnd.fdsn.mseed',
639 637 '.mseq': 'application/vnd.mseq',
640 638 '.msf': 'application/vnd.epson.msf',
641 639 '.msh': 'model/mesh',
642 640 '.msi': 'application/x-msdownload',
643 641 '.msl': 'application/vnd.mobius.msl',
644 642 '.msty': 'application/vnd.muvee.style',
645 643 '.mts': 'model/vnd.mts',
646 644 '.mus': 'application/vnd.musician',
647 645 '.musicxml': 'application/vnd.recordare.musicxml+xml',
648 646 '.mvb': 'application/x-msmediaview',
649 647 '.mwf': 'application/vnd.mfer',
650 648 '.mxf': 'application/mxf',
651 649 '.mxl': 'application/vnd.recordare.musicxml',
652 650 '.mxml': 'application/xv+xml',
653 651 '.mxs': 'application/vnd.triscape.mxs',
654 652 '.mxu': 'video/vnd.mpegurl',
655 653 '.myt': 'application/x-myghty',
656 654 '.n': 'text/x-nemerle',
657 655 '.n-gage': 'application/vnd.nokia.n-gage.symbian.install',
658 656 '.n3': 'text/n3',
659 657 '.nb': 'application/mathematica',
660 658 '.nbp': 'application/vnd.wolfram.player',
661 659 '.nc': 'application/x-netcdf',
662 660 '.ncx': 'application/x-dtbncx+xml',
663 661 '.ngdat': 'application/vnd.nokia.n-gage.data',
664 662 '.nim': 'text/x-nimrod',
665 663 '.nimrod': 'text/x-nimrod',
666 664 '.nl': 'text/x-newlisp',
667 665 '.nlu': 'application/vnd.neurolanguage.nlu',
668 666 '.nml': 'application/vnd.enliven',
669 667 '.nnd': 'application/vnd.noblenet-directory',
670 668 '.nns': 'application/vnd.noblenet-sealer',
671 669 '.nnw': 'application/vnd.noblenet-web',
672 670 '.npx': 'image/vnd.net-fpx',
673 671 '.ns2': 'text/x-newspeak',
674 672 '.nsf': 'application/vnd.lotus-notes',
675 673 '.nsh': 'text/x-nsis',
676 674 '.nsi': 'text/x-nsis',
677 675 '.nws': 'message/rfc822',
678 676 '.o': 'application/octet-stream',
679 677 '.oa2': 'application/vnd.fujitsu.oasys2',
680 678 '.oa3': 'application/vnd.fujitsu.oasys3',
681 679 '.oas': 'application/vnd.fujitsu.oasys',
682 680 '.obd': 'application/x-msbinder',
683 681 '.obj': 'application/octet-stream',
684 682 '.objdump': 'text/x-objdump',
685 683 '.oda': 'application/oda',
686 684 '.odb': 'application/vnd.oasis.opendocument.database',
687 685 '.odc': 'application/vnd.oasis.opendocument.chart',
688 686 '.odf': 'application/vnd.oasis.opendocument.formula',
689 687 '.odft': 'application/vnd.oasis.opendocument.formula-template',
690 688 '.odg': 'application/vnd.oasis.opendocument.graphics',
691 689 '.odi': 'application/vnd.oasis.opendocument.image',
692 690 '.odm': 'application/vnd.oasis.opendocument.text-master',
693 691 '.odp': 'application/vnd.oasis.opendocument.presentation',
694 692 '.ods': 'application/vnd.oasis.opendocument.spreadsheet',
695 693 '.odt': 'application/vnd.oasis.opendocument.text',
696 694 '.oga': 'audio/ogg',
697 695 '.ogg': 'audio/ogg',
698 696 '.ogv': 'video/ogg',
699 697 '.ogx': 'application/ogg',
700 698 '.onepkg': 'application/onenote',
701 699 '.onetmp': 'application/onenote',
702 700 '.onetoc': 'application/onenote',
703 701 '.onetoc2': 'application/onenote',
704 702 '.ooc': 'text/x-ooc',
705 703 '.opa': 'text/x-opa',
706 704 '.opf': 'application/oebps-package+xml',
707 705 '.oprc': 'application/vnd.palm',
708 706 '.org': 'application/vnd.lotus-organizer',
709 707 '.osf': 'application/vnd.yamaha.openscoreformat',
710 708 '.osfpvg': 'application/vnd.yamaha.openscoreformat.osfpvg+xml',
711 709 '.otc': 'application/vnd.oasis.opendocument.chart-template',
712 710 '.otf': 'application/x-font-otf',
713 711 '.otg': 'application/vnd.oasis.opendocument.graphics-template',
714 712 '.oth': 'application/vnd.oasis.opendocument.text-web',
715 713 '.oti': 'application/vnd.oasis.opendocument.image-template',
716 714 '.otp': 'application/vnd.oasis.opendocument.presentation-template',
717 715 '.ots': 'application/vnd.oasis.opendocument.spreadsheet-template',
718 716 '.ott': 'application/vnd.oasis.opendocument.text-template',
719 717 '.oxt': 'application/vnd.openofficeorg.extension',
720 718 '.p': 'text/x-pascal',
721 719 '.p10': 'application/pkcs10',
722 720 '.p12': 'application/x-pkcs12',
723 721 '.p7b': 'application/x-pkcs7-certificates',
724 722 '.p7c': 'application/pkcs7-mime',
725 723 '.p7m': 'application/pkcs7-mime',
726 724 '.p7r': 'application/x-pkcs7-certreqresp',
727 725 '.p7s': 'application/pkcs7-signature',
728 726 '.p8': 'application/pkcs8',
729 727 '.pas': 'text/x-pascal',
730 728 '.patch': 'text/x-diff',
731 729 '.paw': 'application/vnd.pawaafile',
732 730 '.pbd': 'application/vnd.powerbuilder6',
733 731 '.pbm': 'image/x-portable-bitmap',
734 732 '.pcf': 'application/x-font-pcf',
735 733 '.pcl': 'application/vnd.hp-pcl',
736 734 '.pclxl': 'application/vnd.hp-pclxl',
737 735 '.pct': 'image/x-pict',
738 736 '.pcurl': 'application/vnd.curl.pcurl',
739 737 '.pcx': 'image/x-pcx',
740 738 '.pdb': 'application/vnd.palm',
741 739 '.pdf': 'application/pdf',
742 740 '.pfa': 'application/x-font-type1',
743 741 '.pfb': 'application/x-font-type1',
744 742 '.pfm': 'application/x-font-type1',
745 743 '.pfr': 'application/font-tdpfr',
746 744 '.pfx': 'application/x-pkcs12',
747 745 '.pgm': 'image/x-portable-graymap',
748 746 '.pgn': 'application/x-chess-pgn',
749 747 '.pgp': 'application/pgp-encrypted',
750 748 '.php': 'text/x-php',
751 749 '.php[345]': 'text/x-php',
752 750 '.phtml': 'application/x-php',
753 751 '.pic': 'image/x-pict',
754 752 '.pict': 'image/pict',
755 753 '.pkg': 'application/octet-stream',
756 754 '.pki': 'application/pkixcmp',
757 755 '.pkipath': 'application/pkix-pkipath',
758 756 '.pl': 'text/plain',
759 757 '.plb': 'application/vnd.3gpp.pic-bw-large',
760 758 '.plc': 'application/vnd.mobius.plc',
761 759 '.plf': 'application/vnd.pocketlearn',
762 760 '.plot': 'text/x-gnuplot',
763 761 '.pls': 'application/pls+xml',
764 762 '.plt': 'text/x-gnuplot',
765 763 '.pm': 'text/x-perl',
766 764 '.pml': 'application/vnd.ctc-posml',
767 765 '.png': 'image/png',
768 766 '.pnm': 'image/x-portable-anymap',
769 767 '.pnt': 'image/x-macpaint',
770 768 '.pntg': 'image/x-macpaint',
771 769 '.po': 'application/x-gettext',
772 770 '.portpkg': 'application/vnd.macports.portpkg',
773 771 '.pot': 'application/vnd.ms-powerpoint',
774 772 '.potm': 'application/vnd.ms-powerpoint.template.macroenabled.12',
775 773 '.potx': 'application/vnd.openxmlformats-officedocument.presentationml.template',
776 774 '.pov': 'text/x-povray',
777 775 '.ppa': 'application/vnd.ms-powerpoint',
778 776 '.ppam': 'application/vnd.ms-powerpoint.addin.macroenabled.12',
779 777 '.ppd': 'application/vnd.cups-ppd',
780 778 '.ppm': 'image/x-portable-pixmap',
781 779 '.pps': 'application/vnd.ms-powerpoint',
782 780 '.ppsm': 'application/vnd.ms-powerpoint.slideshow.macroenabled.12',
783 781 '.ppsx': 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
784 782 '.ppt': 'application/vnd.ms-powerpoint',
785 783 '.pptm': 'application/vnd.ms-powerpoint.presentation.macroenabled.12',
786 784 '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
787 785 '.pqa': 'application/vnd.palm',
788 786 '.prc': 'application/x-mobipocket-ebook',
789 787 '.pre': 'application/vnd.lotus-freelance',
790 788 '.prf': 'application/pics-rules',
791 789 '.pro': 'text/idl',
792 790 '.prolog': 'text/x-prolog',
793 791 '.properties': 'text/x-properties',
794 792 '.ps': 'application/postscript',
795 793 '.ps1': 'text/x-powershell',
796 794 '.psb': 'application/vnd.3gpp.pic-bw-small',
797 795 '.psd': 'image/vnd.adobe.photoshop',
798 796 '.psf': 'application/x-font-linux-psf',
799 797 '.pskcxml': 'application/pskc+xml',
800 798 '.ptid': 'application/vnd.pvi.ptid1',
801 799 '.pub': 'application/x-mspublisher',
802 800 '.pvb': 'application/vnd.3gpp.pic-bw-var',
803 801 '.pwn': 'application/vnd.3m.post-it-notes',
804 802 '.pwz': 'application/vnd.ms-powerpoint',
805 803 '.pxd': 'text/x-cython',
806 804 '.pxi': 'text/x-cython',
807 805 '.py': 'text/x-python',
808 806 '.py3tb': 'text/x-python3-traceback',
809 807 '.pya': 'audio/vnd.ms-playready.media.pya',
810 808 '.pyc': 'application/x-python-code',
811 809 '.pyo': 'application/x-python-code',
812 810 '.pypylog': 'application/x-pypylog',
813 811 '.pytb': 'text/x-python-traceback',
814 812 '.pyv': 'video/vnd.ms-playready.media.pyv',
815 813 '.pyw': 'text/x-python',
816 814 '.pyx': 'text/x-cython',
817 815 '.qam': 'application/vnd.epson.quickanime',
818 816 '.qbo': 'application/vnd.intu.qbo',
819 817 '.qfx': 'application/vnd.intu.qfx',
820 818 '.qml': 'application/x-qml',
821 819 '.qps': 'application/vnd.publishare-delta-tree',
822 820 '.qt': 'video/quicktime',
823 821 '.qti': 'image/x-quicktime',
824 822 '.qtif': 'image/x-quicktime',
825 823 '.qwd': 'application/vnd.quark.quarkxpress',
826 824 '.qwt': 'application/vnd.quark.quarkxpress',
827 825 '.qxb': 'application/vnd.quark.quarkxpress',
828 826 '.qxd': 'application/vnd.quark.quarkxpress',
829 827 '.qxl': 'application/vnd.quark.quarkxpress',
830 828 '.qxt': 'application/vnd.quark.quarkxpress',
831 829 '.r': 'text/x-rebol',
832 830 '.r3': 'text/x-rebol',
833 831 '.ra': 'audio/x-pn-realaudio',
834 832 '.rake': 'text/x-ruby',
835 833 '.ram': 'audio/x-pn-realaudio',
836 834 '.rar': 'application/x-rar-compressed',
837 835 '.ras': 'image/x-cmu-raster',
838 836 '.rb': 'text/x-ruby',
839 837 '.rbw': 'text/x-ruby',
840 838 '.rbx': 'text/x-ruby',
841 839 '.rc': 'text/x-stsrc',
842 840 '.rcprofile': 'application/vnd.ipunplugged.rcprofile',
843 841 '.rdf': 'application/rdf+xml',
844 842 '.rdz': 'application/vnd.data-vision.rdz',
845 843 '.reg': 'text/x-windows-registry',
846 844 '.rep': 'application/vnd.businessobjects',
847 845 '.res': 'application/x-dtbresource+xml',
848 846 '.rest': 'text/x-rst',
849 847 '.rgb': 'image/x-rgb',
850 848 '.rhtml': 'text/html+ruby',
851 849 '.rif': 'application/reginfo+xml',
852 850 '.rip': 'audio/vnd.rip',
853 851 '.rkt': 'text/x-racket',
854 852 '.rktl': 'text/x-racket',
855 853 '.rl': 'application/resource-lists+xml',
856 854 '.rlc': 'image/vnd.fujixerox.edmics-rlc',
857 855 '.rld': 'application/resource-lists-diff+xml',
858 856 '.rm': 'application/vnd.rn-realmedia',
859 857 '.rmi': 'audio/midi',
860 858 '.rmp': 'audio/x-pn-realaudio-plugin',
861 859 '.rms': 'application/vnd.jcp.javame.midlet-rms',
862 860 '.rnc': 'application/relax-ng-compact-syntax',
863 861 '.robot': 'text/x-robotframework',
864 862 '.roff': 'text/troff',
865 863 '.rp9': 'application/vnd.cloanto.rp9',
866 864 '.rpss': 'application/vnd.nokia.radio-presets',
867 865 '.rpst': 'application/vnd.nokia.radio-preset',
868 866 '.rq': 'application/sparql-query',
869 867 '.rs': 'application/rls-services+xml',
870 868 '.rsd': 'application/rsd+xml',
871 869 '.rss': 'application/rss+xml',
872 870 '.rst': 'text/x-rst',
873 871 '.rtf': 'application/rtf',
874 872 '.rtx': 'text/richtext',
875 873 '.s': 'text/x-asm',
876 874 '.saf': 'application/vnd.yamaha.smaf-audio',
877 875 '.sage': 'text/x-python',
878 876 '.sass': 'text/x-sass',
879 877 '.sbml': 'application/sbml+xml',
880 878 '.sc': 'application/vnd.ibm.secure-container',
881 879 '.scala': 'text/x-scala',
882 880 '.scaml': 'text/x-scaml',
883 881 '.scd': 'application/x-msschedule',
884 882 '.sce': 'text/scilab',
885 883 '.sci': 'text/scilab',
886 884 '.scm': 'application/vnd.lotus-screencam',
887 885 '.scq': 'application/scvp-cv-request',
888 886 '.scs': 'application/scvp-cv-response',
889 887 '.scss': 'text/x-scss',
890 888 '.scurl': 'text/vnd.curl.scurl',
891 889 '.sda': 'application/vnd.stardivision.draw',
892 890 '.sdc': 'application/vnd.stardivision.calc',
893 891 '.sdd': 'application/vnd.stardivision.impress',
894 892 '.sdkd': 'application/vnd.solent.sdkm+xml',
895 893 '.sdkm': 'application/vnd.solent.sdkm+xml',
896 894 '.sdp': 'application/sdp',
897 895 '.sdw': 'application/vnd.stardivision.writer',
898 896 '.see': 'application/vnd.seemail',
899 897 '.seed': 'application/vnd.fdsn.seed',
900 898 '.sema': 'application/vnd.sema',
901 899 '.semd': 'application/vnd.semd',
902 900 '.semf': 'application/vnd.semf',
903 901 '.ser': 'application/java-serialized-object',
904 902 '.setpay': 'application/set-payment-initiation',
905 903 '.setreg': 'application/set-registration-initiation',
906 904 '.sfd-hdstx': 'application/vnd.hydrostatix.sof-data',
907 905 '.sfs': 'application/vnd.spotfire.sfs',
908 906 '.sgl': 'application/vnd.stardivision.writer-global',
909 907 '.sgm': 'text/sgml',
910 908 '.sgml': 'text/sgml',
911 909 '.sh': 'application/x-sh',
912 910 '.sh-session': 'application/x-shell-session',
913 911 '.shar': 'application/x-shar',
914 912 '.shell-session': 'application/x-sh-session',
915 913 '.shf': 'application/shf+xml',
916 914 '.sig': 'application/pgp-signature',
917 915 '.silo': 'model/mesh',
918 916 '.sis': 'application/vnd.symbian.install',
919 917 '.sisx': 'application/vnd.symbian.install',
920 918 '.sit': 'application/x-stuffit',
921 919 '.sitx': 'application/x-stuffitx',
922 920 '.skd': 'application/vnd.koan',
923 921 '.skm': 'application/vnd.koan',
924 922 '.skp': 'application/vnd.koan',
925 923 '.skt': 'application/vnd.koan',
926 924 '.sldm': 'application/vnd.ms-powerpoint.slide.macroenabled.12',
927 925 '.sldx': 'application/vnd.openxmlformats-officedocument.presentationml.slide',
928 926 '.slt': 'application/vnd.epson.salt',
929 927 '.sm': 'application/vnd.stepmania.stepchart',
930 928 '.smali': 'text/smali',
931 929 '.smf': 'application/vnd.stardivision.math',
932 930 '.smi': 'application/smil+xml',
933 931 '.smil': 'application/smil+xml',
934 932 '.sml': 'text/x-standardml',
935 933 '.snd': 'audio/basic',
936 934 '.snf': 'application/x-font-snf',
937 935 '.snobol': 'text/x-snobol',
938 936 '.so': 'application/octet-stream',
939 937 '.sp': 'text/x-sourcepawn',
940 938 '.spc': 'application/x-pkcs7-certificates',
941 939 '.spec': 'text/x-rpm-spec',
942 940 '.spf': 'application/vnd.yamaha.smaf-phrase',
943 941 '.spl': 'application/x-futuresplash',
944 942 '.spot': 'text/vnd.in3d.spot',
945 943 '.spp': 'application/scvp-vp-response',
946 944 '.spq': 'application/scvp-vp-request',
947 945 '.spt': 'application/x-cheetah',
948 946 '.spx': 'audio/ogg',
949 947 '.sql': 'text/x-sql',
950 948 '.sqlite3-console': 'text/x-sqlite3-console',
951 949 '.src': 'application/x-wais-source',
952 950 '.sru': 'application/sru+xml',
953 951 '.srx': 'application/sparql-results+xml',
954 952 '.ss': 'text/x-scheme',
955 953 '.sse': 'application/vnd.kodak-descriptor',
956 954 '.ssf': 'application/vnd.epson.ssf',
957 955 '.ssml': 'application/ssml+xml',
958 956 '.ssp': 'application/x-ssp',
959 957 '.st': 'application/vnd.sailingtracker.track',
960 958 '.stc': 'application/vnd.sun.xml.calc.template',
961 959 '.std': 'application/vnd.sun.xml.draw.template',
962 960 '.stf': 'application/vnd.wt.stf',
963 961 '.sti': 'application/vnd.sun.xml.impress.template',
964 962 '.stk': 'application/hyperstudio',
965 963 '.stl': 'application/vnd.ms-pki.stl',
966 964 '.str': 'application/vnd.pg.format',
967 965 '.stw': 'application/vnd.sun.xml.writer.template',
968 966 '.sub': 'image/vnd.dvb.subtitle',
969 967 '.sus': 'application/vnd.sus-calendar',
970 968 '.susp': 'application/vnd.sus-calendar',
971 969 '.sv': 'text/x-systemverilog',
972 970 '.sv4cpio': 'application/x-sv4cpio',
973 971 '.sv4crc': 'application/x-sv4crc',
974 972 '.svc': 'application/vnd.dvb.service',
975 973 '.svd': 'application/vnd.svd',
976 974 '.svg': 'image/svg+xml',
977 975 '.svgz': 'image/svg+xml',
978 976 '.svh': 'text/x-systemverilog',
979 977 '.swa': 'application/x-director',
980 978 '.swf': 'application/x-shockwave-flash',
981 979 '.swi': 'application/vnd.aristanetworks.swi',
982 980 '.sxc': 'application/vnd.sun.xml.calc',
983 981 '.sxd': 'application/vnd.sun.xml.draw',
984 982 '.sxg': 'application/vnd.sun.xml.writer.global',
985 983 '.sxi': 'application/vnd.sun.xml.impress',
986 984 '.sxm': 'application/vnd.sun.xml.math',
987 985 '.sxw': 'application/vnd.sun.xml.writer',
988 986 '.t': 'text/troff',
989 987 '.tac': 'text/x-python',
990 988 '.tao': 'application/vnd.tao.intent-module-archive',
991 989 '.tar': 'application/x-tar',
992 990 '.tcap': 'application/vnd.3gpp2.tcap',
993 991 '.tcl': 'application/x-tcl',
994 992 '.tcsh': 'application/x-csh',
995 993 '.tea': 'text/x-tea',
996 994 '.teacher': 'application/vnd.smart.teacher',
997 995 '.tei': 'application/tei+xml',
998 996 '.teicorpus': 'application/tei+xml',
999 997 '.tex': 'application/x-tex',
1000 998 '.texi': 'application/x-texinfo',
1001 999 '.texinfo': 'application/x-texinfo',
1002 1000 '.text': 'text/plain',
1003 1001 '.tfi': 'application/thraud+xml',
1004 1002 '.tfm': 'application/x-tex-tfm',
1005 1003 '.thmx': 'application/vnd.ms-officetheme',
1006 1004 '.tif': 'image/tiff',
1007 1005 '.tiff': 'image/tiff',
1008 1006 '.tmo': 'application/vnd.tmobile-livetv',
1009 1007 '.tmpl': 'application/x-cheetah',
1010 1008 '.toc': 'text/x-tex',
1011 1009 '.torrent': 'application/x-bittorrent',
1012 1010 '.tpl': 'application/vnd.groove-tool-template',
1013 1011 '.tpt': 'application/vnd.trid.tpt',
1014 1012 '.tr': 'text/troff',
1015 1013 '.tra': 'application/vnd.trueapp',
1016 1014 '.trm': 'application/x-msterminal',
1017 1015 '.ts': 'video/mp2t',
1018 1016 '.tsd': 'application/timestamped-data',
1019 1017 '.tst': 'text/scilab',
1020 1018 '.tsv': 'text/tab-separated-values',
1021 1019 '.ttc': 'application/x-font-ttf',
1022 1020 '.ttf': 'application/x-font-ttf',
1023 1021 '.ttl': 'text/turtle',
1024 1022 '.twd': 'application/vnd.simtech-mindmapper',
1025 1023 '.twds': 'application/vnd.simtech-mindmapper',
1026 1024 '.txd': 'application/vnd.genomatix.tuxedo',
1027 1025 '.txf': 'application/vnd.mobius.txf',
1028 1026 '.txt': 'text/plain',
1029 1027 '.u': 'application/x-urbiscript',
1030 1028 '.u32': 'application/x-authorware-bin',
1031 1029 '.udeb': 'application/x-debian-package',
1032 1030 '.ufd': 'application/vnd.ufdl',
1033 1031 '.ufdl': 'application/vnd.ufdl',
1034 1032 '.umj': 'application/vnd.umajin',
1035 1033 '.unityweb': 'application/vnd.unity',
1036 1034 '.uoml': 'application/vnd.uoml+xml',
1037 1035 '.uri': 'text/uri-list',
1038 1036 '.uris': 'text/uri-list',
1039 1037 '.urls': 'text/uri-list',
1040 1038 '.ustar': 'application/x-ustar',
1041 1039 '.utz': 'application/vnd.uiq.theme',
1042 1040 '.uu': 'text/x-uuencode',
1043 1041 '.uva': 'audio/vnd.dece.audio',
1044 1042 '.uvd': 'application/vnd.dece.data',
1045 1043 '.uvf': 'application/vnd.dece.data',
1046 1044 '.uvg': 'image/vnd.dece.graphic',
1047 1045 '.uvh': 'video/vnd.dece.hd',
1048 1046 '.uvi': 'image/vnd.dece.graphic',
1049 1047 '.uvm': 'video/vnd.dece.mobile',
1050 1048 '.uvp': 'video/vnd.dece.pd',
1051 1049 '.uvs': 'video/vnd.dece.sd',
1052 1050 '.uvt': 'application/vnd.dece.ttml+xml',
1053 1051 '.uvu': 'video/vnd.uvvu.mp4',
1054 1052 '.uvv': 'video/vnd.dece.video',
1055 1053 '.uvva': 'audio/vnd.dece.audio',
1056 1054 '.uvvd': 'application/vnd.dece.data',
1057 1055 '.uvvf': 'application/vnd.dece.data',
1058 1056 '.uvvg': 'image/vnd.dece.graphic',
1059 1057 '.uvvh': 'video/vnd.dece.hd',
1060 1058 '.uvvi': 'image/vnd.dece.graphic',
1061 1059 '.uvvm': 'video/vnd.dece.mobile',
1062 1060 '.uvvp': 'video/vnd.dece.pd',
1063 1061 '.uvvs': 'video/vnd.dece.sd',
1064 1062 '.uvvt': 'application/vnd.dece.ttml+xml',
1065 1063 '.uvvu': 'video/vnd.uvvu.mp4',
1066 1064 '.uvvv': 'video/vnd.dece.video',
1067 1065 '.uvvx': 'application/vnd.dece.unspecified',
1068 1066 '.uvx': 'application/vnd.dece.unspecified',
1069 1067 '.v': 'text/x-verilog',
1070 1068 '.vala': 'text/x-vala',
1071 1069 '.vapi': 'text/x-vala',
1072 1070 '.vark': 'text/x-gosu',
1073 1071 '.vb': 'text/vbscript',
1074 1072 '.vcd': 'application/x-cdlink',
1075 1073 '.vcf': 'text/x-vcard',
1076 1074 '.vcg': 'application/vnd.groove-vcard',
1077 1075 '.vcs': 'text/x-vcalendar',
1078 1076 '.vcx': 'application/vnd.vcx',
1079 1077 '.vert': 'text/x-glslsrc',
1080 1078 '.vhd': 'text/x-vhdl',
1081 1079 '.vhdl': 'text/x-vhdl',
1082 1080 '.vim': 'text/x-vim',
1083 1081 '.vis': 'application/vnd.visionary',
1084 1082 '.viv': 'video/vnd.vivo',
1085 1083 '.vor': 'application/vnd.stardivision.writer',
1086 1084 '.vox': 'application/x-authorware-bin',
1087 1085 '.vrml': 'model/vrml',
1088 1086 '.vsd': 'application/vnd.visio',
1089 1087 '.vsf': 'application/vnd.vsf',
1090 1088 '.vss': 'application/vnd.visio',
1091 1089 '.vst': 'application/vnd.visio',
1092 1090 '.vsw': 'application/vnd.visio',
1093 1091 '.vtu': 'model/vnd.vtu',
1094 1092 '.vxml': 'application/voicexml+xml',
1095 1093 '.w3d': 'application/x-director',
1096 1094 '.wad': 'application/x-doom',
1097 1095 '.wav': 'audio/x-wav',
1098 1096 '.wax': 'audio/x-ms-wax',
1099 1097 '.wbmp': 'image/vnd.wap.wbmp',
1100 1098 '.wbs': 'application/vnd.criticaltools.wbs+xml',
1101 1099 '.wbxml': 'application/vnd.wap.wbxml',
1102 1100 '.wcm': 'application/vnd.ms-works',
1103 1101 '.wdb': 'application/vnd.ms-works',
1104 1102 '.weba': 'audio/webm',
1105 1103 '.webm': 'video/webm',
1106 1104 '.webp': 'image/webp',
1107 1105 '.weechatlog': 'text/x-irclog',
1108 1106 '.wg': 'application/vnd.pmi.widget',
1109 1107 '.wgt': 'application/widget',
1110 1108 '.wiz': 'application/msword',
1111 1109 '.wks': 'application/vnd.ms-works',
1112 1110 '.wlua': 'text/x-lua',
1113 1111 '.wm': 'video/x-ms-wm',
1114 1112 '.wma': 'audio/x-ms-wma',
1115 1113 '.wmd': 'application/x-ms-wmd',
1116 1114 '.wmf': 'application/x-msmetafile',
1117 1115 '.wml': 'text/vnd.wap.wml',
1118 1116 '.wmlc': 'application/vnd.wap.wmlc',
1119 1117 '.wmls': 'text/vnd.wap.wmlscript',
1120 1118 '.wmlsc': 'application/vnd.wap.wmlscriptc',
1121 1119 '.wmv': 'video/x-ms-wmv',
1122 1120 '.wmx': 'video/x-ms-wmx',
1123 1121 '.wmz': 'application/x-ms-wmz',
1124 1122 '.woff': 'application/x-font-woff',
1125 1123 '.wpd': 'application/vnd.wordperfect',
1126 1124 '.wpl': 'application/vnd.ms-wpl',
1127 1125 '.wps': 'application/vnd.ms-works',
1128 1126 '.wqd': 'application/vnd.wqd',
1129 1127 '.wri': 'application/x-mswrite',
1130 1128 '.wrl': 'model/vrml',
1131 1129 '.wsdl': 'application/wsdl+xml',
1132 1130 '.wspolicy': 'application/wspolicy+xml',
1133 1131 '.wtb': 'application/vnd.webturbo',
1134 1132 '.wvx': 'video/x-ms-wvx',
1135 1133 '.x': 'text/x-logos',
1136 1134 '.x32': 'application/x-authorware-bin',
1137 1135 '.x3d': 'application/vnd.hzn-3d-crossword',
1138 1136 '.xap': 'application/x-silverlight-app',
1139 1137 '.xar': 'application/vnd.xara',
1140 1138 '.xbap': 'application/x-ms-xbap',
1141 1139 '.xbd': 'application/vnd.fujixerox.docuworks.binder',
1142 1140 '.xbm': 'image/x-xbitmap',
1143 1141 '.xdf': 'application/xcap-diff+xml',
1144 1142 '.xdm': 'application/vnd.syncml.dm+xml',
1145 1143 '.xdp': 'application/vnd.adobe.xdp+xml',
1146 1144 '.xdssc': 'application/dssc+xml',
1147 1145 '.xdw': 'application/vnd.fujixerox.docuworks',
1148 1146 '.xenc': 'application/xenc+xml',
1149 1147 '.xer': 'application/patch-ops-error+xml',
1150 1148 '.xfdf': 'application/vnd.adobe.xfdf',
1151 1149 '.xfdl': 'application/vnd.xfdl',
1152 1150 '.xht': 'application/xhtml+xml',
1153 1151 '.xhtml': 'application/xhtml+xml',
1154 1152 '.xhvml': 'application/xv+xml',
1155 1153 '.xi': 'text/x-logos',
1156 1154 '.xif': 'image/vnd.xiff',
1157 1155 '.xla': 'application/vnd.ms-excel',
1158 1156 '.xlam': 'application/vnd.ms-excel.addin.macroenabled.12',
1159 1157 '.xlb': 'application/vnd.ms-excel',
1160 1158 '.xlc': 'application/vnd.ms-excel',
1161 1159 '.xlm': 'application/vnd.ms-excel',
1162 1160 '.xls': 'application/vnd.ms-excel',
1163 1161 '.xlsb': 'application/vnd.ms-excel.sheet.binary.macroenabled.12',
1164 1162 '.xlsm': 'application/vnd.ms-excel.sheet.macroenabled.12',
1165 1163 '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
1166 1164 '.xlt': 'application/vnd.ms-excel',
1167 1165 '.xltm': 'application/vnd.ms-excel.template.macroenabled.12',
1168 1166 '.xltx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
1169 1167 '.xlw': 'application/vnd.ms-excel',
1170 1168 '.xm': 'text/x-logos',
1171 1169 '.xmi': 'text/x-logos',
1172 1170 '.xml': 'application/xml',
1173 1171 '.xo': 'application/vnd.olpc-sugar',
1174 1172 '.xop': 'application/xop+xml',
1175 1173 '.xpdl': 'application/xml',
1176 1174 '.xpi': 'application/x-xpinstall',
1177 1175 '.xpl': 'application/xsl+xml',
1178 1176 '.xpm': 'image/x-xpixmap',
1179 1177 '.xpr': 'application/vnd.is-xpr',
1180 1178 '.xps': 'application/vnd.ms-xpsdocument',
1181 1179 '.xpw': 'application/vnd.intercon.formnet',
1182 1180 '.xpx': 'application/vnd.intercon.formnet',
1183 1181 '.xq': 'application/xquery',
1184 1182 '.xql': 'application/xquery',
1185 1183 '.xqm': 'application/xquery',
1186 1184 '.xquery': 'application/xquery',
1187 1185 '.xqy': 'application/xquery',
1188 1186 '.xsd': 'application/xml',
1189 1187 '.xsl': 'application/xml',
1190 1188 '.xslt': 'application/xslt+xml',
1191 1189 '.xsm': 'application/vnd.syncml+xml',
1192 1190 '.xspf': 'application/xspf+xml',
1193 1191 '.xtend': 'text/x-xtend',
1194 1192 '.xul': 'application/vnd.mozilla.xul+xml',
1195 1193 '.xvm': 'application/xv+xml',
1196 1194 '.xvml': 'application/xv+xml',
1197 1195 '.xwd': 'image/x-xwindowdump',
1198 1196 '.xyz': 'chemical/x-xyz',
1199 1197 '.yaml': 'text/x-yaml',
1200 1198 '.yang': 'application/yang',
1201 1199 '.yin': 'application/yin+xml',
1202 1200 '.yml': 'text/x-yaml',
1203 1201 '.zaz': 'application/vnd.zzazz.deck+xml',
1204 1202 '.zip': 'application/zip',
1205 1203 '.zir': 'application/vnd.zul',
1206 1204 '.zirz': 'application/vnd.zul',
1207 1205 '.zmm': 'application/vnd.handheld-entertainment+xml'}
1208 1206 ]
1209 1207
1210 1208
1211 1209 def get_mimetypes_db(extra_types=None):
1212 1210 import mimetypes
1213 1211 types_map = TYPES_MAP
1214 1212 if extra_types:
1215 1213 types_map = TYPES_MAP[::] # copy the initial version for extending
1216 1214 types_map[1].update(extra_types)
1217 1215 db = mimetypes.MimeTypes()
1218 1216 db.types_map = types_map
1219 1217 db.encodings_map.update(DEFAULTS['encodings_map'])
1220 1218 db.suffix_map.update(DEFAULTS['suffix_map'])
1221 1219 return db
@@ -1,76 +1,74 b''
1
2
3 1 # Copyright (C) 2014-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 """
22 20 Internal settings for vcs-lib
23 21 """
24 22
25 23 # list of default encoding used in safe_str methods
26 24 DEFAULT_ENCODINGS = ['utf8']
27 25
28 26
29 27 # Compatibility version when creating SVN repositories. None means newest.
30 28 # Other available options are: pre-1.4-compatible, pre-1.5-compatible,
31 29 # pre-1.6-compatible, pre-1.8-compatible
32 30 SVN_COMPATIBLE_VERSION = None
33 31
34 32 ALIASES = ['hg', 'git', 'svn']
35 33
36 34 BACKENDS = {
37 35 'hg': 'rhodecode.lib.vcs.backends.hg.MercurialRepository',
38 36 'git': 'rhodecode.lib.vcs.backends.git.GitRepository',
39 37 'svn': 'rhodecode.lib.vcs.backends.svn.SubversionRepository',
40 38 }
41 39
42 40
43 41 ARCHIVE_SPECS = [
44 42 ('tbz2', 'application/x-bzip2', '.tbz2'),
45 43 ('tbz2', 'application/x-bzip2', '.tar.bz2'),
46 44
47 45 ('tgz', 'application/x-gzip', '.tgz'),
48 46 ('tgz', 'application/x-gzip', '.tar.gz'),
49 47
50 48 ('zip', 'application/zip', '.zip'),
51 49 ]
52 50
53 51 HOOKS_PROTOCOL = None
54 52 HOOKS_DIRECT_CALLS = False
55 53 HOOKS_HOST = '127.0.0.1'
56 54
57 55
58 56 MERGE_MESSAGE_TMPL = (
59 u'Merge pull request !{pr_id} from {source_repo} {source_ref_name}\n\n '
60 u'{pr_title}')
57 'Merge pull request !{pr_id} from {source_repo} {source_ref_name}\n\n '
58 '{pr_title}')
61 59 MERGE_DRY_RUN_MESSAGE = 'dry_run_merge_message_from_rhodecode'
62 60 MERGE_DRY_RUN_USER = 'Dry-Run User'
63 61 MERGE_DRY_RUN_EMAIL = 'dry-run-merge@rhodecode.com'
64 62
65 63
66 64 def available_aliases():
67 65 """
68 66 Mercurial is required for the system to work, so in case vcs.backends does
69 67 not include it, we make sure it will be available internally
70 68 TODO: anderson: refactor vcs.backends so it won't be necessary, VCS server
71 69 should be responsible to dictate available backends.
72 70 """
73 71 aliases = ALIASES[:]
74 72 if 'hg' not in aliases:
75 73 aliases += ['hg']
76 74 return aliases
@@ -1,45 +1,43 b''
1
2
3 1 # Copyright (C) 2014-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 """
22 20 Holds connection for remote server.
23 21 """
24 22
25 23
26 24 class NotInitializedConnection(object):
27 25 """Placeholder for objects which have to be initialized first."""
28 26
29 27 def _raise_exc(self):
30 28 raise Exception(
31 29 "rhodecode.lib.vcs is not yet initialized. "
32 30 "Make sure `vcs.server` is enabled in your configuration.")
33 31
34 32 def __getattr__(self, item):
35 33 self._raise_exc()
36 34
37 35 def __call__(self, *args, **kwargs):
38 36 self._raise_exc()
39 37
40 38 # TODO: figure out a nice default value for these things
41 39 Service = NotInitializedConnection()
42 40
43 41 Git = NotInitializedConnection()
44 42 Hg = NotInitializedConnection()
45 43 Svn = NotInitializedConnection()
@@ -1,234 +1,232 b''
1
2
3 1 # Copyright (C) 2014-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 """
22 20 Custom vcs exceptions module.
23 21 """
24 22 import logging
25 23 import functools
26 24 import urllib.error
27 25 import urllib.parse
28 26 import rhodecode
29 27
30 28 log = logging.getLogger(__name__)
31 29
32 30
33 31 class VCSCommunicationError(Exception):
34 32 pass
35 33
36 34
37 35 class HttpVCSCommunicationError(VCSCommunicationError):
38 36 pass
39 37
40 38
41 39 class VCSError(Exception):
42 40 pass
43 41
44 42
45 43 class RepositoryError(VCSError):
46 44 pass
47 45
48 46
49 47 class RepositoryRequirementError(RepositoryError):
50 48 pass
51 49
52 50
53 51 class UnresolvedFilesInRepo(RepositoryError):
54 52 pass
55 53
56 54
57 55 class VCSBackendNotSupportedError(VCSError):
58 56 """
59 57 Exception raised when VCSServer does not support requested backend
60 58 """
61 59
62 60
63 61 class EmptyRepositoryError(RepositoryError):
64 62 pass
65 63
66 64
67 65 class TagAlreadyExistError(RepositoryError):
68 66 pass
69 67
70 68
71 69 class TagDoesNotExistError(RepositoryError):
72 70 pass
73 71
74 72
75 73 class BranchAlreadyExistError(RepositoryError):
76 74 pass
77 75
78 76
79 77 class BranchDoesNotExistError(RepositoryError):
80 78 pass
81 79
82 80
83 81 class CommitError(RepositoryError):
84 82 """
85 83 Exceptions related to an existing commit
86 84 """
87 85
88 86
89 87 class CommitDoesNotExistError(CommitError):
90 88 pass
91 89
92 90
93 91 class CommittingError(RepositoryError):
94 92 """
95 93 Exceptions happening while creating a new commit
96 94 """
97 95
98 96
99 97 class NothingChangedError(CommittingError):
100 98 pass
101 99
102 100
103 101 class NodeError(VCSError):
104 102 pass
105 103
106 104
107 105 class RemovedFileNodeError(NodeError):
108 106 pass
109 107
110 108
111 109 class NodeAlreadyExistsError(CommittingError):
112 110 pass
113 111
114 112
115 113 class NodeAlreadyChangedError(CommittingError):
116 114 pass
117 115
118 116
119 117 class NodeDoesNotExistError(CommittingError):
120 118 pass
121 119
122 120
123 121 class NodeNotChangedError(CommittingError):
124 122 pass
125 123
126 124
127 125 class NodeAlreadyAddedError(CommittingError):
128 126 pass
129 127
130 128
131 129 class NodeAlreadyRemovedError(CommittingError):
132 130 pass
133 131
134 132
135 133 class SubrepoMergeError(RepositoryError):
136 134 """
137 135 This happens if we try to merge a repository which contains subrepos and
138 136 the subrepos cannot be merged. The subrepos are not merged itself but
139 137 their references in the root repo are merged.
140 138 """
141 139
142 140
143 141 class ImproperArchiveTypeError(VCSError):
144 142 pass
145 143
146 144
147 145 class CommandError(VCSError):
148 146 pass
149 147
150 148
151 149 class UnhandledException(VCSError):
152 150 """
153 151 Signals that something unexpected went wrong.
154 152
155 153 This usually means we have a programming error on the side of the VCSServer
156 154 and should inspect the logfile of the VCSServer to find more details.
157 155 """
158 156
159 157
160 158 _EXCEPTION_MAP = {
161 159 'abort': RepositoryError,
162 160 'archive': ImproperArchiveTypeError,
163 161 'error': RepositoryError,
164 162 'lookup': CommitDoesNotExistError,
165 163 'repo_locked': RepositoryError,
166 164 'requirement': RepositoryRequirementError,
167 165 'unhandled': UnhandledException,
168 166 # TODO: johbo: Define our own exception for this and stop abusing
169 167 # urllib's exception class.
170 168 'url_error': urllib.error.URLError,
171 169 'subrepo_merge_error': SubrepoMergeError,
172 170 }
173 171
174 172
175 173 def map_vcs_exceptions(func):
176 174 """
177 175 Utility to decorate functions so that plain exceptions are translated.
178 176
179 177 The translation is based on `exc_map` which maps a `str` indicating
180 178 the error type into an exception class representing this error inside
181 179 of the vcs layer.
182 180 """
183 181
184 182 @functools.wraps(func)
185 183 def wrapper(*args, **kwargs):
186 184 try:
187 185 return func(*args, **kwargs)
188 186 except Exception as e:
189 187 debug = rhodecode.ConfigGet().get_bool('debug')
190 188
191 189 # The error middleware adds information if it finds
192 190 # __traceback_info__ in a frame object. This way the remote
193 191 # traceback information is made available in error reports.
194 192
195 193 remote_tb = getattr(e, '_vcs_server_traceback', None)
196 194 org_remote_tb = getattr(e, '_vcs_server_org_exc_tb', '')
197 195 __traceback_info__ = None
198 196 if remote_tb:
199 197 if isinstance(remote_tb, str):
200 198 remote_tb = [remote_tb]
201 199 __traceback_info__ = (
202 200 'Found VCSServer remote traceback information:\n'
203 201 '{}\n'
204 202 '+++ BEG SOURCE EXCEPTION +++\n\n'
205 203 '{}\n'
206 204 '+++ END SOURCE EXCEPTION +++\n'
207 205 ''.format('\n'.join(remote_tb), org_remote_tb)
208 206 )
209 207
210 208 # Avoid that remote_tb also appears in the frame
211 209 del remote_tb
212 210
213 211 # Special vcs errors had an attribute "_vcs_kind" which is used
214 212 # to translate them to the proper exception class in the vcs
215 213 # client layer.
216 214 kind = getattr(e, '_vcs_kind', None)
217 215 exc_name = getattr(e, '_vcs_server_org_exc_name', None)
218 216
219 217 if kind:
220 218 if any(e.args):
221 219 _args = [a for a in e.args]
222 220 # replace the first argument with a prefix exc name
223 221 args = ['{}:{}'.format(exc_name, _args[0] if _args else '?')] + _args[1:]
224 222 else:
225 args = [__traceback_info__ or '{}: UnhandledException'.format(exc_name)]
223 args = [__traceback_info__ or f'{exc_name}: UnhandledException']
226 224 if debug or __traceback_info__ and kind not in ['unhandled', 'lookup']:
227 225 # for other than unhandled errors also log the traceback
228 226 # can be useful for debugging
229 227 log.error(__traceback_info__)
230 228
231 229 raise _EXCEPTION_MAP[kind](*args)
232 230 else:
233 231 raise
234 232 return wrapper
@@ -1,253 +1,251 b''
1
2
3 1 # Copyright (C) 2016-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 """
22 20 This serves as a drop in replacement for pycurl. It implements the pycurl Curl
23 21 class in a way that is compatible with gevent.
24 22 """
25 23
26 24
27 25 import logging
28 26 import gevent
29 27 import pycurl
30 28 import greenlet
31 29
32 30 # Import everything from pycurl.
33 31 # This allows us to use this module as a drop in replacement of pycurl.
34 32 from pycurl import * # pragma: no cover
35 33
36 34 from gevent import core
37 35 from gevent.hub import Waiter
38 36
39 37
40 38 log = logging.getLogger(__name__)
41 39
42 40
43 41 class GeventCurlMulti(object):
44 42 """
45 43 Wrapper around pycurl.CurlMulti that integrates it into gevent's event
46 44 loop.
47 45
48 46 Parts of this class are a modified version of code copied from the Tornado
49 47 Web Server project which is licensed under the Apache License, Version 2.0
50 48 (the "License"). To be more specific the code originates from this file:
51 49 https://github.com/tornadoweb/tornado/blob/stable/tornado/curl_httpclient.py
52 50
53 51 This is the original license header of the origin:
54 52
55 53 Copyright 2009 Facebook
56 54
57 55 Licensed under the Apache License, Version 2.0 (the "License"); you may
58 56 not use this file except in compliance with the License. You may obtain
59 57 a copy of the License at
60 58
61 59 http://www.apache.org/licenses/LICENSE-2.0
62 60
63 61 Unless required by applicable law or agreed to in writing, software
64 62 distributed under the License is distributed on an "AS IS" BASIS,
65 63 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
66 64 implied. See the License for the specific language governing
67 65 permissions and limitations under the License.
68 66 """
69 67
70 68 def __init__(self, loop=None):
71 69 self._watchers = {}
72 70 self._timeout = None
73 71 self.loop = loop or gevent.get_hub().loop
74 72
75 73 # Setup curl's multi instance.
76 74 self._curl_multi = pycurl.CurlMulti()
77 75 self.setopt(pycurl.M_TIMERFUNCTION, self._set_timeout)
78 76 self.setopt(pycurl.M_SOCKETFUNCTION, self._handle_socket)
79 77
80 78 def __getattr__(self, item):
81 79 """
82 80 The pycurl.CurlMulti class is final and we cannot subclass it.
83 81 Therefore we are wrapping it and forward everything to it here.
84 82 """
85 83 return getattr(self._curl_multi, item)
86 84
87 85 def add_handle(self, curl):
88 86 """
89 87 Add handle variant that also takes care about the initial invocation of
90 88 socket action method. This is done by setting an immediate timeout.
91 89 """
92 90 result = self._curl_multi.add_handle(curl)
93 91 self._set_timeout(0)
94 92 return result
95 93
96 94 def _handle_socket(self, event, fd, multi, data):
97 95 """
98 96 Called by libcurl when it wants to change the file descriptors it cares
99 97 about.
100 98 """
101 99 event_map = {
102 100 pycurl.POLL_NONE: core.NONE,
103 101 pycurl.POLL_IN: core.READ,
104 102 pycurl.POLL_OUT: core.WRITE,
105 103 pycurl.POLL_INOUT: core.READ | core.WRITE
106 104 }
107 105
108 106 if event == pycurl.POLL_REMOVE:
109 107 watcher = self._watchers.pop(fd, None)
110 108 if watcher is not None:
111 109 watcher.stop()
112 110 else:
113 111 gloop_event = event_map[event]
114 112 watcher = self._watchers.get(fd)
115 113 if watcher is None:
116 114 watcher = self.loop.io(fd, gloop_event)
117 115 watcher.start(self._handle_events, fd, pass_events=True)
118 116 self._watchers[fd] = watcher
119 117 else:
120 118 if watcher.events != gloop_event:
121 119 watcher.stop()
122 120 watcher.events = gloop_event
123 121 watcher.start(self._handle_events, fd, pass_events=True)
124 122
125 123 def _set_timeout(self, msecs):
126 124 """
127 125 Called by libcurl to schedule a timeout.
128 126 """
129 127 if self._timeout is not None:
130 128 self._timeout.stop()
131 129 self._timeout = self.loop.timer(msecs/1000.0)
132 130 self._timeout.start(self._handle_timeout)
133 131
134 132 def _handle_events(self, events, fd):
135 133 action = 0
136 134 if events & core.READ:
137 135 action |= pycurl.CSELECT_IN
138 136 if events & core.WRITE:
139 137 action |= pycurl.CSELECT_OUT
140 138 while True:
141 139 try:
142 140 ret, num_handles = self._curl_multi.socket_action(fd, action)
143 141 except pycurl.error as e:
144 142 ret = e.args[0]
145 143 if ret != pycurl.E_CALL_MULTI_PERFORM:
146 144 break
147 145 self._finish_pending_requests()
148 146
149 147 def _handle_timeout(self):
150 148 """
151 149 Called by IOLoop when the requested timeout has passed.
152 150 """
153 151 if self._timeout is not None:
154 152 self._timeout.stop()
155 153 self._timeout = None
156 154 while True:
157 155 try:
158 156 ret, num_handles = self._curl_multi.socket_action(
159 157 pycurl.SOCKET_TIMEOUT, 0)
160 158 except pycurl.error as e:
161 159 ret = e.args[0]
162 160 if ret != pycurl.E_CALL_MULTI_PERFORM:
163 161 break
164 162 self._finish_pending_requests()
165 163
166 164 # In theory, we shouldn't have to do this because curl will call
167 165 # _set_timeout whenever the timeout changes. However, sometimes after
168 166 # _handle_timeout we will need to reschedule immediately even though
169 167 # nothing has changed from curl's perspective. This is because when
170 168 # socket_action is called with SOCKET_TIMEOUT, libcurl decides
171 169 # internally which timeouts need to be processed by using a monotonic
172 170 # clock (where available) while tornado uses python's time.time() to
173 171 # decide when timeouts have occurred. When those clocks disagree on
174 172 # elapsed time (as they will whenever there is an NTP adjustment),
175 173 # tornado might call _handle_timeout before libcurl is ready. After
176 174 # each timeout, resync the scheduled timeout with libcurl's current
177 175 # state.
178 176 new_timeout = self._curl_multi.timeout()
179 177 if new_timeout >= 0:
180 178 self._set_timeout(new_timeout)
181 179
182 180 def _finish_pending_requests(self):
183 181 """
184 182 Process any requests that were completed by the last call to
185 183 multi.socket_action.
186 184 """
187 185 while True:
188 186 num_q, ok_list, err_list = self._curl_multi.info_read()
189 187 for curl in ok_list:
190 188 curl.waiter.switch(None)
191 189 for curl, errnum, errmsg in err_list:
192 190 curl.waiter.throw(Exception('%s %s' % (errnum, errmsg)))
193 191 if num_q == 0:
194 192 break
195 193
196 194
197 195 class GeventCurl(object):
198 196 """
199 197 Gevent compatible implementation of the pycurl.Curl class. Essentially a
200 198 wrapper around pycurl.Curl with a customized perform method. It uses the
201 199 GeventCurlMulti class to implement a blocking API to libcurl's "easy"
202 200 interface.
203 201 """
204 202
205 203 # Reference to the GeventCurlMulti instance.
206 204 _multi_instance = None
207 205
208 206 def __init__(self):
209 207 self._curl = pycurl.Curl()
210 208
211 209 def __getattr__(self, item):
212 210 """
213 211 The pycurl.Curl class is final and we cannot subclass it. Therefore we
214 212 are wrapping it and forward everything to it here.
215 213 """
216 214 return getattr(self._curl, item)
217 215
218 216 @property
219 217 def _multi(self):
220 218 """
221 219 Lazy property that returns the GeventCurlMulti instance. The value is
222 220 cached as a class attribute. Therefore only one instance per process
223 221 exists.
224 222 """
225 223 if GeventCurl._multi_instance is None:
226 224 GeventCurl._multi_instance = GeventCurlMulti()
227 225 return GeventCurl._multi_instance
228 226
229 227 def perform(self):
230 228 """
231 229 This perform method is compatible with gevent because it uses gevent
232 230 synchronization mechanisms to wait for the request to finish.
233 231 """
234 232 if getattr(self._curl, 'waiter', None) is not None:
235 233 current = greenlet.getcurrent()
236 234 msg = 'This curl object is already used by another greenlet, {}, \n' \
237 235 'this is {}'.format(self._curl.waiter, current)
238 236 raise Exception(msg)
239 237
240 238 waiter = self._curl.waiter = Waiter()
241 239 try:
242 240 self._multi.add_handle(self._curl)
243 241 try:
244 242 return waiter.get()
245 243 finally:
246 244 self._multi.remove_handle(self._curl)
247 245 finally:
248 246 del self._curl.waiter
249 247
250 248
251 249 # Curl is originally imported from pycurl. At this point we override it with
252 250 # our custom implementation.
253 251 Curl = GeventCurl
@@ -1,963 +1,962 b''
1 1
2 2
3 3 # Copyright (C) 2014-2023 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Module holding everything related to vcs nodes, with vcs2 architecture.
23 23 """
24 24 import functools
25 25 import os
26 26 import stat
27 27
28 28 from zope.cachedescriptors.property import Lazy as LazyProperty
29 29
30 30 from rhodecode.config.conf import LANGUAGES_EXTENSIONS_MAP
31 31 from rhodecode.lib.str_utils import safe_str, safe_bytes
32 32 from rhodecode.lib.hash_utils import md5
33 33 from rhodecode.lib.vcs import path as vcspath
34 34 from rhodecode.lib.vcs.backends.base import EmptyCommit, FILEMODE_DEFAULT
35 35 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
36 36 from rhodecode.lib.vcs.exceptions import NodeError, RemovedFileNodeError
37 37
38 38 LARGEFILE_PREFIX = '.hglf'
39 39
40 40
41 41 class NodeKind:
42 42 SUBMODULE = -1
43 43 DIR = 1
44 44 FILE = 2
45 45 LARGEFILE = 3
46 46
47 47
48 48 class NodeState:
49 49 ADDED = 'added'
50 50 CHANGED = 'changed'
51 51 NOT_CHANGED = 'not changed'
52 52 REMOVED = 'removed'
53 53
54 54 #TODO: not sure if that should be bytes or str ?
55 55 # most probably bytes because content should be bytes and we check it
56 56 BIN_BYTE_MARKER = b'\0'
57 57
58 58
59 59 class NodeGeneratorBase(object):
60 60 """
61 61 Base class for removed added and changed filenodes, it's a lazy generator
62 62 class that will create filenodes only on iteration or call
63 63
64 64 The len method doesn't need to create filenodes at all
65 65 """
66 66
67 67 def __init__(self, current_paths, cs):
68 68 self.cs = cs
69 69 self.current_paths = current_paths
70 70
71 71 def __call__(self):
72 72 return [n for n in self]
73 73
74 74 def __getitem__(self, key):
75 75 if isinstance(key, slice):
76 76 for p in self.current_paths[key.start:key.stop]:
77 77 yield self.cs.get_node(p)
78 78
79 79 def __len__(self):
80 80 return len(self.current_paths)
81 81
82 82 def __iter__(self):
83 83 for p in self.current_paths:
84 84 yield self.cs.get_node(p)
85 85
86 86
87 87 class AddedFileNodesGenerator(NodeGeneratorBase):
88 88 """
89 89 Class holding added files for current commit
90 90 """
91 91
92 92
93 93 class ChangedFileNodesGenerator(NodeGeneratorBase):
94 94 """
95 95 Class holding changed files for current commit
96 96 """
97 97
98 98
99 99 class RemovedFileNodesGenerator(NodeGeneratorBase):
100 100 """
101 101 Class holding removed files for current commit
102 102 """
103 103 def __iter__(self):
104 104 for p in self.current_paths:
105 105 yield RemovedFileNode(path=safe_bytes(p))
106 106
107 107 def __getitem__(self, key):
108 108 if isinstance(key, slice):
109 109 for p in self.current_paths[key.start:key.stop]:
110 110 yield RemovedFileNode(path=safe_bytes(p))
111 111
112 112
113 113 @functools.total_ordering
114 114 class Node(object):
115 115 """
116 116 Simplest class representing file or directory on repository. SCM backends
117 117 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
118 118 directly.
119 119
120 120 Node's ``path`` cannot start with slash as we operate on *relative* paths
121 121 only. Moreover, every single node is identified by the ``path`` attribute,
122 122 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
123 123 """
124 124 # RTLO marker allows swapping text, and certain
125 125 # security attacks could be used with this
126 126 RTLO_MARKER = "\u202E"
127 127
128 128 commit = None
129 129
130 130 def __init__(self, path: bytes, kind):
131 131 self._validate_path(path) # can throw exception if path is invalid
132 132
133 133 self.bytes_path = path.rstrip(b'/') # store for __repr__
134 134 self.path = safe_str(self.bytes_path) # we store paths as str
135 135
136 136 if self.bytes_path == b'' and kind != NodeKind.DIR:
137 137 raise NodeError("Only DirNode and its subclasses may be "
138 138 "initialized with empty path")
139 139 self.kind = kind
140 140
141 141 if self.is_root() and not self.is_dir():
142 142 raise NodeError("Root node cannot be FILE kind")
143 143
144 144 def __eq__(self, other):
145 145 if type(self) is not type(other):
146 146 return False
147 147 for attr in ['name', 'path', 'kind']:
148 148 if getattr(self, attr) != getattr(other, attr):
149 149 return False
150 150 if self.is_file():
151 151 # FileNode compare, we need to fallback to content compare
152 152 return None
153 153 else:
154 154 # For DirNode's check without entering each dir
155 155 self_nodes_paths = list(sorted(n.path for n in self.nodes))
156 156 other_nodes_paths = list(sorted(n.path for n in self.nodes))
157 157 if self_nodes_paths != other_nodes_paths:
158 158 return False
159 159 return True
160 160
161 161 def __lt__(self, other):
162 162 if self.kind < other.kind:
163 163 return True
164 164 if self.kind > other.kind:
165 165 return False
166 166 if self.path < other.path:
167 167 return True
168 168 if self.path > other.path:
169 169 return False
170 170
171 171 # def __cmp__(self, other):
172 172 # """
173 173 # Comparator using name of the node, needed for quick list sorting.
174 174 # """
175 175 #
176 176 # kind_cmp = cmp(self.kind, other.kind)
177 177 # if kind_cmp:
178 178 # if isinstance(self, SubModuleNode):
179 179 # # we make submodules equal to dirnode for "sorting" purposes
180 180 # return NodeKind.DIR
181 181 # return kind_cmp
182 182 # return cmp(self.name, other.name)
183 183
184 184 def __repr__(self):
185 185 maybe_path = getattr(self, 'path', 'UNKNOWN_PATH')
186 186 return f'<{self.__class__.__name__} {maybe_path!r}>'
187 187
188 188 def __str__(self):
189 189 return self.name
190 190
191 191 def _validate_path(self, path: bytes):
192 192 self._assert_bytes(path)
193 193
194 194 if path.startswith(b'/'):
195 195 raise NodeError(
196 196 f"Cannot initialize Node objects with slash at "
197 197 f"the beginning as only relative paths are supported. "
198 198 f"Got {path}")
199 199
200 200 def _assert_bytes(self, value):
201 201 if not isinstance(value, bytes):
202 202 raise TypeError(f"Bytes required as input, got {type(value)} of {value}.")
203 203
204 204 @LazyProperty
205 205 def parent(self):
206 206 parent_path = self.get_parent_path()
207 207 if parent_path:
208 208 if self.commit:
209 209 return self.commit.get_node(parent_path)
210 210 return DirNode(parent_path)
211 211 return None
212 212
213 213 @LazyProperty
214 214 def str_path(self) -> str:
215 215 return safe_str(self.path)
216 216
217 217 @LazyProperty
218 218 def has_rtlo(self):
219 219 """Detects if a path has right-to-left-override marker"""
220 220 return self.RTLO_MARKER in self.str_path
221 221
222 222 @LazyProperty
223 223 def dir_path(self):
224 224 """
225 225 Returns name of the directory from full path of this vcs node. Empty
226 226 string is returned if there's no directory in the path
227 227 """
228 228 _parts = self.path.rstrip('/').rsplit('/', 1)
229 229 if len(_parts) == 2:
230 230 return _parts[0]
231 231 return ''
232 232
233 233 @LazyProperty
234 234 def name(self):
235 235 """
236 236 Returns name of the node so if its path
237 237 then only last part is returned.
238 238 """
239 239 return self.path.rstrip('/').split('/')[-1]
240 240
241 241 @property
242 242 def kind(self):
243 243 return self._kind
244 244
245 245 @kind.setter
246 246 def kind(self, kind):
247 247 if hasattr(self, '_kind'):
248 248 raise NodeError("Cannot change node's kind")
249 249 else:
250 250 self._kind = kind
251 251 # Post setter check (path's trailing slash)
252 252 if self.path.endswith('/'):
253 253 raise NodeError("Node's path cannot end with slash")
254 254
255 255 def get_parent_path(self) -> bytes:
256 256 """
257 257 Returns node's parent path or empty string if node is root.
258 258 """
259 259 if self.is_root():
260 260 return b''
261 261 str_path = vcspath.dirname(self.path.rstrip('/')) + '/'
262 262
263 263 return safe_bytes(str_path)
264 264
265 265 def is_file(self):
266 266 """
267 267 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
268 268 otherwise.
269 269 """
270 270 return self.kind == NodeKind.FILE
271 271
272 272 def is_dir(self):
273 273 """
274 274 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
275 275 otherwise.
276 276 """
277 277 return self.kind == NodeKind.DIR
278 278
279 279 def is_root(self):
280 280 """
281 281 Returns ``True`` if node is a root node and ``False`` otherwise.
282 282 """
283 283 return self.kind == NodeKind.DIR and self.path == ''
284 284
285 285 def is_submodule(self):
286 286 """
287 287 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
288 288 otherwise.
289 289 """
290 290 return self.kind == NodeKind.SUBMODULE
291 291
292 292 def is_largefile(self):
293 293 """
294 294 Returns ``True`` if node's kind is ``NodeKind.LARGEFILE``, ``False``
295 295 otherwise
296 296 """
297 297 return self.kind == NodeKind.LARGEFILE
298 298
299 299 def is_link(self):
300 300 if self.commit:
301 301 return self.commit.is_link(self.path)
302 302 return False
303 303
304 304 @LazyProperty
305 305 def added(self):
306 306 return self.state is NodeState.ADDED
307 307
308 308 @LazyProperty
309 309 def changed(self):
310 310 return self.state is NodeState.CHANGED
311 311
312 312 @LazyProperty
313 313 def not_changed(self):
314 314 return self.state is NodeState.NOT_CHANGED
315 315
316 316 @LazyProperty
317 317 def removed(self):
318 318 return self.state is NodeState.REMOVED
319 319
320 320
321 321 class FileNode(Node):
322 322 """
323 323 Class representing file nodes.
324 324
325 325 :attribute: path: path to the node, relative to repository's root
326 326 :attribute: content: if given arbitrary sets content of the file
327 327 :attribute: commit: if given, first time content is accessed, callback
328 328 :attribute: mode: stat mode for a node. Default is `FILEMODE_DEFAULT`.
329 329 """
330 330 _filter_pre_load = []
331 331
332 332 def __init__(self, path: bytes, content: bytes | None = None, commit=None, mode=None, pre_load=None):
333 333 """
334 334 Only one of ``content`` and ``commit`` may be given. Passing both
335 335 would raise ``NodeError`` exception.
336 336
337 337 :param path: relative path to the node
338 338 :param content: content may be passed to constructor
339 339 :param commit: if given, will use it to lazily fetch content
340 340 :param mode: ST_MODE (i.e. 0100644)
341 341 """
342 342 if content and commit:
343 343 raise NodeError("Cannot use both content and commit")
344 344
345 345 super().__init__(path, kind=NodeKind.FILE)
346 346
347 347 self.commit = commit
348 348 if content and not isinstance(content, bytes):
349 349 # File content is one thing that inherently must be bytes
350 350 # we support passing str too, and convert the content
351 351 content = safe_bytes(content)
352 352 self._content = content
353 353 self._mode = mode or FILEMODE_DEFAULT
354 354
355 355 self._set_bulk_properties(pre_load)
356 356
357 357 def __eq__(self, other):
358 eq = super(FileNode, self).__eq__(other)
358 eq = super().__eq__(other)
359 359 if eq is not None:
360 360 return eq
361 361 return self.content == other.content
362 362
363 363 def __hash__(self):
364 364 raw_id = getattr(self.commit, 'raw_id', '')
365 365 return hash((self.path, raw_id))
366 366
367 367 def __lt__(self, other):
368 lt = super(FileNode, self).__lt__(other)
368 lt = super().__lt__(other)
369 369 if lt is not None:
370 370 return lt
371 371 return self.content < other.content
372 372
373 373 def __repr__(self):
374 374 short_id = getattr(self.commit, 'short_id', '')
375 375 return f'<{self.__class__.__name__} path={self.path!r}, short_id={short_id}>'
376 376
377 377 def _set_bulk_properties(self, pre_load):
378 378 if not pre_load:
379 379 return
380 380 pre_load = [entry for entry in pre_load
381 381 if entry not in self._filter_pre_load]
382 382 if not pre_load:
383 383 return
384 384
385 385 remote = self.commit.get_remote()
386 386 result = remote.bulk_file_request(self.commit.raw_id, self.path, pre_load)
387 387
388 388 for attr, value in result.items():
389 389 if attr == "flags":
390 390 self.__dict__['mode'] = safe_str(value)
391 391 elif attr == "size":
392 392 self.__dict__['size'] = value
393 393 elif attr == "data":
394 394 self.__dict__['_content'] = value
395 395 elif attr == "is_binary":
396 396 self.__dict__['is_binary'] = value
397 397 elif attr == "md5":
398 398 self.__dict__['md5'] = value
399 399 else:
400 400 raise ValueError(f'Unsupported attr in bulk_property: {attr}')
401 401
402 402 @LazyProperty
403 403 def mode(self):
404 404 """
405 405 Returns lazily mode of the FileNode. If `commit` is not set, would
406 406 use value given at initialization or `FILEMODE_DEFAULT` (default).
407 407 """
408 408 if self.commit:
409 409 mode = self.commit.get_file_mode(self.path)
410 410 else:
411 411 mode = self._mode
412 412 return mode
413 413
414 414 @LazyProperty
415 415 def raw_bytes(self) -> bytes:
416 416 """
417 417 Returns lazily the raw bytes of the FileNode.
418 418 """
419 419 if self.commit:
420 420 if self._content is None:
421 421 self._content = self.commit.get_file_content(self.path)
422 422 content = self._content
423 423 else:
424 424 content = self._content
425 425 return content
426 426
427 427 def content_uncached(self):
428 428 """
429 429 Returns lazily content of the FileNode.
430 430 """
431 431 if self.commit:
432 432 content = self.commit.get_file_content(self.path)
433 433 else:
434 434 content = self._content
435 435 return content
436 436
437 437 def stream_bytes(self):
438 438 """
439 439 Returns an iterator that will stream the content of the file directly from
440 440 vcsserver without loading it to memory.
441 441 """
442 442 if self.commit:
443 443 return self.commit.get_file_content_streamed(self.path)
444 444 raise NodeError("Cannot retrieve stream_bytes without related commit attribute")
445 445
446 446 def metadata_uncached(self):
447 447 """
448 448 Returns md5, binary flag of the file node, without any cache usage.
449 449 """
450 450
451 451 content = self.content_uncached()
452 452
453 453 is_binary = bool(content and BIN_BYTE_MARKER in content)
454 454 size = 0
455 455 if content:
456 456 size = len(content)
457 457
458 458 return is_binary, md5(content), size, content
459 459
460 460 @LazyProperty
461 461 def content(self) -> bytes:
462 462 """
463 463 Returns lazily content of the FileNode.
464 464 """
465 465 content = self.raw_bytes
466 466 if content and not isinstance(content, bytes):
467 467 raise ValueError(f'Content is of type {type(content)} instead of bytes')
468 468 return content
469 469
470 470 @LazyProperty
471 471 def str_content(self) -> str:
472 472 return safe_str(self.raw_bytes)
473 473
474 474 @LazyProperty
475 475 def size(self):
476 476 if self.commit:
477 477 return self.commit.get_file_size(self.path)
478 478 raise NodeError(
479 479 "Cannot retrieve size of the file without related "
480 480 "commit attribute")
481 481
482 482 @LazyProperty
483 483 def message(self):
484 484 if self.commit:
485 485 return self.last_commit.message
486 486 raise NodeError(
487 487 "Cannot retrieve message of the file without related "
488 488 "commit attribute")
489 489
490 490 @LazyProperty
491 491 def last_commit(self):
492 492 if self.commit:
493 493 pre_load = ["author", "date", "message", "parents"]
494 494 return self.commit.get_path_commit(self.path, pre_load=pre_load)
495 495 raise NodeError(
496 496 "Cannot retrieve last commit of the file without "
497 497 "related commit attribute")
498 498
499 499 def get_mimetype(self):
500 500 """
501 501 Mimetype is calculated based on the file's content. If ``_mimetype``
502 502 attribute is available, it will be returned (backends which store
503 503 mimetypes or can easily recognize them, should set this private
504 504 attribute to indicate that type should *NOT* be calculated).
505 505 """
506 506
507 507 if hasattr(self, '_mimetype'):
508 508 if (isinstance(self._mimetype, (tuple, list)) and
509 509 len(self._mimetype) == 2):
510 510 return self._mimetype
511 511 else:
512 512 raise NodeError('given _mimetype attribute must be an 2 '
513 513 'element list or tuple')
514 514
515 515 db = get_mimetypes_db()
516 516 mtype, encoding = db.guess_type(self.name)
517 517
518 518 if mtype is None:
519 519 if not self.is_largefile() and self.is_binary:
520 520 mtype = 'application/octet-stream'
521 521 encoding = None
522 522 else:
523 523 mtype = 'text/plain'
524 524 encoding = None
525 525
526 526 # try with pygments
527 527 try:
528 528 from pygments.lexers import get_lexer_for_filename
529 529 mt = get_lexer_for_filename(self.name).mimetypes
530 530 except Exception:
531 531 mt = None
532 532
533 533 if mt:
534 534 mtype = mt[0]
535 535
536 536 return mtype, encoding
537 537
538 538 @LazyProperty
539 539 def mimetype(self):
540 540 """
541 541 Wrapper around full mimetype info. It returns only type of fetched
542 542 mimetype without the encoding part. use get_mimetype function to fetch
543 543 full set of (type,encoding)
544 544 """
545 545 return self.get_mimetype()[0]
546 546
547 547 @LazyProperty
548 548 def mimetype_main(self):
549 549 return self.mimetype.split('/')[0]
550 550
551 551 @classmethod
552 552 def get_lexer(cls, filename, content=None):
553 553 from pygments import lexers
554 554
555 555 extension = filename.split('.')[-1]
556 556 lexer = None
557 557
558 558 try:
559 559 lexer = lexers.guess_lexer_for_filename(
560 560 filename, content, stripnl=False)
561 561 except lexers.ClassNotFound:
562 562 pass
563 563
564 564 # try our EXTENSION_MAP
565 565 if not lexer:
566 566 try:
567 567 lexer_class = LANGUAGES_EXTENSIONS_MAP.get(extension)
568 568 if lexer_class:
569 569 lexer = lexers.get_lexer_by_name(lexer_class[0])
570 570 except lexers.ClassNotFound:
571 571 pass
572 572
573 573 if not lexer:
574 574 lexer = lexers.TextLexer(stripnl=False)
575 575
576 576 return lexer
577 577
578 578 @LazyProperty
579 579 def lexer(self):
580 580 """
581 581 Returns pygment's lexer class. Would try to guess lexer taking file's
582 582 content, name and mimetype.
583 583 """
584 584 # TODO: this is more proper, but super heavy on investigating the type based on the content
585 585 #self.get_lexer(self.name, self.content)
586 586
587 587 return self.get_lexer(self.name)
588 588
589 589 @LazyProperty
590 590 def lexer_alias(self):
591 591 """
592 592 Returns first alias of the lexer guessed for this file.
593 593 """
594 594 return self.lexer.aliases[0]
595 595
596 596 @LazyProperty
597 597 def history(self):
598 598 """
599 599 Returns a list of commit for this file in which the file was changed
600 600 """
601 601 if self.commit is None:
602 602 raise NodeError('Unable to get commit for this FileNode')
603 603 return self.commit.get_path_history(self.path)
604 604
605 605 @LazyProperty
606 606 def annotate(self):
607 607 """
608 608 Returns a list of three element tuples with lineno, commit and line
609 609 """
610 610 if self.commit is None:
611 611 raise NodeError('Unable to get commit for this FileNode')
612 612 pre_load = ["author", "date", "message", "parents"]
613 613 return self.commit.get_file_annotate(self.path, pre_load=pre_load)
614 614
615 615 @LazyProperty
616 616 def state(self):
617 617 if not self.commit:
618 618 raise NodeError(
619 619 "Cannot check state of the node if it's not "
620 620 "linked with commit")
621 621 elif self.path in (node.path for node in self.commit.added):
622 622 return NodeState.ADDED
623 623 elif self.path in (node.path for node in self.commit.changed):
624 624 return NodeState.CHANGED
625 625 else:
626 626 return NodeState.NOT_CHANGED
627 627
628 628 @LazyProperty
629 629 def is_binary(self):
630 630 """
631 631 Returns True if file has binary content.
632 632 """
633 633 if self.commit:
634 634 return self.commit.is_node_binary(self.path)
635 635 else:
636 636 raw_bytes = self._content
637 637 return bool(raw_bytes and BIN_BYTE_MARKER in raw_bytes)
638 638
639 639 @LazyProperty
640 640 def md5(self):
641 641 """
642 642 Returns md5 of the file node.
643 643 """
644 644
645 645 if self.commit:
646 646 return self.commit.node_md5_hash(self.path)
647 647 else:
648 648 raw_bytes = self._content
649 649 # TODO: this sucks, we're computing md5 on potentially super big stream data...
650 650 return md5(raw_bytes)
651 651
652 652 @LazyProperty
653 653 def extension(self):
654 654 """Returns filenode extension"""
655 655 return self.name.split('.')[-1]
656 656
657 657 @property
658 658 def is_executable(self):
659 659 """
660 660 Returns ``True`` if file has executable flag turned on.
661 661 """
662 662 return bool(self.mode & stat.S_IXUSR)
663 663
664 664 def get_largefile_node(self):
665 665 """
666 666 Try to return a Mercurial FileNode from this node. It does internal
667 667 checks inside largefile store, if that file exist there it will
668 668 create special instance of LargeFileNode which can get content from
669 669 LF store.
670 670 """
671 671 if self.commit:
672 672 return self.commit.get_largefile_node(self.path)
673 673
674 674 def count_lines(self, content: str | bytes, count_empty=False):
675 675 if isinstance(content, str):
676 676 newline_marker = '\n'
677 677 elif isinstance(content, bytes):
678 678 newline_marker = b'\n'
679 679 else:
680 680 raise ValueError('content must be bytes or str got {type(content)} instead')
681 681
682 682 if count_empty:
683 683 all_lines = 0
684 684 empty_lines = 0
685 685 for line in content.splitlines(True):
686 686 if line == newline_marker:
687 687 empty_lines += 1
688 688 all_lines += 1
689 689
690 690 return all_lines, all_lines - empty_lines
691 691 else:
692 692 # fast method
693 693 empty_lines = all_lines = content.count(newline_marker)
694 694 if all_lines == 0 and content:
695 695 # one-line without a newline
696 696 empty_lines = all_lines = 1
697 697
698 698 return all_lines, empty_lines
699 699
700 700 def lines(self, count_empty=False):
701 701 all_lines, empty_lines = 0, 0
702 702
703 703 if not self.is_binary:
704 704 content = self.content
705 705 all_lines, empty_lines = self.count_lines(content, count_empty=count_empty)
706 706 return all_lines, empty_lines
707 707
708 708
709 709 class RemovedFileNode(FileNode):
710 710 """
711 711 Dummy FileNode class - trying to access any public attribute except path,
712 712 name, kind or state (or methods/attributes checking those two) would raise
713 713 RemovedFileNodeError.
714 714 """
715 715 ALLOWED_ATTRIBUTES = [
716 716 'name', 'path', 'state', 'is_root', 'is_file', 'is_dir', 'kind',
717 717 'added', 'changed', 'not_changed', 'removed', 'bytes_path'
718 718 ]
719 719
720 720 def __init__(self, path):
721 721 """
722 722 :param path: relative path to the node
723 723 """
724 724 super().__init__(path=path)
725 725
726 726 def __getattribute__(self, attr):
727 727 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
728 728 return super().__getattribute__(attr)
729 729 raise RemovedFileNodeError(f"Cannot access attribute {attr} on RemovedFileNode. Not in allowed attributes")
730 730
731 731 @LazyProperty
732 732 def state(self):
733 733 return NodeState.REMOVED
734 734
735 735
736 736 class DirNode(Node):
737 737 """
738 738 DirNode stores list of files and directories within this node.
739 739 Nodes may be used standalone but within repository context they
740 740 lazily fetch data within same repository's commit.
741 741 """
742 742
743 743 def __init__(self, path, nodes=(), commit=None, default_pre_load=None):
744 744 """
745 745 Only one of ``nodes`` and ``commit`` may be given. Passing both
746 746 would raise ``NodeError`` exception.
747 747
748 748 :param path: relative path to the node
749 749 :param nodes: content may be passed to constructor
750 750 :param commit: if given, will use it to lazily fetch content
751 751 """
752 752 if nodes and commit:
753 753 raise NodeError("Cannot use both nodes and commit")
754 super(DirNode, self).__init__(path, NodeKind.DIR)
754 super().__init__(path, NodeKind.DIR)
755 755 self.commit = commit
756 756 self._nodes = nodes
757 757 self.default_pre_load = default_pre_load or ['is_binary', 'size']
758 758
759 759 def __iter__(self):
760 for node in self.nodes:
761 yield node
760 yield from self.nodes
762 761
763 762 def __eq__(self, other):
764 eq = super(DirNode, self).__eq__(other)
763 eq = super().__eq__(other)
765 764 if eq is not None:
766 765 return eq
767 766 # check without entering each dir
768 767 self_nodes_paths = list(sorted(n.path for n in self.nodes))
769 768 other_nodes_paths = list(sorted(n.path for n in self.nodes))
770 769 return self_nodes_paths == other_nodes_paths
771 770
772 771 def __lt__(self, other):
773 lt = super(DirNode, self).__lt__(other)
772 lt = super().__lt__(other)
774 773 if lt is not None:
775 774 return lt
776 775 # check without entering each dir
777 776 self_nodes_paths = list(sorted(n.path for n in self.nodes))
778 777 other_nodes_paths = list(sorted(n.path for n in self.nodes))
779 778 return self_nodes_paths < other_nodes_paths
780 779
781 780 @LazyProperty
782 781 def content(self):
783 782 raise NodeError(f"{self} represents a dir and has no `content` attribute")
784 783
785 784 @LazyProperty
786 785 def nodes(self):
787 786 if self.commit:
788 787 nodes = self.commit.get_nodes(self.path, pre_load=self.default_pre_load)
789 788 else:
790 789 nodes = self._nodes
791 self._nodes_dict = dict((node.path, node) for node in nodes)
790 self._nodes_dict = {node.path: node for node in nodes}
792 791 return sorted(nodes)
793 792
794 793 @LazyProperty
795 794 def files(self):
796 return sorted((node for node in self.nodes if node.is_file()))
795 return sorted(node for node in self.nodes if node.is_file())
797 796
798 797 @LazyProperty
799 798 def dirs(self):
800 return sorted((node for node in self.nodes if node.is_dir()))
799 return sorted(node for node in self.nodes if node.is_dir())
801 800
802 801 def get_node(self, path):
803 802 """
804 803 Returns node from within this particular ``DirNode``, so it is now
805 804 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
806 805 'docs'. In order to access deeper nodes one must fetch nodes between
807 806 them first - this would work::
808 807
809 808 docs = root.get_node('docs')
810 809 docs.get_node('api').get_node('index.rst')
811 810
812 811 :param: path - relative to the current node
813 812
814 813 .. note::
815 814 To access lazily (as in example above) node have to be initialized
816 815 with related commit object - without it node is out of
817 816 context and may know nothing about anything else than nearest
818 817 (located at same level) nodes.
819 818 """
820 819 try:
821 820 path = path.rstrip('/')
822 821 if path == '':
823 822 raise NodeError("Cannot retrieve node without path")
824 823 self.nodes # access nodes first in order to set _nodes_dict
825 824 paths = path.split('/')
826 825 if len(paths) == 1:
827 826 if not self.is_root():
828 827 path = '/'.join((self.path, paths[0]))
829 828 else:
830 829 path = paths[0]
831 830 return self._nodes_dict[path]
832 831 elif len(paths) > 1:
833 832 if self.commit is None:
834 833 raise NodeError("Cannot access deeper nodes without commit")
835 834 else:
836 835 path1, path2 = paths[0], '/'.join(paths[1:])
837 836 return self.get_node(path1).get_node(path2)
838 837 else:
839 838 raise KeyError
840 839 except KeyError:
841 840 raise NodeError(f"Node does not exist at {path}")
842 841
843 842 @LazyProperty
844 843 def state(self):
845 844 raise NodeError("Cannot access state of DirNode")
846 845
847 846 @LazyProperty
848 847 def size(self):
849 848 size = 0
850 849 for root, dirs, files in self.commit.walk(self.path):
851 850 for f in files:
852 851 size += f.size
853 852
854 853 return size
855 854
856 855 @LazyProperty
857 856 def last_commit(self):
858 857 if self.commit:
859 858 pre_load = ["author", "date", "message", "parents"]
860 859 return self.commit.get_path_commit(self.path, pre_load=pre_load)
861 860 raise NodeError(
862 861 "Cannot retrieve last commit of the file without "
863 862 "related commit attribute")
864 863
865 864 def __repr__(self):
866 865 short_id = getattr(self.commit, 'short_id', '')
867 866 return f'<{self.__class__.__name__} {self.path!r} @ {short_id}>'
868 867
869 868
870 869 class RootNode(DirNode):
871 870 """
872 871 DirNode being the root node of the repository.
873 872 """
874 873
875 874 def __init__(self, nodes=(), commit=None):
876 super(RootNode, self).__init__(path=b'', nodes=nodes, commit=commit)
875 super().__init__(path=b'', nodes=nodes, commit=commit)
877 876
878 877 def __repr__(self):
879 878 return f'<{self.__class__.__name__}>'
880 879
881 880
882 881 class SubModuleNode(Node):
883 882 """
884 883 represents a SubModule of Git or SubRepo of Mercurial
885 884 """
886 885 is_binary = False
887 886 size = 0
888 887
889 888 def __init__(self, name, url=None, commit=None, alias=None):
890 889 self.path = name
891 890 self.kind = NodeKind.SUBMODULE
892 891 self.alias = alias
893 892
894 893 # we have to use EmptyCommit here since this can point to svn/git/hg
895 894 # submodules we cannot get from repository
896 895 self.commit = EmptyCommit(str(commit), alias=alias)
897 896 self.url = url or self._extract_submodule_url()
898 897
899 898 def __repr__(self):
900 899 short_id = getattr(self.commit, 'short_id', '')
901 900 return f'<{self.__class__.__name__} {self.path!r} @ {short_id}>'
902 901
903 902 def _extract_submodule_url(self):
904 903 # TODO: find a way to parse gits submodule file and extract the
905 904 # linking URL
906 905 return self.path
907 906
908 907 @LazyProperty
909 908 def name(self):
910 909 """
911 910 Returns name of the node so if its path
912 911 then only last part is returned.
913 912 """
914 913 org = safe_str(self.path.rstrip('/').split('/')[-1])
915 914 return f'{org} @ {self.commit.short_id}'
916 915
917 916
918 917 class LargeFileNode(FileNode):
919 918
920 919 def __init__(self, path, url=None, commit=None, alias=None, org_path=None):
921 920 self._validate_path(path) # can throw exception if path is invalid
922 921 self.org_path = org_path # as stored in VCS as LF pointer
923 922
924 923 self.bytes_path = path.rstrip(b'/') # store for __repr__
925 924 self.path = safe_str(self.bytes_path) # we store paths as str
926 925
927 926 self.kind = NodeKind.LARGEFILE
928 927 self.alias = alias
929 928 self._content = b''
930 929
931 930 def _validate_path(self, path: bytes):
932 931 """
933 932 we override check since the LargeFileNode path is system absolute, but we check for bytes only
934 933 """
935 934 self._assert_bytes(path)
936 935
937 936 def __repr__(self):
938 937 return f'<{self.__class__.__name__} {self.org_path} -> {self.path!r}>'
939 938
940 939 @LazyProperty
941 940 def size(self):
942 941 return os.stat(self.path).st_size
943 942
944 943 @LazyProperty
945 944 def raw_bytes(self):
946 945 with open(self.path, 'rb') as f:
947 946 content = f.read()
948 947 return content
949 948
950 949 @LazyProperty
951 950 def name(self):
952 951 """
953 952 Overwrites name to be the org lf path
954 953 """
955 954 return self.org_path
956 955
957 956 def stream_bytes(self):
958 957 with open(self.path, 'rb') as stream:
959 958 while True:
960 959 data = stream.read(16 * 1024)
961 960 if not data:
962 961 break
963 962 yield data
@@ -1,34 +1,32 b''
1
2
3 1 # Copyright (C) 2014-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 """
22 20 Utility functions to handle vcs paths.
23 21 """
24 22
25 23 # Note: Instead of re-implementing things which are provided by posixpath
26 24 # we forward import them here.
27 25 from posixpath import join, dirname, relpath, split
28 26
29 27
30 28 def sanitize(path):
31 29 """
32 30 Sanitizes path into a canonical vcs path
33 31 """
34 32 return path.lstrip('/')
@@ -1,64 +1,63 b''
1
2 1 # Copyright (C) 2010-2023 RhodeCode GmbH
3 2 #
4 3 # This program is free software: you can redistribute it and/or modify
5 4 # it under the terms of the GNU Affero General Public License, version 3
6 5 # (only), as published by the Free Software Foundation.
7 6 #
8 7 # This program is distributed in the hope that it will be useful,
9 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 10 # GNU General Public License for more details.
12 11 #
13 12 # You should have received a copy of the GNU Affero General Public License
14 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 14 #
16 15 # This program is dual-licensed. If you wish to learn more about the
17 16 # RhodeCode Enterprise Edition, including its added features, Support services,
18 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 18
20 19 """
21 20 This module provides some useful tools for ``vcs`` like annotate/diff html
22 21 output. It also includes some internal helpers.
23 22 """
24 23
25 24
26 25 def author_email(author):
27 26 """
28 27 returns email address of given author.
29 28 If any of <,> sign are found, it fallbacks to regex findall()
30 29 and returns first found result or empty string
31 30
32 31 Regex taken from http://www.regular-expressions.info/email.html
33 32 """
34 33 import re
35 34 if not author:
36 35 return ''
37 36
38 37 r = author.find('>')
39 38 l = author.find('<')
40 39
41 40 if l == -1 or r == -1:
42 41 # fallback to regex match of email out of a string
43 42 email_re = re.compile(r"""[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!"""
44 43 r"""#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z"""
45 44 r"""0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]"""
46 45 r"""*[a-z0-9])?""", re.IGNORECASE)
47 46 m = re.findall(email_re, author)
48 47 return m[0] if m else ''
49 48
50 49 return author[l + 1:r].strip()
51 50
52 51
53 52 def author_name(author):
54 53 """
55 54 get name of author, or else username.
56 55 It'll try to find an email in the author string and just cut it off
57 56 to get the username
58 57 """
59 58
60 59 if not author or '@' not in author:
61 60 return author
62 61 else:
63 62 return author.replace(author_email(author), '').replace('<', '')\
64 63 .replace('>', '').strip()
@@ -1,161 +1,159 b''
1
2
3 1 # Copyright (C) 2014-2023 RhodeCode GmbH
4 2 #
5 3 # This program is free software: you can redistribute it and/or modify
6 4 # it under the terms of the GNU Affero General Public License, version 3
7 5 # (only), as published by the Free Software Foundation.
8 6 #
9 7 # This program is distributed in the hope that it will be useful,
10 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 10 # GNU General Public License for more details.
13 11 #
14 12 # You should have received a copy of the GNU Affero General Public License
15 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 14 #
17 15 # This program is dual-licensed. If you wish to learn more about the
18 16 # RhodeCode Enterprise Edition, including its added features, Support services,
19 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 18
21 19 """
22 20 Utilities aimed to help achieve mostly basic tasks.
23 21 """
24 22
25 23
26 24
27 25
28 26 import re
29 27 import os
30 28 import time
31 29 import datetime
32 30 import logging
33 31
34 32 from rhodecode.lib.vcs.conf import settings
35 33 from rhodecode.lib.vcs.exceptions import VCSError, VCSBackendNotSupportedError
36 34
37 35
38 36 log = logging.getLogger(__name__)
39 37
40 38
41 39 def get_scm(path):
42 40 """
43 41 Returns one of alias from ``ALIASES`` (in order of precedence same as
44 42 shortcuts given in ``ALIASES``) and working dir path for the given
45 43 argument. If no scm-specific directory is found or more than one scm is
46 44 found at that directory, ``VCSError`` is raised.
47 45 """
48 46 if not os.path.isdir(path):
49 47 raise VCSError("Given path %s is not a directory" % path)
50 48
51 49 found_scms = [(scm, path) for scm in get_scms_for_path(path)]
52 50
53 51 if len(found_scms) > 1:
54 found = ', '.join((x[0] for x in found_scms))
52 found = ', '.join(x[0] for x in found_scms)
55 53 raise VCSError(
56 'More than one [%s] scm found at given path %s' % (found, path))
54 'More than one [{}] scm found at given path {}'.format(found, path))
57 55
58 56 if len(found_scms) == 0:
59 57 raise VCSError('No scm found at given path %s' % path)
60 58
61 59 return found_scms[0]
62 60
63 61
64 62 def get_scm_backend(backend_type):
65 63 from rhodecode.lib.vcs.backends import get_backend
66 64 return get_backend(backend_type)
67 65
68 66
69 67 def get_scms_for_path(path):
70 68 """
71 69 Returns all scm's found at the given path. If no scm is recognized
72 70 - empty list is returned.
73 71
74 72 :param path: path to directory which should be checked. May be callable.
75 73
76 74 :raises VCSError: if given ``path`` is not a directory
77 75 """
78 76 from rhodecode.lib.vcs.backends import get_backend
79 77 if hasattr(path, '__call__'):
80 78 path = path()
81 79 if not os.path.isdir(path):
82 80 raise VCSError("Given path %r is not a directory" % path)
83 81
84 82 result = []
85 83 for key in settings.available_aliases():
86 84 try:
87 85 backend = get_backend(key)
88 86 except VCSBackendNotSupportedError:
89 87 log.warning('VCSBackendNotSupportedError: %s not supported', key)
90 88 continue
91 89 if backend.is_valid_repository(path):
92 90 result.append(key)
93 91 return result
94 92
95 93
96 94 def parse_datetime(text):
97 95 """
98 96 Parses given text and returns ``datetime.datetime`` instance or raises
99 97 ``ValueError``.
100 98
101 99 :param text: string of desired date/datetime or something more verbose,
102 100 like *yesterday*, *2weeks 3days*, etc.
103 101 """
104 102 if not text:
105 103 raise ValueError('Wrong date: "%s"' % text)
106 104
107 105 if isinstance(text, datetime.datetime):
108 106 return text
109 107
110 108 # we limit a format to no include microseconds e.g 2017-10-17t17:48:23.XXXX
111 109 text = text.strip().lower()[:19]
112 110
113 111 input_formats = (
114 112 '%Y-%m-%d %H:%M:%S',
115 113 '%Y-%m-%dt%H:%M:%S',
116 114 '%Y-%m-%d %H:%M',
117 115 '%Y-%m-%dt%H:%M',
118 116 '%Y-%m-%d',
119 117 '%m/%d/%Y %H:%M:%S',
120 118 '%m/%d/%Yt%H:%M:%S',
121 119 '%m/%d/%Y %H:%M',
122 120 '%m/%d/%Yt%H:%M',
123 121 '%m/%d/%Y',
124 122 '%m/%d/%y %H:%M:%S',
125 123 '%m/%d/%yt%H:%M:%S',
126 124 '%m/%d/%y %H:%M',
127 125 '%m/%d/%yt%H:%M',
128 126 '%m/%d/%y',
129 127 )
130 128 for format_def in input_formats:
131 129 try:
132 130 return datetime.datetime(*time.strptime(text, format_def)[:6])
133 131 except ValueError:
134 132 pass
135 133
136 134 # Try descriptive texts
137 135 if text == 'tomorrow':
138 136 future = datetime.datetime.now() + datetime.timedelta(days=1)
139 137 args = future.timetuple()[:3] + (23, 59, 59)
140 138 return datetime.datetime(*args)
141 139 elif text == 'today':
142 140 return datetime.datetime(*datetime.datetime.today().timetuple()[:3])
143 141 elif text == 'now':
144 142 return datetime.datetime.now()
145 143 elif text == 'yesterday':
146 144 past = datetime.datetime.now() - datetime.timedelta(days=1)
147 145 return datetime.datetime(*past.timetuple()[:3])
148 146 else:
149 147 days = 0
150 148 matched = re.match(
151 149 r'^((?P<weeks>\d+) ?w(eeks?)?)? ?((?P<days>\d+) ?d(ays?)?)?$', text)
152 150 if matched:
153 151 groupdict = matched.groupdict()
154 152 if groupdict['days']:
155 153 days += int(matched.groupdict()['days'])
156 154 if groupdict['weeks']:
157 155 days += int(matched.groupdict()['weeks']) * 7
158 156 past = datetime.datetime.now() - datetime.timedelta(days=days)
159 157 return datetime.datetime(*past.timetuple()[:3])
160 158
161 159 raise ValueError('Wrong date: "%s"' % text)
@@ -1,46 +1,45 b''
1
2 1 # Copyright (C) 2010-2023 RhodeCode GmbH
3 2 #
4 3 # This program is free software: you can redistribute it and/or modify
5 4 # it under the terms of the GNU Affero General Public License, version 3
6 5 # (only), as published by the Free Software Foundation.
7 6 #
8 7 # This program is distributed in the hope that it will be useful,
9 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 10 # GNU General Public License for more details.
12 11 #
13 12 # You should have received a copy of the GNU Affero General Public License
14 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 14 #
16 15 # This program is dual-licensed. If you wish to learn more about the
17 16 # RhodeCode Enterprise Edition, including its added features, Support services,
18 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 18
20 19 from rhodecode.lib.vcs.exceptions import VCSError
21 20
22 21
23 22 def import_class(class_path):
24 23 """
25 24 Returns class from the given path.
26 25
27 26 For example, in order to get class located at
28 27 ``vcs.backends.hg.MercurialRepository``:
29 28
30 29 try:
31 30 hgrepo = import_class('vcs.backends.hg.MercurialRepository')
32 31 except VCSError:
33 32 # hadle error
34 33 """
35 34 splitted = class_path.split('.')
36 35 mod_path = '.'.join(splitted[:-1])
37 36 class_name = splitted[-1]
38 37 try:
39 38 class_mod = __import__(mod_path, {}, {}, [class_name])
40 39 except ImportError as err:
41 40 msg = "There was problem while trying to import backend class. "\
42 41 "Original error was:\n%s" % err
43 42 raise VCSError(msg)
44 43 cls = getattr(class_mod, class_name)
45 44
46 45 return cls
@@ -1,33 +1,32 b''
1
2 1 # Copyright (C) 2010-2023 RhodeCode GmbH
3 2 #
4 3 # This program is free software: you can redistribute it and/or modify
5 4 # it under the terms of the GNU Affero General Public License, version 3
6 5 # (only), as published by the Free Software Foundation.
7 6 #
8 7 # This program is distributed in the hope that it will be useful,
9 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 10 # GNU General Public License for more details.
12 11 #
13 12 # You should have received a copy of the GNU Affero General Public License
14 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 14 #
16 15 # This program is dual-licensed. If you wish to learn more about the
17 16 # RhodeCode Enterprise Edition, including its added features, Support services,
18 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 18
20 19 import os.path
21 20
22 21
23 22 def get_dirs_for_path(*paths):
24 23 """Return list of directories, including intermediate."""
25 24 for path in paths:
26 25 head = path
27 26 while head:
28 27 head, tail = os.path.split(head)
29 28 if head:
30 29 yield head
31 30 else:
32 31 # We don't need to yield empty path
33 32 break
General Comments 0
You need to be logged in to leave comments. Login now