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