diff --git a/src/applications/differential/controller/DifferentialRevisionOperationController.php b/src/applications/differential/controller/DifferentialRevisionOperationController.php index 85f2a0b1b2..a5d5b5f98f 100644 --- a/src/applications/differential/controller/DifferentialRevisionOperationController.php +++ b/src/applications/differential/controller/DifferentialRevisionOperationController.php @@ -1,153 +1,67 @@ getViewer(); $id = $request->getURIData('id'); $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($id)) ->setViewer($viewer) ->needActiveDiffs(true) ->executeOne(); if (!$revision) { return new Aphront404Response(); } $detail_uri = "/D{$id}"; - $repository = $revision->getRepository(); - if (!$repository) { - return $this->rejectOperation( - $revision, - pht('No Repository'), - pht( - 'This revision is not associated with a known repository. Only '. - 'revisions associated with a tracked repository can be landed '. - 'automatically.')); - } - - if (!$repository->canPerformAutomation()) { - return $this->rejectOperation( - $revision, - pht('No Repository Automation'), - pht( - 'The repository this revision is associated with ("%s") is not '. - 'configured to support automation. Configure automation for the '. - 'repository to enable revisions to be landed automatically.', - $repository->getMonogram())); - } - - // TODO: At some point we should allow installs to give "land reviewed - // code" permission to more users than "push any commit", because it is - // a much less powerful operation. For now, just require push so this - // doesn't do anything users can't do on their own. - $can_push = PhabricatorPolicyFilter::hasCapability( - $viewer, - $repository, - DiffusionPushCapability::CAPABILITY); - if (!$can_push) { - return $this->rejectOperation( - $revision, - pht('Unable to Push'), - pht( - 'You do not have permission to push to the repository this '. - 'revision is associated with ("%s"), so you can not land it.', - $repository->getMonogram())); - } - $op = new DrydockLandRepositoryOperation(); - - // Check for other operations. Eventually this should probably be more - // general (e.g., it's OK to land to multiple different branches - // simultaneously) but just put this in as a sanity check for now. - $other_operations = id(new DrydockRepositoryOperationQuery()) - ->setViewer($viewer) - ->withObjectPHIDs(array($revision->getPHID())) - ->withOperationTypes( - array( - $op->getOperationConstant(), - )) - ->withOperationStates( - array( - DrydockRepositoryOperation::STATE_WAIT, - DrydockRepositoryOperation::STATE_WORK, - DrydockRepositoryOperation::STATE_DONE, - )) - ->execute(); - - if ($other_operations) { - $any_done = false; - foreach ($other_operations as $operation) { - if ($operation->isDone()) { - $any_done = true; - break; - } - } - - if ($any_done) { - return $this->rejectOperation( - $revision, - pht('Already Complete'), - pht('This revision has already landed.')); - } else { - return $this->rejectOperation( - $revision, - pht('Already In Flight'), - pht('This revision is already landing.')); - } + $barrier = $op->getBarrierToLanding($viewer, $revision); + if ($barrier) { + return $this->newDialog() + ->setTitle($barrier['title']) + ->appendParagraph($barrier['body']) + ->addCancelButton($detail_uri); } if ($request->isFormPost()) { // NOTE: The operation is locked to the current active diff, so if the // revision is updated before the operation applies nothing sneaky // occurs. $diff = $revision->getActiveDiff(); + $repository = $revision->getRepository(); $operation = DrydockRepositoryOperation::initializeNewOperation($op) ->setAuthorPHID($viewer->getPHID()) ->setObjectPHID($revision->getPHID()) ->setRepositoryPHID($repository->getPHID()) ->setRepositoryTarget('branch:master') ->setProperty('differential.diffPHID', $diff->getPHID()); $operation->save(); $operation->scheduleUpdate(); return id(new AphrontRedirectResponse()) ->setURI($detail_uri); } return $this->newDialog() ->setTitle(pht('Land Revision')) ->appendParagraph( pht( 'In theory, this will do approximately what `arc land` would do. '. 'In practice, that is almost certainly not what it will actually '. 'do.')) ->appendParagraph( pht( 'THIS FEATURE IS EXPERIMENTAL AND DANGEROUS! USE IT AT YOUR '. 'OWN RISK!')) ->addCancelButton($detail_uri) ->addSubmitButton(pht('Mutate Repository Unpredictably')); } - private function rejectOperation( - DifferentialRevision $revision, - $title, - $body) { - - $id = $revision->getID(); - $detail_uri = "/D{$id}"; - - return $this->newDialog() - ->setTitle($title) - ->appendParagraph($body) - ->addCancelButton($detail_uri); - } - } diff --git a/src/applications/differential/landing/DifferentialLandingActionMenuEventListener.php b/src/applications/differential/landing/DifferentialLandingActionMenuEventListener.php index 7dcf3f462d..2cf3eaa4e5 100644 --- a/src/applications/differential/landing/DifferentialLandingActionMenuEventListener.php +++ b/src/applications/differential/landing/DifferentialLandingActionMenuEventListener.php @@ -1,69 +1,81 @@ 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'); if ($object instanceof DifferentialRevision) { $this->renderRevisionAction($event); } } private function renderRevisionAction(PhutilEvent $event) { - if (!$this->canUseApplication($event->getUser())) { + $viewer = $event->getUser(); + + if (!$this->canUseApplication($viewer)) { return null; } $revision = $event->getValue('object'); $repository = $revision->getRepository(); if ($repository === null) { return null; } if ($repository->canPerformAutomation()) { $revision_id = $revision->getID(); + $op = new DrydockLandRepositoryOperation(); + $barrier = $op->getBarrierToLanding($viewer, $revision); + + if ($barrier) { + $can_land = false; + } else { + $can_land = true; + } + $action = id(new PhabricatorActionView()) - ->setWorkflow(true) ->setName(pht('Land Revision')) ->setIcon('fa-fighter-jet') - ->setHref("/differential/revision/operation/{$revision_id}/"); + ->setHref("/differential/revision/operation/{$revision_id}/") + ->setWorkflow(true) + ->setDisabled(!$can_land); + $this->addActionMenuItems($event, $action); } $strategies = id(new PhutilClassMapQuery()) ->setAncestorClass('DifferentialLandingStrategy') ->execute(); foreach ($strategies as $strategy) { - $viewer = $event->getUser(); $action = $strategy->createMenuItem($viewer, $revision, $repository); if ($action == null) { continue; } if ($strategy->isActionDisabled($viewer, $revision, $repository)) { $action->setDisabled(true); } $this->addActionMenuItems($event, $action); } } } diff --git a/src/applications/drydock/operation/DrydockLandRepositoryOperation.php b/src/applications/drydock/operation/DrydockLandRepositoryOperation.php index 00b5c71181..6da4c2ae5d 100644 --- a/src/applications/drydock/operation/DrydockLandRepositoryOperation.php +++ b/src/applications/drydock/operation/DrydockLandRepositoryOperation.php @@ -1,192 +1,289 @@ getRepositoryTarget(); $repository = $operation->getRepository(); switch ($operation->getOperationState()) { case DrydockRepositoryOperation::STATE_WAIT: return pht( 'Waiting to land revision into %s on %s...', $repository->getMonogram(), $target); case DrydockRepositoryOperation::STATE_WORK: return pht( 'Landing revision into %s on %s...', $repository->getMonogram(), $target); case DrydockRepositoryOperation::STATE_DONE: return pht( 'Revision landed into %s.', $repository->getMonogram()); } } public function getWorkingCopyMerges(DrydockRepositoryOperation $operation) { $repository = $operation->getRepository(); $merges = array(); $object = $operation->getObject(); if ($object instanceof DifferentialRevision) { $diff = $this->loadDiff($operation); $merges[] = array( 'src.uri' => $repository->getStagingURI(), 'src.ref' => $diff->getStagingRef(), ); } else { throw new Exception( pht( 'Invalid or unknown object ("%s") for land operation, expected '. 'Differential Revision.', $operation->getObjectPHID())); } return $merges; } public function applyOperation( DrydockRepositoryOperation $operation, DrydockInterface $interface) { $viewer = $this->getViewer(); $repository = $operation->getRepository(); $cmd = array(); $arg = array(); $object = $operation->getObject(); if ($object instanceof DifferentialRevision) { $revision = $object; $diff = $this->loadDiff($operation); $dict = $diff->getDiffAuthorshipDict(); $author_name = idx($dict, 'authorName'); $author_email = idx($dict, 'authorEmail'); $api_method = 'differential.getcommitmessage'; $api_params = array( 'revision_id' => $revision->getID(), ); $commit_message = id(new ConduitCall($api_method, $api_params)) ->setUser($viewer) ->execute(); } else { throw new Exception( pht( 'Invalid or unknown object ("%s") for land operation, expected '. 'Differential Revision.', $operation->getObjectPHID())); } $target = $operation->getRepositoryTarget(); list($type, $name) = explode(':', $target, 2); switch ($type) { case 'branch': $push_dst = 'refs/heads/'.$name; break; default: throw new Exception( pht( 'Unknown repository operation target type "%s" (in target "%s").', $type, $target)); } $committer_info = $this->getCommitterInfo($operation); // NOTE: We're doing this commit with "-F -" so we don't run into trouble // with enormous commit messages which might otherwise exceed the maximum // size of a command. $future = $interface->getExecFuture( 'git -c user.name=%s -c user.email=%s commit --author %s -F - --', $committer_info['name'], $committer_info['email'], "{$author_name} <{$author_email}>"); $future ->write($commit_message) ->resolvex(); $interface->execx( 'git push origin -- %s:%s', 'HEAD', $push_dst); } private function getCommitterInfo(DrydockRepositoryOperation $operation) { $viewer = $this->getViewer(); $committer_name = null; $author_phid = $operation->getAuthorPHID(); $object = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs(array($author_phid)) ->executeOne(); if ($object) { if ($object instanceof PhabricatorUser) { $committer_name = $object->getUsername(); } } if (!strlen($committer_name)) { $committer_name = pht('autocommitter'); } // TODO: Probably let users choose a VCS email address in settings. For // now just make something up so we don't leak anyone's stuff. return array( 'name' => $committer_name, 'email' => 'autocommitter@example.com', ); } private function loadDiff(DrydockRepositoryOperation $operation) { $viewer = $this->getViewer(); $revision = $operation->getObject(); $diff_phid = $operation->getProperty('differential.diffPHID'); $diff = id(new DifferentialDiffQuery()) ->setViewer($viewer) ->withPHIDs(array($diff_phid)) ->executeOne(); if (!$diff) { throw new Exception( pht( 'Unable to load diff "%s".', $diff_phid)); } $diff_revid = $diff->getRevisionID(); $revision_id = $revision->getID(); if ($diff_revid != $revision_id) { throw new Exception( pht( 'Diff ("%s") has wrong revision ID ("%s", expected "%s").', $diff_phid, $diff_revid, $revision_id)); } return $diff; } + public function getBarrierToLanding( + PhabricatorUser $viewer, + DifferentialRevision $revision) { + + $repository = $revision->getRepository(); + if (!$repository) { + return array( + 'title' => pht('No Repository'), + 'body' => pht( + 'This revision is not associated with a known repository. Only '. + 'revisions associated with a tracked repository can be landed '. + 'automatically.'), + ); + } + + if (!$repository->canPerformAutomation()) { + return array( + 'title' => pht('No Repository Automation'), + 'body' => pht( + 'The repository this revision is associated with ("%s") is not '. + 'configured to support automation. Configure automation for the '. + 'repository to enable revisions to be landed automatically.', + $repository->getMonogram()), + ); + } + + // TODO: At some point we should allow installs to give "land reviewed + // code" permission to more users than "push any commit", because it is + // a much less powerful operation. For now, just require push so this + // doesn't do anything users can't do on their own. + $can_push = PhabricatorPolicyFilter::hasCapability( + $viewer, + $repository, + DiffusionPushCapability::CAPABILITY); + if (!$can_push) { + return array( + 'title' => pht('Unable to Push'), + 'body' => pht( + 'You do not have permission to push to the repository this '. + 'revision is associated with ("%s"), so you can not land it.', + $repository->getMonogram()), + ); + } + + $status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED; + if ($revision->getStatus() !== $status_accepted) { + return array( + 'title' => pht('Revision Not Accepted'), + 'body' => pht( + 'This revision is still under review. Only revisions which have '. + 'been accepted may land.'), + ); + } + + // Check for other operations. Eventually this should probably be more + // general (e.g., it's OK to land to multiple different branches + // simultaneously) but just put this in as a sanity check for now. + $other_operations = id(new DrydockRepositoryOperationQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($revision->getPHID())) + ->withOperationTypes( + array( + $this->getOperationConstant(), + )) + ->withOperationStates( + array( + DrydockRepositoryOperation::STATE_WAIT, + DrydockRepositoryOperation::STATE_WORK, + DrydockRepositoryOperation::STATE_DONE, + )) + ->execute(); + + if ($other_operations) { + $any_done = false; + foreach ($other_operations as $operation) { + if ($operation->isDone()) { + $any_done = true; + break; + } + } + + if ($any_done) { + return array( + 'title' => pht('Already Complete'), + 'body' => pht('This revision has already landed.'), + ); + } else { + return array( + 'title' => pht('Already In Flight'), + 'body' => pht('This revision is already landing.'), + ); + } + } + + return null; + } + }