public function testChainMaps() { // This test simulates porting inlines forward across a rebase. // Part 1 is the original diff. // Part 2 is the rebase, which we would normally compute synthetically. // Part 3 is the updated diff against the rebased changes. $diff1 = $this->loadHunks('chain.adjust.1.diff'); $diff2 = $this->loadHunks('chain.adjust.2.diff'); $diff3 = $this->loadHunks('chain.adjust.3.diff'); $map = DifferentialLineAdjustmentMap::newInverseMap(DifferentialLineAdjustmentMap::newFromHunks($diff1)); $map->addMapToChain(DifferentialLineAdjustmentMap::newFromHunks($diff2)); $map->addMapToChain(DifferentialLineAdjustmentMap::newFromHunks($diff3)); $actual = array(); for ($ii = 1; $ii <= 13; $ii++) { $actual[$ii] = array($map->mapLine($ii, false), $map->mapLine($ii, true)); } $this->assertEqual(array(1 => array(array(false, false, 1), array(false, false, 1)), 2 => array(array(true, false, 1), array(true, false, 2)), 3 => array(array(true, false, 1), array(true, false, 2)), 4 => array(array(false, false, 2), array(false, false, 2)), 5 => array(array(false, false, 3), array(false, false, 3)), 6 => array(array(false, false, 4), array(false, false, 4)), 7 => array(array(false, false, 5), array(false, false, 8)), 8 => array(array(false, 0, 5), array(false, false, 9)), 9 => array(array(false, 1, 5), array(false, false, 9)), 10 => array(array(false, 2, 5), array(false, false, 9)), 11 => array(array(false, false, 9), array(false, false, 9)), 12 => array(array(false, false, 10), array(false, false, 10)), 13 => array(array(false, false, 11), array(false, false, 11))), $actual); }
public function adjustInlinesForChangesets(array $inlines, array $old, array $new, DifferentialRevision $revision) { assert_instances_of($inlines, 'DifferentialInlineComment'); assert_instances_of($old, 'DifferentialChangeset'); assert_instances_of($new, 'DifferentialChangeset'); $viewer = $this->getViewer(); $pref = $viewer->loadPreferences()->getPreference(PhabricatorUserPreferences::PREFERENCE_DIFF_GHOSTS); if ($pref == 'disabled') { return $inlines; } $all = array_merge($old, $new); $changeset_ids = mpull($inlines, 'getChangesetID'); $changeset_ids = array_unique($changeset_ids); $all_map = mpull($all, null, 'getID'); // We already have at least some changesets, and we might not need to do // any more data fetching. Remove everything we already have so we can // tell if we need new stuff. foreach ($changeset_ids as $key => $id) { if (isset($all_map[$id])) { unset($changeset_ids[$key]); } } if ($changeset_ids) { $changesets = id(new DifferentialChangesetQuery())->setViewer($viewer)->withIDs($changeset_ids)->execute(); $changesets = mpull($changesets, null, 'getID'); } else { $changesets = array(); } $changesets += $all_map; $id_map = array(); foreach ($all as $changeset) { $id_map[$changeset->getID()] = $changeset->getID(); } // Generate filename maps for older and newer comments. If we're bringing // an older comment forward in a diff-of-diffs, we want to put it on the // left side of the screen, not the right side. Both sides are "new" files // with the same name, so they're both appropriate targets, but the left // is a better target conceptually for users because it's more consistent // with the rest of the UI, which shows old information on the left and // new information on the right. $move_here = DifferentialChangeType::TYPE_MOVE_HERE; $name_map_old = array(); $name_map_new = array(); $move_map = array(); foreach ($all as $changeset) { $changeset_id = $changeset->getID(); $filenames = array(); $filenames[] = $changeset->getFilename(); // If this is the target of a move, also map comments on the old filename // to this changeset. if ($changeset->getChangeType() == $move_here) { $old_file = $changeset->getOldFile(); $filenames[] = $old_file; $move_map[$changeset_id][$old_file] = true; } foreach ($filenames as $filename) { // We update the old map only if we don't already have an entry (oldest // changeset persists). if (empty($name_map_old[$filename])) { $name_map_old[$filename] = $changeset_id; } // We always update the new map (newest changeset overwrites). $name_map_new[$changeset->getFilename()] = $changeset_id; } } // Find the smallest "new" changeset ID. We'll consider everything // larger than this to be "newer", and everything smaller to be "older". $first_new_id = min(mpull($new, 'getID')); $results = array(); foreach ($inlines as $inline) { $changeset_id = $inline->getChangesetID(); if (isset($id_map[$changeset_id])) { // This inline is legitimately on one of the current changesets, so // we can include it in the result set unmodified. $results[] = $inline; continue; } $changeset = idx($changesets, $changeset_id); if (!$changeset) { // Just discard this inline, as it has bogus data. continue; } $target_id = null; if ($changeset_id >= $first_new_id) { $name_map = $name_map_new; $is_new = true; } else { $name_map = $name_map_old; $is_new = false; } $filename = $changeset->getFilename(); if (isset($name_map[$filename])) { // This changeset is on a file with the same name as the current // changeset, so we're going to port it forward or backward. $target_id = $name_map[$filename]; $is_move = isset($move_map[$target_id][$filename]); if ($is_new) { if ($is_move) { $reason = pht('This comment was made on a file with the same name as the ' . 'file this file was moved from, but in a newer diff.'); } else { $reason = pht('This comment was made on a file with the same name, but ' . 'in a newer diff.'); } } else { if ($is_move) { $reason = pht('This comment was made on a file with the same name as the ' . 'file this file was moved from, but in an older diff.'); } else { $reason = pht('This comment was made on a file with the same name, but ' . 'in an older diff.'); } } } // If we didn't find a target and this change is the target of a move, // look for a match against the old filename. if (!$target_id) { if ($changeset->getChangeType() == $move_here) { $filename = $changeset->getOldFile(); if (isset($name_map[$filename])) { $target_id = $name_map[$filename]; if ($is_new) { $reason = pht('This comment was made on a file which this file was moved ' . 'to, but in a newer diff.'); } else { $reason = pht('This comment was made on a file which this file was moved ' . 'to, but in an older diff.'); } } } } // If we found a changeset to port this comment to, bring it forward // or backward and mark it. if ($target_id) { $diff_id = $changeset->getDiffID(); $inline_id = $inline->getID(); $revision_id = $revision->getID(); $href = "/D{$revision_id}?id={$diff_id}#inline-{$inline_id}"; $inline->makeEphemeral(true)->setChangesetID($target_id)->setIsGhost(array('new' => $is_new, 'reason' => $reason, 'href' => $href, 'originalID' => $changeset->getID())); $results[] = $inline; } } // Filter out the inlines we ported forward which won't be visible because // they appear on the wrong side of a file. $keep_map = array(); foreach ($old as $changeset) { $keep_map[$changeset->getID()][0] = true; } foreach ($new as $changeset) { $keep_map[$changeset->getID()][1] = true; } foreach ($results as $key => $inline) { $is_new = (int) $inline->getIsNewFile(); $changeset_id = $inline->getChangesetID(); if (!isset($keep_map[$changeset_id][$is_new])) { unset($results[$key]); continue; } } // Adjust inline line numbers to account for content changes across // updates and rebases. $plan = array(); $need = array(); foreach ($results as $inline) { $ghost = $inline->getIsGhost(); if (!$ghost) { // If this isn't a "ghost" inline, ignore it. continue; } $src_id = $ghost['originalID']; $dst_id = $inline->getChangesetID(); $xforms = array(); // If the comment is on the right, transform it through the inverse map // back to the left. if ($inline->getIsNewFile()) { $xforms[] = array($src_id, $src_id, true); } // Transform it across rebases. $xforms[] = array($src_id, $dst_id, false); // If the comment is on the right, transform it back onto the right. if ($inline->getIsNewFile()) { $xforms[] = array($dst_id, $dst_id, false); } $key = array(); foreach ($xforms as $xform) { list($u, $v, $inverse) = $xform; $short = $u . '/' . $v; $need[$short] = array($u, $v); $part = $u . ($inverse ? '<' : '>') . $v; $key[] = $part; } $key = implode(',', $key); if (empty($plan[$key])) { $plan[$key] = array('xforms' => $xforms, 'inlines' => array()); } $plan[$key]['inlines'][] = $inline; } if ($need) { $maps = DifferentialLineAdjustmentMap::loadMaps($need); } else { $maps = array(); } foreach ($plan as $step) { $xforms = $step['xforms']; $chain = null; foreach ($xforms as $xform) { list($u, $v, $inverse) = $xform; $map = idx(idx($maps, $u, array()), $v); if (!$map) { continue 2; } if ($inverse) { $map = DifferentialLineAdjustmentMap::newInverseMap($map); } else { $map = clone $map; } if ($chain) { $chain->addMapToChain($map); } else { $chain = $map; } } foreach ($step['inlines'] as $inline) { $head_line = $inline->getLineNumber(); $tail_line = $head_line + $inline->getLineLength(); $head_info = $chain->mapLine($head_line, false); $tail_info = $chain->mapLine($tail_line, true); list($head_deleted, $head_offset, $head_line) = $head_info; list($tail_deleted, $tail_offset, $tail_line) = $tail_info; if ($head_offset !== false) { $inline->setLineNumber($head_line + 1 + $head_offset); } else { $inline->setLineNumber($head_line); $inline->setLineLength($tail_line - $head_line); } } } return $results; }
public static function newInverseMap(DifferentialLineAdjustmentMap $map) { $old = $map->getMap(); $inv = array(); $last = 0; foreach ($old as $k => $v) { if (count($v) > 1) { $v = range(reset($v), end($v)); } if ($k == 0) { foreach ($v as $line) { $inv[$line] = array(); $last = $line; } } else { if ($v) { $first = true; foreach ($v as $line) { if ($first) { $first = false; $inv[$line][] = $k; $last = $line; } else { $inv[$line] = array(); } } } else { $inv[$last][] = $k; } } } $inv = self::reduceMapRanges($inv); $obj = new DifferentialLineAdjustmentMap(); $obj->map = $inv; $obj->isInverse = !$map->isInverse; return $obj; }