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