public function __construct(DifferentialRevision $revision, $mode)
 {
     $this->revision = $revision;
     $this->mode = $mode;
     $comments = id(new DifferentialComment())->loadAllWhere('revisionID = %d', $revision->getID());
     $this->comments = $comments;
 }
 protected function readValueFromRevision(DifferentialRevision $revision)
 {
     if (!$revision->getID()) {
         return null;
     }
     return $revision->getTestPlan();
 }
 public static function indexRevision(DifferentialRevision $rev)
 {
     $doc = new PhabricatorSearchAbstractDocument();
     $doc->setPHID($rev->getPHID());
     $doc->setDocumentType(PhabricatorPHIDConstants::PHID_TYPE_DREV);
     $doc->setDocumentTitle($rev->getTitle());
     $doc->setDocumentCreated($rev->getDateCreated());
     $doc->setDocumentModified($rev->getDateModified());
     $doc->addField(PhabricatorSearchField::FIELD_BODY, $rev->getSummary());
     $doc->addField(PhabricatorSearchField::FIELD_TEST_PLAN, $rev->getTestPlan());
     $doc->addRelationship(PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR, $rev->getAuthorPHID(), PhabricatorPHIDConstants::PHID_TYPE_USER, $rev->getDateCreated());
     if ($rev->getStatus() != ArcanistDifferentialRevisionStatus::CLOSED && $rev->getStatus() != ArcanistDifferentialRevisionStatus::ABANDONED) {
         $doc->addRelationship(PhabricatorSearchRelationship::RELATIONSHIP_OPEN, $rev->getPHID(), PhabricatorPHIDConstants::PHID_TYPE_DREV, time());
     }
     $comments = id(new DifferentialComment())->loadAllWhere('revisionID = %d', $rev->getID());
     $inlines = id(new DifferentialInlineComment())->loadAllWhere('revisionID = %d AND commentID IS NOT NULL', $rev->getID());
     $touches = array();
     foreach (array_merge($comments, $inlines) as $comment) {
         if (strlen($comment->getContent())) {
             $doc->addField(PhabricatorSearchField::FIELD_COMMENT, $comment->getContent());
         }
         $author = $comment->getAuthorPHID();
         $touches[$author] = $comment->getDateCreated();
     }
     foreach ($touches as $touch => $time) {
         $doc->addRelationship(PhabricatorSearchRelationship::RELATIONSHIP_TOUCH, $touch, PhabricatorPHIDConstants::PHID_TYPE_USER, $time);
     }
     $rev->loadRelationships();
     // If a revision needs review, the owners are the reviewers. Otherwise, the
     // owner is the author (e.g., accepted, rejected, closed).
     if ($rev->getStatus() == ArcanistDifferentialRevisionStatus::NEEDS_REVIEW) {
         foreach ($rev->getReviewers() as $phid) {
             $doc->addRelationship(PhabricatorSearchRelationship::RELATIONSHIP_OWNER, $phid, PhabricatorPHIDConstants::PHID_TYPE_USER, $rev->getDateModified());
             // Bogus timestamp.
         }
     } else {
         $doc->addRelationship(PhabricatorSearchRelationship::RELATIONSHIP_OWNER, $rev->getAuthorPHID(), PhabricatorPHIDConstants::PHID_TYPE_USER, $rev->getDateCreated());
     }
     $ccphids = $rev->getCCPHIDs();
     $handles = id(new PhabricatorObjectHandleData($ccphids))->loadHandles();
     foreach ($handles as $phid => $handle) {
         $doc->addRelationship(PhabricatorSearchRelationship::RELATIONSHIP_SUBSCRIBER, $phid, $handle->getType(), $rev->getDateModified());
         // Bogus timestamp.
     }
     self::reindexAbstractDocument($doc);
 }
 public function commitRevisionToWorkspace(DifferentialRevision $revision, ArcanistRepositoryAPI $workspace, PhabricatorUser $user)
 {
     $diff_id = $revision->loadActiveDiff()->getID();
     $call = new ConduitCall('differential.getrawdiff', array('diffID' => $diff_id));
     $call->setUser($user);
     $raw_diff = $call->execute();
     $future = $workspace->execFutureLocal('patch --no-commit -');
     $future->write($raw_diff);
     $future->resolvex();
     $workspace->reloadWorkingCopy();
     $call = new ConduitCall('differential.getcommitmessage', array('revision_id' => $revision->getID()));
     $call->setUser($user);
     $message = $call->execute();
     $author = id(new PhabricatorUser())->loadOneWhere('phid = %s', $revision->getAuthorPHID());
     $author_string = sprintf('%s <%s>', $author->getRealName(), $author->loadPrimaryEmailAddress());
     $author_date = $revision->getDateCreated();
     $workspace->execxLocal('commit --date=%s --user=%s ' . '--message=%s', $author_date . ' 0', $author_string, $message);
 }
 public function commitRevisionToWorkspace(DifferentialRevision $revision, ArcanistRepositoryAPI $workspace, PhabricatorUser $user)
 {
     $diff_id = $revision->loadActiveDiff()->getID();
     $call = new ConduitCall('differential.getrawdiff', array('diffID' => $diff_id));
     $call->setUser($user);
     $raw_diff = $call->execute();
     $missing_binary = "\nindex " . "0000000000000000000000000000000000000000.." . "0000000000000000000000000000000000000000\n";
     if (strpos($raw_diff, $missing_binary) !== false) {
         throw new Exception(pht('Patch is missing content for a binary file'));
     }
     $future = $workspace->execFutureLocal('apply --index -');
     $future->write($raw_diff);
     $future->resolvex();
     $workspace->reloadWorkingCopy();
     $call = new ConduitCall('differential.getcommitmessage', array('revision_id' => $revision->getID()));
     $call->setUser($user);
     $message = $call->execute();
     $author = id(new PhabricatorUser())->loadOneWhere('phid = %s', $revision->getAuthorPHID());
     $author_string = sprintf('%s <%s>', $author->getRealName(), $author->loadPrimaryEmailAddress());
     $author_date = $revision->getDateCreated();
     $workspace->execxLocal('-c user.name=%s -c user.email=%s ' . 'commit --date=%s --author=%s ' . '--message=%s', $user->getRealName(), $user->loadPrimaryEmailAddress(), $author_date, $author_string, $message);
 }
 private function buildSideNavView(DifferentialRevision $revision, array $changesets)
 {
     $nav = new AphrontSideNavFilterView();
     $nav->setBaseURI(new PhutilURI('/D' . $revision->getID()));
     $nav->setFlexible(true);
     $nav->addFilter('top', 'D' . $revision->getID(), '#top', $relative = false, 'phabricator-active-nav-focus');
     $tree = new PhutilFileTree();
     foreach ($changesets as $changeset) {
         $tree->addPath($changeset->getFilename(), $changeset);
     }
     require_celerity_resource('phabricator-filetree-view-css');
     $filetree = array();
     $path = $tree;
     while ($path = $path->getNextNode()) {
         $data = $path->getData();
         $name = $path->getName();
         $style = 'padding-left: ' . (2 + 3 * $path->getDepth()) . 'px';
         $href = null;
         if ($data) {
             $href = '#' . $data->getAnchorName();
             $icon = 'phabricator-filetree-icon-file';
         } else {
             $name .= '/';
             $icon = 'phabricator-filetree-icon-dir';
         }
         $icon = phutil_render_tag('span', array('class' => 'phabricator-filetree-icon ' . $icon), '');
         $name_element = phutil_render_tag('span', array('class' => 'phabricator-filetree-name'), phutil_escape_html($name));
         $filetree[] = javelin_render_tag($href ? 'a' : 'span', array('href' => $href, 'style' => $style, 'title' => $name, 'class' => 'phabricator-filetree-item'), $icon . $name_element);
     }
     $tree->destroy();
     $filetree = '<div class="phabricator-filetree">' . implode("\n", $filetree) . '</div>';
     $nav->addFilter('toc', 'Table of Contents', '#toc');
     $nav->addCustomBlock($filetree);
     $nav->addFilter('comment', 'Add Comment', '#comment');
     $nav->setActive(true);
     return $nav;
 }
 private function rejectOperation(DifferentialRevision $revision, $title, $body)
 {
     $id = $revision->getID();
     $detail_uri = "/D{$id}";
     return $this->newDialog()->setTitle($title)->appendParagraph($body)->addCancelButton($detail_uri);
 }
 /**
  * Update the table connecting revisions to DVCS local hashes, so we can
  * identify revisions by commit/tree hashes.
  */
 private function updateRevisionHashTable(DifferentialRevision $revision, DifferentialDiff $diff)
 {
     $vcs = $diff->getSourceControlSystem();
     if ($vcs == DifferentialRevisionControlSystem::SVN) {
         // Subversion has no local commit or tree hash information, so we don't
         // have to do anything.
         return;
     }
     $property = id(new DifferentialDiffProperty())->loadOneWhere('diffID = %d AND name = %s', $diff->getID(), 'local:commits');
     if (!$property) {
         return;
     }
     $hashes = array();
     $data = $property->getData();
     switch ($vcs) {
         case DifferentialRevisionControlSystem::GIT:
             foreach ($data as $commit) {
                 $hashes[] = array(ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT, $commit['commit']);
                 $hashes[] = array(ArcanistDifferentialRevisionHash::HASH_GIT_TREE, $commit['tree']);
             }
             break;
         case DifferentialRevisionControlSystem::MERCURIAL:
             foreach ($data as $commit) {
                 $hashes[] = array(ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT, $commit['rev']);
             }
             break;
     }
     $conn_w = $revision->establishConnection('w');
     $sql = array();
     foreach ($hashes as $info) {
         list($type, $hash) = $info;
         $sql[] = qsprintf($conn_w, '(%d, %s, %s)', $revision->getID(), $type, $hash);
     }
     queryfx($conn_w, 'DELETE FROM %T WHERE revisionID = %d', ArcanistDifferentialRevisionHash::TABLE_NAME, $revision->getID());
     if ($sql) {
         queryfx($conn_w, 'INSERT INTO %T (revisionID, type, hash) VALUES %Q', ArcanistDifferentialRevisionHash::TABLE_NAME, implode(', ', $sql));
     }
 }
 private function loadInlineComments(DifferentialRevision $revision, array &$changesets)
 {
     assert_instances_of($changesets, 'DifferentialChangeset');
     $inline_comments = array();
     $inline_comments = id(new DifferentialInlineCommentQuery())->withRevisionIDs(array($revision->getID()))->withNotDraft(true)->execute();
     $load_changesets = array();
     foreach ($inline_comments as $inline) {
         $changeset_id = $inline->getChangesetID();
         if (isset($changesets[$changeset_id])) {
             continue;
         }
         $load_changesets[$changeset_id] = true;
     }
     $more_changesets = array();
     if ($load_changesets) {
         $changeset_ids = array_keys($load_changesets);
         $more_changesets += id(new DifferentialChangeset())->loadAllWhere('id IN (%Ld)', $changeset_ids);
     }
     if ($more_changesets) {
         $changesets += $more_changesets;
         $changesets = msort($changesets, 'getSortKey');
     }
     return $inline_comments;
 }
 private function getRevisionActions(DifferentialRevision $revision)
 {
     $viewer_phid = $this->getRequest()->getUser()->getPHID();
     $viewer_is_owner = $revision->getAuthorPHID() == $viewer_phid;
     $viewer_is_reviewer = in_array($viewer_phid, $revision->getReviewers());
     $viewer_is_cc = in_array($viewer_phid, $revision->getCCPHIDs());
     $viewer_is_anonymous = !$this->getRequest()->getUser()->isLoggedIn();
     $status = $revision->getStatus();
     $revision_id = $revision->getID();
     $revision_phid = $revision->getPHID();
     $links = array();
     if ($viewer_is_owner) {
         $links[] = array('class' => 'revision-edit', 'href' => "/differential/revision/edit/{$revision_id}/", 'name' => 'Edit Revision');
     }
     if (!$viewer_is_anonymous) {
         if (!$viewer_is_owner && !$viewer_is_reviewer) {
             $action = $viewer_is_cc ? 'rem' : 'add';
             $links[] = array('class' => $viewer_is_cc ? 'subscribe-rem' : 'subscribe-add', 'href' => "/differential/subscribe/{$action}/{$revision_id}/", 'name' => $viewer_is_cc ? 'Unsubscribe' : 'Subscribe', 'instant' => true);
         } else {
             $links[] = array('class' => 'subscribe-rem unavailable', 'name' => 'Automatically Subscribed');
         }
         require_celerity_resource('phabricator-object-selector-css');
         require_celerity_resource('javelin-behavior-phabricator-object-selector');
         $links[] = array('class' => 'action-dependencies', 'name' => 'Edit Dependencies', 'href' => "/search/attach/{$revision_phid}/DREV/dependencies/", 'sigil' => 'workflow');
         if (PhabricatorEnv::getEnvConfig('maniphest.enabled')) {
             $links[] = array('class' => 'attach-maniphest', 'name' => 'Edit Maniphest Tasks', 'href' => "/search/attach/{$revision_phid}/TASK/", 'sigil' => 'workflow');
         }
         $links[] = array('class' => 'transcripts-metamta', 'name' => 'MetaMTA Transcripts', 'href' => "/mail/?phid={$revision_phid}");
         $links[] = array('class' => 'transcripts-herald', 'name' => 'Herald Transcripts', 'href' => "/herald/transcript/?phid={$revision_phid}");
     }
     return $links;
 }
 public function renderValueForRevisionList(DifferentialRevision $revision)
 {
     return 'D' . $revision->getID();
 }
 private function attachToRevision(DifferentialRevision $revision, $actor_phid)
 {
     $drequest = DiffusionRequest::newFromDictionary(array('repository' => $this->repository, 'commit' => $this->commit->getCommitIdentifier()));
     $raw_diff = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest)->loadRawDiff();
     $changes = id(new ArcanistDiffParser())->parseDiff($raw_diff);
     $diff = DifferentialDiff::newFromRawChanges($changes)->setRevisionID($revision->getID())->setAuthorPHID($actor_phid)->setCreationMethod('commit')->setSourceControlSystem($this->repository->getVersionControlSystem())->setLintStatus(DifferentialLintStatus::LINT_SKIP)->setUnitStatus(DifferentialUnitStatus::UNIT_SKIP)->setDateCreated($this->commit->getEpoch())->setDescription('Commit r' . $this->repository->getCallsign() . $this->commit->getCommitIdentifier());
     // TODO: This is not correct in SVN where one repository can have multiple
     // Arcanist projects.
     $arcanist_project = id(new PhabricatorRepositoryArcanistProject())->loadOneWhere('repositoryID = %d LIMIT 1', $this->repository->getID());
     if ($arcanist_project) {
         $diff->setArcanistProjectPHID($arcanist_project->getPHID());
     }
     $parents = DiffusionCommitParentsQuery::newFromDiffusionRequest($drequest)->loadParents();
     if ($parents) {
         $diff->setSourceControlBaseRevision(head_key($parents));
     }
     // TODO: Attach binary files.
     $revision->setLineCount($diff->getLineCount());
     return $diff->save();
 }
 private function buildSideNavView(DifferentialRevision $revision, array $changesets)
 {
     $nav = new AphrontSideNavFilterView();
     $nav->setBaseURI(new PhutilURI('/D' . $revision->getID()));
     $nav->setFlexible(true);
     $nav->addFilter('top', 'D' . $revision->getID(), '#top', $relative = false, 'phabricator-active-nav-focus');
     $tree = new PhutilFileTree();
     foreach ($changesets as $changeset) {
         try {
             $tree->addPath($changeset->getFilename(), $changeset);
         } catch (Exception $ex) {
             // TODO: See T1702. When viewing the versus diff of diffs, we may
             // have files with the same filename. For example, if you have a setup
             // like this in SVN:
             //
             //  a/
             //    README
             //  b/
             //    README
             //
             // ...and you run "arc diff" once from a/, and again from b/, you'll
             // get two diffs with path README. However, in the versus diff view we
             // will compute their absolute repository paths and detect that they
             // aren't really the same file. This is correct, but causes us to
             // throw when inserting them.
             //
             // We should probably compute the smallest unique path for each file
             // and show these as "a/README" and "b/README" when diffed against
             // one another. However, we get this wrong in a lot of places (the
             // other TOC shows two "README" files, and we generate the same anchor
             // hash for both) so I'm just stopping the bleeding until we can get
             // a proper fix in place.
         }
     }
     require_celerity_resource('phabricator-filetree-view-css');
     $filetree = array();
     $path = $tree;
     while ($path = $path->getNextNode()) {
         $data = $path->getData();
         $name = $path->getName();
         $style = 'padding-left: ' . (2 + 3 * $path->getDepth()) . 'px';
         $href = null;
         if ($data) {
             $href = '#' . $data->getAnchorName();
             $title = $name;
             $icon = 'phabricator-filetree-icon-file';
         } else {
             $name .= '/';
             $title = $path->getFullPath() . '/';
             $icon = 'phabricator-filetree-icon-dir';
         }
         $icon = phutil_render_tag('span', array('class' => 'phabricator-filetree-icon ' . $icon), '');
         $name_element = phutil_render_tag('span', array('class' => 'phabricator-filetree-name'), phutil_escape_html($name));
         $filetree[] = javelin_render_tag($href ? 'a' : 'span', array('href' => $href, 'style' => $style, 'title' => $title, 'class' => 'phabricator-filetree-item'), $icon . $name_element);
     }
     $tree->destroy();
     $filetree = '<div class="phabricator-filetree">' . implode("\n", $filetree) . '</div>';
     $nav->addFilter('toc', 'Table of Contents', '#toc');
     $nav->addCustomBlock($filetree);
     $nav->addFilter('comment', 'Add Comment', '#comment');
     $nav->setActive(true);
     return $nav;
 }
 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;
 }
 private function buildCommentForm(DifferentialRevision $revision, $field_list)
 {
     $viewer = $this->getViewer();
     $draft = id(new PhabricatorDraft())->loadOneWhere('authorPHID = %s AND draftKey = %s', $viewer->getPHID(), 'differential-comment-' . $revision->getID());
     $reviewers = array();
     $ccs = array();
     if ($draft) {
         $reviewers = idx($draft->getMetadata(), 'reviewers', array());
         $ccs = idx($draft->getMetadata(), 'ccs', array());
         if ($reviewers || $ccs) {
             $handles = $this->loadViewerHandles(array_merge($reviewers, $ccs));
             $reviewers = array_select_keys($handles, $reviewers);
             $ccs = array_select_keys($handles, $ccs);
         }
     }
     $comment_form = id(new DifferentialAddCommentView())->setRevision($revision);
     $review_warnings = array();
     foreach ($field_list->getFields() as $field) {
         $review_warnings[] = $field->getWarningsForDetailView();
     }
     $review_warnings = array_mergev($review_warnings);
     if ($review_warnings) {
         $review_warnings_panel = id(new PHUIInfoView())->setSeverity(PHUIInfoView::SEVERITY_WARNING)->setErrors($review_warnings);
         $comment_form->setInfoView($review_warnings_panel);
     }
     $action_uri = $this->getApplicationURI('comment/save/' . $revision->getID() . '/');
     $comment_form->setActions($this->getRevisionCommentActions($revision))->setActionURI($action_uri)->setUser($viewer)->setDraft($draft)->setReviewers(mpull($reviewers, 'getFullName', 'getPHID'))->setCCs(mpull($ccs, 'getFullName', 'getPHID'));
     // TODO: This just makes the "Z" key work. Generalize this and remove
     // it at some point.
     $comment_form = phutil_tag('div', array('class' => 'differential-add-comment-panel'), $comment_form);
     return $comment_form;
 }
 public function processRequest()
 {
     $request = $this->getRequest();
     if (!$this->id) {
         $this->id = $request->getInt('revisionID');
     }
     if ($this->id) {
         $revision = id(new DifferentialRevision())->load($this->id);
         if (!$revision) {
             return new Aphront404Response();
         }
     } else {
         $revision = new DifferentialRevision();
     }
     $diff_id = $request->getInt('diffID');
     if ($diff_id) {
         $diff = id(new DifferentialDiff())->load($diff_id);
         if (!$diff) {
             return new Aphront404Response();
         }
         if ($diff->getRevisionID()) {
             // TODO: Redirect?
             throw new Exception("This diff is already attached to a revision!");
         }
     } else {
         $diff = null;
     }
     $e_title = true;
     $e_testplan = true;
     $e_reviewers = null;
     $errors = array();
     $revision->loadRelationships();
     if ($request->isFormPost() && !$request->getStr('viaDiffView')) {
         $revision->setTitle($request->getStr('title'));
         $revision->setSummary($request->getStr('summary'));
         $revision->setTestPlan($request->getStr('testplan'));
         $revision->setBlameRevision($request->getStr('blame'));
         $revision->setRevertPlan($request->getStr('revert'));
         if (!strlen(trim($revision->getTitle()))) {
             $errors[] = 'You must provide a title.';
             $e_title = 'Required';
         } else {
             $e_title = null;
         }
         if (!strlen(trim($revision->getTestPlan()))) {
             $errors[] = 'You must provide a test plan.';
             $e_testplan = 'Required';
         } else {
             $e_testplan = null;
         }
         $user_phid = $request->getUser()->getPHID();
         if (in_array($user_phid, $request->getArr('reviewers'))) {
             $errors[] = 'You may not review your own revision.';
             $e_reviewers = 'Invalid';
         }
         if (!$errors) {
             $editor = new DifferentialRevisionEditor($revision, $user_phid);
             if ($diff) {
                 $editor->addDiff($diff, $request->getStr('comments'));
             }
             $editor->setCCPHIDs($request->getArr('cc'));
             $editor->setReviewers($request->getArr('reviewers'));
             $editor->save();
             return id(new AphrontRedirectResponse())->setURI('/D' . $revision->getID());
         }
         $reviewer_phids = $request->getArr('reviewers');
         $cc_phids = $request->getArr('cc');
     } else {
         $reviewer_phids = $revision->getReviewers();
         $cc_phids = $revision->getCCPHIDs();
     }
     $phids = array_merge($reviewer_phids, $cc_phids);
     $phids = array_unique($phids);
     $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
     $handles = mpull($handles, 'getFullName', 'getPHID');
     $reviewer_map = array_select_keys($handles, $reviewer_phids);
     $cc_map = array_select_keys($handles, $cc_phids);
     $form = new AphrontFormView();
     $form->setUser($request->getUser());
     if ($diff) {
         $form->addHiddenInput('diffID', $diff->getID());
     }
     if ($revision->getID()) {
         $form->setAction('/differential/revision/edit/' . $revision->getID() . '/');
     } else {
         $form->setAction('/differential/revision/edit/');
     }
     $error_view = null;
     if ($errors) {
         $error_view = id(new AphrontErrorView())->setTitle('Form Errors')->setErrors($errors);
     }
     if ($diff && $revision->getID()) {
         $form->appendChild(id(new AphrontFormTextAreaControl())->setLabel('Comments')->setName('comments')->setCaption("Explain what's new in this diff.")->setValue($request->getStr('comments')))->appendChild(id(new AphrontFormSubmitControl())->setValue('Save'))->appendChild(id(new AphrontFormDividerControl()));
     }
     $form->appendChild(id(new AphrontFormTextAreaControl())->setLabel('Title')->setName('title')->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT)->setValue($revision->getTitle())->setError($e_title))->appendChild(id(new AphrontFormTextAreaControl())->setLabel('Summary')->setName('summary')->setValue($revision->getSummary()))->appendChild(id(new AphrontFormTextAreaControl())->setLabel('Test Plan')->setName('testplan')->setValue($revision->getTestPlan())->setError($e_testplan))->appendChild(id(new AphrontFormTokenizerControl())->setLabel('Reviewers')->setName('reviewers')->setDatasource('/typeahead/common/users/')->setError($e_reviewers)->setValue($reviewer_map))->appendChild(id(new AphrontFormTokenizerControl())->setLabel('CC')->setName('cc')->setDatasource('/typeahead/common/mailable/')->setValue($cc_map))->appendChild(id(new AphrontFormTextControl())->setLabel('Blame Revision')->setName('blame')->setValue($revision->getBlameRevision())->setCaption('Revision which broke the stuff which this ' . 'change fixes.'))->appendChild(id(new AphrontFormTextAreaControl())->setLabel('Revert Plan')->setName('revert')->setValue($revision->getRevertPlan())->setCaption('Special steps required to safely revert this change.'));
     $submit = id(new AphrontFormSubmitControl())->setValue('Save');
     if ($diff) {
         $submit->addCancelButton('/differential/diff/' . $diff->getID() . '/');
     } else {
         $submit->addCancelButton('/D' . $revision->getID());
     }
     $form->appendChild($submit);
     $panel = new AphrontPanelView();
     if ($revision->getID()) {
         if ($diff) {
             $panel->setHeader('Update Differential Revision');
         } else {
             $panel->setHeader('Edit Differential Revision');
         }
     } else {
         $panel->setHeader('Create New Differential Revision');
     }
     $panel->appendChild($form);
     $panel->setWidth(AphrontPanelView::WIDTH_FORM);
     return $this->buildStandardPageResponse(array($error_view, $panel), array('title' => 'Edit Differential Revision'));
 }
 private function getRevisionActions(DifferentialRevision $revision)
 {
     $user = $this->getRequest()->getUser();
     $viewer_phid = $user->getPHID();
     $viewer_is_owner = $revision->getAuthorPHID() == $viewer_phid;
     $viewer_is_reviewer = in_array($viewer_phid, $revision->getReviewers());
     $viewer_is_cc = in_array($viewer_phid, $revision->getCCPHIDs());
     $viewer_is_anonymous = !$this->getRequest()->getUser()->isLoggedIn();
     $status = $revision->getStatus();
     $revision_id = $revision->getID();
     $revision_phid = $revision->getPHID();
     $links = array();
     if ($viewer_is_owner) {
         $links[] = array('class' => 'revision-edit', 'href' => "/differential/revision/edit/{$revision_id}/", 'name' => 'Edit Revision');
     }
     if (!$viewer_is_anonymous) {
         require_celerity_resource('phabricator-flag-css');
         $flag = PhabricatorFlagQuery::loadUserFlag($user, $revision_phid);
         if ($flag) {
             $class = PhabricatorFlagColor::getCSSClass($flag->getColor());
             $color = PhabricatorFlagColor::getColorName($flag->getColor());
             $links[] = array('class' => 'flag-clear ' . $class, 'href' => '/flag/delete/' . $flag->getID() . '/', 'name' => phutil_escape_html('Remove ' . $color . ' Flag'), 'sigil' => 'workflow');
         } else {
             $links[] = array('class' => 'flag-add phabricator-flag-ghost', 'href' => '/flag/edit/' . $revision_phid . '/', 'name' => 'Flag Revision', 'sigil' => 'workflow');
         }
         if (!$viewer_is_owner && !$viewer_is_reviewer) {
             $action = $viewer_is_cc ? 'rem' : 'add';
             $links[] = array('class' => $viewer_is_cc ? 'subscribe-rem' : 'subscribe-add', 'href' => "/differential/subscribe/{$action}/{$revision_id}/", 'name' => $viewer_is_cc ? 'Unsubscribe' : 'Subscribe', 'instant' => true);
         } else {
             $links[] = array('class' => 'subscribe-rem unavailable', 'name' => 'Automatically Subscribed');
         }
         require_celerity_resource('phabricator-object-selector-css');
         require_celerity_resource('javelin-behavior-phabricator-object-selector');
         $links[] = array('class' => 'action-dependencies', 'name' => 'Edit Dependencies', 'href' => "/search/attach/{$revision_phid}/DREV/dependencies/", 'sigil' => 'workflow');
         if (PhabricatorEnv::getEnvConfig('maniphest.enabled')) {
             $links[] = array('class' => 'attach-maniphest', 'name' => 'Edit Maniphest Tasks', 'href' => "/search/attach/{$revision_phid}/TASK/", 'sigil' => 'workflow');
         }
         if ($user->getIsAdmin()) {
             $links[] = array('class' => 'transcripts-metamta', 'name' => 'MetaMTA Transcripts', 'href' => "/mail/?phid={$revision_phid}");
         }
         $links[] = array('class' => 'transcripts-herald', 'name' => 'Herald Transcripts', 'href' => "/herald/transcript/?phid={$revision_phid}");
     }
     return $links;
 }
 private static function alterRelationships(DifferentialRevision $revision, array $stable_phids, array $rem_phids, array $add_phids, $reason_phid, $relation_type)
 {
     $rem_map = array_fill_keys($rem_phids, true);
     $add_map = array_fill_keys($add_phids, true);
     $seq_map = array_values($stable_phids);
     $seq_map = array_flip($seq_map);
     foreach ($rem_map as $phid => $ignored) {
         if (!isset($seq_map[$phid])) {
             $seq_map[$phid] = count($seq_map);
         }
     }
     foreach ($add_map as $phid => $ignored) {
         if (!isset($seq_map[$phid])) {
             $seq_map[$phid] = count($seq_map);
         }
     }
     $raw = $revision->getRawRelations($relation_type);
     $raw = ipull($raw, null, 'objectPHID');
     $sequence = count($seq_map);
     foreach ($raw as $phid => $ignored) {
         if (isset($seq_map[$phid])) {
             $raw[$phid]['sequence'] = $seq_map[$phid];
         } else {
             $raw[$phid]['sequence'] = $sequence++;
         }
     }
     $raw = isort($raw, 'sequence');
     foreach ($raw as $phid => $ignored) {
         if (isset($rem_map[$phid])) {
             unset($raw[$phid]);
         }
     }
     foreach ($add_phids as $add) {
         $raw[$add] = array('objectPHID' => $add, 'sequence' => idx($seq_map, $add, $sequence++), 'reasonPHID' => $reason_phid);
     }
     $conn_w = $revision->establishConnection('w');
     $sql = array();
     foreach ($raw as $relation) {
         $sql[] = qsprintf($conn_w, '(%d, %s, %s, %d, %s)', $revision->getID(), $relation_type, $relation['objectPHID'], $relation['sequence'], $relation['reasonPHID']);
     }
     $conn_w->openTransaction();
     queryfx($conn_w, 'DELETE FROM %T WHERE revisionID = %d AND relation = %s', DifferentialRevision::RELATIONSHIP_TABLE, $revision->getID(), $relation_type);
     if ($sql) {
         queryfx($conn_w, 'INSERT INTO %T
         (revisionID, relation, objectPHID, sequence, reasonPHID)
       VALUES %Q', DifferentialRevision::RELATIONSHIP_TABLE, implode(', ', $sql));
     }
     $conn_w->saveTransaction();
     $revision->loadRelationships();
 }
 private function getRevisionActions(DifferentialRevision $revision)
 {
     $viewer = $this->getRequest()->getUser();
     $revision_id = $revision->getID();
     $revision_phid = $revision->getPHID();
     $can_edit = PhabricatorPolicyFilter::hasCapability($viewer, $revision, PhabricatorPolicyCapability::CAN_EDIT);
     $actions = array();
     $actions[] = id(new PhabricatorActionView())->setIcon('fa-pencil')->setHref("/differential/revision/edit/{$revision_id}/")->setName(pht('Edit Revision'))->setDisabled(!$can_edit)->setWorkflow(!$can_edit);
     $actions[] = id(new PhabricatorActionView())->setIcon('fa-upload')->setHref("/differential/revision/update/{$revision_id}/")->setName(pht('Update Diff'))->setDisabled(!$can_edit)->setWorkflow(!$can_edit);
     $this->requireResource('phabricator-object-selector-css');
     $this->requireResource('javelin-behavior-phabricator-object-selector');
     $actions[] = id(new PhabricatorActionView())->setIcon('fa-link')->setName(pht('Edit Dependencies'))->setHref("/search/attach/{$revision_phid}/DREV/dependencies/")->setWorkflow(true)->setDisabled(!$can_edit);
     $maniphest = 'PhabricatorManiphestApplication';
     if (PhabricatorApplication::isClassInstalled($maniphest)) {
         $actions[] = id(new PhabricatorActionView())->setIcon('fa-anchor')->setName(pht('Edit Maniphest Tasks'))->setHref("/search/attach/{$revision_phid}/TASK/")->setWorkflow(true)->setDisabled(!$can_edit);
     }
     $request_uri = $this->getRequest()->getRequestURI();
     $actions[] = id(new PhabricatorActionView())->setIcon('fa-download')->setName(pht('Download Raw Diff'))->setHref($request_uri->alter('download', 'true'));
     return $actions;
 }
 private function renderRevisionTooltip(DifferentialRevision $revision, $handles)
 {
     $viewer = $this->getRequest()->getUser();
     $date = phabricator_date($revision->getDateModified(), $viewer);
     $id = $revision->getID();
     $title = $revision->getTitle();
     $header = "D{$id} {$title}";
     $author = $handles[$revision->getAuthorPHID()]->getName();
     return "{$header}\n{$date} · {$author}";
 }
 private function loadChangedByCommit(DifferentialRevision $revision, DifferentialDiff $diff)
 {
     $repository = $this->repository;
     $vs_diff = id(new DifferentialDiffQuery())->setViewer(PhabricatorUser::getOmnipotentUser())->withRevisionIDs(array($revision->getID()))->needChangesets(true)->setLimit(1)->executeOne();
     if (!$vs_diff) {
         return null;
     }
     if ($vs_diff->getCreationMethod() == 'commit') {
         return null;
     }
     $vs_changesets = array();
     foreach ($vs_diff->getChangesets() as $changeset) {
         $path = $changeset->getAbsoluteRepositoryPath($repository, $vs_diff);
         $path = ltrim($path, '/');
         $vs_changesets[$path] = $changeset;
     }
     $changesets = array();
     foreach ($diff->getChangesets() as $changeset) {
         $path = $changeset->getAbsoluteRepositoryPath($repository, $diff);
         $path = ltrim($path, '/');
         $changesets[$path] = $changeset;
     }
     if (array_fill_keys(array_keys($changesets), true) != array_fill_keys(array_keys($vs_changesets), true)) {
         return $vs_diff;
     }
     $file_phids = array();
     foreach ($vs_changesets as $changeset) {
         $metadata = $changeset->getMetadata();
         $file_phid = idx($metadata, 'new:binary-phid');
         if ($file_phid) {
             $file_phids[$file_phid] = $file_phid;
         }
     }
     $files = array();
     if ($file_phids) {
         $files = id(new PhabricatorFileQuery())->setViewer(PhabricatorUser::getOmnipotentUser())->withPHIDs($file_phids)->execute();
         $files = mpull($files, null, 'getPHID');
     }
     foreach ($changesets as $path => $changeset) {
         $vs_changeset = $vs_changesets[$path];
         $file_phid = idx($vs_changeset->getMetadata(), 'new:binary-phid');
         if ($file_phid) {
             if (!isset($files[$file_phid])) {
                 return $vs_diff;
             }
             $drequest = DiffusionRequest::newFromDictionary(array('user' => PhabricatorUser::getOmnipotentUser(), 'repository' => $this->repository, 'commit' => $this->commit->getCommitIdentifier(), 'path' => $path));
             $corpus = DiffusionFileContentQuery::newFromDiffusionRequest($drequest)->setViewer(PhabricatorUser::getOmnipotentUser())->loadFileContent()->getCorpus();
             if ($files[$file_phid]->loadFileData() != $corpus) {
                 return $vs_diff;
             }
         } else {
             $context = implode("\n", $changeset->makeChangesWithContext());
             $vs_context = implode("\n", $vs_changeset->makeChangesWithContext());
             // We couldn't just compare $context and $vs_context because following
             // diffs will be considered different:
             //
             //   -(empty line)
             //   -echo 'test';
             //    (empty line)
             //
             //    (empty line)
             //   -echo "test";
             //   -(empty line)
             $hunk = id(new DifferentialModernHunk())->setChanges($context);
             $vs_hunk = id(new DifferentialModernHunk())->setChanges($vs_context);
             if ($hunk->makeOldFile() != $vs_hunk->makeOldFile() || $hunk->makeNewFile() != $vs_hunk->makeNewFile()) {
                 return $vs_diff;
             }
         }
     }
     return null;
 }
 public function renderValueForRevisionList(DifferentialRevision $revision)
 {
     return phutil_render_tag('a', array('href' => '/D' . $revision->getID()), phutil_escape_html($revision->getTitle()));
 }