Ticket #7715: 7715-mergeinfo-optimized-eligible-r8263.patch
| File 7715-mergeinfo-optimized-eligible-r8263.patch, 14.7 KB (added by cboos, 3 years ago) |
|---|
-
trac/htdocs/css/browser.css
174 174 margin: 0 0 .4em 1.6em; 175 175 padding: 0; 176 176 } 177 #info .props li { padding:0; overflow: auto; }177 #info .props > li { padding: 2px 0; overflow: auto; } 178 178 179 179 /* Styles for the HTML preview */ 180 180 #preview { background: #fff; clear: both; margin: 0 } -
trac/versioncontrol/svn_fs.py
60 60 from trac.versioncontrol.cache import CachedRepository 61 61 from trac.versioncontrol.svn_authz import SubversionAuthorizer 62 62 from trac.versioncontrol.web_ui.browser import IPropertyRenderer 63 from trac.util import embedded_numbers 63 from trac.versioncontrol.web_ui.changeset import IPropertyDiffRenderer 64 from trac.util import Ranges, embedded_numbers, to_ranges 64 65 from trac.util.text import exception_to_unicode, to_unicode 65 66 from trac.util.translation import _ 66 67 from trac.util.datefmt import utc … … 308 309 309 310 310 311 class SubversionPropertyRenderer(Component): 311 implements(IPropertyRenderer )312 implements(IPropertyRenderer, IPropertyDiffRenderer) 312 313 313 314 def __init__(self): 314 315 self._externals_map = {} … … 323 324 if name == 'svn:externals': 324 325 return self._render_externals(props[name]) 325 326 elif name == 'svn:mergeinfo' or name.startswith('svnmerge-'): 326 return self._render_mergeinfo( props[name])327 return self._render_mergeinfo(name, mode, context, props) 327 328 elif name == 'svn:needs-lock': 328 329 return self._render_needslock(context) 329 330 … … 389 390 return tag.ul([tag.li(tag.a(label, href=href, title=title)) 390 391 for label, href, title in externals_data]) 391 392 392 def _render_mergeinfo(self, prop): 393 prop = prop.rsplit(':', 1) 394 if len(prop) == 2: 395 prop[1] = prop[1].replace(',', u',\u200b') 396 return ':'.join(prop) 393 def _get_blocked_revs(self, props, name, path): 394 """Return the revisions blocked from merging for the given property 395 name and path. 396 """ 397 if name == 'svnmerge-integrated': 398 prop = props.get('svnmerge-blocked', '') 399 else: 400 return "" 401 for line in prop.splitlines(): 402 try: 403 p, revs = line.split(':', 1) 404 if p.strip('/') == path: 405 return revs 406 except Exception: 407 pass 408 return "" 397 409 410 def _render_mergeinfo(self, name, mode, context, props): 411 """Parse svn:mergeinfo and svnmerge-* properties, converting branch 412 names to links and providing links to the revision log for merged 413 and eligible revisions. 414 """ 415 has_eligible = name in ('svnmerge-integrated', 'svn:mergeinfo') 416 no_revisions = tag.span(_('revisions'), title=_('No revisions')) 417 no_eligible = tag.span(_('eligible'), title=_('No eligible revisions')) 418 visible_rows = [] 419 hidden_rows = [] 420 repos = self.env.get_repository() 421 repos = getattr(repos, 'repos', repos) 422 # repos is always a SubversionRepository now 423 target_path = context.resource.id 424 target_rev = context.resource.version 425 if has_eligible: 426 ancestor_path_first_revs = {} 427 node = repos.get_node(target_path, target_rev) 428 while node: 429 node = node.get_copy_origin() 430 if node and node.path != target_path: 431 ancestor_path_first_revs[node.path] = node.rev + 1 432 for line in props[name].splitlines(): 433 node = None 434 try: 435 path, revs = line.split(':', 1) 436 spath = path.strip('/') 437 revs = revs.strip() 438 if 'LOG_VIEW' in context.perm('source', spath): 439 row = [tag.a(path, title=_('View dir'), 440 href=context.href.browser(spath, 441 rev=context.resource.version))] 442 if revs: 443 label = (_('merged'), _('blocked'))[ 444 name.endswith('blocked')] 445 row.append(self._get_revs_link(label, context, spath, 446 revs)) 447 else: 448 row.append(no_revisions) 449 try: 450 import time 451 now = time.time() 452 node = repos.get_node(path, target_rev) 453 except NoSuchNode: 454 node = None 455 if node and has_eligible: 456 first_rev = ancestor_path_first_revs.get(path) 457 if not first_rev: 458 first_rev = node.get_copy_origin_rev() or 1 459 if target_rev - first_rev < 1000: 460 eligible = set(r for p, r in 461 repos._history(path, first_rev, 462 target_rev, 463 node.pool)) 464 else: 465 eligible = set(range(first_rev, target_rev)) 466 467 print spath, 't1', time.time() - now 468 eligible -= set(Ranges(revs)) 469 blocked = self._get_blocked_revs(props, name, spath) 470 eligible -= set(Ranges(blocked)) 471 if eligible: 472 eligible = to_ranges(eligible) 473 print spath, 't2', time.time() - now 474 row.append(tag.a(_('eligible'), 475 title=eligible.replace(',', ', '), 476 href=context.href.log(spath, 477 revs=eligible))) 478 else: 479 row.append(no_eligible) 480 if node: 481 visible_rows.append(tag.td(each) for each in row) 482 else: 483 hidden_rows.append(tag.td(e) for e in line.split(':')) 484 else: 485 revs = revs.replace(',', u',\u200b') 486 visible_rows.append(tag.td(path), 487 tag.td(revs, 488 colspan=has_eligible and 2 or None)) 489 except Exception, e: 490 self.log.warning('Rendering of property %s failed: %s', name, 491 exception_to_unicode(e)) 492 rows = [visible_rows, hidden_rows][bool(node)] 493 rows.append(tag.td(line, colspan=has_eligible and 3 or 2)) 494 vtable = tag.table(tag.tbody(tag.tr(each) for each in visible_rows)) 495 htable = tag.table(tag.tbody(tag.tr(each) for each in hidden_rows), 496 style='display: none', id="oldmerged") 497 ttable = None 498 if hidden_rows: 499 ttable = tag.table(tag.tbody(tag.tr(tag.td( 500 tag.a(_('Toggle deleted branches'), 501 href="#toggleoldmerged", id="toggleoldmerged"))))), 502 return tag.div(vtable, ttable, htable) 503 398 504 def _render_needslock(self, context): 399 505 return tag.img(src=context.href.chrome('common/lock-locked.png'), 400 506 alt="needs lock", title="needs lock") 401 507 508 def _get_revs_link(self, label, context, spath, revs): 509 if ',' in revs or '-' in revs: 510 revs_href = context.href.log(spath, revs=revs) 511 else: 512 revs_href = context.href.changeset(revs, spath) 513 return tag.a(label, title=revs.replace(',', ', '), href=revs_href) 402 514 515 516 # IPropertyDiffRenderer methods 517 518 def match_property_diff(self, name): 519 return (name == 'svn:mergeinfo' or name.startswith('svnmerge-')) and \ 520 4 or 0 521 522 def render_property_diff(self, name, old_context, old_props, 523 new_context, new_props, options): 524 # build 3 columns table showing modifications on merge sources 525 # || source (added|modified|removed) || added revs || removed revs || 526 nothing_added = tag.span('-', title=_('nothing added')) 527 nothing_removed = tag.span('-', title=_('nothing removed')) 528 def parse_sources(props): 529 sources = {} 530 for line in props[name].splitlines(): 531 path, revs = line.split(':', 1) 532 spath = path.strip('/') 533 revs = revs.strip() 534 sources[spath] = set(Ranges(revs)) 535 return sources 536 old_sources = parse_sources(old_props) 537 new_sources = parse_sources(new_props) 538 # go through new sources, detect modified ones or added ones 539 modified_and_added_sources = [] 540 for spath, new_revs in new_sources.iteritems(): 541 if spath in old_sources: 542 old_revs = old_sources.pop(spath) 543 status = '' 544 else: 545 old_revs = set() 546 status = _(' (added)') 547 def revs_link(revs, context): 548 if revs: 549 revs = to_ranges(revs) 550 return self._get_revs_link(revs.replace(',', u',\u200b'), 551 context, spath, revs) 552 source_href = new_context.href.browser(spath, 553 rev=new_context.resource.version) 554 modified_and_added_sources.append([ 555 tag.a(spath, status, title=_('View dir'), href=source_href), 556 revs_link(new_revs - old_revs, new_context) or nothing_added, 557 revs_link(old_revs - new_revs, old_context) or nothing_removed, 558 ]) 559 # go through remaining old sources, those were deleted 560 removed_sources = [] 561 for spath, old_revs in old_sources.iteritems(): 562 removed_sources.append( 563 [tag.a(spath, _(' (removed)'), 564 title=_('View dir'), 565 href=old_context.href.browser(spath, 566 rev=old_context.resource.version)), 567 nothing_added, 568 nothing_removed]) 569 return tag.div( 570 tag.table( 571 tag.thead(tag.th(c) for c in [name, _("added"), _("removed")]), 572 tag.tbody([ 573 tag.tr([tag.td(col) for col in row]) 574 for row in modified_and_added_sources + removed_sources]))) 575 576 403 577 class SubversionRepository(Repository): 404 578 """Repository implementation based on the svn.fs API.""" 405 579 … … 734 908 735 909 class SubversionNode(Node): 736 910 737 def __init__(self, path, rev, repos, pool=None, parent =None):911 def __init__(self, path, rev, repos, pool=None, parent_root=None): 738 912 self.repos = repos 739 913 self.fs_ptr = repos.fs_ptr 740 914 self.authz = repos.authz … … 744 918 self._requested_rev = rev 745 919 pool = self.pool() 746 920 747 if parent and parent._requested_rev == self._requested_rev:748 self.root = parent .root921 if parent_root: 922 self.root = parent_root 749 923 else: 750 924 self.root = fs.revision_root(self.fs_ptr, rev, self.pool()) 751 925 node_type = fs.check_path(self.root, self._scoped_path_utf8, pool) … … 791 965 path.strip('/'))): 792 966 continue 793 967 yield SubversionNode(path, self._requested_rev, self.repos, 794 self.pool, self )968 self.pool, self.root) 795 969 796 970 def get_history(self, limit=None): 797 971 newer = None # 'newer' is the previously seen history tuple … … 871 1045 return fs.node_prop(self.root, self._scoped_path_utf8, name, 872 1046 self.pool()) 873 1047 1048 def get_copy_origin_rev(self): 1049 root_and_path = fs.closest_copy(self.root, self._scoped_path_utf8) 1050 if root_and_path: 1051 return fs.revision_root_revision(root_and_path[0]) 874 1052 1053 def get_copy_origin(self): 1054 root_and_path = fs.closest_copy(self.root, self._scoped_path_utf8) 1055 if root_and_path: 1056 root, path = root_and_path 1057 rev = fs.revision_root_revision(root) 1058 if (path, rev) == (self.path, self.rev): 1059 rev, path = fs.copied_from(root, path) 1060 return SubversionNode(path, rev, self.repos, self.pool, root) 1061 1062 875 1063 class SubversionChangeset(Changeset): 876 1064 877 1065 def __init__(self, rev, authz, scope, fs_ptr, pool=None): -
trac/versioncontrol/templates/browser.html
14 14 <script type="text/javascript" src="${chrome.htdocs_location}js/folding.js"></script> 15 15 <script type="text/javascript"> 16 16 jQuery(document).ready(function($) { 17 $("#toggleoldmerged").click(function() { 18 $("#oldmerged").slideToggle("fast"); 19 return false; 20 }); 17 21 $("#jumploc input").hide(); 18 22 $("#jumploc select").change(function () { 19 23 this.parentNode.parentNode.submit(); -
trac/util/__init__.py
542 542 else: 543 543 return 0 544 544 545 def to_ranges(revs): 546 """Converts a list of revisions to a minimal set of ranges. 547 548 >>> to_ranges([2, 12, 3, 6, 9, 1, 5, 11]) 549 '1-3,5-6,9,11-12' 550 >>> to_ranges([]) 551 '' 552 """ 553 ranges = [] 554 begin = end = None 555 def store(): 556 if end == begin: 557 ranges.append(str(begin)) 558 else: 559 ranges.append('%d-%d' % (begin, end)) 560 for rev in sorted(revs): 561 if begin is None: 562 begin = end = rev 563 elif rev == end + 1: 564 end = rev 565 else: 566 store() 567 begin = end = rev 568 if begin is not None: 569 store() 570 return ','.join(ranges) 571 545 572 def content_disposition(type, filename=None): 546 573 """Generate a properly escaped Content-Disposition header""" 547 574 if filename is not None:
