| | 410 | def _get_node_revs_ref(self, repos, path, version): |
| | 411 | node = repos.get_node(path, version) |
| | 412 | for (p, rev, chg) in node.get_history(): |
| | 413 | if p != path: |
| | 414 | break |
| | 415 | yield rev |
| | 416 | |
| | 417 | def _get_node_revs(self, repos, path, version): |
| | 418 | node = repos.get_node(path, version) # Ensure node exists |
| | 419 | db = self.env.get_db_cnx() |
| | 420 | cursor = db.cursor() |
| | 421 | cursor.execute("SELECT DISTINCT rev FROM node_change " |
| | 422 | "WHERE (path = %%s OR path %s) " |
| | 423 | " AND %s <= %%s" % (db.like(), db.cast('rev', 'int')), |
| | 424 | (path, db.like_escape(path + '/') + '%', version)) |
| | 425 | revs = list(int(row[0]) for row in cursor) |
| | 426 | revs.sort() |
| | 427 | cursor.execute("SELECT rev FROM node_change " |
| | 428 | "WHERE path = %%s " |
| | 429 | " AND change_type IN ('A', 'C', 'M') " |
| | 430 | " AND %s <= %%s " |
| | 431 | "ORDER BY %s DESC " |
| | 432 | "LIMIT 1" % ((db.cast('rev', 'int'),) * 2), |
| | 433 | (path, version)) |
| | 434 | created = 0 |
| | 435 | for row in cursor: |
| | 436 | created = int(row[0]) |
| | 437 | import bisect |
| | 438 | return revs[bisect.bisect_left(revs, created):] |
| | 439 | |
| | 440 | def _render_mergeinfo(self, name, mode, context, props): |
| | 441 | """Parse svn:mergeinfo and svnmerge-* properties, converting branch |
| | 442 | names to links and providing links to the revision log for merged |
| | 443 | and eligible revisions. |
| | 444 | """ |
| | 445 | has_eligible = name in ('svnmerge-integrated', 'svn:mergeinfo') |
| | 446 | revs_label = (_('merged'), _('blocked'))[name.endswith('blocked')] |
| | 447 | repos = self.env.get_repository() |
| | 448 | rows = [] |
| | 449 | for line in props[name].splitlines(): |
| | 450 | try: |
| | 451 | path, revs = line.split(':', 1) |
| | 452 | spath = path.strip('/') |
| | 453 | revs = revs.strip() |
| | 454 | if 'LOG_VIEW' in context.perm('source', spath): |
| | 455 | row = [tag.a(path, title=_('View dir'), |
| | 456 | href=context.href.browser(spath, |
| | 457 | rev=context.resource.version))] |
| | 458 | row.append(self._get_revs_link(revs_label, context, spath, |
| | 459 | revs)) |
| | 460 | if has_eligible: |
| | 461 | t = time.time() |
| | 462 | eligible = set(self._get_node_revs(repos, spath, |
| | 463 | context.resource.version)) |
| | 464 | t = time.time() - t |
| | 465 | t_ref = time.time() |
| | 466 | eligible_ref = set(self._get_node_revs_ref(repos, spath, |
| | 467 | context.resource.version)) |
| | 468 | t_ref = time.time() - t_ref |
| | 469 | eligible_error = eligible ^ eligible_ref |
| | 470 | eligible -= set(Ranges(revs)) |
| | 471 | blocked = self._get_blocked_revs(props, name, spath) |
| | 472 | eligible -= set(Ranges(blocked)) |
| | 473 | if eligible: |
| | 474 | eligible = to_ranges(eligible) |
| | 475 | row.append(tag.a(_('eligible'), |
| | 476 | title=eligible.replace(',', ', '), |
| | 477 | href=context.href.log(spath, |
| | 478 | revs=eligible))) |
| | 479 | else: |
| | 480 | row.append(tag.span(_('eligible'), |
| | 481 | title=_('No eligible revisions'))) |
| | 482 | row.append(tag("(time=%.3f, time_ref=%.3f, error: %s)" |
| | 483 | % (t, t_ref, ", ".join(str(rev) |
| | 484 | for rev in eligible_error)))) |
| | 485 | rows.append(tag.td(each) for each in row) |
| | 486 | else: |
| | 487 | revs = revs.replace(',', u',\u200b') |
| | 488 | rows.append(tag.td(path), |
| | 489 | tag.td(revs, |
| | 490 | colspan=has_eligible and 2 or None)) |
| | 491 | except Exception, e: |
| | 492 | self.log.warning('Rendering of property %s failed: %s', name, |
| | 493 | exception_to_unicode(e)) |
| | 494 | rows.append(tag.td(line, colspan=has_eligible and 3 or 2)) |
| | 495 | return tag.div(tag.table(tag.tbody(tag.tr(each) for each in rows))) |
| | 496 | |
| | 497 | def _get_revs_link(self, label, context, spath, revs): |
| | 498 | if not revs: |
| | 499 | return tag.span(label, title=_('No revisions')) |
| | 500 | elif ',' in revs or '-' in revs: |
| | 501 | revs_href = context.href.log(spath, revs=revs) |
| | 502 | else: |
| | 503 | revs_href = context.href.changeset(revs, spath) |
| | 504 | return tag.a(label, title=revs.replace(',', ', '), href=revs_href) |
| | 505 | |
| | 510 | # IPropertyDiffRenderer methods |
| | 511 | |
| | 512 | def match_property_diff(self, name): |
| | 513 | return (name == 'svn:mergeinfo' or name.startswith('svnmerge-')) and \ |
| | 514 | 4 or 0 |
| | 515 | |
| | 516 | def render_property_diff(self, name, old_context, old_props, |
| | 517 | new_context, new_props, options): |
| | 518 | # build 3 columns table showing modifications on merge sources |
| | 519 | # || source (added|modified|removed) || added revs || removed revs || |
| | 520 | def parse_sources(props): |
| | 521 | sources = {} |
| | 522 | for line in props[name].splitlines(): |
| | 523 | path, revs = line.split(':', 1) |
| | 524 | spath = path.strip('/') |
| | 525 | revs = revs.strip() |
| | 526 | sources[spath] = set(Ranges(revs)) |
| | 527 | return sources |
| | 528 | old_sources = parse_sources(old_props) |
| | 529 | new_sources = parse_sources(new_props) |
| | 530 | # go through new sources, detect modified ones or added ones |
| | 531 | blocked = name.endswith('blocked') |
| | 532 | added_label = [_('merged: '), _('blocked: ')][blocked] |
| | 533 | removed_lable = [_('un-merged: '), _('un-blocked: ')][blocked] |
| | 534 | def revs_link(revs, context): |
| | 535 | if revs: |
| | 536 | revs = to_ranges(revs) |
| | 537 | return self._get_revs_link(revs.replace(',', u',\u200b'), |
| | 538 | context, spath, revs) |
| | 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 = None |
| | 544 | else: |
| | 545 | old_revs = set() |
| | 546 | status = _(' (added)') |
| | 547 | source_href = new_context.href.browser(spath, |
| | 548 | rev=new_context.resource.version) |
| | 549 | added = revs_link(new_revs - old_revs, new_context) |
| | 550 | removed = revs_link(old_revs - new_revs, old_context) |
| | 551 | if added or removed: |
| | 552 | modified_and_added_sources.append([ |
| | 553 | [tag.a(spath, title=_('View dir'), href=source_href), |
| | 554 | status], |
| | 555 | added and tag(added_label, added), |
| | 556 | removed and tag(removed_label, removed)]) |
| | 557 | # go through remaining old sources, those were deleted |
| | 558 | removed_sources = [] |
| | 559 | for spath, old_revs in old_sources.iteritems(): |
| | 560 | removed_sources.append( |
| | 561 | tag.a(_("Source %(source)s removed", source=spath), |
| | 562 | title=_('View dir'), |
| | 563 | href=old_context.href.browser(spath, |
| | 564 | rev=old_context.resource.version))) |
| | 565 | return tag.li(tag_("Property %(prop)s changed", prop=tag.strong(name)), |
| | 566 | tag.table(tag.tbody( |
| | 567 | [tag.tr(tag.td(dir), tag.td(added), tag.td(removed)) |
| | 568 | for dir, added, removed in modified_and_added_sources], |
| | 569 | [tag.tr(tag.td(dir, colspan=3)) |
| | 570 | for dir in removed_sources]), class_='props')) |
| | 571 | |