##// END OF EJS Templates
url-checkers: raise UrlError on acutally failed calls to the path, before we leaked org requests exception out
super-admin -
r1160:04a63439 default
parent child Browse files
Show More
@@ -1,1462 +1,1462 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-2023 RhodeCode GmbH
2 # Copyright (C) 2014-2023 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 re
21 import re
22 import stat
22 import stat
23 import traceback
23 import traceback
24 import urllib.request
24 import urllib.request
25 import urllib.parse
25 import urllib.parse
26 import urllib.error
26 import urllib.error
27 from functools import wraps
27 from functools import wraps
28
28
29 import more_itertools
29 import more_itertools
30 import pygit2
30 import pygit2
31 from pygit2 import Repository as LibGit2Repo
31 from pygit2 import Repository as LibGit2Repo
32 from pygit2 import index as LibGit2Index
32 from pygit2 import index as LibGit2Index
33 from dulwich import index, objects
33 from dulwich import index, objects
34 from dulwich.client import HttpGitClient, LocalGitClient, FetchPackResult
34 from dulwich.client import HttpGitClient, LocalGitClient, FetchPackResult
35 from dulwich.errors import (
35 from dulwich.errors import (
36 NotGitRepository, ChecksumMismatch, WrongObjectException,
36 NotGitRepository, ChecksumMismatch, WrongObjectException,
37 MissingCommitError, ObjectMissing, HangupException,
37 MissingCommitError, ObjectMissing, HangupException,
38 UnexpectedCommandError)
38 UnexpectedCommandError)
39 from dulwich.repo import Repo as DulwichRepo
39 from dulwich.repo import Repo as DulwichRepo
40 from dulwich.server import update_server_info
40 from dulwich.server import update_server_info
41
41
42 from vcsserver import exceptions, settings, subprocessio
42 from vcsserver import exceptions, settings, subprocessio
43 from vcsserver.str_utils import safe_str, safe_int, safe_bytes, ascii_bytes
43 from vcsserver.str_utils import safe_str, safe_int, safe_bytes, ascii_bytes
44 from vcsserver.base import RepoFactory, obfuscate_qs, ArchiveNode, store_archive_in_cache, BytesEnvelope, BinaryEnvelope
44 from vcsserver.base import RepoFactory, obfuscate_qs, ArchiveNode, store_archive_in_cache, BytesEnvelope, BinaryEnvelope
45 from vcsserver.hgcompat import (
45 from vcsserver.hgcompat import (
46 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler)
46 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler)
47 from vcsserver.git_lfs.lib import LFSOidStore
47 from vcsserver.git_lfs.lib import LFSOidStore
48 from vcsserver.vcs_base import RemoteBase
48 from vcsserver.vcs_base import RemoteBase
49
49
50 DIR_STAT = stat.S_IFDIR
50 DIR_STAT = stat.S_IFDIR
51 FILE_MODE = stat.S_IFMT
51 FILE_MODE = stat.S_IFMT
52 GIT_LINK = objects.S_IFGITLINK
52 GIT_LINK = objects.S_IFGITLINK
53 PEELED_REF_MARKER = b'^{}'
53 PEELED_REF_MARKER = b'^{}'
54 HEAD_MARKER = b'HEAD'
54 HEAD_MARKER = b'HEAD'
55
55
56 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
57
57
58
58
59 def reraise_safe_exceptions(func):
59 def reraise_safe_exceptions(func):
60 """Converts Dulwich exceptions to something neutral."""
60 """Converts Dulwich exceptions to something neutral."""
61
61
62 @wraps(func)
62 @wraps(func)
63 def wrapper(*args, **kwargs):
63 def wrapper(*args, **kwargs):
64 try:
64 try:
65 return func(*args, **kwargs)
65 return func(*args, **kwargs)
66 except (ChecksumMismatch, WrongObjectException, MissingCommitError, ObjectMissing,) as e:
66 except (ChecksumMismatch, WrongObjectException, MissingCommitError, ObjectMissing,) as e:
67 exc = exceptions.LookupException(org_exc=e)
67 exc = exceptions.LookupException(org_exc=e)
68 raise exc(safe_str(e))
68 raise exc(safe_str(e))
69 except (HangupException, UnexpectedCommandError) as e:
69 except (HangupException, UnexpectedCommandError) as e:
70 exc = exceptions.VcsException(org_exc=e)
70 exc = exceptions.VcsException(org_exc=e)
71 raise exc(safe_str(e))
71 raise exc(safe_str(e))
72 except Exception:
72 except Exception:
73 # NOTE(marcink): because of how dulwich handles some exceptions
73 # NOTE(marcink): because of how dulwich handles some exceptions
74 # (KeyError on empty repos), we cannot track this and catch all
74 # (KeyError on empty repos), we cannot track this and catch all
75 # exceptions, it's an exceptions from other handlers
75 # exceptions, it's an exceptions from other handlers
76 #if not hasattr(e, '_vcs_kind'):
76 #if not hasattr(e, '_vcs_kind'):
77 #log.exception("Unhandled exception in git remote call")
77 #log.exception("Unhandled exception in git remote call")
78 #raise_from_original(exceptions.UnhandledException)
78 #raise_from_original(exceptions.UnhandledException)
79 raise
79 raise
80 return wrapper
80 return wrapper
81
81
82
82
83 class Repo(DulwichRepo):
83 class Repo(DulwichRepo):
84 """
84 """
85 A wrapper for dulwich Repo class.
85 A wrapper for dulwich Repo class.
86
86
87 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
87 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
88 "Too many open files" error. We need to close all opened file descriptors
88 "Too many open files" error. We need to close all opened file descriptors
89 once the repo object is destroyed.
89 once the repo object is destroyed.
90 """
90 """
91 def __del__(self):
91 def __del__(self):
92 if hasattr(self, 'object_store'):
92 if hasattr(self, 'object_store'):
93 self.close()
93 self.close()
94
94
95
95
96 class Repository(LibGit2Repo):
96 class Repository(LibGit2Repo):
97
97
98 def __enter__(self):
98 def __enter__(self):
99 return self
99 return self
100
100
101 def __exit__(self, exc_type, exc_val, exc_tb):
101 def __exit__(self, exc_type, exc_val, exc_tb):
102 self.free()
102 self.free()
103
103
104
104
105 class GitFactory(RepoFactory):
105 class GitFactory(RepoFactory):
106 repo_type = 'git'
106 repo_type = 'git'
107
107
108 def _create_repo(self, wire, create, use_libgit2=False):
108 def _create_repo(self, wire, create, use_libgit2=False):
109 if use_libgit2:
109 if use_libgit2:
110 repo = Repository(safe_bytes(wire['path']))
110 repo = Repository(safe_bytes(wire['path']))
111 else:
111 else:
112 # dulwich mode
112 # dulwich mode
113 repo_path = safe_str(wire['path'], to_encoding=settings.WIRE_ENCODING)
113 repo_path = safe_str(wire['path'], to_encoding=settings.WIRE_ENCODING)
114 repo = Repo(repo_path)
114 repo = Repo(repo_path)
115
115
116 log.debug('repository created: got GIT object: %s', repo)
116 log.debug('repository created: got GIT object: %s', repo)
117 return repo
117 return repo
118
118
119 def repo(self, wire, create=False, use_libgit2=False):
119 def repo(self, wire, create=False, use_libgit2=False):
120 """
120 """
121 Get a repository instance for the given path.
121 Get a repository instance for the given path.
122 """
122 """
123 return self._create_repo(wire, create, use_libgit2)
123 return self._create_repo(wire, create, use_libgit2)
124
124
125 def repo_libgit2(self, wire):
125 def repo_libgit2(self, wire):
126 return self.repo(wire, use_libgit2=True)
126 return self.repo(wire, use_libgit2=True)
127
127
128
128
129 def create_signature_from_string(author_str, **kwargs):
129 def create_signature_from_string(author_str, **kwargs):
130 """
130 """
131 Creates a pygit2.Signature object from a string of the format 'Name <email>'.
131 Creates a pygit2.Signature object from a string of the format 'Name <email>'.
132
132
133 :param author_str: String of the format 'Name <email>'
133 :param author_str: String of the format 'Name <email>'
134 :return: pygit2.Signature object
134 :return: pygit2.Signature object
135 """
135 """
136 match = re.match(r'^(.+) <(.+)>$', author_str)
136 match = re.match(r'^(.+) <(.+)>$', author_str)
137 if match is None:
137 if match is None:
138 raise ValueError(f"Invalid format: {author_str}")
138 raise ValueError(f"Invalid format: {author_str}")
139
139
140 name, email = match.groups()
140 name, email = match.groups()
141 return pygit2.Signature(name, email, **kwargs)
141 return pygit2.Signature(name, email, **kwargs)
142
142
143
143
144 def get_obfuscated_url(url_obj):
144 def get_obfuscated_url(url_obj):
145 url_obj.passwd = b'*****' if url_obj.passwd else url_obj.passwd
145 url_obj.passwd = b'*****' if url_obj.passwd else url_obj.passwd
146 url_obj.query = obfuscate_qs(url_obj.query)
146 url_obj.query = obfuscate_qs(url_obj.query)
147 obfuscated_uri = str(url_obj)
147 obfuscated_uri = str(url_obj)
148 return obfuscated_uri
148 return obfuscated_uri
149
149
150
150
151 class GitRemote(RemoteBase):
151 class GitRemote(RemoteBase):
152
152
153 def __init__(self, factory):
153 def __init__(self, factory):
154 self._factory = factory
154 self._factory = factory
155 self._bulk_methods = {
155 self._bulk_methods = {
156 "date": self.date,
156 "date": self.date,
157 "author": self.author,
157 "author": self.author,
158 "branch": self.branch,
158 "branch": self.branch,
159 "message": self.message,
159 "message": self.message,
160 "parents": self.parents,
160 "parents": self.parents,
161 "_commit": self.revision,
161 "_commit": self.revision,
162 }
162 }
163 self._bulk_file_methods = {
163 self._bulk_file_methods = {
164 "size": self.get_node_size,
164 "size": self.get_node_size,
165 "data": self.get_node_data,
165 "data": self.get_node_data,
166 "flags": self.get_node_flags,
166 "flags": self.get_node_flags,
167 "is_binary": self.get_node_is_binary,
167 "is_binary": self.get_node_is_binary,
168 "md5": self.md5_hash
168 "md5": self.md5_hash
169 }
169 }
170
170
171 def _wire_to_config(self, wire):
171 def _wire_to_config(self, wire):
172 if 'config' in wire:
172 if 'config' in wire:
173 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']}
174 return {}
174 return {}
175
175
176 def _remote_conf(self, config):
176 def _remote_conf(self, config):
177 params = [
177 params = [
178 '-c', 'core.askpass=""',
178 '-c', 'core.askpass=""',
179 ]
179 ]
180 ssl_cert_dir = config.get('vcs_ssl_dir')
180 ssl_cert_dir = config.get('vcs_ssl_dir')
181 if ssl_cert_dir:
181 if ssl_cert_dir:
182 params.extend(['-c', f'http.sslCAinfo={ssl_cert_dir}'])
182 params.extend(['-c', f'http.sslCAinfo={ssl_cert_dir}'])
183 return params
183 return params
184
184
185 @reraise_safe_exceptions
185 @reraise_safe_exceptions
186 def discover_git_version(self):
186 def discover_git_version(self):
187 stdout, _ = self.run_git_command(
187 stdout, _ = self.run_git_command(
188 {}, ['--version'], _bare=True, _safe=True)
188 {}, ['--version'], _bare=True, _safe=True)
189 prefix = b'git version'
189 prefix = b'git version'
190 if stdout.startswith(prefix):
190 if stdout.startswith(prefix):
191 stdout = stdout[len(prefix):]
191 stdout = stdout[len(prefix):]
192 return safe_str(stdout.strip())
192 return safe_str(stdout.strip())
193
193
194 @reraise_safe_exceptions
194 @reraise_safe_exceptions
195 def is_empty(self, wire):
195 def is_empty(self, wire):
196 repo_init = self._factory.repo_libgit2(wire)
196 repo_init = self._factory.repo_libgit2(wire)
197 with repo_init as repo:
197 with repo_init as repo:
198
198
199 try:
199 try:
200 has_head = repo.head.name
200 has_head = repo.head.name
201 if has_head:
201 if has_head:
202 return False
202 return False
203
203
204 # NOTE(marcink): check again using more expensive method
204 # NOTE(marcink): check again using more expensive method
205 return repo.is_empty
205 return repo.is_empty
206 except Exception:
206 except Exception:
207 pass
207 pass
208
208
209 return True
209 return True
210
210
211 @reraise_safe_exceptions
211 @reraise_safe_exceptions
212 def assert_correct_path(self, wire):
212 def assert_correct_path(self, wire):
213 cache_on, context_uid, repo_id = self._cache_on(wire)
213 cache_on, context_uid, repo_id = self._cache_on(wire)
214 region = self._region(wire)
214 region = self._region(wire)
215
215
216 @region.conditional_cache_on_arguments(condition=cache_on)
216 @region.conditional_cache_on_arguments(condition=cache_on)
217 def _assert_correct_path(_context_uid, _repo_id, fast_check):
217 def _assert_correct_path(_context_uid, _repo_id, fast_check):
218 if fast_check:
218 if fast_check:
219 path = safe_str(wire['path'])
219 path = safe_str(wire['path'])
220 if pygit2.discover_repository(path):
220 if pygit2.discover_repository(path):
221 return True
221 return True
222 return False
222 return False
223 else:
223 else:
224 try:
224 try:
225 repo_init = self._factory.repo_libgit2(wire)
225 repo_init = self._factory.repo_libgit2(wire)
226 with repo_init:
226 with repo_init:
227 pass
227 pass
228 except pygit2.GitError:
228 except pygit2.GitError:
229 path = wire.get('path')
229 path = wire.get('path')
230 tb = traceback.format_exc()
230 tb = traceback.format_exc()
231 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
231 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
232 return False
232 return False
233 return True
233 return True
234
234
235 return _assert_correct_path(context_uid, repo_id, True)
235 return _assert_correct_path(context_uid, repo_id, True)
236
236
237 @reraise_safe_exceptions
237 @reraise_safe_exceptions
238 def bare(self, wire):
238 def bare(self, wire):
239 repo_init = self._factory.repo_libgit2(wire)
239 repo_init = self._factory.repo_libgit2(wire)
240 with repo_init as repo:
240 with repo_init as repo:
241 return repo.is_bare
241 return repo.is_bare
242
242
243 @reraise_safe_exceptions
243 @reraise_safe_exceptions
244 def get_node_data(self, wire, commit_id, path):
244 def get_node_data(self, wire, commit_id, path):
245 repo_init = self._factory.repo_libgit2(wire)
245 repo_init = self._factory.repo_libgit2(wire)
246 with repo_init as repo:
246 with repo_init as repo:
247 commit = repo[commit_id]
247 commit = repo[commit_id]
248 blob_obj = commit.tree[path]
248 blob_obj = commit.tree[path]
249
249
250 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
250 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
251 raise exceptions.LookupException()(
251 raise exceptions.LookupException()(
252 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
252 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
253
253
254 return BytesEnvelope(blob_obj.data)
254 return BytesEnvelope(blob_obj.data)
255
255
256 @reraise_safe_exceptions
256 @reraise_safe_exceptions
257 def get_node_size(self, wire, commit_id, path):
257 def get_node_size(self, wire, commit_id, path):
258 repo_init = self._factory.repo_libgit2(wire)
258 repo_init = self._factory.repo_libgit2(wire)
259 with repo_init as repo:
259 with repo_init as repo:
260 commit = repo[commit_id]
260 commit = repo[commit_id]
261 blob_obj = commit.tree[path]
261 blob_obj = commit.tree[path]
262
262
263 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
263 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
264 raise exceptions.LookupException()(
264 raise exceptions.LookupException()(
265 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
265 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
266
266
267 return blob_obj.size
267 return blob_obj.size
268
268
269 @reraise_safe_exceptions
269 @reraise_safe_exceptions
270 def get_node_flags(self, wire, commit_id, path):
270 def get_node_flags(self, wire, commit_id, path):
271 repo_init = self._factory.repo_libgit2(wire)
271 repo_init = self._factory.repo_libgit2(wire)
272 with repo_init as repo:
272 with repo_init as repo:
273 commit = repo[commit_id]
273 commit = repo[commit_id]
274 blob_obj = commit.tree[path]
274 blob_obj = commit.tree[path]
275
275
276 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
276 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
277 raise exceptions.LookupException()(
277 raise exceptions.LookupException()(
278 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
278 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
279
279
280 return blob_obj.filemode
280 return blob_obj.filemode
281
281
282 @reraise_safe_exceptions
282 @reraise_safe_exceptions
283 def get_node_is_binary(self, wire, commit_id, path):
283 def get_node_is_binary(self, wire, commit_id, path):
284 repo_init = self._factory.repo_libgit2(wire)
284 repo_init = self._factory.repo_libgit2(wire)
285 with repo_init as repo:
285 with repo_init as repo:
286 commit = repo[commit_id]
286 commit = repo[commit_id]
287 blob_obj = commit.tree[path]
287 blob_obj = commit.tree[path]
288
288
289 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
289 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
290 raise exceptions.LookupException()(
290 raise exceptions.LookupException()(
291 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
291 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
292
292
293 return blob_obj.is_binary
293 return blob_obj.is_binary
294
294
295 @reraise_safe_exceptions
295 @reraise_safe_exceptions
296 def blob_as_pretty_string(self, wire, sha):
296 def blob_as_pretty_string(self, wire, sha):
297 repo_init = self._factory.repo_libgit2(wire)
297 repo_init = self._factory.repo_libgit2(wire)
298 with repo_init as repo:
298 with repo_init as repo:
299 blob_obj = repo[sha]
299 blob_obj = repo[sha]
300 return BytesEnvelope(blob_obj.data)
300 return BytesEnvelope(blob_obj.data)
301
301
302 @reraise_safe_exceptions
302 @reraise_safe_exceptions
303 def blob_raw_length(self, wire, sha):
303 def blob_raw_length(self, wire, sha):
304 cache_on, context_uid, repo_id = self._cache_on(wire)
304 cache_on, context_uid, repo_id = self._cache_on(wire)
305 region = self._region(wire)
305 region = self._region(wire)
306
306
307 @region.conditional_cache_on_arguments(condition=cache_on)
307 @region.conditional_cache_on_arguments(condition=cache_on)
308 def _blob_raw_length(_repo_id, _sha):
308 def _blob_raw_length(_repo_id, _sha):
309
309
310 repo_init = self._factory.repo_libgit2(wire)
310 repo_init = self._factory.repo_libgit2(wire)
311 with repo_init as repo:
311 with repo_init as repo:
312 blob = repo[sha]
312 blob = repo[sha]
313 return blob.size
313 return blob.size
314
314
315 return _blob_raw_length(repo_id, sha)
315 return _blob_raw_length(repo_id, sha)
316
316
317 def _parse_lfs_pointer(self, raw_content):
317 def _parse_lfs_pointer(self, raw_content):
318 spec_string = b'version https://git-lfs.github.com/spec'
318 spec_string = b'version https://git-lfs.github.com/spec'
319 if raw_content and raw_content.startswith(spec_string):
319 if raw_content and raw_content.startswith(spec_string):
320
320
321 pattern = re.compile(rb"""
321 pattern = re.compile(rb"""
322 (?:\n)?
322 (?:\n)?
323 ^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
324 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
324 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
325 ^size[ ](?P<oid_size>[0-9]+)\n
325 ^size[ ](?P<oid_size>[0-9]+)\n
326 (?:\n)?
326 (?:\n)?
327 """, re.VERBOSE | re.MULTILINE)
327 """, re.VERBOSE | re.MULTILINE)
328 match = pattern.match(raw_content)
328 match = pattern.match(raw_content)
329 if match:
329 if match:
330 return match.groupdict()
330 return match.groupdict()
331
331
332 return {}
332 return {}
333
333
334 @reraise_safe_exceptions
334 @reraise_safe_exceptions
335 def is_large_file(self, wire, commit_id):
335 def is_large_file(self, wire, commit_id):
336 cache_on, context_uid, repo_id = self._cache_on(wire)
336 cache_on, context_uid, repo_id = self._cache_on(wire)
337 region = self._region(wire)
337 region = self._region(wire)
338
338
339 @region.conditional_cache_on_arguments(condition=cache_on)
339 @region.conditional_cache_on_arguments(condition=cache_on)
340 def _is_large_file(_repo_id, _sha):
340 def _is_large_file(_repo_id, _sha):
341 repo_init = self._factory.repo_libgit2(wire)
341 repo_init = self._factory.repo_libgit2(wire)
342 with repo_init as repo:
342 with repo_init as repo:
343 blob = repo[commit_id]
343 blob = repo[commit_id]
344 if blob.is_binary:
344 if blob.is_binary:
345 return {}
345 return {}
346
346
347 return self._parse_lfs_pointer(blob.data)
347 return self._parse_lfs_pointer(blob.data)
348
348
349 return _is_large_file(repo_id, commit_id)
349 return _is_large_file(repo_id, commit_id)
350
350
351 @reraise_safe_exceptions
351 @reraise_safe_exceptions
352 def is_binary(self, wire, tree_id):
352 def is_binary(self, wire, tree_id):
353 cache_on, context_uid, repo_id = self._cache_on(wire)
353 cache_on, context_uid, repo_id = self._cache_on(wire)
354 region = self._region(wire)
354 region = self._region(wire)
355
355
356 @region.conditional_cache_on_arguments(condition=cache_on)
356 @region.conditional_cache_on_arguments(condition=cache_on)
357 def _is_binary(_repo_id, _tree_id):
357 def _is_binary(_repo_id, _tree_id):
358 repo_init = self._factory.repo_libgit2(wire)
358 repo_init = self._factory.repo_libgit2(wire)
359 with repo_init as repo:
359 with repo_init as repo:
360 blob_obj = repo[tree_id]
360 blob_obj = repo[tree_id]
361 return blob_obj.is_binary
361 return blob_obj.is_binary
362
362
363 return _is_binary(repo_id, tree_id)
363 return _is_binary(repo_id, tree_id)
364
364
365 @reraise_safe_exceptions
365 @reraise_safe_exceptions
366 def md5_hash(self, wire, commit_id, path):
366 def md5_hash(self, wire, commit_id, path):
367 cache_on, context_uid, repo_id = self._cache_on(wire)
367 cache_on, context_uid, repo_id = self._cache_on(wire)
368 region = self._region(wire)
368 region = self._region(wire)
369
369
370 @region.conditional_cache_on_arguments(condition=cache_on)
370 @region.conditional_cache_on_arguments(condition=cache_on)
371 def _md5_hash(_repo_id, _commit_id, _path):
371 def _md5_hash(_repo_id, _commit_id, _path):
372 repo_init = self._factory.repo_libgit2(wire)
372 repo_init = self._factory.repo_libgit2(wire)
373 with repo_init as repo:
373 with repo_init as repo:
374 commit = repo[_commit_id]
374 commit = repo[_commit_id]
375 blob_obj = commit.tree[_path]
375 blob_obj = commit.tree[_path]
376
376
377 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
377 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
378 raise exceptions.LookupException()(
378 raise exceptions.LookupException()(
379 f'Tree for commit_id:{_commit_id} is not a blob: {blob_obj.type_str}')
379 f'Tree for commit_id:{_commit_id} is not a blob: {blob_obj.type_str}')
380
380
381 return ''
381 return ''
382
382
383 return _md5_hash(repo_id, commit_id, path)
383 return _md5_hash(repo_id, commit_id, path)
384
384
385 @reraise_safe_exceptions
385 @reraise_safe_exceptions
386 def in_largefiles_store(self, wire, oid):
386 def in_largefiles_store(self, wire, oid):
387 conf = self._wire_to_config(wire)
387 conf = self._wire_to_config(wire)
388 repo_init = self._factory.repo_libgit2(wire)
388 repo_init = self._factory.repo_libgit2(wire)
389 with repo_init as repo:
389 with repo_init as repo:
390 repo_name = repo.path
390 repo_name = repo.path
391
391
392 store_location = conf.get('vcs_git_lfs_store_location')
392 store_location = conf.get('vcs_git_lfs_store_location')
393 if store_location:
393 if store_location:
394
394
395 store = LFSOidStore(
395 store = LFSOidStore(
396 oid=oid, repo=repo_name, store_location=store_location)
396 oid=oid, repo=repo_name, store_location=store_location)
397 return store.has_oid()
397 return store.has_oid()
398
398
399 return False
399 return False
400
400
401 @reraise_safe_exceptions
401 @reraise_safe_exceptions
402 def store_path(self, wire, oid):
402 def store_path(self, wire, oid):
403 conf = self._wire_to_config(wire)
403 conf = self._wire_to_config(wire)
404 repo_init = self._factory.repo_libgit2(wire)
404 repo_init = self._factory.repo_libgit2(wire)
405 with repo_init as repo:
405 with repo_init as repo:
406 repo_name = repo.path
406 repo_name = repo.path
407
407
408 store_location = conf.get('vcs_git_lfs_store_location')
408 store_location = conf.get('vcs_git_lfs_store_location')
409 if store_location:
409 if store_location:
410 store = LFSOidStore(
410 store = LFSOidStore(
411 oid=oid, repo=repo_name, store_location=store_location)
411 oid=oid, repo=repo_name, store_location=store_location)
412 return store.oid_path
412 return store.oid_path
413 raise ValueError(f'Unable to fetch oid with path {oid}')
413 raise ValueError(f'Unable to fetch oid with path {oid}')
414
414
415 @reraise_safe_exceptions
415 @reraise_safe_exceptions
416 def bulk_request(self, wire, rev, pre_load):
416 def bulk_request(self, wire, rev, pre_load):
417 cache_on, context_uid, repo_id = self._cache_on(wire)
417 cache_on, context_uid, repo_id = self._cache_on(wire)
418 region = self._region(wire)
418 region = self._region(wire)
419
419
420 @region.conditional_cache_on_arguments(condition=cache_on)
420 @region.conditional_cache_on_arguments(condition=cache_on)
421 def _bulk_request(_repo_id, _rev, _pre_load):
421 def _bulk_request(_repo_id, _rev, _pre_load):
422 result = {}
422 result = {}
423 for attr in pre_load:
423 for attr in pre_load:
424 try:
424 try:
425 method = self._bulk_methods[attr]
425 method = self._bulk_methods[attr]
426 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
427 args = [wire, rev]
427 args = [wire, rev]
428 result[attr] = method(*args)
428 result[attr] = method(*args)
429 except KeyError as e:
429 except KeyError as e:
430 raise exceptions.VcsException(e)(f"Unknown bulk attribute: {attr}")
430 raise exceptions.VcsException(e)(f"Unknown bulk attribute: {attr}")
431 return result
431 return result
432
432
433 return _bulk_request(repo_id, rev, sorted(pre_load))
433 return _bulk_request(repo_id, rev, sorted(pre_load))
434
434
435 @reraise_safe_exceptions
435 @reraise_safe_exceptions
436 def bulk_file_request(self, wire, commit_id, path, pre_load):
436 def bulk_file_request(self, wire, commit_id, path, pre_load):
437 cache_on, context_uid, repo_id = self._cache_on(wire)
437 cache_on, context_uid, repo_id = self._cache_on(wire)
438 region = self._region(wire)
438 region = self._region(wire)
439
439
440 @region.conditional_cache_on_arguments(condition=cache_on)
440 @region.conditional_cache_on_arguments(condition=cache_on)
441 def _bulk_file_request(_repo_id, _commit_id, _path, _pre_load):
441 def _bulk_file_request(_repo_id, _commit_id, _path, _pre_load):
442 result = {}
442 result = {}
443 for attr in pre_load:
443 for attr in pre_load:
444 try:
444 try:
445 method = self._bulk_file_methods[attr]
445 method = self._bulk_file_methods[attr]
446 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
446 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
447 result[attr] = method(wire, _commit_id, _path)
447 result[attr] = method(wire, _commit_id, _path)
448 except KeyError as e:
448 except KeyError as e:
449 raise exceptions.VcsException(e)(f'Unknown bulk attribute: "{attr}"')
449 raise exceptions.VcsException(e)(f'Unknown bulk attribute: "{attr}"')
450 return result
450 return result
451
451
452 return BinaryEnvelope(_bulk_file_request(repo_id, commit_id, path, sorted(pre_load)))
452 return BinaryEnvelope(_bulk_file_request(repo_id, commit_id, path, sorted(pre_load)))
453
453
454 def _build_opener(self, url: str):
454 def _build_opener(self, url: str):
455 handlers = []
455 handlers = []
456 url_obj = url_parser(safe_bytes(url))
456 url_obj = url_parser(safe_bytes(url))
457 authinfo = url_obj.authinfo()[1]
457 authinfo = url_obj.authinfo()[1]
458
458
459 if authinfo:
459 if authinfo:
460 # create a password manager
460 # create a password manager
461 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
461 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
462 passmgr.add_password(*authinfo)
462 passmgr.add_password(*authinfo)
463
463
464 handlers.extend((httpbasicauthhandler(passmgr),
464 handlers.extend((httpbasicauthhandler(passmgr),
465 httpdigestauthhandler(passmgr)))
465 httpdigestauthhandler(passmgr)))
466
466
467 return urllib.request.build_opener(*handlers)
467 return urllib.request.build_opener(*handlers)
468
468
469 @reraise_safe_exceptions
469 @reraise_safe_exceptions
470 def check_url(self, url, config):
470 def check_url(self, url, config):
471 url_obj = url_parser(safe_bytes(url))
471 url_obj = url_parser(safe_bytes(url))
472
472
473 test_uri = safe_str(url_obj.authinfo()[0])
473 test_uri = safe_str(url_obj.authinfo()[0])
474 obfuscated_uri = get_obfuscated_url(url_obj)
474 obfuscated_uri = get_obfuscated_url(url_obj)
475
475
476 log.info("Checking URL for remote cloning/import: %s", obfuscated_uri)
476 log.info("Checking URL for remote cloning/import: %s", obfuscated_uri)
477
477
478 if not test_uri.endswith('info/refs'):
478 if not test_uri.endswith('info/refs'):
479 test_uri = test_uri.rstrip('/') + '/info/refs'
479 test_uri = test_uri.rstrip('/') + '/info/refs'
480
480
481 o = self._build_opener(test_uri)
481 o = self._build_opener(test_uri)
482 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
483
483
484 q = {"service": 'git-upload-pack'}
484 q = {"service": 'git-upload-pack'}
485 qs = f'?{urllib.parse.urlencode(q)}'
485 qs = f'?{urllib.parse.urlencode(q)}'
486 cu = f"{test_uri}{qs}"
486 cu = f"{test_uri}{qs}"
487 req = urllib.request.Request(cu, None, {})
488
487
489 try:
488 try:
489 req = urllib.request.Request(cu, None, {})
490 log.debug("Trying to open URL %s", obfuscated_uri)
490 log.debug("Trying to open URL %s", obfuscated_uri)
491 resp = o.open(req)
491 resp = o.open(req)
492 if resp.code != 200:
492 if resp.code != 200:
493 raise exceptions.URLError()('Return Code is not 200')
493 raise exceptions.URLError()('Return Code is not 200')
494 except Exception as e:
494 except Exception as e:
495 log.warning("URL cannot be opened: %s", obfuscated_uri, exc_info=True)
495 log.warning("URL cannot be opened: %s", obfuscated_uri, exc_info=True)
496 # means it cannot be cloned
496 # means it cannot be cloned
497 raise exceptions.URLError(e)(f"[{obfuscated_uri}] org_exc: {e}")
497 raise exceptions.URLError(e)(f"[{obfuscated_uri}] org_exc: {e}")
498
498
499 # now detect if it's proper git repo
499 # now detect if it's proper git repo
500 gitdata: bytes = resp.read()
500 gitdata: bytes = resp.read()
501
501
502 if b'service=git-upload-pack' in gitdata:
502 if b'service=git-upload-pack' in gitdata:
503 pass
503 pass
504 elif re.findall(br'[0-9a-fA-F]{40}\s+refs', gitdata):
504 elif re.findall(br'[0-9a-fA-F]{40}\s+refs', gitdata):
505 # old style git can return some other format !
505 # old style git can return some other format!
506 pass
506 pass
507 else:
507 else:
508 e = None
508 e = None
509 raise exceptions.URLError(e)(
509 raise exceptions.URLError(e)(
510 f"url [{obfuscated_uri}] does not look like an hg repo org_exc: {e}")
510 f"url [{obfuscated_uri}] does not look like an hg repo org_exc: {e}")
511
511
512 return True
512 return True
513
513
514 @reraise_safe_exceptions
514 @reraise_safe_exceptions
515 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
515 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
516 # TODO(marcink): deprecate this method. Last i checked we don't use it anymore
516 # TODO(marcink): deprecate this method. Last i checked we don't use it anymore
517 remote_refs = self.pull(wire, url, apply_refs=False)
517 remote_refs = self.pull(wire, url, apply_refs=False)
518 repo = self._factory.repo(wire)
518 repo = self._factory.repo(wire)
519 if isinstance(valid_refs, list):
519 if isinstance(valid_refs, list):
520 valid_refs = tuple(valid_refs)
520 valid_refs = tuple(valid_refs)
521
521
522 for k in remote_refs:
522 for k in remote_refs:
523 # only parse heads/tags and skip so called deferred tags
523 # only parse heads/tags and skip so called deferred tags
524 if k.startswith(valid_refs) and not k.endswith(deferred):
524 if k.startswith(valid_refs) and not k.endswith(deferred):
525 repo[k] = remote_refs[k]
525 repo[k] = remote_refs[k]
526
526
527 if update_after_clone:
527 if update_after_clone:
528 # we want to checkout HEAD
528 # we want to checkout HEAD
529 repo["HEAD"] = remote_refs["HEAD"]
529 repo["HEAD"] = remote_refs["HEAD"]
530 index.build_index_from_tree(repo.path, repo.index_path(),
530 index.build_index_from_tree(repo.path, repo.index_path(),
531 repo.object_store, repo["HEAD"].tree)
531 repo.object_store, repo["HEAD"].tree)
532
532
533 @reraise_safe_exceptions
533 @reraise_safe_exceptions
534 def branch(self, wire, commit_id):
534 def branch(self, wire, commit_id):
535 cache_on, context_uid, repo_id = self._cache_on(wire)
535 cache_on, context_uid, repo_id = self._cache_on(wire)
536 region = self._region(wire)
536 region = self._region(wire)
537
537
538 @region.conditional_cache_on_arguments(condition=cache_on)
538 @region.conditional_cache_on_arguments(condition=cache_on)
539 def _branch(_context_uid, _repo_id, _commit_id):
539 def _branch(_context_uid, _repo_id, _commit_id):
540 regex = re.compile('^refs/heads')
540 regex = re.compile('^refs/heads')
541
541
542 def filter_with(ref):
542 def filter_with(ref):
543 return regex.match(ref[0]) and ref[1] == _commit_id
543 return regex.match(ref[0]) and ref[1] == _commit_id
544
544
545 branches = list(filter(filter_with, list(self.get_refs(wire).items())))
545 branches = list(filter(filter_with, list(self.get_refs(wire).items())))
546 return [x[0].split('refs/heads/')[-1] for x in branches]
546 return [x[0].split('refs/heads/')[-1] for x in branches]
547
547
548 return _branch(context_uid, repo_id, commit_id)
548 return _branch(context_uid, repo_id, commit_id)
549
549
550 @reraise_safe_exceptions
550 @reraise_safe_exceptions
551 def commit_branches(self, wire, commit_id):
551 def commit_branches(self, wire, commit_id):
552 cache_on, context_uid, repo_id = self._cache_on(wire)
552 cache_on, context_uid, repo_id = self._cache_on(wire)
553 region = self._region(wire)
553 region = self._region(wire)
554
554
555 @region.conditional_cache_on_arguments(condition=cache_on)
555 @region.conditional_cache_on_arguments(condition=cache_on)
556 def _commit_branches(_context_uid, _repo_id, _commit_id):
556 def _commit_branches(_context_uid, _repo_id, _commit_id):
557 repo_init = self._factory.repo_libgit2(wire)
557 repo_init = self._factory.repo_libgit2(wire)
558 with repo_init as repo:
558 with repo_init as repo:
559 branches = [x for x in repo.branches.with_commit(_commit_id)]
559 branches = [x for x in repo.branches.with_commit(_commit_id)]
560 return branches
560 return branches
561
561
562 return _commit_branches(context_uid, repo_id, commit_id)
562 return _commit_branches(context_uid, repo_id, commit_id)
563
563
564 @reraise_safe_exceptions
564 @reraise_safe_exceptions
565 def add_object(self, wire, content):
565 def add_object(self, wire, content):
566 repo_init = self._factory.repo_libgit2(wire)
566 repo_init = self._factory.repo_libgit2(wire)
567 with repo_init as repo:
567 with repo_init as repo:
568 blob = objects.Blob()
568 blob = objects.Blob()
569 blob.set_raw_string(content)
569 blob.set_raw_string(content)
570 repo.object_store.add_object(blob)
570 repo.object_store.add_object(blob)
571 return blob.id
571 return blob.id
572
572
573 @reraise_safe_exceptions
573 @reraise_safe_exceptions
574 def create_commit(self, wire, author, committer, message, branch, new_tree_id, date_args: list[int, int] = None):
574 def create_commit(self, wire, author, committer, message, branch, new_tree_id, date_args: list[int, int] = None):
575 repo_init = self._factory.repo_libgit2(wire)
575 repo_init = self._factory.repo_libgit2(wire)
576 with repo_init as repo:
576 with repo_init as repo:
577
577
578 if date_args:
578 if date_args:
579 current_time, offset = date_args
579 current_time, offset = date_args
580
580
581 kw = {
581 kw = {
582 'time': current_time,
582 'time': current_time,
583 'offset': offset
583 'offset': offset
584 }
584 }
585 author = create_signature_from_string(author, **kw)
585 author = create_signature_from_string(author, **kw)
586 committer = create_signature_from_string(committer, **kw)
586 committer = create_signature_from_string(committer, **kw)
587
587
588 tree = new_tree_id
588 tree = new_tree_id
589 if isinstance(tree, (bytes, str)):
589 if isinstance(tree, (bytes, str)):
590 # validate this tree is in the repo...
590 # validate this tree is in the repo...
591 tree = repo[safe_str(tree)].id
591 tree = repo[safe_str(tree)].id
592
592
593 parents = []
593 parents = []
594 # ensure we COMMIT on top of given branch head
594 # ensure we COMMIT on top of given branch head
595 # check if this repo has ANY branches, otherwise it's a new branch case we need to make
595 # check if this repo has ANY branches, otherwise it's a new branch case we need to make
596 if branch in repo.branches.local:
596 if branch in repo.branches.local:
597 parents += [repo.branches[branch].target]
597 parents += [repo.branches[branch].target]
598 elif [x for x in repo.branches.local]:
598 elif [x for x in repo.branches.local]:
599 parents += [repo.head.target]
599 parents += [repo.head.target]
600 #else:
600 #else:
601 # in case we want to commit on new branch we create it on top of HEAD
601 # in case we want to commit on new branch we create it on top of HEAD
602 #repo.branches.local.create(branch, repo.revparse_single('HEAD'))
602 #repo.branches.local.create(branch, repo.revparse_single('HEAD'))
603
603
604 # # Create a new commit
604 # # Create a new commit
605 commit_oid = repo.create_commit(
605 commit_oid = repo.create_commit(
606 f'refs/heads/{branch}', # the name of the reference to update
606 f'refs/heads/{branch}', # the name of the reference to update
607 author, # the author of the commit
607 author, # the author of the commit
608 committer, # the committer of the commit
608 committer, # the committer of the commit
609 message, # the commit message
609 message, # the commit message
610 tree, # the tree produced by the index
610 tree, # the tree produced by the index
611 parents # list of parents for the new commit, usually just one,
611 parents # list of parents for the new commit, usually just one,
612 )
612 )
613
613
614 new_commit_id = safe_str(commit_oid)
614 new_commit_id = safe_str(commit_oid)
615
615
616 return new_commit_id
616 return new_commit_id
617
617
618 @reraise_safe_exceptions
618 @reraise_safe_exceptions
619 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
619 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
620
620
621 def mode2pygit(mode):
621 def mode2pygit(mode):
622 """
622 """
623 git only supports two filemode 644 and 755
623 git only supports two filemode 644 and 755
624
624
625 0o100755 -> 33261
625 0o100755 -> 33261
626 0o100644 -> 33188
626 0o100644 -> 33188
627 """
627 """
628 return {
628 return {
629 0o100644: pygit2.GIT_FILEMODE_BLOB,
629 0o100644: pygit2.GIT_FILEMODE_BLOB,
630 0o100755: pygit2.GIT_FILEMODE_BLOB_EXECUTABLE,
630 0o100755: pygit2.GIT_FILEMODE_BLOB_EXECUTABLE,
631 0o120000: pygit2.GIT_FILEMODE_LINK
631 0o120000: pygit2.GIT_FILEMODE_LINK
632 }.get(mode) or pygit2.GIT_FILEMODE_BLOB
632 }.get(mode) or pygit2.GIT_FILEMODE_BLOB
633
633
634 repo_init = self._factory.repo_libgit2(wire)
634 repo_init = self._factory.repo_libgit2(wire)
635 with repo_init as repo:
635 with repo_init as repo:
636 repo_index = repo.index
636 repo_index = repo.index
637
637
638 for pathspec in updated:
638 for pathspec in updated:
639 blob_id = repo.create_blob(pathspec['content'])
639 blob_id = repo.create_blob(pathspec['content'])
640 ie = pygit2.IndexEntry(pathspec['path'], blob_id, mode2pygit(pathspec['mode']))
640 ie = pygit2.IndexEntry(pathspec['path'], blob_id, mode2pygit(pathspec['mode']))
641 repo_index.add(ie)
641 repo_index.add(ie)
642
642
643 for pathspec in removed:
643 for pathspec in removed:
644 repo_index.remove(pathspec)
644 repo_index.remove(pathspec)
645
645
646 # Write changes to the index
646 # Write changes to the index
647 repo_index.write()
647 repo_index.write()
648
648
649 # Create a tree from the updated index
649 # Create a tree from the updated index
650 commit_tree = repo_index.write_tree()
650 commit_tree = repo_index.write_tree()
651
651
652 new_tree_id = commit_tree
652 new_tree_id = commit_tree
653
653
654 author = commit_data['author']
654 author = commit_data['author']
655 committer = commit_data['committer']
655 committer = commit_data['committer']
656 message = commit_data['message']
656 message = commit_data['message']
657
657
658 date_args = [int(commit_data['commit_time']), int(commit_data['commit_timezone'])]
658 date_args = [int(commit_data['commit_time']), int(commit_data['commit_timezone'])]
659
659
660 new_commit_id = self.create_commit(wire, author, committer, message, branch,
660 new_commit_id = self.create_commit(wire, author, committer, message, branch,
661 new_tree_id, date_args=date_args)
661 new_tree_id, date_args=date_args)
662
662
663 # libgit2, ensure the branch is there and exists
663 # libgit2, ensure the branch is there and exists
664 self.create_branch(wire, branch, new_commit_id)
664 self.create_branch(wire, branch, new_commit_id)
665
665
666 # libgit2, set new ref to this created commit
666 # libgit2, set new ref to this created commit
667 self.set_refs(wire, f'refs/heads/{branch}', new_commit_id)
667 self.set_refs(wire, f'refs/heads/{branch}', new_commit_id)
668
668
669 return new_commit_id
669 return new_commit_id
670
670
671 @reraise_safe_exceptions
671 @reraise_safe_exceptions
672 def pull(self, wire, url, apply_refs=True, refs=None, update_after=False):
672 def pull(self, wire, url, apply_refs=True, refs=None, update_after=False):
673 if url != 'default' and '://' not in url:
673 if url != 'default' and '://' not in url:
674 client = LocalGitClient(url)
674 client = LocalGitClient(url)
675 else:
675 else:
676 url_obj = url_parser(safe_bytes(url))
676 url_obj = url_parser(safe_bytes(url))
677 o = self._build_opener(url)
677 o = self._build_opener(url)
678 url = url_obj.authinfo()[0]
678 url = url_obj.authinfo()[0]
679 client = HttpGitClient(base_url=url, opener=o)
679 client = HttpGitClient(base_url=url, opener=o)
680 repo = self._factory.repo(wire)
680 repo = self._factory.repo(wire)
681
681
682 determine_wants = repo.object_store.determine_wants_all
682 determine_wants = repo.object_store.determine_wants_all
683 if refs:
683 if refs:
684 refs = [ascii_bytes(x) for x in refs]
684 refs = [ascii_bytes(x) for x in refs]
685
685
686 def determine_wants_requested(remote_refs):
686 def determine_wants_requested(remote_refs):
687 determined = []
687 determined = []
688 for ref_name, ref_hash in remote_refs.items():
688 for ref_name, ref_hash in remote_refs.items():
689 bytes_ref_name = safe_bytes(ref_name)
689 bytes_ref_name = safe_bytes(ref_name)
690
690
691 if bytes_ref_name in refs:
691 if bytes_ref_name in refs:
692 bytes_ref_hash = safe_bytes(ref_hash)
692 bytes_ref_hash = safe_bytes(ref_hash)
693 determined.append(bytes_ref_hash)
693 determined.append(bytes_ref_hash)
694 return determined
694 return determined
695
695
696 # swap with our custom requested wants
696 # swap with our custom requested wants
697 determine_wants = determine_wants_requested
697 determine_wants = determine_wants_requested
698
698
699 try:
699 try:
700 remote_refs = client.fetch(
700 remote_refs = client.fetch(
701 path=url, target=repo, determine_wants=determine_wants)
701 path=url, target=repo, determine_wants=determine_wants)
702
702
703 except NotGitRepository as e:
703 except NotGitRepository as e:
704 log.warning(
704 log.warning(
705 'Trying to fetch from "%s" failed, not a Git repository.', url)
705 'Trying to fetch from "%s" failed, not a Git repository.', url)
706 # Exception can contain unicode which we convert
706 # Exception can contain unicode which we convert
707 raise exceptions.AbortException(e)(repr(e))
707 raise exceptions.AbortException(e)(repr(e))
708
708
709 # mikhail: client.fetch() returns all the remote refs, but fetches only
709 # mikhail: client.fetch() returns all the remote refs, but fetches only
710 # refs filtered by `determine_wants` function. We need to filter result
710 # refs filtered by `determine_wants` function. We need to filter result
711 # as well
711 # as well
712 if refs:
712 if refs:
713 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
713 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
714
714
715 if apply_refs:
715 if apply_refs:
716 # TODO: johbo: Needs proper test coverage with a git repository
716 # TODO: johbo: Needs proper test coverage with a git repository
717 # that contains a tag object, so that we would end up with
717 # that contains a tag object, so that we would end up with
718 # a peeled ref at this point.
718 # a peeled ref at this point.
719 for k in remote_refs:
719 for k in remote_refs:
720 if k.endswith(PEELED_REF_MARKER):
720 if k.endswith(PEELED_REF_MARKER):
721 log.debug("Skipping peeled reference %s", k)
721 log.debug("Skipping peeled reference %s", k)
722 continue
722 continue
723 repo[k] = remote_refs[k]
723 repo[k] = remote_refs[k]
724
724
725 if refs and not update_after:
725 if refs and not update_after:
726 # mikhail: explicitly set the head to the last ref.
726 # mikhail: explicitly set the head to the last ref.
727 repo[HEAD_MARKER] = remote_refs[refs[-1]]
727 repo[HEAD_MARKER] = remote_refs[refs[-1]]
728
728
729 if update_after:
729 if update_after:
730 # we want to check out HEAD
730 # we want to check out HEAD
731 repo[HEAD_MARKER] = remote_refs[HEAD_MARKER]
731 repo[HEAD_MARKER] = remote_refs[HEAD_MARKER]
732 index.build_index_from_tree(repo.path, repo.index_path(),
732 index.build_index_from_tree(repo.path, repo.index_path(),
733 repo.object_store, repo[HEAD_MARKER].tree)
733 repo.object_store, repo[HEAD_MARKER].tree)
734
734
735 if isinstance(remote_refs, FetchPackResult):
735 if isinstance(remote_refs, FetchPackResult):
736 return remote_refs.refs
736 return remote_refs.refs
737 return remote_refs
737 return remote_refs
738
738
739 @reraise_safe_exceptions
739 @reraise_safe_exceptions
740 def sync_fetch(self, wire, url, refs=None, all_refs=False):
740 def sync_fetch(self, wire, url, refs=None, all_refs=False):
741 self._factory.repo(wire)
741 self._factory.repo(wire)
742 if refs and not isinstance(refs, (list, tuple)):
742 if refs and not isinstance(refs, (list, tuple)):
743 refs = [refs]
743 refs = [refs]
744
744
745 config = self._wire_to_config(wire)
745 config = self._wire_to_config(wire)
746 # get all remote refs we'll use to fetch later
746 # get all remote refs we'll use to fetch later
747 cmd = ['ls-remote']
747 cmd = ['ls-remote']
748 if not all_refs:
748 if not all_refs:
749 cmd += ['--heads', '--tags']
749 cmd += ['--heads', '--tags']
750 cmd += [url]
750 cmd += [url]
751 output, __ = self.run_git_command(
751 output, __ = self.run_git_command(
752 wire, cmd, fail_on_stderr=False,
752 wire, cmd, fail_on_stderr=False,
753 _copts=self._remote_conf(config),
753 _copts=self._remote_conf(config),
754 extra_env={'GIT_TERMINAL_PROMPT': '0'})
754 extra_env={'GIT_TERMINAL_PROMPT': '0'})
755
755
756 remote_refs = collections.OrderedDict()
756 remote_refs = collections.OrderedDict()
757 fetch_refs = []
757 fetch_refs = []
758
758
759 for ref_line in output.splitlines():
759 for ref_line in output.splitlines():
760 sha, ref = ref_line.split(b'\t')
760 sha, ref = ref_line.split(b'\t')
761 sha = sha.strip()
761 sha = sha.strip()
762 if ref in remote_refs:
762 if ref in remote_refs:
763 # duplicate, skip
763 # duplicate, skip
764 continue
764 continue
765 if ref.endswith(PEELED_REF_MARKER):
765 if ref.endswith(PEELED_REF_MARKER):
766 log.debug("Skipping peeled reference %s", ref)
766 log.debug("Skipping peeled reference %s", ref)
767 continue
767 continue
768 # don't sync HEAD
768 # don't sync HEAD
769 if ref in [HEAD_MARKER]:
769 if ref in [HEAD_MARKER]:
770 continue
770 continue
771
771
772 remote_refs[ref] = sha
772 remote_refs[ref] = sha
773
773
774 if refs and sha in refs:
774 if refs and sha in refs:
775 # we filter fetch using our specified refs
775 # we filter fetch using our specified refs
776 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
776 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
777 elif not refs:
777 elif not refs:
778 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
778 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
779 log.debug('Finished obtaining fetch refs, total: %s', len(fetch_refs))
779 log.debug('Finished obtaining fetch refs, total: %s', len(fetch_refs))
780
780
781 if fetch_refs:
781 if fetch_refs:
782 for chunk in more_itertools.chunked(fetch_refs, 1024 * 4):
782 for chunk in more_itertools.chunked(fetch_refs, 1024 * 4):
783 fetch_refs_chunks = list(chunk)
783 fetch_refs_chunks = list(chunk)
784 log.debug('Fetching %s refs from import url', len(fetch_refs_chunks))
784 log.debug('Fetching %s refs from import url', len(fetch_refs_chunks))
785 self.run_git_command(
785 self.run_git_command(
786 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs_chunks,
786 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs_chunks,
787 fail_on_stderr=False,
787 fail_on_stderr=False,
788 _copts=self._remote_conf(config),
788 _copts=self._remote_conf(config),
789 extra_env={'GIT_TERMINAL_PROMPT': '0'})
789 extra_env={'GIT_TERMINAL_PROMPT': '0'})
790
790
791 return remote_refs
791 return remote_refs
792
792
793 @reraise_safe_exceptions
793 @reraise_safe_exceptions
794 def sync_push(self, wire, url, refs=None):
794 def sync_push(self, wire, url, refs=None):
795 if not self.check_url(url, wire):
795 if not self.check_url(url, wire):
796 return
796 return
797 config = self._wire_to_config(wire)
797 config = self._wire_to_config(wire)
798 self._factory.repo(wire)
798 self._factory.repo(wire)
799 self.run_git_command(
799 self.run_git_command(
800 wire, ['push', url, '--mirror'], fail_on_stderr=False,
800 wire, ['push', url, '--mirror'], fail_on_stderr=False,
801 _copts=self._remote_conf(config),
801 _copts=self._remote_conf(config),
802 extra_env={'GIT_TERMINAL_PROMPT': '0'})
802 extra_env={'GIT_TERMINAL_PROMPT': '0'})
803
803
804 @reraise_safe_exceptions
804 @reraise_safe_exceptions
805 def get_remote_refs(self, wire, url):
805 def get_remote_refs(self, wire, url):
806 repo = Repo(url)
806 repo = Repo(url)
807 return repo.get_refs()
807 return repo.get_refs()
808
808
809 @reraise_safe_exceptions
809 @reraise_safe_exceptions
810 def get_description(self, wire):
810 def get_description(self, wire):
811 repo = self._factory.repo(wire)
811 repo = self._factory.repo(wire)
812 return repo.get_description()
812 return repo.get_description()
813
813
814 @reraise_safe_exceptions
814 @reraise_safe_exceptions
815 def get_missing_revs(self, wire, rev1, rev2, path2):
815 def get_missing_revs(self, wire, rev1, rev2, path2):
816 repo = self._factory.repo(wire)
816 repo = self._factory.repo(wire)
817 LocalGitClient(thin_packs=False).fetch(path2, repo)
817 LocalGitClient(thin_packs=False).fetch(path2, repo)
818
818
819 wire_remote = wire.copy()
819 wire_remote = wire.copy()
820 wire_remote['path'] = path2
820 wire_remote['path'] = path2
821 repo_remote = self._factory.repo(wire_remote)
821 repo_remote = self._factory.repo(wire_remote)
822 LocalGitClient(thin_packs=False).fetch(path2, repo_remote)
822 LocalGitClient(thin_packs=False).fetch(path2, repo_remote)
823
823
824 revs = [
824 revs = [
825 x.commit.id
825 x.commit.id
826 for x in repo_remote.get_walker(include=[safe_bytes(rev2)], exclude=[safe_bytes(rev1)])]
826 for x in repo_remote.get_walker(include=[safe_bytes(rev2)], exclude=[safe_bytes(rev1)])]
827 return revs
827 return revs
828
828
829 @reraise_safe_exceptions
829 @reraise_safe_exceptions
830 def get_object(self, wire, sha, maybe_unreachable=False):
830 def get_object(self, wire, sha, maybe_unreachable=False):
831 cache_on, context_uid, repo_id = self._cache_on(wire)
831 cache_on, context_uid, repo_id = self._cache_on(wire)
832 region = self._region(wire)
832 region = self._region(wire)
833
833
834 @region.conditional_cache_on_arguments(condition=cache_on)
834 @region.conditional_cache_on_arguments(condition=cache_on)
835 def _get_object(_context_uid, _repo_id, _sha):
835 def _get_object(_context_uid, _repo_id, _sha):
836 repo_init = self._factory.repo_libgit2(wire)
836 repo_init = self._factory.repo_libgit2(wire)
837 with repo_init as repo:
837 with repo_init as repo:
838
838
839 missing_commit_err = 'Commit {} does not exist for `{}`'.format(sha, wire['path'])
839 missing_commit_err = 'Commit {} does not exist for `{}`'.format(sha, wire['path'])
840 try:
840 try:
841 commit = repo.revparse_single(sha)
841 commit = repo.revparse_single(sha)
842 except KeyError:
842 except KeyError:
843 # NOTE(marcink): KeyError doesn't give us any meaningful information
843 # NOTE(marcink): KeyError doesn't give us any meaningful information
844 # here, we instead give something more explicit
844 # here, we instead give something more explicit
845 e = exceptions.RefNotFoundException('SHA: %s not found', sha)
845 e = exceptions.RefNotFoundException('SHA: %s not found', sha)
846 raise exceptions.LookupException(e)(missing_commit_err)
846 raise exceptions.LookupException(e)(missing_commit_err)
847 except ValueError as e:
847 except ValueError as e:
848 raise exceptions.LookupException(e)(missing_commit_err)
848 raise exceptions.LookupException(e)(missing_commit_err)
849
849
850 is_tag = False
850 is_tag = False
851 if isinstance(commit, pygit2.Tag):
851 if isinstance(commit, pygit2.Tag):
852 commit = repo.get(commit.target)
852 commit = repo.get(commit.target)
853 is_tag = True
853 is_tag = True
854
854
855 check_dangling = True
855 check_dangling = True
856 if is_tag:
856 if is_tag:
857 check_dangling = False
857 check_dangling = False
858
858
859 if check_dangling and maybe_unreachable:
859 if check_dangling and maybe_unreachable:
860 check_dangling = False
860 check_dangling = False
861
861
862 # we used a reference and it parsed means we're not having a dangling commit
862 # we used a reference and it parsed means we're not having a dangling commit
863 if sha != commit.hex:
863 if sha != commit.hex:
864 check_dangling = False
864 check_dangling = False
865
865
866 if check_dangling:
866 if check_dangling:
867 # check for dangling commit
867 # check for dangling commit
868 for branch in repo.branches.with_commit(commit.hex):
868 for branch in repo.branches.with_commit(commit.hex):
869 if branch:
869 if branch:
870 break
870 break
871 else:
871 else:
872 # NOTE(marcink): Empty error doesn't give us any meaningful information
872 # NOTE(marcink): Empty error doesn't give us any meaningful information
873 # here, we instead give something more explicit
873 # here, we instead give something more explicit
874 e = exceptions.RefNotFoundException('SHA: %s not found in branches', sha)
874 e = exceptions.RefNotFoundException('SHA: %s not found in branches', sha)
875 raise exceptions.LookupException(e)(missing_commit_err)
875 raise exceptions.LookupException(e)(missing_commit_err)
876
876
877 commit_id = commit.hex
877 commit_id = commit.hex
878 type_str = commit.type_str
878 type_str = commit.type_str
879
879
880 return {
880 return {
881 'id': commit_id,
881 'id': commit_id,
882 'type': type_str,
882 'type': type_str,
883 'commit_id': commit_id,
883 'commit_id': commit_id,
884 'idx': 0
884 'idx': 0
885 }
885 }
886
886
887 return _get_object(context_uid, repo_id, sha)
887 return _get_object(context_uid, repo_id, sha)
888
888
889 @reraise_safe_exceptions
889 @reraise_safe_exceptions
890 def get_refs(self, wire):
890 def get_refs(self, wire):
891 cache_on, context_uid, repo_id = self._cache_on(wire)
891 cache_on, context_uid, repo_id = self._cache_on(wire)
892 region = self._region(wire)
892 region = self._region(wire)
893
893
894 @region.conditional_cache_on_arguments(condition=cache_on)
894 @region.conditional_cache_on_arguments(condition=cache_on)
895 def _get_refs(_context_uid, _repo_id):
895 def _get_refs(_context_uid, _repo_id):
896
896
897 repo_init = self._factory.repo_libgit2(wire)
897 repo_init = self._factory.repo_libgit2(wire)
898 with repo_init as repo:
898 with repo_init as repo:
899 regex = re.compile('^refs/(heads|tags)/')
899 regex = re.compile('^refs/(heads|tags)/')
900 return {x.name: x.target.hex for x in
900 return {x.name: x.target.hex for x in
901 [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]}
901 [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]}
902
902
903 return _get_refs(context_uid, repo_id)
903 return _get_refs(context_uid, repo_id)
904
904
905 @reraise_safe_exceptions
905 @reraise_safe_exceptions
906 def get_branch_pointers(self, wire):
906 def get_branch_pointers(self, wire):
907 cache_on, context_uid, repo_id = self._cache_on(wire)
907 cache_on, context_uid, repo_id = self._cache_on(wire)
908 region = self._region(wire)
908 region = self._region(wire)
909
909
910 @region.conditional_cache_on_arguments(condition=cache_on)
910 @region.conditional_cache_on_arguments(condition=cache_on)
911 def _get_branch_pointers(_context_uid, _repo_id):
911 def _get_branch_pointers(_context_uid, _repo_id):
912
912
913 repo_init = self._factory.repo_libgit2(wire)
913 repo_init = self._factory.repo_libgit2(wire)
914 regex = re.compile('^refs/heads')
914 regex = re.compile('^refs/heads')
915 with repo_init as repo:
915 with repo_init as repo:
916 branches = [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]
916 branches = [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]
917 return {x.target.hex: x.shorthand for x in branches}
917 return {x.target.hex: x.shorthand for x in branches}
918
918
919 return _get_branch_pointers(context_uid, repo_id)
919 return _get_branch_pointers(context_uid, repo_id)
920
920
921 @reraise_safe_exceptions
921 @reraise_safe_exceptions
922 def head(self, wire, show_exc=True):
922 def head(self, wire, show_exc=True):
923 cache_on, context_uid, repo_id = self._cache_on(wire)
923 cache_on, context_uid, repo_id = self._cache_on(wire)
924 region = self._region(wire)
924 region = self._region(wire)
925
925
926 @region.conditional_cache_on_arguments(condition=cache_on)
926 @region.conditional_cache_on_arguments(condition=cache_on)
927 def _head(_context_uid, _repo_id, _show_exc):
927 def _head(_context_uid, _repo_id, _show_exc):
928 repo_init = self._factory.repo_libgit2(wire)
928 repo_init = self._factory.repo_libgit2(wire)
929 with repo_init as repo:
929 with repo_init as repo:
930 try:
930 try:
931 return repo.head.peel().hex
931 return repo.head.peel().hex
932 except Exception:
932 except Exception:
933 if show_exc:
933 if show_exc:
934 raise
934 raise
935 return _head(context_uid, repo_id, show_exc)
935 return _head(context_uid, repo_id, show_exc)
936
936
937 @reraise_safe_exceptions
937 @reraise_safe_exceptions
938 def init(self, wire):
938 def init(self, wire):
939 repo_path = safe_str(wire['path'])
939 repo_path = safe_str(wire['path'])
940 pygit2.init_repository(repo_path, bare=False)
940 pygit2.init_repository(repo_path, bare=False)
941
941
942 @reraise_safe_exceptions
942 @reraise_safe_exceptions
943 def init_bare(self, wire):
943 def init_bare(self, wire):
944 repo_path = safe_str(wire['path'])
944 repo_path = safe_str(wire['path'])
945 pygit2.init_repository(repo_path, bare=True)
945 pygit2.init_repository(repo_path, bare=True)
946
946
947 @reraise_safe_exceptions
947 @reraise_safe_exceptions
948 def revision(self, wire, rev):
948 def revision(self, wire, rev):
949
949
950 cache_on, context_uid, repo_id = self._cache_on(wire)
950 cache_on, context_uid, repo_id = self._cache_on(wire)
951 region = self._region(wire)
951 region = self._region(wire)
952
952
953 @region.conditional_cache_on_arguments(condition=cache_on)
953 @region.conditional_cache_on_arguments(condition=cache_on)
954 def _revision(_context_uid, _repo_id, _rev):
954 def _revision(_context_uid, _repo_id, _rev):
955 repo_init = self._factory.repo_libgit2(wire)
955 repo_init = self._factory.repo_libgit2(wire)
956 with repo_init as repo:
956 with repo_init as repo:
957 commit = repo[rev]
957 commit = repo[rev]
958 obj_data = {
958 obj_data = {
959 'id': commit.id.hex,
959 'id': commit.id.hex,
960 }
960 }
961 # tree objects itself don't have tree_id attribute
961 # tree objects itself don't have tree_id attribute
962 if hasattr(commit, 'tree_id'):
962 if hasattr(commit, 'tree_id'):
963 obj_data['tree'] = commit.tree_id.hex
963 obj_data['tree'] = commit.tree_id.hex
964
964
965 return obj_data
965 return obj_data
966 return _revision(context_uid, repo_id, rev)
966 return _revision(context_uid, repo_id, rev)
967
967
968 @reraise_safe_exceptions
968 @reraise_safe_exceptions
969 def date(self, wire, commit_id):
969 def date(self, wire, commit_id):
970 cache_on, context_uid, repo_id = self._cache_on(wire)
970 cache_on, context_uid, repo_id = self._cache_on(wire)
971 region = self._region(wire)
971 region = self._region(wire)
972
972
973 @region.conditional_cache_on_arguments(condition=cache_on)
973 @region.conditional_cache_on_arguments(condition=cache_on)
974 def _date(_repo_id, _commit_id):
974 def _date(_repo_id, _commit_id):
975 repo_init = self._factory.repo_libgit2(wire)
975 repo_init = self._factory.repo_libgit2(wire)
976 with repo_init as repo:
976 with repo_init as repo:
977 commit = repo[commit_id]
977 commit = repo[commit_id]
978
978
979 if hasattr(commit, 'commit_time'):
979 if hasattr(commit, 'commit_time'):
980 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
980 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
981 else:
981 else:
982 commit = commit.get_object()
982 commit = commit.get_object()
983 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
983 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
984
984
985 # TODO(marcink): check dulwich difference of offset vs timezone
985 # TODO(marcink): check dulwich difference of offset vs timezone
986 return [commit_time, commit_time_offset]
986 return [commit_time, commit_time_offset]
987 return _date(repo_id, commit_id)
987 return _date(repo_id, commit_id)
988
988
989 @reraise_safe_exceptions
989 @reraise_safe_exceptions
990 def author(self, wire, commit_id):
990 def author(self, wire, commit_id):
991 cache_on, context_uid, repo_id = self._cache_on(wire)
991 cache_on, context_uid, repo_id = self._cache_on(wire)
992 region = self._region(wire)
992 region = self._region(wire)
993
993
994 @region.conditional_cache_on_arguments(condition=cache_on)
994 @region.conditional_cache_on_arguments(condition=cache_on)
995 def _author(_repo_id, _commit_id):
995 def _author(_repo_id, _commit_id):
996 repo_init = self._factory.repo_libgit2(wire)
996 repo_init = self._factory.repo_libgit2(wire)
997 with repo_init as repo:
997 with repo_init as repo:
998 commit = repo[commit_id]
998 commit = repo[commit_id]
999
999
1000 if hasattr(commit, 'author'):
1000 if hasattr(commit, 'author'):
1001 author = commit.author
1001 author = commit.author
1002 else:
1002 else:
1003 author = commit.get_object().author
1003 author = commit.get_object().author
1004
1004
1005 if author.email:
1005 if author.email:
1006 return f"{author.name} <{author.email}>"
1006 return f"{author.name} <{author.email}>"
1007
1007
1008 try:
1008 try:
1009 return f"{author.name}"
1009 return f"{author.name}"
1010 except Exception:
1010 except Exception:
1011 return f"{safe_str(author.raw_name)}"
1011 return f"{safe_str(author.raw_name)}"
1012
1012
1013 return _author(repo_id, commit_id)
1013 return _author(repo_id, commit_id)
1014
1014
1015 @reraise_safe_exceptions
1015 @reraise_safe_exceptions
1016 def message(self, wire, commit_id):
1016 def message(self, wire, commit_id):
1017 cache_on, context_uid, repo_id = self._cache_on(wire)
1017 cache_on, context_uid, repo_id = self._cache_on(wire)
1018 region = self._region(wire)
1018 region = self._region(wire)
1019
1019
1020 @region.conditional_cache_on_arguments(condition=cache_on)
1020 @region.conditional_cache_on_arguments(condition=cache_on)
1021 def _message(_repo_id, _commit_id):
1021 def _message(_repo_id, _commit_id):
1022 repo_init = self._factory.repo_libgit2(wire)
1022 repo_init = self._factory.repo_libgit2(wire)
1023 with repo_init as repo:
1023 with repo_init as repo:
1024 commit = repo[commit_id]
1024 commit = repo[commit_id]
1025 return commit.message
1025 return commit.message
1026 return _message(repo_id, commit_id)
1026 return _message(repo_id, commit_id)
1027
1027
1028 @reraise_safe_exceptions
1028 @reraise_safe_exceptions
1029 def parents(self, wire, commit_id):
1029 def parents(self, wire, commit_id):
1030 cache_on, context_uid, repo_id = self._cache_on(wire)
1030 cache_on, context_uid, repo_id = self._cache_on(wire)
1031 region = self._region(wire)
1031 region = self._region(wire)
1032
1032
1033 @region.conditional_cache_on_arguments(condition=cache_on)
1033 @region.conditional_cache_on_arguments(condition=cache_on)
1034 def _parents(_repo_id, _commit_id):
1034 def _parents(_repo_id, _commit_id):
1035 repo_init = self._factory.repo_libgit2(wire)
1035 repo_init = self._factory.repo_libgit2(wire)
1036 with repo_init as repo:
1036 with repo_init as repo:
1037 commit = repo[commit_id]
1037 commit = repo[commit_id]
1038 if hasattr(commit, 'parent_ids'):
1038 if hasattr(commit, 'parent_ids'):
1039 parent_ids = commit.parent_ids
1039 parent_ids = commit.parent_ids
1040 else:
1040 else:
1041 parent_ids = commit.get_object().parent_ids
1041 parent_ids = commit.get_object().parent_ids
1042
1042
1043 return [x.hex for x in parent_ids]
1043 return [x.hex for x in parent_ids]
1044 return _parents(repo_id, commit_id)
1044 return _parents(repo_id, commit_id)
1045
1045
1046 @reraise_safe_exceptions
1046 @reraise_safe_exceptions
1047 def children(self, wire, commit_id):
1047 def children(self, wire, commit_id):
1048 cache_on, context_uid, repo_id = self._cache_on(wire)
1048 cache_on, context_uid, repo_id = self._cache_on(wire)
1049 region = self._region(wire)
1049 region = self._region(wire)
1050
1050
1051 head = self.head(wire)
1051 head = self.head(wire)
1052
1052
1053 @region.conditional_cache_on_arguments(condition=cache_on)
1053 @region.conditional_cache_on_arguments(condition=cache_on)
1054 def _children(_repo_id, _commit_id):
1054 def _children(_repo_id, _commit_id):
1055
1055
1056 output, __ = self.run_git_command(
1056 output, __ = self.run_git_command(
1057 wire, ['rev-list', '--all', '--children', f'{commit_id}^..{head}'])
1057 wire, ['rev-list', '--all', '--children', f'{commit_id}^..{head}'])
1058
1058
1059 child_ids = []
1059 child_ids = []
1060 pat = re.compile(fr'^{commit_id}')
1060 pat = re.compile(fr'^{commit_id}')
1061 for line in output.splitlines():
1061 for line in output.splitlines():
1062 line = safe_str(line)
1062 line = safe_str(line)
1063 if pat.match(line):
1063 if pat.match(line):
1064 found_ids = line.split(' ')[1:]
1064 found_ids = line.split(' ')[1:]
1065 child_ids.extend(found_ids)
1065 child_ids.extend(found_ids)
1066 break
1066 break
1067
1067
1068 return child_ids
1068 return child_ids
1069 return _children(repo_id, commit_id)
1069 return _children(repo_id, commit_id)
1070
1070
1071 @reraise_safe_exceptions
1071 @reraise_safe_exceptions
1072 def set_refs(self, wire, key, value):
1072 def set_refs(self, wire, key, value):
1073 repo_init = self._factory.repo_libgit2(wire)
1073 repo_init = self._factory.repo_libgit2(wire)
1074 with repo_init as repo:
1074 with repo_init as repo:
1075 repo.references.create(key, value, force=True)
1075 repo.references.create(key, value, force=True)
1076
1076
1077 @reraise_safe_exceptions
1077 @reraise_safe_exceptions
1078 def create_branch(self, wire, branch_name, commit_id, force=False):
1078 def create_branch(self, wire, branch_name, commit_id, force=False):
1079 repo_init = self._factory.repo_libgit2(wire)
1079 repo_init = self._factory.repo_libgit2(wire)
1080 with repo_init as repo:
1080 with repo_init as repo:
1081 if commit_id:
1081 if commit_id:
1082 commit = repo[commit_id]
1082 commit = repo[commit_id]
1083 else:
1083 else:
1084 # if commit is not given just use the HEAD
1084 # if commit is not given just use the HEAD
1085 commit = repo.head()
1085 commit = repo.head()
1086
1086
1087 if force:
1087 if force:
1088 repo.branches.local.create(branch_name, commit, force=force)
1088 repo.branches.local.create(branch_name, commit, force=force)
1089 elif not repo.branches.get(branch_name):
1089 elif not repo.branches.get(branch_name):
1090 # create only if that branch isn't existing
1090 # create only if that branch isn't existing
1091 repo.branches.local.create(branch_name, commit, force=force)
1091 repo.branches.local.create(branch_name, commit, force=force)
1092
1092
1093 @reraise_safe_exceptions
1093 @reraise_safe_exceptions
1094 def remove_ref(self, wire, key):
1094 def remove_ref(self, wire, key):
1095 repo_init = self._factory.repo_libgit2(wire)
1095 repo_init = self._factory.repo_libgit2(wire)
1096 with repo_init as repo:
1096 with repo_init as repo:
1097 repo.references.delete(key)
1097 repo.references.delete(key)
1098
1098
1099 @reraise_safe_exceptions
1099 @reraise_safe_exceptions
1100 def tag_remove(self, wire, tag_name):
1100 def tag_remove(self, wire, tag_name):
1101 repo_init = self._factory.repo_libgit2(wire)
1101 repo_init = self._factory.repo_libgit2(wire)
1102 with repo_init as repo:
1102 with repo_init as repo:
1103 key = f'refs/tags/{tag_name}'
1103 key = f'refs/tags/{tag_name}'
1104 repo.references.delete(key)
1104 repo.references.delete(key)
1105
1105
1106 @reraise_safe_exceptions
1106 @reraise_safe_exceptions
1107 def tree_changes(self, wire, source_id, target_id):
1107 def tree_changes(self, wire, source_id, target_id):
1108 repo = self._factory.repo(wire)
1108 repo = self._factory.repo(wire)
1109 # source can be empty
1109 # source can be empty
1110 source_id = safe_bytes(source_id if source_id else b'')
1110 source_id = safe_bytes(source_id if source_id else b'')
1111 target_id = safe_bytes(target_id)
1111 target_id = safe_bytes(target_id)
1112
1112
1113 source = repo[source_id].tree if source_id else None
1113 source = repo[source_id].tree if source_id else None
1114 target = repo[target_id].tree
1114 target = repo[target_id].tree
1115 result = repo.object_store.tree_changes(source, target)
1115 result = repo.object_store.tree_changes(source, target)
1116
1116
1117 added = set()
1117 added = set()
1118 modified = set()
1118 modified = set()
1119 deleted = set()
1119 deleted = set()
1120 for (old_path, new_path), (_, _), (_, _) in list(result):
1120 for (old_path, new_path), (_, _), (_, _) in list(result):
1121 if new_path and old_path:
1121 if new_path and old_path:
1122 modified.add(new_path)
1122 modified.add(new_path)
1123 elif new_path and not old_path:
1123 elif new_path and not old_path:
1124 added.add(new_path)
1124 added.add(new_path)
1125 elif not new_path and old_path:
1125 elif not new_path and old_path:
1126 deleted.add(old_path)
1126 deleted.add(old_path)
1127
1127
1128 return list(added), list(modified), list(deleted)
1128 return list(added), list(modified), list(deleted)
1129
1129
1130 @reraise_safe_exceptions
1130 @reraise_safe_exceptions
1131 def tree_and_type_for_path(self, wire, commit_id, path):
1131 def tree_and_type_for_path(self, wire, commit_id, path):
1132
1132
1133 cache_on, context_uid, repo_id = self._cache_on(wire)
1133 cache_on, context_uid, repo_id = self._cache_on(wire)
1134 region = self._region(wire)
1134 region = self._region(wire)
1135
1135
1136 @region.conditional_cache_on_arguments(condition=cache_on)
1136 @region.conditional_cache_on_arguments(condition=cache_on)
1137 def _tree_and_type_for_path(_context_uid, _repo_id, _commit_id, _path):
1137 def _tree_and_type_for_path(_context_uid, _repo_id, _commit_id, _path):
1138 repo_init = self._factory.repo_libgit2(wire)
1138 repo_init = self._factory.repo_libgit2(wire)
1139
1139
1140 with repo_init as repo:
1140 with repo_init as repo:
1141 commit = repo[commit_id]
1141 commit = repo[commit_id]
1142 try:
1142 try:
1143 tree = commit.tree[path]
1143 tree = commit.tree[path]
1144 except KeyError:
1144 except KeyError:
1145 return None, None, None
1145 return None, None, None
1146
1146
1147 return tree.id.hex, tree.type_str, tree.filemode
1147 return tree.id.hex, tree.type_str, tree.filemode
1148 return _tree_and_type_for_path(context_uid, repo_id, commit_id, path)
1148 return _tree_and_type_for_path(context_uid, repo_id, commit_id, path)
1149
1149
1150 @reraise_safe_exceptions
1150 @reraise_safe_exceptions
1151 def tree_items(self, wire, tree_id):
1151 def tree_items(self, wire, tree_id):
1152 cache_on, context_uid, repo_id = self._cache_on(wire)
1152 cache_on, context_uid, repo_id = self._cache_on(wire)
1153 region = self._region(wire)
1153 region = self._region(wire)
1154
1154
1155 @region.conditional_cache_on_arguments(condition=cache_on)
1155 @region.conditional_cache_on_arguments(condition=cache_on)
1156 def _tree_items(_repo_id, _tree_id):
1156 def _tree_items(_repo_id, _tree_id):
1157
1157
1158 repo_init = self._factory.repo_libgit2(wire)
1158 repo_init = self._factory.repo_libgit2(wire)
1159 with repo_init as repo:
1159 with repo_init as repo:
1160 try:
1160 try:
1161 tree = repo[tree_id]
1161 tree = repo[tree_id]
1162 except KeyError:
1162 except KeyError:
1163 raise ObjectMissing(f'No tree with id: {tree_id}')
1163 raise ObjectMissing(f'No tree with id: {tree_id}')
1164
1164
1165 result = []
1165 result = []
1166 for item in tree:
1166 for item in tree:
1167 item_sha = item.hex
1167 item_sha = item.hex
1168 item_mode = item.filemode
1168 item_mode = item.filemode
1169 item_type = item.type_str
1169 item_type = item.type_str
1170
1170
1171 if item_type == 'commit':
1171 if item_type == 'commit':
1172 # NOTE(marcink): submodules we translate to 'link' for backward compat
1172 # NOTE(marcink): submodules we translate to 'link' for backward compat
1173 item_type = 'link'
1173 item_type = 'link'
1174
1174
1175 result.append((item.name, item_mode, item_sha, item_type))
1175 result.append((item.name, item_mode, item_sha, item_type))
1176 return result
1176 return result
1177 return _tree_items(repo_id, tree_id)
1177 return _tree_items(repo_id, tree_id)
1178
1178
1179 @reraise_safe_exceptions
1179 @reraise_safe_exceptions
1180 def diff_2(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1180 def diff_2(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1181 """
1181 """
1182 Old version that uses subprocess to call diff
1182 Old version that uses subprocess to call diff
1183 """
1183 """
1184
1184
1185 flags = [
1185 flags = [
1186 f'-U{context}', '--patch',
1186 f'-U{context}', '--patch',
1187 '--binary',
1187 '--binary',
1188 '--find-renames',
1188 '--find-renames',
1189 '--no-indent-heuristic',
1189 '--no-indent-heuristic',
1190 # '--indent-heuristic',
1190 # '--indent-heuristic',
1191 #'--full-index',
1191 #'--full-index',
1192 #'--abbrev=40'
1192 #'--abbrev=40'
1193 ]
1193 ]
1194
1194
1195 if opt_ignorews:
1195 if opt_ignorews:
1196 flags.append('--ignore-all-space')
1196 flags.append('--ignore-all-space')
1197
1197
1198 if commit_id_1 == self.EMPTY_COMMIT:
1198 if commit_id_1 == self.EMPTY_COMMIT:
1199 cmd = ['show'] + flags + [commit_id_2]
1199 cmd = ['show'] + flags + [commit_id_2]
1200 else:
1200 else:
1201 cmd = ['diff'] + flags + [commit_id_1, commit_id_2]
1201 cmd = ['diff'] + flags + [commit_id_1, commit_id_2]
1202
1202
1203 if file_filter:
1203 if file_filter:
1204 cmd.extend(['--', file_filter])
1204 cmd.extend(['--', file_filter])
1205
1205
1206 diff, __ = self.run_git_command(wire, cmd)
1206 diff, __ = self.run_git_command(wire, cmd)
1207 # If we used 'show' command, strip first few lines (until actual diff
1207 # If we used 'show' command, strip first few lines (until actual diff
1208 # starts)
1208 # starts)
1209 if commit_id_1 == self.EMPTY_COMMIT:
1209 if commit_id_1 == self.EMPTY_COMMIT:
1210 lines = diff.splitlines()
1210 lines = diff.splitlines()
1211 x = 0
1211 x = 0
1212 for line in lines:
1212 for line in lines:
1213 if line.startswith(b'diff'):
1213 if line.startswith(b'diff'):
1214 break
1214 break
1215 x += 1
1215 x += 1
1216 # Append new line just like 'diff' command do
1216 # Append new line just like 'diff' command do
1217 diff = '\n'.join(lines[x:]) + '\n'
1217 diff = '\n'.join(lines[x:]) + '\n'
1218 return diff
1218 return diff
1219
1219
1220 @reraise_safe_exceptions
1220 @reraise_safe_exceptions
1221 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1221 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1222 repo_init = self._factory.repo_libgit2(wire)
1222 repo_init = self._factory.repo_libgit2(wire)
1223
1223
1224 with repo_init as repo:
1224 with repo_init as repo:
1225 swap = True
1225 swap = True
1226 flags = 0
1226 flags = 0
1227 flags |= pygit2.GIT_DIFF_SHOW_BINARY
1227 flags |= pygit2.GIT_DIFF_SHOW_BINARY
1228
1228
1229 if opt_ignorews:
1229 if opt_ignorews:
1230 flags |= pygit2.GIT_DIFF_IGNORE_WHITESPACE
1230 flags |= pygit2.GIT_DIFF_IGNORE_WHITESPACE
1231
1231
1232 if commit_id_1 == self.EMPTY_COMMIT:
1232 if commit_id_1 == self.EMPTY_COMMIT:
1233 comm1 = repo[commit_id_2]
1233 comm1 = repo[commit_id_2]
1234 diff_obj = comm1.tree.diff_to_tree(
1234 diff_obj = comm1.tree.diff_to_tree(
1235 flags=flags, context_lines=context, swap=swap)
1235 flags=flags, context_lines=context, swap=swap)
1236
1236
1237 else:
1237 else:
1238 comm1 = repo[commit_id_2]
1238 comm1 = repo[commit_id_2]
1239 comm2 = repo[commit_id_1]
1239 comm2 = repo[commit_id_1]
1240 diff_obj = comm1.tree.diff_to_tree(
1240 diff_obj = comm1.tree.diff_to_tree(
1241 comm2.tree, flags=flags, context_lines=context, swap=swap)
1241 comm2.tree, flags=flags, context_lines=context, swap=swap)
1242 similar_flags = 0
1242 similar_flags = 0
1243 similar_flags |= pygit2.GIT_DIFF_FIND_RENAMES
1243 similar_flags |= pygit2.GIT_DIFF_FIND_RENAMES
1244 diff_obj.find_similar(flags=similar_flags)
1244 diff_obj.find_similar(flags=similar_flags)
1245
1245
1246 if file_filter:
1246 if file_filter:
1247 for p in diff_obj:
1247 for p in diff_obj:
1248 if p.delta.old_file.path == file_filter:
1248 if p.delta.old_file.path == file_filter:
1249 return BytesEnvelope(p.data) or BytesEnvelope(b'')
1249 return BytesEnvelope(p.data) or BytesEnvelope(b'')
1250 # fo matching path == no diff
1250 # fo matching path == no diff
1251 return BytesEnvelope(b'')
1251 return BytesEnvelope(b'')
1252
1252
1253 return BytesEnvelope(safe_bytes(diff_obj.patch)) or BytesEnvelope(b'')
1253 return BytesEnvelope(safe_bytes(diff_obj.patch)) or BytesEnvelope(b'')
1254
1254
1255 @reraise_safe_exceptions
1255 @reraise_safe_exceptions
1256 def node_history(self, wire, commit_id, path, limit):
1256 def node_history(self, wire, commit_id, path, limit):
1257 cache_on, context_uid, repo_id = self._cache_on(wire)
1257 cache_on, context_uid, repo_id = self._cache_on(wire)
1258 region = self._region(wire)
1258 region = self._region(wire)
1259
1259
1260 @region.conditional_cache_on_arguments(condition=cache_on)
1260 @region.conditional_cache_on_arguments(condition=cache_on)
1261 def _node_history(_context_uid, _repo_id, _commit_id, _path, _limit):
1261 def _node_history(_context_uid, _repo_id, _commit_id, _path, _limit):
1262 # optimize for n==1, rev-list is much faster for that use-case
1262 # optimize for n==1, rev-list is much faster for that use-case
1263 if limit == 1:
1263 if limit == 1:
1264 cmd = ['rev-list', '-1', commit_id, '--', path]
1264 cmd = ['rev-list', '-1', commit_id, '--', path]
1265 else:
1265 else:
1266 cmd = ['log']
1266 cmd = ['log']
1267 if limit:
1267 if limit:
1268 cmd.extend(['-n', str(safe_int(limit, 0))])
1268 cmd.extend(['-n', str(safe_int(limit, 0))])
1269 cmd.extend(['--pretty=format: %H', '-s', commit_id, '--', path])
1269 cmd.extend(['--pretty=format: %H', '-s', commit_id, '--', path])
1270
1270
1271 output, __ = self.run_git_command(wire, cmd)
1271 output, __ = self.run_git_command(wire, cmd)
1272 commit_ids = re.findall(rb'[0-9a-fA-F]{40}', output)
1272 commit_ids = re.findall(rb'[0-9a-fA-F]{40}', output)
1273
1273
1274 return [x for x in commit_ids]
1274 return [x for x in commit_ids]
1275 return _node_history(context_uid, repo_id, commit_id, path, limit)
1275 return _node_history(context_uid, repo_id, commit_id, path, limit)
1276
1276
1277 @reraise_safe_exceptions
1277 @reraise_safe_exceptions
1278 def node_annotate_legacy(self, wire, commit_id, path):
1278 def node_annotate_legacy(self, wire, commit_id, path):
1279 # note: replaced by pygit2 implementation
1279 # note: replaced by pygit2 implementation
1280 cmd = ['blame', '-l', '--root', '-r', commit_id, '--', path]
1280 cmd = ['blame', '-l', '--root', '-r', commit_id, '--', path]
1281 # -l ==> outputs long shas (and we need all 40 characters)
1281 # -l ==> outputs long shas (and we need all 40 characters)
1282 # --root ==> doesn't put '^' character for boundaries
1282 # --root ==> doesn't put '^' character for boundaries
1283 # -r commit_id ==> blames for the given commit
1283 # -r commit_id ==> blames for the given commit
1284 output, __ = self.run_git_command(wire, cmd)
1284 output, __ = self.run_git_command(wire, cmd)
1285
1285
1286 result = []
1286 result = []
1287 for i, blame_line in enumerate(output.splitlines()[:-1]):
1287 for i, blame_line in enumerate(output.splitlines()[:-1]):
1288 line_no = i + 1
1288 line_no = i + 1
1289 blame_commit_id, line = re.split(rb' ', blame_line, 1)
1289 blame_commit_id, line = re.split(rb' ', blame_line, 1)
1290 result.append((line_no, blame_commit_id, line))
1290 result.append((line_no, blame_commit_id, line))
1291
1291
1292 return result
1292 return result
1293
1293
1294 @reraise_safe_exceptions
1294 @reraise_safe_exceptions
1295 def node_annotate(self, wire, commit_id, path):
1295 def node_annotate(self, wire, commit_id, path):
1296
1296
1297 result_libgit = []
1297 result_libgit = []
1298 repo_init = self._factory.repo_libgit2(wire)
1298 repo_init = self._factory.repo_libgit2(wire)
1299 with repo_init as repo:
1299 with repo_init as repo:
1300 commit = repo[commit_id]
1300 commit = repo[commit_id]
1301 blame_obj = repo.blame(path, newest_commit=commit_id)
1301 blame_obj = repo.blame(path, newest_commit=commit_id)
1302 for i, line in enumerate(commit.tree[path].data.splitlines()):
1302 for i, line in enumerate(commit.tree[path].data.splitlines()):
1303 line_no = i + 1
1303 line_no = i + 1
1304 hunk = blame_obj.for_line(line_no)
1304 hunk = blame_obj.for_line(line_no)
1305 blame_commit_id = hunk.final_commit_id.hex
1305 blame_commit_id = hunk.final_commit_id.hex
1306
1306
1307 result_libgit.append((line_no, blame_commit_id, line))
1307 result_libgit.append((line_no, blame_commit_id, line))
1308
1308
1309 return BinaryEnvelope(result_libgit)
1309 return BinaryEnvelope(result_libgit)
1310
1310
1311 @reraise_safe_exceptions
1311 @reraise_safe_exceptions
1312 def update_server_info(self, wire):
1312 def update_server_info(self, wire):
1313 repo = self._factory.repo(wire)
1313 repo = self._factory.repo(wire)
1314 update_server_info(repo)
1314 update_server_info(repo)
1315
1315
1316 @reraise_safe_exceptions
1316 @reraise_safe_exceptions
1317 def get_all_commit_ids(self, wire):
1317 def get_all_commit_ids(self, wire):
1318
1318
1319 cache_on, context_uid, repo_id = self._cache_on(wire)
1319 cache_on, context_uid, repo_id = self._cache_on(wire)
1320 region = self._region(wire)
1320 region = self._region(wire)
1321
1321
1322 @region.conditional_cache_on_arguments(condition=cache_on)
1322 @region.conditional_cache_on_arguments(condition=cache_on)
1323 def _get_all_commit_ids(_context_uid, _repo_id):
1323 def _get_all_commit_ids(_context_uid, _repo_id):
1324
1324
1325 cmd = ['rev-list', '--reverse', '--date-order', '--branches', '--tags']
1325 cmd = ['rev-list', '--reverse', '--date-order', '--branches', '--tags']
1326 try:
1326 try:
1327 output, __ = self.run_git_command(wire, cmd)
1327 output, __ = self.run_git_command(wire, cmd)
1328 return output.splitlines()
1328 return output.splitlines()
1329 except Exception:
1329 except Exception:
1330 # Can be raised for empty repositories
1330 # Can be raised for empty repositories
1331 return []
1331 return []
1332
1332
1333 @region.conditional_cache_on_arguments(condition=cache_on)
1333 @region.conditional_cache_on_arguments(condition=cache_on)
1334 def _get_all_commit_ids_pygit2(_context_uid, _repo_id):
1334 def _get_all_commit_ids_pygit2(_context_uid, _repo_id):
1335 repo_init = self._factory.repo_libgit2(wire)
1335 repo_init = self._factory.repo_libgit2(wire)
1336 from pygit2 import GIT_SORT_REVERSE, GIT_SORT_TIME, GIT_BRANCH_ALL
1336 from pygit2 import GIT_SORT_REVERSE, GIT_SORT_TIME, GIT_BRANCH_ALL
1337 results = []
1337 results = []
1338 with repo_init as repo:
1338 with repo_init as repo:
1339 for commit in repo.walk(repo.head.target, GIT_SORT_TIME | GIT_BRANCH_ALL | GIT_SORT_REVERSE):
1339 for commit in repo.walk(repo.head.target, GIT_SORT_TIME | GIT_BRANCH_ALL | GIT_SORT_REVERSE):
1340 results.append(commit.id.hex)
1340 results.append(commit.id.hex)
1341
1341
1342 return _get_all_commit_ids(context_uid, repo_id)
1342 return _get_all_commit_ids(context_uid, repo_id)
1343
1343
1344 @reraise_safe_exceptions
1344 @reraise_safe_exceptions
1345 def run_git_command(self, wire, cmd, **opts):
1345 def run_git_command(self, wire, cmd, **opts):
1346 path = wire.get('path', None)
1346 path = wire.get('path', None)
1347
1347
1348 if path and os.path.isdir(path):
1348 if path and os.path.isdir(path):
1349 opts['cwd'] = path
1349 opts['cwd'] = path
1350
1350
1351 if '_bare' in opts:
1351 if '_bare' in opts:
1352 _copts = []
1352 _copts = []
1353 del opts['_bare']
1353 del opts['_bare']
1354 else:
1354 else:
1355 _copts = ['-c', 'core.quotepath=false', '-c', 'advice.diverging=false']
1355 _copts = ['-c', 'core.quotepath=false', '-c', 'advice.diverging=false']
1356 safe_call = False
1356 safe_call = False
1357 if '_safe' in opts:
1357 if '_safe' in opts:
1358 # no exc on failure
1358 # no exc on failure
1359 del opts['_safe']
1359 del opts['_safe']
1360 safe_call = True
1360 safe_call = True
1361
1361
1362 if '_copts' in opts:
1362 if '_copts' in opts:
1363 _copts.extend(opts['_copts'] or [])
1363 _copts.extend(opts['_copts'] or [])
1364 del opts['_copts']
1364 del opts['_copts']
1365
1365
1366 gitenv = os.environ.copy()
1366 gitenv = os.environ.copy()
1367 gitenv.update(opts.pop('extra_env', {}))
1367 gitenv.update(opts.pop('extra_env', {}))
1368 # need to clean fix GIT_DIR !
1368 # need to clean fix GIT_DIR !
1369 if 'GIT_DIR' in gitenv:
1369 if 'GIT_DIR' in gitenv:
1370 del gitenv['GIT_DIR']
1370 del gitenv['GIT_DIR']
1371 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
1371 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
1372 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
1372 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
1373
1373
1374 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
1374 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
1375 _opts = {'env': gitenv, 'shell': False}
1375 _opts = {'env': gitenv, 'shell': False}
1376
1376
1377 proc = None
1377 proc = None
1378 try:
1378 try:
1379 _opts.update(opts)
1379 _opts.update(opts)
1380 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
1380 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
1381
1381
1382 return b''.join(proc), b''.join(proc.stderr)
1382 return b''.join(proc), b''.join(proc.stderr)
1383 except OSError as err:
1383 except OSError as err:
1384 cmd = ' '.join(map(safe_str, cmd)) # human friendly CMD
1384 cmd = ' '.join(map(safe_str, cmd)) # human friendly CMD
1385 tb_err = ("Couldn't run git command (%s).\n"
1385 tb_err = ("Couldn't run git command (%s).\n"
1386 "Original error was:%s\n"
1386 "Original error was:%s\n"
1387 "Call options:%s\n"
1387 "Call options:%s\n"
1388 % (cmd, err, _opts))
1388 % (cmd, err, _opts))
1389 log.exception(tb_err)
1389 log.exception(tb_err)
1390 if safe_call:
1390 if safe_call:
1391 return '', err
1391 return '', err
1392 else:
1392 else:
1393 raise exceptions.VcsException()(tb_err)
1393 raise exceptions.VcsException()(tb_err)
1394 finally:
1394 finally:
1395 if proc:
1395 if proc:
1396 proc.close()
1396 proc.close()
1397
1397
1398 @reraise_safe_exceptions
1398 @reraise_safe_exceptions
1399 def install_hooks(self, wire, force=False):
1399 def install_hooks(self, wire, force=False):
1400 from vcsserver.hook_utils import install_git_hooks
1400 from vcsserver.hook_utils import install_git_hooks
1401 bare = self.bare(wire)
1401 bare = self.bare(wire)
1402 path = wire['path']
1402 path = wire['path']
1403 binary_dir = settings.BINARY_DIR
1403 binary_dir = settings.BINARY_DIR
1404 if binary_dir:
1404 if binary_dir:
1405 os.path.join(binary_dir, 'python3')
1405 os.path.join(binary_dir, 'python3')
1406 return install_git_hooks(path, bare, force_create=force)
1406 return install_git_hooks(path, bare, force_create=force)
1407
1407
1408 @reraise_safe_exceptions
1408 @reraise_safe_exceptions
1409 def get_hooks_info(self, wire):
1409 def get_hooks_info(self, wire):
1410 from vcsserver.hook_utils import (
1410 from vcsserver.hook_utils import (
1411 get_git_pre_hook_version, get_git_post_hook_version)
1411 get_git_pre_hook_version, get_git_post_hook_version)
1412 bare = self.bare(wire)
1412 bare = self.bare(wire)
1413 path = wire['path']
1413 path = wire['path']
1414 return {
1414 return {
1415 'pre_version': get_git_pre_hook_version(path, bare),
1415 'pre_version': get_git_pre_hook_version(path, bare),
1416 'post_version': get_git_post_hook_version(path, bare),
1416 'post_version': get_git_post_hook_version(path, bare),
1417 }
1417 }
1418
1418
1419 @reraise_safe_exceptions
1419 @reraise_safe_exceptions
1420 def set_head_ref(self, wire, head_name):
1420 def set_head_ref(self, wire, head_name):
1421 log.debug('Setting refs/head to `%s`', head_name)
1421 log.debug('Setting refs/head to `%s`', head_name)
1422 repo_init = self._factory.repo_libgit2(wire)
1422 repo_init = self._factory.repo_libgit2(wire)
1423 with repo_init as repo:
1423 with repo_init as repo:
1424 repo.set_head(f'refs/heads/{head_name}')
1424 repo.set_head(f'refs/heads/{head_name}')
1425
1425
1426 return [head_name] + [f'set HEAD to refs/heads/{head_name}']
1426 return [head_name] + [f'set HEAD to refs/heads/{head_name}']
1427
1427
1428 @reraise_safe_exceptions
1428 @reraise_safe_exceptions
1429 def archive_repo(self, wire, archive_name_key, kind, mtime, archive_at_path,
1429 def archive_repo(self, wire, archive_name_key, kind, mtime, archive_at_path,
1430 archive_dir_name, commit_id, cache_config):
1430 archive_dir_name, commit_id, cache_config):
1431
1431
1432 def file_walker(_commit_id, path):
1432 def file_walker(_commit_id, path):
1433 repo_init = self._factory.repo_libgit2(wire)
1433 repo_init = self._factory.repo_libgit2(wire)
1434
1434
1435 with repo_init as repo:
1435 with repo_init as repo:
1436 commit = repo[commit_id]
1436 commit = repo[commit_id]
1437
1437
1438 if path in ['', '/']:
1438 if path in ['', '/']:
1439 tree = commit.tree
1439 tree = commit.tree
1440 else:
1440 else:
1441 tree = commit.tree[path.rstrip('/')]
1441 tree = commit.tree[path.rstrip('/')]
1442 tree_id = tree.id.hex
1442 tree_id = tree.id.hex
1443 try:
1443 try:
1444 tree = repo[tree_id]
1444 tree = repo[tree_id]
1445 except KeyError:
1445 except KeyError:
1446 raise ObjectMissing(f'No tree with id: {tree_id}')
1446 raise ObjectMissing(f'No tree with id: {tree_id}')
1447
1447
1448 index = LibGit2Index.Index()
1448 index = LibGit2Index.Index()
1449 index.read_tree(tree)
1449 index.read_tree(tree)
1450 file_iter = index
1450 file_iter = index
1451
1451
1452 for file_node in file_iter:
1452 for file_node in file_iter:
1453 file_path = file_node.path
1453 file_path = file_node.path
1454 mode = file_node.mode
1454 mode = file_node.mode
1455 is_link = stat.S_ISLNK(mode)
1455 is_link = stat.S_ISLNK(mode)
1456 if mode == pygit2.GIT_FILEMODE_COMMIT:
1456 if mode == pygit2.GIT_FILEMODE_COMMIT:
1457 log.debug('Skipping path %s as a commit node', file_path)
1457 log.debug('Skipping path %s as a commit node', file_path)
1458 continue
1458 continue
1459 yield ArchiveNode(file_path, mode, is_link, repo[file_node.hex].read_raw)
1459 yield ArchiveNode(file_path, mode, is_link, repo[file_node.hex].read_raw)
1460
1460
1461 return store_archive_in_cache(
1461 return store_archive_in_cache(
1462 file_walker, archive_name_key, kind, mtime, archive_at_path, archive_dir_name, commit_id, cache_config=cache_config)
1462 file_walker, archive_name_key, kind, mtime, archive_at_path, archive_dir_name, commit_id, cache_config=cache_config)
@@ -1,1201 +1,1201 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-2023 RhodeCode GmbH
2 # Copyright (C) 2014-2023 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 sys
21 import sys
22 import urllib.request
22 import urllib.request
23 import urllib.parse
23 import urllib.parse
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 from mercurial.error import AmbiguousPrefixLookupError
32 from mercurial.error import AmbiguousPrefixLookupError
33
33
34 import vcsserver
34 import vcsserver
35 from vcsserver import exceptions
35 from vcsserver import exceptions
36 from vcsserver.base import (
36 from vcsserver.base import (
37 RepoFactory,
37 RepoFactory,
38 obfuscate_qs,
38 obfuscate_qs,
39 raise_from_original,
39 raise_from_original,
40 store_archive_in_cache,
40 store_archive_in_cache,
41 ArchiveNode,
41 ArchiveNode,
42 BytesEnvelope,
42 BytesEnvelope,
43 BinaryEnvelope,
43 BinaryEnvelope,
44 )
44 )
45 from vcsserver.hgcompat import (
45 from vcsserver.hgcompat import (
46 archival,
46 archival,
47 bin,
47 bin,
48 clone,
48 clone,
49 config as hgconfig,
49 config as hgconfig,
50 diffopts,
50 diffopts,
51 hex,
51 hex,
52 get_ctx,
52 get_ctx,
53 hg_url as url_parser,
53 hg_url as url_parser,
54 httpbasicauthhandler,
54 httpbasicauthhandler,
55 httpdigestauthhandler,
55 httpdigestauthhandler,
56 makepeer,
56 makepeer,
57 instance,
57 instance,
58 match,
58 match,
59 memctx,
59 memctx,
60 exchange,
60 exchange,
61 memfilectx,
61 memfilectx,
62 nullrev,
62 nullrev,
63 hg_merge,
63 hg_merge,
64 patch,
64 patch,
65 peer,
65 peer,
66 revrange,
66 revrange,
67 ui,
67 ui,
68 hg_tag,
68 hg_tag,
69 Abort,
69 Abort,
70 LookupError,
70 LookupError,
71 RepoError,
71 RepoError,
72 RepoLookupError,
72 RepoLookupError,
73 InterventionRequired,
73 InterventionRequired,
74 RequirementError,
74 RequirementError,
75 alwaysmatcher,
75 alwaysmatcher,
76 patternmatcher,
76 patternmatcher,
77 hgutil,
77 hgutil,
78 hgext_strip,
78 hgext_strip,
79 )
79 )
80 from vcsserver.str_utils import ascii_bytes, ascii_str, safe_str, safe_bytes
80 from vcsserver.str_utils import ascii_bytes, ascii_str, safe_str, safe_bytes
81 from vcsserver.vcs_base import RemoteBase
81 from vcsserver.vcs_base import RemoteBase
82 from vcsserver.config import hooks as hooks_config
82 from vcsserver.config import hooks as hooks_config
83 from vcsserver.lib.exc_tracking import format_exc
83 from vcsserver.lib.exc_tracking import format_exc
84
84
85 log = logging.getLogger(__name__)
85 log = logging.getLogger(__name__)
86
86
87
87
88 def make_ui_from_config(repo_config):
88 def make_ui_from_config(repo_config):
89
89
90 class LoggingUI(ui.ui):
90 class LoggingUI(ui.ui):
91
91
92 def status(self, *msg, **opts):
92 def status(self, *msg, **opts):
93 str_msg = map(safe_str, msg)
93 str_msg = map(safe_str, msg)
94 log.info(' '.join(str_msg).rstrip('\n'))
94 log.info(' '.join(str_msg).rstrip('\n'))
95 #super(LoggingUI, self).status(*msg, **opts)
95 #super(LoggingUI, self).status(*msg, **opts)
96
96
97 def warn(self, *msg, **opts):
97 def warn(self, *msg, **opts):
98 str_msg = map(safe_str, msg)
98 str_msg = map(safe_str, msg)
99 log.warning('ui_logger:'+' '.join(str_msg).rstrip('\n'))
99 log.warning('ui_logger:'+' '.join(str_msg).rstrip('\n'))
100 #super(LoggingUI, self).warn(*msg, **opts)
100 #super(LoggingUI, self).warn(*msg, **opts)
101
101
102 def error(self, *msg, **opts):
102 def error(self, *msg, **opts):
103 str_msg = map(safe_str, msg)
103 str_msg = map(safe_str, msg)
104 log.error('ui_logger:'+' '.join(str_msg).rstrip('\n'))
104 log.error('ui_logger:'+' '.join(str_msg).rstrip('\n'))
105 #super(LoggingUI, self).error(*msg, **opts)
105 #super(LoggingUI, self).error(*msg, **opts)
106
106
107 def note(self, *msg, **opts):
107 def note(self, *msg, **opts):
108 str_msg = map(safe_str, msg)
108 str_msg = map(safe_str, msg)
109 log.info('ui_logger:'+' '.join(str_msg).rstrip('\n'))
109 log.info('ui_logger:'+' '.join(str_msg).rstrip('\n'))
110 #super(LoggingUI, self).note(*msg, **opts)
110 #super(LoggingUI, self).note(*msg, **opts)
111
111
112 def debug(self, *msg, **opts):
112 def debug(self, *msg, **opts):
113 str_msg = map(safe_str, msg)
113 str_msg = map(safe_str, msg)
114 log.debug('ui_logger:'+' '.join(str_msg).rstrip('\n'))
114 log.debug('ui_logger:'+' '.join(str_msg).rstrip('\n'))
115 #super(LoggingUI, self).debug(*msg, **opts)
115 #super(LoggingUI, self).debug(*msg, **opts)
116
116
117 baseui = LoggingUI()
117 baseui = LoggingUI()
118
118
119 # clean the baseui object
119 # clean the baseui object
120 baseui._ocfg = hgconfig.config()
120 baseui._ocfg = hgconfig.config()
121 baseui._ucfg = hgconfig.config()
121 baseui._ucfg = hgconfig.config()
122 baseui._tcfg = hgconfig.config()
122 baseui._tcfg = hgconfig.config()
123
123
124 for section, option, value in repo_config:
124 for section, option, value in repo_config:
125 baseui.setconfig(ascii_bytes(section), ascii_bytes(option), ascii_bytes(value))
125 baseui.setconfig(ascii_bytes(section), ascii_bytes(option), ascii_bytes(value))
126
126
127 # make our hgweb quiet so it doesn't print output
127 # make our hgweb quiet so it doesn't print output
128 baseui.setconfig(b'ui', b'quiet', b'true')
128 baseui.setconfig(b'ui', b'quiet', b'true')
129
129
130 baseui.setconfig(b'ui', b'paginate', b'never')
130 baseui.setconfig(b'ui', b'paginate', b'never')
131 # for better Error reporting of Mercurial
131 # for better Error reporting of Mercurial
132 baseui.setconfig(b'ui', b'message-output', b'stderr')
132 baseui.setconfig(b'ui', b'message-output', b'stderr')
133
133
134 # force mercurial to only use 1 thread, otherwise it may try to set a
134 # force mercurial to only use 1 thread, otherwise it may try to set a
135 # signal in a non-main thread, thus generating a ValueError.
135 # signal in a non-main thread, thus generating a ValueError.
136 baseui.setconfig(b'worker', b'numcpus', 1)
136 baseui.setconfig(b'worker', b'numcpus', 1)
137
137
138 # If there is no config for the largefiles extension, we explicitly disable
138 # If there is no config for the largefiles extension, we explicitly disable
139 # it here. This overrides settings from repositories hgrc file. Recent
139 # it here. This overrides settings from repositories hgrc file. Recent
140 # mercurial versions enable largefiles in hgrc on clone from largefile
140 # mercurial versions enable largefiles in hgrc on clone from largefile
141 # repo.
141 # repo.
142 if not baseui.hasconfig(b'extensions', b'largefiles'):
142 if not baseui.hasconfig(b'extensions', b'largefiles'):
143 log.debug('Explicitly disable largefiles extension for repo.')
143 log.debug('Explicitly disable largefiles extension for repo.')
144 baseui.setconfig(b'extensions', b'largefiles', b'!')
144 baseui.setconfig(b'extensions', b'largefiles', b'!')
145
145
146 return baseui
146 return baseui
147
147
148
148
149 def reraise_safe_exceptions(func):
149 def reraise_safe_exceptions(func):
150 """Decorator for converting mercurial exceptions to something neutral."""
150 """Decorator for converting mercurial exceptions to something neutral."""
151
151
152 def wrapper(*args, **kwargs):
152 def wrapper(*args, **kwargs):
153 try:
153 try:
154 return func(*args, **kwargs)
154 return func(*args, **kwargs)
155 except (Abort, InterventionRequired) as e:
155 except (Abort, InterventionRequired) as e:
156 raise_from_original(exceptions.AbortException(e), e)
156 raise_from_original(exceptions.AbortException(e), e)
157 except RepoLookupError as e:
157 except RepoLookupError as e:
158 raise_from_original(exceptions.LookupException(e), e)
158 raise_from_original(exceptions.LookupException(e), e)
159 except RequirementError as e:
159 except RequirementError as e:
160 raise_from_original(exceptions.RequirementException(e), e)
160 raise_from_original(exceptions.RequirementException(e), e)
161 except RepoError as e:
161 except RepoError as e:
162 raise_from_original(exceptions.VcsException(e), e)
162 raise_from_original(exceptions.VcsException(e), e)
163 except LookupError as e:
163 except LookupError as e:
164 raise_from_original(exceptions.LookupException(e), e)
164 raise_from_original(exceptions.LookupException(e), e)
165 except Exception as e:
165 except Exception as e:
166 if not hasattr(e, '_vcs_kind'):
166 if not hasattr(e, '_vcs_kind'):
167 log.exception("Unhandled exception in hg remote call")
167 log.exception("Unhandled exception in hg remote call")
168 raise_from_original(exceptions.UnhandledException(e), e)
168 raise_from_original(exceptions.UnhandledException(e), e)
169
169
170 raise
170 raise
171 return wrapper
171 return wrapper
172
172
173
173
174 class MercurialFactory(RepoFactory):
174 class MercurialFactory(RepoFactory):
175 repo_type = 'hg'
175 repo_type = 'hg'
176
176
177 def _create_config(self, config, hooks=True):
177 def _create_config(self, config, hooks=True):
178 if not hooks:
178 if not hooks:
179
179
180 hooks_to_clean = {
180 hooks_to_clean = {
181
181
182 hooks_config.HOOK_REPO_SIZE,
182 hooks_config.HOOK_REPO_SIZE,
183 hooks_config.HOOK_PRE_PULL,
183 hooks_config.HOOK_PRE_PULL,
184 hooks_config.HOOK_PULL,
184 hooks_config.HOOK_PULL,
185
185
186 hooks_config.HOOK_PRE_PUSH,
186 hooks_config.HOOK_PRE_PUSH,
187 # TODO: what about PRETXT, this was disabled in pre 5.0.0
187 # TODO: what about PRETXT, this was disabled in pre 5.0.0
188 hooks_config.HOOK_PRETX_PUSH,
188 hooks_config.HOOK_PRETX_PUSH,
189
189
190 }
190 }
191 new_config = []
191 new_config = []
192 for section, option, value in config:
192 for section, option, value in config:
193 if section == 'hooks' and option in hooks_to_clean:
193 if section == 'hooks' and option in hooks_to_clean:
194 continue
194 continue
195 new_config.append((section, option, value))
195 new_config.append((section, option, value))
196 config = new_config
196 config = new_config
197
197
198 baseui = make_ui_from_config(config)
198 baseui = make_ui_from_config(config)
199 return baseui
199 return baseui
200
200
201 def _create_repo(self, wire, create):
201 def _create_repo(self, wire, create):
202 baseui = self._create_config(wire["config"])
202 baseui = self._create_config(wire["config"])
203 repo = instance(baseui, safe_bytes(wire["path"]), create)
203 repo = instance(baseui, safe_bytes(wire["path"]), create)
204 log.debug('repository created: got HG object: %s', repo)
204 log.debug('repository created: got HG object: %s', repo)
205 return repo
205 return repo
206
206
207 def repo(self, wire, create=False):
207 def repo(self, wire, create=False):
208 """
208 """
209 Get a repository instance for the given path.
209 Get a repository instance for the given path.
210 """
210 """
211 return self._create_repo(wire, create)
211 return self._create_repo(wire, create)
212
212
213
213
214 def patch_ui_message_output(baseui):
214 def patch_ui_message_output(baseui):
215 baseui.setconfig(b'ui', b'quiet', b'false')
215 baseui.setconfig(b'ui', b'quiet', b'false')
216 output = io.BytesIO()
216 output = io.BytesIO()
217
217
218 def write(data, **unused_kwargs):
218 def write(data, **unused_kwargs):
219 output.write(data)
219 output.write(data)
220
220
221 baseui.status = write
221 baseui.status = write
222 baseui.write = write
222 baseui.write = write
223 baseui.warn = write
223 baseui.warn = write
224 baseui.debug = write
224 baseui.debug = write
225
225
226 return baseui, output
226 return baseui, output
227
227
228
228
229 def get_obfuscated_url(url_obj):
229 def get_obfuscated_url(url_obj):
230 url_obj.passwd = b'*****' if url_obj.passwd else url_obj.passwd
230 url_obj.passwd = b'*****' if url_obj.passwd else url_obj.passwd
231 url_obj.query = obfuscate_qs(url_obj.query)
231 url_obj.query = obfuscate_qs(url_obj.query)
232 obfuscated_uri = str(url_obj)
232 obfuscated_uri = str(url_obj)
233 return obfuscated_uri
233 return obfuscated_uri
234
234
235
235
236 def normalize_url_for_hg(url: str):
236 def normalize_url_for_hg(url: str):
237 _proto = None
237 _proto = None
238
238
239 if '+' in url[:url.find('://')]:
239 if '+' in url[:url.find('://')]:
240 _proto = url[0:url.find('+')]
240 _proto = url[0:url.find('+')]
241 url = url[url.find('+') + 1:]
241 url = url[url.find('+') + 1:]
242 return url, _proto
242 return url, _proto
243
243
244
244
245 class HgRemote(RemoteBase):
245 class HgRemote(RemoteBase):
246
246
247 def __init__(self, factory):
247 def __init__(self, factory):
248 self._factory = factory
248 self._factory = factory
249 self._bulk_methods = {
249 self._bulk_methods = {
250 "affected_files": self.ctx_files,
250 "affected_files": self.ctx_files,
251 "author": self.ctx_user,
251 "author": self.ctx_user,
252 "branch": self.ctx_branch,
252 "branch": self.ctx_branch,
253 "children": self.ctx_children,
253 "children": self.ctx_children,
254 "date": self.ctx_date,
254 "date": self.ctx_date,
255 "message": self.ctx_description,
255 "message": self.ctx_description,
256 "parents": self.ctx_parents,
256 "parents": self.ctx_parents,
257 "status": self.ctx_status,
257 "status": self.ctx_status,
258 "obsolete": self.ctx_obsolete,
258 "obsolete": self.ctx_obsolete,
259 "phase": self.ctx_phase,
259 "phase": self.ctx_phase,
260 "hidden": self.ctx_hidden,
260 "hidden": self.ctx_hidden,
261 "_file_paths": self.ctx_list,
261 "_file_paths": self.ctx_list,
262 }
262 }
263 self._bulk_file_methods = {
263 self._bulk_file_methods = {
264 "size": self.fctx_size,
264 "size": self.fctx_size,
265 "data": self.fctx_node_data,
265 "data": self.fctx_node_data,
266 "flags": self.fctx_flags,
266 "flags": self.fctx_flags,
267 "is_binary": self.is_binary,
267 "is_binary": self.is_binary,
268 "md5": self.md5_hash,
268 "md5": self.md5_hash,
269 }
269 }
270
270
271 def _get_ctx(self, repo, ref):
271 def _get_ctx(self, repo, ref):
272 return get_ctx(repo, ref)
272 return get_ctx(repo, ref)
273
273
274 @reraise_safe_exceptions
274 @reraise_safe_exceptions
275 def discover_hg_version(self):
275 def discover_hg_version(self):
276 from mercurial import util
276 from mercurial import util
277 return safe_str(util.version())
277 return safe_str(util.version())
278
278
279 @reraise_safe_exceptions
279 @reraise_safe_exceptions
280 def is_empty(self, wire):
280 def is_empty(self, wire):
281 repo = self._factory.repo(wire)
281 repo = self._factory.repo(wire)
282
282
283 try:
283 try:
284 return len(repo) == 0
284 return len(repo) == 0
285 except Exception:
285 except Exception:
286 log.exception("failed to read object_store")
286 log.exception("failed to read object_store")
287 return False
287 return False
288
288
289 @reraise_safe_exceptions
289 @reraise_safe_exceptions
290 def bookmarks(self, wire):
290 def bookmarks(self, wire):
291 cache_on, context_uid, repo_id = self._cache_on(wire)
291 cache_on, context_uid, repo_id = self._cache_on(wire)
292 region = self._region(wire)
292 region = self._region(wire)
293
293
294 @region.conditional_cache_on_arguments(condition=cache_on)
294 @region.conditional_cache_on_arguments(condition=cache_on)
295 def _bookmarks(_context_uid, _repo_id):
295 def _bookmarks(_context_uid, _repo_id):
296 repo = self._factory.repo(wire)
296 repo = self._factory.repo(wire)
297 return {safe_str(name): ascii_str(hex(sha)) for name, sha in repo._bookmarks.items()}
297 return {safe_str(name): ascii_str(hex(sha)) for name, sha in repo._bookmarks.items()}
298
298
299 return _bookmarks(context_uid, repo_id)
299 return _bookmarks(context_uid, repo_id)
300
300
301 @reraise_safe_exceptions
301 @reraise_safe_exceptions
302 def branches(self, wire, normal, closed):
302 def branches(self, wire, normal, closed):
303 cache_on, context_uid, repo_id = self._cache_on(wire)
303 cache_on, context_uid, repo_id = self._cache_on(wire)
304 region = self._region(wire)
304 region = self._region(wire)
305
305
306 @region.conditional_cache_on_arguments(condition=cache_on)
306 @region.conditional_cache_on_arguments(condition=cache_on)
307 def _branches(_context_uid, _repo_id, _normal, _closed):
307 def _branches(_context_uid, _repo_id, _normal, _closed):
308 repo = self._factory.repo(wire)
308 repo = self._factory.repo(wire)
309 iter_branches = repo.branchmap().iterbranches()
309 iter_branches = repo.branchmap().iterbranches()
310 bt = {}
310 bt = {}
311 for branch_name, _heads, tip_node, is_closed in iter_branches:
311 for branch_name, _heads, tip_node, is_closed in iter_branches:
312 if normal and not is_closed:
312 if normal and not is_closed:
313 bt[safe_str(branch_name)] = ascii_str(hex(tip_node))
313 bt[safe_str(branch_name)] = ascii_str(hex(tip_node))
314 if closed and is_closed:
314 if closed and is_closed:
315 bt[safe_str(branch_name)] = ascii_str(hex(tip_node))
315 bt[safe_str(branch_name)] = ascii_str(hex(tip_node))
316
316
317 return bt
317 return bt
318
318
319 return _branches(context_uid, repo_id, normal, closed)
319 return _branches(context_uid, repo_id, normal, closed)
320
320
321 @reraise_safe_exceptions
321 @reraise_safe_exceptions
322 def bulk_request(self, wire, commit_id, pre_load):
322 def bulk_request(self, wire, commit_id, pre_load):
323 cache_on, context_uid, repo_id = self._cache_on(wire)
323 cache_on, context_uid, repo_id = self._cache_on(wire)
324 region = self._region(wire)
324 region = self._region(wire)
325
325
326 @region.conditional_cache_on_arguments(condition=cache_on)
326 @region.conditional_cache_on_arguments(condition=cache_on)
327 def _bulk_request(_repo_id, _commit_id, _pre_load):
327 def _bulk_request(_repo_id, _commit_id, _pre_load):
328 result = {}
328 result = {}
329 for attr in pre_load:
329 for attr in pre_load:
330 try:
330 try:
331 method = self._bulk_methods[attr]
331 method = self._bulk_methods[attr]
332 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
332 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
333 result[attr] = method(wire, commit_id)
333 result[attr] = method(wire, commit_id)
334 except KeyError as e:
334 except KeyError as e:
335 raise exceptions.VcsException(e)(
335 raise exceptions.VcsException(e)(
336 f'Unknown bulk attribute: "{attr}"')
336 f'Unknown bulk attribute: "{attr}"')
337 return result
337 return result
338
338
339 return _bulk_request(repo_id, commit_id, sorted(pre_load))
339 return _bulk_request(repo_id, commit_id, sorted(pre_load))
340
340
341 @reraise_safe_exceptions
341 @reraise_safe_exceptions
342 def ctx_branch(self, wire, commit_id):
342 def ctx_branch(self, wire, commit_id):
343 cache_on, context_uid, repo_id = self._cache_on(wire)
343 cache_on, context_uid, repo_id = self._cache_on(wire)
344 region = self._region(wire)
344 region = self._region(wire)
345
345
346 @region.conditional_cache_on_arguments(condition=cache_on)
346 @region.conditional_cache_on_arguments(condition=cache_on)
347 def _ctx_branch(_repo_id, _commit_id):
347 def _ctx_branch(_repo_id, _commit_id):
348 repo = self._factory.repo(wire)
348 repo = self._factory.repo(wire)
349 ctx = self._get_ctx(repo, commit_id)
349 ctx = self._get_ctx(repo, commit_id)
350 return ctx.branch()
350 return ctx.branch()
351 return _ctx_branch(repo_id, commit_id)
351 return _ctx_branch(repo_id, commit_id)
352
352
353 @reraise_safe_exceptions
353 @reraise_safe_exceptions
354 def ctx_date(self, wire, commit_id):
354 def ctx_date(self, wire, commit_id):
355 cache_on, context_uid, repo_id = self._cache_on(wire)
355 cache_on, context_uid, repo_id = self._cache_on(wire)
356 region = self._region(wire)
356 region = self._region(wire)
357
357
358 @region.conditional_cache_on_arguments(condition=cache_on)
358 @region.conditional_cache_on_arguments(condition=cache_on)
359 def _ctx_date(_repo_id, _commit_id):
359 def _ctx_date(_repo_id, _commit_id):
360 repo = self._factory.repo(wire)
360 repo = self._factory.repo(wire)
361 ctx = self._get_ctx(repo, commit_id)
361 ctx = self._get_ctx(repo, commit_id)
362 return ctx.date()
362 return ctx.date()
363 return _ctx_date(repo_id, commit_id)
363 return _ctx_date(repo_id, commit_id)
364
364
365 @reraise_safe_exceptions
365 @reraise_safe_exceptions
366 def ctx_description(self, wire, revision):
366 def ctx_description(self, wire, revision):
367 repo = self._factory.repo(wire)
367 repo = self._factory.repo(wire)
368 ctx = self._get_ctx(repo, revision)
368 ctx = self._get_ctx(repo, revision)
369 return ctx.description()
369 return ctx.description()
370
370
371 @reraise_safe_exceptions
371 @reraise_safe_exceptions
372 def ctx_files(self, wire, commit_id):
372 def ctx_files(self, wire, commit_id):
373 cache_on, context_uid, repo_id = self._cache_on(wire)
373 cache_on, context_uid, repo_id = self._cache_on(wire)
374 region = self._region(wire)
374 region = self._region(wire)
375
375
376 @region.conditional_cache_on_arguments(condition=cache_on)
376 @region.conditional_cache_on_arguments(condition=cache_on)
377 def _ctx_files(_repo_id, _commit_id):
377 def _ctx_files(_repo_id, _commit_id):
378 repo = self._factory.repo(wire)
378 repo = self._factory.repo(wire)
379 ctx = self._get_ctx(repo, commit_id)
379 ctx = self._get_ctx(repo, commit_id)
380 return ctx.files()
380 return ctx.files()
381
381
382 return _ctx_files(repo_id, commit_id)
382 return _ctx_files(repo_id, commit_id)
383
383
384 @reraise_safe_exceptions
384 @reraise_safe_exceptions
385 def ctx_list(self, path, revision):
385 def ctx_list(self, path, revision):
386 repo = self._factory.repo(path)
386 repo = self._factory.repo(path)
387 ctx = self._get_ctx(repo, revision)
387 ctx = self._get_ctx(repo, revision)
388 return list(ctx)
388 return list(ctx)
389
389
390 @reraise_safe_exceptions
390 @reraise_safe_exceptions
391 def ctx_parents(self, wire, commit_id):
391 def ctx_parents(self, wire, commit_id):
392 cache_on, context_uid, repo_id = self._cache_on(wire)
392 cache_on, context_uid, repo_id = self._cache_on(wire)
393 region = self._region(wire)
393 region = self._region(wire)
394
394
395 @region.conditional_cache_on_arguments(condition=cache_on)
395 @region.conditional_cache_on_arguments(condition=cache_on)
396 def _ctx_parents(_repo_id, _commit_id):
396 def _ctx_parents(_repo_id, _commit_id):
397 repo = self._factory.repo(wire)
397 repo = self._factory.repo(wire)
398 ctx = self._get_ctx(repo, commit_id)
398 ctx = self._get_ctx(repo, commit_id)
399 return [parent.hex() for parent in ctx.parents()
399 return [parent.hex() for parent in ctx.parents()
400 if not (parent.hidden() or parent.obsolete())]
400 if not (parent.hidden() or parent.obsolete())]
401
401
402 return _ctx_parents(repo_id, commit_id)
402 return _ctx_parents(repo_id, commit_id)
403
403
404 @reraise_safe_exceptions
404 @reraise_safe_exceptions
405 def ctx_children(self, wire, commit_id):
405 def ctx_children(self, wire, commit_id):
406 cache_on, context_uid, repo_id = self._cache_on(wire)
406 cache_on, context_uid, repo_id = self._cache_on(wire)
407 region = self._region(wire)
407 region = self._region(wire)
408
408
409 @region.conditional_cache_on_arguments(condition=cache_on)
409 @region.conditional_cache_on_arguments(condition=cache_on)
410 def _ctx_children(_repo_id, _commit_id):
410 def _ctx_children(_repo_id, _commit_id):
411 repo = self._factory.repo(wire)
411 repo = self._factory.repo(wire)
412 ctx = self._get_ctx(repo, commit_id)
412 ctx = self._get_ctx(repo, commit_id)
413 return [child.hex() for child in ctx.children()
413 return [child.hex() for child in ctx.children()
414 if not (child.hidden() or child.obsolete())]
414 if not (child.hidden() or child.obsolete())]
415
415
416 return _ctx_children(repo_id, commit_id)
416 return _ctx_children(repo_id, commit_id)
417
417
418 @reraise_safe_exceptions
418 @reraise_safe_exceptions
419 def ctx_phase(self, wire, commit_id):
419 def ctx_phase(self, wire, commit_id):
420 cache_on, context_uid, repo_id = self._cache_on(wire)
420 cache_on, context_uid, repo_id = self._cache_on(wire)
421 region = self._region(wire)
421 region = self._region(wire)
422
422
423 @region.conditional_cache_on_arguments(condition=cache_on)
423 @region.conditional_cache_on_arguments(condition=cache_on)
424 def _ctx_phase(_context_uid, _repo_id, _commit_id):
424 def _ctx_phase(_context_uid, _repo_id, _commit_id):
425 repo = self._factory.repo(wire)
425 repo = self._factory.repo(wire)
426 ctx = self._get_ctx(repo, commit_id)
426 ctx = self._get_ctx(repo, commit_id)
427 # public=0, draft=1, secret=3
427 # public=0, draft=1, secret=3
428 return ctx.phase()
428 return ctx.phase()
429 return _ctx_phase(context_uid, repo_id, commit_id)
429 return _ctx_phase(context_uid, repo_id, commit_id)
430
430
431 @reraise_safe_exceptions
431 @reraise_safe_exceptions
432 def ctx_obsolete(self, wire, commit_id):
432 def ctx_obsolete(self, wire, commit_id):
433 cache_on, context_uid, repo_id = self._cache_on(wire)
433 cache_on, context_uid, repo_id = self._cache_on(wire)
434 region = self._region(wire)
434 region = self._region(wire)
435
435
436 @region.conditional_cache_on_arguments(condition=cache_on)
436 @region.conditional_cache_on_arguments(condition=cache_on)
437 def _ctx_obsolete(_context_uid, _repo_id, _commit_id):
437 def _ctx_obsolete(_context_uid, _repo_id, _commit_id):
438 repo = self._factory.repo(wire)
438 repo = self._factory.repo(wire)
439 ctx = self._get_ctx(repo, commit_id)
439 ctx = self._get_ctx(repo, commit_id)
440 return ctx.obsolete()
440 return ctx.obsolete()
441 return _ctx_obsolete(context_uid, repo_id, commit_id)
441 return _ctx_obsolete(context_uid, repo_id, commit_id)
442
442
443 @reraise_safe_exceptions
443 @reraise_safe_exceptions
444 def ctx_hidden(self, wire, commit_id):
444 def ctx_hidden(self, wire, commit_id):
445 cache_on, context_uid, repo_id = self._cache_on(wire)
445 cache_on, context_uid, repo_id = self._cache_on(wire)
446 region = self._region(wire)
446 region = self._region(wire)
447
447
448 @region.conditional_cache_on_arguments(condition=cache_on)
448 @region.conditional_cache_on_arguments(condition=cache_on)
449 def _ctx_hidden(_context_uid, _repo_id, _commit_id):
449 def _ctx_hidden(_context_uid, _repo_id, _commit_id):
450 repo = self._factory.repo(wire)
450 repo = self._factory.repo(wire)
451 ctx = self._get_ctx(repo, commit_id)
451 ctx = self._get_ctx(repo, commit_id)
452 return ctx.hidden()
452 return ctx.hidden()
453 return _ctx_hidden(context_uid, repo_id, commit_id)
453 return _ctx_hidden(context_uid, repo_id, commit_id)
454
454
455 @reraise_safe_exceptions
455 @reraise_safe_exceptions
456 def ctx_substate(self, wire, revision):
456 def ctx_substate(self, wire, revision):
457 repo = self._factory.repo(wire)
457 repo = self._factory.repo(wire)
458 ctx = self._get_ctx(repo, revision)
458 ctx = self._get_ctx(repo, revision)
459 return ctx.substate
459 return ctx.substate
460
460
461 @reraise_safe_exceptions
461 @reraise_safe_exceptions
462 def ctx_status(self, wire, revision):
462 def ctx_status(self, wire, revision):
463 repo = self._factory.repo(wire)
463 repo = self._factory.repo(wire)
464 ctx = self._get_ctx(repo, revision)
464 ctx = self._get_ctx(repo, revision)
465 status = repo[ctx.p1().node()].status(other=ctx.node())
465 status = repo[ctx.p1().node()].status(other=ctx.node())
466 # object of status (odd, custom named tuple in mercurial) is not
466 # object of status (odd, custom named tuple in mercurial) is not
467 # correctly serializable, we make it a list, as the underling
467 # correctly serializable, we make it a list, as the underling
468 # API expects this to be a list
468 # API expects this to be a list
469 return list(status)
469 return list(status)
470
470
471 @reraise_safe_exceptions
471 @reraise_safe_exceptions
472 def ctx_user(self, wire, revision):
472 def ctx_user(self, wire, revision):
473 repo = self._factory.repo(wire)
473 repo = self._factory.repo(wire)
474 ctx = self._get_ctx(repo, revision)
474 ctx = self._get_ctx(repo, revision)
475 return ctx.user()
475 return ctx.user()
476
476
477 @reraise_safe_exceptions
477 @reraise_safe_exceptions
478 def check_url(self, url, config):
478 def check_url(self, url, config):
479 url, _proto = normalize_url_for_hg(url)
479 url, _proto = normalize_url_for_hg(url)
480 url_obj = url_parser(safe_bytes(url))
480 url_obj = url_parser(safe_bytes(url))
481
481
482 test_uri = safe_str(url_obj.authinfo()[0])
482 test_uri = safe_str(url_obj.authinfo()[0])
483 authinfo = url_obj.authinfo()[1]
483 authinfo = url_obj.authinfo()[1]
484 obfuscated_uri = get_obfuscated_url(url_obj)
484 obfuscated_uri = get_obfuscated_url(url_obj)
485 log.info("Checking URL for remote cloning/import: %s", obfuscated_uri)
485 log.info("Checking URL for remote cloning/import: %s", obfuscated_uri)
486
486
487 handlers = []
487 handlers = []
488 if authinfo:
488 if authinfo:
489 # create a password manager
489 # create a password manager
490 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
490 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
491 passmgr.add_password(*authinfo)
491 passmgr.add_password(*authinfo)
492
492
493 handlers.extend((httpbasicauthhandler(passmgr),
493 handlers.extend((httpbasicauthhandler(passmgr),
494 httpdigestauthhandler(passmgr)))
494 httpdigestauthhandler(passmgr)))
495
495
496 o = urllib.request.build_opener(*handlers)
496 o = urllib.request.build_opener(*handlers)
497 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
497 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
498 ('Accept', 'application/mercurial-0.1')]
498 ('Accept', 'application/mercurial-0.1')]
499
499
500 q = {"cmd": 'between'}
500 q = {"cmd": 'between'}
501 q.update({'pairs': "{}-{}".format('0' * 40, '0' * 40)})
501 q.update({'pairs': "{}-{}".format('0' * 40, '0' * 40)})
502 qs = f'?{urllib.parse.urlencode(q)}'
502 qs = f'?{urllib.parse.urlencode(q)}'
503 cu = f"{test_uri}{qs}"
503 cu = f"{test_uri}{qs}"
504 req = urllib.request.Request(cu, None, {})
505
504
506 try:
505 try:
506 req = urllib.request.Request(cu, None, {})
507 log.debug("Trying to open URL %s", obfuscated_uri)
507 log.debug("Trying to open URL %s", obfuscated_uri)
508 resp = o.open(req)
508 resp = o.open(req)
509 if resp.code != 200:
509 if resp.code != 200:
510 raise exceptions.URLError()('Return Code is not 200')
510 raise exceptions.URLError()('Return Code is not 200')
511 except Exception as e:
511 except Exception as e:
512 log.warning("URL cannot be opened: %s", obfuscated_uri, exc_info=True)
512 log.warning("URL cannot be opened: %s", obfuscated_uri, exc_info=True)
513 # means it cannot be cloned
513 # means it cannot be cloned
514 raise exceptions.URLError(e)(f"[{obfuscated_uri}] org_exc: {e}")
514 raise exceptions.URLError(e)(f"[{obfuscated_uri}] org_exc: {e}")
515
515
516 # now check if it's a proper hg repo, but don't do it for svn
516 # now check if it's a proper hg repo, but don't do it for svn
517 try:
517 try:
518 if _proto == 'svn':
518 if _proto == 'svn':
519 pass
519 pass
520 else:
520 else:
521 # check for pure hg repos
521 # check for pure hg repos
522 log.debug(
522 log.debug(
523 "Verifying if URL is a Mercurial repository: %s", obfuscated_uri)
523 "Verifying if URL is a Mercurial repository: %s", obfuscated_uri)
524 ui = make_ui_from_config(config)
524 ui = make_ui_from_config(config)
525 peer_checker = makepeer(ui, safe_bytes(url))
525 peer_checker = makepeer(ui, safe_bytes(url))
526 peer_checker.lookup(b'tip')
526 peer_checker.lookup(b'tip')
527 except Exception as e:
527 except Exception as e:
528 log.warning("URL is not a valid Mercurial repository: %s",
528 log.warning("URL is not a valid Mercurial repository: %s",
529 obfuscated_uri)
529 obfuscated_uri)
530 raise exceptions.URLError(e)(
530 raise exceptions.URLError(e)(
531 f"url [{obfuscated_uri}] does not look like an hg repo org_exc: {e}")
531 f"url [{obfuscated_uri}] does not look like an hg repo org_exc: {e}")
532
532
533 log.info("URL is a valid Mercurial repository: %s", obfuscated_uri)
533 log.info("URL is a valid Mercurial repository: %s", obfuscated_uri)
534 return True
534 return True
535
535
536 @reraise_safe_exceptions
536 @reraise_safe_exceptions
537 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_git, opt_ignorews, context):
537 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_git, opt_ignorews, context):
538 repo = self._factory.repo(wire)
538 repo = self._factory.repo(wire)
539
539
540 if file_filter:
540 if file_filter:
541 # unpack the file-filter
541 # unpack the file-filter
542 repo_path, node_path = file_filter
542 repo_path, node_path = file_filter
543 match_filter = match(safe_bytes(repo_path), b'', [safe_bytes(node_path)])
543 match_filter = match(safe_bytes(repo_path), b'', [safe_bytes(node_path)])
544 else:
544 else:
545 match_filter = file_filter
545 match_filter = file_filter
546 opts = diffopts(git=opt_git, ignorews=opt_ignorews, context=context, showfunc=1)
546 opts = diffopts(git=opt_git, ignorews=opt_ignorews, context=context, showfunc=1)
547
547
548 try:
548 try:
549 diff_iter = patch.diff(
549 diff_iter = patch.diff(
550 repo, node1=commit_id_1, node2=commit_id_2, match=match_filter, opts=opts)
550 repo, node1=commit_id_1, node2=commit_id_2, match=match_filter, opts=opts)
551 return BytesEnvelope(b"".join(diff_iter))
551 return BytesEnvelope(b"".join(diff_iter))
552 except RepoLookupError as e:
552 except RepoLookupError as e:
553 raise exceptions.LookupException(e)()
553 raise exceptions.LookupException(e)()
554
554
555 @reraise_safe_exceptions
555 @reraise_safe_exceptions
556 def node_history(self, wire, revision, path, limit):
556 def node_history(self, wire, revision, path, limit):
557 cache_on, context_uid, repo_id = self._cache_on(wire)
557 cache_on, context_uid, repo_id = self._cache_on(wire)
558 region = self._region(wire)
558 region = self._region(wire)
559
559
560 @region.conditional_cache_on_arguments(condition=cache_on)
560 @region.conditional_cache_on_arguments(condition=cache_on)
561 def _node_history(_context_uid, _repo_id, _revision, _path, _limit):
561 def _node_history(_context_uid, _repo_id, _revision, _path, _limit):
562 repo = self._factory.repo(wire)
562 repo = self._factory.repo(wire)
563
563
564 ctx = self._get_ctx(repo, revision)
564 ctx = self._get_ctx(repo, revision)
565 fctx = ctx.filectx(safe_bytes(path))
565 fctx = ctx.filectx(safe_bytes(path))
566
566
567 def history_iter():
567 def history_iter():
568 limit_rev = fctx.rev()
568 limit_rev = fctx.rev()
569
569
570 for fctx_candidate in reversed(list(fctx.filelog())):
570 for fctx_candidate in reversed(list(fctx.filelog())):
571 f_obj = fctx.filectx(fctx_candidate)
571 f_obj = fctx.filectx(fctx_candidate)
572
572
573 # NOTE: This can be problematic...we can hide ONLY history node resulting in empty history
573 # NOTE: This can be problematic...we can hide ONLY history node resulting in empty history
574 _ctx = f_obj.changectx()
574 _ctx = f_obj.changectx()
575 if _ctx.hidden() or _ctx.obsolete():
575 if _ctx.hidden() or _ctx.obsolete():
576 continue
576 continue
577
577
578 if limit_rev >= f_obj.rev():
578 if limit_rev >= f_obj.rev():
579 yield f_obj
579 yield f_obj
580
580
581 history = []
581 history = []
582 for cnt, obj in enumerate(history_iter()):
582 for cnt, obj in enumerate(history_iter()):
583 if limit and cnt >= limit:
583 if limit and cnt >= limit:
584 break
584 break
585 history.append(hex(obj.node()))
585 history.append(hex(obj.node()))
586
586
587 return [x for x in history]
587 return [x for x in history]
588 return _node_history(context_uid, repo_id, revision, path, limit)
588 return _node_history(context_uid, repo_id, revision, path, limit)
589
589
590 @reraise_safe_exceptions
590 @reraise_safe_exceptions
591 def node_history_until(self, wire, revision, path, limit):
591 def node_history_until(self, wire, revision, path, limit):
592 cache_on, context_uid, repo_id = self._cache_on(wire)
592 cache_on, context_uid, repo_id = self._cache_on(wire)
593 region = self._region(wire)
593 region = self._region(wire)
594
594
595 @region.conditional_cache_on_arguments(condition=cache_on)
595 @region.conditional_cache_on_arguments(condition=cache_on)
596 def _node_history_until(_context_uid, _repo_id):
596 def _node_history_until(_context_uid, _repo_id):
597 repo = self._factory.repo(wire)
597 repo = self._factory.repo(wire)
598 ctx = self._get_ctx(repo, revision)
598 ctx = self._get_ctx(repo, revision)
599 fctx = ctx.filectx(safe_bytes(path))
599 fctx = ctx.filectx(safe_bytes(path))
600
600
601 file_log = list(fctx.filelog())
601 file_log = list(fctx.filelog())
602 if limit:
602 if limit:
603 # Limit to the last n items
603 # Limit to the last n items
604 file_log = file_log[-limit:]
604 file_log = file_log[-limit:]
605
605
606 return [hex(fctx.filectx(cs).node()) for cs in reversed(file_log)]
606 return [hex(fctx.filectx(cs).node()) for cs in reversed(file_log)]
607 return _node_history_until(context_uid, repo_id, revision, path, limit)
607 return _node_history_until(context_uid, repo_id, revision, path, limit)
608
608
609 @reraise_safe_exceptions
609 @reraise_safe_exceptions
610 def bulk_file_request(self, wire, commit_id, path, pre_load):
610 def bulk_file_request(self, wire, commit_id, path, pre_load):
611 cache_on, context_uid, repo_id = self._cache_on(wire)
611 cache_on, context_uid, repo_id = self._cache_on(wire)
612 region = self._region(wire)
612 region = self._region(wire)
613
613
614 @region.conditional_cache_on_arguments(condition=cache_on)
614 @region.conditional_cache_on_arguments(condition=cache_on)
615 def _bulk_file_request(_repo_id, _commit_id, _path, _pre_load):
615 def _bulk_file_request(_repo_id, _commit_id, _path, _pre_load):
616 result = {}
616 result = {}
617 for attr in pre_load:
617 for attr in pre_load:
618 try:
618 try:
619 method = self._bulk_file_methods[attr]
619 method = self._bulk_file_methods[attr]
620 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
620 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
621 result[attr] = method(wire, _commit_id, _path)
621 result[attr] = method(wire, _commit_id, _path)
622 except KeyError as e:
622 except KeyError as e:
623 raise exceptions.VcsException(e)(f'Unknown bulk attribute: "{attr}"')
623 raise exceptions.VcsException(e)(f'Unknown bulk attribute: "{attr}"')
624 return result
624 return result
625
625
626 return BinaryEnvelope(_bulk_file_request(repo_id, commit_id, path, sorted(pre_load)))
626 return BinaryEnvelope(_bulk_file_request(repo_id, commit_id, path, sorted(pre_load)))
627
627
628 @reraise_safe_exceptions
628 @reraise_safe_exceptions
629 def fctx_annotate(self, wire, revision, path):
629 def fctx_annotate(self, wire, revision, path):
630 repo = self._factory.repo(wire)
630 repo = self._factory.repo(wire)
631 ctx = self._get_ctx(repo, revision)
631 ctx = self._get_ctx(repo, revision)
632 fctx = ctx.filectx(safe_bytes(path))
632 fctx = ctx.filectx(safe_bytes(path))
633
633
634 result = []
634 result = []
635 for i, annotate_obj in enumerate(fctx.annotate(), 1):
635 for i, annotate_obj in enumerate(fctx.annotate(), 1):
636 ln_no = i
636 ln_no = i
637 sha = hex(annotate_obj.fctx.node())
637 sha = hex(annotate_obj.fctx.node())
638 content = annotate_obj.text
638 content = annotate_obj.text
639 result.append((ln_no, ascii_str(sha), content))
639 result.append((ln_no, ascii_str(sha), content))
640 return BinaryEnvelope(result)
640 return BinaryEnvelope(result)
641
641
642 @reraise_safe_exceptions
642 @reraise_safe_exceptions
643 def fctx_node_data(self, wire, revision, path):
643 def fctx_node_data(self, wire, revision, path):
644 repo = self._factory.repo(wire)
644 repo = self._factory.repo(wire)
645 ctx = self._get_ctx(repo, revision)
645 ctx = self._get_ctx(repo, revision)
646 fctx = ctx.filectx(safe_bytes(path))
646 fctx = ctx.filectx(safe_bytes(path))
647 return BytesEnvelope(fctx.data())
647 return BytesEnvelope(fctx.data())
648
648
649 @reraise_safe_exceptions
649 @reraise_safe_exceptions
650 def fctx_flags(self, wire, commit_id, path):
650 def fctx_flags(self, wire, commit_id, path):
651 cache_on, context_uid, repo_id = self._cache_on(wire)
651 cache_on, context_uid, repo_id = self._cache_on(wire)
652 region = self._region(wire)
652 region = self._region(wire)
653
653
654 @region.conditional_cache_on_arguments(condition=cache_on)
654 @region.conditional_cache_on_arguments(condition=cache_on)
655 def _fctx_flags(_repo_id, _commit_id, _path):
655 def _fctx_flags(_repo_id, _commit_id, _path):
656 repo = self._factory.repo(wire)
656 repo = self._factory.repo(wire)
657 ctx = self._get_ctx(repo, commit_id)
657 ctx = self._get_ctx(repo, commit_id)
658 fctx = ctx.filectx(safe_bytes(path))
658 fctx = ctx.filectx(safe_bytes(path))
659 return fctx.flags()
659 return fctx.flags()
660
660
661 return _fctx_flags(repo_id, commit_id, path)
661 return _fctx_flags(repo_id, commit_id, path)
662
662
663 @reraise_safe_exceptions
663 @reraise_safe_exceptions
664 def fctx_size(self, wire, commit_id, path):
664 def fctx_size(self, wire, commit_id, path):
665 cache_on, context_uid, repo_id = self._cache_on(wire)
665 cache_on, context_uid, repo_id = self._cache_on(wire)
666 region = self._region(wire)
666 region = self._region(wire)
667
667
668 @region.conditional_cache_on_arguments(condition=cache_on)
668 @region.conditional_cache_on_arguments(condition=cache_on)
669 def _fctx_size(_repo_id, _revision, _path):
669 def _fctx_size(_repo_id, _revision, _path):
670 repo = self._factory.repo(wire)
670 repo = self._factory.repo(wire)
671 ctx = self._get_ctx(repo, commit_id)
671 ctx = self._get_ctx(repo, commit_id)
672 fctx = ctx.filectx(safe_bytes(path))
672 fctx = ctx.filectx(safe_bytes(path))
673 return fctx.size()
673 return fctx.size()
674 return _fctx_size(repo_id, commit_id, path)
674 return _fctx_size(repo_id, commit_id, path)
675
675
676 @reraise_safe_exceptions
676 @reraise_safe_exceptions
677 def get_all_commit_ids(self, wire, name):
677 def get_all_commit_ids(self, wire, name):
678 cache_on, context_uid, repo_id = self._cache_on(wire)
678 cache_on, context_uid, repo_id = self._cache_on(wire)
679 region = self._region(wire)
679 region = self._region(wire)
680
680
681 @region.conditional_cache_on_arguments(condition=cache_on)
681 @region.conditional_cache_on_arguments(condition=cache_on)
682 def _get_all_commit_ids(_context_uid, _repo_id, _name):
682 def _get_all_commit_ids(_context_uid, _repo_id, _name):
683 repo = self._factory.repo(wire)
683 repo = self._factory.repo(wire)
684 revs = [ascii_str(repo[x].hex()) for x in repo.filtered(b'visible').changelog.revs()]
684 revs = [ascii_str(repo[x].hex()) for x in repo.filtered(b'visible').changelog.revs()]
685 return revs
685 return revs
686 return _get_all_commit_ids(context_uid, repo_id, name)
686 return _get_all_commit_ids(context_uid, repo_id, name)
687
687
688 @reraise_safe_exceptions
688 @reraise_safe_exceptions
689 def get_config_value(self, wire, section, name, untrusted=False):
689 def get_config_value(self, wire, section, name, untrusted=False):
690 repo = self._factory.repo(wire)
690 repo = self._factory.repo(wire)
691 return repo.ui.config(ascii_bytes(section), ascii_bytes(name), untrusted=untrusted)
691 return repo.ui.config(ascii_bytes(section), ascii_bytes(name), untrusted=untrusted)
692
692
693 @reraise_safe_exceptions
693 @reraise_safe_exceptions
694 def is_large_file(self, wire, commit_id, path):
694 def is_large_file(self, wire, commit_id, path):
695 cache_on, context_uid, repo_id = self._cache_on(wire)
695 cache_on, context_uid, repo_id = self._cache_on(wire)
696 region = self._region(wire)
696 region = self._region(wire)
697
697
698 @region.conditional_cache_on_arguments(condition=cache_on)
698 @region.conditional_cache_on_arguments(condition=cache_on)
699 def _is_large_file(_context_uid, _repo_id, _commit_id, _path):
699 def _is_large_file(_context_uid, _repo_id, _commit_id, _path):
700 return largefiles.lfutil.isstandin(safe_bytes(path))
700 return largefiles.lfutil.isstandin(safe_bytes(path))
701
701
702 return _is_large_file(context_uid, repo_id, commit_id, path)
702 return _is_large_file(context_uid, repo_id, commit_id, path)
703
703
704 @reraise_safe_exceptions
704 @reraise_safe_exceptions
705 def is_binary(self, wire, revision, path):
705 def is_binary(self, wire, revision, path):
706 cache_on, context_uid, repo_id = self._cache_on(wire)
706 cache_on, context_uid, repo_id = self._cache_on(wire)
707 region = self._region(wire)
707 region = self._region(wire)
708
708
709 @region.conditional_cache_on_arguments(condition=cache_on)
709 @region.conditional_cache_on_arguments(condition=cache_on)
710 def _is_binary(_repo_id, _sha, _path):
710 def _is_binary(_repo_id, _sha, _path):
711 repo = self._factory.repo(wire)
711 repo = self._factory.repo(wire)
712 ctx = self._get_ctx(repo, revision)
712 ctx = self._get_ctx(repo, revision)
713 fctx = ctx.filectx(safe_bytes(path))
713 fctx = ctx.filectx(safe_bytes(path))
714 return fctx.isbinary()
714 return fctx.isbinary()
715
715
716 return _is_binary(repo_id, revision, path)
716 return _is_binary(repo_id, revision, path)
717
717
718 @reraise_safe_exceptions
718 @reraise_safe_exceptions
719 def md5_hash(self, wire, revision, path):
719 def md5_hash(self, wire, revision, path):
720 cache_on, context_uid, repo_id = self._cache_on(wire)
720 cache_on, context_uid, repo_id = self._cache_on(wire)
721 region = self._region(wire)
721 region = self._region(wire)
722
722
723 @region.conditional_cache_on_arguments(condition=cache_on)
723 @region.conditional_cache_on_arguments(condition=cache_on)
724 def _md5_hash(_repo_id, _sha, _path):
724 def _md5_hash(_repo_id, _sha, _path):
725 repo = self._factory.repo(wire)
725 repo = self._factory.repo(wire)
726 ctx = self._get_ctx(repo, revision)
726 ctx = self._get_ctx(repo, revision)
727 fctx = ctx.filectx(safe_bytes(path))
727 fctx = ctx.filectx(safe_bytes(path))
728 return hashlib.md5(fctx.data()).hexdigest()
728 return hashlib.md5(fctx.data()).hexdigest()
729
729
730 return _md5_hash(repo_id, revision, path)
730 return _md5_hash(repo_id, revision, path)
731
731
732 @reraise_safe_exceptions
732 @reraise_safe_exceptions
733 def in_largefiles_store(self, wire, sha):
733 def in_largefiles_store(self, wire, sha):
734 repo = self._factory.repo(wire)
734 repo = self._factory.repo(wire)
735 return largefiles.lfutil.instore(repo, sha)
735 return largefiles.lfutil.instore(repo, sha)
736
736
737 @reraise_safe_exceptions
737 @reraise_safe_exceptions
738 def in_user_cache(self, wire, sha):
738 def in_user_cache(self, wire, sha):
739 repo = self._factory.repo(wire)
739 repo = self._factory.repo(wire)
740 return largefiles.lfutil.inusercache(repo.ui, sha)
740 return largefiles.lfutil.inusercache(repo.ui, sha)
741
741
742 @reraise_safe_exceptions
742 @reraise_safe_exceptions
743 def store_path(self, wire, sha):
743 def store_path(self, wire, sha):
744 repo = self._factory.repo(wire)
744 repo = self._factory.repo(wire)
745 return largefiles.lfutil.storepath(repo, sha)
745 return largefiles.lfutil.storepath(repo, sha)
746
746
747 @reraise_safe_exceptions
747 @reraise_safe_exceptions
748 def link(self, wire, sha, path):
748 def link(self, wire, sha, path):
749 repo = self._factory.repo(wire)
749 repo = self._factory.repo(wire)
750 largefiles.lfutil.link(
750 largefiles.lfutil.link(
751 largefiles.lfutil.usercachepath(repo.ui, sha), path)
751 largefiles.lfutil.usercachepath(repo.ui, sha), path)
752
752
753 @reraise_safe_exceptions
753 @reraise_safe_exceptions
754 def localrepository(self, wire, create=False):
754 def localrepository(self, wire, create=False):
755 self._factory.repo(wire, create=create)
755 self._factory.repo(wire, create=create)
756
756
757 @reraise_safe_exceptions
757 @reraise_safe_exceptions
758 def lookup(self, wire, revision, both):
758 def lookup(self, wire, revision, both):
759 cache_on, context_uid, repo_id = self._cache_on(wire)
759 cache_on, context_uid, repo_id = self._cache_on(wire)
760 region = self._region(wire)
760 region = self._region(wire)
761
761
762 @region.conditional_cache_on_arguments(condition=cache_on)
762 @region.conditional_cache_on_arguments(condition=cache_on)
763 def _lookup(_context_uid, _repo_id, _revision, _both):
763 def _lookup(_context_uid, _repo_id, _revision, _both):
764 repo = self._factory.repo(wire)
764 repo = self._factory.repo(wire)
765 rev = _revision
765 rev = _revision
766 if isinstance(rev, int):
766 if isinstance(rev, int):
767 # NOTE(marcink):
767 # NOTE(marcink):
768 # since Mercurial doesn't support negative indexes properly
768 # since Mercurial doesn't support negative indexes properly
769 # we need to shift accordingly by one to get proper index, e.g
769 # we need to shift accordingly by one to get proper index, e.g
770 # repo[-1] => repo[-2]
770 # repo[-1] => repo[-2]
771 # repo[0] => repo[-1]
771 # repo[0] => repo[-1]
772 if rev <= 0:
772 if rev <= 0:
773 rev = rev + -1
773 rev = rev + -1
774 try:
774 try:
775 ctx = self._get_ctx(repo, rev)
775 ctx = self._get_ctx(repo, rev)
776 except AmbiguousPrefixLookupError:
776 except AmbiguousPrefixLookupError:
777 e = RepoLookupError(rev)
777 e = RepoLookupError(rev)
778 e._org_exc_tb = format_exc(sys.exc_info())
778 e._org_exc_tb = format_exc(sys.exc_info())
779 raise exceptions.LookupException(e)(rev)
779 raise exceptions.LookupException(e)(rev)
780 except (TypeError, RepoLookupError, binascii.Error) as e:
780 except (TypeError, RepoLookupError, binascii.Error) as e:
781 e._org_exc_tb = format_exc(sys.exc_info())
781 e._org_exc_tb = format_exc(sys.exc_info())
782 raise exceptions.LookupException(e)(rev)
782 raise exceptions.LookupException(e)(rev)
783 except LookupError as e:
783 except LookupError as e:
784 e._org_exc_tb = format_exc(sys.exc_info())
784 e._org_exc_tb = format_exc(sys.exc_info())
785 raise exceptions.LookupException(e)(e.name)
785 raise exceptions.LookupException(e)(e.name)
786
786
787 if not both:
787 if not both:
788 return ctx.hex()
788 return ctx.hex()
789
789
790 ctx = repo[ctx.hex()]
790 ctx = repo[ctx.hex()]
791 return ctx.hex(), ctx.rev()
791 return ctx.hex(), ctx.rev()
792
792
793 return _lookup(context_uid, repo_id, revision, both)
793 return _lookup(context_uid, repo_id, revision, both)
794
794
795 @reraise_safe_exceptions
795 @reraise_safe_exceptions
796 def sync_push(self, wire, url):
796 def sync_push(self, wire, url):
797 if not self.check_url(url, wire['config']):
797 if not self.check_url(url, wire['config']):
798 return
798 return
799
799
800 repo = self._factory.repo(wire)
800 repo = self._factory.repo(wire)
801
801
802 # Disable any prompts for this repo
802 # Disable any prompts for this repo
803 repo.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
803 repo.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
804
804
805 bookmarks = list(dict(repo._bookmarks).keys())
805 bookmarks = list(dict(repo._bookmarks).keys())
806 remote = peer(repo, {}, safe_bytes(url))
806 remote = peer(repo, {}, safe_bytes(url))
807 # Disable any prompts for this remote
807 # Disable any prompts for this remote
808 remote.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
808 remote.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
809
809
810 return exchange.push(
810 return exchange.push(
811 repo, remote, newbranch=True, bookmarks=bookmarks).cgresult
811 repo, remote, newbranch=True, bookmarks=bookmarks).cgresult
812
812
813 @reraise_safe_exceptions
813 @reraise_safe_exceptions
814 def revision(self, wire, rev):
814 def revision(self, wire, rev):
815 repo = self._factory.repo(wire)
815 repo = self._factory.repo(wire)
816 ctx = self._get_ctx(repo, rev)
816 ctx = self._get_ctx(repo, rev)
817 return ctx.rev()
817 return ctx.rev()
818
818
819 @reraise_safe_exceptions
819 @reraise_safe_exceptions
820 def rev_range(self, wire, commit_filter):
820 def rev_range(self, wire, commit_filter):
821 cache_on, context_uid, repo_id = self._cache_on(wire)
821 cache_on, context_uid, repo_id = self._cache_on(wire)
822 region = self._region(wire)
822 region = self._region(wire)
823
823
824 @region.conditional_cache_on_arguments(condition=cache_on)
824 @region.conditional_cache_on_arguments(condition=cache_on)
825 def _rev_range(_context_uid, _repo_id, _filter):
825 def _rev_range(_context_uid, _repo_id, _filter):
826 repo = self._factory.repo(wire)
826 repo = self._factory.repo(wire)
827 revisions = [
827 revisions = [
828 ascii_str(repo[rev].hex())
828 ascii_str(repo[rev].hex())
829 for rev in revrange(repo, list(map(ascii_bytes, commit_filter)))
829 for rev in revrange(repo, list(map(ascii_bytes, commit_filter)))
830 ]
830 ]
831 return revisions
831 return revisions
832
832
833 return _rev_range(context_uid, repo_id, sorted(commit_filter))
833 return _rev_range(context_uid, repo_id, sorted(commit_filter))
834
834
835 @reraise_safe_exceptions
835 @reraise_safe_exceptions
836 def rev_range_hash(self, wire, node):
836 def rev_range_hash(self, wire, node):
837 repo = self._factory.repo(wire)
837 repo = self._factory.repo(wire)
838
838
839 def get_revs(repo, rev_opt):
839 def get_revs(repo, rev_opt):
840 if rev_opt:
840 if rev_opt:
841 revs = revrange(repo, rev_opt)
841 revs = revrange(repo, rev_opt)
842 if len(revs) == 0:
842 if len(revs) == 0:
843 return (nullrev, nullrev)
843 return (nullrev, nullrev)
844 return max(revs), min(revs)
844 return max(revs), min(revs)
845 else:
845 else:
846 return len(repo) - 1, 0
846 return len(repo) - 1, 0
847
847
848 stop, start = get_revs(repo, [node + ':'])
848 stop, start = get_revs(repo, [node + ':'])
849 revs = [ascii_str(repo[r].hex()) for r in range(start, stop + 1)]
849 revs = [ascii_str(repo[r].hex()) for r in range(start, stop + 1)]
850 return revs
850 return revs
851
851
852 @reraise_safe_exceptions
852 @reraise_safe_exceptions
853 def revs_from_revspec(self, wire, rev_spec, *args, **kwargs):
853 def revs_from_revspec(self, wire, rev_spec, *args, **kwargs):
854 org_path = safe_bytes(wire["path"])
854 org_path = safe_bytes(wire["path"])
855 other_path = safe_bytes(kwargs.pop('other_path', ''))
855 other_path = safe_bytes(kwargs.pop('other_path', ''))
856
856
857 # case when we want to compare two independent repositories
857 # case when we want to compare two independent repositories
858 if other_path and other_path != wire["path"]:
858 if other_path and other_path != wire["path"]:
859 baseui = self._factory._create_config(wire["config"])
859 baseui = self._factory._create_config(wire["config"])
860 repo = unionrepo.makeunionrepository(baseui, other_path, org_path)
860 repo = unionrepo.makeunionrepository(baseui, other_path, org_path)
861 else:
861 else:
862 repo = self._factory.repo(wire)
862 repo = self._factory.repo(wire)
863 return list(repo.revs(rev_spec, *args))
863 return list(repo.revs(rev_spec, *args))
864
864
865 @reraise_safe_exceptions
865 @reraise_safe_exceptions
866 def verify(self, wire,):
866 def verify(self, wire,):
867 repo = self._factory.repo(wire)
867 repo = self._factory.repo(wire)
868 baseui = self._factory._create_config(wire['config'])
868 baseui = self._factory._create_config(wire['config'])
869
869
870 baseui, output = patch_ui_message_output(baseui)
870 baseui, output = patch_ui_message_output(baseui)
871
871
872 repo.ui = baseui
872 repo.ui = baseui
873 verify.verify(repo)
873 verify.verify(repo)
874 return output.getvalue()
874 return output.getvalue()
875
875
876 @reraise_safe_exceptions
876 @reraise_safe_exceptions
877 def hg_update_cache(self, wire,):
877 def hg_update_cache(self, wire,):
878 repo = self._factory.repo(wire)
878 repo = self._factory.repo(wire)
879 baseui = self._factory._create_config(wire['config'])
879 baseui = self._factory._create_config(wire['config'])
880 baseui, output = patch_ui_message_output(baseui)
880 baseui, output = patch_ui_message_output(baseui)
881
881
882 repo.ui = baseui
882 repo.ui = baseui
883 with repo.wlock(), repo.lock():
883 with repo.wlock(), repo.lock():
884 repo.updatecaches(full=True)
884 repo.updatecaches(full=True)
885
885
886 return output.getvalue()
886 return output.getvalue()
887
887
888 @reraise_safe_exceptions
888 @reraise_safe_exceptions
889 def hg_rebuild_fn_cache(self, wire,):
889 def hg_rebuild_fn_cache(self, wire,):
890 repo = self._factory.repo(wire)
890 repo = self._factory.repo(wire)
891 baseui = self._factory._create_config(wire['config'])
891 baseui = self._factory._create_config(wire['config'])
892 baseui, output = patch_ui_message_output(baseui)
892 baseui, output = patch_ui_message_output(baseui)
893
893
894 repo.ui = baseui
894 repo.ui = baseui
895
895
896 repair.rebuildfncache(baseui, repo)
896 repair.rebuildfncache(baseui, repo)
897
897
898 return output.getvalue()
898 return output.getvalue()
899
899
900 @reraise_safe_exceptions
900 @reraise_safe_exceptions
901 def tags(self, wire):
901 def tags(self, wire):
902 cache_on, context_uid, repo_id = self._cache_on(wire)
902 cache_on, context_uid, repo_id = self._cache_on(wire)
903 region = self._region(wire)
903 region = self._region(wire)
904
904
905 @region.conditional_cache_on_arguments(condition=cache_on)
905 @region.conditional_cache_on_arguments(condition=cache_on)
906 def _tags(_context_uid, _repo_id):
906 def _tags(_context_uid, _repo_id):
907 repo = self._factory.repo(wire)
907 repo = self._factory.repo(wire)
908 return {safe_str(name): ascii_str(hex(sha)) for name, sha in repo.tags().items()}
908 return {safe_str(name): ascii_str(hex(sha)) for name, sha in repo.tags().items()}
909
909
910 return _tags(context_uid, repo_id)
910 return _tags(context_uid, repo_id)
911
911
912 @reraise_safe_exceptions
912 @reraise_safe_exceptions
913 def update(self, wire, node='', clean=False):
913 def update(self, wire, node='', clean=False):
914 repo = self._factory.repo(wire)
914 repo = self._factory.repo(wire)
915 baseui = self._factory._create_config(wire['config'])
915 baseui = self._factory._create_config(wire['config'])
916 node = safe_bytes(node)
916 node = safe_bytes(node)
917
917
918 commands.update(baseui, repo, node=node, clean=clean)
918 commands.update(baseui, repo, node=node, clean=clean)
919
919
920 @reraise_safe_exceptions
920 @reraise_safe_exceptions
921 def identify(self, wire):
921 def identify(self, wire):
922 repo = self._factory.repo(wire)
922 repo = self._factory.repo(wire)
923 baseui = self._factory._create_config(wire['config'])
923 baseui = self._factory._create_config(wire['config'])
924 output = io.BytesIO()
924 output = io.BytesIO()
925 baseui.write = output.write
925 baseui.write = output.write
926 # This is required to get a full node id
926 # This is required to get a full node id
927 baseui.debugflag = True
927 baseui.debugflag = True
928 commands.identify(baseui, repo, id=True)
928 commands.identify(baseui, repo, id=True)
929
929
930 return output.getvalue()
930 return output.getvalue()
931
931
932 @reraise_safe_exceptions
932 @reraise_safe_exceptions
933 def heads(self, wire, branch=None):
933 def heads(self, wire, branch=None):
934 repo = self._factory.repo(wire)
934 repo = self._factory.repo(wire)
935 baseui = self._factory._create_config(wire['config'])
935 baseui = self._factory._create_config(wire['config'])
936 output = io.BytesIO()
936 output = io.BytesIO()
937
937
938 def write(data, **unused_kwargs):
938 def write(data, **unused_kwargs):
939 output.write(data)
939 output.write(data)
940
940
941 baseui.write = write
941 baseui.write = write
942 if branch:
942 if branch:
943 args = [safe_bytes(branch)]
943 args = [safe_bytes(branch)]
944 else:
944 else:
945 args = []
945 args = []
946 commands.heads(baseui, repo, template=b'{node} ', *args)
946 commands.heads(baseui, repo, template=b'{node} ', *args)
947
947
948 return output.getvalue()
948 return output.getvalue()
949
949
950 @reraise_safe_exceptions
950 @reraise_safe_exceptions
951 def ancestor(self, wire, revision1, revision2):
951 def ancestor(self, wire, revision1, revision2):
952 repo = self._factory.repo(wire)
952 repo = self._factory.repo(wire)
953 changelog = repo.changelog
953 changelog = repo.changelog
954 lookup = repo.lookup
954 lookup = repo.lookup
955 a = changelog.ancestor(lookup(safe_bytes(revision1)), lookup(safe_bytes(revision2)))
955 a = changelog.ancestor(lookup(safe_bytes(revision1)), lookup(safe_bytes(revision2)))
956 return hex(a)
956 return hex(a)
957
957
958 @reraise_safe_exceptions
958 @reraise_safe_exceptions
959 def clone(self, wire, source, dest, update_after_clone=False, hooks=True):
959 def clone(self, wire, source, dest, update_after_clone=False, hooks=True):
960 baseui = self._factory._create_config(wire["config"], hooks=hooks)
960 baseui = self._factory._create_config(wire["config"], hooks=hooks)
961 clone(baseui, safe_bytes(source), safe_bytes(dest), noupdate=not update_after_clone)
961 clone(baseui, safe_bytes(source), safe_bytes(dest), noupdate=not update_after_clone)
962
962
963 @reraise_safe_exceptions
963 @reraise_safe_exceptions
964 def commitctx(self, wire, message, parents, commit_time, commit_timezone, user, files, extra, removed, updated):
964 def commitctx(self, wire, message, parents, commit_time, commit_timezone, user, files, extra, removed, updated):
965
965
966 repo = self._factory.repo(wire)
966 repo = self._factory.repo(wire)
967 baseui = self._factory._create_config(wire['config'])
967 baseui = self._factory._create_config(wire['config'])
968 publishing = baseui.configbool(b'phases', b'publish')
968 publishing = baseui.configbool(b'phases', b'publish')
969
969
970 def _filectxfn(_repo, ctx, path: bytes):
970 def _filectxfn(_repo, ctx, path: bytes):
971 """
971 """
972 Marks given path as added/changed/removed in a given _repo. This is
972 Marks given path as added/changed/removed in a given _repo. This is
973 for internal mercurial commit function.
973 for internal mercurial commit function.
974 """
974 """
975
975
976 # check if this path is removed
976 # check if this path is removed
977 if safe_str(path) in removed:
977 if safe_str(path) in removed:
978 # returning None is a way to mark node for removal
978 # returning None is a way to mark node for removal
979 return None
979 return None
980
980
981 # check if this path is added
981 # check if this path is added
982 for node in updated:
982 for node in updated:
983 if safe_bytes(node['path']) == path:
983 if safe_bytes(node['path']) == path:
984 return memfilectx(
984 return memfilectx(
985 _repo,
985 _repo,
986 changectx=ctx,
986 changectx=ctx,
987 path=safe_bytes(node['path']),
987 path=safe_bytes(node['path']),
988 data=safe_bytes(node['content']),
988 data=safe_bytes(node['content']),
989 islink=False,
989 islink=False,
990 isexec=bool(node['mode'] & stat.S_IXUSR),
990 isexec=bool(node['mode'] & stat.S_IXUSR),
991 copysource=False)
991 copysource=False)
992 abort_exc = exceptions.AbortException()
992 abort_exc = exceptions.AbortException()
993 raise abort_exc(f"Given path haven't been marked as added, changed or removed ({path})")
993 raise abort_exc(f"Given path haven't been marked as added, changed or removed ({path})")
994
994
995 if publishing:
995 if publishing:
996 new_commit_phase = b'public'
996 new_commit_phase = b'public'
997 else:
997 else:
998 new_commit_phase = b'draft'
998 new_commit_phase = b'draft'
999 with repo.ui.configoverride({(b'phases', b'new-commit'): new_commit_phase}):
999 with repo.ui.configoverride({(b'phases', b'new-commit'): new_commit_phase}):
1000 kwargs = {safe_bytes(k): safe_bytes(v) for k, v in extra.items()}
1000 kwargs = {safe_bytes(k): safe_bytes(v) for k, v in extra.items()}
1001 commit_ctx = memctx(
1001 commit_ctx = memctx(
1002 repo=repo,
1002 repo=repo,
1003 parents=parents,
1003 parents=parents,
1004 text=safe_bytes(message),
1004 text=safe_bytes(message),
1005 files=[safe_bytes(x) for x in files],
1005 files=[safe_bytes(x) for x in files],
1006 filectxfn=_filectxfn,
1006 filectxfn=_filectxfn,
1007 user=safe_bytes(user),
1007 user=safe_bytes(user),
1008 date=(commit_time, commit_timezone),
1008 date=(commit_time, commit_timezone),
1009 extra=kwargs)
1009 extra=kwargs)
1010
1010
1011 n = repo.commitctx(commit_ctx)
1011 n = repo.commitctx(commit_ctx)
1012 new_id = hex(n)
1012 new_id = hex(n)
1013
1013
1014 return new_id
1014 return new_id
1015
1015
1016 @reraise_safe_exceptions
1016 @reraise_safe_exceptions
1017 def pull(self, wire, url, commit_ids=None):
1017 def pull(self, wire, url, commit_ids=None):
1018 repo = self._factory.repo(wire)
1018 repo = self._factory.repo(wire)
1019 # Disable any prompts for this repo
1019 # Disable any prompts for this repo
1020 repo.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
1020 repo.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
1021
1021
1022 remote = peer(repo, {}, safe_bytes(url))
1022 remote = peer(repo, {}, safe_bytes(url))
1023 # Disable any prompts for this remote
1023 # Disable any prompts for this remote
1024 remote.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
1024 remote.ui.setconfig(b'ui', b'interactive', b'off', b'-y')
1025
1025
1026 if commit_ids:
1026 if commit_ids:
1027 commit_ids = [bin(commit_id) for commit_id in commit_ids]
1027 commit_ids = [bin(commit_id) for commit_id in commit_ids]
1028
1028
1029 return exchange.pull(
1029 return exchange.pull(
1030 repo, remote, heads=commit_ids, force=None).cgresult
1030 repo, remote, heads=commit_ids, force=None).cgresult
1031
1031
1032 @reraise_safe_exceptions
1032 @reraise_safe_exceptions
1033 def pull_cmd(self, wire, source, bookmark='', branch='', revision='', hooks=True):
1033 def pull_cmd(self, wire, source, bookmark='', branch='', revision='', hooks=True):
1034 repo = self._factory.repo(wire)
1034 repo = self._factory.repo(wire)
1035 baseui = self._factory._create_config(wire['config'], hooks=hooks)
1035 baseui = self._factory._create_config(wire['config'], hooks=hooks)
1036
1036
1037 source = safe_bytes(source)
1037 source = safe_bytes(source)
1038
1038
1039 # Mercurial internally has a lot of logic that checks ONLY if
1039 # Mercurial internally has a lot of logic that checks ONLY if
1040 # option is defined, we just pass those if they are defined then
1040 # option is defined, we just pass those if they are defined then
1041 opts = {}
1041 opts = {}
1042
1042
1043 if bookmark:
1043 if bookmark:
1044 opts['bookmark'] = [safe_bytes(x) for x in bookmark] \
1044 opts['bookmark'] = [safe_bytes(x) for x in bookmark] \
1045 if isinstance(bookmark, list) else safe_bytes(bookmark)
1045 if isinstance(bookmark, list) else safe_bytes(bookmark)
1046
1046
1047 if branch:
1047 if branch:
1048 opts['branch'] = [safe_bytes(x) for x in branch] \
1048 opts['branch'] = [safe_bytes(x) for x in branch] \
1049 if isinstance(branch, list) else safe_bytes(branch)
1049 if isinstance(branch, list) else safe_bytes(branch)
1050
1050
1051 if revision:
1051 if revision:
1052 opts['rev'] = [safe_bytes(x) for x in revision] \
1052 opts['rev'] = [safe_bytes(x) for x in revision] \
1053 if isinstance(revision, list) else safe_bytes(revision)
1053 if isinstance(revision, list) else safe_bytes(revision)
1054
1054
1055 commands.pull(baseui, repo, source, **opts)
1055 commands.pull(baseui, repo, source, **opts)
1056
1056
1057 @reraise_safe_exceptions
1057 @reraise_safe_exceptions
1058 def push(self, wire, revisions, dest_path, hooks: bool = True, push_branches: bool = False):
1058 def push(self, wire, revisions, dest_path, hooks: bool = True, push_branches: bool = False):
1059 repo = self._factory.repo(wire)
1059 repo = self._factory.repo(wire)
1060 baseui = self._factory._create_config(wire['config'], hooks=hooks)
1060 baseui = self._factory._create_config(wire['config'], hooks=hooks)
1061
1061
1062 revisions = [safe_bytes(x) for x in revisions] \
1062 revisions = [safe_bytes(x) for x in revisions] \
1063 if isinstance(revisions, list) else safe_bytes(revisions)
1063 if isinstance(revisions, list) else safe_bytes(revisions)
1064
1064
1065 commands.push(baseui, repo, safe_bytes(dest_path),
1065 commands.push(baseui, repo, safe_bytes(dest_path),
1066 rev=revisions,
1066 rev=revisions,
1067 new_branch=push_branches)
1067 new_branch=push_branches)
1068
1068
1069 @reraise_safe_exceptions
1069 @reraise_safe_exceptions
1070 def strip(self, wire, revision, update, backup):
1070 def strip(self, wire, revision, update, backup):
1071 repo = self._factory.repo(wire)
1071 repo = self._factory.repo(wire)
1072 ctx = self._get_ctx(repo, revision)
1072 ctx = self._get_ctx(repo, revision)
1073 hgext_strip.strip(
1073 hgext_strip.strip(
1074 repo.baseui, repo, ctx.node(), update=update, backup=backup)
1074 repo.baseui, repo, ctx.node(), update=update, backup=backup)
1075
1075
1076 @reraise_safe_exceptions
1076 @reraise_safe_exceptions
1077 def get_unresolved_files(self, wire):
1077 def get_unresolved_files(self, wire):
1078 repo = self._factory.repo(wire)
1078 repo = self._factory.repo(wire)
1079
1079
1080 log.debug('Calculating unresolved files for repo: %s', repo)
1080 log.debug('Calculating unresolved files for repo: %s', repo)
1081 output = io.BytesIO()
1081 output = io.BytesIO()
1082
1082
1083 def write(data, **unused_kwargs):
1083 def write(data, **unused_kwargs):
1084 output.write(data)
1084 output.write(data)
1085
1085
1086 baseui = self._factory._create_config(wire['config'])
1086 baseui = self._factory._create_config(wire['config'])
1087 baseui.write = write
1087 baseui.write = write
1088
1088
1089 commands.resolve(baseui, repo, list=True)
1089 commands.resolve(baseui, repo, list=True)
1090 unresolved = output.getvalue().splitlines(0)
1090 unresolved = output.getvalue().splitlines(0)
1091 return unresolved
1091 return unresolved
1092
1092
1093 @reraise_safe_exceptions
1093 @reraise_safe_exceptions
1094 def merge(self, wire, revision):
1094 def merge(self, wire, revision):
1095 repo = self._factory.repo(wire)
1095 repo = self._factory.repo(wire)
1096 baseui = self._factory._create_config(wire['config'])
1096 baseui = self._factory._create_config(wire['config'])
1097 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
1097 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
1098
1098
1099 # In case of sub repositories are used mercurial prompts the user in
1099 # In case of sub repositories are used mercurial prompts the user in
1100 # case of merge conflicts or different sub repository sources. By
1100 # case of merge conflicts or different sub repository sources. By
1101 # setting the interactive flag to `False` mercurial doesn't prompt the
1101 # setting the interactive flag to `False` mercurial doesn't prompt the
1102 # used but instead uses a default value.
1102 # used but instead uses a default value.
1103 repo.ui.setconfig(b'ui', b'interactive', False)
1103 repo.ui.setconfig(b'ui', b'interactive', False)
1104 commands.merge(baseui, repo, rev=safe_bytes(revision))
1104 commands.merge(baseui, repo, rev=safe_bytes(revision))
1105
1105
1106 @reraise_safe_exceptions
1106 @reraise_safe_exceptions
1107 def merge_state(self, wire):
1107 def merge_state(self, wire):
1108 repo = self._factory.repo(wire)
1108 repo = self._factory.repo(wire)
1109 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
1109 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
1110
1110
1111 # In case of sub repositories are used mercurial prompts the user in
1111 # In case of sub repositories are used mercurial prompts the user in
1112 # case of merge conflicts or different sub repository sources. By
1112 # case of merge conflicts or different sub repository sources. By
1113 # setting the interactive flag to `False` mercurial doesn't prompt the
1113 # setting the interactive flag to `False` mercurial doesn't prompt the
1114 # used but instead uses a default value.
1114 # used but instead uses a default value.
1115 repo.ui.setconfig(b'ui', b'interactive', False)
1115 repo.ui.setconfig(b'ui', b'interactive', False)
1116 ms = hg_merge.mergestate(repo)
1116 ms = hg_merge.mergestate(repo)
1117 return [x for x in ms.unresolved()]
1117 return [x for x in ms.unresolved()]
1118
1118
1119 @reraise_safe_exceptions
1119 @reraise_safe_exceptions
1120 def commit(self, wire, message, username, close_branch=False):
1120 def commit(self, wire, message, username, close_branch=False):
1121 repo = self._factory.repo(wire)
1121 repo = self._factory.repo(wire)
1122 baseui = self._factory._create_config(wire['config'])
1122 baseui = self._factory._create_config(wire['config'])
1123 repo.ui.setconfig(b'ui', b'username', safe_bytes(username))
1123 repo.ui.setconfig(b'ui', b'username', safe_bytes(username))
1124 commands.commit(baseui, repo, message=safe_bytes(message), close_branch=close_branch)
1124 commands.commit(baseui, repo, message=safe_bytes(message), close_branch=close_branch)
1125
1125
1126 @reraise_safe_exceptions
1126 @reraise_safe_exceptions
1127 def rebase(self, wire, source='', dest='', abort=False):
1127 def rebase(self, wire, source='', dest='', abort=False):
1128 repo = self._factory.repo(wire)
1128 repo = self._factory.repo(wire)
1129 baseui = self._factory._create_config(wire['config'])
1129 baseui = self._factory._create_config(wire['config'])
1130 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
1130 repo.ui.setconfig(b'ui', b'merge', b'internal:dump')
1131 # In case of sub repositories are used mercurial prompts the user in
1131 # In case of sub repositories are used mercurial prompts the user in
1132 # case of merge conflicts or different sub repository sources. By
1132 # case of merge conflicts or different sub repository sources. By
1133 # setting the interactive flag to `False` mercurial doesn't prompt the
1133 # setting the interactive flag to `False` mercurial doesn't prompt the
1134 # used but instead uses a default value.
1134 # used but instead uses a default value.
1135 repo.ui.setconfig(b'ui', b'interactive', False)
1135 repo.ui.setconfig(b'ui', b'interactive', False)
1136
1136
1137 rebase.rebase(baseui, repo, base=safe_bytes(source or ''), dest=safe_bytes(dest or ''),
1137 rebase.rebase(baseui, repo, base=safe_bytes(source or ''), dest=safe_bytes(dest or ''),
1138 abort=abort, keep=not abort)
1138 abort=abort, keep=not abort)
1139
1139
1140 @reraise_safe_exceptions
1140 @reraise_safe_exceptions
1141 def tag(self, wire, name, revision, message, local, user, tag_time, tag_timezone):
1141 def tag(self, wire, name, revision, message, local, user, tag_time, tag_timezone):
1142 repo = self._factory.repo(wire)
1142 repo = self._factory.repo(wire)
1143 ctx = self._get_ctx(repo, revision)
1143 ctx = self._get_ctx(repo, revision)
1144 node = ctx.node()
1144 node = ctx.node()
1145
1145
1146 date = (tag_time, tag_timezone)
1146 date = (tag_time, tag_timezone)
1147 try:
1147 try:
1148 hg_tag.tag(repo, safe_bytes(name), node, safe_bytes(message), local, safe_bytes(user), date)
1148 hg_tag.tag(repo, safe_bytes(name), node, safe_bytes(message), local, safe_bytes(user), date)
1149 except Abort as e:
1149 except Abort as e:
1150 log.exception("Tag operation aborted")
1150 log.exception("Tag operation aborted")
1151 # Exception can contain unicode which we convert
1151 # Exception can contain unicode which we convert
1152 raise exceptions.AbortException(e)(repr(e))
1152 raise exceptions.AbortException(e)(repr(e))
1153
1153
1154 @reraise_safe_exceptions
1154 @reraise_safe_exceptions
1155 def bookmark(self, wire, bookmark, revision=''):
1155 def bookmark(self, wire, bookmark, revision=''):
1156 repo = self._factory.repo(wire)
1156 repo = self._factory.repo(wire)
1157 baseui = self._factory._create_config(wire['config'])
1157 baseui = self._factory._create_config(wire['config'])
1158 revision = revision or ''
1158 revision = revision or ''
1159 commands.bookmark(baseui, repo, safe_bytes(bookmark), rev=safe_bytes(revision), force=True)
1159 commands.bookmark(baseui, repo, safe_bytes(bookmark), rev=safe_bytes(revision), force=True)
1160
1160
1161 @reraise_safe_exceptions
1161 @reraise_safe_exceptions
1162 def install_hooks(self, wire, force=False):
1162 def install_hooks(self, wire, force=False):
1163 # we don't need any special hooks for Mercurial
1163 # we don't need any special hooks for Mercurial
1164 pass
1164 pass
1165
1165
1166 @reraise_safe_exceptions
1166 @reraise_safe_exceptions
1167 def get_hooks_info(self, wire):
1167 def get_hooks_info(self, wire):
1168 return {
1168 return {
1169 'pre_version': vcsserver.__version__,
1169 'pre_version': vcsserver.__version__,
1170 'post_version': vcsserver.__version__,
1170 'post_version': vcsserver.__version__,
1171 }
1171 }
1172
1172
1173 @reraise_safe_exceptions
1173 @reraise_safe_exceptions
1174 def set_head_ref(self, wire, head_name):
1174 def set_head_ref(self, wire, head_name):
1175 pass
1175 pass
1176
1176
1177 @reraise_safe_exceptions
1177 @reraise_safe_exceptions
1178 def archive_repo(self, wire, archive_name_key, kind, mtime, archive_at_path,
1178 def archive_repo(self, wire, archive_name_key, kind, mtime, archive_at_path,
1179 archive_dir_name, commit_id, cache_config):
1179 archive_dir_name, commit_id, cache_config):
1180
1180
1181 def file_walker(_commit_id, path):
1181 def file_walker(_commit_id, path):
1182 repo = self._factory.repo(wire)
1182 repo = self._factory.repo(wire)
1183 ctx = repo[_commit_id]
1183 ctx = repo[_commit_id]
1184 is_root = path in ['', '/']
1184 is_root = path in ['', '/']
1185 if is_root:
1185 if is_root:
1186 matcher = alwaysmatcher(badfn=None)
1186 matcher = alwaysmatcher(badfn=None)
1187 else:
1187 else:
1188 matcher = patternmatcher('', [(b'glob', safe_bytes(path)+b'/**', b'')], badfn=None)
1188 matcher = patternmatcher('', [(b'glob', safe_bytes(path)+b'/**', b'')], badfn=None)
1189 file_iter = ctx.manifest().walk(matcher)
1189 file_iter = ctx.manifest().walk(matcher)
1190
1190
1191 for fn in file_iter:
1191 for fn in file_iter:
1192 file_path = fn
1192 file_path = fn
1193 flags = ctx.flags(fn)
1193 flags = ctx.flags(fn)
1194 mode = b'x' in flags and 0o755 or 0o644
1194 mode = b'x' in flags and 0o755 or 0o644
1195 is_link = b'l' in flags
1195 is_link = b'l' in flags
1196
1196
1197 yield ArchiveNode(file_path, mode, is_link, ctx[fn].data)
1197 yield ArchiveNode(file_path, mode, is_link, ctx[fn].data)
1198
1198
1199 return store_archive_in_cache(
1199 return store_archive_in_cache(
1200 file_walker, archive_name_key, kind, mtime, archive_at_path, archive_dir_name, commit_id, cache_config=cache_config)
1200 file_walker, archive_name_key, kind, mtime, archive_at_path, archive_dir_name, commit_id, cache_config=cache_config)
1201
1201
General Comments 0
You need to be logged in to leave comments. Login now