Path: blob/master/src/infrastructure/diff/engine/PhabricatorInlineCommentAdjustmentEngine.php
12242 views
<?php12final class PhabricatorInlineCommentAdjustmentEngine3extends Phobject {45private $viewer;6private $inlines;7private $revision;8private $oldChangesets;9private $newChangesets;1011public function setViewer(PhabricatorUser $viewer) {12$this->viewer = $viewer;13return $this;14}1516public function getViewer() {17return $this->viewer;18}1920public function setInlines(array $inlines) {21assert_instances_of($inlines, 'DifferentialInlineComment');22$this->inlines = $inlines;23return $this;24}2526public function getInlines() {27return $this->inlines;28}2930public function setOldChangesets(array $old_changesets) {31assert_instances_of($old_changesets, 'DifferentialChangeset');32$this->oldChangesets = $old_changesets;33return $this;34}3536public function getOldChangesets() {37return $this->oldChangesets;38}3940public function setNewChangesets(array $new_changesets) {41assert_instances_of($new_changesets, 'DifferentialChangeset');42$this->newChangesets = $new_changesets;43return $this;44}4546public function getNewChangesets() {47return $this->newChangesets;48}4950public function setRevision(DifferentialRevision $revision) {51$this->revision = $revision;52return $this;53}5455public function getRevision() {56return $this->revision;57}5859public function execute() {60$viewer = $this->getViewer();61$inlines = $this->getInlines();62$revision = $this->getRevision();63$old = $this->getOldChangesets();64$new = $this->getNewChangesets();6566$no_ghosts = $viewer->compareUserSetting(67PhabricatorOlderInlinesSetting::SETTINGKEY,68PhabricatorOlderInlinesSetting::VALUE_GHOST_INLINES_DISABLED);69if ($no_ghosts) {70return $inlines;71}7273$all = array_merge($old, $new);7475$changeset_ids = mpull($inlines, 'getChangesetID');76$changeset_ids = array_unique($changeset_ids);7778$all_map = mpull($all, null, 'getID');7980// We already have at least some changesets, and we might not need to do81// any more data fetching. Remove everything we already have so we can82// tell if we need new stuff.83foreach ($changeset_ids as $key => $id) {84if (isset($all_map[$id])) {85unset($changeset_ids[$key]);86}87}8889if ($changeset_ids) {90$changesets = id(new DifferentialChangesetQuery())91->setViewer($viewer)92->withIDs($changeset_ids)93->execute();94$changesets = mpull($changesets, null, 'getID');95} else {96$changesets = array();97}98$changesets += $all_map;99100$id_map = array();101foreach ($all as $changeset) {102$id_map[$changeset->getID()] = $changeset->getID();103}104105// Generate filename maps for older and newer comments. If we're bringing106// an older comment forward in a diff-of-diffs, we want to put it on the107// left side of the screen, not the right side. Both sides are "new" files108// with the same name, so they're both appropriate targets, but the left109// is a better target conceptually for users because it's more consistent110// with the rest of the UI, which shows old information on the left and111// new information on the right.112$move_here = DifferentialChangeType::TYPE_MOVE_HERE;113114$name_map_old = array();115$name_map_new = array();116$move_map = array();117foreach ($all as $changeset) {118$changeset_id = $changeset->getID();119120$filenames = array();121$filenames[] = $changeset->getFilename();122123// If this is the target of a move, also map comments on the old filename124// to this changeset.125if ($changeset->getChangeType() == $move_here) {126$old_file = $changeset->getOldFile();127$filenames[] = $old_file;128$move_map[$changeset_id][$old_file] = true;129}130131foreach ($filenames as $filename) {132// We update the old map only if we don't already have an entry (oldest133// changeset persists).134if (empty($name_map_old[$filename])) {135$name_map_old[$filename] = $changeset_id;136}137138// We always update the new map (newest changeset overwrites).139$name_map_new[$changeset->getFilename()] = $changeset_id;140}141}142143$new_id_map = mpull($new, null, 'getID');144145$results = array();146foreach ($inlines as $inline) {147$changeset_id = $inline->getChangesetID();148if (isset($id_map[$changeset_id])) {149// This inline is legitimately on one of the current changesets, so150// we can include it in the result set unmodified.151$results[] = $inline;152continue;153}154155$changeset = idx($changesets, $changeset_id);156if (!$changeset) {157// Just discard this inline, as it has bogus data.158continue;159}160161$target_id = null;162163if (isset($new_id_map[$changeset_id])) {164$name_map = $name_map_new;165$is_new = true;166} else {167$name_map = $name_map_old;168$is_new = false;169}170171$filename = $changeset->getFilename();172if (isset($name_map[$filename])) {173// This changeset is on a file with the same name as the current174// changeset, so we're going to port it forward or backward.175$target_id = $name_map[$filename];176177$is_move = isset($move_map[$target_id][$filename]);178if ($is_new) {179if ($is_move) {180$reason = pht(181'This comment was made on a file with the same name as the '.182'file this file was moved from, but in a newer diff.');183} else {184$reason = pht(185'This comment was made on a file with the same name, but '.186'in a newer diff.');187}188} else {189if ($is_move) {190$reason = pht(191'This comment was made on a file with the same name as the '.192'file this file was moved from, but in an older diff.');193} else {194$reason = pht(195'This comment was made on a file with the same name, but '.196'in an older diff.');197}198}199}200201202// If we didn't find a target and this change is the target of a move,203// look for a match against the old filename.204if (!$target_id) {205if ($changeset->getChangeType() == $move_here) {206$filename = $changeset->getOldFile();207if (isset($name_map[$filename])) {208$target_id = $name_map[$filename];209if ($is_new) {210$reason = pht(211'This comment was made on a file which this file was moved '.212'to, but in a newer diff.');213} else {214$reason = pht(215'This comment was made on a file which this file was moved '.216'to, but in an older diff.');217}218}219}220}221222223// If we found a changeset to port this comment to, bring it forward224// or backward and mark it.225if ($target_id) {226$diff_id = $changeset->getDiffID();227$inline_id = $inline->getID();228$revision_id = $revision->getID();229$href = "/D{$revision_id}?id={$diff_id}#inline-{$inline_id}";230231$inline232->makeEphemeral(true)233->setChangesetID($target_id)234->setIsGhost(235array(236'new' => $is_new,237'reason' => $reason,238'href' => $href,239'originalID' => $changeset->getID(),240));241242$results[] = $inline;243}244}245246// Filter out the inlines we ported forward which won't be visible because247// they appear on the wrong side of a file.248$keep_map = array();249foreach ($old as $changeset) {250$keep_map[$changeset->getID()][0] = true;251}252foreach ($new as $changeset) {253$keep_map[$changeset->getID()][1] = true;254}255256foreach ($results as $key => $inline) {257$is_new = (int)$inline->getIsNewFile();258$changeset_id = $inline->getChangesetID();259if (!isset($keep_map[$changeset_id][$is_new])) {260unset($results[$key]);261continue;262}263}264265// Adjust inline line numbers to account for content changes across266// updates and rebases.267$plan = array();268$need = array();269foreach ($results as $inline) {270$ghost = $inline->getIsGhost();271if (!$ghost) {272// If this isn't a "ghost" inline, ignore it.273continue;274}275276$src_id = $ghost['originalID'];277$dst_id = $inline->getChangesetID();278279$xforms = array();280281// If the comment is on the right, transform it through the inverse map282// back to the left.283if ($inline->getIsNewFile()) {284$xforms[] = array($src_id, $src_id, true);285}286287// Transform it across rebases.288$xforms[] = array($src_id, $dst_id, false);289290// If the comment is on the right, transform it back onto the right.291if ($inline->getIsNewFile()) {292$xforms[] = array($dst_id, $dst_id, false);293}294295$key = array();296foreach ($xforms as $xform) {297list($u, $v, $inverse) = $xform;298299$short = $u.'/'.$v;300$need[$short] = array($u, $v);301302$part = $u.($inverse ? '<' : '>').$v;303$key[] = $part;304}305$key = implode(',', $key);306307if (empty($plan[$key])) {308$plan[$key] = array(309'xforms' => $xforms,310'inlines' => array(),311);312}313314$plan[$key]['inlines'][] = $inline;315}316317if ($need) {318$maps = DifferentialLineAdjustmentMap::loadMaps($need);319} else {320$maps = array();321}322323foreach ($plan as $step) {324$xforms = $step['xforms'];325326$chain = null;327foreach ($xforms as $xform) {328list($u, $v, $inverse) = $xform;329$map = idx(idx($maps, $u, array()), $v);330if (!$map) {331continue 2;332}333334if ($inverse) {335$map = DifferentialLineAdjustmentMap::newInverseMap($map);336} else {337$map = clone $map;338}339340if ($chain) {341$chain->addMapToChain($map);342} else {343$chain = $map;344}345}346347foreach ($step['inlines'] as $inline) {348$head_line = $inline->getLineNumber();349$tail_line = ($head_line + $inline->getLineLength());350351$head_info = $chain->mapLine($head_line, false);352$tail_info = $chain->mapLine($tail_line, true);353354list($head_deleted, $head_offset, $head_line) = $head_info;355list($tail_deleted, $tail_offset, $tail_line) = $tail_info;356357if ($head_offset !== false) {358$inline->setLineNumber($head_line + $head_offset);359} else {360$inline->setLineNumber($head_line);361$inline->setLineLength($tail_line - $head_line);362}363}364}365366return $results;367}368369}370371372