diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -600,6 +600,7 @@ 'DiffusionCommitRevisionReviewersHeraldField' => 'applications/diffusion/herald/DiffusionCommitRevisionReviewersHeraldField.php', 'DiffusionCommitRevisionSubscribersHeraldField' => 'applications/diffusion/herald/DiffusionCommitRevisionSubscribersHeraldField.php', 'DiffusionCommitTagsController' => 'applications/diffusion/controller/DiffusionCommitTagsController.php', + 'DiffusionCompareController' => 'applications/diffusion/controller/DiffusionCompareController.php', 'DiffusionConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionConduitAPIMethod.php', 'DiffusionController' => 'applications/diffusion/controller/DiffusionController.php', 'DiffusionCreateCommentConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionCreateCommentConduitAPIMethod.php', @@ -757,6 +758,7 @@ 'DiffusionRequest' => 'applications/diffusion/request/DiffusionRequest.php', 'DiffusionResolveRefsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionResolveRefsConduitAPIMethod.php', 'DiffusionResolveUserQuery' => 'applications/diffusion/query/DiffusionResolveUserQuery.php', + 'DiffusionRevlistConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRevlistConduitAPIMethod.php', 'DiffusionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSSHWorkflow.php', 'DiffusionSearchQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionSearchQueryConduitAPIMethod.php', 'DiffusionServeController' => 'applications/diffusion/controller/DiffusionServeController.php', @@ -4675,6 +4677,7 @@ 'DiffusionCommitRevisionReviewersHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitRevisionSubscribersHeraldField' => 'DiffusionCommitHeraldField', 'DiffusionCommitTagsController' => 'DiffusionController', + 'DiffusionCompareController' => 'DiffusionController', 'DiffusionConduitAPIMethod' => 'ConduitAPIMethod', 'DiffusionController' => 'PhabricatorController', 'DiffusionCreateCommentConduitAPIMethod' => 'DiffusionConduitAPIMethod', @@ -4832,6 +4835,7 @@ 'DiffusionRequest' => 'Phobject', 'DiffusionResolveRefsConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionResolveUserQuery' => 'Phobject', + 'DiffusionRevlistConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionSSHWorkflow' => 'PhabricatorSSHWorkflow', 'DiffusionSearchQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionServeController' => 'DiffusionController', diff --git a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php --- a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php +++ b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php @@ -77,6 +77,7 @@ 'browse/(?P.*)' => 'DiffusionBrowseController', 'lastmodified/(?P.*)' => 'DiffusionLastModifiedController', 'diff/' => 'DiffusionDiffController', + 'compare/' => 'DiffusionCompareController', 'tags/(?P.*)' => 'DiffusionTagListController', 'branches/(?P.*)' => 'DiffusionBranchTableController', 'refs/(?P.*)' => 'DiffusionRefTableController', diff --git a/src/applications/diffusion/conduit/DiffusionRevlistConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionRevlistConduitAPIMethod.php new file mode 100644 --- /dev/null +++ b/src/applications/diffusion/conduit/DiffusionRevlistConduitAPIMethod.php @@ -0,0 +1,115 @@ + 'required string', + 'onto' => 'required string', + 'path' => 'optional string', + 'offset' => 'required int', + 'limit' => 'required int', + ); + } + + protected function getResult(ConduitAPIRequest $request) { + $path_changes = parent::getResult($request); + + return array( + 'pathChanges' => mpull($path_changes, 'toDictionary'), + 'parents' => $this->parents, + 'cherryPicked' => $this->cherryPicked, + 'mergeBase' => $this->mergeBase, + 'commitsCount' => $this->listCount, + 'reversedComparisonSize' => $this->reverseListCount, + ); + } + + protected function getGitResult(ConduitAPIRequest $request) { + $drequest = $this->getDiffusionRequest(); + $repository = $drequest->getRepository(); + $from = $request->getValue('from'); + $onto = $request->getValue('onto'); + $path = $request->getValue('path', ''); + $offset = $request->getValue('offset'); + $limit = $request->getValue('limit'); + + + list($stdout) = $repository->execxLocalCommand( + 'rev-list '. + '--skip=%d '. + '-n %d '. + '--parents '. + '--left-only '. + '%s...%s -- %C', + $offset, + $limit, + $from, + $onto, + // Git omits merge commits if the path is provided, even if it is empty. + (strlen($path) ? csprintf('%s', $path) : '')); + + $lines = explode("\n", trim($stdout)); + $lines = array_filter($lines); + if (!$lines) { + return array(); + } + + $hash_list = array(); + $parent_map = array(); + $cherry_picked = array(); + foreach ($lines as $line) { + list($hash, $parents) = explode(' ', $line, 2); + $hash_list[] = $hash; + $parent_map[$hash] = preg_split('/\s+/', $parents); + } + + $this->parents = $parent_map; + $this->cherryPicked = $cherry_picked; + + list($stdout) = $repository->execxLocalCommand( + 'rev-list '. + '--count '. + '--left-right '. + '%s...%s -- %C', + $from, + $onto, + // Git omits merge commits if the path is provided, even if it is empty. + (strlen($path) ? csprintf('%s', $path) : '')); + + list($count, $reverse_count) = preg_split('/\s+/', $stdout, 2); + $this->listCount = $count; + $this->reverseListCount = $reverse_count; + + list($stdout) = $repository->execxLocalCommand( + 'merge-base '. + '%s %s', + $from, + $onto); + + $this->mergeBase = $stdout; + + return DiffusionQuery::loadHistoryForCommitIdentifiers( + $hash_list, + $drequest); + } +} diff --git a/src/applications/diffusion/controller/DiffusionCompareController.php b/src/applications/diffusion/controller/DiffusionCompareController.php new file mode 100644 --- /dev/null +++ b/src/applications/diffusion/controller/DiffusionCompareController.php @@ -0,0 +1,251 @@ +loadDiffusionContext(); + if ($response) { + return $response; + } + + $viewer = $this->getViewer(); + $drequest = $this->getDiffusionRequest(); + $repository = $drequest->getRepository(); + + $content = array(); + + $crumbs = $this->buildCrumbs(array('view' => 'compare')); + + $empty_title = null; + $error_message = null; + + if ($repository->getVersionControlSystem() != + PhabricatorRepositoryType::REPOSITORY_TYPE_GIT) { + $empty_title = pht('Not supported'); + $error_message = pht( + 'This feature is not yet supported for %s repositories.', + $repository->getVersionControlSystem()); + $content[] = id(new PHUIInfoView()) + ->setTitle($empty_title) + ->setSeverity(PHUIInfoView::SEVERITY_WARNING) + ->setErrors(array($error_message)); + } + + $from_ref = $request->getStr('from'); + $onto_ref = $request->getStr('onto'); + + $refs = id(new DiffusionCachedResolveRefsQuery()) + ->setRepository($repository) + ->withRefs(array($from_ref, $onto_ref)) + ->execute(); + + + if (count($refs) == 2) { + $content[] = $this->buildCompareContent($drequest); + } else { + $content[] = $this->buildCompareForm($request, $refs); + } + + + return $this->newPage() + ->setTitle( + array( + $repository->getName(), + $repository->getDisplayName(), + )) + ->setCrumbs($crumbs) + ->appendChild($content); + } + + private function buildCompareForm(AphrontRequest $request, array $resolved) { + $viewer = $this->getViewer(); + + $from_ref = $request->getStr('from'); + $onto_ref = $request->getStr('onto'); + + if (idx($resolved, $from_ref)) { + $e_from = null; + } else { + $e_from = 'Not Found'; + } + + if (idx($resolved, $onto_ref)) { + $e_onto = null; + } else { + $e_onto = 'Not Found'; + } + + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->setMethod('GET') + ->appendControl( + id(new AphrontFormTextControl()) + ->setLabel(pht('From')) + ->setName('from') + ->setError($e_from) + ->setValue($from_ref)) + ->appendControl( + id(new AphrontFormTextControl()) + ->setLabel(pht('Onto')) + ->setName('onto') + ->setError($e_onto) + ->setValue($onto_ref)) + ->appendControl( + id(new AphrontFormSubmitControl()) + ->setValue('Compare')); + + return $form; + } + + private function buildCompareContent(DiffusionRequest $drequest) { + $request = $this->getRequest(); + $repository = $drequest->getRepository(); + + $from_ref = $request->getStr('from'); + $onto_ref = $request->getStr('onto'); + + $content = array(); + + try { + $revlist = $this->callConduitWithDiffusionRequest( + 'diffusion.revlist', + array( + 'from' => $from_ref, + 'onto' => $onto_ref, + 'path' => $drequest->getPath(), + 'offset' => 0, + 'limit' => 100, + )); + $history = DiffusionPathChange::newFromConduit( + $revlist['pathChanges']); + + $history_exception = null; + } catch (Exception $ex) { + $revlist = null; + $history = null; + $history_exception = $ex; + } + + + $content[] = $this->buildCompareProperties($drequest, $revlist); + $content[] = $this->buildCompareForm( + $request, + array($from_ref => true, $onto_ref => true)); + + $content[] = $this->buildHistoryTable( + $revlist, + $history, + $history_exception); + + return $content; + } + + private function buildCompareProperties($drequest, $revlist) { + $viewer = $this->getViewer(); + + $request = $this->getRequest(); + $repository = $drequest->getRepository(); + + $from_ref = $request->getStr('from'); + $onto_ref = $request->getStr('onto'); + + $reverse_uri = $repository->getPathURI( + "compare/?from=${onto_ref}&onto=${from_ref}"); + $actions = id(new PhabricatorActionListView()); + $actions->setUser($viewer); + + + $actions->addAction(id(new PhabricatorActionView()) + ->setName(pht('Reverse Comparison')) + ->setHref($reverse_uri) + ->setIcon('fa-list')); + + $view = id(new PHUIPropertyListView()) + ->setUser($viewer) + ->setActionList($actions); + + $readme = + 'These are the commits that will be added to **Onto** if you merge '. + '**From** onto it.'; + $readme = new PHUIRemarkupView($viewer, $readme); + $view->addTextContent($readme); + + $view->addProperty(pht('From'), $from_ref); + $view->addProperty(pht('Onto'), $onto_ref); + $view->addProperty( + pht('merge-base'), + new PHUIRemarkupView($viewer, idx($revlist, 'mergeBase'))); + + $compare_text = pht( + '%d Commits (Reverse comparison is %d commits)', + idx($revlist, 'commitsCount'), + idx($revlist, 'reversedComparisonSize')); + $view->addProperty(pht('Comparison size'), $compare_text); + + $merge_instructions = "`git checkout ${onto_ref} && git merge ${from_ref}`"; + $merge_instructions = new PHUIRemarkupView($viewer, $merge_instructions); + $view->addProperty(pht('To Merge'), $merge_instructions); + + $header = id(new PHUIHeaderView()) + ->setUser($viewer) + ->setPolicyObject($drequest->getRepository()); + + $box = id(new PHUIObjectBoxView()) + ->setUser($viewer) + ->setHeader($header) + ->addPropertyList($view); + return $box; + } + + private function buildHistoryTable( + $revlist, + $history, + $history_exception) { + + $request = $this->getRequest(); + $viewer = $request->getUser(); + $drequest = $this->getDiffusionRequest(); + $repository = $drequest->getRepository(); + + if ($history_exception) { + if ($repository->isImporting()) { + return $this->renderStatusMessage( + pht('Still Importing...'), + pht( + 'This repository is still importing. History is not yet '. + 'available.')); + } else { + return $this->renderStatusMessage( + pht('Unable to Retrieve History'), + $history_exception->getMessage()); + } + } + + $history_table = id(new DiffusionHistoryTableView()) + ->setUser($viewer) + ->setDiffusionRequest($drequest) + ->setHistory($history); + + // TODO: Super sketchy? + $history_table->loadRevisions(); + + if ($revlist) { + $history_table->setParents($revlist['parents']); + } + + // TODO also expose diff. + + $panel = new PHUIObjectBoxView(); + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Missing Commits')); + $panel->setHeader($header); + $panel->setTable($history_table); + + return $panel; + } +} diff --git a/src/applications/diffusion/controller/DiffusionController.php b/src/applications/diffusion/controller/DiffusionController.php --- a/src/applications/diffusion/controller/DiffusionController.php +++ b/src/applications/diffusion/controller/DiffusionController.php @@ -206,6 +206,9 @@ case 'change': $view_name = pht('Change'); break; + case 'compare': + $view_name = pht('Compare'); + break; } $crumb = id(new PHUICrumbView()) diff --git a/src/applications/diffusion/controller/DiffusionRepositoryController.php b/src/applications/diffusion/controller/DiffusionRepositoryController.php --- a/src/applications/diffusion/controller/DiffusionRepositoryController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryController.php @@ -506,6 +506,12 @@ ->setHref($push_uri)); } + $view->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Compare Branches')) + ->setIcon('fa-usb') + ->setHref($repository->getPathURI('compare/'))); + return $view; }