diff --git a/src/applications/diffusion/conduit/DiffusionQueryCommitsConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionQueryCommitsConduitAPIMethod.php index 652f93fa63..4a1c83b400 100644 --- a/src/applications/diffusion/conduit/DiffusionQueryCommitsConduitAPIMethod.php +++ b/src/applications/diffusion/conduit/DiffusionQueryCommitsConduitAPIMethod.php @@ -1,122 +1,122 @@ '; } protected function defineParamTypes() { return array( 'ids' => 'optional list', 'phids' => 'optional list', 'names' => 'optional list', 'repositoryPHID' => 'optional phid', 'needMessages' => 'optional bool', 'bypassCache' => 'optional bool', ) + $this->getPagerParamTypes(); } protected function execute(ConduitAPIRequest $request) { $need_messages = $request->getValue('needMessages'); $viewer = $request->getUser(); $query = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->needCommitData(true); $repository_phid = $request->getValue('repositoryPHID'); if ($repository_phid) { $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withPHIDs(array($repository_phid)) ->executeOne(); if ($repository) { $query->withRepository($repository); } } $names = $request->getValue('names'); if ($names) { $query->withIdentifiers($names); } $ids = $request->getValue('ids'); if ($ids) { $query->withIDs($ids); } $phids = $request->getValue('phids'); if ($phids) { $query->withPHIDs($phids); } $pager = $this->newPager($request); $commits = $query->executeWithCursorPager($pager); $map = $query->getIdentifierMap(); $map = mpull($map, 'getPHID'); $data = array(); foreach ($commits as $commit) { $commit_data = $commit->getCommitData(); $uri = $commit->getURI(); $uri = PhabricatorEnv::getProductionURI($uri); $dict = array( 'id' => $commit->getID(), 'phid' => $commit->getPHID(), 'repositoryPHID' => $commit->getRepository()->getPHID(), 'identifier' => $commit->getCommitIdentifier(), 'epoch' => $commit->getEpoch(), - 'authorEpoch' => $commit_data->getCommitDetail('authorEpoch'), + 'authorEpoch' => $commit_data->getAuthorEpoch(), 'uri' => $uri, 'isImporting' => !$commit->isImported(), 'summary' => $commit->getSummary(), 'authorPHID' => $commit->getAuthorPHID(), 'committerPHID' => $commit_data->getCommitDetail('committerPHID'), - 'author' => $commit_data->getAuthorName(), - 'authorName' => $commit_data->getCommitDetail('authorName'), - 'authorEmail' => $commit_data->getCommitDetail('authorEmail'), - 'committer' => $commit_data->getCommitDetail('committer'), - 'committerName' => $commit_data->getCommitDetail('committerName'), - 'committerEmail' => $commit_data->getCommitDetail('committerEmail'), + 'author' => $commit_data->getAuthorString(), + 'authorName' => $commit_data->getAuthorDisplayName(), + 'authorEmail' => $commit_data->getAuthorEmail(), + 'committer' => $commit_data->getCommitterString(), + 'committerName' => $commit_data->getCommitterDisplayName(), + 'committerEmail' => $commit_data->getCommitterEmail(), 'hashes' => array(), ); if ($need_messages) { $dict['message'] = $commit_data->getCommitMessage(); } $data[$commit->getPHID()] = $dict; } $result = array( 'data' => $data, 'identifierMap' => nonempty($map, (object)array()), ); return $this->addPagerResults($result, $pager); } } diff --git a/src/applications/diffusion/controller/DiffusionCommitController.php b/src/applications/diffusion/controller/DiffusionCommitController.php index 8d52b7facf..473e8911ac 100644 --- a/src/applications/diffusion/controller/DiffusionCommitController.php +++ b/src/applications/diffusion/controller/DiffusionCommitController.php @@ -1,1172 +1,1172 @@ loadDiffusionContext(); if ($response) { return $response; } $drequest = $this->getDiffusionRequest(); $viewer = $request->getUser(); $repository = $drequest->getRepository(); $commit_identifier = $drequest->getCommit(); // If this page is being accessed via "/source/xyz/commit/...", redirect // to the canonical URI. $has_callsign = strlen($request->getURIData('repositoryCallsign')); $has_id = strlen($request->getURIData('repositoryID')); if (!$has_callsign && !$has_id) { $canonical_uri = $repository->getCommitURI($commit_identifier); return id(new AphrontRedirectResponse()) ->setURI($canonical_uri); } if ($request->getStr('diff')) { return $this->buildRawDiffResponse($drequest); } $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withRepository($repository) ->withIdentifiers(array($commit_identifier)) ->needCommitData(true) ->needAuditRequests(true) ->needAuditAuthority(array($viewer)) ->setLimit(100) ->needIdentities(true) ->execute(); $multiple_results = count($commits) > 1; $crumbs = $this->buildCrumbs(array( 'commit' => !$multiple_results, )); $crumbs->setBorder(true); if (!$commits) { if (!$this->getCommitExists()) { return new Aphront404Response(); } $error = id(new PHUIInfoView()) ->setTitle(pht('Commit Still Parsing')) ->appendChild( pht( 'Failed to load the commit because the commit has not been '. 'parsed yet.')); $title = pht('Commit Still Parsing'); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild($error); } else if ($multiple_results) { $warning_message = pht( 'The identifier %s is ambiguous and matches more than one commit.', phutil_tag( 'strong', array(), $commit_identifier)); $error = id(new PHUIInfoView()) ->setTitle(pht('Ambiguous Commit')) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->appendChild($warning_message); $list = id(new DiffusionCommitGraphView()) ->setViewer($viewer) ->setCommits($commits); $crumbs->addTextCrumb(pht('Ambiguous Commit')); $matched_commits = id(new PHUITwoColumnView()) ->setFooter(array( $error, $list, )); return $this->newPage() ->setTitle(pht('Ambiguous Commit')) ->setCrumbs($crumbs) ->appendChild($matched_commits); } else { $commit = head($commits); } $audit_requests = $commit->getAudits(); $commit_data = $commit->getCommitData(); $is_foreign = $commit_data->getCommitDetail('foreign-svn-stub'); $error_panel = null; $unpublished_panel = null; $hard_limit = 1000; if ($commit->isImported()) { $change_query = DiffusionPathChangeQuery::newFromDiffusionRequest( $drequest); $change_query->setLimit($hard_limit + 1); $changes = $change_query->loadChanges(); } else { $changes = array(); } $was_limited = (count($changes) > $hard_limit); if ($was_limited) { $changes = array_slice($changes, 0, $hard_limit); } $count = count($changes); $is_unreadable = false; $hint = null; if (!$count || $commit->isUnreachable()) { $hint = id(new DiffusionCommitHintQuery()) ->setViewer($viewer) ->withRepositoryPHIDs(array($repository->getPHID())) ->withOldCommitIdentifiers(array($commit->getCommitIdentifier())) ->executeOne(); if ($hint) { $is_unreadable = $hint->isUnreadable(); } } if ($is_foreign) { $subpath = $commit_data->getCommitDetail('svn-subpath'); $error_panel = new PHUIInfoView(); $error_panel->setTitle(pht('Commit Not Tracked')); $error_panel->setSeverity(PHUIInfoView::SEVERITY_WARNING); $error_panel->appendChild( pht( "This Diffusion repository is configured to track only one ". "subdirectory of the entire Subversion repository, and this commit ". "didn't affect the tracked subdirectory ('%s'), so no ". "information is available.", $subpath)); } else { $engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine(); $engine->setConfig('viewer', $viewer); $commit_tag = $this->renderCommitHashTag($drequest); $header = id(new PHUIHeaderView()) ->setHeader(nonempty($commit->getSummary(), pht('Commit Detail'))) ->setHeaderIcon('fa-code-fork') ->addTag($commit_tag); if (!$commit->isAuditStatusNoAudit()) { $status = $commit->getAuditStatusObject(); $icon = $status->getIcon(); $color = $status->getColor(); $status = $status->getName(); $header->setStatus($icon, $color, $status); } $curtain = $this->buildCurtain($commit, $repository); $details = $this->buildPropertyListView( $commit, $commit_data, $audit_requests); $message = $commit_data->getCommitMessage(); $revision = $commit->getCommitIdentifier(); $message = $this->linkBugtraq($message); $message = $engine->markupText($message); $detail_list = new PHUIPropertyListView(); $detail_list->addTextContent( phutil_tag( 'div', array( 'class' => 'diffusion-commit-message phabricator-remarkup', ), $message)); if ($commit->isUnreachable()) { $did_rewrite = false; if ($hint) { if ($hint->isRewritten()) { $rewritten = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withRepository($repository) ->withIdentifiers(array($hint->getNewCommitIdentifier())) ->executeOne(); if ($rewritten) { $did_rewrite = true; $rewritten_uri = $rewritten->getURI(); $rewritten_name = $rewritten->getLocalName(); $rewritten_link = phutil_tag( 'a', array( 'href' => $rewritten_uri, ), $rewritten_name); $this->commitErrors[] = pht( 'This commit was rewritten after it was published, which '. 'changed the commit hash. This old version of the commit is '. 'no longer reachable from any branch, tag or ref. The new '. 'version of this commit is %s.', $rewritten_link); } } } if (!$did_rewrite) { $this->commitErrors[] = pht( 'This commit has been deleted in the repository: it is no longer '. 'reachable from any branch, tag, or ref.'); } } if (!$commit->isPermanentCommit()) { $nonpermanent_tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_SHADE) ->setName(pht('Unpublished')) ->setColor(PHUITagView::COLOR_ORANGE); $header->addTag($nonpermanent_tag); $holds = $commit_data->newPublisherHoldReasons(); $reasons = array(); foreach ($holds as $hold) { $reasons[] = array( phutil_tag('strong', array(), pht('%s:', $hold->getName())), ' ', $hold->getSummary(), ); } if (!$holds) { $reasons[] = pht('No further details are available.'); } $doc_href = PhabricatorEnv::getDoclink( 'Diffusion User Guide: Permanent Refs'); $doc_link = phutil_tag( 'a', array( 'href' => $doc_href, 'target' => '_blank', ), pht('Learn More')); $title = array( pht('Unpublished Commit'), pht(" \xC2\xB7 "), $doc_link, ); $unpublished_panel = id(new PHUIInfoView()) ->setTitle($title) ->setErrors($reasons) ->setSeverity(PHUIInfoView::SEVERITY_WARNING); } if ($this->getCommitErrors()) { $error_panel = id(new PHUIInfoView()) ->appendChild($this->getCommitErrors()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING); } } $timeline = $this->buildComments($commit); $merge_table = $this->buildMergesTable($commit); $show_changesets = false; $info_panel = null; $change_list = null; $change_table = null; if ($is_unreadable) { $info_panel = $this->renderStatusMessage( pht('Unreadable Commit'), pht( 'This commit has been marked as unreadable by an administrator. '. 'It may have been corrupted or created improperly by an external '. 'tool.')); } else if ($is_foreign) { // Don't render anything else. } else if (!$commit->isImported()) { $info_panel = $this->renderStatusMessage( pht('Still Importing...'), pht( 'This commit is still importing. Changes will be visible once '. 'the import finishes.')); } else if (!count($changes)) { $info_panel = $this->renderStatusMessage( pht('Empty Commit'), pht( 'This commit is empty and does not affect any paths.')); } else if ($was_limited) { $info_panel = $this->renderStatusMessage( pht('Very Large Commit'), pht( 'This commit is very large, and affects more than %d files. '. 'Changes are not shown.', $hard_limit)); } else if (!$this->getCommitExists()) { $info_panel = $this->renderStatusMessage( pht('Commit No Longer Exists'), pht('This commit no longer exists in the repository.')); } else { $show_changesets = true; // The user has clicked "Show All Changes", and we should show all the // changes inline even if there are more than the soft limit. $show_all_details = $request->getBool('show_all'); $change_header = id(new PHUIHeaderView()) ->setHeader(pht('Changes (%s)', new PhutilNumber($count))); $warning_view = null; if ($count > self::CHANGES_LIMIT && !$show_all_details) { $button = id(new PHUIButtonView()) ->setText(pht('Show All Changes')) ->setHref('?show_all=true') ->setTag('a') ->setIcon('fa-files-o'); $warning_view = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setTitle(pht('Very Large Commit')) ->appendChild( pht('This commit is very large. Load each file individually.')); $change_header->addActionLink($button); } $changesets = DiffusionPathChange::convertToDifferentialChangesets( $viewer, $changes); // TODO: This table and panel shouldn't really be separate, but we need // to clean up the "Load All Files" interaction first. $change_table = $this->buildTableOfContents( $changesets, $change_header, $warning_view); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $vcs_supports_directory_changes = true; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $vcs_supports_directory_changes = false; break; default: throw new Exception(pht('Unknown VCS.')); } $references = array(); foreach ($changesets as $key => $changeset) { $file_type = $changeset->getFileType(); if ($file_type == DifferentialChangeType::FILE_DIRECTORY) { if (!$vcs_supports_directory_changes) { unset($changesets[$key]); continue; } } $references[$key] = $drequest->generateURI( array( 'action' => 'rendering-ref', 'path' => $changeset->getFilename(), )); } // TODO: Some parts of the views still rely on properties of the // DifferentialChangeset. Make the objects ephemeral to make sure we don't // accidentally save them, and then set their ID to the appropriate ID for // this application (the path IDs). $path_ids = array_flip(mpull($changes, 'getPath')); foreach ($changesets as $changeset) { $changeset->makeEphemeral(); $changeset->setID($path_ids[$changeset->getFilename()]); } if ($count <= self::CHANGES_LIMIT || $show_all_details) { $visible_changesets = $changesets; } else { $visible_changesets = array(); $inlines = id(new DiffusionDiffInlineCommentQuery()) ->setViewer($viewer) ->withCommitPHIDs(array($commit->getPHID())) ->withPublishedComments(true) ->withPublishableComments(true) ->execute(); $inlines = mpull($inlines, 'newInlineCommentObject'); $path_ids = mpull($inlines, null, 'getPathID'); foreach ($changesets as $key => $changeset) { if (array_key_exists($changeset->getID(), $path_ids)) { $visible_changesets[$key] = $changeset; } } } $change_list_title = $commit->getDisplayName(); $change_list = new DifferentialChangesetListView(); $change_list->setTitle($change_list_title); $change_list->setChangesets($changesets); $change_list->setVisibleChangesets($visible_changesets); $change_list->setRenderingReferences($references); $change_list->setRenderURI($repository->getPathURI('diff/')); $change_list->setRepository($repository); $change_list->setUser($viewer); $change_list->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); // TODO: Try to setBranch() to something reasonable here? $change_list->setStandaloneURI( $repository->getPathURI('diff/')); $change_list->setRawFileURIs( // TODO: Implement this, somewhat tricky if there's an octopus merge // or whatever? null, $repository->getPathURI('diff/?view=r')); $change_list->setInlineCommentControllerURI( '/diffusion/inline/edit/'.phutil_escape_uri($commit->getPHID()).'/'); } $add_comment = $this->renderAddCommentPanel( $commit, $timeline); $filetree = id(new DifferentialFileTreeEngine()) ->setViewer($viewer) ->setDisabled(!$show_changesets); if ($show_changesets) { $filetree->setChangesets($changesets); } $description_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Description')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($detail_list); $detail_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Details')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($details); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) ->setMainColumn( array( $unpublished_panel, $error_panel, $description_box, $detail_box, $timeline, $merge_table, $info_panel, )) ->setFooter( array( $change_table, $change_list, $add_comment, )); $main_content = array( $crumbs, $view, ); $main_content = $filetree->newView($main_content); if (!$filetree->getDisabled()) { $change_list->setFormationView($main_content); } $page = $this->newPage() ->setTitle($commit->getDisplayName()) ->setPageObjectPHIDS(array($commit->getPHID())) ->appendChild($main_content); return $page; } private function buildPropertyListView( PhabricatorRepositoryCommit $commit, PhabricatorRepositoryCommitData $data, array $audit_requests) { $viewer = $this->getViewer(); $commit_phid = $commit->getPHID(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $view = id(new PHUIPropertyListView()) ->setUser($this->getRequest()->getUser()) ->setObject($commit); $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($commit_phid)) ->withEdgeTypes(array( DiffusionCommitHasTaskEdgeType::EDGECONST, DiffusionCommitHasRevisionEdgeType::EDGECONST, DiffusionCommitRevertsCommitEdgeType::EDGECONST, DiffusionCommitRevertedByCommitEdgeType::EDGECONST, )); $edges = $edge_query->execute(); $task_phids = array_keys( $edges[$commit_phid][DiffusionCommitHasTaskEdgeType::EDGECONST]); $revision_phid = key( $edges[$commit_phid][DiffusionCommitHasRevisionEdgeType::EDGECONST]); $reverts_phids = array_keys( $edges[$commit_phid][DiffusionCommitRevertsCommitEdgeType::EDGECONST]); $reverted_by_phids = array_keys( $edges[$commit_phid][DiffusionCommitRevertedByCommitEdgeType::EDGECONST]); $phids = $edge_query->getDestinationPHIDs(array($commit_phid)); if ($data->getCommitDetail('reviewerPHID')) { $phids[] = $data->getCommitDetail('reviewerPHID'); } $phids[] = $commit->getCommitterDisplayPHID(); $phids[] = $commit->getAuthorDisplayPHID(); // NOTE: We should never normally have more than a single push log, but // it can occur naturally if a commit is pushed, then the branch it was // on is deleted, then the commit is pushed again (or through other similar // chains of events). This should be rare, but does not indicate a bug // or data issue. // NOTE: We never query push logs in SVN because the committer is always // the pusher and the commit time is always the push time; the push log // is redundant and we save a query by skipping it. $push_logs = array(); if ($repository->isHosted() && !$repository->isSVN()) { $push_logs = id(new PhabricatorRepositoryPushLogQuery()) ->setViewer($viewer) ->withRepositoryPHIDs(array($repository->getPHID())) ->withNewRefs(array($commit->getCommitIdentifier())) ->withRefTypes(array(PhabricatorRepositoryPushLog::REFTYPE_COMMIT)) ->execute(); foreach ($push_logs as $log) { $phids[] = $log->getPusherPHID(); } } $handles = array(); if ($phids) { $handles = $this->loadViewerHandles($phids); } $props = array(); if ($audit_requests) { $user_requests = array(); $other_requests = array(); foreach ($audit_requests as $audit_request) { if (!$audit_request->isInteresting()) { continue; } if ($audit_request->isUser()) { $user_requests[] = $audit_request; } else { $other_requests[] = $audit_request; } } if ($user_requests) { $view->addProperty( pht('Auditors'), $this->renderAuditStatusView($commit, $user_requests)); } if ($other_requests) { $view->addProperty( pht('Group Auditors'), $this->renderAuditStatusView($commit, $other_requests)); } } $provenance_list = new PHUIStatusListView(); $author_view = $commit->newCommitAuthorView($viewer); if ($author_view) { - $author_date = $data->getCommitDetail('authorEpoch'); + $author_date = $data->getAuthorEpoch(); $author_date = phabricator_datetime($author_date, $viewer); $provenance_list->addItem( id(new PHUIStatusItemView()) ->setTarget($author_view) ->setNote(pht('Authored on %s', $author_date))); } if (!$commit->isAuthorSameAsCommitter()) { $committer_view = $commit->newCommitCommitterView($viewer); if ($committer_view) { $committer_date = $commit->getEpoch(); $committer_date = phabricator_datetime($committer_date, $viewer); $provenance_list->addItem( id(new PHUIStatusItemView()) ->setTarget($committer_view) ->setNote(pht('Committed on %s', $committer_date))); } } if ($push_logs) { $pushed_list = new PHUIStatusListView(); foreach ($push_logs as $push_log) { $pusher_date = $push_log->getEpoch(); $pusher_date = phabricator_datetime($pusher_date, $viewer); $pusher_view = $handles[$push_log->getPusherPHID()]->renderLink(); $provenance_list->addItem( id(new PHUIStatusItemView()) ->setTarget($pusher_view) ->setNote(pht('Pushed on %s', $pusher_date))); } } $view->addProperty(pht('Provenance'), $provenance_list); $reviewer_phid = $data->getCommitDetail('reviewerPHID'); if ($reviewer_phid) { $view->addProperty( pht('Reviewer'), $handles[$reviewer_phid]->renderLink()); } if ($revision_phid) { $view->addProperty( pht('Differential Revision'), $handles[$revision_phid]->renderLink()); } $parents = $this->getCommitParents(); if ($parents) { $view->addProperty( pht('Parents'), $viewer->renderHandleList(mpull($parents, 'getPHID'))); } if ($this->getCommitExists()) { $view->addProperty( pht('Branches'), phutil_tag( 'span', array( 'id' => 'commit-branches', ), pht('Unknown'))); $view->addProperty( pht('Tags'), phutil_tag( 'span', array( 'id' => 'commit-tags', ), pht('Unknown'))); $identifier = $commit->getCommitIdentifier(); $root = $repository->getPathURI("commit/{$identifier}"); Javelin::initBehavior( 'diffusion-commit-branches', array( $root.'/branches/' => 'commit-branches', $root.'/tags/' => 'commit-tags', )); } $refs = $this->getCommitRefs(); if ($refs) { $ref_links = array(); foreach ($refs as $ref_data) { $ref_links[] = phutil_tag( 'a', array( 'href' => $ref_data['href'], ), $ref_data['ref']); } $view->addProperty( pht('References'), phutil_implode_html(', ', $ref_links)); } if ($reverts_phids) { $view->addProperty( pht('Reverts'), $viewer->renderHandleList($reverts_phids)); } if ($reverted_by_phids) { $view->addProperty( pht('Reverted By'), $viewer->renderHandleList($reverted_by_phids)); } if ($task_phids) { $task_list = array(); foreach ($task_phids as $phid) { $task_list[] = $handles[$phid]->renderLink(); } $task_list = phutil_implode_html(phutil_tag('br'), $task_list); $view->addProperty( pht('Tasks'), $task_list); } return $view; } private function buildComments(PhabricatorRepositoryCommit $commit) { $timeline = $this->buildTransactionTimeline( $commit, new PhabricatorAuditTransactionQuery()); $timeline->setQuoteRef($commit->getMonogram()); return $timeline; } private function renderAddCommentPanel( PhabricatorRepositoryCommit $commit, $timeline) { $request = $this->getRequest(); $viewer = $request->getUser(); // TODO: This is pretty awkward, unify the CSS between Diffusion and // Differential better. require_celerity_resource('differential-core-view-css'); $comment_view = id(new DiffusionCommitEditEngine()) ->setViewer($viewer) ->buildEditEngineCommentView($commit); $comment_view->setTransactionTimeline($timeline); return $comment_view; } private function buildMergesTable(PhabricatorRepositoryCommit $commit) { $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $merges = $this->getCommitMerges(); if (!$merges) { return null; } $limit = $this->getMergeDisplayLimit(); $caption = null; if (count($merges) > $limit) { $merges = array_slice($merges, 0, $limit); $caption = new PHUIInfoView(); $caption->setSeverity(PHUIInfoView::SEVERITY_NOTICE); $caption->appendChild( pht( 'This commit merges a very large number of changes. '. 'Only the first %s are shown.', new PhutilNumber($limit))); } $commit_list = id(new DiffusionCommitGraphView()) ->setViewer($viewer) ->setDiffusionRequest($drequest) ->setHistory($merges); $panel = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Merged Changes')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setObjectList($commit_list->newObjectItemListView()); if ($caption) { $panel->setInfoView($caption); } return $panel; } private function buildCurtain( PhabricatorRepositoryCommit $commit, PhabricatorRepository $repository) { $request = $this->getRequest(); $viewer = $this->getViewer(); $curtain = $this->newCurtainView($commit); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $commit, PhabricatorPolicyCapability::CAN_EDIT); $id = $commit->getID(); $edit_uri = $this->getApplicationURI("/commit/edit/{$id}/"); $action = id(new PhabricatorActionView()) ->setName(pht('Edit Commit')) ->setHref($edit_uri) ->setIcon('fa-pencil') ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit); $curtain->addAction($action); $action = id(new PhabricatorActionView()) ->setName(pht('Download Raw Diff')) ->setHref($request->getRequestURI()->alter('diff', true)) ->setIcon('fa-download'); $curtain->addAction($action); $relationship_list = PhabricatorObjectRelationshipList::newForObject( $viewer, $commit); $relationship_submenu = $relationship_list->newActionMenu(); if ($relationship_submenu) { $curtain->addAction($relationship_submenu); } return $curtain; } private function buildRawDiffResponse(DiffusionRequest $drequest) { $diff_info = $this->callConduitWithDiffusionRequest( 'diffusion.rawdiffquery', array( 'commit' => $drequest->getCommit(), 'path' => $drequest->getPath(), )); $file_phid = $diff_info['filePHID']; $file = id(new PhabricatorFileQuery()) ->setViewer($this->getViewer()) ->withPHIDs(array($file_phid)) ->executeOne(); if (!$file) { throw new Exception( pht( 'Failed to load file ("%s") returned by "%s".', $file_phid, 'diffusion.rawdiffquery')); } return $file->getRedirectResponse(); } private function renderAuditStatusView( PhabricatorRepositoryCommit $commit, array $audit_requests) { assert_instances_of($audit_requests, 'PhabricatorRepositoryAuditRequest'); $viewer = $this->getViewer(); $view = new PHUIStatusListView(); foreach ($audit_requests as $request) { $code = $request->getAuditStatus(); $item = new PHUIStatusItemView(); $item->setIcon( PhabricatorAuditStatusConstants::getStatusIcon($code), PhabricatorAuditStatusConstants::getStatusColor($code), PhabricatorAuditStatusConstants::getStatusName($code)); $auditor_phid = $request->getAuditorPHID(); $target = $viewer->renderHandle($auditor_phid); $item->setTarget($target); if ($commit->hasAuditAuthority($viewer, $request)) { $item->setHighlighted(true); } $view->addItem($item); } return $view; } private function linkBugtraq($corpus) { $url = PhabricatorEnv::getEnvConfig('bugtraq.url'); if (!strlen($url)) { return $corpus; } $regexes = PhabricatorEnv::getEnvConfig('bugtraq.logregex'); if (!$regexes) { return $corpus; } $parser = id(new PhutilBugtraqParser()) ->setBugtraqPattern("[[ {$url} | %BUGID% ]]") ->setBugtraqCaptureExpression(array_shift($regexes)); $select = array_shift($regexes); if ($select) { $parser->setBugtraqSelectExpression($select); } return $parser->processCorpus($corpus); } private function buildTableOfContents( array $changesets, $header, $info_view) { $drequest = $this->getDiffusionRequest(); $viewer = $this->getViewer(); $toc_view = id(new PHUIDiffTableOfContentsListView()) ->setUser($viewer) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); if ($info_view) { $toc_view->setInfoView($info_view); } // TODO: This is hacky, we just want access to the linkX() methods on // DiffusionView. $diffusion_view = id(new DiffusionEmptyResultView()) ->setDiffusionRequest($drequest); $have_owners = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorOwnersApplication', $viewer); if (!$changesets) { $have_owners = false; } if ($have_owners) { if ($viewer->getPHID()) { $packages = id(new PhabricatorOwnersPackageQuery()) ->setViewer($viewer) ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE)) ->withAuthorityPHIDs(array($viewer->getPHID())) ->execute(); $toc_view->setAuthorityPackages($packages); } $repository = $drequest->getRepository(); $repository_phid = $repository->getPHID(); $control_query = id(new PhabricatorOwnersPackageQuery()) ->setViewer($viewer) ->withStatuses(array(PhabricatorOwnersPackage::STATUS_ACTIVE)) ->withControl($repository_phid, mpull($changesets, 'getFilename')); $control_query->execute(); } foreach ($changesets as $changeset_id => $changeset) { $path = $changeset->getFilename(); $anchor = $changeset->getAnchorName(); $history_link = $diffusion_view->linkHistory($path); $browse_link = $diffusion_view->linkBrowse( $path, array( 'type' => $changeset->getFileType(), )); $item = id(new PHUIDiffTableOfContentsItemView()) ->setChangeset($changeset) ->setAnchor($anchor) ->setContext( array( $history_link, ' ', $browse_link, )); if ($have_owners) { $packages = $control_query->getControllingPackagesForPath( $repository_phid, $changeset->getFilename()); $item->setPackages($packages); } $toc_view->addItem($item); } return $toc_view; } private function loadCommitState() { $viewer = $this->getViewer(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $commit = $drequest->getCommit(); // TODO: We could use futures here and resolve these calls in parallel. $exceptions = array(); try { $parent_refs = $this->callConduitWithDiffusionRequest( 'diffusion.commitparentsquery', array( 'commit' => $commit, )); if ($parent_refs) { $parents = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withRepository($repository) ->withIdentifiers($parent_refs) ->execute(); } else { $parents = array(); } $this->commitParents = $parents; } catch (Exception $ex) { $this->commitParents = false; $exceptions[] = $ex; } $merge_limit = $this->getMergeDisplayLimit(); try { if ($repository->isSVN()) { $this->commitMerges = array(); } else { $merges = $this->callConduitWithDiffusionRequest( 'diffusion.mergedcommitsquery', array( 'commit' => $commit, 'limit' => $merge_limit + 1, )); $this->commitMerges = DiffusionPathChange::newFromConduit($merges); } } catch (Exception $ex) { $this->commitMerges = false; $exceptions[] = $ex; } try { if ($repository->isGit()) { $refs = $this->callConduitWithDiffusionRequest( 'diffusion.refsquery', array( 'commit' => $commit, )); } else { $refs = array(); } $this->commitRefs = $refs; } catch (Exception $ex) { $this->commitRefs = false; $exceptions[] = $ex; } if ($exceptions) { $exists = $this->callConduitWithDiffusionRequest( 'diffusion.existsquery', array( 'commit' => $commit, )); if ($exists) { $this->commitExists = true; foreach ($exceptions as $exception) { $this->commitErrors[] = $exception->getMessage(); } } else { $this->commitExists = false; $this->commitErrors[] = pht( 'This commit no longer exists in the repository. It may have '. 'been part of a branch which was deleted.'); } } else { $this->commitExists = true; $this->commitErrors = array(); } } private function getMergeDisplayLimit() { return 50; } private function getCommitExists() { if ($this->commitExists === null) { $this->loadCommitState(); } return $this->commitExists; } private function getCommitParents() { if ($this->commitParents === null) { $this->loadCommitState(); } return $this->commitParents; } private function getCommitRefs() { if ($this->commitRefs === null) { $this->loadCommitState(); } return $this->commitRefs; } private function getCommitMerges() { if ($this->commitMerges === null) { $this->loadCommitState(); } return $this->commitMerges; } private function getCommitErrors() { if ($this->commitErrors === null) { $this->loadCommitState(); } return $this->commitErrors; } } diff --git a/src/applications/diffusion/data/DiffusionPathChange.php b/src/applications/diffusion/data/DiffusionPathChange.php index 62dfdd6ace..02db490d4d 100644 --- a/src/applications/diffusion/data/DiffusionPathChange.php +++ b/src/applications/diffusion/data/DiffusionPathChange.php @@ -1,200 +1,200 @@ path = $path; return $this; } public function getPath() { return $this->path; } public function setChangeType($change_type) { $this->changeType = $change_type; return $this; } public function getChangeType() { return $this->changeType; } public function setFileType($file_type) { $this->fileType = $file_type; return $this; } public function getFileType() { return $this->fileType; } public function setTargetPath($target_path) { $this->targetPath = $target_path; return $this; } public function getTargetPath() { return $this->targetPath; } public function setAwayPaths(array $away_paths) { $this->awayPaths = $away_paths; return $this; } public function getAwayPaths() { return $this->awayPaths; } public function setCommitIdentifier($commit) { $this->commitIdentifier = $commit; return $this; } public function getCommitIdentifier() { return $this->commitIdentifier; } public function setTargetCommitIdentifier($target_commit_identifier) { $this->targetCommitIdentifier = $target_commit_identifier; return $this; } public function getTargetCommitIdentifier() { return $this->targetCommitIdentifier; } public function setCommit($commit) { $this->commit = $commit; return $this; } public function getCommit() { return $this->commit; } public function setCommitData($commit_data) { $this->commitData = $commit_data; return $this; } public function getCommitData() { return $this->commitData; } public function getEpoch() { if ($this->getCommit()) { return $this->getCommit()->getEpoch(); } return null; } public function getAuthorName() { if ($this->getCommitData()) { - return $this->getCommitData()->getAuthorName(); + return $this->getCommitData()->getAuthorString(); } return null; } public function getSummary() { if (!$this->getCommitData()) { return null; } return $this->getCommitData()->getSummary(); } public static function convertToArcanistChanges(array $changes) { assert_instances_of($changes, __CLASS__); $direct = array(); $result = array(); foreach ($changes as $path) { $change = new ArcanistDiffChange(); $change->setCurrentPath($path->getPath()); $direct[] = $path->getPath(); $change->setType($path->getChangeType()); $file_type = $path->getFileType(); if ($file_type == DifferentialChangeType::FILE_NORMAL) { $file_type = DifferentialChangeType::FILE_TEXT; } $change->setFileType($file_type); $change->setOldPath($path->getTargetPath()); foreach ($path->getAwayPaths() as $away_path) { $change->addAwayPath($away_path); } $result[$path->getPath()] = $change; } return array_select_keys($result, $direct); } public static function convertToDifferentialChangesets( PhabricatorUser $user, array $changes) { assert_instances_of($changes, __CLASS__); $arcanist_changes = self::convertToArcanistChanges($changes); $diff = DifferentialDiff::newEphemeralFromRawChanges( $arcanist_changes); return $diff->getChangesets(); } public function toDictionary() { $commit = $this->getCommit(); if ($commit) { $commit_dict = $commit->toDictionary(); } else { $commit_dict = array(); } $commit_data = $this->getCommitData(); if ($commit_data) { $commit_data_dict = $commit_data->toDictionary(); } else { $commit_data_dict = array(); } return array( 'path' => $this->getPath(), 'commitIdentifier' => $this->getCommitIdentifier(), 'commit' => $commit_dict, 'commitData' => $commit_data_dict, 'fileType' => $this->getFileType(), 'changeType' => $this->getChangeType(), 'targetPath' => $this->getTargetPath(), 'targetCommitIdentifier' => $this->getTargetCommitIdentifier(), 'awayPaths' => $this->getAwayPaths(), ); } public static function newFromConduit(array $dicts) { $results = array(); foreach ($dicts as $dict) { $commit = PhabricatorRepositoryCommit::newFromDictionary($dict['commit']); $commit_data = PhabricatorRepositoryCommitData::newFromDictionary( $dict['commitData']); $results[] = id(new DiffusionPathChange()) ->setPath($dict['path']) ->setCommitIdentifier($dict['commitIdentifier']) ->setCommit($commit) ->setCommitData($commit_data) ->setFileType($dict['fileType']) ->setChangeType($dict['changeType']) ->setTargetPath($dict['targetPath']) ->setTargetCommitIdentifier($dict['targetCommitIdentifier']) ->setAwayPaths($dict['awayPaths']); } return $results; } } diff --git a/src/applications/diffusion/view/DiffusionTagListView.php b/src/applications/diffusion/view/DiffusionTagListView.php index 0f8cbeae71..0fa7db4c0f 100644 --- a/src/applications/diffusion/view/DiffusionTagListView.php +++ b/src/applications/diffusion/view/DiffusionTagListView.php @@ -1,169 +1,169 @@ tags = $tags; return $this; } public function setCommits(array $commits) { $this->commits = mpull($commits, null, 'getCommitIdentifier'); return $this; } public function setHandles(array $handles) { $this->handles = $handles; return $this; } public function getRequiredHandlePHIDs() { return array_filter(mpull($this->commits, 'getAuthorPHID')); } public function render() { $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $viewer = $this->getViewer(); require_celerity_resource('diffusion-css'); $buildables = $this->loadBuildables($this->commits); $list = id(new PHUIObjectItemListView()) ->setFlush(true) ->addClass('diffusion-history-list'); foreach ($this->tags as $tag) { $commit = idx($this->commits, $tag->getCommitIdentifier()); $button_bar = new PHUIButtonBarView(); $tag_href = $drequest->generateURI( array( 'action' => 'history', 'commit' => $tag->getName(), )); $commit_href = $drequest->generateURI( array( 'action' => 'commit', 'commit' => $tag->getCommitIdentifier(), )); if ($commit) { $author = $this->renderAuthor($tag, $commit); } else { $author = null; } $description = null; if ($tag->getType() == 'git/tag') { // In Git, a tag may be a "real" tag, or just a reference to a commit. // If it's a real tag, use the message on the tag, since this may be // unique data which isn't otherwise available. $description = $tag->getDescription(); } else { if ($commit) { $description = $commit->getSummary(); } else { $description = $tag->getDescription(); } } $build_view = null; if ($commit) { $buildable = idx($buildables, $commit->getPHID()); if ($buildable) { $build_view = $this->renderBuildable($buildable, 'button'); } } if ($repository->supportsBranchComparison()) { $compare_uri = $drequest->generateURI( array( 'action' => 'compare', 'head' => $tag->getName(), )); $button_bar->addButton( id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-balance-scale') ->setToolTip(pht('Compare')) ->setButtonType(PHUIButtonView::BUTTONTYPE_SIMPLE) ->setWorkflow(true) ->setHref($compare_uri)); } $commit_name = $repository->formatCommitName( $tag->getCommitIdentifier(), $local = true); $browse_href = $drequest->generateURI( array( 'action' => 'browse', 'commit' => $tag->getName(), )); $button_bar->addButton( id(new PHUIButtonView()) ->setTooltip(pht('Browse')) ->setIcon('fa-code') ->setHref($browse_href) ->setTag('a') ->setButtonType(PHUIButtonView::BUTTONTYPE_SIMPLE)); $commit_tag = id(new PHUITagView()) ->setName($commit_name) ->setHref($commit_href) ->setType(PHUITagView::TYPE_SHADE) ->setColor(PHUITagView::COLOR_INDIGO) ->setBorder(PHUITagView::BORDER_NONE) ->setSlimShady(true); $item = id(new PHUIObjectItemView()) ->setHeader($tag->getName()) ->setHref($tag_href) ->addAttribute(array($commit_tag)) ->addAttribute($description) ->setSideColumn(array( $build_view, $button_bar, )); if ($author) { $item->addAttribute($author); } $list->addItem($item); } return $list; } private function renderAuthor( DiffusionRepositoryTag $tag, PhabricatorRepositoryCommit $commit) { $viewer = $this->getViewer(); if ($commit->getAuthorPHID()) { $author = $this->handles[$commit->getAuthorPHID()]->renderLink(); } else if ($commit->getCommitData()) { - $author = self::renderName($commit->getCommitData()->getAuthorName()); + $author = self::renderName($commit->getCommitData()->getAuthorString()); } else { $author = self::renderName($tag->getAuthor()); } $committed = phabricator_datetime($commit->getEpoch(), $viewer); $author_name = phutil_tag( 'strong', array( 'class' => 'diffusion-history-author-name', ), $author); return pht('%s on %s.', $author_name, $committed); } } diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php index bd906aa5da..9a0f68713e 100644 --- a/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php +++ b/src/applications/repository/management/PhabricatorRepositoryManagementRebuildIdentitiesWorkflow.php @@ -1,433 +1,433 @@ setName('rebuild-identities') ->setExamples( '**rebuild-identities** [__options__] __repository__') ->setSynopsis(pht('Rebuild repository identities from commits.')) ->setArguments( array( array( 'name' => 'all-repositories', 'help' => pht('Rebuild identities across all repositories.'), ), array( 'name' => 'all-identities', 'help' => pht('Rebuild all currently-known identities.'), ), array( 'name' => 'repository', 'param' => 'repository', 'repeat' => true, 'help' => pht('Rebuild identities in a repository.'), ), array( 'name' => 'commit', 'param' => 'commit', 'repeat' => true, 'help' => pht('Rebuild identities for a commit.'), ), array( 'name' => 'user', 'param' => 'user', 'repeat' => true, 'help' => pht('Rebuild identities for a user.'), ), array( 'name' => 'email', 'param' => 'email', 'repeat' => true, 'help' => pht('Rebuild identities for an email address.'), ), array( 'name' => 'raw', 'param' => 'raw', 'repeat' => true, 'help' => pht('Rebuild identities for a raw commit string.'), ), array( 'name' => 'dry-run', 'help' => pht('Show changes, but do not make any changes.'), ), )); } public function execute(PhutilArgumentParser $args) { $viewer = $this->getViewer(); $rebuilt_anything = false; $all_repositories = $args->getArg('all-repositories'); $repositories = $args->getArg('repository'); if ($all_repositories && $repositories) { throw new PhutilArgumentUsageException( pht( 'Flags "--all-repositories" and "--repository" are not '. 'compatible.')); } $all_identities = $args->getArg('all-identities'); $raw = $args->getArg('raw'); if ($all_identities && $raw) { throw new PhutilArgumentUsageException( pht( 'Flags "--all-identities" and "--raw" are not '. 'compatible.')); } $dry_run = $args->getArg('dry-run'); $this->dryRun = $dry_run; if ($this->dryRun) { $this->logWarn( pht('DRY RUN'), pht('This is a dry run, so no changes will be written.')); } if ($all_repositories || $repositories) { $rebuilt_anything = true; if ($repositories) { $repository_list = $this->loadRepositories($args, 'repository'); } else { $repository_query = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer); $repository_list = new PhabricatorQueryIterator($repository_query); } foreach ($repository_list as $repository) { $commit_query = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->needCommitData(true) ->withRepositoryIDs(array($repository->getID())); // See T13457. Adjust ordering to hit keys better and tweak page size // to improve performance slightly, since these records are small. $commit_query->setOrderVector(array('-epoch', '-id')); $commit_iterator = id(new PhabricatorQueryIterator($commit_query)) ->setPageSize(1000); $this->rebuildCommits($commit_iterator); } } $commits = $args->getArg('commit'); if ($commits) { $rebuilt_anything = true; $commit_list = $this->loadCommits($args, 'commit'); // Reload commits to get commit data. $commit_list = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->needCommitData(true) ->withIDs(mpull($commit_list, 'getID')) ->execute(); $this->rebuildCommits($commit_list); } $users = $args->getArg('user'); if ($users) { $rebuilt_anything = true; $user_list = $this->loadUsersFromArguments($users); $this->rebuildUsers($user_list); } $emails = $args->getArg('email'); if ($emails) { $rebuilt_anything = true; $this->rebuildEmails($emails); } if ($all_identities || $raw) { $rebuilt_anything = true; if ($raw) { $identities = id(new PhabricatorRepositoryIdentityQuery()) ->setViewer($viewer) ->withIdentityNames($raw) ->execute(); $identities = mpull($identities, null, 'getIdentityNameRaw'); foreach ($raw as $raw_identity) { if (!isset($identities[$raw_identity])) { throw new PhutilArgumentUsageException( pht( 'No identity "%s" exists. When selecting identities with '. '"--raw", the entire identity must match exactly.', $raw_identity)); } } $identity_list = $identities; } else { $identity_query = id(new PhabricatorRepositoryIdentityQuery()) ->setViewer($viewer); $identity_list = new PhabricatorQueryIterator($identity_query); $this->logInfo( pht('REBUILD'), pht('Rebuilding all existing identities.')); } $this->rebuildIdentities($identity_list); } if (!$rebuilt_anything) { throw new PhutilArgumentUsageException( pht( 'Nothing specified to rebuild. Use flags to choose which '. 'identities to rebuild, or "--help" for help.')); } return 0; } private function rebuildCommits($commits) { foreach ($commits as $commit) { $needs_update = false; $data = $commit->getCommitData(); - $author_name = $data->getAuthorName(); + $author = $data->getAuthorString(); $author_identity = $this->getIdentityForCommit( $commit, - $author_name); + $author); $author_phid = $commit->getAuthorIdentityPHID(); $identity_phid = $author_identity->getPHID(); $aidentity_phid = $identity_phid; if ($author_phid !== $identity_phid) { $commit->setAuthorIdentityPHID($identity_phid); $data->setCommitDetail('authorIdentityPHID', $identity_phid); $needs_update = true; } - $committer_name = $data->getCommitDetail('committer', null); + $committer_name = $data->getCommitterString(); $committer_phid = $commit->getCommitterIdentityPHID(); if (strlen($committer_name)) { $committer_identity = $this->getIdentityForCommit( $commit, $committer_name); $identity_phid = $committer_identity->getPHID(); } else { $identity_phid = null; } if ($committer_phid !== $identity_phid) { $commit->setCommitterIdentityPHID($identity_phid); $data->setCommitDetail('committerIdentityPHID', $identity_phid); $needs_update = true; } if ($needs_update) { $commit->save(); $data->save(); $this->logInfo( pht('COMMIT'), pht( 'Rebuilt identities for "%s".', $commit->getDisplayName())); } else { $this->logInfo( pht('SKIP'), pht( 'No changes for commit "%s".', $commit->getDisplayName())); } } } private function getIdentityForCommit( PhabricatorRepositoryCommit $commit, $raw_identity) { if (!isset($this->identityCache[$raw_identity])) { $identity = $this->newIdentityEngine() ->setSourcePHID($commit->getPHID()) ->newResolvedIdentity($raw_identity); $this->identityCache[$raw_identity] = $identity; } return $this->identityCache[$raw_identity]; } private function rebuildUsers($users) { $viewer = $this->getViewer(); foreach ($users as $user) { $this->logInfo( pht('USER'), pht( 'Rebuilding identities for user "%s".', $user->getMonogram())); $emails = id(new PhabricatorUserEmail())->loadAllWhere( 'userPHID = %s', $user->getPHID()); if ($emails) { $this->rebuildEmails(mpull($emails, 'getAddress')); } $identities = id(new PhabricatorRepositoryIdentityQuery()) ->setViewer($viewer) ->withRelatedPHIDs(array($user->getPHID())) ->execute(); if (!$identities) { $this->logWarn( pht('NO IDENTITIES'), pht('Found no identities directly related to user.')); continue; } $this->rebuildIdentities($identities); } } private function rebuildEmails($emails) { $viewer = $this->getViewer(); foreach ($emails as $email) { $this->logInfo( pht('EMAIL'), pht('Rebuilding identities for email address "%s".', $email)); $identities = id(new PhabricatorRepositoryIdentityQuery()) ->setViewer($viewer) ->withEmailAddresses(array($email)) ->execute(); if (!$identities) { $this->logWarn( pht('NO IDENTITIES'), pht('Found no identities for email address "%s".', $email)); continue; } $this->rebuildIdentities($identities); } } private function rebuildIdentities($identities) { $dry_run = $this->dryRun; foreach ($identities as $identity) { $raw_identity = $identity->getIdentityName(); if (isset($this->identityCache[$raw_identity])) { $this->logInfo( pht('SKIP'), pht( 'Identity "%s" has already been rebuilt.', $raw_identity)); continue; } $this->logInfo( pht('IDENTITY'), pht( 'Rebuilding identity "%s".', $raw_identity)); $old_auto = $identity->getAutomaticGuessedUserPHID(); $old_assign = $identity->getManuallySetUserPHID(); $identity = $this->newIdentityEngine() ->newUpdatedIdentity($identity); $this->identityCache[$raw_identity] = $identity; $new_auto = $identity->getAutomaticGuessedUserPHID(); $new_assign = $identity->getManuallySetUserPHID(); $same_auto = ($old_auto === $new_auto); $same_assign = ($old_assign === $new_assign); if ($same_auto && $same_assign) { $this->logInfo( pht('UNCHANGED'), pht('No changes to identity.')); } else { if (!$same_auto) { if ($dry_run) { $this->logWarn( pht('DETECTED PHID'), pht( '(Dry Run) Would update detected user from "%s" to "%s".', $this->renderPHID($old_auto), $this->renderPHID($new_auto))); } else { $this->logWarn( pht('DETECTED PHID'), pht( 'Detected user updated from "%s" to "%s".', $this->renderPHID($old_auto), $this->renderPHID($new_auto))); } } if (!$same_assign) { if ($dry_run) { $this->logWarn( pht('ASSIGNED PHID'), pht( '(Dry Run) Would update assigned user from "%s" to "%s".', $this->renderPHID($old_assign), $this->renderPHID($new_assign))); } else { $this->logWarn( pht('ASSIGNED PHID'), pht( 'Assigned user updated from "%s" to "%s".', $this->renderPHID($old_assign), $this->renderPHID($new_assign))); } } } } } private function renderPHID($phid) { if ($phid == null) { return pht('NULL'); } if (!isset($this->phidCache[$phid])) { $viewer = $this->getViewer(); $handles = $viewer->loadHandles(array($phid)); $this->phidCache[$phid] = pht( '%s <%s>', $handles[$phid]->getFullName(), $phid); } return $this->phidCache[$phid]; } private function newIdentityEngine() { $viewer = $this->getViewer(); return id(new DiffusionRepositoryIdentityEngine()) ->setViewer($viewer) ->setDryRun($this->dryRun); } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommit.php b/src/applications/repository/storage/PhabricatorRepositoryCommit.php index 7093b75370..ef65219b4b 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryCommit.php +++ b/src/applications/repository/storage/PhabricatorRepositoryCommit.php @@ -1,969 +1,964 @@ repository = $repository; return $this; } public function getRepository($assert_attached = true) { if ($assert_attached) { return $this->assertAttached($this->repository); } return $this->repository; } public function isPartiallyImported($mask) { return (($mask & $this->getImportStatus()) == $mask); } public function isImported() { return $this->isPartiallyImported(self::IMPORTED_ALL); } public function isUnreachable() { return $this->isPartiallyImported(self::IMPORTED_UNREACHABLE); } public function writeImportStatusFlag($flag) { return $this->adjustImportStatusFlag($flag, true); } public function clearImportStatusFlag($flag) { return $this->adjustImportStatusFlag($flag, false); } private function adjustImportStatusFlag($flag, $set) { $conn_w = $this->establishConnection('w'); $table_name = $this->getTableName(); $id = $this->getID(); if ($set) { queryfx( $conn_w, 'UPDATE %T SET importStatus = (importStatus | %d) WHERE id = %d', $table_name, $flag, $id); $this->setImportStatus($this->getImportStatus() | $flag); } else { queryfx( $conn_w, 'UPDATE %T SET importStatus = (importStatus & ~%d) WHERE id = %d', $table_name, $flag, $id); $this->setImportStatus($this->getImportStatus() & ~$flag); } return $this; } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_TIMESTAMPS => false, self::CONFIG_COLUMN_SCHEMA => array( 'commitIdentifier' => 'text40', 'authorPHID' => 'phid?', 'authorIdentityPHID' => 'phid?', 'committerIdentityPHID' => 'phid?', 'auditStatus' => 'text32', 'summary' => 'text255', 'importStatus' => 'uint32', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'repositoryID' => array( 'columns' => array('repositoryID', 'importStatus'), ), 'authorPHID' => array( 'columns' => array('authorPHID', 'auditStatus', 'epoch'), ), 'repositoryID_2' => array( 'columns' => array('repositoryID', 'epoch'), ), 'key_commit_identity' => array( 'columns' => array('commitIdentifier', 'repositoryID'), 'unique' => true, ), 'key_epoch' => array( 'columns' => array('epoch'), ), 'key_author' => array( 'columns' => array('authorPHID', 'epoch'), ), ), self::CONFIG_NO_MUTATE => array( 'importStatus', ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorRepositoryCommitPHIDType::TYPECONST); } public function loadCommitData() { if (!$this->getID()) { return null; } return id(new PhabricatorRepositoryCommitData())->loadOneWhere( 'commitID = %d', $this->getID()); } public function attachCommitData( PhabricatorRepositoryCommitData $data = null) { $this->commitData = $data; return $this; } public function getCommitData() { return $this->assertAttached($this->commitData); } public function attachAudits(array $audits) { assert_instances_of($audits, 'PhabricatorRepositoryAuditRequest'); $this->audits = $audits; return $this; } public function getAudits() { return $this->assertAttached($this->audits); } public function hasAttachedAudits() { return ($this->audits !== self::ATTACHABLE); } public function attachIdentities( PhabricatorRepositoryIdentity $author = null, PhabricatorRepositoryIdentity $committer = null) { $this->authorIdentity = $author; $this->committerIdentity = $committer; return $this; } public function getAuthorIdentity() { return $this->assertAttached($this->authorIdentity); } public function getCommitterIdentity() { return $this->assertAttached($this->committerIdentity); } public function attachAuditAuthority( PhabricatorUser $user, array $authority) { $user_phid = $user->getPHID(); if (!$user->getPHID()) { throw new Exception( pht('You can not attach audit authority for a user with no PHID.')); } $this->auditAuthorityPHIDs[$user_phid] = $authority; return $this; } public function hasAuditAuthority( PhabricatorUser $user, PhabricatorRepositoryAuditRequest $audit) { $user_phid = $user->getPHID(); if (!$user_phid) { return false; } $map = $this->assertAttachedKey($this->auditAuthorityPHIDs, $user_phid); return isset($map[$audit->getAuditorPHID()]); } public function writeOwnersEdges(array $package_phids) { $src_phid = $this->getPHID(); $edge_type = DiffusionCommitHasPackageEdgeType::EDGECONST; $editor = new PhabricatorEdgeEditor(); $dst_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $src_phid, $edge_type); foreach ($dst_phids as $dst_phid) { $editor->removeEdge($src_phid, $edge_type, $dst_phid); } foreach ($package_phids as $package_phid) { $editor->addEdge($src_phid, $edge_type, $package_phid); } $editor->save(); return $this; } public function getAuditorPHIDsForEdit() { $audits = $this->getAudits(); return mpull($audits, 'getAuditorPHID'); } public function delete() { $data = $this->loadCommitData(); $audits = id(new PhabricatorRepositoryAuditRequest()) ->loadAllWhere('commitPHID = %s', $this->getPHID()); $this->openTransaction(); if ($data) { $data->delete(); } foreach ($audits as $audit) { $audit->delete(); } $result = parent::delete(); $this->saveTransaction(); return $result; } public function getDateCreated() { // This is primarily to make analysis of commits with the Fact engine work. return $this->getEpoch(); } public function getURI() { return '/'.$this->getMonogram(); } /** * Synchronize a commit's overall audit status with the individual audit * triggers. */ public function updateAuditStatus(array $requests) { assert_instances_of($requests, 'PhabricatorRepositoryAuditRequest'); $any_concern = false; $any_accept = false; $any_need = false; foreach ($requests as $request) { switch ($request->getAuditStatus()) { case PhabricatorAuditStatusConstants::AUDIT_REQUIRED: case PhabricatorAuditStatusConstants::AUDIT_REQUESTED: $any_need = true; break; case PhabricatorAuditStatusConstants::ACCEPTED: $any_accept = true; break; case PhabricatorAuditStatusConstants::CONCERNED: $any_concern = true; break; } } if ($any_concern) { if ($this->isAuditStatusNeedsVerification()) { // If the change is in "Needs Verification", we keep it there as // long as any auditors still have concerns. $status = DiffusionCommitAuditStatus::NEEDS_VERIFICATION; } else { $status = DiffusionCommitAuditStatus::CONCERN_RAISED; } } else if ($any_accept) { if ($any_need) { $status = DiffusionCommitAuditStatus::PARTIALLY_AUDITED; } else { $status = DiffusionCommitAuditStatus::AUDITED; } } else if ($any_need) { $status = DiffusionCommitAuditStatus::NEEDS_AUDIT; } else { $status = DiffusionCommitAuditStatus::NONE; } return $this->setAuditStatus($status); } public function getMonogram() { $repository = $this->getRepository(); $callsign = $repository->getCallsign(); $identifier = $this->getCommitIdentifier(); if ($callsign !== null) { return "r{$callsign}{$identifier}"; } else { $id = $repository->getID(); return "R{$id}:{$identifier}"; } } public function getDisplayName() { $repository = $this->getRepository(); $identifier = $this->getCommitIdentifier(); return $repository->formatCommitName($identifier); } /** * Return a local display name for use in the context of the containing * repository. * * In Git and Mercurial, this returns only a short hash, like "abcdef012345". * See @{method:getDisplayName} for a short name that always includes * repository context. * * @return string Short human-readable name for use inside a repository. */ public function getLocalName() { $repository = $this->getRepository(); $identifier = $this->getCommitIdentifier(); return $repository->formatCommitName($identifier, $local = true); } public function loadIdentities(PhabricatorUser $viewer) { if ($this->authorIdentity !== self::ATTACHABLE) { return $this; } $commit = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withIDs(array($this->getID())) ->needIdentities(true) ->executeOne(); $author_identity = $commit->getAuthorIdentity(); $committer_identity = $commit->getCommitterIdentity(); return $this->attachIdentities($author_identity, $committer_identity); } public function hasCommitterIdentity() { return ($this->getCommitterIdentity() !== null); } public function hasAuthorIdentity() { return ($this->getAuthorIdentity() !== null); } public function getCommitterDisplayPHID() { if ($this->hasCommitterIdentity()) { return $this->getCommitterIdentity()->getIdentityDisplayPHID(); } $data = $this->getCommitData(); return $data->getCommitDetail('committerPHID'); } public function getAuthorDisplayPHID() { if ($this->hasAuthorIdentity()) { return $this->getAuthorIdentity()->getIdentityDisplayPHID(); } $data = $this->getCommitData(); return $data->getCommitDetail('authorPHID'); } public function getEffectiveAuthorPHID() { if ($this->hasAuthorIdentity()) { $identity = $this->getAuthorIdentity(); if ($identity->hasEffectiveUser()) { return $identity->getCurrentEffectiveUserPHID(); } } $data = $this->getCommitData(); return $data->getCommitDetail('authorPHID'); } public function getAuditStatusObject() { $status = $this->getAuditStatus(); return DiffusionCommitAuditStatus::newForStatus($status); } public function isAuditStatusNoAudit() { return $this->getAuditStatusObject()->isNoAudit(); } public function isAuditStatusNeedsAudit() { return $this->getAuditStatusObject()->isNeedsAudit(); } public function isAuditStatusConcernRaised() { return $this->getAuditStatusObject()->isConcernRaised(); } public function isAuditStatusNeedsVerification() { return $this->getAuditStatusObject()->isNeedsVerification(); } public function isAuditStatusPartiallyAudited() { return $this->getAuditStatusObject()->isPartiallyAudited(); } public function isAuditStatusAudited() { return $this->getAuditStatusObject()->isAudited(); } public function isPermanentCommit() { return (bool)$this->isPartiallyImported(self::IMPORTED_CLOSEABLE); } public function newCommitAuthorView(PhabricatorUser $viewer) { $author_phid = $this->getAuthorDisplayPHID(); if ($author_phid) { $handles = $viewer->loadHandles(array($author_phid)); return $handles[$author_phid]->renderLink(); } $author = $this->getRawAuthorStringForDisplay(); if (strlen($author)) { return DiffusionView::renderName($author); } return null; } public function newCommitCommitterView(PhabricatorUser $viewer) { $committer_phid = $this->getCommitterDisplayPHID(); if ($committer_phid) { $handles = $viewer->loadHandles(array($committer_phid)); return $handles[$committer_phid]->renderLink(); } $committer = $this->getRawCommitterStringForDisplay(); if (strlen($committer)) { return DiffusionView::renderName($committer); } return null; } public function isAuthorSameAsCommitter() { $author_phid = $this->getAuthorDisplayPHID(); $committer_phid = $this->getCommitterDisplayPHID(); if ($author_phid && $committer_phid) { return ($author_phid === $committer_phid); } if ($author_phid || $committer_phid) { return false; } $author = $this->getRawAuthorStringForDisplay(); $committer = $this->getRawCommitterStringForDisplay(); return ($author === $committer); } private function getRawAuthorStringForDisplay() { $data = $this->getCommitData(); - return $data->getAuthorName(); + return $data->getAuthorString(); } private function getRawCommitterStringForDisplay() { $data = $this->getCommitData(); - return $data->getCommitDetail('committer'); + return $data->getCommitterString(); } public function newCommitRef(PhabricatorUser $viewer) { $repository = $this->getRepository(); $future = $repository->newConduitFuture( $viewer, 'internal.commit.search', array( 'constraints' => array( 'repositoryPHIDs' => array($repository->getPHID()), 'phids' => array($this->getPHID()), ), )); $result = $future->resolve(); $commit_display = $this->getMonogram(); if (empty($result['data'])) { throw new Exception( pht( 'Unable to retrieve details for commit "%s"!', $commit_display)); } if (count($result['data']) !== 1) { throw new Exception( pht( 'Got too many results (%s) for commit "%s", expected %s.', phutil_count($result['data']), $commit_display, 1)); } $record = head($result['data']); $ref_record = idxv($record, array('fields', 'ref')); if (!$ref_record) { throw new Exception( pht( 'Unable to retrieve CommitRef record for commit "%s".', $commit_display)); } return DiffusionCommitRef::newFromDictionary($ref_record); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getRepository()->getPolicy($capability); case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_USER; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getRepository()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht( 'Commits inherit the policies of the repository they belong to.'); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } /* -( Stuff for serialization )---------------------------------------------- */ /** * NOTE: this is not a complete serialization; only the 'protected' fields are * involved. This is due to ease of (ab)using the Lisk abstraction to get this * done, as well as complexity of the other fields. */ public function toDictionary() { return array( 'repositoryID' => $this->getRepositoryID(), 'phid' => $this->getPHID(), 'commitIdentifier' => $this->getCommitIdentifier(), 'epoch' => $this->getEpoch(), 'authorPHID' => $this->getAuthorPHID(), 'auditStatus' => $this->getAuditStatus(), 'summary' => $this->getSummary(), 'importStatus' => $this->getImportStatus(), ); } public static function newFromDictionary(array $dict) { return id(new PhabricatorRepositoryCommit()) ->loadFromArray($dict); } /* -( HarbormasterBuildableInterface )------------------------------------- */ public function getHarbormasterBuildableDisplayPHID() { return $this->getHarbormasterBuildablePHID(); } public function getHarbormasterBuildablePHID() { return $this->getPHID(); } public function getHarbormasterContainerPHID() { return $this->getRepository()->getPHID(); } public function getBuildVariables() { $results = array(); $results['buildable.commit'] = $this->getCommitIdentifier(); $repo = $this->getRepository(); $results['repository.callsign'] = $repo->getCallsign(); $results['repository.phid'] = $repo->getPHID(); $results['repository.vcs'] = $repo->getVersionControlSystem(); $results['repository.uri'] = $repo->getPublicCloneURI(); return $results; } public function getAvailableBuildVariables() { return array( 'buildable.commit' => pht('The commit identifier, if applicable.'), 'repository.callsign' => pht('The callsign of the repository in Phabricator.'), 'repository.phid' => pht('The PHID of the repository in Phabricator.'), 'repository.vcs' => pht('The version control system, either "svn", "hg" or "git".'), 'repository.uri' => pht('The URI to clone or checkout the repository from.'), ); } public function newBuildableEngine() { return new DiffusionBuildableEngine(); } /* -( HarbormasterCircleCIBuildableInterface )----------------------------- */ public function getCircleCIGitHubRepositoryURI() { $repository = $this->getRepository(); $commit_phid = $this->getPHID(); $repository_phid = $repository->getPHID(); if ($repository->isHosted()) { throw new Exception( pht( 'This commit ("%s") is associated with a hosted repository '. '("%s"). Repositories must be imported from GitHub to be built '. 'with CircleCI.', $commit_phid, $repository_phid)); } $remote_uri = $repository->getRemoteURI(); $path = HarbormasterCircleCIBuildStepImplementation::getGitHubPath( $remote_uri); if (!$path) { throw new Exception( pht( 'This commit ("%s") is associated with a repository ("%s") that '. 'with a remote URI ("%s") that does not appear to be hosted on '. 'GitHub. Repositories must be hosted on GitHub to be built with '. 'CircleCI.', $commit_phid, $repository_phid, $remote_uri)); } return $remote_uri; } public function getCircleCIBuildIdentifierType() { return 'revision'; } public function getCircleCIBuildIdentifier() { return $this->getCommitIdentifier(); } /* -( HarbormasterBuildkiteBuildableInterface )---------------------------- */ public function getBuildkiteBranch() { $viewer = PhabricatorUser::getOmnipotentUser(); $repository = $this->getRepository(); $branches = DiffusionQuery::callConduitWithDiffusionRequest( $viewer, DiffusionRequest::newFromDictionary( array( 'repository' => $repository, 'user' => $viewer, )), 'diffusion.branchquery', array( 'contains' => $this->getCommitIdentifier(), 'repository' => $repository->getPHID(), )); if (!$branches) { throw new Exception( pht( 'Commit "%s" is not an ancestor of any branch head, so it can not '. 'be built with Buildkite.', $this->getCommitIdentifier())); } $branch = head($branches); return 'refs/heads/'.$branch['shortName']; } public function getBuildkiteCommit() { return $this->getCommitIdentifier(); } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('diffusion.fields'); } public function getCustomFieldBaseClass() { return 'PhabricatorCommitCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { // TODO: This should also list auditors, but handling that is a bit messy // right now because we are not guaranteed to have the data. (It should not // include resigned auditors.) return ($phid == $this->getAuthorPHID()); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorAuditEditor(); } public function getApplicationTransactionTemplate() { return new PhabricatorAuditTransaction(); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new DiffusionCommitFulltextEngine(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new DiffusionCommitFerretEngine(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('identifier') ->setType('string') ->setDescription(pht('The commit identifier.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('repositoryPHID') ->setType('phid') ->setDescription(pht('The repository this commit belongs to.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('author') ->setType('map') ->setDescription(pht('Information about the commit author.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('committer') ->setType('map') ->setDescription(pht('Information about the committer.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('isImported') ->setType('bool') ->setDescription(pht('True if the commit is fully imported.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('isUnreachable') ->setType('bool') ->setDescription( pht( 'True if the commit is not the ancestor of any tag, branch, or '. 'ref.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('auditStatus') ->setType('map') ->setDescription(pht('Information about the current audit status.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('message') ->setType('string') ->setDescription(pht('The commit message.')), ); } public function getFieldValuesForConduit() { $data = $this->getCommitData(); $author_identity = $this->getAuthorIdentity(); if ($author_identity) { $author_name = $author_identity->getIdentityDisplayName(); $author_email = $author_identity->getIdentityEmailAddress(); $author_raw = $author_identity->getIdentityName(); $author_identity_phid = $author_identity->getPHID(); $author_user_phid = $author_identity->getCurrentEffectiveUserPHID(); } else { $author_name = null; $author_email = null; $author_raw = null; $author_identity_phid = null; $author_user_phid = null; } $committer_identity = $this->getCommitterIdentity(); if ($committer_identity) { $committer_name = $committer_identity->getIdentityDisplayName(); $committer_email = $committer_identity->getIdentityEmailAddress(); $committer_raw = $committer_identity->getIdentityName(); $committer_identity_phid = $committer_identity->getPHID(); $committer_user_phid = $committer_identity->getCurrentEffectiveUserPHID(); } else { $committer_name = null; $committer_email = null; $committer_raw = null; $committer_identity_phid = null; $committer_user_phid = null; } - $author_epoch = $data->getCommitDetail('authorEpoch'); - if ($author_epoch) { - $author_epoch = (int)$author_epoch; - } else { - $author_epoch = null; - } + $author_epoch = $data->getAuthorEpoch(); $audit_status = $this->getAuditStatusObject(); return array( 'identifier' => $this->getCommitIdentifier(), 'repositoryPHID' => $this->getRepository()->getPHID(), 'author' => array( 'name' => $author_name, 'email' => $author_email, 'raw' => $author_raw, 'epoch' => $author_epoch, 'identityPHID' => $author_identity_phid, 'userPHID' => $author_user_phid, ), 'committer' => array( 'name' => $committer_name, 'email' => $committer_email, 'raw' => $committer_raw, 'epoch' => (int)$this->getEpoch(), 'identityPHID' => $committer_identity_phid, 'userPHID' => $committer_user_phid, ), 'isUnreachable' => (bool)$this->isUnreachable(), 'isImported' => (bool)$this->isImported(), 'auditStatus' => array( 'value' => $audit_status->getKey(), 'name' => $audit_status->getName(), 'closed' => (bool)$audit_status->getIsClosed(), 'color.ansi' => $audit_status->getAnsiColor(), ), 'message' => $data->getCommitMessage(), ); } public function getConduitSearchAttachments() { return array(); } /* -( PhabricatorDraftInterface )------------------------------------------ */ public function newDraftEngine() { return new DiffusionCommitDraftEngine(); } 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; } /* -( PhabricatorTimelineInterface )--------------------------------------- */ public function newTimelineEngine() { return new DiffusionCommitTimelineEngine(); } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommitData.php b/src/applications/repository/storage/PhabricatorRepositoryCommitData.php index f60784fc01..84f9522785 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryCommitData.php +++ b/src/applications/repository/storage/PhabricatorRepositoryCommitData.php @@ -1,104 +1,146 @@ false, self::CONFIG_SERIALIZATION => array( 'commitDetails' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'authorName' => 'text', 'commitMessage' => 'text', ), self::CONFIG_KEY_SCHEMA => array( 'commitID' => array( 'columns' => array('commitID'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function getSummary() { $message = $this->getCommitMessage(); return self::summarizeCommitMessage($message); } public static function summarizeCommitMessage($message) { $max_bytes = id(new PhabricatorRepositoryCommit()) ->getColumnMaximumByteLength('summary'); $summary = phutil_split_lines($message, $retain_endings = false); $summary = head($summary); $summary = id(new PhutilUTF8StringTruncator()) ->setMaximumBytes($max_bytes) ->setMaximumGlyphs(80) ->truncateString($summary); return $summary; } public function getCommitDetail($key, $default = null) { return idx($this->commitDetails, $key, $default); } public function setCommitDetail($key, $value) { $this->commitDetails[$key] = $value; return $this; } public function toDictionary() { return array( 'commitID' => $this->commitID, 'authorName' => $this->authorName, 'commitMessage' => $this->commitMessage, 'commitDetails' => json_encode($this->commitDetails), ); } public static function newFromDictionary(array $dict) { return id(new PhabricatorRepositoryCommitData()) ->loadFromArray($dict); } public function newPublisherHoldReasons() { $holds = $this->getCommitDetail('holdReasons'); // Look for the legacy "autocloseReason" if we don't have a modern list // of hold reasons. if (!$holds) { $old_hold = $this->getCommitDetail('autocloseReason'); if ($old_hold) { $holds = array($old_hold); } } if (!$holds) { $holds = array(); } foreach ($holds as $key => $reason) { $holds[$key] = PhabricatorRepositoryPublisherHoldReason::newForHoldKey( $reason); } return array_values($holds); } - public function setCommitRef(DiffusionCommitRef $ref) { - $this->setCommitDetail('commitRef', $ref->newDictionary()); + public function getAuthorString() { + $author = phutil_string_cast($this->authorName); + + if (strlen($author)) { + return $author; + } + + return null; } - public function newCommitRef() { - $map = $this->getCommitDetail('commitRef', array()); - return DiffusionCommitRef::neWFromDictionary($map); + public function getAuthorDisplayName() { + return $this->getCommitDetailString('authorName'); + } + + public function getAuthorEmail() { + return $this->getCommitDetailString('authorEmail'); + } + + public function getAuthorEpoch() { + $epoch = $this->getCommitDetail('authorEpoch'); + + if ($epoch) { + return (int)$epoch; + } + + return null; + } + + public function getCommitterString() { + return $this->getCommitDetailString('committer'); + } + + public function getCommitterDisplayName() { + return $this->getCommitDetailString('committerName'); + } + + public function getCommitterEmail() { + return $this->getCommitDetailString('committerEmail'); + } + + private function getCommitDetailString($key) { + $string = $this->getCommitDetail($key); + $string = phutil_string_cast($string); + + if (strlen($string)) { + return $string; + } + + return null; } } diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitPublishWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitPublishWorker.php index 0b0a194806..3a555e7075 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryCommitPublishWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryCommitPublishWorker.php @@ -1,394 +1,394 @@ shouldSkipImportStep()) { $this->publishCommit($repository, $commit); $commit->writeImportStatusFlag($this->getImportStepFlag()); } // This is the last task in the sequence, so we don't need to queue any // followup workers. } private function publishCommit( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { $viewer = PhabricatorUser::getOmnipotentUser(); $publisher = $repository->newPublisher(); if (!$publisher->shouldPublishCommit($commit)) { return; } $commit_phid = $commit->getPHID(); // Reload the commit to get the commit data, identities, and any // outstanding audit requests. $commit = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withPHIDs(array($commit_phid)) ->needCommitData(true) ->needIdentities(true) ->needAuditRequests(true) ->executeOne(); if (!$commit) { throw new PhabricatorWorkerPermanentFailureException( pht( 'Failed to reload commit "%s".', $commit_phid)); } $xactions = array( $this->newAuditTransactions($commit), $this->newPublishTransactions($commit), ); $xactions = array_mergev($xactions); $acting_phid = $this->getPublishAsPHID($commit); $content_source = $this->newContentSource(); $revision = DiffusionCommitRevisionQuery::loadRevisionForCommit( $viewer, $commit); // Prevent the commit from generating a mention of the associated // revision, if one exists, so we don't double up because of the URI // in the commit message. $unmentionable_phids = array(); if ($revision) { $unmentionable_phids[] = $revision->getPHID(); } $editor = $commit->getApplicationTransactionEditor() ->setActor($viewer) ->setActingAsPHID($acting_phid) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->setContentSource($content_source) ->addUnmentionablePHIDs($unmentionable_phids); try { $raw_patch = $this->loadRawPatchText($repository, $commit); } catch (Exception $ex) { $raw_patch = pht('Unable to generate patch: %s', $ex->getMessage()); } $editor->setRawPatch($raw_patch); $editor->applyTransactions($commit, $xactions); } private function getPublishAsPHID(PhabricatorRepositoryCommit $commit) { if ($commit->hasCommitterIdentity()) { return $commit->getCommitterIdentity()->getIdentityDisplayPHID(); } if ($commit->hasAuthorIdentity()) { return $commit->getAuthorIdentity()->getIdentityDisplayPHID(); } return id(new PhabricatorDiffusionApplication())->getPHID(); } private function newPublishTransactions(PhabricatorRepositoryCommit $commit) { $data = $commit->getCommitData(); $xactions = array(); $xactions[] = $commit->getApplicationTransactionTemplate() ->setTransactionType(PhabricatorAuditTransaction::TYPE_COMMIT) ->setDateCreated($commit->getEpoch()) ->setNewValue( array( 'description' => $data->getCommitMessage(), 'summary' => $data->getSummary(), - 'authorName' => $data->getAuthorName(), + 'authorName' => $data->getAuthorString(), 'authorPHID' => $commit->getAuthorPHID(), - 'committerName' => $data->getCommitDetail('committer'), + 'committerName' => $data->getCommitterString(), 'committerPHID' => $data->getCommitDetail('committerPHID'), )); return $xactions; } private function newAuditTransactions(PhabricatorRepositoryCommit $commit) { $viewer = PhabricatorUser::getOmnipotentUser(); $repository = $commit->getRepository(); $affected_paths = PhabricatorOwnerPathQuery::loadAffectedPaths( $repository, $commit, PhabricatorUser::getOmnipotentUser()); $affected_packages = PhabricatorOwnersPackage::loadAffectedPackages( $repository, $affected_paths); $commit->writeOwnersEdges(mpull($affected_packages, 'getPHID')); if (!$affected_packages) { return array(); } $data = $commit->getCommitData(); $author_phid = $commit->getEffectiveAuthorPHID(); $revision = DiffusionCommitRevisionQuery::loadRevisionForCommit( $viewer, $commit); $requests = $commit->getAudits(); $requests = mpull($requests, null, 'getAuditorPHID'); $auditor_phids = array(); foreach ($affected_packages as $package) { $request = idx($requests, $package->getPHID()); if ($request) { // Don't update request if it exists already. continue; } $should_audit = $this->shouldTriggerAudit( $commit, $package, $author_phid, $revision); if (!$should_audit) { continue; } $auditor_phids[] = $package->getPHID(); } // If none of the packages are triggering audits, we're all done. if (!$auditor_phids) { return array(); } $audit_type = DiffusionCommitAuditorsTransaction::TRANSACTIONTYPE; $xactions = array(); $xactions[] = $commit->getApplicationTransactionTemplate() ->setTransactionType($audit_type) ->setNewValue( array( '+' => array_fuse($auditor_phids), )); return $xactions; } private function shouldTriggerAudit( PhabricatorRepositoryCommit $commit, PhabricatorOwnersPackage $package, $author_phid, $revision) { $audit_uninvolved = false; $audit_unreviewed = false; $rule = $package->newAuditingRule(); switch ($rule->getKey()) { case PhabricatorOwnersAuditRule::AUDITING_NONE: return false; case PhabricatorOwnersAuditRule::AUDITING_ALL: return true; case PhabricatorOwnersAuditRule::AUDITING_NO_OWNER: $audit_uninvolved = true; break; case PhabricatorOwnersAuditRule::AUDITING_UNREVIEWED: $audit_unreviewed = true; break; case PhabricatorOwnersAuditRule::AUDITING_NO_OWNER_AND_UNREVIEWED: $audit_uninvolved = true; $audit_unreviewed = true; break; } // If auditing is configured to trigger on unreviewed changes, check if // the revision was "Accepted" when it landed. If not, trigger an audit. // We may be running before the revision actually closes, so we'll count // either an "Accepted" or a "Closed, Previously Accepted" revision as // good enough. if ($audit_unreviewed) { $commit_unreviewed = true; if ($revision) { if ($revision->isAccepted()) { $commit_unreviewed = false; } else { $was_accepted = DifferentialRevision::PROPERTY_CLOSED_FROM_ACCEPTED; if ($revision->isPublished()) { if ($revision->getProperty($was_accepted)) { $commit_unreviewed = false; } } } } if ($commit_unreviewed) { return true; } } // If auditing is configured to trigger on changes with no involved owner, // check for an owner. If we don't find one, trigger an audit. if ($audit_uninvolved) { $owner_involved = $this->isOwnerInvolved( $commit, $package, $author_phid, $revision); if (!$owner_involved) { return true; } } // We can't find any reason to trigger an audit for this commit. return false; } private function isOwnerInvolved( PhabricatorRepositoryCommit $commit, PhabricatorOwnersPackage $package, $author_phid, $revision) { $owner_phids = PhabricatorOwnersOwner::loadAffiliatedUserPHIDs( array( $package->getID(), )); $owner_phids = array_fuse($owner_phids); // For the purposes of deciding whether the owners were involved in the // revision or not, consider a review by the package itself to count as // involvement. This can happen when human reviewers force-accept on // behalf of packages they don't own but have authority over. $owner_phids[$package->getPHID()] = $package->getPHID(); // If the commit author is identifiable and a package owner, they're // involved. if ($author_phid) { if (isset($owner_phids[$author_phid])) { return true; } } // Otherwise, we need to find an owner as a reviewer. // If we don't have a revision, this is hopeless: no owners are involved. if (!$revision) { return true; } $accepted_statuses = array( DifferentialReviewerStatus::STATUS_ACCEPTED, DifferentialReviewerStatus::STATUS_ACCEPTED_OLDER, ); $accepted_statuses = array_fuse($accepted_statuses); $found_accept = false; foreach ($revision->getReviewers() as $reviewer) { $reviewer_phid = $reviewer->getReviewerPHID(); // If this reviewer isn't a package owner or the package itself, // just ignore them. if (empty($owner_phids[$reviewer_phid])) { continue; } // If this reviewer accepted the revision and owns the package (or is // the package), we've found an involved owner. if (isset($accepted_statuses[$reviewer->getReviewerStatus()])) { $found_accept = true; break; } } if ($found_accept) { return true; } return false; } private function loadRawPatchText( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { $viewer = PhabricatorUser::getOmnipotentUser(); $identifier = $commit->getCommitIdentifier(); $drequest = DiffusionRequest::newFromDictionary( array( 'user' => $viewer, 'repository' => $repository, )); $time_key = 'metamta.diffusion.time-limit'; $byte_key = 'metamta.diffusion.byte-limit'; $time_limit = PhabricatorEnv::getEnvConfig($time_key); $byte_limit = PhabricatorEnv::getEnvConfig($byte_key); $diff_info = DiffusionQuery::callConduitWithDiffusionRequest( $viewer, $drequest, 'diffusion.rawdiffquery', array( 'commit' => $identifier, 'linesOfContext' => 3, 'timeout' => $time_limit, 'byteLimit' => $byte_limit, )); if ($diff_info['tooSlow']) { throw new Exception( pht( 'Patch generation took longer than configured limit ("%s") of '. '%s second(s).', $time_key, new PhutilNumber($time_limit))); } if ($diff_info['tooHuge']) { $pretty_limit = phutil_format_bytes($byte_limit); throw new Exception( pht( 'Patch size exceeds configured byte size limit ("%s") of %s.', $byte_key, $pretty_limit)); } $file_phid = $diff_info['filePHID']; $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($file_phid)) ->executeOne(); if (!$file) { throw new Exception( pht( 'Failed to load file ("%s") returned by "%s".', $file_phid, 'diffusion.rawdiffquery')); } return $file->loadFileData(); } }