##// END OF EJS Templates
fix(security): hide env details when debug is disabled....
super-admin -
r1181:d7983c1a default
parent child Browse files
Show More
@@ -1,1485 +1,1491 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 import rhodecode
42 from vcsserver import exceptions, settings, subprocessio
43 from vcsserver import exceptions, settings, subprocessio
43 from vcsserver.str_utils import safe_str, safe_int, safe_bytes, ascii_bytes
44 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
45 from vcsserver.base import RepoFactory, obfuscate_qs, ArchiveNode, store_archive_in_cache, BytesEnvelope, BinaryEnvelope
45 from vcsserver.hgcompat import (
46 from vcsserver.hgcompat import (
46 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler)
47 hg_url as url_parser, httpbasicauthhandler, httpdigestauthhandler)
47 from vcsserver.git_lfs.lib import LFSOidStore
48 from vcsserver.git_lfs.lib import LFSOidStore
48 from vcsserver.vcs_base import RemoteBase
49 from vcsserver.vcs_base import RemoteBase
49
50
50 DIR_STAT = stat.S_IFDIR
51 DIR_STAT = stat.S_IFDIR
51 FILE_MODE = stat.S_IFMT
52 FILE_MODE = stat.S_IFMT
52 GIT_LINK = objects.S_IFGITLINK
53 GIT_LINK = objects.S_IFGITLINK
53 PEELED_REF_MARKER = b'^{}'
54 PEELED_REF_MARKER = b'^{}'
54 HEAD_MARKER = b'HEAD'
55 HEAD_MARKER = b'HEAD'
55
56
56 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
57
58
58
59
59 def reraise_safe_exceptions(func):
60 def reraise_safe_exceptions(func):
60 """Converts Dulwich exceptions to something neutral."""
61 """Converts Dulwich exceptions to something neutral."""
61
62
62 @wraps(func)
63 @wraps(func)
63 def wrapper(*args, **kwargs):
64 def wrapper(*args, **kwargs):
64 try:
65 try:
65 return func(*args, **kwargs)
66 return func(*args, **kwargs)
66 except (ChecksumMismatch, WrongObjectException, MissingCommitError, ObjectMissing,) as e:
67 except (ChecksumMismatch, WrongObjectException, MissingCommitError, ObjectMissing,) as e:
67 exc = exceptions.LookupException(org_exc=e)
68 exc = exceptions.LookupException(org_exc=e)
68 raise exc(safe_str(e))
69 raise exc(safe_str(e))
69 except (HangupException, UnexpectedCommandError) as e:
70 except (HangupException, UnexpectedCommandError) as e:
70 exc = exceptions.VcsException(org_exc=e)
71 exc = exceptions.VcsException(org_exc=e)
71 raise exc(safe_str(e))
72 raise exc(safe_str(e))
72 except Exception:
73 except Exception:
73 # NOTE(marcink): because of how dulwich handles some exceptions
74 # NOTE(marcink): because of how dulwich handles some exceptions
74 # (KeyError on empty repos), we cannot track this and catch all
75 # (KeyError on empty repos), we cannot track this and catch all
75 # exceptions, it's an exceptions from other handlers
76 # exceptions, it's an exceptions from other handlers
76 #if not hasattr(e, '_vcs_kind'):
77 #if not hasattr(e, '_vcs_kind'):
77 #log.exception("Unhandled exception in git remote call")
78 #log.exception("Unhandled exception in git remote call")
78 #raise_from_original(exceptions.UnhandledException)
79 #raise_from_original(exceptions.UnhandledException)
79 raise
80 raise
80 return wrapper
81 return wrapper
81
82
82
83
83 class Repo(DulwichRepo):
84 class Repo(DulwichRepo):
84 """
85 """
85 A wrapper for dulwich Repo class.
86 A wrapper for dulwich Repo class.
86
87
87 Since dulwich is sometimes keeping .idx file descriptors open, it leads to
88 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
89 "Too many open files" error. We need to close all opened file descriptors
89 once the repo object is destroyed.
90 once the repo object is destroyed.
90 """
91 """
91 def __del__(self):
92 def __del__(self):
92 if hasattr(self, 'object_store'):
93 if hasattr(self, 'object_store'):
93 self.close()
94 self.close()
94
95
95
96
96 class Repository(LibGit2Repo):
97 class Repository(LibGit2Repo):
97
98
98 def __enter__(self):
99 def __enter__(self):
99 return self
100 return self
100
101
101 def __exit__(self, exc_type, exc_val, exc_tb):
102 def __exit__(self, exc_type, exc_val, exc_tb):
102 self.free()
103 self.free()
103
104
104
105
105 class GitFactory(RepoFactory):
106 class GitFactory(RepoFactory):
106 repo_type = 'git'
107 repo_type = 'git'
107
108
108 def _create_repo(self, wire, create, use_libgit2=False):
109 def _create_repo(self, wire, create, use_libgit2=False):
109 if use_libgit2:
110 if use_libgit2:
110 repo = Repository(safe_bytes(wire['path']))
111 repo = Repository(safe_bytes(wire['path']))
111 else:
112 else:
112 # dulwich mode
113 # dulwich mode
113 repo_path = safe_str(wire['path'], to_encoding=settings.WIRE_ENCODING)
114 repo_path = safe_str(wire['path'], to_encoding=settings.WIRE_ENCODING)
114 repo = Repo(repo_path)
115 repo = Repo(repo_path)
115
116
116 log.debug('repository created: got GIT object: %s', repo)
117 log.debug('repository created: got GIT object: %s', repo)
117 return repo
118 return repo
118
119
119 def repo(self, wire, create=False, use_libgit2=False):
120 def repo(self, wire, create=False, use_libgit2=False):
120 """
121 """
121 Get a repository instance for the given path.
122 Get a repository instance for the given path.
122 """
123 """
123 return self._create_repo(wire, create, use_libgit2)
124 return self._create_repo(wire, create, use_libgit2)
124
125
125 def repo_libgit2(self, wire):
126 def repo_libgit2(self, wire):
126 return self.repo(wire, use_libgit2=True)
127 return self.repo(wire, use_libgit2=True)
127
128
128
129
129 def create_signature_from_string(author_str, **kwargs):
130 def create_signature_from_string(author_str, **kwargs):
130 """
131 """
131 Creates a pygit2.Signature object from a string of the format 'Name <email>'.
132 Creates a pygit2.Signature object from a string of the format 'Name <email>'.
132
133
133 :param author_str: String of the format 'Name <email>'
134 :param author_str: String of the format 'Name <email>'
134 :return: pygit2.Signature object
135 :return: pygit2.Signature object
135 """
136 """
136 match = re.match(r'^(.+) <(.+)>$', author_str)
137 match = re.match(r'^(.+) <(.+)>$', author_str)
137 if match is None:
138 if match is None:
138 raise ValueError(f"Invalid format: {author_str}")
139 raise ValueError(f"Invalid format: {author_str}")
139
140
140 name, email = match.groups()
141 name, email = match.groups()
141 return pygit2.Signature(name, email, **kwargs)
142 return pygit2.Signature(name, email, **kwargs)
142
143
143
144
144 def get_obfuscated_url(url_obj):
145 def get_obfuscated_url(url_obj):
145 url_obj.passwd = b'*****' if url_obj.passwd else url_obj.passwd
146 url_obj.passwd = b'*****' if url_obj.passwd else url_obj.passwd
146 url_obj.query = obfuscate_qs(url_obj.query)
147 url_obj.query = obfuscate_qs(url_obj.query)
147 obfuscated_uri = str(url_obj)
148 obfuscated_uri = str(url_obj)
148 return obfuscated_uri
149 return obfuscated_uri
149
150
150
151
151 class GitRemote(RemoteBase):
152 class GitRemote(RemoteBase):
152
153
153 def __init__(self, factory):
154 def __init__(self, factory):
154 self._factory = factory
155 self._factory = factory
155 self._bulk_methods = {
156 self._bulk_methods = {
156 "date": self.date,
157 "date": self.date,
157 "author": self.author,
158 "author": self.author,
158 "branch": self.branch,
159 "branch": self.branch,
159 "message": self.message,
160 "message": self.message,
160 "parents": self.parents,
161 "parents": self.parents,
161 "_commit": self.revision,
162 "_commit": self.revision,
162 }
163 }
163 self._bulk_file_methods = {
164 self._bulk_file_methods = {
164 "size": self.get_node_size,
165 "size": self.get_node_size,
165 "data": self.get_node_data,
166 "data": self.get_node_data,
166 "flags": self.get_node_flags,
167 "flags": self.get_node_flags,
167 "is_binary": self.get_node_is_binary,
168 "is_binary": self.get_node_is_binary,
168 "md5": self.md5_hash
169 "md5": self.md5_hash
169 }
170 }
170
171
171 def _wire_to_config(self, wire):
172 def _wire_to_config(self, wire):
172 if 'config' in wire:
173 if 'config' in wire:
173 return {x[0] + '_' + x[1]: x[2] for x in wire['config']}
174 return {x[0] + '_' + x[1]: x[2] for x in wire['config']}
174 return {}
175 return {}
175
176
176 def _remote_conf(self, config):
177 def _remote_conf(self, config):
177 params = [
178 params = [
178 '-c', 'core.askpass=""',
179 '-c', 'core.askpass=""',
179 ]
180 ]
180 ssl_cert_dir = config.get('vcs_ssl_dir')
181 ssl_cert_dir = config.get('vcs_ssl_dir')
181 if ssl_cert_dir:
182 if ssl_cert_dir:
182 params.extend(['-c', f'http.sslCAinfo={ssl_cert_dir}'])
183 params.extend(['-c', f'http.sslCAinfo={ssl_cert_dir}'])
183 return params
184 return params
184
185
185 @reraise_safe_exceptions
186 @reraise_safe_exceptions
186 def discover_git_version(self):
187 def discover_git_version(self):
187 stdout, _ = self.run_git_command(
188 stdout, _ = self.run_git_command(
188 {}, ['--version'], _bare=True, _safe=True)
189 {}, ['--version'], _bare=True, _safe=True)
189 prefix = b'git version'
190 prefix = b'git version'
190 if stdout.startswith(prefix):
191 if stdout.startswith(prefix):
191 stdout = stdout[len(prefix):]
192 stdout = stdout[len(prefix):]
192 return safe_str(stdout.strip())
193 return safe_str(stdout.strip())
193
194
194 @reraise_safe_exceptions
195 @reraise_safe_exceptions
195 def is_empty(self, wire):
196 def is_empty(self, wire):
196 repo_init = self._factory.repo_libgit2(wire)
197 repo_init = self._factory.repo_libgit2(wire)
197 with repo_init as repo:
198 with repo_init as repo:
198
199
199 try:
200 try:
200 has_head = repo.head.name
201 has_head = repo.head.name
201 if has_head:
202 if has_head:
202 return False
203 return False
203
204
204 # NOTE(marcink): check again using more expensive method
205 # NOTE(marcink): check again using more expensive method
205 return repo.is_empty
206 return repo.is_empty
206 except Exception:
207 except Exception:
207 pass
208 pass
208
209
209 return True
210 return True
210
211
211 @reraise_safe_exceptions
212 @reraise_safe_exceptions
212 def assert_correct_path(self, wire):
213 def assert_correct_path(self, wire):
213 cache_on, context_uid, repo_id = self._cache_on(wire)
214 cache_on, context_uid, repo_id = self._cache_on(wire)
214 region = self._region(wire)
215 region = self._region(wire)
215
216
216 @region.conditional_cache_on_arguments(condition=cache_on)
217 @region.conditional_cache_on_arguments(condition=cache_on)
217 def _assert_correct_path(_context_uid, _repo_id, fast_check):
218 def _assert_correct_path(_context_uid, _repo_id, fast_check):
218 if fast_check:
219 if fast_check:
219 path = safe_str(wire['path'])
220 path = safe_str(wire['path'])
220 if pygit2.discover_repository(path):
221 if pygit2.discover_repository(path):
221 return True
222 return True
222 return False
223 return False
223 else:
224 else:
224 try:
225 try:
225 repo_init = self._factory.repo_libgit2(wire)
226 repo_init = self._factory.repo_libgit2(wire)
226 with repo_init:
227 with repo_init:
227 pass
228 pass
228 except pygit2.GitError:
229 except pygit2.GitError:
229 path = wire.get('path')
230 path = wire.get('path')
230 tb = traceback.format_exc()
231 tb = traceback.format_exc()
231 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
232 log.debug("Invalid Git path `%s`, tb: %s", path, tb)
232 return False
233 return False
233 return True
234 return True
234
235
235 return _assert_correct_path(context_uid, repo_id, True)
236 return _assert_correct_path(context_uid, repo_id, True)
236
237
237 @reraise_safe_exceptions
238 @reraise_safe_exceptions
238 def bare(self, wire):
239 def bare(self, wire):
239 repo_init = self._factory.repo_libgit2(wire)
240 repo_init = self._factory.repo_libgit2(wire)
240 with repo_init as repo:
241 with repo_init as repo:
241 return repo.is_bare
242 return repo.is_bare
242
243
243 @reraise_safe_exceptions
244 @reraise_safe_exceptions
244 def get_node_data(self, wire, commit_id, path):
245 def get_node_data(self, wire, commit_id, path):
245 repo_init = self._factory.repo_libgit2(wire)
246 repo_init = self._factory.repo_libgit2(wire)
246 with repo_init as repo:
247 with repo_init as repo:
247 commit = repo[commit_id]
248 commit = repo[commit_id]
248 blob_obj = commit.tree[path]
249 blob_obj = commit.tree[path]
249
250
250 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
251 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
251 raise exceptions.LookupException()(
252 raise exceptions.LookupException()(
252 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
253 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
253
254
254 return BytesEnvelope(blob_obj.data)
255 return BytesEnvelope(blob_obj.data)
255
256
256 @reraise_safe_exceptions
257 @reraise_safe_exceptions
257 def get_node_size(self, wire, commit_id, path):
258 def get_node_size(self, wire, commit_id, path):
258 repo_init = self._factory.repo_libgit2(wire)
259 repo_init = self._factory.repo_libgit2(wire)
259 with repo_init as repo:
260 with repo_init as repo:
260 commit = repo[commit_id]
261 commit = repo[commit_id]
261 blob_obj = commit.tree[path]
262 blob_obj = commit.tree[path]
262
263
263 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
264 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
264 raise exceptions.LookupException()(
265 raise exceptions.LookupException()(
265 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
266 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
266
267
267 return blob_obj.size
268 return blob_obj.size
268
269
269 @reraise_safe_exceptions
270 @reraise_safe_exceptions
270 def get_node_flags(self, wire, commit_id, path):
271 def get_node_flags(self, wire, commit_id, path):
271 repo_init = self._factory.repo_libgit2(wire)
272 repo_init = self._factory.repo_libgit2(wire)
272 with repo_init as repo:
273 with repo_init as repo:
273 commit = repo[commit_id]
274 commit = repo[commit_id]
274 blob_obj = commit.tree[path]
275 blob_obj = commit.tree[path]
275
276
276 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
277 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
277 raise exceptions.LookupException()(
278 raise exceptions.LookupException()(
278 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
279 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
279
280
280 return blob_obj.filemode
281 return blob_obj.filemode
281
282
282 @reraise_safe_exceptions
283 @reraise_safe_exceptions
283 def get_node_is_binary(self, wire, commit_id, path):
284 def get_node_is_binary(self, wire, commit_id, path):
284 repo_init = self._factory.repo_libgit2(wire)
285 repo_init = self._factory.repo_libgit2(wire)
285 with repo_init as repo:
286 with repo_init as repo:
286 commit = repo[commit_id]
287 commit = repo[commit_id]
287 blob_obj = commit.tree[path]
288 blob_obj = commit.tree[path]
288
289
289 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
290 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
290 raise exceptions.LookupException()(
291 raise exceptions.LookupException()(
291 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
292 f'Tree for commit_id:{commit_id} is not a blob: {blob_obj.type_str}')
292
293
293 return blob_obj.is_binary
294 return blob_obj.is_binary
294
295
295 @reraise_safe_exceptions
296 @reraise_safe_exceptions
296 def blob_as_pretty_string(self, wire, sha):
297 def blob_as_pretty_string(self, wire, sha):
297 repo_init = self._factory.repo_libgit2(wire)
298 repo_init = self._factory.repo_libgit2(wire)
298 with repo_init as repo:
299 with repo_init as repo:
299 blob_obj = repo[sha]
300 blob_obj = repo[sha]
300 return BytesEnvelope(blob_obj.data)
301 return BytesEnvelope(blob_obj.data)
301
302
302 @reraise_safe_exceptions
303 @reraise_safe_exceptions
303 def blob_raw_length(self, wire, sha):
304 def blob_raw_length(self, wire, sha):
304 cache_on, context_uid, repo_id = self._cache_on(wire)
305 cache_on, context_uid, repo_id = self._cache_on(wire)
305 region = self._region(wire)
306 region = self._region(wire)
306
307
307 @region.conditional_cache_on_arguments(condition=cache_on)
308 @region.conditional_cache_on_arguments(condition=cache_on)
308 def _blob_raw_length(_repo_id, _sha):
309 def _blob_raw_length(_repo_id, _sha):
309
310
310 repo_init = self._factory.repo_libgit2(wire)
311 repo_init = self._factory.repo_libgit2(wire)
311 with repo_init as repo:
312 with repo_init as repo:
312 blob = repo[sha]
313 blob = repo[sha]
313 return blob.size
314 return blob.size
314
315
315 return _blob_raw_length(repo_id, sha)
316 return _blob_raw_length(repo_id, sha)
316
317
317 def _parse_lfs_pointer(self, raw_content):
318 def _parse_lfs_pointer(self, raw_content):
318 spec_string = b'version https://git-lfs.github.com/spec'
319 spec_string = b'version https://git-lfs.github.com/spec'
319 if raw_content and raw_content.startswith(spec_string):
320 if raw_content and raw_content.startswith(spec_string):
320
321
321 pattern = re.compile(rb"""
322 pattern = re.compile(rb"""
322 (?:\n)?
323 (?:\n)?
323 ^version[ ]https://git-lfs\.github\.com/spec/(?P<spec_ver>v\d+)\n
324 ^version[ ]https://git-lfs\.github\.com/spec/(?P<spec_ver>v\d+)\n
324 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
325 ^oid[ ] sha256:(?P<oid_hash>[0-9a-f]{64})\n
325 ^size[ ](?P<oid_size>[0-9]+)\n
326 ^size[ ](?P<oid_size>[0-9]+)\n
326 (?:\n)?
327 (?:\n)?
327 """, re.VERBOSE | re.MULTILINE)
328 """, re.VERBOSE | re.MULTILINE)
328 match = pattern.match(raw_content)
329 match = pattern.match(raw_content)
329 if match:
330 if match:
330 return match.groupdict()
331 return match.groupdict()
331
332
332 return {}
333 return {}
333
334
334 @reraise_safe_exceptions
335 @reraise_safe_exceptions
335 def is_large_file(self, wire, commit_id):
336 def is_large_file(self, wire, commit_id):
336 cache_on, context_uid, repo_id = self._cache_on(wire)
337 cache_on, context_uid, repo_id = self._cache_on(wire)
337 region = self._region(wire)
338 region = self._region(wire)
338
339
339 @region.conditional_cache_on_arguments(condition=cache_on)
340 @region.conditional_cache_on_arguments(condition=cache_on)
340 def _is_large_file(_repo_id, _sha):
341 def _is_large_file(_repo_id, _sha):
341 repo_init = self._factory.repo_libgit2(wire)
342 repo_init = self._factory.repo_libgit2(wire)
342 with repo_init as repo:
343 with repo_init as repo:
343 blob = repo[commit_id]
344 blob = repo[commit_id]
344 if blob.is_binary:
345 if blob.is_binary:
345 return {}
346 return {}
346
347
347 return self._parse_lfs_pointer(blob.data)
348 return self._parse_lfs_pointer(blob.data)
348
349
349 return _is_large_file(repo_id, commit_id)
350 return _is_large_file(repo_id, commit_id)
350
351
351 @reraise_safe_exceptions
352 @reraise_safe_exceptions
352 def is_binary(self, wire, tree_id):
353 def is_binary(self, wire, tree_id):
353 cache_on, context_uid, repo_id = self._cache_on(wire)
354 cache_on, context_uid, repo_id = self._cache_on(wire)
354 region = self._region(wire)
355 region = self._region(wire)
355
356
356 @region.conditional_cache_on_arguments(condition=cache_on)
357 @region.conditional_cache_on_arguments(condition=cache_on)
357 def _is_binary(_repo_id, _tree_id):
358 def _is_binary(_repo_id, _tree_id):
358 repo_init = self._factory.repo_libgit2(wire)
359 repo_init = self._factory.repo_libgit2(wire)
359 with repo_init as repo:
360 with repo_init as repo:
360 blob_obj = repo[tree_id]
361 blob_obj = repo[tree_id]
361 return blob_obj.is_binary
362 return blob_obj.is_binary
362
363
363 return _is_binary(repo_id, tree_id)
364 return _is_binary(repo_id, tree_id)
364
365
365 @reraise_safe_exceptions
366 @reraise_safe_exceptions
366 def md5_hash(self, wire, commit_id, path):
367 def md5_hash(self, wire, commit_id, path):
367 cache_on, context_uid, repo_id = self._cache_on(wire)
368 cache_on, context_uid, repo_id = self._cache_on(wire)
368 region = self._region(wire)
369 region = self._region(wire)
369
370
370 @region.conditional_cache_on_arguments(condition=cache_on)
371 @region.conditional_cache_on_arguments(condition=cache_on)
371 def _md5_hash(_repo_id, _commit_id, _path):
372 def _md5_hash(_repo_id, _commit_id, _path):
372 repo_init = self._factory.repo_libgit2(wire)
373 repo_init = self._factory.repo_libgit2(wire)
373 with repo_init as repo:
374 with repo_init as repo:
374 commit = repo[_commit_id]
375 commit = repo[_commit_id]
375 blob_obj = commit.tree[_path]
376 blob_obj = commit.tree[_path]
376
377
377 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
378 if blob_obj.type != pygit2.GIT_OBJ_BLOB:
378 raise exceptions.LookupException()(
379 raise exceptions.LookupException()(
379 f'Tree for commit_id:{_commit_id} is not a blob: {blob_obj.type_str}')
380 f'Tree for commit_id:{_commit_id} is not a blob: {blob_obj.type_str}')
380
381
381 return ''
382 return ''
382
383
383 return _md5_hash(repo_id, commit_id, path)
384 return _md5_hash(repo_id, commit_id, path)
384
385
385 @reraise_safe_exceptions
386 @reraise_safe_exceptions
386 def in_largefiles_store(self, wire, oid):
387 def in_largefiles_store(self, wire, oid):
387 conf = self._wire_to_config(wire)
388 conf = self._wire_to_config(wire)
388 repo_init = self._factory.repo_libgit2(wire)
389 repo_init = self._factory.repo_libgit2(wire)
389 with repo_init as repo:
390 with repo_init as repo:
390 repo_name = repo.path
391 repo_name = repo.path
391
392
392 store_location = conf.get('vcs_git_lfs_store_location')
393 store_location = conf.get('vcs_git_lfs_store_location')
393 if store_location:
394 if store_location:
394
395
395 store = LFSOidStore(
396 store = LFSOidStore(
396 oid=oid, repo=repo_name, store_location=store_location)
397 oid=oid, repo=repo_name, store_location=store_location)
397 return store.has_oid()
398 return store.has_oid()
398
399
399 return False
400 return False
400
401
401 @reraise_safe_exceptions
402 @reraise_safe_exceptions
402 def store_path(self, wire, oid):
403 def store_path(self, wire, oid):
403 conf = self._wire_to_config(wire)
404 conf = self._wire_to_config(wire)
404 repo_init = self._factory.repo_libgit2(wire)
405 repo_init = self._factory.repo_libgit2(wire)
405 with repo_init as repo:
406 with repo_init as repo:
406 repo_name = repo.path
407 repo_name = repo.path
407
408
408 store_location = conf.get('vcs_git_lfs_store_location')
409 store_location = conf.get('vcs_git_lfs_store_location')
409 if store_location:
410 if store_location:
410 store = LFSOidStore(
411 store = LFSOidStore(
411 oid=oid, repo=repo_name, store_location=store_location)
412 oid=oid, repo=repo_name, store_location=store_location)
412 return store.oid_path
413 return store.oid_path
413 raise ValueError(f'Unable to fetch oid with path {oid}')
414 raise ValueError(f'Unable to fetch oid with path {oid}')
414
415
415 @reraise_safe_exceptions
416 @reraise_safe_exceptions
416 def bulk_request(self, wire, rev, pre_load):
417 def bulk_request(self, wire, rev, pre_load):
417 cache_on, context_uid, repo_id = self._cache_on(wire)
418 cache_on, context_uid, repo_id = self._cache_on(wire)
418 region = self._region(wire)
419 region = self._region(wire)
419
420
420 @region.conditional_cache_on_arguments(condition=cache_on)
421 @region.conditional_cache_on_arguments(condition=cache_on)
421 def _bulk_request(_repo_id, _rev, _pre_load):
422 def _bulk_request(_repo_id, _rev, _pre_load):
422 result = {}
423 result = {}
423 for attr in pre_load:
424 for attr in pre_load:
424 try:
425 try:
425 method = self._bulk_methods[attr]
426 method = self._bulk_methods[attr]
426 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
427 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
427 args = [wire, rev]
428 args = [wire, rev]
428 result[attr] = method(*args)
429 result[attr] = method(*args)
429 except KeyError as e:
430 except KeyError as e:
430 raise exceptions.VcsException(e)(f"Unknown bulk attribute: {attr}")
431 raise exceptions.VcsException(e)(f"Unknown bulk attribute: {attr}")
431 return result
432 return result
432
433
433 return _bulk_request(repo_id, rev, sorted(pre_load))
434 return _bulk_request(repo_id, rev, sorted(pre_load))
434
435
435 @reraise_safe_exceptions
436 @reraise_safe_exceptions
436 def bulk_file_request(self, wire, commit_id, path, pre_load):
437 def bulk_file_request(self, wire, commit_id, path, pre_load):
437 cache_on, context_uid, repo_id = self._cache_on(wire)
438 cache_on, context_uid, repo_id = self._cache_on(wire)
438 region = self._region(wire)
439 region = self._region(wire)
439
440
440 @region.conditional_cache_on_arguments(condition=cache_on)
441 @region.conditional_cache_on_arguments(condition=cache_on)
441 def _bulk_file_request(_repo_id, _commit_id, _path, _pre_load):
442 def _bulk_file_request(_repo_id, _commit_id, _path, _pre_load):
442 result = {}
443 result = {}
443 for attr in pre_load:
444 for attr in pre_load:
444 try:
445 try:
445 method = self._bulk_file_methods[attr]
446 method = self._bulk_file_methods[attr]
446 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
447 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
447 result[attr] = method(wire, _commit_id, _path)
448 result[attr] = method(wire, _commit_id, _path)
448 except KeyError as e:
449 except KeyError as e:
449 raise exceptions.VcsException(e)(f'Unknown bulk attribute: "{attr}"')
450 raise exceptions.VcsException(e)(f'Unknown bulk attribute: "{attr}"')
450 return result
451 return result
451
452
452 return BinaryEnvelope(_bulk_file_request(repo_id, commit_id, path, sorted(pre_load)))
453 return BinaryEnvelope(_bulk_file_request(repo_id, commit_id, path, sorted(pre_load)))
453
454
454 def _build_opener(self, url: str):
455 def _build_opener(self, url: str):
455 handlers = []
456 handlers = []
456 url_obj = url_parser(safe_bytes(url))
457 url_obj = url_parser(safe_bytes(url))
457 authinfo = url_obj.authinfo()[1]
458 authinfo = url_obj.authinfo()[1]
458
459
459 if authinfo:
460 if authinfo:
460 # create a password manager
461 # create a password manager
461 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
462 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
462 passmgr.add_password(*authinfo)
463 passmgr.add_password(*authinfo)
463
464
464 handlers.extend((httpbasicauthhandler(passmgr),
465 handlers.extend((httpbasicauthhandler(passmgr),
465 httpdigestauthhandler(passmgr)))
466 httpdigestauthhandler(passmgr)))
466
467
467 return urllib.request.build_opener(*handlers)
468 return urllib.request.build_opener(*handlers)
468
469
469 @reraise_safe_exceptions
470 @reraise_safe_exceptions
470 def check_url(self, url, config):
471 def check_url(self, url, config):
471 url_obj = url_parser(safe_bytes(url))
472 url_obj = url_parser(safe_bytes(url))
472
473
473 test_uri = safe_str(url_obj.authinfo()[0])
474 test_uri = safe_str(url_obj.authinfo()[0])
474 obfuscated_uri = get_obfuscated_url(url_obj)
475 obfuscated_uri = get_obfuscated_url(url_obj)
475
476
476 log.info("Checking URL for remote cloning/import: %s", obfuscated_uri)
477 log.info("Checking URL for remote cloning/import: %s", obfuscated_uri)
477
478
478 if not test_uri.endswith('info/refs'):
479 if not test_uri.endswith('info/refs'):
479 test_uri = test_uri.rstrip('/') + '/info/refs'
480 test_uri = test_uri.rstrip('/') + '/info/refs'
480
481
481 o = self._build_opener(test_uri)
482 o = self._build_opener(test_uri)
482 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
483 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
483
484
484 q = {"service": 'git-upload-pack'}
485 q = {"service": 'git-upload-pack'}
485 qs = f'?{urllib.parse.urlencode(q)}'
486 qs = f'?{urllib.parse.urlencode(q)}'
486 cu = f"{test_uri}{qs}"
487 cu = f"{test_uri}{qs}"
487
488
488 try:
489 try:
489 req = urllib.request.Request(cu, None, {})
490 req = urllib.request.Request(cu, None, {})
490 log.debug("Trying to open URL %s", obfuscated_uri)
491 log.debug("Trying to open URL %s", obfuscated_uri)
491 resp = o.open(req)
492 resp = o.open(req)
492 if resp.code != 200:
493 if resp.code != 200:
493 raise exceptions.URLError()('Return Code is not 200')
494 raise exceptions.URLError()('Return Code is not 200')
494 except Exception as e:
495 except Exception as e:
495 log.warning("URL cannot be opened: %s", obfuscated_uri, exc_info=True)
496 log.warning("URL cannot be opened: %s", obfuscated_uri, exc_info=True)
496 # means it cannot be cloned
497 # means it cannot be cloned
497 raise exceptions.URLError(e)(f"[{obfuscated_uri}] org_exc: {e}")
498 raise exceptions.URLError(e)(f"[{obfuscated_uri}] org_exc: {e}")
498
499
499 # now detect if it's proper git repo
500 # now detect if it's proper git repo
500 gitdata: bytes = resp.read()
501 gitdata: bytes = resp.read()
501
502
502 if b'service=git-upload-pack' in gitdata:
503 if b'service=git-upload-pack' in gitdata:
503 pass
504 pass
504 elif re.findall(br'[0-9a-fA-F]{40}\s+refs', gitdata):
505 elif re.findall(br'[0-9a-fA-F]{40}\s+refs', gitdata):
505 # old style git can return some other format!
506 # old style git can return some other format!
506 pass
507 pass
507 else:
508 else:
508 e = None
509 e = None
509 raise exceptions.URLError(e)(
510 raise exceptions.URLError(e)(
510 f"url [{obfuscated_uri}] does not look like an hg repo org_exc: {e}")
511 f"url [{obfuscated_uri}] does not look like an hg repo org_exc: {e}")
511
512
512 return True
513 return True
513
514
514 @reraise_safe_exceptions
515 @reraise_safe_exceptions
515 def clone(self, wire, url, deferred, valid_refs, update_after_clone):
516 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
517 # TODO(marcink): deprecate this method. Last i checked we don't use it anymore
517 remote_refs = self.pull(wire, url, apply_refs=False)
518 remote_refs = self.pull(wire, url, apply_refs=False)
518 repo = self._factory.repo(wire)
519 repo = self._factory.repo(wire)
519 if isinstance(valid_refs, list):
520 if isinstance(valid_refs, list):
520 valid_refs = tuple(valid_refs)
521 valid_refs = tuple(valid_refs)
521
522
522 for k in remote_refs:
523 for k in remote_refs:
523 # only parse heads/tags and skip so called deferred tags
524 # only parse heads/tags and skip so called deferred tags
524 if k.startswith(valid_refs) and not k.endswith(deferred):
525 if k.startswith(valid_refs) and not k.endswith(deferred):
525 repo[k] = remote_refs[k]
526 repo[k] = remote_refs[k]
526
527
527 if update_after_clone:
528 if update_after_clone:
528 # we want to checkout HEAD
529 # we want to checkout HEAD
529 repo["HEAD"] = remote_refs["HEAD"]
530 repo["HEAD"] = remote_refs["HEAD"]
530 index.build_index_from_tree(repo.path, repo.index_path(),
531 index.build_index_from_tree(repo.path, repo.index_path(),
531 repo.object_store, repo["HEAD"].tree)
532 repo.object_store, repo["HEAD"].tree)
532
533
533 @reraise_safe_exceptions
534 @reraise_safe_exceptions
534 def branch(self, wire, commit_id):
535 def branch(self, wire, commit_id):
535 cache_on, context_uid, repo_id = self._cache_on(wire)
536 cache_on, context_uid, repo_id = self._cache_on(wire)
536 region = self._region(wire)
537 region = self._region(wire)
537
538
538 @region.conditional_cache_on_arguments(condition=cache_on)
539 @region.conditional_cache_on_arguments(condition=cache_on)
539 def _branch(_context_uid, _repo_id, _commit_id):
540 def _branch(_context_uid, _repo_id, _commit_id):
540 regex = re.compile('^refs/heads')
541 regex = re.compile('^refs/heads')
541
542
542 def filter_with(ref):
543 def filter_with(ref):
543 return regex.match(ref[0]) and ref[1] == _commit_id
544 return regex.match(ref[0]) and ref[1] == _commit_id
544
545
545 branches = list(filter(filter_with, list(self.get_refs(wire).items())))
546 branches = list(filter(filter_with, list(self.get_refs(wire).items())))
546 return [x[0].split('refs/heads/')[-1] for x in branches]
547 return [x[0].split('refs/heads/')[-1] for x in branches]
547
548
548 return _branch(context_uid, repo_id, commit_id)
549 return _branch(context_uid, repo_id, commit_id)
549
550
550 @reraise_safe_exceptions
551 @reraise_safe_exceptions
551 def commit_branches(self, wire, commit_id):
552 def commit_branches(self, wire, commit_id):
552 cache_on, context_uid, repo_id = self._cache_on(wire)
553 cache_on, context_uid, repo_id = self._cache_on(wire)
553 region = self._region(wire)
554 region = self._region(wire)
554
555
555 @region.conditional_cache_on_arguments(condition=cache_on)
556 @region.conditional_cache_on_arguments(condition=cache_on)
556 def _commit_branches(_context_uid, _repo_id, _commit_id):
557 def _commit_branches(_context_uid, _repo_id, _commit_id):
557 repo_init = self._factory.repo_libgit2(wire)
558 repo_init = self._factory.repo_libgit2(wire)
558 with repo_init as repo:
559 with repo_init as repo:
559 branches = [x for x in repo.branches.with_commit(_commit_id)]
560 branches = [x for x in repo.branches.with_commit(_commit_id)]
560 return branches
561 return branches
561
562
562 return _commit_branches(context_uid, repo_id, commit_id)
563 return _commit_branches(context_uid, repo_id, commit_id)
563
564
564 @reraise_safe_exceptions
565 @reraise_safe_exceptions
565 def add_object(self, wire, content):
566 def add_object(self, wire, content):
566 repo_init = self._factory.repo_libgit2(wire)
567 repo_init = self._factory.repo_libgit2(wire)
567 with repo_init as repo:
568 with repo_init as repo:
568 blob = objects.Blob()
569 blob = objects.Blob()
569 blob.set_raw_string(content)
570 blob.set_raw_string(content)
570 repo.object_store.add_object(blob)
571 repo.object_store.add_object(blob)
571 return blob.id
572 return blob.id
572
573
573 @reraise_safe_exceptions
574 @reraise_safe_exceptions
574 def create_commit(self, wire, author, committer, message, branch, new_tree_id,
575 def create_commit(self, wire, author, committer, message, branch, new_tree_id,
575 date_args: list[int, int] = None,
576 date_args: list[int, int] = None,
576 parents: list | None = None):
577 parents: list | None = None):
577
578
578 repo_init = self._factory.repo_libgit2(wire)
579 repo_init = self._factory.repo_libgit2(wire)
579 with repo_init as repo:
580 with repo_init as repo:
580
581
581 if date_args:
582 if date_args:
582 current_time, offset = date_args
583 current_time, offset = date_args
583
584
584 kw = {
585 kw = {
585 'time': current_time,
586 'time': current_time,
586 'offset': offset
587 'offset': offset
587 }
588 }
588 author = create_signature_from_string(author, **kw)
589 author = create_signature_from_string(author, **kw)
589 committer = create_signature_from_string(committer, **kw)
590 committer = create_signature_from_string(committer, **kw)
590
591
591 tree = new_tree_id
592 tree = new_tree_id
592 if isinstance(tree, (bytes, str)):
593 if isinstance(tree, (bytes, str)):
593 # validate this tree is in the repo...
594 # validate this tree is in the repo...
594 tree = repo[safe_str(tree)].id
595 tree = repo[safe_str(tree)].id
595
596
596 if parents:
597 if parents:
597 # run via sha's and validate them in repo
598 # run via sha's and validate them in repo
598 parents = [repo[c].id for c in parents]
599 parents = [repo[c].id for c in parents]
599 else:
600 else:
600 parents = []
601 parents = []
601 # ensure we COMMIT on top of given branch head
602 # ensure we COMMIT on top of given branch head
602 # check if this repo has ANY branches, otherwise it's a new branch case we need to make
603 # check if this repo has ANY branches, otherwise it's a new branch case we need to make
603 if branch in repo.branches.local:
604 if branch in repo.branches.local:
604 parents += [repo.branches[branch].target]
605 parents += [repo.branches[branch].target]
605 elif [x for x in repo.branches.local]:
606 elif [x for x in repo.branches.local]:
606 parents += [repo.head.target]
607 parents += [repo.head.target]
607 #else:
608 #else:
608 # in case we want to commit on new branch we create it on top of HEAD
609 # in case we want to commit on new branch we create it on top of HEAD
609 #repo.branches.local.create(branch, repo.revparse_single('HEAD'))
610 #repo.branches.local.create(branch, repo.revparse_single('HEAD'))
610
611
611 # # Create a new commit
612 # # Create a new commit
612 commit_oid = repo.create_commit(
613 commit_oid = repo.create_commit(
613 f'refs/heads/{branch}', # the name of the reference to update
614 f'refs/heads/{branch}', # the name of the reference to update
614 author, # the author of the commit
615 author, # the author of the commit
615 committer, # the committer of the commit
616 committer, # the committer of the commit
616 message, # the commit message
617 message, # the commit message
617 tree, # the tree produced by the index
618 tree, # the tree produced by the index
618 parents # list of parents for the new commit, usually just one,
619 parents # list of parents for the new commit, usually just one,
619 )
620 )
620
621
621 new_commit_id = safe_str(commit_oid)
622 new_commit_id = safe_str(commit_oid)
622
623
623 return new_commit_id
624 return new_commit_id
624
625
625 @reraise_safe_exceptions
626 @reraise_safe_exceptions
626 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
627 def commit(self, wire, commit_data, branch, commit_tree, updated, removed):
627
628
628 def mode2pygit(mode):
629 def mode2pygit(mode):
629 """
630 """
630 git only supports two filemode 644 and 755
631 git only supports two filemode 644 and 755
631
632
632 0o100755 -> 33261
633 0o100755 -> 33261
633 0o100644 -> 33188
634 0o100644 -> 33188
634 """
635 """
635 return {
636 return {
636 0o100644: pygit2.GIT_FILEMODE_BLOB,
637 0o100644: pygit2.GIT_FILEMODE_BLOB,
637 0o100755: pygit2.GIT_FILEMODE_BLOB_EXECUTABLE,
638 0o100755: pygit2.GIT_FILEMODE_BLOB_EXECUTABLE,
638 0o120000: pygit2.GIT_FILEMODE_LINK
639 0o120000: pygit2.GIT_FILEMODE_LINK
639 }.get(mode) or pygit2.GIT_FILEMODE_BLOB
640 }.get(mode) or pygit2.GIT_FILEMODE_BLOB
640
641
641 repo_init = self._factory.repo_libgit2(wire)
642 repo_init = self._factory.repo_libgit2(wire)
642 with repo_init as repo:
643 with repo_init as repo:
643 repo_index = repo.index
644 repo_index = repo.index
644
645
645 commit_parents = None
646 commit_parents = None
646 if commit_tree and commit_data['parents']:
647 if commit_tree and commit_data['parents']:
647 commit_parents = commit_data['parents']
648 commit_parents = commit_data['parents']
648 parent_commit = repo[commit_parents[0]]
649 parent_commit = repo[commit_parents[0]]
649 repo_index.read_tree(parent_commit.tree)
650 repo_index.read_tree(parent_commit.tree)
650
651
651 for pathspec in updated:
652 for pathspec in updated:
652 blob_id = repo.create_blob(pathspec['content'])
653 blob_id = repo.create_blob(pathspec['content'])
653 ie = pygit2.IndexEntry(pathspec['path'], blob_id, mode2pygit(pathspec['mode']))
654 ie = pygit2.IndexEntry(pathspec['path'], blob_id, mode2pygit(pathspec['mode']))
654 repo_index.add(ie)
655 repo_index.add(ie)
655
656
656 for pathspec in removed:
657 for pathspec in removed:
657 repo_index.remove(pathspec)
658 repo_index.remove(pathspec)
658
659
659 # Write changes to the index
660 # Write changes to the index
660 repo_index.write()
661 repo_index.write()
661
662
662 # Create a tree from the updated index
663 # Create a tree from the updated index
663 written_commit_tree = repo_index.write_tree()
664 written_commit_tree = repo_index.write_tree()
664
665
665 new_tree_id = written_commit_tree
666 new_tree_id = written_commit_tree
666
667
667 author = commit_data['author']
668 author = commit_data['author']
668 committer = commit_data['committer']
669 committer = commit_data['committer']
669 message = commit_data['message']
670 message = commit_data['message']
670
671
671 date_args = [int(commit_data['commit_time']), int(commit_data['commit_timezone'])]
672 date_args = [int(commit_data['commit_time']), int(commit_data['commit_timezone'])]
672
673
673 new_commit_id = self.create_commit(wire, author, committer, message, branch,
674 new_commit_id = self.create_commit(wire, author, committer, message, branch,
674 new_tree_id, date_args=date_args, parents=commit_parents)
675 new_tree_id, date_args=date_args, parents=commit_parents)
675
676
676 # libgit2, ensure the branch is there and exists
677 # libgit2, ensure the branch is there and exists
677 self.create_branch(wire, branch, new_commit_id)
678 self.create_branch(wire, branch, new_commit_id)
678
679
679 # libgit2, set new ref to this created commit
680 # libgit2, set new ref to this created commit
680 self.set_refs(wire, f'refs/heads/{branch}', new_commit_id)
681 self.set_refs(wire, f'refs/heads/{branch}', new_commit_id)
681
682
682 return new_commit_id
683 return new_commit_id
683
684
684 @reraise_safe_exceptions
685 @reraise_safe_exceptions
685 def pull(self, wire, url, apply_refs=True, refs=None, update_after=False):
686 def pull(self, wire, url, apply_refs=True, refs=None, update_after=False):
686 if url != 'default' and '://' not in url:
687 if url != 'default' and '://' not in url:
687 client = LocalGitClient(url)
688 client = LocalGitClient(url)
688 else:
689 else:
689 url_obj = url_parser(safe_bytes(url))
690 url_obj = url_parser(safe_bytes(url))
690 o = self._build_opener(url)
691 o = self._build_opener(url)
691 url = url_obj.authinfo()[0]
692 url = url_obj.authinfo()[0]
692 client = HttpGitClient(base_url=url, opener=o)
693 client = HttpGitClient(base_url=url, opener=o)
693 repo = self._factory.repo(wire)
694 repo = self._factory.repo(wire)
694
695
695 determine_wants = repo.object_store.determine_wants_all
696 determine_wants = repo.object_store.determine_wants_all
696
697
697 if refs:
698 if refs:
698 refs: list[bytes] = [ascii_bytes(x) for x in refs]
699 refs: list[bytes] = [ascii_bytes(x) for x in refs]
699
700
700 def determine_wants_requested(_remote_refs):
701 def determine_wants_requested(_remote_refs):
701 determined = []
702 determined = []
702 for ref_name, ref_hash in _remote_refs.items():
703 for ref_name, ref_hash in _remote_refs.items():
703 bytes_ref_name = safe_bytes(ref_name)
704 bytes_ref_name = safe_bytes(ref_name)
704
705
705 if bytes_ref_name in refs:
706 if bytes_ref_name in refs:
706 bytes_ref_hash = safe_bytes(ref_hash)
707 bytes_ref_hash = safe_bytes(ref_hash)
707 determined.append(bytes_ref_hash)
708 determined.append(bytes_ref_hash)
708 return determined
709 return determined
709
710
710 # swap with our custom requested wants
711 # swap with our custom requested wants
711 determine_wants = determine_wants_requested
712 determine_wants = determine_wants_requested
712
713
713 try:
714 try:
714 remote_refs = client.fetch(
715 remote_refs = client.fetch(
715 path=url, target=repo, determine_wants=determine_wants)
716 path=url, target=repo, determine_wants=determine_wants)
716
717
717 except NotGitRepository as e:
718 except NotGitRepository as e:
718 log.warning(
719 log.warning(
719 'Trying to fetch from "%s" failed, not a Git repository.', url)
720 'Trying to fetch from "%s" failed, not a Git repository.', url)
720 # Exception can contain unicode which we convert
721 # Exception can contain unicode which we convert
721 raise exceptions.AbortException(e)(repr(e))
722 raise exceptions.AbortException(e)(repr(e))
722
723
723 # mikhail: client.fetch() returns all the remote refs, but fetches only
724 # mikhail: client.fetch() returns all the remote refs, but fetches only
724 # refs filtered by `determine_wants` function. We need to filter result
725 # refs filtered by `determine_wants` function. We need to filter result
725 # as well
726 # as well
726 if refs:
727 if refs:
727 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
728 remote_refs = {k: remote_refs[k] for k in remote_refs if k in refs}
728
729
729 if apply_refs:
730 if apply_refs:
730 # TODO: johbo: Needs proper test coverage with a git repository
731 # TODO: johbo: Needs proper test coverage with a git repository
731 # that contains a tag object, so that we would end up with
732 # that contains a tag object, so that we would end up with
732 # a peeled ref at this point.
733 # a peeled ref at this point.
733 for k in remote_refs:
734 for k in remote_refs:
734 if k.endswith(PEELED_REF_MARKER):
735 if k.endswith(PEELED_REF_MARKER):
735 log.debug("Skipping peeled reference %s", k)
736 log.debug("Skipping peeled reference %s", k)
736 continue
737 continue
737 repo[k] = remote_refs[k]
738 repo[k] = remote_refs[k]
738
739
739 if refs and not update_after:
740 if refs and not update_after:
740 # update to ref
741 # update to ref
741 # mikhail: explicitly set the head to the last ref.
742 # mikhail: explicitly set the head to the last ref.
742 update_to_ref = refs[-1]
743 update_to_ref = refs[-1]
743 if isinstance(update_after, str):
744 if isinstance(update_after, str):
744 update_to_ref = update_after
745 update_to_ref = update_after
745
746
746 repo[HEAD_MARKER] = remote_refs[update_to_ref]
747 repo[HEAD_MARKER] = remote_refs[update_to_ref]
747
748
748 if update_after:
749 if update_after:
749 # we want to check out HEAD
750 # we want to check out HEAD
750 repo[HEAD_MARKER] = remote_refs[HEAD_MARKER]
751 repo[HEAD_MARKER] = remote_refs[HEAD_MARKER]
751 index.build_index_from_tree(repo.path, repo.index_path(),
752 index.build_index_from_tree(repo.path, repo.index_path(),
752 repo.object_store, repo[HEAD_MARKER].tree)
753 repo.object_store, repo[HEAD_MARKER].tree)
753
754
754 if isinstance(remote_refs, FetchPackResult):
755 if isinstance(remote_refs, FetchPackResult):
755 return remote_refs.refs
756 return remote_refs.refs
756 return remote_refs
757 return remote_refs
757
758
758 @reraise_safe_exceptions
759 @reraise_safe_exceptions
759 def sync_fetch(self, wire, url, refs=None, all_refs=False):
760 def sync_fetch(self, wire, url, refs=None, all_refs=False):
760 self._factory.repo(wire)
761 self._factory.repo(wire)
761 if refs and not isinstance(refs, (list, tuple)):
762 if refs and not isinstance(refs, (list, tuple)):
762 refs = [refs]
763 refs = [refs]
763
764
764 config = self._wire_to_config(wire)
765 config = self._wire_to_config(wire)
765 # get all remote refs we'll use to fetch later
766 # get all remote refs we'll use to fetch later
766 cmd = ['ls-remote']
767 cmd = ['ls-remote']
767 if not all_refs:
768 if not all_refs:
768 cmd += ['--heads', '--tags']
769 cmd += ['--heads', '--tags']
769 cmd += [url]
770 cmd += [url]
770 output, __ = self.run_git_command(
771 output, __ = self.run_git_command(
771 wire, cmd, fail_on_stderr=False,
772 wire, cmd, fail_on_stderr=False,
772 _copts=self._remote_conf(config),
773 _copts=self._remote_conf(config),
773 extra_env={'GIT_TERMINAL_PROMPT': '0'})
774 extra_env={'GIT_TERMINAL_PROMPT': '0'})
774
775
775 remote_refs = collections.OrderedDict()
776 remote_refs = collections.OrderedDict()
776 fetch_refs = []
777 fetch_refs = []
777
778
778 for ref_line in output.splitlines():
779 for ref_line in output.splitlines():
779 sha, ref = ref_line.split(b'\t')
780 sha, ref = ref_line.split(b'\t')
780 sha = sha.strip()
781 sha = sha.strip()
781 if ref in remote_refs:
782 if ref in remote_refs:
782 # duplicate, skip
783 # duplicate, skip
783 continue
784 continue
784 if ref.endswith(PEELED_REF_MARKER):
785 if ref.endswith(PEELED_REF_MARKER):
785 log.debug("Skipping peeled reference %s", ref)
786 log.debug("Skipping peeled reference %s", ref)
786 continue
787 continue
787 # don't sync HEAD
788 # don't sync HEAD
788 if ref in [HEAD_MARKER]:
789 if ref in [HEAD_MARKER]:
789 continue
790 continue
790
791
791 remote_refs[ref] = sha
792 remote_refs[ref] = sha
792
793
793 if refs and sha in refs:
794 if refs and sha in refs:
794 # we filter fetch using our specified refs
795 # we filter fetch using our specified refs
795 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
796 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
796 elif not refs:
797 elif not refs:
797 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
798 fetch_refs.append(f'{safe_str(ref)}:{safe_str(ref)}')
798 log.debug('Finished obtaining fetch refs, total: %s', len(fetch_refs))
799 log.debug('Finished obtaining fetch refs, total: %s', len(fetch_refs))
799
800
800 if fetch_refs:
801 if fetch_refs:
801 for chunk in more_itertools.chunked(fetch_refs, 1024 * 4):
802 for chunk in more_itertools.chunked(fetch_refs, 1024 * 4):
802 fetch_refs_chunks = list(chunk)
803 fetch_refs_chunks = list(chunk)
803 log.debug('Fetching %s refs from import url', len(fetch_refs_chunks))
804 log.debug('Fetching %s refs from import url', len(fetch_refs_chunks))
804 self.run_git_command(
805 self.run_git_command(
805 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs_chunks,
806 wire, ['fetch', url, '--force', '--prune', '--'] + fetch_refs_chunks,
806 fail_on_stderr=False,
807 fail_on_stderr=False,
807 _copts=self._remote_conf(config),
808 _copts=self._remote_conf(config),
808 extra_env={'GIT_TERMINAL_PROMPT': '0'})
809 extra_env={'GIT_TERMINAL_PROMPT': '0'})
809
810
810 return remote_refs
811 return remote_refs
811
812
812 @reraise_safe_exceptions
813 @reraise_safe_exceptions
813 def sync_push(self, wire, url, refs=None):
814 def sync_push(self, wire, url, refs=None):
814 if not self.check_url(url, wire):
815 if not self.check_url(url, wire):
815 return
816 return
816 config = self._wire_to_config(wire)
817 config = self._wire_to_config(wire)
817 self._factory.repo(wire)
818 self._factory.repo(wire)
818 self.run_git_command(
819 self.run_git_command(
819 wire, ['push', url, '--mirror'], fail_on_stderr=False,
820 wire, ['push', url, '--mirror'], fail_on_stderr=False,
820 _copts=self._remote_conf(config),
821 _copts=self._remote_conf(config),
821 extra_env={'GIT_TERMINAL_PROMPT': '0'})
822 extra_env={'GIT_TERMINAL_PROMPT': '0'})
822
823
823 @reraise_safe_exceptions
824 @reraise_safe_exceptions
824 def get_remote_refs(self, wire, url):
825 def get_remote_refs(self, wire, url):
825 repo = Repo(url)
826 repo = Repo(url)
826 return repo.get_refs()
827 return repo.get_refs()
827
828
828 @reraise_safe_exceptions
829 @reraise_safe_exceptions
829 def get_description(self, wire):
830 def get_description(self, wire):
830 repo = self._factory.repo(wire)
831 repo = self._factory.repo(wire)
831 return repo.get_description()
832 return repo.get_description()
832
833
833 @reraise_safe_exceptions
834 @reraise_safe_exceptions
834 def get_missing_revs(self, wire, rev1, rev2, other_repo_path):
835 def get_missing_revs(self, wire, rev1, rev2, other_repo_path):
835 origin_repo_path = wire['path']
836 origin_repo_path = wire['path']
836 repo = self._factory.repo(wire)
837 repo = self._factory.repo(wire)
837 # fetch from other_repo_path to our origin repo
838 # fetch from other_repo_path to our origin repo
838 LocalGitClient(thin_packs=False).fetch(other_repo_path, repo)
839 LocalGitClient(thin_packs=False).fetch(other_repo_path, repo)
839
840
840 wire_remote = wire.copy()
841 wire_remote = wire.copy()
841 wire_remote['path'] = other_repo_path
842 wire_remote['path'] = other_repo_path
842 repo_remote = self._factory.repo(wire_remote)
843 repo_remote = self._factory.repo(wire_remote)
843
844
844 # fetch from origin_repo_path to our remote repo
845 # fetch from origin_repo_path to our remote repo
845 LocalGitClient(thin_packs=False).fetch(origin_repo_path, repo_remote)
846 LocalGitClient(thin_packs=False).fetch(origin_repo_path, repo_remote)
846
847
847 revs = [
848 revs = [
848 x.commit.id
849 x.commit.id
849 for x in repo_remote.get_walker(include=[safe_bytes(rev2)], exclude=[safe_bytes(rev1)])]
850 for x in repo_remote.get_walker(include=[safe_bytes(rev2)], exclude=[safe_bytes(rev1)])]
850 return revs
851 return revs
851
852
852 @reraise_safe_exceptions
853 @reraise_safe_exceptions
853 def get_object(self, wire, sha, maybe_unreachable=False):
854 def get_object(self, wire, sha, maybe_unreachable=False):
854 cache_on, context_uid, repo_id = self._cache_on(wire)
855 cache_on, context_uid, repo_id = self._cache_on(wire)
855 region = self._region(wire)
856 region = self._region(wire)
856
857
857 @region.conditional_cache_on_arguments(condition=cache_on)
858 @region.conditional_cache_on_arguments(condition=cache_on)
858 def _get_object(_context_uid, _repo_id, _sha):
859 def _get_object(_context_uid, _repo_id, _sha):
859 repo_init = self._factory.repo_libgit2(wire)
860 repo_init = self._factory.repo_libgit2(wire)
860 with repo_init as repo:
861 with repo_init as repo:
861
862
862 missing_commit_err = 'Commit {} does not exist for `{}`'.format(sha, wire['path'])
863 missing_commit_err = 'Commit {} does not exist for `{}`'.format(sha, wire['path'])
863 try:
864 try:
864 commit = repo.revparse_single(sha)
865 commit = repo.revparse_single(sha)
865 except KeyError:
866 except KeyError:
866 # NOTE(marcink): KeyError doesn't give us any meaningful information
867 # NOTE(marcink): KeyError doesn't give us any meaningful information
867 # here, we instead give something more explicit
868 # here, we instead give something more explicit
868 e = exceptions.RefNotFoundException('SHA: %s not found', sha)
869 e = exceptions.RefNotFoundException('SHA: %s not found', sha)
869 raise exceptions.LookupException(e)(missing_commit_err)
870 raise exceptions.LookupException(e)(missing_commit_err)
870 except ValueError as e:
871 except ValueError as e:
871 raise exceptions.LookupException(e)(missing_commit_err)
872 raise exceptions.LookupException(e)(missing_commit_err)
872
873
873 is_tag = False
874 is_tag = False
874 if isinstance(commit, pygit2.Tag):
875 if isinstance(commit, pygit2.Tag):
875 commit = repo.get(commit.target)
876 commit = repo.get(commit.target)
876 is_tag = True
877 is_tag = True
877
878
878 check_dangling = True
879 check_dangling = True
879 if is_tag:
880 if is_tag:
880 check_dangling = False
881 check_dangling = False
881
882
882 if check_dangling and maybe_unreachable:
883 if check_dangling and maybe_unreachable:
883 check_dangling = False
884 check_dangling = False
884
885
885 # we used a reference and it parsed means we're not having a dangling commit
886 # we used a reference and it parsed means we're not having a dangling commit
886 if sha != commit.hex:
887 if sha != commit.hex:
887 check_dangling = False
888 check_dangling = False
888
889
889 if check_dangling:
890 if check_dangling:
890 # check for dangling commit
891 # check for dangling commit
891 for branch in repo.branches.with_commit(commit.hex):
892 for branch in repo.branches.with_commit(commit.hex):
892 if branch:
893 if branch:
893 break
894 break
894 else:
895 else:
895 # NOTE(marcink): Empty error doesn't give us any meaningful information
896 # NOTE(marcink): Empty error doesn't give us any meaningful information
896 # here, we instead give something more explicit
897 # here, we instead give something more explicit
897 e = exceptions.RefNotFoundException('SHA: %s not found in branches', sha)
898 e = exceptions.RefNotFoundException('SHA: %s not found in branches', sha)
898 raise exceptions.LookupException(e)(missing_commit_err)
899 raise exceptions.LookupException(e)(missing_commit_err)
899
900
900 commit_id = commit.hex
901 commit_id = commit.hex
901 type_str = commit.type_str
902 type_str = commit.type_str
902
903
903 return {
904 return {
904 'id': commit_id,
905 'id': commit_id,
905 'type': type_str,
906 'type': type_str,
906 'commit_id': commit_id,
907 'commit_id': commit_id,
907 'idx': 0
908 'idx': 0
908 }
909 }
909
910
910 return _get_object(context_uid, repo_id, sha)
911 return _get_object(context_uid, repo_id, sha)
911
912
912 @reraise_safe_exceptions
913 @reraise_safe_exceptions
913 def get_refs(self, wire):
914 def get_refs(self, wire):
914 cache_on, context_uid, repo_id = self._cache_on(wire)
915 cache_on, context_uid, repo_id = self._cache_on(wire)
915 region = self._region(wire)
916 region = self._region(wire)
916
917
917 @region.conditional_cache_on_arguments(condition=cache_on)
918 @region.conditional_cache_on_arguments(condition=cache_on)
918 def _get_refs(_context_uid, _repo_id):
919 def _get_refs(_context_uid, _repo_id):
919
920
920 repo_init = self._factory.repo_libgit2(wire)
921 repo_init = self._factory.repo_libgit2(wire)
921 with repo_init as repo:
922 with repo_init as repo:
922 regex = re.compile('^refs/(heads|tags)/')
923 regex = re.compile('^refs/(heads|tags)/')
923 return {x.name: x.target.hex for x in
924 return {x.name: x.target.hex for x in
924 [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]}
925 [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]}
925
926
926 return _get_refs(context_uid, repo_id)
927 return _get_refs(context_uid, repo_id)
927
928
928 @reraise_safe_exceptions
929 @reraise_safe_exceptions
929 def get_branch_pointers(self, wire):
930 def get_branch_pointers(self, wire):
930 cache_on, context_uid, repo_id = self._cache_on(wire)
931 cache_on, context_uid, repo_id = self._cache_on(wire)
931 region = self._region(wire)
932 region = self._region(wire)
932
933
933 @region.conditional_cache_on_arguments(condition=cache_on)
934 @region.conditional_cache_on_arguments(condition=cache_on)
934 def _get_branch_pointers(_context_uid, _repo_id):
935 def _get_branch_pointers(_context_uid, _repo_id):
935
936
936 repo_init = self._factory.repo_libgit2(wire)
937 repo_init = self._factory.repo_libgit2(wire)
937 regex = re.compile('^refs/heads')
938 regex = re.compile('^refs/heads')
938 with repo_init as repo:
939 with repo_init as repo:
939 branches = [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]
940 branches = [ref for ref in repo.listall_reference_objects() if regex.match(ref.name)]
940 return {x.target.hex: x.shorthand for x in branches}
941 return {x.target.hex: x.shorthand for x in branches}
941
942
942 return _get_branch_pointers(context_uid, repo_id)
943 return _get_branch_pointers(context_uid, repo_id)
943
944
944 @reraise_safe_exceptions
945 @reraise_safe_exceptions
945 def head(self, wire, show_exc=True):
946 def head(self, wire, show_exc=True):
946 cache_on, context_uid, repo_id = self._cache_on(wire)
947 cache_on, context_uid, repo_id = self._cache_on(wire)
947 region = self._region(wire)
948 region = self._region(wire)
948
949
949 @region.conditional_cache_on_arguments(condition=cache_on)
950 @region.conditional_cache_on_arguments(condition=cache_on)
950 def _head(_context_uid, _repo_id, _show_exc):
951 def _head(_context_uid, _repo_id, _show_exc):
951 repo_init = self._factory.repo_libgit2(wire)
952 repo_init = self._factory.repo_libgit2(wire)
952 with repo_init as repo:
953 with repo_init as repo:
953 try:
954 try:
954 return repo.head.peel().hex
955 return repo.head.peel().hex
955 except Exception:
956 except Exception:
956 if show_exc:
957 if show_exc:
957 raise
958 raise
958 return _head(context_uid, repo_id, show_exc)
959 return _head(context_uid, repo_id, show_exc)
959
960
960 @reraise_safe_exceptions
961 @reraise_safe_exceptions
961 def init(self, wire):
962 def init(self, wire):
962 repo_path = safe_str(wire['path'])
963 repo_path = safe_str(wire['path'])
963 pygit2.init_repository(repo_path, bare=False)
964 pygit2.init_repository(repo_path, bare=False)
964
965
965 @reraise_safe_exceptions
966 @reraise_safe_exceptions
966 def init_bare(self, wire):
967 def init_bare(self, wire):
967 repo_path = safe_str(wire['path'])
968 repo_path = safe_str(wire['path'])
968 pygit2.init_repository(repo_path, bare=True)
969 pygit2.init_repository(repo_path, bare=True)
969
970
970 @reraise_safe_exceptions
971 @reraise_safe_exceptions
971 def revision(self, wire, rev):
972 def revision(self, wire, rev):
972
973
973 cache_on, context_uid, repo_id = self._cache_on(wire)
974 cache_on, context_uid, repo_id = self._cache_on(wire)
974 region = self._region(wire)
975 region = self._region(wire)
975
976
976 @region.conditional_cache_on_arguments(condition=cache_on)
977 @region.conditional_cache_on_arguments(condition=cache_on)
977 def _revision(_context_uid, _repo_id, _rev):
978 def _revision(_context_uid, _repo_id, _rev):
978 repo_init = self._factory.repo_libgit2(wire)
979 repo_init = self._factory.repo_libgit2(wire)
979 with repo_init as repo:
980 with repo_init as repo:
980 commit = repo[rev]
981 commit = repo[rev]
981 obj_data = {
982 obj_data = {
982 'id': commit.id.hex,
983 'id': commit.id.hex,
983 }
984 }
984 # tree objects itself don't have tree_id attribute
985 # tree objects itself don't have tree_id attribute
985 if hasattr(commit, 'tree_id'):
986 if hasattr(commit, 'tree_id'):
986 obj_data['tree'] = commit.tree_id.hex
987 obj_data['tree'] = commit.tree_id.hex
987
988
988 return obj_data
989 return obj_data
989 return _revision(context_uid, repo_id, rev)
990 return _revision(context_uid, repo_id, rev)
990
991
991 @reraise_safe_exceptions
992 @reraise_safe_exceptions
992 def date(self, wire, commit_id):
993 def date(self, wire, commit_id):
993 cache_on, context_uid, repo_id = self._cache_on(wire)
994 cache_on, context_uid, repo_id = self._cache_on(wire)
994 region = self._region(wire)
995 region = self._region(wire)
995
996
996 @region.conditional_cache_on_arguments(condition=cache_on)
997 @region.conditional_cache_on_arguments(condition=cache_on)
997 def _date(_repo_id, _commit_id):
998 def _date(_repo_id, _commit_id):
998 repo_init = self._factory.repo_libgit2(wire)
999 repo_init = self._factory.repo_libgit2(wire)
999 with repo_init as repo:
1000 with repo_init as repo:
1000 commit = repo[commit_id]
1001 commit = repo[commit_id]
1001
1002
1002 if hasattr(commit, 'commit_time'):
1003 if hasattr(commit, 'commit_time'):
1003 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
1004 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
1004 else:
1005 else:
1005 commit = commit.get_object()
1006 commit = commit.get_object()
1006 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
1007 commit_time, commit_time_offset = commit.commit_time, commit.commit_time_offset
1007
1008
1008 # TODO(marcink): check dulwich difference of offset vs timezone
1009 # TODO(marcink): check dulwich difference of offset vs timezone
1009 return [commit_time, commit_time_offset]
1010 return [commit_time, commit_time_offset]
1010 return _date(repo_id, commit_id)
1011 return _date(repo_id, commit_id)
1011
1012
1012 @reraise_safe_exceptions
1013 @reraise_safe_exceptions
1013 def author(self, wire, commit_id):
1014 def author(self, wire, commit_id):
1014 cache_on, context_uid, repo_id = self._cache_on(wire)
1015 cache_on, context_uid, repo_id = self._cache_on(wire)
1015 region = self._region(wire)
1016 region = self._region(wire)
1016
1017
1017 @region.conditional_cache_on_arguments(condition=cache_on)
1018 @region.conditional_cache_on_arguments(condition=cache_on)
1018 def _author(_repo_id, _commit_id):
1019 def _author(_repo_id, _commit_id):
1019 repo_init = self._factory.repo_libgit2(wire)
1020 repo_init = self._factory.repo_libgit2(wire)
1020 with repo_init as repo:
1021 with repo_init as repo:
1021 commit = repo[commit_id]
1022 commit = repo[commit_id]
1022
1023
1023 if hasattr(commit, 'author'):
1024 if hasattr(commit, 'author'):
1024 author = commit.author
1025 author = commit.author
1025 else:
1026 else:
1026 author = commit.get_object().author
1027 author = commit.get_object().author
1027
1028
1028 if author.email:
1029 if author.email:
1029 return f"{author.name} <{author.email}>"
1030 return f"{author.name} <{author.email}>"
1030
1031
1031 try:
1032 try:
1032 return f"{author.name}"
1033 return f"{author.name}"
1033 except Exception:
1034 except Exception:
1034 return f"{safe_str(author.raw_name)}"
1035 return f"{safe_str(author.raw_name)}"
1035
1036
1036 return _author(repo_id, commit_id)
1037 return _author(repo_id, commit_id)
1037
1038
1038 @reraise_safe_exceptions
1039 @reraise_safe_exceptions
1039 def message(self, wire, commit_id):
1040 def message(self, wire, commit_id):
1040 cache_on, context_uid, repo_id = self._cache_on(wire)
1041 cache_on, context_uid, repo_id = self._cache_on(wire)
1041 region = self._region(wire)
1042 region = self._region(wire)
1042
1043
1043 @region.conditional_cache_on_arguments(condition=cache_on)
1044 @region.conditional_cache_on_arguments(condition=cache_on)
1044 def _message(_repo_id, _commit_id):
1045 def _message(_repo_id, _commit_id):
1045 repo_init = self._factory.repo_libgit2(wire)
1046 repo_init = self._factory.repo_libgit2(wire)
1046 with repo_init as repo:
1047 with repo_init as repo:
1047 commit = repo[commit_id]
1048 commit = repo[commit_id]
1048 return commit.message
1049 return commit.message
1049 return _message(repo_id, commit_id)
1050 return _message(repo_id, commit_id)
1050
1051
1051 @reraise_safe_exceptions
1052 @reraise_safe_exceptions
1052 def parents(self, wire, commit_id):
1053 def parents(self, wire, commit_id):
1053 cache_on, context_uid, repo_id = self._cache_on(wire)
1054 cache_on, context_uid, repo_id = self._cache_on(wire)
1054 region = self._region(wire)
1055 region = self._region(wire)
1055
1056
1056 @region.conditional_cache_on_arguments(condition=cache_on)
1057 @region.conditional_cache_on_arguments(condition=cache_on)
1057 def _parents(_repo_id, _commit_id):
1058 def _parents(_repo_id, _commit_id):
1058 repo_init = self._factory.repo_libgit2(wire)
1059 repo_init = self._factory.repo_libgit2(wire)
1059 with repo_init as repo:
1060 with repo_init as repo:
1060 commit = repo[commit_id]
1061 commit = repo[commit_id]
1061 if hasattr(commit, 'parent_ids'):
1062 if hasattr(commit, 'parent_ids'):
1062 parent_ids = commit.parent_ids
1063 parent_ids = commit.parent_ids
1063 else:
1064 else:
1064 parent_ids = commit.get_object().parent_ids
1065 parent_ids = commit.get_object().parent_ids
1065
1066
1066 return [x.hex for x in parent_ids]
1067 return [x.hex for x in parent_ids]
1067 return _parents(repo_id, commit_id)
1068 return _parents(repo_id, commit_id)
1068
1069
1069 @reraise_safe_exceptions
1070 @reraise_safe_exceptions
1070 def children(self, wire, commit_id):
1071 def children(self, wire, commit_id):
1071 cache_on, context_uid, repo_id = self._cache_on(wire)
1072 cache_on, context_uid, repo_id = self._cache_on(wire)
1072 region = self._region(wire)
1073 region = self._region(wire)
1073
1074
1074 head = self.head(wire)
1075 head = self.head(wire)
1075
1076
1076 @region.conditional_cache_on_arguments(condition=cache_on)
1077 @region.conditional_cache_on_arguments(condition=cache_on)
1077 def _children(_repo_id, _commit_id):
1078 def _children(_repo_id, _commit_id):
1078
1079
1079 output, __ = self.run_git_command(
1080 output, __ = self.run_git_command(
1080 wire, ['rev-list', '--all', '--children', f'{commit_id}^..{head}'])
1081 wire, ['rev-list', '--all', '--children', f'{commit_id}^..{head}'])
1081
1082
1082 child_ids = []
1083 child_ids = []
1083 pat = re.compile(fr'^{commit_id}')
1084 pat = re.compile(fr'^{commit_id}')
1084 for line in output.splitlines():
1085 for line in output.splitlines():
1085 line = safe_str(line)
1086 line = safe_str(line)
1086 if pat.match(line):
1087 if pat.match(line):
1087 found_ids = line.split(' ')[1:]
1088 found_ids = line.split(' ')[1:]
1088 child_ids.extend(found_ids)
1089 child_ids.extend(found_ids)
1089 break
1090 break
1090
1091
1091 return child_ids
1092 return child_ids
1092 return _children(repo_id, commit_id)
1093 return _children(repo_id, commit_id)
1093
1094
1094 @reraise_safe_exceptions
1095 @reraise_safe_exceptions
1095 def set_refs(self, wire, key, value):
1096 def set_refs(self, wire, key, value):
1096 repo_init = self._factory.repo_libgit2(wire)
1097 repo_init = self._factory.repo_libgit2(wire)
1097 with repo_init as repo:
1098 with repo_init as repo:
1098 repo.references.create(key, value, force=True)
1099 repo.references.create(key, value, force=True)
1099
1100
1100 @reraise_safe_exceptions
1101 @reraise_safe_exceptions
1101 def create_branch(self, wire, branch_name, commit_id, force=False):
1102 def create_branch(self, wire, branch_name, commit_id, force=False):
1102 repo_init = self._factory.repo_libgit2(wire)
1103 repo_init = self._factory.repo_libgit2(wire)
1103 with repo_init as repo:
1104 with repo_init as repo:
1104 if commit_id:
1105 if commit_id:
1105 commit = repo[commit_id]
1106 commit = repo[commit_id]
1106 else:
1107 else:
1107 # if commit is not given just use the HEAD
1108 # if commit is not given just use the HEAD
1108 commit = repo.head()
1109 commit = repo.head()
1109
1110
1110 if force:
1111 if force:
1111 repo.branches.local.create(branch_name, commit, force=force)
1112 repo.branches.local.create(branch_name, commit, force=force)
1112 elif not repo.branches.get(branch_name):
1113 elif not repo.branches.get(branch_name):
1113 # create only if that branch isn't existing
1114 # create only if that branch isn't existing
1114 repo.branches.local.create(branch_name, commit, force=force)
1115 repo.branches.local.create(branch_name, commit, force=force)
1115
1116
1116 @reraise_safe_exceptions
1117 @reraise_safe_exceptions
1117 def remove_ref(self, wire, key):
1118 def remove_ref(self, wire, key):
1118 repo_init = self._factory.repo_libgit2(wire)
1119 repo_init = self._factory.repo_libgit2(wire)
1119 with repo_init as repo:
1120 with repo_init as repo:
1120 repo.references.delete(key)
1121 repo.references.delete(key)
1121
1122
1122 @reraise_safe_exceptions
1123 @reraise_safe_exceptions
1123 def tag_remove(self, wire, tag_name):
1124 def tag_remove(self, wire, tag_name):
1124 repo_init = self._factory.repo_libgit2(wire)
1125 repo_init = self._factory.repo_libgit2(wire)
1125 with repo_init as repo:
1126 with repo_init as repo:
1126 key = f'refs/tags/{tag_name}'
1127 key = f'refs/tags/{tag_name}'
1127 repo.references.delete(key)
1128 repo.references.delete(key)
1128
1129
1129 @reraise_safe_exceptions
1130 @reraise_safe_exceptions
1130 def tree_changes(self, wire, source_id, target_id):
1131 def tree_changes(self, wire, source_id, target_id):
1131 repo = self._factory.repo(wire)
1132 repo = self._factory.repo(wire)
1132 # source can be empty
1133 # source can be empty
1133 source_id = safe_bytes(source_id if source_id else b'')
1134 source_id = safe_bytes(source_id if source_id else b'')
1134 target_id = safe_bytes(target_id)
1135 target_id = safe_bytes(target_id)
1135
1136
1136 source = repo[source_id].tree if source_id else None
1137 source = repo[source_id].tree if source_id else None
1137 target = repo[target_id].tree
1138 target = repo[target_id].tree
1138 result = repo.object_store.tree_changes(source, target)
1139 result = repo.object_store.tree_changes(source, target)
1139
1140
1140 added = set()
1141 added = set()
1141 modified = set()
1142 modified = set()
1142 deleted = set()
1143 deleted = set()
1143 for (old_path, new_path), (_, _), (_, _) in list(result):
1144 for (old_path, new_path), (_, _), (_, _) in list(result):
1144 if new_path and old_path:
1145 if new_path and old_path:
1145 modified.add(new_path)
1146 modified.add(new_path)
1146 elif new_path and not old_path:
1147 elif new_path and not old_path:
1147 added.add(new_path)
1148 added.add(new_path)
1148 elif not new_path and old_path:
1149 elif not new_path and old_path:
1149 deleted.add(old_path)
1150 deleted.add(old_path)
1150
1151
1151 return list(added), list(modified), list(deleted)
1152 return list(added), list(modified), list(deleted)
1152
1153
1153 @reraise_safe_exceptions
1154 @reraise_safe_exceptions
1154 def tree_and_type_for_path(self, wire, commit_id, path):
1155 def tree_and_type_for_path(self, wire, commit_id, path):
1155
1156
1156 cache_on, context_uid, repo_id = self._cache_on(wire)
1157 cache_on, context_uid, repo_id = self._cache_on(wire)
1157 region = self._region(wire)
1158 region = self._region(wire)
1158
1159
1159 @region.conditional_cache_on_arguments(condition=cache_on)
1160 @region.conditional_cache_on_arguments(condition=cache_on)
1160 def _tree_and_type_for_path(_context_uid, _repo_id, _commit_id, _path):
1161 def _tree_and_type_for_path(_context_uid, _repo_id, _commit_id, _path):
1161 repo_init = self._factory.repo_libgit2(wire)
1162 repo_init = self._factory.repo_libgit2(wire)
1162
1163
1163 with repo_init as repo:
1164 with repo_init as repo:
1164 commit = repo[commit_id]
1165 commit = repo[commit_id]
1165 try:
1166 try:
1166 tree = commit.tree[path]
1167 tree = commit.tree[path]
1167 except KeyError:
1168 except KeyError:
1168 return None, None, None
1169 return None, None, None
1169
1170
1170 return tree.id.hex, tree.type_str, tree.filemode
1171 return tree.id.hex, tree.type_str, tree.filemode
1171 return _tree_and_type_for_path(context_uid, repo_id, commit_id, path)
1172 return _tree_and_type_for_path(context_uid, repo_id, commit_id, path)
1172
1173
1173 @reraise_safe_exceptions
1174 @reraise_safe_exceptions
1174 def tree_items(self, wire, tree_id):
1175 def tree_items(self, wire, tree_id):
1175 cache_on, context_uid, repo_id = self._cache_on(wire)
1176 cache_on, context_uid, repo_id = self._cache_on(wire)
1176 region = self._region(wire)
1177 region = self._region(wire)
1177
1178
1178 @region.conditional_cache_on_arguments(condition=cache_on)
1179 @region.conditional_cache_on_arguments(condition=cache_on)
1179 def _tree_items(_repo_id, _tree_id):
1180 def _tree_items(_repo_id, _tree_id):
1180
1181
1181 repo_init = self._factory.repo_libgit2(wire)
1182 repo_init = self._factory.repo_libgit2(wire)
1182 with repo_init as repo:
1183 with repo_init as repo:
1183 try:
1184 try:
1184 tree = repo[tree_id]
1185 tree = repo[tree_id]
1185 except KeyError:
1186 except KeyError:
1186 raise ObjectMissing(f'No tree with id: {tree_id}')
1187 raise ObjectMissing(f'No tree with id: {tree_id}')
1187
1188
1188 result = []
1189 result = []
1189 for item in tree:
1190 for item in tree:
1190 item_sha = item.hex
1191 item_sha = item.hex
1191 item_mode = item.filemode
1192 item_mode = item.filemode
1192 item_type = item.type_str
1193 item_type = item.type_str
1193
1194
1194 if item_type == 'commit':
1195 if item_type == 'commit':
1195 # NOTE(marcink): submodules we translate to 'link' for backward compat
1196 # NOTE(marcink): submodules we translate to 'link' for backward compat
1196 item_type = 'link'
1197 item_type = 'link'
1197
1198
1198 result.append((item.name, item_mode, item_sha, item_type))
1199 result.append((item.name, item_mode, item_sha, item_type))
1199 return result
1200 return result
1200 return _tree_items(repo_id, tree_id)
1201 return _tree_items(repo_id, tree_id)
1201
1202
1202 @reraise_safe_exceptions
1203 @reraise_safe_exceptions
1203 def diff_2(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1204 def diff_2(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1204 """
1205 """
1205 Old version that uses subprocess to call diff
1206 Old version that uses subprocess to call diff
1206 """
1207 """
1207
1208
1208 flags = [
1209 flags = [
1209 f'-U{context}', '--patch',
1210 f'-U{context}', '--patch',
1210 '--binary',
1211 '--binary',
1211 '--find-renames',
1212 '--find-renames',
1212 '--no-indent-heuristic',
1213 '--no-indent-heuristic',
1213 # '--indent-heuristic',
1214 # '--indent-heuristic',
1214 #'--full-index',
1215 #'--full-index',
1215 #'--abbrev=40'
1216 #'--abbrev=40'
1216 ]
1217 ]
1217
1218
1218 if opt_ignorews:
1219 if opt_ignorews:
1219 flags.append('--ignore-all-space')
1220 flags.append('--ignore-all-space')
1220
1221
1221 if commit_id_1 == self.EMPTY_COMMIT:
1222 if commit_id_1 == self.EMPTY_COMMIT:
1222 cmd = ['show'] + flags + [commit_id_2]
1223 cmd = ['show'] + flags + [commit_id_2]
1223 else:
1224 else:
1224 cmd = ['diff'] + flags + [commit_id_1, commit_id_2]
1225 cmd = ['diff'] + flags + [commit_id_1, commit_id_2]
1225
1226
1226 if file_filter:
1227 if file_filter:
1227 cmd.extend(['--', file_filter])
1228 cmd.extend(['--', file_filter])
1228
1229
1229 diff, __ = self.run_git_command(wire, cmd)
1230 diff, __ = self.run_git_command(wire, cmd)
1230 # If we used 'show' command, strip first few lines (until actual diff
1231 # If we used 'show' command, strip first few lines (until actual diff
1231 # starts)
1232 # starts)
1232 if commit_id_1 == self.EMPTY_COMMIT:
1233 if commit_id_1 == self.EMPTY_COMMIT:
1233 lines = diff.splitlines()
1234 lines = diff.splitlines()
1234 x = 0
1235 x = 0
1235 for line in lines:
1236 for line in lines:
1236 if line.startswith(b'diff'):
1237 if line.startswith(b'diff'):
1237 break
1238 break
1238 x += 1
1239 x += 1
1239 # Append new line just like 'diff' command do
1240 # Append new line just like 'diff' command do
1240 diff = '\n'.join(lines[x:]) + '\n'
1241 diff = '\n'.join(lines[x:]) + '\n'
1241 return diff
1242 return diff
1242
1243
1243 @reraise_safe_exceptions
1244 @reraise_safe_exceptions
1244 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1245 def diff(self, wire, commit_id_1, commit_id_2, file_filter, opt_ignorews, context):
1245 repo_init = self._factory.repo_libgit2(wire)
1246 repo_init = self._factory.repo_libgit2(wire)
1246
1247
1247 with repo_init as repo:
1248 with repo_init as repo:
1248 swap = True
1249 swap = True
1249 flags = 0
1250 flags = 0
1250 flags |= pygit2.GIT_DIFF_SHOW_BINARY
1251 flags |= pygit2.GIT_DIFF_SHOW_BINARY
1251
1252
1252 if opt_ignorews:
1253 if opt_ignorews:
1253 flags |= pygit2.GIT_DIFF_IGNORE_WHITESPACE
1254 flags |= pygit2.GIT_DIFF_IGNORE_WHITESPACE
1254
1255
1255 if commit_id_1 == self.EMPTY_COMMIT:
1256 if commit_id_1 == self.EMPTY_COMMIT:
1256 comm1 = repo[commit_id_2]
1257 comm1 = repo[commit_id_2]
1257 diff_obj = comm1.tree.diff_to_tree(
1258 diff_obj = comm1.tree.diff_to_tree(
1258 flags=flags, context_lines=context, swap=swap)
1259 flags=flags, context_lines=context, swap=swap)
1259
1260
1260 else:
1261 else:
1261 comm1 = repo[commit_id_2]
1262 comm1 = repo[commit_id_2]
1262 comm2 = repo[commit_id_1]
1263 comm2 = repo[commit_id_1]
1263 diff_obj = comm1.tree.diff_to_tree(
1264 diff_obj = comm1.tree.diff_to_tree(
1264 comm2.tree, flags=flags, context_lines=context, swap=swap)
1265 comm2.tree, flags=flags, context_lines=context, swap=swap)
1265 similar_flags = 0
1266 similar_flags = 0
1266 similar_flags |= pygit2.GIT_DIFF_FIND_RENAMES
1267 similar_flags |= pygit2.GIT_DIFF_FIND_RENAMES
1267 diff_obj.find_similar(flags=similar_flags)
1268 diff_obj.find_similar(flags=similar_flags)
1268
1269
1269 if file_filter:
1270 if file_filter:
1270 for p in diff_obj:
1271 for p in diff_obj:
1271 if p.delta.old_file.path == file_filter:
1272 if p.delta.old_file.path == file_filter:
1272 return BytesEnvelope(p.data) or BytesEnvelope(b'')
1273 return BytesEnvelope(p.data) or BytesEnvelope(b'')
1273 # fo matching path == no diff
1274 # fo matching path == no diff
1274 return BytesEnvelope(b'')
1275 return BytesEnvelope(b'')
1275
1276
1276 return BytesEnvelope(safe_bytes(diff_obj.patch)) or BytesEnvelope(b'')
1277 return BytesEnvelope(safe_bytes(diff_obj.patch)) or BytesEnvelope(b'')
1277
1278
1278 @reraise_safe_exceptions
1279 @reraise_safe_exceptions
1279 def node_history(self, wire, commit_id, path, limit):
1280 def node_history(self, wire, commit_id, path, limit):
1280 cache_on, context_uid, repo_id = self._cache_on(wire)
1281 cache_on, context_uid, repo_id = self._cache_on(wire)
1281 region = self._region(wire)
1282 region = self._region(wire)
1282
1283
1283 @region.conditional_cache_on_arguments(condition=cache_on)
1284 @region.conditional_cache_on_arguments(condition=cache_on)
1284 def _node_history(_context_uid, _repo_id, _commit_id, _path, _limit):
1285 def _node_history(_context_uid, _repo_id, _commit_id, _path, _limit):
1285 # optimize for n==1, rev-list is much faster for that use-case
1286 # optimize for n==1, rev-list is much faster for that use-case
1286 if limit == 1:
1287 if limit == 1:
1287 cmd = ['rev-list', '-1', commit_id, '--', path]
1288 cmd = ['rev-list', '-1', commit_id, '--', path]
1288 else:
1289 else:
1289 cmd = ['log']
1290 cmd = ['log']
1290 if limit:
1291 if limit:
1291 cmd.extend(['-n', str(safe_int(limit, 0))])
1292 cmd.extend(['-n', str(safe_int(limit, 0))])
1292 cmd.extend(['--pretty=format: %H', '-s', commit_id, '--', path])
1293 cmd.extend(['--pretty=format: %H', '-s', commit_id, '--', path])
1293
1294
1294 output, __ = self.run_git_command(wire, cmd)
1295 output, __ = self.run_git_command(wire, cmd)
1295 commit_ids = re.findall(rb'[0-9a-fA-F]{40}', output)
1296 commit_ids = re.findall(rb'[0-9a-fA-F]{40}', output)
1296
1297
1297 return [x for x in commit_ids]
1298 return [x for x in commit_ids]
1298 return _node_history(context_uid, repo_id, commit_id, path, limit)
1299 return _node_history(context_uid, repo_id, commit_id, path, limit)
1299
1300
1300 @reraise_safe_exceptions
1301 @reraise_safe_exceptions
1301 def node_annotate_legacy(self, wire, commit_id, path):
1302 def node_annotate_legacy(self, wire, commit_id, path):
1302 # note: replaced by pygit2 implementation
1303 # note: replaced by pygit2 implementation
1303 cmd = ['blame', '-l', '--root', '-r', commit_id, '--', path]
1304 cmd = ['blame', '-l', '--root', '-r', commit_id, '--', path]
1304 # -l ==> outputs long shas (and we need all 40 characters)
1305 # -l ==> outputs long shas (and we need all 40 characters)
1305 # --root ==> doesn't put '^' character for boundaries
1306 # --root ==> doesn't put '^' character for boundaries
1306 # -r commit_id ==> blames for the given commit
1307 # -r commit_id ==> blames for the given commit
1307 output, __ = self.run_git_command(wire, cmd)
1308 output, __ = self.run_git_command(wire, cmd)
1308
1309
1309 result = []
1310 result = []
1310 for i, blame_line in enumerate(output.splitlines()[:-1]):
1311 for i, blame_line in enumerate(output.splitlines()[:-1]):
1311 line_no = i + 1
1312 line_no = i + 1
1312 blame_commit_id, line = re.split(rb' ', blame_line, 1)
1313 blame_commit_id, line = re.split(rb' ', blame_line, 1)
1313 result.append((line_no, blame_commit_id, line))
1314 result.append((line_no, blame_commit_id, line))
1314
1315
1315 return result
1316 return result
1316
1317
1317 @reraise_safe_exceptions
1318 @reraise_safe_exceptions
1318 def node_annotate(self, wire, commit_id, path):
1319 def node_annotate(self, wire, commit_id, path):
1319
1320
1320 result_libgit = []
1321 result_libgit = []
1321 repo_init = self._factory.repo_libgit2(wire)
1322 repo_init = self._factory.repo_libgit2(wire)
1322 with repo_init as repo:
1323 with repo_init as repo:
1323 commit = repo[commit_id]
1324 commit = repo[commit_id]
1324 blame_obj = repo.blame(path, newest_commit=commit_id)
1325 blame_obj = repo.blame(path, newest_commit=commit_id)
1325 for i, line in enumerate(commit.tree[path].data.splitlines()):
1326 for i, line in enumerate(commit.tree[path].data.splitlines()):
1326 line_no = i + 1
1327 line_no = i + 1
1327 hunk = blame_obj.for_line(line_no)
1328 hunk = blame_obj.for_line(line_no)
1328 blame_commit_id = hunk.final_commit_id.hex
1329 blame_commit_id = hunk.final_commit_id.hex
1329
1330
1330 result_libgit.append((line_no, blame_commit_id, line))
1331 result_libgit.append((line_no, blame_commit_id, line))
1331
1332
1332 return BinaryEnvelope(result_libgit)
1333 return BinaryEnvelope(result_libgit)
1333
1334
1334 @reraise_safe_exceptions
1335 @reraise_safe_exceptions
1335 def update_server_info(self, wire):
1336 def update_server_info(self, wire):
1336 repo = self._factory.repo(wire)
1337 repo = self._factory.repo(wire)
1337 update_server_info(repo)
1338 update_server_info(repo)
1338
1339
1339 @reraise_safe_exceptions
1340 @reraise_safe_exceptions
1340 def get_all_commit_ids(self, wire):
1341 def get_all_commit_ids(self, wire):
1341
1342
1342 cache_on, context_uid, repo_id = self._cache_on(wire)
1343 cache_on, context_uid, repo_id = self._cache_on(wire)
1343 region = self._region(wire)
1344 region = self._region(wire)
1344
1345
1345 @region.conditional_cache_on_arguments(condition=cache_on)
1346 @region.conditional_cache_on_arguments(condition=cache_on)
1346 def _get_all_commit_ids(_context_uid, _repo_id):
1347 def _get_all_commit_ids(_context_uid, _repo_id):
1347
1348
1348 cmd = ['rev-list', '--reverse', '--date-order', '--branches', '--tags']
1349 cmd = ['rev-list', '--reverse', '--date-order', '--branches', '--tags']
1349 try:
1350 try:
1350 output, __ = self.run_git_command(wire, cmd)
1351 output, __ = self.run_git_command(wire, cmd)
1351 return output.splitlines()
1352 return output.splitlines()
1352 except Exception:
1353 except Exception:
1353 # Can be raised for empty repositories
1354 # Can be raised for empty repositories
1354 return []
1355 return []
1355
1356
1356 @region.conditional_cache_on_arguments(condition=cache_on)
1357 @region.conditional_cache_on_arguments(condition=cache_on)
1357 def _get_all_commit_ids_pygit2(_context_uid, _repo_id):
1358 def _get_all_commit_ids_pygit2(_context_uid, _repo_id):
1358 repo_init = self._factory.repo_libgit2(wire)
1359 repo_init = self._factory.repo_libgit2(wire)
1359 from pygit2 import GIT_SORT_REVERSE, GIT_SORT_TIME, GIT_BRANCH_ALL
1360 from pygit2 import GIT_SORT_REVERSE, GIT_SORT_TIME, GIT_BRANCH_ALL
1360 results = []
1361 results = []
1361 with repo_init as repo:
1362 with repo_init as repo:
1362 for commit in repo.walk(repo.head.target, GIT_SORT_TIME | GIT_BRANCH_ALL | GIT_SORT_REVERSE):
1363 for commit in repo.walk(repo.head.target, GIT_SORT_TIME | GIT_BRANCH_ALL | GIT_SORT_REVERSE):
1363 results.append(commit.id.hex)
1364 results.append(commit.id.hex)
1364
1365
1365 return _get_all_commit_ids(context_uid, repo_id)
1366 return _get_all_commit_ids(context_uid, repo_id)
1366
1367
1367 @reraise_safe_exceptions
1368 @reraise_safe_exceptions
1368 def run_git_command(self, wire, cmd, **opts):
1369 def run_git_command(self, wire, cmd, **opts):
1369 path = wire.get('path', None)
1370 path = wire.get('path', None)
1371 debug_mode = rhodecode.ConfigGet().get_bool('debug')
1370
1372
1371 if path and os.path.isdir(path):
1373 if path and os.path.isdir(path):
1372 opts['cwd'] = path
1374 opts['cwd'] = path
1373
1375
1374 if '_bare' in opts:
1376 if '_bare' in opts:
1375 _copts = []
1377 _copts = []
1376 del opts['_bare']
1378 del opts['_bare']
1377 else:
1379 else:
1378 _copts = ['-c', 'core.quotepath=false', '-c', 'advice.diverging=false']
1380 _copts = ['-c', 'core.quotepath=false', '-c', 'advice.diverging=false']
1379 safe_call = False
1381 safe_call = False
1380 if '_safe' in opts:
1382 if '_safe' in opts:
1381 # no exc on failure
1383 # no exc on failure
1382 del opts['_safe']
1384 del opts['_safe']
1383 safe_call = True
1385 safe_call = True
1384
1386
1385 if '_copts' in opts:
1387 if '_copts' in opts:
1386 _copts.extend(opts['_copts'] or [])
1388 _copts.extend(opts['_copts'] or [])
1387 del opts['_copts']
1389 del opts['_copts']
1388
1390
1389 gitenv = os.environ.copy()
1391 gitenv = os.environ.copy()
1390 gitenv.update(opts.pop('extra_env', {}))
1392 gitenv.update(opts.pop('extra_env', {}))
1391 # need to clean fix GIT_DIR !
1393 # need to clean fix GIT_DIR !
1392 if 'GIT_DIR' in gitenv:
1394 if 'GIT_DIR' in gitenv:
1393 del gitenv['GIT_DIR']
1395 del gitenv['GIT_DIR']
1394 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
1396 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
1395 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
1397 gitenv['GIT_DISCOVERY_ACROSS_FILESYSTEM'] = '1'
1396
1398
1397 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
1399 cmd = [settings.GIT_EXECUTABLE] + _copts + cmd
1398 _opts = {'env': gitenv, 'shell': False}
1400 _opts = {'env': gitenv, 'shell': False}
1399
1401
1400 proc = None
1402 proc = None
1401 try:
1403 try:
1402 _opts.update(opts)
1404 _opts.update(opts)
1403 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
1405 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
1404
1406
1405 return b''.join(proc), b''.join(proc.stderr)
1407 return b''.join(proc), b''.join(proc.stderr)
1406 except OSError as err:
1408 except OSError as err:
1407 cmd = ' '.join(map(safe_str, cmd)) # human friendly CMD
1409 cmd = ' '.join(map(safe_str, cmd)) # human friendly CMD
1408 tb_err = ("Couldn't run git command (%s).\n"
1410 call_opts = {}
1409 "Original error was:%s\n"
1411 if debug_mode:
1410 "Call options:%s\n"
1412 call_opts = _opts
1411 % (cmd, err, _opts))
1413
1414 tb_err = ("Couldn't run git command ({}).\n"
1415 "Original error was:{}\n"
1416 "Call options:{}\n"
1417 .format(cmd, err, call_opts))
1412 log.exception(tb_err)
1418 log.exception(tb_err)
1413 if safe_call:
1419 if safe_call:
1414 return '', err
1420 return '', err
1415 else:
1421 else:
1416 raise exceptions.VcsException()(tb_err)
1422 raise exceptions.VcsException()(tb_err)
1417 finally:
1423 finally:
1418 if proc:
1424 if proc:
1419 proc.close()
1425 proc.close()
1420
1426
1421 @reraise_safe_exceptions
1427 @reraise_safe_exceptions
1422 def install_hooks(self, wire, force=False):
1428 def install_hooks(self, wire, force=False):
1423 from vcsserver.hook_utils import install_git_hooks
1429 from vcsserver.hook_utils import install_git_hooks
1424 bare = self.bare(wire)
1430 bare = self.bare(wire)
1425 path = wire['path']
1431 path = wire['path']
1426 binary_dir = settings.BINARY_DIR
1432 binary_dir = settings.BINARY_DIR
1427 if binary_dir:
1433 if binary_dir:
1428 os.path.join(binary_dir, 'python3')
1434 os.path.join(binary_dir, 'python3')
1429 return install_git_hooks(path, bare, force_create=force)
1435 return install_git_hooks(path, bare, force_create=force)
1430
1436
1431 @reraise_safe_exceptions
1437 @reraise_safe_exceptions
1432 def get_hooks_info(self, wire):
1438 def get_hooks_info(self, wire):
1433 from vcsserver.hook_utils import (
1439 from vcsserver.hook_utils import (
1434 get_git_pre_hook_version, get_git_post_hook_version)
1440 get_git_pre_hook_version, get_git_post_hook_version)
1435 bare = self.bare(wire)
1441 bare = self.bare(wire)
1436 path = wire['path']
1442 path = wire['path']
1437 return {
1443 return {
1438 'pre_version': get_git_pre_hook_version(path, bare),
1444 'pre_version': get_git_pre_hook_version(path, bare),
1439 'post_version': get_git_post_hook_version(path, bare),
1445 'post_version': get_git_post_hook_version(path, bare),
1440 }
1446 }
1441
1447
1442 @reraise_safe_exceptions
1448 @reraise_safe_exceptions
1443 def set_head_ref(self, wire, head_name):
1449 def set_head_ref(self, wire, head_name):
1444 log.debug('Setting refs/head to `%s`', head_name)
1450 log.debug('Setting refs/head to `%s`', head_name)
1445 repo_init = self._factory.repo_libgit2(wire)
1451 repo_init = self._factory.repo_libgit2(wire)
1446 with repo_init as repo:
1452 with repo_init as repo:
1447 repo.set_head(f'refs/heads/{head_name}')
1453 repo.set_head(f'refs/heads/{head_name}')
1448
1454
1449 return [head_name] + [f'set HEAD to refs/heads/{head_name}']
1455 return [head_name] + [f'set HEAD to refs/heads/{head_name}']
1450
1456
1451 @reraise_safe_exceptions
1457 @reraise_safe_exceptions
1452 def archive_repo(self, wire, archive_name_key, kind, mtime, archive_at_path,
1458 def archive_repo(self, wire, archive_name_key, kind, mtime, archive_at_path,
1453 archive_dir_name, commit_id, cache_config):
1459 archive_dir_name, commit_id, cache_config):
1454
1460
1455 def file_walker(_commit_id, path):
1461 def file_walker(_commit_id, path):
1456 repo_init = self._factory.repo_libgit2(wire)
1462 repo_init = self._factory.repo_libgit2(wire)
1457
1463
1458 with repo_init as repo:
1464 with repo_init as repo:
1459 commit = repo[commit_id]
1465 commit = repo[commit_id]
1460
1466
1461 if path in ['', '/']:
1467 if path in ['', '/']:
1462 tree = commit.tree
1468 tree = commit.tree
1463 else:
1469 else:
1464 tree = commit.tree[path.rstrip('/')]
1470 tree = commit.tree[path.rstrip('/')]
1465 tree_id = tree.id.hex
1471 tree_id = tree.id.hex
1466 try:
1472 try:
1467 tree = repo[tree_id]
1473 tree = repo[tree_id]
1468 except KeyError:
1474 except KeyError:
1469 raise ObjectMissing(f'No tree with id: {tree_id}')
1475 raise ObjectMissing(f'No tree with id: {tree_id}')
1470
1476
1471 index = LibGit2Index.Index()
1477 index = LibGit2Index.Index()
1472 index.read_tree(tree)
1478 index.read_tree(tree)
1473 file_iter = index
1479 file_iter = index
1474
1480
1475 for file_node in file_iter:
1481 for file_node in file_iter:
1476 file_path = file_node.path
1482 file_path = file_node.path
1477 mode = file_node.mode
1483 mode = file_node.mode
1478 is_link = stat.S_ISLNK(mode)
1484 is_link = stat.S_ISLNK(mode)
1479 if mode == pygit2.GIT_FILEMODE_COMMIT:
1485 if mode == pygit2.GIT_FILEMODE_COMMIT:
1480 log.debug('Skipping path %s as a commit node', file_path)
1486 log.debug('Skipping path %s as a commit node', file_path)
1481 continue
1487 continue
1482 yield ArchiveNode(file_path, mode, is_link, repo[file_node.hex].read_raw)
1488 yield ArchiveNode(file_path, mode, is_link, repo[file_node.hex].read_raw)
1483
1489
1484 return store_archive_in_cache(
1490 return store_archive_in_cache(
1485 file_walker, archive_name_key, kind, mtime, archive_at_path, archive_dir_name, commit_id, cache_config=cache_config)
1491 file_walker, archive_name_key, kind, mtime, archive_at_path, archive_dir_name, commit_id, cache_config=cache_config)
@@ -1,940 +1,946 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
18
19 import os
19 import os
20 import subprocess
20 import subprocess
21 from urllib.error import URLError
21 from urllib.error import URLError
22 import urllib.parse
22 import urllib.parse
23 import logging
23 import logging
24 import posixpath as vcspath
24 import posixpath as vcspath
25 import io
25 import io
26 import urllib.request
26 import urllib.request
27 import urllib.parse
27 import urllib.parse
28 import urllib.error
28 import urllib.error
29 import traceback
29 import traceback
30
30
31
31
32 import svn.client # noqa
32 import svn.client # noqa
33 import svn.core # noqa
33 import svn.core # noqa
34 import svn.delta # noqa
34 import svn.delta # noqa
35 import svn.diff # noqa
35 import svn.diff # noqa
36 import svn.fs # noqa
36 import svn.fs # noqa
37 import svn.repos # noqa
37 import svn.repos # noqa
38
38
39 import rhodecode
39 from vcsserver import svn_diff, exceptions, subprocessio, settings
40 from vcsserver import svn_diff, exceptions, subprocessio, settings
40 from vcsserver.base import (
41 from vcsserver.base import (
41 RepoFactory,
42 RepoFactory,
42 raise_from_original,
43 raise_from_original,
43 ArchiveNode,
44 ArchiveNode,
44 store_archive_in_cache,
45 store_archive_in_cache,
45 BytesEnvelope,
46 BytesEnvelope,
46 BinaryEnvelope,
47 BinaryEnvelope,
47 )
48 )
48 from vcsserver.exceptions import NoContentException
49 from vcsserver.exceptions import NoContentException
49 from vcsserver.str_utils import safe_str, safe_bytes
50 from vcsserver.str_utils import safe_str, safe_bytes
50 from vcsserver.type_utils import assert_bytes
51 from vcsserver.type_utils import assert_bytes
51 from vcsserver.vcs_base import RemoteBase
52 from vcsserver.vcs_base import RemoteBase
52 from vcsserver.lib.svnremoterepo import svnremoterepo
53 from vcsserver.lib.svnremoterepo import svnremoterepo
53
54
54 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
55
56
56
57
57 svn_compatible_versions_map = {
58 svn_compatible_versions_map = {
58 'pre-1.4-compatible': '1.3',
59 'pre-1.4-compatible': '1.3',
59 'pre-1.5-compatible': '1.4',
60 'pre-1.5-compatible': '1.4',
60 'pre-1.6-compatible': '1.5',
61 'pre-1.6-compatible': '1.5',
61 'pre-1.8-compatible': '1.7',
62 'pre-1.8-compatible': '1.7',
62 'pre-1.9-compatible': '1.8',
63 'pre-1.9-compatible': '1.8',
63 }
64 }
64
65
65 current_compatible_version = '1.14'
66 current_compatible_version = '1.14'
66
67
67
68
68 def reraise_safe_exceptions(func):
69 def reraise_safe_exceptions(func):
69 """Decorator for converting svn exceptions to something neutral."""
70 """Decorator for converting svn exceptions to something neutral."""
70 def wrapper(*args, **kwargs):
71 def wrapper(*args, **kwargs):
71 try:
72 try:
72 return func(*args, **kwargs)
73 return func(*args, **kwargs)
73 except Exception as e:
74 except Exception as e:
74 if not hasattr(e, '_vcs_kind'):
75 if not hasattr(e, '_vcs_kind'):
75 log.exception("Unhandled exception in svn remote call")
76 log.exception("Unhandled exception in svn remote call")
76 raise_from_original(exceptions.UnhandledException(e), e)
77 raise_from_original(exceptions.UnhandledException(e), e)
77 raise
78 raise
78 return wrapper
79 return wrapper
79
80
80
81
81 class SubversionFactory(RepoFactory):
82 class SubversionFactory(RepoFactory):
82 repo_type = 'svn'
83 repo_type = 'svn'
83
84
84 def _create_repo(self, wire, create, compatible_version):
85 def _create_repo(self, wire, create, compatible_version):
85 path = svn.core.svn_path_canonicalize(wire['path'])
86 path = svn.core.svn_path_canonicalize(wire['path'])
86 if create:
87 if create:
87 fs_config = {'compatible-version': current_compatible_version}
88 fs_config = {'compatible-version': current_compatible_version}
88 if compatible_version:
89 if compatible_version:
89
90
90 compatible_version_string = \
91 compatible_version_string = \
91 svn_compatible_versions_map.get(compatible_version) \
92 svn_compatible_versions_map.get(compatible_version) \
92 or compatible_version
93 or compatible_version
93 fs_config['compatible-version'] = compatible_version_string
94 fs_config['compatible-version'] = compatible_version_string
94
95
95 log.debug('Create SVN repo with config `%s`', fs_config)
96 log.debug('Create SVN repo with config `%s`', fs_config)
96 repo = svn.repos.create(path, "", "", None, fs_config)
97 repo = svn.repos.create(path, "", "", None, fs_config)
97 else:
98 else:
98 repo = svn.repos.open(path)
99 repo = svn.repos.open(path)
99
100
100 log.debug('repository created: got SVN object: %s', repo)
101 log.debug('repository created: got SVN object: %s', repo)
101 return repo
102 return repo
102
103
103 def repo(self, wire, create=False, compatible_version=None):
104 def repo(self, wire, create=False, compatible_version=None):
104 """
105 """
105 Get a repository instance for the given path.
106 Get a repository instance for the given path.
106 """
107 """
107 return self._create_repo(wire, create, compatible_version)
108 return self._create_repo(wire, create, compatible_version)
108
109
109
110
110 NODE_TYPE_MAPPING = {
111 NODE_TYPE_MAPPING = {
111 svn.core.svn_node_file: 'file',
112 svn.core.svn_node_file: 'file',
112 svn.core.svn_node_dir: 'dir',
113 svn.core.svn_node_dir: 'dir',
113 }
114 }
114
115
115
116
116 class SvnRemote(RemoteBase):
117 class SvnRemote(RemoteBase):
117
118
118 def __init__(self, factory, hg_factory=None):
119 def __init__(self, factory, hg_factory=None):
119 self._factory = factory
120 self._factory = factory
120
121
121 self._bulk_methods = {
122 self._bulk_methods = {
122 # NOT supported in SVN ATM...
123 # NOT supported in SVN ATM...
123 }
124 }
124 self._bulk_file_methods = {
125 self._bulk_file_methods = {
125 "size": self.get_file_size,
126 "size": self.get_file_size,
126 "data": self.get_file_content,
127 "data": self.get_file_content,
127 "flags": self.get_node_type,
128 "flags": self.get_node_type,
128 "is_binary": self.is_binary,
129 "is_binary": self.is_binary,
129 "md5": self.md5_hash
130 "md5": self.md5_hash
130 }
131 }
131
132
132 @reraise_safe_exceptions
133 @reraise_safe_exceptions
133 def bulk_file_request(self, wire, commit_id, path, pre_load):
134 def bulk_file_request(self, wire, commit_id, path, pre_load):
134 cache_on, context_uid, repo_id = self._cache_on(wire)
135 cache_on, context_uid, repo_id = self._cache_on(wire)
135 region = self._region(wire)
136 region = self._region(wire)
136
137
137 # since we use unified API, we need to cast from str to in for SVN
138 # since we use unified API, we need to cast from str to in for SVN
138 commit_id = int(commit_id)
139 commit_id = int(commit_id)
139
140
140 @region.conditional_cache_on_arguments(condition=cache_on)
141 @region.conditional_cache_on_arguments(condition=cache_on)
141 def _bulk_file_request(_repo_id, _commit_id, _path, _pre_load):
142 def _bulk_file_request(_repo_id, _commit_id, _path, _pre_load):
142 result = {}
143 result = {}
143 for attr in pre_load:
144 for attr in pre_load:
144 try:
145 try:
145 method = self._bulk_file_methods[attr]
146 method = self._bulk_file_methods[attr]
146 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
147 wire.update({'cache': False}) # disable cache for bulk calls so we don't double cache
147 result[attr] = method(wire, _commit_id, _path)
148 result[attr] = method(wire, _commit_id, _path)
148 except KeyError as e:
149 except KeyError as e:
149 raise exceptions.VcsException(e)(f'Unknown bulk attribute: "{attr}"')
150 raise exceptions.VcsException(e)(f'Unknown bulk attribute: "{attr}"')
150 return result
151 return result
151
152
152 return BinaryEnvelope(_bulk_file_request(repo_id, commit_id, path, sorted(pre_load)))
153 return BinaryEnvelope(_bulk_file_request(repo_id, commit_id, path, sorted(pre_load)))
153
154
154 @reraise_safe_exceptions
155 @reraise_safe_exceptions
155 def discover_svn_version(self):
156 def discover_svn_version(self):
156 try:
157 try:
157 import svn.core
158 import svn.core
158 svn_ver = svn.core.SVN_VERSION
159 svn_ver = svn.core.SVN_VERSION
159 except ImportError:
160 except ImportError:
160 svn_ver = None
161 svn_ver = None
161 return safe_str(svn_ver)
162 return safe_str(svn_ver)
162
163
163 @reraise_safe_exceptions
164 @reraise_safe_exceptions
164 def is_empty(self, wire):
165 def is_empty(self, wire):
165 try:
166 try:
166 return self.lookup(wire, -1) == 0
167 return self.lookup(wire, -1) == 0
167 except Exception:
168 except Exception:
168 log.exception("failed to read object_store")
169 log.exception("failed to read object_store")
169 return False
170 return False
170
171
171 def check_url(self, url, config):
172 def check_url(self, url, config):
172
173
173 # uuid function gets only valid UUID from proper repo, else
174 # uuid function gets only valid UUID from proper repo, else
174 # throws exception
175 # throws exception
175 username, password, src_url = self.get_url_and_credentials(url)
176 username, password, src_url = self.get_url_and_credentials(url)
176 try:
177 try:
177 svnremoterepo(safe_bytes(username), safe_bytes(password), safe_bytes(src_url)).svn().uuid
178 svnremoterepo(safe_bytes(username), safe_bytes(password), safe_bytes(src_url)).svn().uuid
178 except Exception:
179 except Exception:
179 tb = traceback.format_exc()
180 tb = traceback.format_exc()
180 log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb)
181 log.debug("Invalid Subversion url: `%s`, tb: %s", url, tb)
181 raise URLError(f'"{url}" is not a valid Subversion source url.')
182 raise URLError(f'"{url}" is not a valid Subversion source url.')
182 return True
183 return True
183
184
184 def is_path_valid_repository(self, wire, path):
185 def is_path_valid_repository(self, wire, path):
185 # NOTE(marcink): short circuit the check for SVN repo
186 # NOTE(marcink): short circuit the check for SVN repo
186 # the repos.open might be expensive to check, but we have one cheap
187 # the repos.open might be expensive to check, but we have one cheap
187 # pre-condition that we can use, to check for 'format' file
188 # pre-condition that we can use, to check for 'format' file
188 if not os.path.isfile(os.path.join(path, 'format')):
189 if not os.path.isfile(os.path.join(path, 'format')):
189 return False
190 return False
190
191
191 try:
192 try:
192 svn.repos.open(path)
193 svn.repos.open(path)
193 except svn.core.SubversionException:
194 except svn.core.SubversionException:
194 tb = traceback.format_exc()
195 tb = traceback.format_exc()
195 log.debug("Invalid Subversion path `%s`, tb: %s", path, tb)
196 log.debug("Invalid Subversion path `%s`, tb: %s", path, tb)
196 return False
197 return False
197 return True
198 return True
198
199
199 @reraise_safe_exceptions
200 @reraise_safe_exceptions
200 def verify(self, wire,):
201 def verify(self, wire,):
201 repo_path = wire['path']
202 repo_path = wire['path']
202 if not self.is_path_valid_repository(wire, repo_path):
203 if not self.is_path_valid_repository(wire, repo_path):
203 raise Exception(
204 raise Exception(
204 f"Path {repo_path} is not a valid Subversion repository.")
205 f"Path {repo_path} is not a valid Subversion repository.")
205
206
206 cmd = ['svnadmin', 'info', repo_path]
207 cmd = ['svnadmin', 'info', repo_path]
207 stdout, stderr = subprocessio.run_command(cmd)
208 stdout, stderr = subprocessio.run_command(cmd)
208 return stdout
209 return stdout
209
210
210 @reraise_safe_exceptions
211 @reraise_safe_exceptions
211 def lookup(self, wire, revision):
212 def lookup(self, wire, revision):
212 if revision not in [-1, None, 'HEAD']:
213 if revision not in [-1, None, 'HEAD']:
213 raise NotImplementedError
214 raise NotImplementedError
214 repo = self._factory.repo(wire)
215 repo = self._factory.repo(wire)
215 fs_ptr = svn.repos.fs(repo)
216 fs_ptr = svn.repos.fs(repo)
216 head = svn.fs.youngest_rev(fs_ptr)
217 head = svn.fs.youngest_rev(fs_ptr)
217 return head
218 return head
218
219
219 @reraise_safe_exceptions
220 @reraise_safe_exceptions
220 def lookup_interval(self, wire, start_ts, end_ts):
221 def lookup_interval(self, wire, start_ts, end_ts):
221 repo = self._factory.repo(wire)
222 repo = self._factory.repo(wire)
222 fsobj = svn.repos.fs(repo)
223 fsobj = svn.repos.fs(repo)
223 start_rev = None
224 start_rev = None
224 end_rev = None
225 end_rev = None
225 if start_ts:
226 if start_ts:
226 start_ts_svn = apr_time_t(start_ts)
227 start_ts_svn = apr_time_t(start_ts)
227 start_rev = svn.repos.dated_revision(repo, start_ts_svn) + 1
228 start_rev = svn.repos.dated_revision(repo, start_ts_svn) + 1
228 else:
229 else:
229 start_rev = 1
230 start_rev = 1
230 if end_ts:
231 if end_ts:
231 end_ts_svn = apr_time_t(end_ts)
232 end_ts_svn = apr_time_t(end_ts)
232 end_rev = svn.repos.dated_revision(repo, end_ts_svn)
233 end_rev = svn.repos.dated_revision(repo, end_ts_svn)
233 else:
234 else:
234 end_rev = svn.fs.youngest_rev(fsobj)
235 end_rev = svn.fs.youngest_rev(fsobj)
235 return start_rev, end_rev
236 return start_rev, end_rev
236
237
237 @reraise_safe_exceptions
238 @reraise_safe_exceptions
238 def revision_properties(self, wire, revision):
239 def revision_properties(self, wire, revision):
239
240
240 cache_on, context_uid, repo_id = self._cache_on(wire)
241 cache_on, context_uid, repo_id = self._cache_on(wire)
241 region = self._region(wire)
242 region = self._region(wire)
242
243
243 @region.conditional_cache_on_arguments(condition=cache_on)
244 @region.conditional_cache_on_arguments(condition=cache_on)
244 def _revision_properties(_repo_id, _revision):
245 def _revision_properties(_repo_id, _revision):
245 repo = self._factory.repo(wire)
246 repo = self._factory.repo(wire)
246 fs_ptr = svn.repos.fs(repo)
247 fs_ptr = svn.repos.fs(repo)
247 return svn.fs.revision_proplist(fs_ptr, revision)
248 return svn.fs.revision_proplist(fs_ptr, revision)
248 return _revision_properties(repo_id, revision)
249 return _revision_properties(repo_id, revision)
249
250
250 def revision_changes(self, wire, revision):
251 def revision_changes(self, wire, revision):
251
252
252 repo = self._factory.repo(wire)
253 repo = self._factory.repo(wire)
253 fsobj = svn.repos.fs(repo)
254 fsobj = svn.repos.fs(repo)
254 rev_root = svn.fs.revision_root(fsobj, revision)
255 rev_root = svn.fs.revision_root(fsobj, revision)
255
256
256 editor = svn.repos.ChangeCollector(fsobj, rev_root)
257 editor = svn.repos.ChangeCollector(fsobj, rev_root)
257 editor_ptr, editor_baton = svn.delta.make_editor(editor)
258 editor_ptr, editor_baton = svn.delta.make_editor(editor)
258 base_dir = ""
259 base_dir = ""
259 send_deltas = False
260 send_deltas = False
260 svn.repos.replay2(
261 svn.repos.replay2(
261 rev_root, base_dir, svn.core.SVN_INVALID_REVNUM, send_deltas,
262 rev_root, base_dir, svn.core.SVN_INVALID_REVNUM, send_deltas,
262 editor_ptr, editor_baton, None)
263 editor_ptr, editor_baton, None)
263
264
264 added = []
265 added = []
265 changed = []
266 changed = []
266 removed = []
267 removed = []
267
268
268 # TODO: CHANGE_ACTION_REPLACE: Figure out where it belongs
269 # TODO: CHANGE_ACTION_REPLACE: Figure out where it belongs
269 for path, change in editor.changes.items():
270 for path, change in editor.changes.items():
270 # TODO: Decide what to do with directory nodes. Subversion can add
271 # TODO: Decide what to do with directory nodes. Subversion can add
271 # empty directories.
272 # empty directories.
272
273
273 if change.item_kind == svn.core.svn_node_dir:
274 if change.item_kind == svn.core.svn_node_dir:
274 continue
275 continue
275 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
276 if change.action in [svn.repos.CHANGE_ACTION_ADD]:
276 added.append(path)
277 added.append(path)
277 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
278 elif change.action in [svn.repos.CHANGE_ACTION_MODIFY,
278 svn.repos.CHANGE_ACTION_REPLACE]:
279 svn.repos.CHANGE_ACTION_REPLACE]:
279 changed.append(path)
280 changed.append(path)
280 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
281 elif change.action in [svn.repos.CHANGE_ACTION_DELETE]:
281 removed.append(path)
282 removed.append(path)
282 else:
283 else:
283 raise NotImplementedError(
284 raise NotImplementedError(
284 "Action {} not supported on path {}".format(
285 "Action {} not supported on path {}".format(
285 change.action, path))
286 change.action, path))
286
287
287 changes = {
288 changes = {
288 'added': added,
289 'added': added,
289 'changed': changed,
290 'changed': changed,
290 'removed': removed,
291 'removed': removed,
291 }
292 }
292 return changes
293 return changes
293
294
294 @reraise_safe_exceptions
295 @reraise_safe_exceptions
295 def node_history(self, wire, path, revision, limit):
296 def node_history(self, wire, path, revision, limit):
296 cache_on, context_uid, repo_id = self._cache_on(wire)
297 cache_on, context_uid, repo_id = self._cache_on(wire)
297 region = self._region(wire)
298 region = self._region(wire)
298
299
299 @region.conditional_cache_on_arguments(condition=cache_on)
300 @region.conditional_cache_on_arguments(condition=cache_on)
300 def _assert_correct_path(_context_uid, _repo_id, _path, _revision, _limit):
301 def _assert_correct_path(_context_uid, _repo_id, _path, _revision, _limit):
301 cross_copies = False
302 cross_copies = False
302 repo = self._factory.repo(wire)
303 repo = self._factory.repo(wire)
303 fsobj = svn.repos.fs(repo)
304 fsobj = svn.repos.fs(repo)
304 rev_root = svn.fs.revision_root(fsobj, revision)
305 rev_root = svn.fs.revision_root(fsobj, revision)
305
306
306 history_revisions = []
307 history_revisions = []
307 history = svn.fs.node_history(rev_root, path)
308 history = svn.fs.node_history(rev_root, path)
308 history = svn.fs.history_prev(history, cross_copies)
309 history = svn.fs.history_prev(history, cross_copies)
309 while history:
310 while history:
310 __, node_revision = svn.fs.history_location(history)
311 __, node_revision = svn.fs.history_location(history)
311 history_revisions.append(node_revision)
312 history_revisions.append(node_revision)
312 if limit and len(history_revisions) >= limit:
313 if limit and len(history_revisions) >= limit:
313 break
314 break
314 history = svn.fs.history_prev(history, cross_copies)
315 history = svn.fs.history_prev(history, cross_copies)
315 return history_revisions
316 return history_revisions
316 return _assert_correct_path(context_uid, repo_id, path, revision, limit)
317 return _assert_correct_path(context_uid, repo_id, path, revision, limit)
317
318
318 @reraise_safe_exceptions
319 @reraise_safe_exceptions
319 def node_properties(self, wire, path, revision):
320 def node_properties(self, wire, path, revision):
320 cache_on, context_uid, repo_id = self._cache_on(wire)
321 cache_on, context_uid, repo_id = self._cache_on(wire)
321 region = self._region(wire)
322 region = self._region(wire)
322
323
323 @region.conditional_cache_on_arguments(condition=cache_on)
324 @region.conditional_cache_on_arguments(condition=cache_on)
324 def _node_properties(_repo_id, _path, _revision):
325 def _node_properties(_repo_id, _path, _revision):
325 repo = self._factory.repo(wire)
326 repo = self._factory.repo(wire)
326 fsobj = svn.repos.fs(repo)
327 fsobj = svn.repos.fs(repo)
327 rev_root = svn.fs.revision_root(fsobj, revision)
328 rev_root = svn.fs.revision_root(fsobj, revision)
328 return svn.fs.node_proplist(rev_root, path)
329 return svn.fs.node_proplist(rev_root, path)
329 return _node_properties(repo_id, path, revision)
330 return _node_properties(repo_id, path, revision)
330
331
331 def file_annotate(self, wire, path, revision):
332 def file_annotate(self, wire, path, revision):
332 abs_path = 'file://' + urllib.request.pathname2url(
333 abs_path = 'file://' + urllib.request.pathname2url(
333 vcspath.join(wire['path'], path))
334 vcspath.join(wire['path'], path))
334 file_uri = svn.core.svn_path_canonicalize(abs_path)
335 file_uri = svn.core.svn_path_canonicalize(abs_path)
335
336
336 start_rev = svn_opt_revision_value_t(0)
337 start_rev = svn_opt_revision_value_t(0)
337 peg_rev = svn_opt_revision_value_t(revision)
338 peg_rev = svn_opt_revision_value_t(revision)
338 end_rev = peg_rev
339 end_rev = peg_rev
339
340
340 annotations = []
341 annotations = []
341
342
342 def receiver(line_no, revision, author, date, line, pool):
343 def receiver(line_no, revision, author, date, line, pool):
343 annotations.append((line_no, revision, line))
344 annotations.append((line_no, revision, line))
344
345
345 # TODO: Cannot use blame5, missing typemap function in the swig code
346 # TODO: Cannot use blame5, missing typemap function in the swig code
346 try:
347 try:
347 svn.client.blame2(
348 svn.client.blame2(
348 file_uri, peg_rev, start_rev, end_rev,
349 file_uri, peg_rev, start_rev, end_rev,
349 receiver, svn.client.create_context())
350 receiver, svn.client.create_context())
350 except svn.core.SubversionException as exc:
351 except svn.core.SubversionException as exc:
351 log.exception("Error during blame operation.")
352 log.exception("Error during blame operation.")
352 raise Exception(
353 raise Exception(
353 f"Blame not supported or file does not exist at path {path}. "
354 f"Blame not supported or file does not exist at path {path}. "
354 f"Error {exc}.")
355 f"Error {exc}.")
355
356
356 return BinaryEnvelope(annotations)
357 return BinaryEnvelope(annotations)
357
358
358 @reraise_safe_exceptions
359 @reraise_safe_exceptions
359 def get_node_type(self, wire, revision=None, path=''):
360 def get_node_type(self, wire, revision=None, path=''):
360
361
361 cache_on, context_uid, repo_id = self._cache_on(wire)
362 cache_on, context_uid, repo_id = self._cache_on(wire)
362 region = self._region(wire)
363 region = self._region(wire)
363
364
364 @region.conditional_cache_on_arguments(condition=cache_on)
365 @region.conditional_cache_on_arguments(condition=cache_on)
365 def _get_node_type(_repo_id, _revision, _path):
366 def _get_node_type(_repo_id, _revision, _path):
366 repo = self._factory.repo(wire)
367 repo = self._factory.repo(wire)
367 fs_ptr = svn.repos.fs(repo)
368 fs_ptr = svn.repos.fs(repo)
368 if _revision is None:
369 if _revision is None:
369 _revision = svn.fs.youngest_rev(fs_ptr)
370 _revision = svn.fs.youngest_rev(fs_ptr)
370 root = svn.fs.revision_root(fs_ptr, _revision)
371 root = svn.fs.revision_root(fs_ptr, _revision)
371 node = svn.fs.check_path(root, path)
372 node = svn.fs.check_path(root, path)
372 return NODE_TYPE_MAPPING.get(node, None)
373 return NODE_TYPE_MAPPING.get(node, None)
373 return _get_node_type(repo_id, revision, path)
374 return _get_node_type(repo_id, revision, path)
374
375
375 @reraise_safe_exceptions
376 @reraise_safe_exceptions
376 def get_nodes(self, wire, revision=None, path=''):
377 def get_nodes(self, wire, revision=None, path=''):
377
378
378 cache_on, context_uid, repo_id = self._cache_on(wire)
379 cache_on, context_uid, repo_id = self._cache_on(wire)
379 region = self._region(wire)
380 region = self._region(wire)
380
381
381 @region.conditional_cache_on_arguments(condition=cache_on)
382 @region.conditional_cache_on_arguments(condition=cache_on)
382 def _get_nodes(_repo_id, _path, _revision):
383 def _get_nodes(_repo_id, _path, _revision):
383 repo = self._factory.repo(wire)
384 repo = self._factory.repo(wire)
384 fsobj = svn.repos.fs(repo)
385 fsobj = svn.repos.fs(repo)
385 if _revision is None:
386 if _revision is None:
386 _revision = svn.fs.youngest_rev(fsobj)
387 _revision = svn.fs.youngest_rev(fsobj)
387 root = svn.fs.revision_root(fsobj, _revision)
388 root = svn.fs.revision_root(fsobj, _revision)
388 entries = svn.fs.dir_entries(root, path)
389 entries = svn.fs.dir_entries(root, path)
389 result = []
390 result = []
390 for entry_path, entry_info in entries.items():
391 for entry_path, entry_info in entries.items():
391 result.append(
392 result.append(
392 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
393 (entry_path, NODE_TYPE_MAPPING.get(entry_info.kind, None)))
393 return result
394 return result
394 return _get_nodes(repo_id, path, revision)
395 return _get_nodes(repo_id, path, revision)
395
396
396 @reraise_safe_exceptions
397 @reraise_safe_exceptions
397 def get_file_content(self, wire, rev=None, path=''):
398 def get_file_content(self, wire, rev=None, path=''):
398 repo = self._factory.repo(wire)
399 repo = self._factory.repo(wire)
399 fsobj = svn.repos.fs(repo)
400 fsobj = svn.repos.fs(repo)
400
401
401 if rev is None:
402 if rev is None:
402 rev = svn.fs.youngest_rev(fsobj)
403 rev = svn.fs.youngest_rev(fsobj)
403
404
404 root = svn.fs.revision_root(fsobj, rev)
405 root = svn.fs.revision_root(fsobj, rev)
405 content = svn.core.Stream(svn.fs.file_contents(root, path))
406 content = svn.core.Stream(svn.fs.file_contents(root, path))
406 return BytesEnvelope(content.read())
407 return BytesEnvelope(content.read())
407
408
408 @reraise_safe_exceptions
409 @reraise_safe_exceptions
409 def get_file_size(self, wire, revision=None, path=''):
410 def get_file_size(self, wire, revision=None, path=''):
410
411
411 cache_on, context_uid, repo_id = self._cache_on(wire)
412 cache_on, context_uid, repo_id = self._cache_on(wire)
412 region = self._region(wire)
413 region = self._region(wire)
413
414
414 @region.conditional_cache_on_arguments(condition=cache_on)
415 @region.conditional_cache_on_arguments(condition=cache_on)
415 def _get_file_size(_repo_id, _revision, _path):
416 def _get_file_size(_repo_id, _revision, _path):
416 repo = self._factory.repo(wire)
417 repo = self._factory.repo(wire)
417 fsobj = svn.repos.fs(repo)
418 fsobj = svn.repos.fs(repo)
418 if _revision is None:
419 if _revision is None:
419 _revision = svn.fs.youngest_revision(fsobj)
420 _revision = svn.fs.youngest_revision(fsobj)
420 root = svn.fs.revision_root(fsobj, _revision)
421 root = svn.fs.revision_root(fsobj, _revision)
421 size = svn.fs.file_length(root, path)
422 size = svn.fs.file_length(root, path)
422 return size
423 return size
423 return _get_file_size(repo_id, revision, path)
424 return _get_file_size(repo_id, revision, path)
424
425
425 def create_repository(self, wire, compatible_version=None):
426 def create_repository(self, wire, compatible_version=None):
426 log.info('Creating Subversion repository in path "%s"', wire['path'])
427 log.info('Creating Subversion repository in path "%s"', wire['path'])
427 self._factory.repo(wire, create=True,
428 self._factory.repo(wire, create=True,
428 compatible_version=compatible_version)
429 compatible_version=compatible_version)
429
430
430 def get_url_and_credentials(self, src_url) -> tuple[str, str, str]:
431 def get_url_and_credentials(self, src_url) -> tuple[str, str, str]:
431 obj = urllib.parse.urlparse(src_url)
432 obj = urllib.parse.urlparse(src_url)
432 username = obj.username or ''
433 username = obj.username or ''
433 password = obj.password or ''
434 password = obj.password or ''
434 return username, password, src_url
435 return username, password, src_url
435
436
436 def import_remote_repository(self, wire, src_url):
437 def import_remote_repository(self, wire, src_url):
437 repo_path = wire['path']
438 repo_path = wire['path']
438 if not self.is_path_valid_repository(wire, repo_path):
439 if not self.is_path_valid_repository(wire, repo_path):
439 raise Exception(
440 raise Exception(
440 f"Path {repo_path} is not a valid Subversion repository.")
441 f"Path {repo_path} is not a valid Subversion repository.")
441
442
442 username, password, src_url = self.get_url_and_credentials(src_url)
443 username, password, src_url = self.get_url_and_credentials(src_url)
443 rdump_cmd = ['svnrdump', 'dump', '--non-interactive',
444 rdump_cmd = ['svnrdump', 'dump', '--non-interactive',
444 '--trust-server-cert-failures=unknown-ca']
445 '--trust-server-cert-failures=unknown-ca']
445 if username and password:
446 if username and password:
446 rdump_cmd += ['--username', username, '--password', password]
447 rdump_cmd += ['--username', username, '--password', password]
447 rdump_cmd += [src_url]
448 rdump_cmd += [src_url]
448
449
449 rdump = subprocess.Popen(
450 rdump = subprocess.Popen(
450 rdump_cmd,
451 rdump_cmd,
451 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
452 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
452 load = subprocess.Popen(
453 load = subprocess.Popen(
453 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
454 ['svnadmin', 'load', repo_path], stdin=rdump.stdout)
454
455
455 # TODO: johbo: This can be a very long operation, might be better
456 # TODO: johbo: This can be a very long operation, might be better
456 # to track some kind of status and provide an api to check if the
457 # to track some kind of status and provide an api to check if the
457 # import is done.
458 # import is done.
458 rdump.wait()
459 rdump.wait()
459 load.wait()
460 load.wait()
460
461
461 log.debug('Return process ended with code: %s', rdump.returncode)
462 log.debug('Return process ended with code: %s', rdump.returncode)
462 if rdump.returncode != 0:
463 if rdump.returncode != 0:
463 errors = rdump.stderr.read()
464 errors = rdump.stderr.read()
464 log.error('svnrdump dump failed: statuscode %s: message: %s', rdump.returncode, errors)
465 log.error('svnrdump dump failed: statuscode %s: message: %s', rdump.returncode, errors)
465
466
466 reason = 'UNKNOWN'
467 reason = 'UNKNOWN'
467 if b'svnrdump: E230001:' in errors:
468 if b'svnrdump: E230001:' in errors:
468 reason = 'INVALID_CERTIFICATE'
469 reason = 'INVALID_CERTIFICATE'
469
470
470 if reason == 'UNKNOWN':
471 if reason == 'UNKNOWN':
471 reason = f'UNKNOWN:{safe_str(errors)}'
472 reason = f'UNKNOWN:{safe_str(errors)}'
472
473
473 raise Exception(
474 raise Exception(
474 'Failed to dump the remote repository from {}. Reason:{}'.format(
475 'Failed to dump the remote repository from {}. Reason:{}'.format(
475 src_url, reason))
476 src_url, reason))
476 if load.returncode != 0:
477 if load.returncode != 0:
477 raise Exception(
478 raise Exception(
478 f'Failed to load the dump of remote repository from {src_url}.')
479 f'Failed to load the dump of remote repository from {src_url}.')
479
480
480 def commit(self, wire, message, author, timestamp, updated, removed):
481 def commit(self, wire, message, author, timestamp, updated, removed):
481
482
482 message = safe_bytes(message)
483 message = safe_bytes(message)
483 author = safe_bytes(author)
484 author = safe_bytes(author)
484
485
485 repo = self._factory.repo(wire)
486 repo = self._factory.repo(wire)
486 fsobj = svn.repos.fs(repo)
487 fsobj = svn.repos.fs(repo)
487
488
488 rev = svn.fs.youngest_rev(fsobj)
489 rev = svn.fs.youngest_rev(fsobj)
489 txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
490 txn = svn.repos.fs_begin_txn_for_commit(repo, rev, author, message)
490 txn_root = svn.fs.txn_root(txn)
491 txn_root = svn.fs.txn_root(txn)
491
492
492 for node in updated:
493 for node in updated:
493 TxnNodeProcessor(node, txn_root).update()
494 TxnNodeProcessor(node, txn_root).update()
494 for node in removed:
495 for node in removed:
495 TxnNodeProcessor(node, txn_root).remove()
496 TxnNodeProcessor(node, txn_root).remove()
496
497
497 commit_id = svn.repos.fs_commit_txn(repo, txn)
498 commit_id = svn.repos.fs_commit_txn(repo, txn)
498
499
499 if timestamp:
500 if timestamp:
500 apr_time = apr_time_t(timestamp)
501 apr_time = apr_time_t(timestamp)
501 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
502 ts_formatted = svn.core.svn_time_to_cstring(apr_time)
502 svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
503 svn.fs.change_rev_prop(fsobj, commit_id, 'svn:date', ts_formatted)
503
504
504 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
505 log.debug('Committed revision "%s" to "%s".', commit_id, wire['path'])
505 return commit_id
506 return commit_id
506
507
507 @reraise_safe_exceptions
508 @reraise_safe_exceptions
508 def diff(self, wire, rev1, rev2, path1=None, path2=None,
509 def diff(self, wire, rev1, rev2, path1=None, path2=None,
509 ignore_whitespace=False, context=3):
510 ignore_whitespace=False, context=3):
510
511
511 wire.update(cache=False)
512 wire.update(cache=False)
512 repo = self._factory.repo(wire)
513 repo = self._factory.repo(wire)
513 diff_creator = SvnDiffer(
514 diff_creator = SvnDiffer(
514 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
515 repo, rev1, path1, rev2, path2, ignore_whitespace, context)
515 try:
516 try:
516 return BytesEnvelope(diff_creator.generate_diff())
517 return BytesEnvelope(diff_creator.generate_diff())
517 except svn.core.SubversionException as e:
518 except svn.core.SubversionException as e:
518 log.exception(
519 log.exception(
519 "Error during diff operation operation. "
520 "Error during diff operation operation. "
520 "Path might not exist %s, %s", path1, path2)
521 "Path might not exist %s, %s", path1, path2)
521 return BytesEnvelope(b'')
522 return BytesEnvelope(b'')
522
523
523 @reraise_safe_exceptions
524 @reraise_safe_exceptions
524 def is_large_file(self, wire, path):
525 def is_large_file(self, wire, path):
525 return False
526 return False
526
527
527 @reraise_safe_exceptions
528 @reraise_safe_exceptions
528 def is_binary(self, wire, rev, path):
529 def is_binary(self, wire, rev, path):
529 cache_on, context_uid, repo_id = self._cache_on(wire)
530 cache_on, context_uid, repo_id = self._cache_on(wire)
530 region = self._region(wire)
531 region = self._region(wire)
531
532
532 @region.conditional_cache_on_arguments(condition=cache_on)
533 @region.conditional_cache_on_arguments(condition=cache_on)
533 def _is_binary(_repo_id, _rev, _path):
534 def _is_binary(_repo_id, _rev, _path):
534 raw_bytes = self.get_file_content(wire, rev, path)
535 raw_bytes = self.get_file_content(wire, rev, path)
535 if not raw_bytes:
536 if not raw_bytes:
536 return False
537 return False
537 return b'\0' in raw_bytes
538 return b'\0' in raw_bytes
538
539
539 return _is_binary(repo_id, rev, path)
540 return _is_binary(repo_id, rev, path)
540
541
541 @reraise_safe_exceptions
542 @reraise_safe_exceptions
542 def md5_hash(self, wire, rev, path):
543 def md5_hash(self, wire, rev, path):
543 cache_on, context_uid, repo_id = self._cache_on(wire)
544 cache_on, context_uid, repo_id = self._cache_on(wire)
544 region = self._region(wire)
545 region = self._region(wire)
545
546
546 @region.conditional_cache_on_arguments(condition=cache_on)
547 @region.conditional_cache_on_arguments(condition=cache_on)
547 def _md5_hash(_repo_id, _rev, _path):
548 def _md5_hash(_repo_id, _rev, _path):
548 return ''
549 return ''
549
550
550 return _md5_hash(repo_id, rev, path)
551 return _md5_hash(repo_id, rev, path)
551
552
552 @reraise_safe_exceptions
553 @reraise_safe_exceptions
553 def run_svn_command(self, wire, cmd, **opts):
554 def run_svn_command(self, wire, cmd, **opts):
554 path = wire.get('path', None)
555 path = wire.get('path', None)
556 debug_mode = rhodecode.ConfigGet().get_bool('debug')
555
557
556 if path and os.path.isdir(path):
558 if path and os.path.isdir(path):
557 opts['cwd'] = path
559 opts['cwd'] = path
558
560
559 safe_call = opts.pop('_safe', False)
561 safe_call = opts.pop('_safe', False)
560
562
561 svnenv = os.environ.copy()
563 svnenv = os.environ.copy()
562 svnenv.update(opts.pop('extra_env', {}))
564 svnenv.update(opts.pop('extra_env', {}))
563
565
564 _opts = {'env': svnenv, 'shell': False}
566 _opts = {'env': svnenv, 'shell': False}
565
567
566 try:
568 try:
567 _opts.update(opts)
569 _opts.update(opts)
568 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
570 proc = subprocessio.SubprocessIOChunker(cmd, **_opts)
569
571
570 return b''.join(proc), b''.join(proc.stderr)
572 return b''.join(proc), b''.join(proc.stderr)
571 except OSError as err:
573 except OSError as err:
572 if safe_call:
574 if safe_call:
573 return '', safe_str(err).strip()
575 return '', safe_str(err).strip()
574 else:
576 else:
575 cmd = ' '.join(map(safe_str, cmd)) # human friendly CMD
577 cmd = ' '.join(map(safe_str, cmd)) # human friendly CMD
576 tb_err = ("Couldn't run svn command (%s).\n"
578 call_opts = {}
577 "Original error was:%s\n"
579 if debug_mode:
578 "Call options:%s\n"
580 call_opts = _opts
579 % (cmd, err, _opts))
581
582 tb_err = ("Couldn't run svn command ({}).\n"
583 "Original error was:{}\n"
584 "Call options:{}\n"
585 .format(cmd, err, call_opts))
580 log.exception(tb_err)
586 log.exception(tb_err)
581 raise exceptions.VcsException()(tb_err)
587 raise exceptions.VcsException()(tb_err)
582
588
583 @reraise_safe_exceptions
589 @reraise_safe_exceptions
584 def install_hooks(self, wire, force=False):
590 def install_hooks(self, wire, force=False):
585 from vcsserver.hook_utils import install_svn_hooks
591 from vcsserver.hook_utils import install_svn_hooks
586 repo_path = wire['path']
592 repo_path = wire['path']
587 binary_dir = settings.BINARY_DIR
593 binary_dir = settings.BINARY_DIR
588 executable = None
594 executable = None
589 if binary_dir:
595 if binary_dir:
590 executable = os.path.join(binary_dir, 'python3')
596 executable = os.path.join(binary_dir, 'python3')
591 return install_svn_hooks(repo_path, force_create=force)
597 return install_svn_hooks(repo_path, force_create=force)
592
598
593 @reraise_safe_exceptions
599 @reraise_safe_exceptions
594 def get_hooks_info(self, wire):
600 def get_hooks_info(self, wire):
595 from vcsserver.hook_utils import (
601 from vcsserver.hook_utils import (
596 get_svn_pre_hook_version, get_svn_post_hook_version)
602 get_svn_pre_hook_version, get_svn_post_hook_version)
597 repo_path = wire['path']
603 repo_path = wire['path']
598 return {
604 return {
599 'pre_version': get_svn_pre_hook_version(repo_path),
605 'pre_version': get_svn_pre_hook_version(repo_path),
600 'post_version': get_svn_post_hook_version(repo_path),
606 'post_version': get_svn_post_hook_version(repo_path),
601 }
607 }
602
608
603 @reraise_safe_exceptions
609 @reraise_safe_exceptions
604 def set_head_ref(self, wire, head_name):
610 def set_head_ref(self, wire, head_name):
605 pass
611 pass
606
612
607 @reraise_safe_exceptions
613 @reraise_safe_exceptions
608 def archive_repo(self, wire, archive_name_key, kind, mtime, archive_at_path,
614 def archive_repo(self, wire, archive_name_key, kind, mtime, archive_at_path,
609 archive_dir_name, commit_id, cache_config):
615 archive_dir_name, commit_id, cache_config):
610
616
611 def walk_tree(root, root_dir, _commit_id):
617 def walk_tree(root, root_dir, _commit_id):
612 """
618 """
613 Special recursive svn repo walker
619 Special recursive svn repo walker
614 """
620 """
615 root_dir = safe_bytes(root_dir)
621 root_dir = safe_bytes(root_dir)
616
622
617 filemode_default = 0o100644
623 filemode_default = 0o100644
618 filemode_executable = 0o100755
624 filemode_executable = 0o100755
619
625
620 file_iter = svn.fs.dir_entries(root, root_dir)
626 file_iter = svn.fs.dir_entries(root, root_dir)
621 for f_name in file_iter:
627 for f_name in file_iter:
622 f_type = NODE_TYPE_MAPPING.get(file_iter[f_name].kind, None)
628 f_type = NODE_TYPE_MAPPING.get(file_iter[f_name].kind, None)
623
629
624 if f_type == 'dir':
630 if f_type == 'dir':
625 # return only DIR, and then all entries in that dir
631 # return only DIR, and then all entries in that dir
626 yield os.path.join(root_dir, f_name), {'mode': filemode_default}, f_type
632 yield os.path.join(root_dir, f_name), {'mode': filemode_default}, f_type
627 new_root = os.path.join(root_dir, f_name)
633 new_root = os.path.join(root_dir, f_name)
628 yield from walk_tree(root, new_root, _commit_id)
634 yield from walk_tree(root, new_root, _commit_id)
629 else:
635 else:
630
636
631 f_path = os.path.join(root_dir, f_name).rstrip(b'/')
637 f_path = os.path.join(root_dir, f_name).rstrip(b'/')
632 prop_list = svn.fs.node_proplist(root, f_path)
638 prop_list = svn.fs.node_proplist(root, f_path)
633
639
634 f_mode = filemode_default
640 f_mode = filemode_default
635 if prop_list.get('svn:executable'):
641 if prop_list.get('svn:executable'):
636 f_mode = filemode_executable
642 f_mode = filemode_executable
637
643
638 f_is_link = False
644 f_is_link = False
639 if prop_list.get('svn:special'):
645 if prop_list.get('svn:special'):
640 f_is_link = True
646 f_is_link = True
641
647
642 data = {
648 data = {
643 'is_link': f_is_link,
649 'is_link': f_is_link,
644 'mode': f_mode,
650 'mode': f_mode,
645 'content_stream': svn.core.Stream(svn.fs.file_contents(root, f_path)).read
651 'content_stream': svn.core.Stream(svn.fs.file_contents(root, f_path)).read
646 }
652 }
647
653
648 yield f_path, data, f_type
654 yield f_path, data, f_type
649
655
650 def file_walker(_commit_id, path):
656 def file_walker(_commit_id, path):
651 repo = self._factory.repo(wire)
657 repo = self._factory.repo(wire)
652 root = svn.fs.revision_root(svn.repos.fs(repo), int(commit_id))
658 root = svn.fs.revision_root(svn.repos.fs(repo), int(commit_id))
653
659
654 def no_content():
660 def no_content():
655 raise NoContentException()
661 raise NoContentException()
656
662
657 for f_name, f_data, f_type in walk_tree(root, path, _commit_id):
663 for f_name, f_data, f_type in walk_tree(root, path, _commit_id):
658 file_path = f_name
664 file_path = f_name
659
665
660 if f_type == 'dir':
666 if f_type == 'dir':
661 mode = f_data['mode']
667 mode = f_data['mode']
662 yield ArchiveNode(file_path, mode, False, no_content)
668 yield ArchiveNode(file_path, mode, False, no_content)
663 else:
669 else:
664 mode = f_data['mode']
670 mode = f_data['mode']
665 is_link = f_data['is_link']
671 is_link = f_data['is_link']
666 data_stream = f_data['content_stream']
672 data_stream = f_data['content_stream']
667 yield ArchiveNode(file_path, mode, is_link, data_stream)
673 yield ArchiveNode(file_path, mode, is_link, data_stream)
668
674
669 return store_archive_in_cache(
675 return store_archive_in_cache(
670 file_walker, archive_name_key, kind, mtime, archive_at_path, archive_dir_name, commit_id, cache_config=cache_config)
676 file_walker, archive_name_key, kind, mtime, archive_at_path, archive_dir_name, commit_id, cache_config=cache_config)
671
677
672
678
673 class SvnDiffer:
679 class SvnDiffer:
674 """
680 """
675 Utility to create diffs based on difflib and the Subversion api
681 Utility to create diffs based on difflib and the Subversion api
676 """
682 """
677
683
678 binary_content = False
684 binary_content = False
679
685
680 def __init__(
686 def __init__(
681 self, repo, src_rev, src_path, tgt_rev, tgt_path,
687 self, repo, src_rev, src_path, tgt_rev, tgt_path,
682 ignore_whitespace, context):
688 ignore_whitespace, context):
683 self.repo = repo
689 self.repo = repo
684 self.ignore_whitespace = ignore_whitespace
690 self.ignore_whitespace = ignore_whitespace
685 self.context = context
691 self.context = context
686
692
687 fsobj = svn.repos.fs(repo)
693 fsobj = svn.repos.fs(repo)
688
694
689 self.tgt_rev = tgt_rev
695 self.tgt_rev = tgt_rev
690 self.tgt_path = tgt_path or ''
696 self.tgt_path = tgt_path or ''
691 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
697 self.tgt_root = svn.fs.revision_root(fsobj, tgt_rev)
692 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
698 self.tgt_kind = svn.fs.check_path(self.tgt_root, self.tgt_path)
693
699
694 self.src_rev = src_rev
700 self.src_rev = src_rev
695 self.src_path = src_path or self.tgt_path
701 self.src_path = src_path or self.tgt_path
696 self.src_root = svn.fs.revision_root(fsobj, src_rev)
702 self.src_root = svn.fs.revision_root(fsobj, src_rev)
697 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
703 self.src_kind = svn.fs.check_path(self.src_root, self.src_path)
698
704
699 self._validate()
705 self._validate()
700
706
701 def _validate(self):
707 def _validate(self):
702 if (self.tgt_kind != svn.core.svn_node_none and
708 if (self.tgt_kind != svn.core.svn_node_none and
703 self.src_kind != svn.core.svn_node_none and
709 self.src_kind != svn.core.svn_node_none and
704 self.src_kind != self.tgt_kind):
710 self.src_kind != self.tgt_kind):
705 # TODO: johbo: proper error handling
711 # TODO: johbo: proper error handling
706 raise Exception(
712 raise Exception(
707 "Source and target are not compatible for diff generation. "
713 "Source and target are not compatible for diff generation. "
708 "Source type: %s, target type: %s" %
714 "Source type: %s, target type: %s" %
709 (self.src_kind, self.tgt_kind))
715 (self.src_kind, self.tgt_kind))
710
716
711 def generate_diff(self) -> bytes:
717 def generate_diff(self) -> bytes:
712 buf = io.BytesIO()
718 buf = io.BytesIO()
713 if self.tgt_kind == svn.core.svn_node_dir:
719 if self.tgt_kind == svn.core.svn_node_dir:
714 self._generate_dir_diff(buf)
720 self._generate_dir_diff(buf)
715 else:
721 else:
716 self._generate_file_diff(buf)
722 self._generate_file_diff(buf)
717 return buf.getvalue()
723 return buf.getvalue()
718
724
719 def _generate_dir_diff(self, buf: io.BytesIO):
725 def _generate_dir_diff(self, buf: io.BytesIO):
720 editor = DiffChangeEditor()
726 editor = DiffChangeEditor()
721 editor_ptr, editor_baton = svn.delta.make_editor(editor)
727 editor_ptr, editor_baton = svn.delta.make_editor(editor)
722 svn.repos.dir_delta2(
728 svn.repos.dir_delta2(
723 self.src_root,
729 self.src_root,
724 self.src_path,
730 self.src_path,
725 '', # src_entry
731 '', # src_entry
726 self.tgt_root,
732 self.tgt_root,
727 self.tgt_path,
733 self.tgt_path,
728 editor_ptr, editor_baton,
734 editor_ptr, editor_baton,
729 authorization_callback_allow_all,
735 authorization_callback_allow_all,
730 False, # text_deltas
736 False, # text_deltas
731 svn.core.svn_depth_infinity, # depth
737 svn.core.svn_depth_infinity, # depth
732 False, # entry_props
738 False, # entry_props
733 False, # ignore_ancestry
739 False, # ignore_ancestry
734 )
740 )
735
741
736 for path, __, change in sorted(editor.changes):
742 for path, __, change in sorted(editor.changes):
737 self._generate_node_diff(
743 self._generate_node_diff(
738 buf, change, path, self.tgt_path, path, self.src_path)
744 buf, change, path, self.tgt_path, path, self.src_path)
739
745
740 def _generate_file_diff(self, buf: io.BytesIO):
746 def _generate_file_diff(self, buf: io.BytesIO):
741 change = None
747 change = None
742 if self.src_kind == svn.core.svn_node_none:
748 if self.src_kind == svn.core.svn_node_none:
743 change = "add"
749 change = "add"
744 elif self.tgt_kind == svn.core.svn_node_none:
750 elif self.tgt_kind == svn.core.svn_node_none:
745 change = "delete"
751 change = "delete"
746 tgt_base, tgt_path = vcspath.split(self.tgt_path)
752 tgt_base, tgt_path = vcspath.split(self.tgt_path)
747 src_base, src_path = vcspath.split(self.src_path)
753 src_base, src_path = vcspath.split(self.src_path)
748 self._generate_node_diff(
754 self._generate_node_diff(
749 buf, change, tgt_path, tgt_base, src_path, src_base)
755 buf, change, tgt_path, tgt_base, src_path, src_base)
750
756
751 def _generate_node_diff(
757 def _generate_node_diff(
752 self, buf: io.BytesIO, change, tgt_path, tgt_base, src_path, src_base):
758 self, buf: io.BytesIO, change, tgt_path, tgt_base, src_path, src_base):
753
759
754 tgt_path_bytes = safe_bytes(tgt_path)
760 tgt_path_bytes = safe_bytes(tgt_path)
755 tgt_path = safe_str(tgt_path)
761 tgt_path = safe_str(tgt_path)
756
762
757 src_path_bytes = safe_bytes(src_path)
763 src_path_bytes = safe_bytes(src_path)
758 src_path = safe_str(src_path)
764 src_path = safe_str(src_path)
759
765
760 if self.src_rev == self.tgt_rev and tgt_base == src_base:
766 if self.src_rev == self.tgt_rev and tgt_base == src_base:
761 # makes consistent behaviour with git/hg to return empty diff if
767 # makes consistent behaviour with git/hg to return empty diff if
762 # we compare same revisions
768 # we compare same revisions
763 return
769 return
764
770
765 tgt_full_path = vcspath.join(tgt_base, tgt_path)
771 tgt_full_path = vcspath.join(tgt_base, tgt_path)
766 src_full_path = vcspath.join(src_base, src_path)
772 src_full_path = vcspath.join(src_base, src_path)
767
773
768 self.binary_content = False
774 self.binary_content = False
769 mime_type = self._get_mime_type(tgt_full_path)
775 mime_type = self._get_mime_type(tgt_full_path)
770
776
771 if mime_type and not mime_type.startswith(b'text'):
777 if mime_type and not mime_type.startswith(b'text'):
772 self.binary_content = True
778 self.binary_content = True
773 buf.write(b"=" * 67 + b'\n')
779 buf.write(b"=" * 67 + b'\n')
774 buf.write(b"Cannot display: file marked as a binary type.\n")
780 buf.write(b"Cannot display: file marked as a binary type.\n")
775 buf.write(b"svn:mime-type = %s\n" % mime_type)
781 buf.write(b"svn:mime-type = %s\n" % mime_type)
776 buf.write(b"Index: %b\n" % tgt_path_bytes)
782 buf.write(b"Index: %b\n" % tgt_path_bytes)
777 buf.write(b"=" * 67 + b'\n')
783 buf.write(b"=" * 67 + b'\n')
778 buf.write(b"diff --git a/%b b/%b\n" % (tgt_path_bytes, tgt_path_bytes))
784 buf.write(b"diff --git a/%b b/%b\n" % (tgt_path_bytes, tgt_path_bytes))
779
785
780 if change == 'add':
786 if change == 'add':
781 # TODO: johbo: SVN is missing a zero here compared to git
787 # TODO: johbo: SVN is missing a zero here compared to git
782 buf.write(b"new file mode 10644\n")
788 buf.write(b"new file mode 10644\n")
783
789
784 # TODO(marcink): intro to binary detection of svn patches
790 # TODO(marcink): intro to binary detection of svn patches
785 # if self.binary_content:
791 # if self.binary_content:
786 # buf.write(b'GIT binary patch\n')
792 # buf.write(b'GIT binary patch\n')
787
793
788 buf.write(b"--- /dev/null\t(revision 0)\n")
794 buf.write(b"--- /dev/null\t(revision 0)\n")
789 src_lines = []
795 src_lines = []
790 else:
796 else:
791 if change == 'delete':
797 if change == 'delete':
792 buf.write(b"deleted file mode 10644\n")
798 buf.write(b"deleted file mode 10644\n")
793
799
794 # TODO(marcink): intro to binary detection of svn patches
800 # TODO(marcink): intro to binary detection of svn patches
795 # if self.binary_content:
801 # if self.binary_content:
796 # buf.write('GIT binary patch\n')
802 # buf.write('GIT binary patch\n')
797
803
798 buf.write(b"--- a/%b\t(revision %d)\n" % (src_path_bytes, self.src_rev))
804 buf.write(b"--- a/%b\t(revision %d)\n" % (src_path_bytes, self.src_rev))
799 src_lines = self._svn_readlines(self.src_root, src_full_path)
805 src_lines = self._svn_readlines(self.src_root, src_full_path)
800
806
801 if change == 'delete':
807 if change == 'delete':
802 buf.write(b"+++ /dev/null\t(revision %d)\n" % self.tgt_rev)
808 buf.write(b"+++ /dev/null\t(revision %d)\n" % self.tgt_rev)
803 tgt_lines = []
809 tgt_lines = []
804 else:
810 else:
805 buf.write(b"+++ b/%b\t(revision %d)\n" % (tgt_path_bytes, self.tgt_rev))
811 buf.write(b"+++ b/%b\t(revision %d)\n" % (tgt_path_bytes, self.tgt_rev))
806 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
812 tgt_lines = self._svn_readlines(self.tgt_root, tgt_full_path)
807
813
808 # we made our diff header, time to generate the diff content into our buffer
814 # we made our diff header, time to generate the diff content into our buffer
809
815
810 if not self.binary_content:
816 if not self.binary_content:
811 udiff = svn_diff.unified_diff(
817 udiff = svn_diff.unified_diff(
812 src_lines, tgt_lines, context=self.context,
818 src_lines, tgt_lines, context=self.context,
813 ignore_blank_lines=self.ignore_whitespace,
819 ignore_blank_lines=self.ignore_whitespace,
814 ignore_case=False,
820 ignore_case=False,
815 ignore_space_changes=self.ignore_whitespace)
821 ignore_space_changes=self.ignore_whitespace)
816
822
817 buf.writelines(udiff)
823 buf.writelines(udiff)
818
824
819 def _get_mime_type(self, path) -> bytes:
825 def _get_mime_type(self, path) -> bytes:
820 try:
826 try:
821 mime_type = svn.fs.node_prop(
827 mime_type = svn.fs.node_prop(
822 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
828 self.tgt_root, path, svn.core.SVN_PROP_MIME_TYPE)
823 except svn.core.SubversionException:
829 except svn.core.SubversionException:
824 mime_type = svn.fs.node_prop(
830 mime_type = svn.fs.node_prop(
825 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
831 self.src_root, path, svn.core.SVN_PROP_MIME_TYPE)
826 return mime_type
832 return mime_type
827
833
828 def _svn_readlines(self, fs_root, node_path):
834 def _svn_readlines(self, fs_root, node_path):
829 if self.binary_content:
835 if self.binary_content:
830 return []
836 return []
831 node_kind = svn.fs.check_path(fs_root, node_path)
837 node_kind = svn.fs.check_path(fs_root, node_path)
832 if node_kind not in (
838 if node_kind not in (
833 svn.core.svn_node_file, svn.core.svn_node_symlink):
839 svn.core.svn_node_file, svn.core.svn_node_symlink):
834 return []
840 return []
835 content = svn.core.Stream(
841 content = svn.core.Stream(
836 svn.fs.file_contents(fs_root, node_path)).read()
842 svn.fs.file_contents(fs_root, node_path)).read()
837
843
838 return content.splitlines(True)
844 return content.splitlines(True)
839
845
840
846
841 class DiffChangeEditor(svn.delta.Editor):
847 class DiffChangeEditor(svn.delta.Editor):
842 """
848 """
843 Records changes between two given revisions
849 Records changes between two given revisions
844 """
850 """
845
851
846 def __init__(self):
852 def __init__(self):
847 self.changes = []
853 self.changes = []
848
854
849 def delete_entry(self, path, revision, parent_baton, pool=None):
855 def delete_entry(self, path, revision, parent_baton, pool=None):
850 self.changes.append((path, None, 'delete'))
856 self.changes.append((path, None, 'delete'))
851
857
852 def add_file(
858 def add_file(
853 self, path, parent_baton, copyfrom_path, copyfrom_revision,
859 self, path, parent_baton, copyfrom_path, copyfrom_revision,
854 file_pool=None):
860 file_pool=None):
855 self.changes.append((path, 'file', 'add'))
861 self.changes.append((path, 'file', 'add'))
856
862
857 def open_file(self, path, parent_baton, base_revision, file_pool=None):
863 def open_file(self, path, parent_baton, base_revision, file_pool=None):
858 self.changes.append((path, 'file', 'change'))
864 self.changes.append((path, 'file', 'change'))
859
865
860
866
861 def authorization_callback_allow_all(root, path, pool):
867 def authorization_callback_allow_all(root, path, pool):
862 return True
868 return True
863
869
864
870
865 class TxnNodeProcessor:
871 class TxnNodeProcessor:
866 """
872 """
867 Utility to process the change of one node within a transaction root.
873 Utility to process the change of one node within a transaction root.
868
874
869 It encapsulates the knowledge of how to add, update or remove
875 It encapsulates the knowledge of how to add, update or remove
870 a node for a given transaction root. The purpose is to support the method
876 a node for a given transaction root. The purpose is to support the method
871 `SvnRemote.commit`.
877 `SvnRemote.commit`.
872 """
878 """
873
879
874 def __init__(self, node, txn_root):
880 def __init__(self, node, txn_root):
875 assert_bytes(node['path'])
881 assert_bytes(node['path'])
876
882
877 self.node = node
883 self.node = node
878 self.txn_root = txn_root
884 self.txn_root = txn_root
879
885
880 def update(self):
886 def update(self):
881 self._ensure_parent_dirs()
887 self._ensure_parent_dirs()
882 self._add_file_if_node_does_not_exist()
888 self._add_file_if_node_does_not_exist()
883 self._update_file_content()
889 self._update_file_content()
884 self._update_file_properties()
890 self._update_file_properties()
885
891
886 def remove(self):
892 def remove(self):
887 svn.fs.delete(self.txn_root, self.node['path'])
893 svn.fs.delete(self.txn_root, self.node['path'])
888 # TODO: Clean up directory if empty
894 # TODO: Clean up directory if empty
889
895
890 def _ensure_parent_dirs(self):
896 def _ensure_parent_dirs(self):
891 curdir = vcspath.dirname(self.node['path'])
897 curdir = vcspath.dirname(self.node['path'])
892 dirs_to_create = []
898 dirs_to_create = []
893 while not self._svn_path_exists(curdir):
899 while not self._svn_path_exists(curdir):
894 dirs_to_create.append(curdir)
900 dirs_to_create.append(curdir)
895 curdir = vcspath.dirname(curdir)
901 curdir = vcspath.dirname(curdir)
896
902
897 for curdir in reversed(dirs_to_create):
903 for curdir in reversed(dirs_to_create):
898 log.debug('Creating missing directory "%s"', curdir)
904 log.debug('Creating missing directory "%s"', curdir)
899 svn.fs.make_dir(self.txn_root, curdir)
905 svn.fs.make_dir(self.txn_root, curdir)
900
906
901 def _svn_path_exists(self, path):
907 def _svn_path_exists(self, path):
902 path_status = svn.fs.check_path(self.txn_root, path)
908 path_status = svn.fs.check_path(self.txn_root, path)
903 return path_status != svn.core.svn_node_none
909 return path_status != svn.core.svn_node_none
904
910
905 def _add_file_if_node_does_not_exist(self):
911 def _add_file_if_node_does_not_exist(self):
906 kind = svn.fs.check_path(self.txn_root, self.node['path'])
912 kind = svn.fs.check_path(self.txn_root, self.node['path'])
907 if kind == svn.core.svn_node_none:
913 if kind == svn.core.svn_node_none:
908 svn.fs.make_file(self.txn_root, self.node['path'])
914 svn.fs.make_file(self.txn_root, self.node['path'])
909
915
910 def _update_file_content(self):
916 def _update_file_content(self):
911 assert_bytes(self.node['content'])
917 assert_bytes(self.node['content'])
912
918
913 handler, baton = svn.fs.apply_textdelta(
919 handler, baton = svn.fs.apply_textdelta(
914 self.txn_root, self.node['path'], None, None)
920 self.txn_root, self.node['path'], None, None)
915 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
921 svn.delta.svn_txdelta_send_string(self.node['content'], handler, baton)
916
922
917 def _update_file_properties(self):
923 def _update_file_properties(self):
918 properties = self.node.get('properties', {})
924 properties = self.node.get('properties', {})
919 for key, value in properties.items():
925 for key, value in properties.items():
920 svn.fs.change_node_prop(
926 svn.fs.change_node_prop(
921 self.txn_root, self.node['path'], safe_bytes(key), safe_bytes(value))
927 self.txn_root, self.node['path'], safe_bytes(key), safe_bytes(value))
922
928
923
929
924 def apr_time_t(timestamp):
930 def apr_time_t(timestamp):
925 """
931 """
926 Convert a Python timestamp into APR timestamp type apr_time_t
932 Convert a Python timestamp into APR timestamp type apr_time_t
927 """
933 """
928 return int(timestamp * 1E6)
934 return int(timestamp * 1E6)
929
935
930
936
931 def svn_opt_revision_value_t(num):
937 def svn_opt_revision_value_t(num):
932 """
938 """
933 Put `num` into a `svn_opt_revision_value_t` structure.
939 Put `num` into a `svn_opt_revision_value_t` structure.
934 """
940 """
935 value = svn.core.svn_opt_revision_value_t()
941 value = svn.core.svn_opt_revision_value_t()
936 value.number = num
942 value.number = num
937 revision = svn.core.svn_opt_revision_t()
943 revision = svn.core.svn_opt_revision_t()
938 revision.kind = svn.core.svn_opt_revision_number
944 revision.kind = svn.core.svn_opt_revision_number
939 revision.value = value
945 revision.value = value
940 return revision
946 return revision
General Comments 0
You need to be logged in to leave comments. Login now