Page MenuHomePhabricator

D7486.id16907.diff
No OneTemporary

D7486.id16907.diff

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 @@
+<?php
+
+/**
+ * Can't find a good place for this, so I'm putting it in the most notably
+ * wrong place.
+ */
+final class DifferentialGetWorkingCopy {
+
+ /**
+ * Creates and/or cleans a workspace for the requested repo.
+ *
+ * return ArcanistGitAPI
+ */
+ public static function getCleanGitWorkspace(
+ PhabricatorRepository $repo) {
+
+ $origin_path = $repo->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<id>[1-9]\d*)/)?'
=> 'DifferentialRevisionEditController',
+ 'revision/land/(?:(?P<id>[1-9]\d*))/(?:(?P<strategy>\w*))/'
+ => 'DifferentialRevisionLandController',
'comment/' => array(
'preview/(?P<id>[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 @@
+<?php
+
+final class DifferentialRevisionLandController extends DifferentialController {
+
+ private $revisionID;
+ private $strategyClass;
+ private $pushStrategy;
+
+ public function willProcessRequest(array $data) {
+ $this->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:<br><pre>%s</pre>',
+ $ex->getMessage(),
+ $ex->getPreviousException()->getMessage());
+ $text = id(new AphrontErrorView())
+ ->appendChild($text);
+ } catch (Exception $ex) {
+ $title = "Failed to land revision";
+ $text = hsprintf('<pre>%s</pre>', $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. <br><br>'.
+ '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 @@
+<?php
+
+abstract class DifferentialLandingStrategy {
+
+ public abstract function assertSupportForRepository(
+ PhabricatorRepository $repository);
+
+ public abstract function processLandRequest(
+ AphrontRequest $request,
+ DifferentialRevision $revision,
+ PhabricatorRepository $repository);
+
+ /**
+ * returns PhabricatorActionView or an array of PhabricatorActionView or null.
+ */
+ abstract function createMenuItems(
+ PhabricatorUser $viewer,
+ DifferentialRevision $revision,
+ PhabricatorRepository $repository);
+
+ /**
+ * returns PhabricatorActionView which can be attached to the revision view.
+ */
+ protected function createActionView($revision, $name, $disabled = false) {
+ $strategy = get_class($this);
+ $revision_id = $revision->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 @@
+<?php
+
+final class DifferentialLandingToHostedGit
+ extends DifferentialLandingStrategy {
+
+ public function processLandRequest(
+ AphrontRequest $request,
+ DifferentialRevision $revision,
+ PhabricatorRepository $repository) {
+
+ $viewer = $request->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 @@
+<?php
+
+final class DifferntialLandingActionMenuEventListener
+ extends PhabricatorEventListener {
+
+ public function register() {
+ $this->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);
+ }
+ }
+
+}
+

File Metadata

Mime Type
text/plain
Expires
Sun, Feb 9, 3:42 AM (20 h, 54 m)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7105082
Default Alt Text
D7486.id16907.diff (19 KB)

Event Timeline