Index: src/__phutil_library_map__.php =================================================================== --- src/__phutil_library_map__.php +++ src/__phutil_library_map__.php @@ -367,6 +367,7 @@ 'DifferentialFieldValidationException' => 'applications/differential/field/exception/DifferentialFieldValidationException.php', 'DifferentialFreeformFieldSpecification' => 'applications/differential/field/specification/DifferentialFreeformFieldSpecification.php', 'DifferentialFreeformFieldTestCase' => 'applications/differential/field/specification/__tests__/DifferentialFreeformFieldTestCase.php', + 'DifferentialGetWorkingCopy' => 'applications/differential/DifferentialGetWorkingCopy.php', 'DifferentialGitSVNIDFieldSpecification' => 'applications/differential/field/specification/DifferentialGitSVNIDFieldSpecification.php', 'DifferentialHostFieldSpecification' => 'applications/differential/field/specification/DifferentialHostFieldSpecification.php', 'DifferentialHovercardEventListener' => 'applications/differential/event/DifferentialHovercardEventListener.php', @@ -381,6 +382,8 @@ 'DifferentialInlineCommentQuery' => 'applications/differential/query/DifferentialInlineCommentQuery.php', 'DifferentialInlineCommentView' => 'applications/differential/view/DifferentialInlineCommentView.php', 'DifferentialJIRAIssuesFieldSpecification' => 'applications/differential/field/specification/DifferentialJIRAIssuesFieldSpecification.php', + 'DifferentialLandingStrategy' => 'applications/differential/landing/DifferentialLandingStrategy.php', + 'DifferentialLandingToHostedGit' => 'applications/differential/landing/DifferentialLandingToHostedGit.php', 'DifferentialLinesFieldSpecification' => 'applications/differential/field/specification/DifferentialLinesFieldSpecification.php', 'DifferentialLintFieldSpecification' => 'applications/differential/field/specification/DifferentialLintFieldSpecification.php', 'DifferentialLintStatus' => 'applications/differential/constants/DifferentialLintStatus.php', @@ -418,6 +421,7 @@ 'DifferentialRevisionEditor' => 'applications/differential/editor/DifferentialRevisionEditor.php', 'DifferentialRevisionIDFieldParserTestCase' => 'applications/differential/field/specification/__tests__/DifferentialRevisionIDFieldParserTestCase.php', 'DifferentialRevisionIDFieldSpecification' => 'applications/differential/field/specification/DifferentialRevisionIDFieldSpecification.php', + 'DifferentialRevisionLandController' => 'applications/differential/controller/DifferentialRevisionLandController.php', 'DifferentialRevisionListController' => 'applications/differential/controller/DifferentialRevisionListController.php', 'DifferentialRevisionListView' => 'applications/differential/view/DifferentialRevisionListView.php', 'DifferentialRevisionMailReceiver' => 'applications/differential/mail/DifferentialRevisionMailReceiver.php', @@ -440,6 +444,7 @@ 'DifferentialUnitStatus' => 'applications/differential/constants/DifferentialUnitStatus.php', 'DifferentialUnitTestResult' => 'applications/differential/constants/DifferentialUnitTestResult.php', 'DifferentialViewPolicyFieldSpecification' => 'applications/differential/field/specification/DifferentialViewPolicyFieldSpecification.php', + 'DifferntialLandingActionMenuEventListener' => 'applications/differential/landing/DifferntialLandingActionMenuEventListener.php', 'DiffusionBranchInformation' => 'applications/diffusion/data/DiffusionBranchInformation.php', 'DiffusionBranchTableController' => 'applications/diffusion/controller/DiffusionBranchTableController.php', 'DiffusionBranchTableView' => 'applications/diffusion/view/DiffusionBranchTableView.php', @@ -2588,6 +2593,7 @@ 'DifferentialInlineCommentQuery' => 'PhabricatorOffsetPagedQuery', 'DifferentialInlineCommentView' => 'AphrontView', 'DifferentialJIRAIssuesFieldSpecification' => 'DifferentialFieldSpecification', + 'DifferentialLandingToHostedGit' => 'DifferentialLandingStrategy', 'DifferentialLinesFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialLintFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialLocalCommitsView' => 'AphrontView', @@ -2625,6 +2631,7 @@ 'DifferentialRevisionEditor' => 'PhabricatorEditor', 'DifferentialRevisionIDFieldParserTestCase' => 'PhabricatorTestCase', 'DifferentialRevisionIDFieldSpecification' => 'DifferentialFieldSpecification', + 'DifferentialRevisionLandController' => 'DifferentialController', 'DifferentialRevisionListController' => array( 0 => 'DifferentialController', @@ -2647,6 +2654,7 @@ 'DifferentialTransactionComment' => 'PhabricatorApplicationTransactionComment', 'DifferentialUnitFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialViewPolicyFieldSpecification' => 'DifferentialFieldSpecification', + 'DifferntialLandingActionMenuEventListener' => 'PhabricatorEventListener', 'DiffusionBranchTableController' => 'DiffusionController', 'DiffusionBranchTableView' => 'DiffusionView', 'DiffusionBrowseController' => 'DiffusionController', Index: src/applications/differential/DifferentialGetWorkingCopy.php =================================================================== --- /dev/null +++ src/applications/differential/DifferentialGetWorkingCopy.php @@ -0,0 +1,40 @@ +getLocalPath(); + + $path = rtrim($origin_path, '/'); + $path = $path . '__workspace'; + + if (!Filesystem::pathExists($path)) { + $future = $repo->getLocalCommandFuture( + 'clone -- file://%s %s', + $origin_path, + $path); + $future->resolvex(); + } + + $workspace = new ArcanistGitAPI($path); + $workspace->execxLocal('clean -f -d'); + $workspace->execxLocal('checkout master'); + $workspace->execxLocal('fetch'); + $workspace->execxLocal('reset --hard origin/master'); + $workspace->reloadWorkingCopy(); + + return $workspace; + } + +} Index: src/applications/differential/application/PhabricatorApplicationDifferential.php =================================================================== --- src/applications/differential/application/PhabricatorApplicationDifferential.php +++ src/applications/differential/application/PhabricatorApplicationDifferential.php @@ -32,6 +32,7 @@ return array( new DifferentialActionMenuEventListener(), new DifferentialHovercardEventListener(), + new DifferntialLandingActionMenuEventListener(), ); } @@ -48,6 +49,8 @@ 'changeset/' => 'DifferentialChangesetViewController', 'revision/edit/(?:(?P[1-9]\d*)/)?' => 'DifferentialRevisionEditController', + 'revision/land/(?:(?P[1-9]\d*))/(?:(?P\w*))/' + => 'DifferentialRevisionLandController', 'comment/' => array( 'preview/(?P[1-9]\d*)/' => 'DifferentialCommentPreviewController', 'save/' => 'DifferentialCommentSaveController', Index: src/applications/differential/config/PhabricatorDifferentialConfigOptions.php =================================================================== --- src/applications/differential/config/PhabricatorDifferentialConfigOptions.php +++ src/applications/differential/config/PhabricatorDifferentialConfigOptions.php @@ -36,6 +36,12 @@ 'DifferentialDefaultFieldSelector') ->setBaseClass('DifferentialFieldSelector') ->setDescription(pht('Field selector class')), + $this->newOption( + 'differential.land-strategy', + 'class', + 'DifferentialLandingToHostedGit') + ->setBaseClass('DifferentialLandingStrategy') + ->setDescription(pht('Implementation for landing revisions')), $this->newOption('differential.show-host-field', 'bool', false) ->setBoolOptions( array( Index: src/applications/differential/controller/DifferentialRevisionLandController.php =================================================================== --- /dev/null +++ src/applications/differential/controller/DifferentialRevisionLandController.php @@ -0,0 +1,137 @@ +revisionID = $data['id']; + $this->strategyClass = $data['strategy']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $revision_id = $this->revisionID; + + $revision = id(new DifferentialRevisionQuery()) + ->withIDs(array($revision_id)) + ->setViewer($viewer) + ->executeOne(); + if (!$revision) { + return new Aphront404Response(); + } + + if (is_subclass_of($this->strategyClass, 'DifferentialLandingStrategy')) { + $this->pushStrategy = newv($this->strategyClass, array()); + } else { + throw new Exception( + "Strategy type must be a valid class name and must subclass ". + "DifferentialLandingStrategy. ". + "'{$this->strategyClass}' is not a subclass of ". + "DifferentialLandingStrategy."); + } + + if ($request->isDialogFormPost()) { + try { + $this->attemptLand($revision, $request); + $title = "Success!"; + $text = "Revision was successfully landed."; + } catch (PhutilProxyException $ex) { + $title = "Failed to land revision"; + $text = hsprintf( + '%s:
%s
', + $ex->getMessage(), + $ex->getPreviousException()->getMessage()); + $text = id(new AphrontErrorView()) + ->appendChild($text); + } catch (Exception $ex) { + $title = "Failed to land revision"; + $text = hsprintf('
%s
', $ex->getMessage()); + $text = id(new AphrontErrorView()) + ->appendChild($text); + } + + $dialog = id(new AphrontDialogView()) + ->setUser($viewer) + ->setTitle($title) + ->appendChild(phutil_tag('p', array(), $text)) + ->setSubmitURI('/D'.$revision_id) + ->addSubmitButton('Back to revision'); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + + $prompt = hsprintf( + 'This will squash and rebase revision %s, and '. + 'push it to origin/master.

'. + 'It is an experimental feature and may not work.', + $revision_id); + + $dialog = id(new AphrontDialogView()) + ->setUser($viewer) + ->setTitle("Land Revision ${revision_id}?") + ->appendChild($prompt) + ->setSubmitURI($request->getRequestURI()) + ->addSubmitButton('Land it!') + ->addCancelButton('/D'.$revision_id); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + + private function attemptLand($revision, $request) { + $status = $revision->getStatus(); + if ($status != ArcanistDifferentialRevisionStatus::ACCEPTED) { + throw new Exception("Only Accepted revisions can be landed."); + } + + $repository = $revision->getRepository(); + + if ($repository === null) { + throw new Exception("revision is not attached to a repository."); + } + + $can_push = PhabricatorPolicyFilter::hasCapability( + $request->getUser(), + $repository, + DiffusionCapabilityPush::CAPABILITY); + + if (!$can_push) { + throw new Exception( + pht('You do not have permission to push to this repository.')); + } + + try { + $this->pushStrategy->assertSupportForRepository($repository); + } catch (Exception $e) { + throw new PhutilProxyException( + 'This strategy does not support this repository', + $e); + } + + $lock = $this->lockRepository($repository); + + try { + $this->pushStrategy->processLandRequest( + $request, + $revision, + $repository); + } catch (Exception $e) { + $lock->unlock(); + throw $e; + } + + $lock->unlock(); + } + + private static function lockRepository($repository) { + $lock_name = __CLASS__.':'.($repository->getCallsign()); + $lock = PhabricatorGlobalLock::newLock($lock_name); + $lock->lock(); + return $lock; + } +} + Index: src/applications/differential/landing/DifferentialLandingStrategy.php =================================================================== --- /dev/null +++ src/applications/differential/landing/DifferentialLandingStrategy.php @@ -0,0 +1,33 @@ +getId(); + return id(new PhabricatorActionView()) + ->setRenderAsForm(true) + ->setName($name) + ->setHref("/differential/revision/land/${revision_id}/${strategy}/") + ->setDisabled($disabled); + } +} Index: src/applications/differential/landing/DifferentialLandingToHostedGit.php =================================================================== --- /dev/null +++ src/applications/differential/landing/DifferentialLandingToHostedGit.php @@ -0,0 +1,161 @@ +getUser(); + + try { + $workspace = + DifferentialGetWorkingCopy::getCleanGitWorkspace($repository); + } catch (Exception $e) { + throw new PhutilProxyException ( + 'Failed to allocate a workspace', + $e); + } + + try { + $this->commitRevisionToWorkspace( + $revision, + $workspace, + $viewer); + } catch (Exception $e) { + throw new PhutilProxyException( + 'Failed to commit patch', + $e); + } + + try { + $this->pushWorkspaceRepository( + $repository, + $workspace, + $viewer); + } catch (Exception $e) { + throw new PhutilProxyException( + 'Failed to push changes upstream', + $e); + } + } + + public function commitRevisionToWorkspace( + DifferentialRevision $revision, + ArcanistRepositoryAPI $workspace, + PhabricatorUser $user) { + + $diff_id = $revision->loadActiveDiff()->getID(); + + $call = new ConduitCall( + 'differential.getrawdiff', + array( + 'diffID' => $diff_id, + )); + + $call->setUser($user); + $raw_diff = $call->execute(); + + $missing_binary = + "\nindex " + . "0000000000000000000000000000000000000000.." + . "0000000000000000000000000000000000000000\n"; + if (strpos($raw_diff, $missing_binary) !== false) { + throw new Exception("Patch is missing content for a binary file"); + } + + $future = $workspace->execFutureLocal('apply --index -'); + $future->write($raw_diff); + $future->resolvex(); + + $workspace->reloadWorkingCopy(); + + $call = new ConduitCall( + 'differential.getcommitmessage', + array( + 'revision_id' => $revision->getID(), + )); + + $call->setUser($user); + $message = $call->execute(); + + $author = id(new PhabricatorUser())->loadOneWhere( + 'phid = %s', + $revision->getAuthorPHID()); + + $author_string = sprintf( + '%s <%s>', + $author->getRealName(), + $author->loadPrimaryEmailAddress()); + $author_date = $revision->getDateCreated(); + + $workspace->execxLocal( + '-c user.name=%s -c user.email=%s ' . + 'commit --date=%s --author=%s '. + '--message=%s', + // -c will set the 'committer' + $user->getRealName(), + $user->loadPrimaryEmailAddress(), + $author_date, + $author_string, + $message); + } + + + public function pushWorkspaceRepository( + PhabricatorRepository $repository, + ArcanistRepositoryAPI $workspace, + PhabricatorUser $user) { + + $workspace->execxLocal("push origin HEAD:master"); + } + + public function createMenuItems( + PhabricatorUser $viewer, + DifferentialRevision $revision, + PhabricatorRepository $repository) { + + $vcs = $repository->getVersionControlSystem(); + if ($vcs !== PhabricatorRepositoryType::REPOSITORY_TYPE_GIT) { + return; + } + + if (!$repository->isHosted()) { + return; + } + + if (!$repository->isWorkingCopyBare()) { + return; + } + + $can_push = PhabricatorPolicyFilter::hasCapability( + $viewer, + $repository, + DiffusionCapabilityPush::CAPABILITY); + + return $this->createActionView( + $revision, + pht('Land to Hosted Repository'), + !$can_push); + } + + public function assertSupportForRepository( + PhabricatorRepository $repository) { + + $vcs = $repository->getVersionControlSystem(); + if ($vcs !== PhabricatorRepositoryType::REPOSITORY_TYPE_GIT) { + throw new Exception('Only Git repositories are supported'); + } + + if (!$repository->isHosted()) { + throw new Exception('Repository must be Hosted'); + } + + if (!$repository->isWorkingCopyBare()) { + throw new Exception('Repository must be hosted as "bare".'); + } + } + +} Index: src/applications/differential/landing/DifferntialLandingActionMenuEventListener.php =================================================================== --- /dev/null +++ src/applications/differential/landing/DifferntialLandingActionMenuEventListener.php @@ -0,0 +1,54 @@ +listen(PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS); + } + + public function handleEvent(PhutilEvent $event) { + switch ($event->getType()) { + case PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS: + $this->handleActionsEvent($event); + break; + } + } + + private function handleActionsEvent(PhutilEvent $event) { + $object = $event->getValue('object'); + + $actions = null; + if ($object instanceof DifferentialRevision) { + $actions = $this->renderRevisionAction($event); + } + + $this->addActionMenuItems($event, $actions); + } + + private function renderRevisionAction(PhutilEvent $event) { + if (!$this->canUseApplication($event->getUser())) { + return null; + } + + $revision = $event->getValue('object'); + + $repository = $revision->getRepository(); + if ($repository === null) { + return null; + } + + $strategies = id(new PhutilSymbolLoader()) + ->setAncestorClass('DifferentialLandingStrategy') + ->loadObjects(); + foreach ($strategies as $strategy) { + $actions = $strategy->createMenuItems( + $event->getUser(), + $revision, + $repository); + $this->addActionMenuItems($event, $actions); + } + } + +} +