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