diff --git a/resources/sql/autopatches/20180403.draft.01.broadcast.php b/resources/sql/autopatches/20180403.draft.01.broadcast.php new file mode 100644 index 0000000000..b237894c90 --- /dev/null +++ b/resources/sql/autopatches/20180403.draft.01.broadcast.php @@ -0,0 +1,20 @@ +establishConnection('w'); + +$drafts = $table->loadAllWhere( + 'status = %s', + DifferentialRevisionStatus::DRAFT); +foreach ($drafts as $draft) { + $properties = $draft->getProperties(); + + $properties[DifferentialRevision::PROPERTY_SHOULD_BROADCAST] = false; + + queryfx( + $conn, + 'UPDATE %T SET properties = %s WHERE id = %d', + id(new DifferentialRevision())->getTableName(), + phutil_json_encode($properties), + $draft->getID()); +} diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php index d7280cb0b6..b03c4e8c4f 100644 --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -1,1153 +1,1169 @@ getViewer(); $this->revisionID = $request->getURIData('id'); $viewer_is_anonymous = !$viewer->isLoggedIn(); $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($this->revisionID)) ->setViewer($viewer) ->needReviewers(true) ->needReviewerAuthority(true) ->executeOne(); if (!$revision) { return new Aphront404Response(); } $diffs = id(new DifferentialDiffQuery()) ->setViewer($viewer) ->withRevisionIDs(array($this->revisionID)) ->execute(); $diffs = array_reverse($diffs, $preserve_keys = true); if (!$diffs) { throw new Exception( pht('This revision has no diffs. Something has gone quite wrong.')); } $revision->attachActiveDiff(last($diffs)); $diff_vs = $request->getInt('vs'); $target_id = $request->getInt('id'); $target = idx($diffs, $target_id, end($diffs)); $target_manual = $target; if (!$target_id) { foreach ($diffs as $diff) { if ($diff->getCreationMethod() != 'commit') { $target_manual = $diff; } } } if (empty($diffs[$diff_vs])) { $diff_vs = null; } $repository = null; $repository_phid = $target->getRepositoryPHID(); if ($repository_phid) { if ($repository_phid == $revision->getRepositoryPHID()) { $repository = $revision->getRepository(); } else { $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withPHIDs(array($repository_phid)) ->executeOne(); } } list($changesets, $vs_map, $vs_changesets, $rendering_references) = $this->loadChangesetsAndVsMap( $target, idx($diffs, $diff_vs), $repository); if ($request->getExists('download')) { return $this->buildRawDiffResponse( $revision, $changesets, $vs_changesets, $vs_map, $repository); } $map = $vs_map; if (!$map) { $map = array_fill_keys(array_keys($changesets), 0); } $old_ids = array(); $new_ids = array(); foreach ($map as $id => $vs) { if ($vs <= 0) { $old_ids[] = $id; $new_ids[] = $id; } else { $new_ids[] = $id; $new_ids[] = $vs; } } $this->loadDiffProperties($diffs); $props = $target_manual->getDiffProperties(); $subscriber_phids = PhabricatorSubscribersQuery::loadSubscribersForPHID( $revision->getPHID()); $object_phids = array_merge( $revision->getReviewerPHIDs(), $subscriber_phids, $revision->loadCommitPHIDs(), array( $revision->getAuthorPHID(), $viewer->getPHID(), )); foreach ($revision->getAttached() as $type => $phids) { foreach ($phids as $phid => $info) { $object_phids[] = $phid; } } $field_list = PhabricatorCustomField::getObjectFields( $revision, PhabricatorCustomField::ROLE_VIEW); $field_list->setViewer($viewer); $field_list->readFieldsFromStorage($revision); $warning_handle_map = array(); foreach ($field_list->getFields() as $key => $field) { $req = $field->getRequiredHandlePHIDsForRevisionHeaderWarnings(); foreach ($req as $phid) { $warning_handle_map[$key][] = $phid; $object_phids[] = $phid; } } $handles = $this->loadViewerHandles($object_phids); $request_uri = $request->getRequestURI(); $limit = 100; $large = $request->getStr('large'); if (count($changesets) > $limit && !$large) { $count = count($changesets); $warning = new PHUIInfoView(); $warning->setTitle(pht('Very Large Diff')); $warning->setSeverity(PHUIInfoView::SEVERITY_WARNING); $warning->appendChild(hsprintf( '%s %s', pht( 'This diff is very large and affects %s files. '. 'You may load each file individually or ', new PhutilNumber($count)), phutil_tag( 'a', array( 'class' => 'button button-grey', 'href' => $request_uri ->alter('large', 'true') ->setFragment('toc'), ), pht('Show All Files Inline')))); $warning = $warning->render(); $old = array_select_keys($changesets, $old_ids); $new = array_select_keys($changesets, $new_ids); $query = id(new DifferentialInlineCommentQuery()) ->setViewer($viewer) ->needHidden(true) ->withRevisionPHIDs(array($revision->getPHID())); $inlines = $query->execute(); $inlines = $query->adjustInlinesForChangesets( $inlines, $old, $new, $revision); $visible_changesets = array(); foreach ($inlines as $inline) { $changeset_id = $inline->getChangesetID(); if (isset($changesets[$changeset_id])) { $visible_changesets[$changeset_id] = $changesets[$changeset_id]; } } } else { $warning = null; $visible_changesets = $changesets; } $commit_hashes = mpull($diffs, 'getSourceControlBaseRevision'); $local_commits = idx($props, 'local:commits', array()); foreach ($local_commits as $local_commit) { $commit_hashes[] = idx($local_commit, 'tree'); $commit_hashes[] = idx($local_commit, 'local'); } $commit_hashes = array_unique(array_filter($commit_hashes)); if ($commit_hashes) { $commits_for_links = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withIdentifiers($commit_hashes) ->execute(); $commits_for_links = mpull( $commits_for_links, null, 'getCommitIdentifier'); } else { $commits_for_links = array(); } $header = $this->buildHeader($revision); $subheader = $this->buildSubheaderView($revision); $details = $this->buildDetails($revision, $field_list); $curtain = $this->buildCurtain($revision); $whitespace = $request->getStr( 'whitespace', DifferentialChangesetParser::WHITESPACE_IGNORE_MOST); $repository = $revision->getRepository(); if ($repository) { $symbol_indexes = $this->buildSymbolIndexes( $repository, $visible_changesets); } else { $symbol_indexes = array(); } $revision_warnings = $this->buildRevisionWarnings( $revision, $field_list, $warning_handle_map, $handles); $info_view = null; if ($revision_warnings) { $info_view = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setErrors($revision_warnings); } $detail_diffs = array_select_keys( $diffs, array($diff_vs, $target->getID())); $detail_diffs = mpull($detail_diffs, null, 'getPHID'); $this->loadHarbormasterData($detail_diffs); $diff_detail_box = $this->buildDiffDetailView( $detail_diffs, $revision, $field_list); $unit_box = $this->buildUnitMessagesView( $target, $revision); $timeline = $this->buildTransactions( $revision, $diff_vs ? $diffs[$diff_vs] : $target, $target, $old_ids, $new_ids); $timeline->setQuoteRef($revision->getMonogram()); $changeset_view = id(new DifferentialChangesetListView()) ->setChangesets($changesets) ->setVisibleChangesets($visible_changesets) ->setStandaloneURI('/differential/changeset/') ->setRawFileURIs( '/differential/changeset/?view=old', '/differential/changeset/?view=new') ->setUser($viewer) ->setDiff($target) ->setRenderingReferences($rendering_references) ->setVsMap($vs_map) ->setWhitespace($whitespace) ->setSymbolIndexes($symbol_indexes) ->setTitle(pht('Diff %s', $target->getID())) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); $revision_id = $revision->getID(); $inline_list_uri = "/revision/inlines/{$revision_id}/"; $inline_list_uri = $this->getApplicationURI($inline_list_uri); $changeset_view->setInlineListURI($inline_list_uri); if ($repository) { $changeset_view->setRepository($repository); } if (!$viewer_is_anonymous) { $changeset_view->setInlineCommentControllerURI( '/differential/comment/inline/edit/'.$revision->getID().'/'); } $broken_diffs = $this->loadHistoryDiffStatus($diffs); $history = id(new DifferentialRevisionUpdateHistoryView()) ->setUser($viewer) ->setDiffs($diffs) ->setDiffUnitStatuses($broken_diffs) ->setSelectedVersusDiffID($diff_vs) ->setSelectedDiffID($target->getID()) ->setSelectedWhitespace($whitespace) ->setCommitsForLinks($commits_for_links); $local_table = id(new DifferentialLocalCommitsView()) ->setUser($viewer) ->setLocalCommits(idx($props, 'local:commits')) ->setCommitsForLinks($commits_for_links); if ($repository) { $other_revisions = $this->loadOtherRevisions( $changesets, $target, $repository); } else { $other_revisions = array(); } $other_view = null; if ($other_revisions) { $other_view = $this->renderOtherRevisions($other_revisions); } $this->buildPackageMaps($changesets); $toc_view = $this->buildTableOfContents( $changesets, $visible_changesets, $target->loadCoverageMap($viewer)); // Attach changesets to each reviewer so we can show which Owners package // reviewers own no files. foreach ($revision->getReviewers() as $reviewer) { $reviewer_phid = $reviewer->getReviewerPHID(); $reviewer_changesets = $this->getPackageChangesets($reviewer_phid); $reviewer->attachChangesets($reviewer_changesets); } $tab_group = id(new PHUITabGroupView()) ->addTab( id(new PHUITabView()) ->setName(pht('Files')) ->setKey('files') ->appendChild($toc_view)) ->addTab( id(new PHUITabView()) ->setName(pht('History')) ->setKey('history') ->appendChild($history)) ->addTab( id(new PHUITabView()) ->setName(pht('Commits')) ->setKey('commits') ->appendChild($local_table)); $stack_graph = id(new DifferentialRevisionGraph()) ->setViewer($viewer) ->setSeedPHID($revision->getPHID()) ->setLoadEntireGraph(true) ->loadGraph(); if (!$stack_graph->isEmpty()) { $stack_table = $stack_graph->newGraphTable(); $parent_type = DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST; $reachable = $stack_graph->getReachableObjects($parent_type); foreach ($reachable as $key => $reachable_revision) { if ($reachable_revision->isClosed()) { unset($reachable[$key]); } } if ($reachable) { $stack_name = pht('Stack (%s Open)', phutil_count($reachable)); $stack_color = PHUIListItemView::STATUS_FAIL; } else { $stack_name = pht('Stack'); $stack_color = null; } $tab_group->addTab( id(new PHUITabView()) ->setName($stack_name) ->setKey('stack') ->setColor($stack_color) ->appendChild($stack_table)); } if ($other_view) { $tab_group->addTab( id(new PHUITabView()) ->setName(pht('Similar')) ->setKey('similar') ->appendChild($other_view)); } $tab_view = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Revision Contents')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->addTabGroup($tab_group); $signatures = DifferentialRequiredSignaturesField::loadForRevision( $revision); $missing_signatures = false; foreach ($signatures as $phid => $signed) { if (!$signed) { $missing_signatures = true; } } $footer = array(); $signature_message = null; if ($missing_signatures) { $signature_message = id(new PHUIInfoView()) ->setTitle(pht('Content Hidden')) ->appendChild( pht( 'The content of this revision is hidden until the author has '. 'signed all of the required legal agreements.')); } else { $anchor = id(new PhabricatorAnchorView()) ->setAnchorName('toc') ->setNavigationMarker(true); $footer[] = array( $anchor, $warning, $tab_view, $changeset_view, ); } $comment_view = id(new DifferentialRevisionEditEngine()) ->setViewer($viewer) ->buildEditEngineCommentView($revision); $comment_view->setTransactionTimeline($timeline); $review_warnings = array(); foreach ($field_list->getFields() as $field) { $review_warnings[] = $field->getWarningsForDetailView(); } $review_warnings = array_mergev($review_warnings); if ($review_warnings) { $warnings_view = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setErrors($review_warnings); $comment_view->setInfoView($warnings_view); } $footer[] = $comment_view; $monogram = $revision->getMonogram(); $operations_box = $this->buildOperationsBox($revision); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($monogram); $crumbs->setBorder(true); $filetree_on = $viewer->compareUserSetting( PhabricatorShowFiletreeSetting::SETTINGKEY, PhabricatorShowFiletreeSetting::VALUE_ENABLE_FILETREE); $nav = null; if ($filetree_on) { $collapsed_key = PhabricatorFiletreeVisibleSetting::SETTINGKEY; $collapsed_value = $viewer->getUserSetting($collapsed_key); $width_key = PhabricatorFiletreeWidthSetting::SETTINGKEY; $width_value = $viewer->getUserSetting($width_key); $nav = id(new DifferentialChangesetFileTreeSideNavBuilder()) ->setTitle($monogram) ->setBaseURI(new PhutilURI($revision->getURI())) ->setCollapsed((bool)$collapsed_value) ->setWidth((int)$width_value) ->build($changesets); } Javelin::initBehavior('differential-user-select'); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setSubheader($subheader) ->setCurtain($curtain) ->setMainColumn( array( $operations_box, $info_view, $details, $diff_detail_box, $unit_box, $timeline, $signature_message, )) ->setFooter($footer); $page = $this->newPage() ->setTitle($monogram.' '.$revision->getTitle()) ->setCrumbs($crumbs) ->setPageObjectPHIDs(array($revision->getPHID())) ->appendChild($view); if ($nav) { $page->setNavigation($nav); } return $page; } private function buildHeader(DifferentialRevision $revision) { $view = id(new PHUIHeaderView()) ->setHeader($revision->getTitle($revision)) ->setUser($this->getViewer()) ->setPolicyObject($revision) ->setHeaderIcon('fa-cog'); $status_tag = id(new PHUITagView()) ->setName($revision->getStatusDisplayName()) ->setIcon($revision->getStatusIcon()) - ->setColor($revision->getStatusIconColor()) + ->setColor($revision->getStatusTagColor()) ->setType(PHUITagView::TYPE_SHADE); $view->addProperty(PHUIHeaderView::PROPERTY_STATUS, $status_tag); + // If the revision is in a status other than "Draft", but not broadcasting, + // add an additional "Draft" tag to the header to make it clear that this + // revision hasn't promoted yet. + if (!$revision->getShouldBroadcast() && !$revision->isDraft()) { + $draft_status = DifferentialRevisionStatus::newForStatus( + DifferentialRevisionStatus::DRAFT); + + $draft_tag = id(new PHUITagView()) + ->setName($draft_status->getDisplayName()) + ->setIcon($draft_status->getIcon()) + ->setColor($draft_status->getTagColor()) + ->setType(PHUITagView::TYPE_SHADE); + + $view->addTag($draft_tag); + } + return $view; } private function buildSubheaderView(DifferentialRevision $revision) { $viewer = $this->getViewer(); $author_phid = $revision->getAuthorPHID(); $author = $viewer->renderHandle($author_phid)->render(); $date = phabricator_datetime($revision->getDateCreated(), $viewer); $author = phutil_tag('strong', array(), $author); $handles = $viewer->loadHandles(array($author_phid)); $image_uri = $handles[$author_phid]->getImageURI(); $image_href = $handles[$author_phid]->getURI(); $content = pht('Authored by %s on %s.', $author, $date); return id(new PHUIHeadThingView()) ->setImage($image_uri) ->setImageHref($image_href) ->setContent($content); } private function buildDetails( DifferentialRevision $revision, $custom_fields) { $viewer = $this->getViewer(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer); if ($custom_fields) { $custom_fields->appendFieldsToPropertyList( $revision, $viewer, $properties); } $header = id(new PHUIHeaderView()) ->setHeader(pht('Details')); return id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($properties); } private function buildCurtain(DifferentialRevision $revision) { $viewer = $this->getViewer(); $revision_id = $revision->getID(); $revision_phid = $revision->getPHID(); $curtain = $this->newCurtainView($revision); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $revision, PhabricatorPolicyCapability::CAN_EDIT); $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setHref("/differential/revision/edit/{$revision_id}/") ->setName(pht('Edit Revision')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-upload') ->setHref("/differential/revision/update/{$revision_id}/") ->setName(pht('Update Diff')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $request_uri = $this->getRequest()->getRequestURI(); $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-download') ->setName(pht('Download Raw Diff')) ->setHref($request_uri->alter('download', 'true'))); $relationship_list = PhabricatorObjectRelationshipList::newForObject( $viewer, $revision); $revision_actions = array( DifferentialRevisionHasParentRelationship::RELATIONSHIPKEY, DifferentialRevisionHasChildRelationship::RELATIONSHIPKEY, ); $revision_submenu = $relationship_list->newActionSubmenu($revision_actions) ->setName(pht('Edit Related Revisions...')) ->setIcon('fa-cog'); $curtain->addAction($revision_submenu); $relationship_submenu = $relationship_list->newActionMenu(); if ($relationship_submenu) { $curtain->addAction($relationship_submenu); } $repository = $revision->getRepository(); if ($repository && $repository->canPerformAutomation()) { $revision_id = $revision->getID(); $op = new DrydockLandRepositoryOperation(); $barrier = $op->getBarrierToLanding($viewer, $revision); if ($barrier) { $can_land = false; } else { $can_land = true; } $action = id(new PhabricatorActionView()) ->setName(pht('Land Revision')) ->setIcon('fa-fighter-jet') ->setHref("/differential/revision/operation/{$revision_id}/") ->setWorkflow(true) ->setDisabled(!$can_land); $curtain->addAction($action); } return $curtain; } private function loadHistoryDiffStatus(array $diffs) { assert_instances_of($diffs, 'DifferentialDiff'); $diff_phids = mpull($diffs, 'getPHID'); $bad_unit_status = array( ArcanistUnitTestResult::RESULT_FAIL, ArcanistUnitTestResult::RESULT_BROKEN, ); $message = new HarbormasterBuildUnitMessage(); $target = new HarbormasterBuildTarget(); $build = new HarbormasterBuild(); $buildable = new HarbormasterBuildable(); $broken_diffs = queryfx_all( $message->establishConnection('r'), 'SELECT distinct a.buildablePHID FROM %T m JOIN %T t ON m.buildTargetPHID = t.phid JOIN %T b ON t.buildPHID = b.phid JOIN %T a ON b.buildablePHID = a.phid WHERE a.buildablePHID IN (%Ls) AND m.result in (%Ls)', $message->getTableName(), $target->getTableName(), $build->getTableName(), $buildable->getTableName(), $diff_phids, $bad_unit_status); $unit_status = array(); foreach ($broken_diffs as $broken) { $phid = $broken['buildablePHID']; $unit_status[$phid] = DifferentialUnitStatus::UNIT_FAIL; } return $unit_status; } private function loadChangesetsAndVsMap( DifferentialDiff $target, DifferentialDiff $diff_vs = null, PhabricatorRepository $repository = null) { $load_diffs = array($target); if ($diff_vs) { $load_diffs[] = $diff_vs; } $raw_changesets = id(new DifferentialChangesetQuery()) ->setViewer($this->getRequest()->getUser()) ->withDiffs($load_diffs) ->execute(); $changeset_groups = mgroup($raw_changesets, 'getDiffID'); $changesets = idx($changeset_groups, $target->getID(), array()); $changesets = mpull($changesets, null, 'getID'); $refs = array(); $vs_map = array(); $vs_changesets = array(); if ($diff_vs) { $vs_id = $diff_vs->getID(); $vs_changesets_path_map = array(); foreach (idx($changeset_groups, $vs_id, array()) as $changeset) { $path = $changeset->getAbsoluteRepositoryPath($repository, $diff_vs); $vs_changesets_path_map[$path] = $changeset; $vs_changesets[$changeset->getID()] = $changeset; } foreach ($changesets as $key => $changeset) { $path = $changeset->getAbsoluteRepositoryPath($repository, $target); if (isset($vs_changesets_path_map[$path])) { $vs_map[$changeset->getID()] = $vs_changesets_path_map[$path]->getID(); $refs[$changeset->getID()] = $changeset->getID().'/'.$vs_changesets_path_map[$path]->getID(); unset($vs_changesets_path_map[$path]); } else { $refs[$changeset->getID()] = $changeset->getID(); } } foreach ($vs_changesets_path_map as $path => $changeset) { $changesets[$changeset->getID()] = $changeset; $vs_map[$changeset->getID()] = -1; $refs[$changeset->getID()] = $changeset->getID().'/-1'; } } else { foreach ($changesets as $changeset) { $refs[$changeset->getID()] = $changeset->getID(); } } $changesets = msort($changesets, 'getSortKey'); return array($changesets, $vs_map, $vs_changesets, $refs); } private function buildSymbolIndexes( PhabricatorRepository $repository, array $visible_changesets) { assert_instances_of($visible_changesets, 'DifferentialChangeset'); $engine = PhabricatorSyntaxHighlighter::newEngine(); $langs = $repository->getSymbolLanguages(); $langs = nonempty($langs, array()); $sources = $repository->getSymbolSources(); $sources = nonempty($sources, array()); $symbol_indexes = array(); if ($langs && $sources) { $have_symbols = id(new DiffusionSymbolQuery()) ->existsSymbolsInRepository($repository->getPHID()); if (!$have_symbols) { return $symbol_indexes; } } $repository_phids = array_merge( array($repository->getPHID()), $sources); $indexed_langs = array_fill_keys($langs, true); foreach ($visible_changesets as $key => $changeset) { $lang = $engine->getLanguageFromFilename($changeset->getFilename()); if (empty($indexed_langs) || isset($indexed_langs[$lang])) { $symbol_indexes[$key] = array( 'lang' => $lang, 'repositories' => $repository_phids, ); } } return $symbol_indexes; } private function loadOtherRevisions( array $changesets, DifferentialDiff $target, PhabricatorRepository $repository) { assert_instances_of($changesets, 'DifferentialChangeset'); $paths = array(); foreach ($changesets as $changeset) { $paths[] = $changeset->getAbsoluteRepositoryPath( $repository, $target); } if (!$paths) { return array(); } $path_map = id(new DiffusionPathIDQuery($paths))->loadPathIDs(); if (!$path_map) { return array(); } $recent = (PhabricatorTime::getNow() - phutil_units('30 days in seconds')); $query = id(new DifferentialRevisionQuery()) ->setViewer($this->getRequest()->getUser()) ->withIsOpen(true) ->withUpdatedEpochBetween($recent, null) ->setOrder(DifferentialRevisionQuery::ORDER_MODIFIED) ->setLimit(10) ->needFlags(true) ->needDrafts(true) ->needReviewers(true); foreach ($path_map as $path => $path_id) { $query->withPath($repository->getID(), $path_id); } $results = $query->execute(); // Strip out *this* revision. foreach ($results as $key => $result) { if ($result->getID() == $this->revisionID) { unset($results[$key]); } } return $results; } private function renderOtherRevisions(array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $viewer = $this->getViewer(); $header = id(new PHUIHeaderView()) ->setHeader(pht('Recent Similar Revisions')); $view = id(new DifferentialRevisionListView()) ->setRevisions($revisions) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setNoBox(true) ->setUser($viewer); $phids = $view->getRequiredHandlePHIDs(); $handles = $this->loadViewerHandles($phids); $view->setHandles($handles); return $view; } /** * Note this code is somewhat similar to the buildPatch method in * @{class:DifferentialReviewRequestMail}. * * @return @{class:AphrontRedirectResponse} */ private function buildRawDiffResponse( DifferentialRevision $revision, array $changesets, array $vs_changesets, array $vs_map, PhabricatorRepository $repository = null) { assert_instances_of($changesets, 'DifferentialChangeset'); assert_instances_of($vs_changesets, 'DifferentialChangeset'); $viewer = $this->getViewer(); id(new DifferentialHunkQuery()) ->setViewer($viewer) ->withChangesets($changesets) ->needAttachToChangesets(true) ->execute(); $diff = new DifferentialDiff(); $diff->attachChangesets($changesets); $raw_changes = $diff->buildChangesList(); $changes = array(); foreach ($raw_changes as $changedict) { $changes[] = ArcanistDiffChange::newFromDictionary($changedict); } $loader = id(new PhabricatorFileBundleLoader()) ->setViewer($viewer); $bundle = ArcanistBundle::newFromChanges($changes); $bundle->setLoadFileDataCallback(array($loader, 'loadFileData')); $vcs = $repository ? $repository->getVersionControlSystem() : null; switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $raw_diff = $bundle->toGitPatch(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: default: $raw_diff = $bundle->toUnifiedDiff(); break; } $request_uri = $this->getRequest()->getRequestURI(); // this ends up being something like // D123.diff // or the verbose // D123.vs123.id123.whitespaceignore-all.diff // lame but nice to include these options $file_name = ltrim($request_uri->getPath(), '/').'.'; foreach ($request_uri->getQueryParams() as $key => $value) { if ($key == 'download') { continue; } $file_name .= $key.$value.'.'; } $file_name .= 'diff'; $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $file = PhabricatorFile::newFromFileData( $raw_diff, array( 'name' => $file_name, 'ttl.relative' => phutil_units('24 hours in seconds'), 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, )); $file->attachToObject($revision->getPHID()); unset($unguarded); return $file->getRedirectResponse(); } private function buildTransactions( DifferentialRevision $revision, DifferentialDiff $left_diff, DifferentialDiff $right_diff, array $old_ids, array $new_ids) { $timeline = $this->buildTransactionTimeline( $revision, new DifferentialTransactionQuery(), $engine = null, array( 'left' => $left_diff->getID(), 'right' => $right_diff->getID(), 'old' => implode(',', $old_ids), 'new' => implode(',', $new_ids), )); return $timeline; } private function buildRevisionWarnings( DifferentialRevision $revision, PhabricatorCustomFieldList $field_list, array $warning_handle_map, array $handles) { $warnings = array(); foreach ($field_list->getFields() as $key => $field) { $phids = idx($warning_handle_map, $key, array()); $field_handles = array_select_keys($handles, $phids); $field_warnings = $field->getWarningsForRevisionHeader($field_handles); foreach ($field_warnings as $warning) { $warnings[] = $warning; } } return $warnings; } private function buildDiffDetailView( array $diffs, DifferentialRevision $revision, PhabricatorCustomFieldList $field_list) { $viewer = $this->getViewer(); $fields = array(); foreach ($field_list->getFields() as $field) { if ($field->shouldAppearInDiffPropertyView()) { $fields[] = $field; } } if (!$fields) { return null; } $property_lists = array(); foreach ($this->getDiffTabLabels($diffs) as $tab) { list($label, $diff) = $tab; $property_lists[] = array( $label, $this->buildDiffPropertyList($diff, $revision, $fields), ); } $tab_group = id(new PHUITabGroupView()) ->setHideSingleTab(true); foreach ($property_lists as $key => $property_list) { list($tab_name, $list_view) = $property_list; $tab = id(new PHUITabView()) ->setKey($key) ->setName($tab_name) ->appendChild($list_view); $tab_group->addTab($tab); $tab_group->selectTab($key); } return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Diff Detail')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setUser($viewer) ->addTabGroup($tab_group); } private function buildDiffPropertyList( DifferentialDiff $diff, DifferentialRevision $revision, array $fields) { $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($diff); foreach ($fields as $field) { $label = $field->renderDiffPropertyViewLabel($diff); $value = $field->renderDiffPropertyViewValue($diff); if ($value !== null) { $view->addProperty($label, $value); } } return $view; } private function buildOperationsBox(DifferentialRevision $revision) { $viewer = $this->getViewer(); // Save a query if we can't possibly have pending operations. $repository = $revision->getRepository(); if (!$repository || !$repository->canPerformAutomation()) { return null; } $operations = id(new DrydockRepositoryOperationQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($revision->getPHID())) ->withIsDismissed(false) ->withOperationTypes( array( DrydockLandRepositoryOperation::OPCONST, )) ->execute(); if (!$operations) { return null; } $state_fail = DrydockRepositoryOperation::STATE_FAIL; // We're going to show the oldest operation which hasn't failed, or the // most recent failure if they're all failures. $operations = msort($operations, 'getID'); foreach ($operations as $operation) { if ($operation->getOperationState() != $state_fail) { break; } } // If we found a completed operation, don't render anything. We don't want // to show an older error after the thing worked properly. if ($operation->isDone()) { return null; } $box_view = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Active Operations')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); return id(new DrydockRepositoryOperationStatusView()) ->setUser($viewer) ->setBoxView($box_view) ->setOperation($operation); } private function buildUnitMessagesView( $diff, DifferentialRevision $revision) { $viewer = $this->getViewer(); if (!$diff->getBuildable()) { return null; } if (!$diff->getUnitMessages()) { return null; } $interesting_messages = array(); foreach ($diff->getUnitMessages() as $message) { switch ($message->getResult()) { case ArcanistUnitTestResult::RESULT_PASS: case ArcanistUnitTestResult::RESULT_SKIP: break; default: $interesting_messages[] = $message; break; } } if (!$interesting_messages) { return null; } $excuse = null; if ($diff->hasDiffProperty('arc:unit-excuse')) { $excuse = $diff->getProperty('arc:unit-excuse'); } return id(new HarbormasterUnitSummaryView()) ->setUser($viewer) ->setExcuse($excuse) ->setBuildable($diff->getBuildable()) ->setUnitMessages($diff->getUnitMessages()) ->setLimit(5) ->setShowViewAll(true); } } diff --git a/src/applications/differential/customfield/DifferentialDraftField.php b/src/applications/differential/customfield/DifferentialDraftField.php index 70f7291148..f89007590f 100644 --- a/src/applications/differential/customfield/DifferentialDraftField.php +++ b/src/applications/differential/customfield/DifferentialDraftField.php @@ -1,116 +1,116 @@ getViewer(); $revision = $this->getObject(); if (!$revision->isDraft()) { return array(); } // If the author has held this revision as a draft explicitly, don't // show any misleading messages about it autosubmitting later. We do show // reminder text. if ($revision->getHoldAsDraft()) { return array( pht( 'This is a draft revision that has not yet been submitted for '. 'review.'), ); } $warnings = array(); $blocking_map = array( HarbormasterBuildStatus::STATUS_FAILED, HarbormasterBuildStatus::STATUS_ABORTED, HarbormasterBuildStatus::STATUS_ERROR, HarbormasterBuildStatus::STATUS_PAUSED, HarbormasterBuildStatus::STATUS_DEADLOCKED, ); $blocking_map = array_fuse($blocking_map); $builds = $revision->loadImpactfulBuilds($viewer); $waiting = array(); $blocking = array(); foreach ($builds as $build) { if (isset($blocking_map[$build->getBuildStatus()])) { $blocking[] = $build; } else { $waiting[] = $build; } } $blocking_list = $viewer->renderHandleList(mpull($blocking, 'getPHID')) ->setAsInline(true); $waiting_list = $viewer->renderHandleList(mpull($waiting, 'getPHID')) ->setAsInline(true); if ($blocking) { $warnings[] = pht( 'This draft revision will not be submitted for review because %s '. 'build(s) failed: %s.', phutil_count($blocking), $blocking_list); $warnings[] = pht( 'Fix build failures and update the revision.'); } else if ($waiting) { $warnings[] = pht( 'This draft revision will be sent for review once %s '. 'build(s) pass: %s.', phutil_count($waiting), $waiting_list); } else { $warnings[] = pht( 'This is a draft revision that has not yet been submitted for '. 'review.'); } return $warnings; } public function getWarningsForDetailView() { $revision = $this->getObject(); - if (!$revision->isDraft()) { + if ($revision->getShouldBroadcast()) { return array(); } return array( pht( 'This revision is currently a draft. You can leave comments, but '. 'no one will be notified until the revision is submitted for '. 'review.'), ); } } diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index 3bdbc11c7b..81ba158d20 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -1,1638 +1,1638 @@ firstBroadcast; } public function getDiffUpdateTransaction(array $xactions) { $type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE; foreach ($xactions as $xaction) { if ($xaction->getTransactionType() == $type_update) { return $xaction; } } return null; } public function setIsCloseByCommit($is_close_by_commit) { $this->isCloseByCommit = $is_close_by_commit; return $this; } public function getIsCloseByCommit() { return $this->isCloseByCommit; } public function setChangedPriorToCommitURI($uri) { $this->changedPriorToCommitURI = $uri; return $this; } public function getChangedPriorToCommitURI() { return $this->changedPriorToCommitURI; } public function setRepositoryPHIDOverride($phid_or_null) { $this->repositoryPHIDOverride = $phid_or_null; return $this; } public function getTransactionTypes() { $types = parent::getTransactionTypes(); $types[] = PhabricatorTransactions::TYPE_COMMENT; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; $types[] = PhabricatorTransactions::TYPE_INLINESTATE; $types[] = DifferentialTransaction::TYPE_INLINE; return $types; } protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case DifferentialTransaction::TYPE_INLINE: return null; } return parent::getCustomTransactionOldValue($object, $xaction); } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case DifferentialTransaction::TYPE_INLINE: return null; } return parent::getCustomTransactionNewValue($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case DifferentialTransaction::TYPE_INLINE: return; } return parent::applyCustomInternalTransaction($object, $xaction); } protected function expandTransactions( PhabricatorLiskDAO $object, array $xactions) { foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_INLINESTATE: // If we have an "Inline State" transaction already, the caller // built it for us so we don't need to expand it again. $this->didExpandInlineState = true; break; case DifferentialRevisionAcceptTransaction::TRANSACTIONTYPE: case DifferentialRevisionRejectTransaction::TRANSACTIONTYPE: case DifferentialRevisionResignTransaction::TRANSACTIONTYPE: // If we have a review transaction, we'll skip marking the user // as "Commented" later. This should get cleaner after T10967. $this->hasReviewTransaction = true; break; } } $this->wasBroadcasting = $object->getShouldBroadcast(); return parent::expandTransactions($object, $xactions); } protected function expandTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $results = parent::expandTransaction($object, $xaction); $actor = $this->getActor(); $actor_phid = $this->getActingAsPHID(); $type_edge = PhabricatorTransactions::TYPE_EDGE; $edge_ref_task = DifferentialRevisionHasTaskEdgeType::EDGECONST; $want_downgrade = array(); $must_downgrade = array(); if ($this->getIsCloseByCommit()) { // Never downgrade reviewers when we're closing a revision after a // commit. } else { switch ($xaction->getTransactionType()) { case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE: $want_downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED; $want_downgrade[] = DifferentialReviewerStatus::STATUS_REJECTED; break; case DifferentialRevisionRequestReviewTransaction::TRANSACTIONTYPE: if (!$object->isChangePlanned()) { // If the old state isn't "Changes Planned", downgrade the accepts // even if they're sticky. // We don't downgrade for "Changes Planned" to allow an author to // undo a "Plan Changes" by immediately following it up with a // "Request Review". $want_downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED; $must_downgrade[] = DifferentialReviewerStatus::STATUS_ACCEPTED; } $want_downgrade[] = DifferentialReviewerStatus::STATUS_REJECTED; break; } } if ($want_downgrade) { $void_type = DifferentialRevisionVoidTransaction::TRANSACTIONTYPE; $results[] = id(new DifferentialTransaction()) ->setTransactionType($void_type) ->setIgnoreOnNoEffect(true) ->setMetadataValue('void.force', $must_downgrade) ->setNewValue($want_downgrade); } $is_commandeer = false; switch ($xaction->getTransactionType()) { case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE: if ($this->getIsCloseByCommit()) { // Don't bother with any of this if this update is a side effect of // commit detection. break; } // When a revision is updated and the diff comes from a branch named // "T123" or similar, automatically associate the commit with the // task that the branch names. $maniphest = 'PhabricatorManiphestApplication'; if (PhabricatorApplication::isClassInstalled($maniphest)) { $diff = $this->requireDiff($xaction->getNewValue()); $branch = $diff->getBranch(); // No "$", to allow for branches like T123_demo. $match = null; if (preg_match('/^T(\d+)/i', $branch, $match)) { $task_id = $match[1]; $tasks = id(new ManiphestTaskQuery()) ->setViewer($this->getActor()) ->withIDs(array($task_id)) ->execute(); if ($tasks) { $task = head($tasks); $task_phid = $task->getPHID(); $results[] = id(new DifferentialTransaction()) ->setTransactionType($type_edge) ->setMetadataValue('edge:type', $edge_ref_task) ->setIgnoreOnNoEffect(true) ->setNewValue(array('+' => array($task_phid => $task_phid))); } } } break; case DifferentialRevisionCommandeerTransaction::TRANSACTIONTYPE: $is_commandeer = true; break; } if ($is_commandeer) { $results[] = $this->newCommandeerReviewerTransaction($object); } if (!$this->didExpandInlineState) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE: case DifferentialTransaction::TYPE_INLINE: $this->didExpandInlineState = true; $actor_phid = $this->getActingAsPHID(); $actor_is_author = ($object->getAuthorPHID() == $actor_phid); if (!$actor_is_author) { break; } $state_map = PhabricatorTransactions::getInlineStateMap(); $inlines = id(new DifferentialDiffInlineCommentQuery()) ->setViewer($this->getActor()) ->withRevisionPHIDs(array($object->getPHID())) ->withFixedStates(array_keys($state_map)) ->execute(); if (!$inlines) { break; } $old_value = mpull($inlines, 'getFixedState', 'getPHID'); $new_value = array(); foreach ($old_value as $key => $state) { $new_value[$key] = $state_map[$state]; } $results[] = id(new DifferentialTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE) ->setIgnoreOnNoEffect(true) ->setOldValue($old_value) ->setNewValue($new_value); break; } } return $results; } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case DifferentialTransaction::TYPE_INLINE: $reply = $xaction->getComment()->getReplyToComment(); if ($reply && !$reply->getHasReplies()) { $reply->setHasReplies(1)->save(); } return; } return parent::applyCustomExternalTransaction($object, $xaction); } protected function applyBuiltinExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_INLINESTATE: $table = new DifferentialTransactionComment(); $conn_w = $table->establishConnection('w'); foreach ($xaction->getNewValue() as $phid => $state) { queryfx( $conn_w, 'UPDATE %T SET fixedState = %s WHERE phid = %s', $table->getTableName(), $state, $phid); } break; } return parent::applyBuiltinExternalTransaction($object, $xaction); } protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { // Load the most up-to-date version of the revision and its reviewers, // so we don't need to try to deduce the state of reviewers by examining // all the changes made by the transactions. Then, update the reviewers // on the object to make sure we're acting on the current reviewer set // (and, for example, sending mail to the right people). $new_revision = id(new DifferentialRevisionQuery()) ->setViewer($this->getActor()) ->needReviewers(true) ->needActiveDiffs(true) ->withIDs(array($object->getID())) ->executeOne(); if (!$new_revision) { throw new Exception( pht('Failed to load revision from transaction finalization.')); } $object->attachReviewers($new_revision->getReviewers()); $object->attachActiveDiff($new_revision->getActiveDiff()); $object->attachRepository($new_revision->getRepository()); foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE: $diff = $this->requireDiff($xaction->getNewValue(), true); // Update these denormalized index tables when we attach a new // diff to a revision. $this->updateRevisionHashTable($object, $diff); $this->updateAffectedPathTable($object, $diff); break; } } $xactions = $this->updateReviewStatus($object, $xactions); $this->markReviewerComments($object, $xactions); return $xactions; } private function updateReviewStatus( DifferentialRevision $revision, array $xactions) { $was_accepted = $revision->isAccepted(); $was_revision = $revision->isNeedsRevision(); $was_review = $revision->isNeedsReview(); if (!$was_accepted && !$was_revision && !$was_review) { // Revisions can't transition out of other statuses (like closed or // abandoned) as a side effect of reviewer status changes. return $xactions; } // Try to move a revision to "accepted". We look for: // // - at least one accepting reviewer who is a user; and // - no rejects; and // - no rejects of older diffs; and // - no blocking reviewers. $has_accepting_user = false; $has_rejecting_reviewer = false; $has_rejecting_older_reviewer = false; $has_blocking_reviewer = false; $active_diff = $revision->getActiveDiff(); foreach ($revision->getReviewers() as $reviewer) { $reviewer_status = $reviewer->getReviewerStatus(); switch ($reviewer_status) { case DifferentialReviewerStatus::STATUS_REJECTED: $active_phid = $active_diff->getPHID(); if ($reviewer->isRejected($active_phid)) { $has_rejecting_reviewer = true; } else { $has_rejecting_older_reviewer = true; } break; case DifferentialReviewerStatus::STATUS_REJECTED_OLDER: $has_rejecting_older_reviewer = true; break; case DifferentialReviewerStatus::STATUS_BLOCKING: $has_blocking_reviewer = true; break; case DifferentialReviewerStatus::STATUS_ACCEPTED: if ($reviewer->isUser()) { $active_phid = $active_diff->getPHID(); if ($reviewer->isAccepted($active_phid)) { $has_accepting_user = true; } } break; } } $new_status = null; if ($has_accepting_user && !$has_rejecting_reviewer && !$has_rejecting_older_reviewer && !$has_blocking_reviewer) { $new_status = DifferentialRevisionStatus::ACCEPTED; } else if ($has_rejecting_reviewer) { // This isn't accepted, and there's at least one rejecting reviewer, // so the revision needs changes. This usually happens after a // "reject". $new_status = DifferentialRevisionStatus::NEEDS_REVISION; } else if ($was_accepted) { // This revision was accepted, but it no longer satisfies the // conditions for acceptance. This usually happens after an accepting // reviewer resigns or is removed. $new_status = DifferentialRevisionStatus::NEEDS_REVIEW; } if ($new_status === null) { return $xactions; } $old_status = $revision->getModernRevisionStatus(); if ($new_status == $old_status) { return $xactions; } $xaction = id(new DifferentialTransaction()) ->setTransactionType( DifferentialRevisionStatusTransaction::TRANSACTIONTYPE) ->setOldValue($old_status) ->setNewValue($new_status); $xaction = $this->populateTransaction($revision, $xaction) ->save(); $xactions[] = $xaction; // Save the status adjustment we made earlier. $revision ->setModernRevisionStatus($new_status) ->save(); return $xactions; } protected function sortTransactions(array $xactions) { $xactions = parent::sortTransactions($xactions); $head = array(); $tail = array(); foreach ($xactions as $xaction) { $type = $xaction->getTransactionType(); if ($type == DifferentialTransaction::TYPE_INLINE) { $tail[] = $xaction; } else { $head[] = $xaction; } } return array_values(array_merge($head, $tail)); } protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { - if (!$object->shouldBroadcast()) { + if (!$object->getShouldBroadcast()) { return false; } return true; } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { - if (!$object->shouldBroadcast()) { + if (!$object->getShouldBroadcast()) { return false; } return true; } protected function getMailTo(PhabricatorLiskDAO $object) { $this->requireReviewers($object); $phids = array(); $phids[] = $object->getAuthorPHID(); foreach ($object->getReviewers() as $reviewer) { if ($reviewer->isResigned()) { continue; } $phids[] = $reviewer->getReviewerPHID(); } return $phids; } protected function newMailUnexpandablePHIDs(PhabricatorLiskDAO $object) { $this->requireReviewers($object); $phids = array(); foreach ($object->getReviewers() as $reviewer) { if ($reviewer->isResigned()) { $phids[] = $reviewer->getReviewerPHID(); } } return $phids; } protected function getMailAction( PhabricatorLiskDAO $object, array $xactions) { $show_lines = false; if ($this->isFirstBroadcast()) { $action = pht('Request'); $show_lines = true; } else { $action = parent::getMailAction($object, $xactions); $strongest = $this->getStrongestAction($object, $xactions); $type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE; if ($strongest->getTransactionType() == $type_update) { $show_lines = true; } } if ($show_lines) { $count = new PhutilNumber($object->getLineCount()); $action = pht('%s, %s line(s)', $action, $count); } return $action; } protected function getMailSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.differential.subject-prefix'); } protected function getMailThreadID(PhabricatorLiskDAO $object) { // This is nonstandard, but retains threading with older messages. $phid = $object->getPHID(); return "differential-rev-{$phid}-req"; } protected function buildReplyHandler(PhabricatorLiskDAO $object) { return id(new DifferentialReplyHandler()) ->setMailReceiver($object); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $id = $object->getID(); $title = $object->getTitle(); $subject = "D{$id}: {$title}"; return id(new PhabricatorMetaMTAMail()) ->setSubject($subject); } protected function getTransactionsForMail( PhabricatorLiskDAO $object, array $xactions) { // If this is the first time we're sending mail about this revision, we // generate mail for all prior transactions, not just whatever is being // applied now. This gets the "added reviewers" lines and other relevant // information into the mail. if ($this->isFirstBroadcast()) { return $this->loadUnbroadcastTransactions($object); } return $xactions; } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $viewer = $this->requireActor(); $body = new PhabricatorMetaMTAMailBody(); $body->setViewer($this->requireActor()); $revision_uri = PhabricatorEnv::getProductionURI('/D'.$object->getID()); $this->addHeadersAndCommentsToMailBody( $body, $xactions, pht('View Revision'), $revision_uri); $type_inline = DifferentialTransaction::TYPE_INLINE; $inlines = array(); foreach ($xactions as $xaction) { if ($xaction->getTransactionType() == $type_inline) { $inlines[] = $xaction; } } if ($inlines) { $this->appendInlineCommentsForMail($object, $inlines, $body); } $changed_uri = $this->getChangedPriorToCommitURI(); if ($changed_uri) { $body->addLinkSection( pht('CHANGED PRIOR TO COMMIT'), $changed_uri); } $this->addCustomFieldsToMailBody($body, $object, $xactions); $body->addLinkSection( pht('REVISION DETAIL'), $revision_uri); $update_xaction = null; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE: $update_xaction = $xaction; break; } } if ($update_xaction) { $diff = $this->requireDiff($update_xaction->getNewValue(), true); $body->addTextSection( pht('AFFECTED FILES'), $this->renderAffectedFilesForMail($diff)); $config_key_inline = 'metamta.differential.inline-patches'; $config_inline = PhabricatorEnv::getEnvConfig($config_key_inline); $config_key_attach = 'metamta.differential.attach-patches'; $config_attach = PhabricatorEnv::getEnvConfig($config_key_attach); if ($config_inline || $config_attach) { $body_limit = PhabricatorEnv::getEnvConfig('metamta.email-body-limit'); $patch = $this->buildPatchForMail($diff); if ($config_inline) { $lines = substr_count($patch, "\n"); $bytes = strlen($patch); // Limit the patch size to the smaller of 256 bytes per line or // the mail body limit. This prevents degenerate behavior for patches // with one line that is 10MB long. See T11748. $byte_limits = array(); $byte_limits[] = (256 * $config_inline); $byte_limits[] = $body_limit; $byte_limit = min($byte_limits); $lines_ok = ($lines <= $config_inline); $bytes_ok = ($bytes <= $byte_limit); if ($lines_ok && $bytes_ok) { $this->appendChangeDetailsForMail($object, $diff, $patch, $body); } else { // TODO: Provide a helpful message about the patch being too // large or lengthy here. } } if ($config_attach) { // See T12033, T11767, and PHI55. This is a crude fix to stop the // major concrete problems that lackluster email size limits cause. if (strlen($patch) < $body_limit) { $name = pht('D%s.%s.patch', $object->getID(), $diff->getID()); $mime_type = 'text/x-patch; charset=utf-8'; $body->addAttachment( new PhabricatorMetaMTAAttachment($patch, $name, $mime_type)); } } } } return $body; } public function getMailTagsMap() { return array( DifferentialTransaction::MAILTAG_REVIEW_REQUEST => pht('A revision is created.'), DifferentialTransaction::MAILTAG_UPDATED => pht('A revision is updated.'), DifferentialTransaction::MAILTAG_COMMENT => pht('Someone comments on a revision.'), DifferentialTransaction::MAILTAG_CLOSED => pht('A revision is closed.'), DifferentialTransaction::MAILTAG_REVIEWERS => pht("A revision's reviewers change."), DifferentialTransaction::MAILTAG_CC => pht("A revision's CCs change."), DifferentialTransaction::MAILTAG_OTHER => pht('Other revision activity not listed above occurs.'), ); } protected function supportsSearch() { return true; } protected function expandCustomRemarkupBlockTransactions( PhabricatorLiskDAO $object, array $xactions, array $changes, PhutilMarkupEngine $engine) { // For "Fixes ..." and "Depends on ...", we're only going to look at // content blocks which are part of the revision itself (like "Summary" // and "Test Plan"), not comments. $content_parts = array(); foreach ($changes as $change) { if ($change->getTransaction()->isCommentTransaction()) { continue; } $content_parts[] = $change->getNewValue(); } if (!$content_parts) { return array(); } $content_block = implode("\n\n", $content_parts); $task_map = array(); $task_refs = id(new ManiphestCustomFieldStatusParser()) ->parseCorpus($content_block); foreach ($task_refs as $match) { foreach ($match['monograms'] as $monogram) { $task_id = (int)trim($monogram, 'tT'); $task_map[$task_id] = true; } } $rev_map = array(); $rev_refs = id(new DifferentialCustomFieldDependsOnParser()) ->parseCorpus($content_block); foreach ($rev_refs as $match) { foreach ($match['monograms'] as $monogram) { $rev_id = (int)trim($monogram, 'dD'); $rev_map[$rev_id] = true; } } $edges = array(); $task_phids = array(); $rev_phids = array(); if ($task_map) { $tasks = id(new ManiphestTaskQuery()) ->setViewer($this->getActor()) ->withIDs(array_keys($task_map)) ->execute(); if ($tasks) { $task_phids = mpull($tasks, 'getPHID', 'getPHID'); $edge_related = DifferentialRevisionHasTaskEdgeType::EDGECONST; $edges[$edge_related] = $task_phids; } } if ($rev_map) { $revs = id(new DifferentialRevisionQuery()) ->setViewer($this->getActor()) ->withIDs(array_keys($rev_map)) ->execute(); $rev_phids = mpull($revs, 'getPHID', 'getPHID'); // NOTE: Skip any write attempts if a user cleverly implies a revision // depends upon itself. unset($rev_phids[$object->getPHID()]); if ($revs) { $depends = DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST; $edges[$depends] = $rev_phids; } } $revert_refs = id(new DifferentialCustomFieldRevertsParser()) ->parseCorpus($content_block); $revert_monograms = array(); foreach ($revert_refs as $match) { foreach ($match['monograms'] as $monogram) { $revert_monograms[] = $monogram; } } if ($revert_monograms) { $revert_objects = id(new PhabricatorObjectQuery()) ->setViewer($this->getActor()) ->withNames($revert_monograms) ->withTypes( array( DifferentialRevisionPHIDType::TYPECONST, PhabricatorRepositoryCommitPHIDType::TYPECONST, )) ->execute(); $revert_phids = mpull($revert_objects, 'getPHID', 'getPHID'); // Don't let an object revert itself, although other silly stuff like // cycles of objects reverting each other is not prevented. unset($revert_phids[$object->getPHID()]); $revert_type = DiffusionCommitRevertsCommitEdgeType::EDGECONST; $edges[$revert_type] = $revert_phids; } else { $revert_phids = array(); } $this->setUnmentionablePHIDMap( array_merge( $task_phids, $rev_phids, $revert_phids)); $result = array(); foreach ($edges as $type => $specs) { $result[] = id(new DifferentialTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $type) ->setNewValue(array('+' => $specs)); } return $result; } private function appendInlineCommentsForMail( PhabricatorLiskDAO $object, array $inlines, PhabricatorMetaMTAMailBody $body) { $section = id(new DifferentialInlineCommentMailView()) ->setViewer($this->getActor()) ->setInlines($inlines) ->buildMailSection(); $header = pht('INLINE COMMENTS'); $section_text = "\n".$section->getPlaintext(); $style = array( 'margin: 6px 0 12px 0;', ); $section_html = phutil_tag( 'div', array( 'style' => implode(' ', $style), ), $section->getHTML()); $body->addPlaintextSection($header, $section_text, false); $body->addHTMLSection($header, $section_html); } private function appendChangeDetailsForMail( PhabricatorLiskDAO $object, DifferentialDiff $diff, $patch, PhabricatorMetaMTAMailBody $body) { $section = id(new DifferentialChangeDetailMailView()) ->setViewer($this->getActor()) ->setDiff($diff) ->setPatch($patch) ->buildMailSection(); $header = pht('CHANGE DETAILS'); $section_text = "\n".$section->getPlaintext(); $style = array( 'margin: 6px 0 12px 0;', ); $section_html = phutil_tag( 'div', array( 'style' => implode(' ', $style), ), $section->getHTML()); $body->addPlaintextSection($header, $section_text, false); $body->addHTMLSection($header, $section_html); } private function loadDiff($phid, $need_changesets = false) { $query = id(new DifferentialDiffQuery()) ->withPHIDs(array($phid)) ->setViewer($this->getActor()); if ($need_changesets) { $query->needChangesets(true); } return $query->executeOne(); } public function requireDiff($phid, $need_changesets = false) { $diff = $this->loadDiff($phid, $need_changesets); if (!$diff) { throw new Exception(pht('Diff "%s" does not exist!', $phid)); } return $diff; } /* -( Herald Integration )------------------------------------------------- */ protected function shouldApplyHeraldRules( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function didApplyHeraldRules( PhabricatorLiskDAO $object, HeraldAdapter $adapter, HeraldTranscript $transcript) { $repository = $object->getRepository(); if (!$repository) { return array(); } if (!$this->affectedPaths) { return array(); } $packages = PhabricatorOwnersPackage::loadAffectedPackages( $repository, $this->affectedPaths); if (!$packages) { return array(); } // Identify the packages with "Non-Owner Author" review rules and remove // them if the author has authority over the package. $autoreview_map = PhabricatorOwnersPackage::getAutoreviewOptionsMap(); $need_authority = array(); foreach ($packages as $package) { $autoreview_setting = $package->getAutoReview(); $spec = idx($autoreview_map, $autoreview_setting); if (!$spec) { continue; } if (idx($spec, 'authority')) { $need_authority[$package->getPHID()] = $package->getPHID(); } } if ($need_authority) { $authority = id(new PhabricatorOwnersPackageQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs($need_authority) ->withAuthorityPHIDs(array($object->getAuthorPHID())) ->execute(); $authority = mpull($authority, null, 'getPHID'); foreach ($packages as $key => $package) { $package_phid = $package->getPHID(); if (isset($authority[$package_phid])) { unset($packages[$key]); continue; } } if (!$packages) { return array(); } } $auto_subscribe = array(); $auto_review = array(); $auto_block = array(); foreach ($packages as $package) { switch ($package->getAutoReview()) { case PhabricatorOwnersPackage::AUTOREVIEW_REVIEW: case PhabricatorOwnersPackage::AUTOREVIEW_REVIEW_ALWAYS: $auto_review[] = $package; break; case PhabricatorOwnersPackage::AUTOREVIEW_BLOCK: case PhabricatorOwnersPackage::AUTOREVIEW_BLOCK_ALWAYS: $auto_block[] = $package; break; case PhabricatorOwnersPackage::AUTOREVIEW_SUBSCRIBE: case PhabricatorOwnersPackage::AUTOREVIEW_SUBSCRIBE_ALWAYS: $auto_subscribe[] = $package; break; case PhabricatorOwnersPackage::AUTOREVIEW_NONE: default: break; } } $owners_phid = id(new PhabricatorOwnersApplication()) ->getPHID(); $xactions = array(); if ($auto_subscribe) { $xactions[] = $object->getApplicationTransactionTemplate() ->setAuthorPHID($owners_phid) ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) ->setNewValue( array( '+' => mpull($auto_subscribe, 'getPHID'), )); } $specs = array( array($auto_review, false), array($auto_block, true), ); foreach ($specs as $spec) { list($reviewers, $blocking) = $spec; if (!$reviewers) { continue; } $phids = mpull($reviewers, 'getPHID'); $xaction = $this->newAutoReviewTransaction($object, $phids, $blocking); if ($xaction) { $xactions[] = $xaction; } } return $xactions; } private function newAutoReviewTransaction( PhabricatorLiskDAO $object, array $phids, $is_blocking) { // TODO: This is substantially similar to DifferentialReviewersHeraldAction // and both are needlessly complex. This logic should live in the normal // transaction application pipeline. See T10967. $reviewers = $object->getReviewers(); $reviewers = mpull($reviewers, null, 'getReviewerPHID'); if ($is_blocking) { $new_status = DifferentialReviewerStatus::STATUS_BLOCKING; } else { $new_status = DifferentialReviewerStatus::STATUS_ADDED; } $new_strength = DifferentialReviewerStatus::getStatusStrength( $new_status); $current = array(); foreach ($phids as $phid) { if (!isset($reviewers[$phid])) { continue; } // If we're applying a stronger status (usually, upgrading a reviewer // into a blocking reviewer), skip this check so we apply the change. $old_strength = DifferentialReviewerStatus::getStatusStrength( $reviewers[$phid]->getReviewerStatus()); if ($old_strength <= $new_strength) { continue; } $current[] = $phid; } $phids = array_diff($phids, $current); if (!$phids) { return null; } $phids = array_fuse($phids); $value = array(); foreach ($phids as $phid) { if ($is_blocking) { $value[] = 'blocking('.$phid.')'; } else { $value[] = $phid; } } $owners_phid = id(new PhabricatorOwnersApplication()) ->getPHID(); $reviewers_type = DifferentialRevisionReviewersTransaction::TRANSACTIONTYPE; return $object->getApplicationTransactionTemplate() ->setAuthorPHID($owners_phid) ->setTransactionType($reviewers_type) ->setNewValue( array( '+' => $value, )); } protected function buildHeraldAdapter( PhabricatorLiskDAO $object, array $xactions) { $revision = id(new DifferentialRevisionQuery()) ->setViewer($this->getActor()) ->withPHIDs(array($object->getPHID())) ->needActiveDiffs(true) ->needReviewers(true) ->executeOne(); if (!$revision) { throw new Exception( pht('Failed to load revision for Herald adapter construction!')); } $adapter = HeraldDifferentialRevisionAdapter::newLegacyAdapter( $revision, $revision->getActiveDiff()); // If the object is still a draft, prevent "Send me an email" and other // similar rules from acting yet. - if (!$object->shouldBroadcast()) { + if (!$object->getShouldBroadcast()) { $adapter->setForbiddenAction( HeraldMailableState::STATECONST, DifferentialHeraldStateReasons::REASON_DRAFT); } // If this edit didn't actually change the diff (for example, a user // edited the title or changed subscribers), prevent "Run build plan" // and other similar rules from acting yet, since the build results will // not (or, at least, should not) change unless the actual source changes. // We also don't run Differential builds if the update was caused by // discovering a commit, as the expectation is that Diffusion builds take // over once things land. $has_update = false; $has_commit = false; $type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE; foreach ($xactions as $xaction) { if ($xaction->getTransactionType() != $type_update) { continue; } if ($xaction->getMetadataValue('isCommitUpdate')) { $has_commit = true; } else { $has_update = true; } break; } if ($has_commit) { $adapter->setForbiddenAction( HeraldBuildableState::STATECONST, DifferentialHeraldStateReasons::REASON_LANDED); } else if (!$has_update) { $adapter->setForbiddenAction( HeraldBuildableState::STATECONST, DifferentialHeraldStateReasons::REASON_UNCHANGED); } return $adapter; } /** * Update the table which links Differential revisions to paths they affect, * so Diffusion can efficiently find pending revisions for a given file. */ private function updateAffectedPathTable( DifferentialRevision $revision, DifferentialDiff $diff) { $repository = $revision->getRepository(); if (!$repository) { // The repository where the code lives is untracked. return; } $path_prefix = null; $local_root = $diff->getSourceControlPath(); if ($local_root) { // We're in a working copy which supports subdirectory checkouts (e.g., // SVN) so we need to figure out what prefix we should add to each path // (e.g., trunk/projects/example/) to get the absolute path from the // root of the repository. DVCS systems like Git and Mercurial are not // affected. // Normalize both paths and check if the repository root is a prefix of // the local root. If so, throw it away. Note that this correctly handles // the case where the remote path is "/". $local_root = id(new PhutilURI($local_root))->getPath(); $local_root = rtrim($local_root, '/'); $repo_root = id(new PhutilURI($repository->getRemoteURI()))->getPath(); $repo_root = rtrim($repo_root, '/'); if (!strncmp($repo_root, $local_root, strlen($repo_root))) { $path_prefix = substr($local_root, strlen($repo_root)); } } $changesets = $diff->getChangesets(); $paths = array(); foreach ($changesets as $changeset) { $paths[] = $path_prefix.'/'.$changeset->getFilename(); } // Save the affected paths; we'll use them later to query Owners. This // uses the un-expanded paths. $this->affectedPaths = $paths; // Mark this as also touching all parent paths, so you can see all pending // changes to any file within a directory. $all_paths = array(); foreach ($paths as $local) { foreach (DiffusionPathIDQuery::expandPathToRoot($local) as $path) { $all_paths[$path] = true; } } $all_paths = array_keys($all_paths); $path_ids = PhabricatorRepositoryCommitChangeParserWorker::lookupOrCreatePaths( $all_paths); $table = new DifferentialAffectedPath(); $conn_w = $table->establishConnection('w'); $sql = array(); foreach ($path_ids as $path_id) { $sql[] = qsprintf( $conn_w, '(%d, %d, %d, %d)', $repository->getID(), $path_id, time(), $revision->getID()); } queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d', $table->getTableName(), $revision->getID()); foreach (array_chunk($sql, 256) as $chunk) { queryfx( $conn_w, 'INSERT INTO %T (repositoryID, pathID, epoch, revisionID) VALUES %Q', $table->getTableName(), implode(', ', $chunk)); } } /** * 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 renderAffectedFilesForMail(DifferentialDiff $diff) { $changesets = $diff->getChangesets(); $filenames = mpull($changesets, 'getDisplayFilename'); sort($filenames); $count = count($filenames); $max = 250; if ($count > $max) { $filenames = array_slice($filenames, 0, $max); $filenames[] = pht('(%d more files...)', ($count - $max)); } return implode("\n", $filenames); } private function renderPatchHTMLForMail($patch) { return phutil_tag('pre', array('style' => 'font-family: monospace;'), $patch); } private function buildPatchForMail(DifferentialDiff $diff) { $format = PhabricatorEnv::getEnvConfig('metamta.differential.patch-format'); return id(new DifferentialRawDiffRenderer()) ->setViewer($this->getActor()) ->setFormat($format) ->setChangesets($diff->getChangesets()) ->buildPatch(); } protected function willPublish(PhabricatorLiskDAO $object, array $xactions) { // Reload to pick up the active diff and reviewer status. return id(new DifferentialRevisionQuery()) ->setViewer($this->getActor()) ->needReviewers(true) ->needActiveDiffs(true) ->withIDs(array($object->getID())) ->executeOne(); } protected function getCustomWorkerState() { return array( 'changedPriorToCommitURI' => $this->changedPriorToCommitURI, 'firstBroadcast' => $this->firstBroadcast, ); } protected function loadCustomWorkerState(array $state) { $this->changedPriorToCommitURI = idx($state, 'changedPriorToCommitURI'); $this->firstBroadcast = idx($state, 'firstBroadcast'); return $this; } private function newCommandeerReviewerTransaction( DifferentialRevision $revision) { $actor_phid = $this->getActingAsPHID(); $owner_phid = $revision->getAuthorPHID(); // If the user is commandeering, add the previous owner as a // reviewer and remove the actor. $edits = array( '-' => array( $actor_phid, ), '+' => array( $owner_phid, ), ); // NOTE: We're setting setIsCommandeerSideEffect() on this because normally // you can't add a revision's author as a reviewer, but this action swaps // them after validation executes. $xaction_type = DifferentialRevisionReviewersTransaction::TRANSACTIONTYPE; return id(new DifferentialTransaction()) ->setTransactionType($xaction_type) ->setIgnoreOnNoEffect(true) ->setIsCommandeerSideEffect(true) ->setNewValue($edits); } public function getActiveDiff($object) { if ($this->getIsNewObject()) { return null; } else { return $object->getActiveDiff(); } } /** * When a reviewer makes a comment, mark the last revision they commented * on. * * This allows us to show a hint to help authors and other reviewers quickly * distinguish between reviewers who have participated in the discussion and * reviewers who haven't been part of it. */ private function markReviewerComments($object, array $xactions) { $acting_phid = $this->getActingAsPHID(); if (!$acting_phid) { return; } $diff = $this->getActiveDiff($object); if (!$diff) { return; } $has_comment = false; foreach ($xactions as $xaction) { if ($xaction->hasComment()) { $has_comment = true; break; } } if (!$has_comment) { return; } $reviewer_table = new DifferentialReviewer(); $conn = $reviewer_table->establishConnection('w'); queryfx( $conn, 'UPDATE %T SET lastCommentDiffPHID = %s WHERE revisionPHID = %s AND reviewerPHID = %s', $reviewer_table->getTableName(), $diff->getPHID(), $object->getPHID(), $acting_phid); } private function loadUnbroadcastTransactions($object) { $viewer = $this->requireActor(); $xactions = id(new DifferentialTransactionQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($object->getPHID())) ->execute(); return array_reverse($xactions); } protected function didApplyTransactions($object, array $xactions) { // In a moment, we're going to try to publish draft revisions which have // completed all their builds. However, we only want to do that if the // actor is either the revision author or an omnipotent user (generally, // the Harbormaster application). // If we let any actor publish the revision as a side effect of other // changes then an unlucky third party who innocently comments on the draft // can end up racing Harbormaster and promoting the revision. At best, this // is confusing. It can also run into validation problems with the "Request // Review" transaction. See PHI309 for some discussion. $author_phid = $object->getAuthorPHID(); $viewer = $this->requireActor(); $can_undraft = ($this->getActingAsPHID() === $author_phid) || ($viewer->isOmnipotent()); // If a draft revision has no outstanding builds and we're automatically // making drafts public after builds finish, make the revision public. if ($can_undraft) { $auto_undraft = !$object->getHoldAsDraft(); } else { $auto_undraft = false; } if ($object->isDraft() && $auto_undraft) { $status = $this->loadCompletedBuildableStatus($object); $is_passed = ($status === HarbormasterBuildableStatus::STATUS_PASSED); $is_failed = ($status === HarbormasterBuildableStatus::STATUS_FAILED); if ($is_passed) { // When Harbormaster moves a revision out of the draft state, we // attribute the action to the revision author since this is more // natural and more useful. // Additionally, we change the acting PHID for the transaction set // to the author if it isn't already a user so that mail comes from // the natural author. $acting_phid = $this->getActingAsPHID(); $user_type = PhabricatorPeopleUserPHIDType::TYPECONST; if (phid_get_type($acting_phid) != $user_type) { $this->setActingAsPHID($author_phid); } $xaction = $object->getApplicationTransactionTemplate() ->setAuthorPHID($author_phid) ->setTransactionType( DifferentialRevisionRequestReviewTransaction::TRANSACTIONTYPE) ->setNewValue(true); // If we're creating this revision and immediately moving it out of // the draft state, mark this as a create transaction so it gets // hidden in the timeline and mail, since it isn't interesting: it // is as though the draft phase never happened. if ($this->getIsNewObject()) { $xaction->setIsCreateTransaction(true); } // Queue this transaction and apply it separately after the current // batch of transactions finishes so that Herald can fire on the new // revision state. See T13027 for discussion. $this->queueTransaction($xaction); } else if ($is_failed) { // TODO: Change to "Changes Planned + Draft", notify the author (only) // of the build failure. } } // If the revision is new or was a draft, and is no longer a draft, we // might be sending the first email about it. // This might mean it was created directly into a non-draft state, or // it just automatically undrafted after builds finished, or a user // explicitly promoted it out of the draft state with an action like // "Request Review". // If we haven't sent any email about it yet, mark this email as the first // email so the mail gets enriched with "SUMMARY" and "TEST PLAN". $is_new = $this->getIsNewObject(); $was_broadcasting = $this->wasBroadcasting; if ($object->getShouldBroadcast()) { if (!$was_broadcasting || $is_new) { // Mark this as the first broadcast we're sending about the revision // so mail can generate specially. $this->firstBroadcast = true; } } return $xactions; } private function loadCompletedBuildableStatus( DifferentialRevision $revision) { $viewer = $this->requireActor(); $builds = $revision->loadImpactfulBuilds($viewer); return $revision->newBuildableStatusForBuilds($builds); } private function requireReviewers(DifferentialRevision $revision) { if ($revision->hasAttachedReviewers()) { return; } $with_reviewers = id(new DifferentialRevisionQuery()) ->setViewer($this->getActor()) ->needReviewers(true) ->withPHIDs(array($revision->getPHID())) ->executeOne(); if (!$with_reviewers) { throw new Exception( pht( 'Failed to reload revision ("%s").', $revision->getPHID())); } $revision->attachReviewers($with_reviewers->getReviewers()); } } diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php index 434b3222fa..7b67dc27ef 100644 --- a/src/applications/differential/storage/DifferentialRevision.php +++ b/src/applications/differential/storage/DifferentialRevision.php @@ -1,1143 +1,1138 @@ setViewer($actor) ->withClasses(array('PhabricatorDifferentialApplication')) ->executeOne(); $view_policy = $app->getPolicy( DifferentialDefaultViewCapability::CAPABILITY); if (PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) { $initial_state = DifferentialRevisionStatus::DRAFT; $should_broadcast = false; } else { $initial_state = DifferentialRevisionStatus::NEEDS_REVIEW; $should_broadcast = true; } return id(new DifferentialRevision()) ->setViewPolicy($view_policy) ->setAuthorPHID($actor->getPHID()) ->attachRepository(null) ->attachActiveDiff(null) ->attachReviewers(array()) ->setModernRevisionStatus($initial_state) ->setShouldBroadcast($should_broadcast); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'attached' => self::SERIALIZATION_JSON, 'unsubscribed' => self::SERIALIZATION_JSON, 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255', 'status' => 'text32', 'summary' => 'text', 'testPlan' => 'text', 'authorPHID' => 'phid?', 'lastReviewerPHID' => 'phid?', 'lineCount' => 'uint32?', 'mailKey' => 'bytes40', 'branchName' => 'text255?', 'repositoryPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'authorPHID' => array( 'columns' => array('authorPHID', 'status'), ), 'repositoryPHID' => array( 'columns' => array('repositoryPHID'), ), // If you (or a project you are a member of) is reviewing a significant // fraction of the revisions on an install, the result set of open // revisions may be smaller than the result set of revisions where you // are a reviewer. In these cases, this key is better than keys on the // edge table. 'key_status' => array( 'columns' => array('status', 'phid'), ), ), ) + parent::getConfiguration(); } public function setProperty($key, $value) { $this->properties[$key] = $value; return $this; } public function getProperty($key, $default = null) { return idx($this->properties, $key, $default); } public function hasRevisionProperty($key) { return array_key_exists($key, $this->properties); } public function getMonogram() { $id = $this->getID(); return "D{$id}"; } public function getURI() { return '/'.$this->getMonogram(); } public function loadIDsByCommitPHIDs($phids) { if (!$phids) { return array(); } $revision_ids = queryfx_all( $this->establishConnection('r'), 'SELECT * FROM %T WHERE commitPHID IN (%Ls)', self::TABLE_COMMIT, $phids); return ipull($revision_ids, 'revisionID', 'commitPHID'); } public function loadCommitPHIDs() { if (!$this->getID()) { return ($this->commits = array()); } $commits = queryfx_all( $this->establishConnection('r'), 'SELECT commitPHID FROM %T WHERE revisionID = %d', self::TABLE_COMMIT, $this->getID()); $commits = ipull($commits, 'commitPHID'); return ($this->commits = $commits); } public function getCommitPHIDs() { return $this->assertAttached($this->commits); } public function getActiveDiff() { // TODO: Because it's currently technically possible to create a revision // without an associated diff, we allow an attached-but-null active diff. // It would be good to get rid of this once we make diff-attaching // transactional. return $this->assertAttached($this->activeDiff); } public function attachActiveDiff($diff) { $this->activeDiff = $diff; return $this; } public function getDiffIDs() { return $this->assertAttached($this->diffIDs); } public function attachDiffIDs(array $ids) { rsort($ids); $this->diffIDs = array_values($ids); return $this; } public function attachCommitPHIDs(array $phids) { $this->commits = array_values($phids); return $this; } public function getAttachedPHIDs($type) { return array_keys(idx($this->attached, $type, array())); } public function setAttachedPHIDs($type, array $phids) { $this->attached[$type] = array_fill_keys($phids, array()); return $this; } public function generatePHID() { return PhabricatorPHID::generateNewPHID( DifferentialRevisionPHIDType::TYPECONST); } public function loadActiveDiff() { return id(new DifferentialDiff())->loadOneWhere( 'revisionID = %d ORDER BY id DESC LIMIT 1', $this->getID()); } public function save() { if (!$this->getMailKey()) { $this->mailKey = Filesystem::readRandomCharacters(40); } return parent::save(); } public function getHashes() { return $this->assertAttached($this->hashes); } public function attachHashes(array $hashes) { $this->hashes = $hashes; return $this; } public function canReviewerForceAccept( PhabricatorUser $viewer, DifferentialReviewer $reviewer) { if (!$reviewer->isPackage()) { return false; } $map = $this->getReviewerForceAcceptMap($viewer); if (!$map) { return false; } if (isset($map[$reviewer->getReviewerPHID()])) { return true; } return false; } private function getReviewerForceAcceptMap(PhabricatorUser $viewer) { $fragment = $viewer->getCacheFragment(); if (!array_key_exists($fragment, $this->forceMap)) { $map = $this->newReviewerForceAcceptMap($viewer); $this->forceMap[$fragment] = $map; } return $this->forceMap[$fragment]; } private function newReviewerForceAcceptMap(PhabricatorUser $viewer) { $diff = $this->getActiveDiff(); if (!$diff) { return null; } $repository_phid = $diff->getRepositoryPHID(); if (!$repository_phid) { return null; } $paths = array(); try { $changesets = $diff->getChangesets(); } catch (Exception $ex) { $changesets = id(new DifferentialChangesetQuery()) ->setViewer($viewer) ->withDiffs(array($diff)) ->execute(); } foreach ($changesets as $changeset) { $paths[] = $changeset->getOwnersFilename(); } if (!$paths) { return null; } $reviewer_phids = array(); foreach ($this->getReviewers() as $reviewer) { if (!$reviewer->isPackage()) { continue; } $reviewer_phids[] = $reviewer->getReviewerPHID(); } if (!$reviewer_phids) { return null; } // Load all the reviewing packages which have control over some of the // paths in the change. These are packages which the actor may be able // to force-accept on behalf of. $control_query = id(new PhabricatorOwnersPackageQuery()) ->setViewer($viewer) ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE)) ->withPHIDs($reviewer_phids) ->withControl($repository_phid, $paths); $control_packages = $control_query->execute(); if (!$control_packages) { return null; } // Load all the packages which have potential control over some of the // paths in the change and are owned by the actor. These are packages // which the actor may be able to use their authority over to gain the // ability to force-accept for other packages. This query doesn't apply // dominion rules yet, and we'll bypass those rules later on. $authority_query = id(new PhabricatorOwnersPackageQuery()) ->setViewer($viewer) ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE)) ->withAuthorityPHIDs(array($viewer->getPHID())) ->withControl($repository_phid, $paths); $authority_packages = $authority_query->execute(); if (!$authority_packages) { return null; } $authority_packages = mpull($authority_packages, null, 'getPHID'); // Build a map from each path in the revision to the reviewer packages // which control it. $control_map = array(); foreach ($paths as $path) { $control_packages = $control_query->getControllingPackagesForPath( $repository_phid, $path); // Remove packages which the viewer has authority over. We don't need // to check these for force-accept because they can just accept them // normally. $control_packages = mpull($control_packages, null, 'getPHID'); foreach ($control_packages as $phid => $control_package) { if (isset($authority_packages[$phid])) { unset($control_packages[$phid]); } } if (!$control_packages) { continue; } $control_map[$path] = $control_packages; } if (!$control_map) { return null; } // From here on out, we only care about paths which we have at least one // controlling package for. $paths = array_keys($control_map); // Now, build a map from each path to the packages which would control it // if there were no dominion rules. $authority_map = array(); foreach ($paths as $path) { $authority_packages = $authority_query->getControllingPackagesForPath( $repository_phid, $path, $ignore_dominion = true); $authority_map[$path] = mpull($authority_packages, null, 'getPHID'); } // For each path, find the most general package that the viewer has // authority over. For example, we'll prefer a package that owns "/" to a // package that owns "/src/". $force_map = array(); foreach ($authority_map as $path => $package_map) { $path_fragments = PhabricatorOwnersPackage::splitPath($path); $fragment_count = count($path_fragments); // Find the package that we have authority over which has the most // general match for this path. $best_match = null; $best_package = null; foreach ($package_map as $package_phid => $package) { $package_paths = $package->getPathsForRepository($repository_phid); foreach ($package_paths as $package_path) { // NOTE: A strength of 0 means "no match". A strength of 1 means // that we matched "/", so we can not possibly find another stronger // match. $strength = $package_path->getPathMatchStrength( $path_fragments, $fragment_count); if (!$strength) { continue; } if ($strength < $best_match || !$best_package) { $best_match = $strength; $best_package = $package; if ($strength == 1) { break 2; } } } } if ($best_package) { $force_map[$path] = array( 'strength' => $best_match, 'package' => $best_package, ); } } // For each path which the viewer owns a package for, find other packages // which that authority can be used to force-accept. Once we find a way to // force-accept a package, we don't need to keep looking. $has_control = array(); foreach ($force_map as $path => $spec) { $path_fragments = PhabricatorOwnersPackage::splitPath($path); $fragment_count = count($path_fragments); $authority_strength = $spec['strength']; $control_packages = $control_map[$path]; foreach ($control_packages as $control_phid => $control_package) { if (isset($has_control[$control_phid])) { continue; } $control_paths = $control_package->getPathsForRepository( $repository_phid); foreach ($control_paths as $control_path) { $strength = $control_path->getPathMatchStrength( $path_fragments, $fragment_count); if (!$strength) { continue; } if ($strength > $authority_strength) { $authority = $spec['package']; $has_control[$control_phid] = array( 'authority' => $authority, 'phid' => $authority->getPHID(), ); break; } } } } // Return a map from packages which may be force accepted to the packages // which permit that forced acceptance. return ipull($has_control, 'phid'); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { // A revision's author (which effectively means "owner" after we added // commandeering) can always view and edit it. $author_phid = $this->getAuthorPHID(); if ($author_phid) { if ($user->getPHID() == $author_phid) { return true; } } return false; } public function describeAutomaticCapability($capability) { $description = array( pht('The owner of a revision can always view and edit it.'), ); switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $description[] = pht( 'If a revision belongs to a repository, other users must be able '. 'to view the repository in order to view the revision.'); break; } return $description; } /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ public function getExtendedPolicy($capability, PhabricatorUser $viewer) { $extended = array(); switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $repository_phid = $this->getRepositoryPHID(); $repository = $this->getRepository(); // Try to use the object if we have it, since it will save us some // data fetching later on. In some cases, we might not have it. $repository_ref = nonempty($repository, $repository_phid); if ($repository_ref) { $extended[] = array( $repository_ref, PhabricatorPolicyCapability::CAN_VIEW, ); } break; } return $extended; } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } public function getReviewers() { return $this->assertAttached($this->reviewerStatus); } public function attachReviewers(array $reviewers) { assert_instances_of($reviewers, 'DifferentialReviewer'); $reviewers = mpull($reviewers, null, 'getReviewerPHID'); $this->reviewerStatus = $reviewers; return $this; } public function hasAttachedReviewers() { return ($this->reviewerStatus !== self::ATTACHABLE); } public function getReviewerPHIDs() { $reviewers = $this->getReviewers(); return mpull($reviewers, 'getReviewerPHID'); } public function getReviewerPHIDsForEdit() { $reviewers = $this->getReviewers(); $status_blocking = DifferentialReviewerStatus::STATUS_BLOCKING; $value = array(); foreach ($reviewers as $reviewer) { $phid = $reviewer->getReviewerPHID(); if ($reviewer->getReviewerStatus() == $status_blocking) { $value[] = 'blocking('.$phid.')'; } else { $value[] = $phid; } } return $value; } public function getRepository() { return $this->assertAttached($this->repository); } public function attachRepository(PhabricatorRepository $repository = null) { $this->repository = $repository; return $this; } public function setModernRevisionStatus($status) { return $this->setStatus($status); } public function getModernRevisionStatus() { return $this->getStatus(); } public function getLegacyRevisionStatus() { return $this->getStatusObject()->getLegacyKey(); } public function isClosed() { return $this->getStatusObject()->isClosedStatus(); } public function isAbandoned() { return $this->getStatusObject()->isAbandoned(); } public function isAccepted() { return $this->getStatusObject()->isAccepted(); } public function isNeedsReview() { return $this->getStatusObject()->isNeedsReview(); } public function isNeedsRevision() { return $this->getStatusObject()->isNeedsRevision(); } public function isChangePlanned() { return $this->getStatusObject()->isChangePlanned(); } public function isPublished() { return $this->getStatusObject()->isPublished(); } public function isDraft() { return $this->getStatusObject()->isDraft(); } public function getStatusIcon() { return $this->getStatusObject()->getIcon(); } public function getStatusDisplayName() { return $this->getStatusObject()->getDisplayName(); } public function getStatusIconColor() { return $this->getStatusObject()->getIconColor(); } + public function getStatusTagColor() { + return $this->getStatusObject()->getTagColor(); + } + public function getStatusObject() { $status = $this->getStatus(); return DifferentialRevisionStatus::newForStatus($status); } public function getFlag(PhabricatorUser $viewer) { return $this->assertAttachedKey($this->flags, $viewer->getPHID()); } public function attachFlag( PhabricatorUser $viewer, PhabricatorFlag $flag = null) { $this->flags[$viewer->getPHID()] = $flag; return $this; } public function getHasDraft(PhabricatorUser $viewer) { return $this->assertAttachedKey($this->drafts, $viewer->getCacheFragment()); } public function attachHasDraft(PhabricatorUser $viewer, $has_draft) { $this->drafts[$viewer->getCacheFragment()] = $has_draft; return $this; } - public function shouldBroadcast() { - if (!$this->isDraft()) { - return true; - } - - return false; - } - public function getHoldAsDraft() { return $this->getProperty(self::PROPERTY_DRAFT_HOLD, false); } public function setHoldAsDraft($hold) { return $this->setProperty(self::PROPERTY_DRAFT_HOLD, $hold); } public function getShouldBroadcast() { - return $this->getProperty(self::PROPERTY_SHOULD_BROADCAST, false); + return $this->getProperty(self::PROPERTY_SHOULD_BROADCAST, true); } public function setShouldBroadcast($should_broadcast) { return $this->setProperty( self::PROPERTY_SHOULD_BROADCAST, $should_broadcast); } public function setAddedLineCount($count) { return $this->setProperty(self::PROPERTY_LINES_ADDED, $count); } public function getAddedLineCount() { return $this->getProperty(self::PROPERTY_LINES_ADDED); } public function setRemovedLineCount($count) { return $this->setProperty(self::PROPERTY_LINES_REMOVED, $count); } public function getRemovedLineCount() { return $this->getProperty(self::PROPERTY_LINES_REMOVED); } - public function getBuildableStatus($phid) { $buildables = $this->getProperty(self::PROPERTY_BUILDABLES); if (!is_array($buildables)) { $buildables = array(); } $buildable = idx($buildables, $phid); if (!is_array($buildable)) { $buildable = array(); } return idx($buildable, 'status'); } public function setBuildableStatus($phid, $status) { $buildables = $this->getProperty(self::PROPERTY_BUILDABLES); if (!is_array($buildables)) { $buildables = array(); } $buildable = idx($buildables, $phid); if (!is_array($buildable)) { $buildable = array(); } $buildable['status'] = $status; $buildables[$phid] = $buildable; return $this->setProperty(self::PROPERTY_BUILDABLES, $buildables); } public function newBuildableStatus(PhabricatorUser $viewer, $phid) { // For Differential, we're ignoring autobuilds (local lint and unit) // when computing build status. Differential only cares about remote // builds when making publishing and undrafting decisions. $builds = $this->loadImpactfulBuildsForBuildablePHIDs( $viewer, array($phid)); return $this->newBuildableStatusForBuilds($builds); } public function newBuildableStatusForBuilds(array $builds) { // If we have nothing but passing builds, the buildable passes. if (!$builds) { return HarbormasterBuildableStatus::STATUS_PASSED; } // If we have any completed, non-passing builds, the buildable fails. foreach ($builds as $build) { if ($build->isComplete()) { return HarbormasterBuildableStatus::STATUS_FAILED; } } // Otherwise, we're still waiting for the build to pass or fail. return null; } public function loadImpactfulBuilds(PhabricatorUser $viewer) { $diff = $this->getActiveDiff(); // NOTE: We can't use `withContainerPHIDs()` here because the container // update in Harbormaster is not synchronous. $buildables = id(new HarbormasterBuildableQuery()) ->setViewer($viewer) ->withBuildablePHIDs(array($diff->getPHID())) ->withManualBuildables(false) ->execute(); if (!$buildables) { return array(); } return $this->loadImpactfulBuildsForBuildablePHIDs( $viewer, mpull($buildables, 'getPHID')); } private function loadImpactfulBuildsForBuildablePHIDs( PhabricatorUser $viewer, array $phids) { return id(new HarbormasterBuildQuery()) ->setViewer($viewer) ->withBuildablePHIDs($phids) ->withAutobuilds(false) ->withBuildStatuses( array( HarbormasterBuildStatus::STATUS_INACTIVE, HarbormasterBuildStatus::STATUS_PENDING, HarbormasterBuildStatus::STATUS_BUILDING, HarbormasterBuildStatus::STATUS_FAILED, HarbormasterBuildStatus::STATUS_ABORTED, HarbormasterBuildStatus::STATUS_ERROR, HarbormasterBuildStatus::STATUS_PAUSED, HarbormasterBuildStatus::STATUS_DEADLOCKED, )) ->execute(); } /* -( HarbormasterBuildableInterface )------------------------------------- */ public function getHarbormasterBuildableDisplayPHID() { return $this->getHarbormasterContainerPHID(); } public function getHarbormasterBuildablePHID() { return $this->loadActiveDiff()->getPHID(); } public function getHarbormasterContainerPHID() { return $this->getPHID(); } public function getBuildVariables() { return array(); } public function getAvailableBuildVariables() { return array(); } public function newBuildableEngine() { return new DifferentialBuildableEngine(); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { if ($phid == $this->getAuthorPHID()) { return true; } // TODO: This only happens when adding or removing CCs, and is safe from a // policy perspective, but the subscription pathway should have some // opportunity to load this data properly. For now, this is the only case // where implicit subscription is not an intrinsic property of the object. if ($this->reviewerStatus == self::ATTACHABLE) { $reviewers = id(new DifferentialRevisionQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($this->getPHID())) ->needReviewers(true) ->executeOne() ->getReviewers(); } else { $reviewers = $this->getReviewers(); } foreach ($reviewers as $reviewer) { if ($reviewer->getReviewerPHID() !== $phid) { continue; } if ($reviewer->isResigned()) { continue; } return true; } return false; } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('differential.fields'); } public function getCustomFieldBaseClass() { return 'DifferentialCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new DifferentialTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new DifferentialTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { $viewer = $request->getViewer(); $render_data = $timeline->getRenderData(); $left = $request->getInt('left', idx($render_data, 'left')); $right = $request->getInt('right', idx($render_data, 'right')); $diffs = id(new DifferentialDiffQuery()) ->setViewer($request->getUser()) ->withIDs(array($left, $right)) ->execute(); $diffs = mpull($diffs, null, 'getID'); $left_diff = $diffs[$left]; $right_diff = $diffs[$right]; $old_ids = $request->getStr('old', idx($render_data, 'old')); $new_ids = $request->getStr('new', idx($render_data, 'new')); $old_ids = array_filter(explode(',', $old_ids)); $new_ids = array_filter(explode(',', $new_ids)); $type_inline = DifferentialTransaction::TYPE_INLINE; $changeset_ids = array_merge($old_ids, $new_ids); $inlines = array(); foreach ($timeline->getTransactions() as $xaction) { if ($xaction->getTransactionType() == $type_inline) { $inlines[] = $xaction->getComment(); $changeset_ids[] = $xaction->getComment()->getChangesetID(); } } if ($changeset_ids) { $changesets = id(new DifferentialChangesetQuery()) ->setViewer($request->getUser()) ->withIDs($changeset_ids) ->execute(); $changesets = mpull($changesets, null, 'getID'); } else { $changesets = array(); } foreach ($inlines as $key => $inline) { $inlines[$key] = DifferentialInlineComment::newFromModernComment( $inline); } $query = id(new DifferentialInlineCommentQuery()) ->needHidden(true) ->setViewer($viewer); // NOTE: This is a bit sketchy: this method adjusts the inlines as a // side effect, which means it will ultimately adjust the transaction // comments and affect timeline rendering. $query->adjustInlinesForChangesets( $inlines, array_select_keys($changesets, $old_ids), array_select_keys($changesets, $new_ids), $this); return $timeline ->setChangesets($changesets) ->setRevision($this) ->setLeftDiff($left_diff) ->setRightDiff($right_diff); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $diffs = id(new DifferentialDiffQuery()) ->setViewer($engine->getViewer()) ->withRevisionIDs(array($this->getID())) ->execute(); foreach ($diffs as $diff) { $engine->destroyObject($diff); } $conn_w = $this->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d', self::TABLE_COMMIT, $this->getID()); // we have to do paths a little differently as they do not have // an id or phid column for delete() to act on $dummy_path = new DifferentialAffectedPath(); queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d', $dummy_path->getTableName(), $this->getID()); $this->delete(); $this->saveTransaction(); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new DifferentialRevisionFulltextEngine(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new DifferentialRevisionFerretEngine(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('title') ->setType('string') ->setDescription(pht('The revision title.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('authorPHID') ->setType('phid') ->setDescription(pht('Revision author PHID.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('status') ->setType('map') ->setDescription(pht('Information about revision status.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('repositoryPHID') ->setType('phid?') ->setDescription(pht('Revision repository PHID.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('diffPHID') ->setType('phid') ->setDescription(pht('Active diff PHID.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('summary') ->setType('string') ->setDescription(pht('Revision summary.')), ); } public function getFieldValuesForConduit() { $status = $this->getStatusObject(); $status_info = array( 'value' => $status->getKey(), 'name' => $status->getDisplayName(), 'closed' => $status->isClosedStatus(), 'color.ansi' => $status->getANSIColor(), ); return array( 'title' => $this->getTitle(), 'authorPHID' => $this->getAuthorPHID(), 'status' => $status_info, 'repositoryPHID' => $this->getRepositoryPHID(), 'diffPHID' => $this->getActiveDiffPHID(), 'summary' => $this->getSummary(), ); } public function getConduitSearchAttachments() { return array( id(new DifferentialReviewersSearchEngineAttachment()) ->setAttachmentKey('reviewers'), ); } /* -( PhabricatorDraftInterface )------------------------------------------ */ public function newDraftEngine() { return new DifferentialRevisionDraftEngine(); } }