Path: blob/master/src/applications/drydock/operation/DrydockLandRepositoryOperation.php
12256 views
<?php12final class DrydockLandRepositoryOperation3extends DrydockRepositoryOperationType {45const OPCONST = 'land';67const PHASE_PUSH = 'op.land.push';8const PHASE_COMMIT = 'op.land.commit';910public function getOperationDescription(11DrydockRepositoryOperation $operation,12PhabricatorUser $viewer) {13return pht('Land Revision');14}1516public function getOperationCurrentStatus(17DrydockRepositoryOperation $operation,18PhabricatorUser $viewer) {1920$target = $operation->getRepositoryTarget();21$repository = $operation->getRepository();22switch ($operation->getOperationState()) {23case DrydockRepositoryOperation::STATE_WAIT:24return pht(25'Waiting to land revision into %s on %s...',26$repository->getMonogram(),27$target);28case DrydockRepositoryOperation::STATE_WORK:29return pht(30'Landing revision into %s on %s...',31$repository->getMonogram(),32$target);33case DrydockRepositoryOperation::STATE_DONE:34return pht(35'Revision landed into %s.',36$repository->getMonogram());37}38}3940public function getWorkingCopyMerges(DrydockRepositoryOperation $operation) {41$repository = $operation->getRepository();42$merges = array();4344$object = $operation->getObject();45if ($object instanceof DifferentialRevision) {46$diff = $this->loadDiff($operation);47$merges[] = array(48'src.uri' => $repository->getStagingURI(),49'src.ref' => $diff->getStagingRef(),50);51} else {52throw new Exception(53pht(54'Invalid or unknown object ("%s") for land operation, expected '.55'Differential Revision.',56$operation->getObjectPHID()));57}5859return $merges;60}6162public function applyOperation(63DrydockRepositoryOperation $operation,64DrydockInterface $interface) {65$viewer = $this->getViewer();66$repository = $operation->getRepository();6768$cmd = array();69$arg = array();7071$object = $operation->getObject();72if ($object instanceof DifferentialRevision) {73$revision = $object;7475$diff = $this->loadDiff($operation);7677$dict = $diff->getDiffAuthorshipDict();78$author_name = idx($dict, 'authorName');79$author_email = idx($dict, 'authorEmail');8081$api_method = 'differential.getcommitmessage';82$api_params = array(83'revision_id' => $revision->getID(),84);8586$commit_message = id(new ConduitCall($api_method, $api_params))87->setUser($viewer)88->execute();89} else {90throw new Exception(91pht(92'Invalid or unknown object ("%s") for land operation, expected '.93'Differential Revision.',94$operation->getObjectPHID()));95}9697$target = $operation->getRepositoryTarget();98list($type, $name) = explode(':', $target, 2);99switch ($type) {100case 'branch':101$push_dst = 'refs/heads/'.$name;102break;103default:104throw new Exception(105pht(106'Unknown repository operation target type "%s" (in target "%s").',107$type,108$target));109}110111$committer_info = $this->getCommitterInfo($operation);112113// NOTE: We're doing this commit with "-F -" so we don't run into trouble114// with enormous commit messages which might otherwise exceed the maximum115// size of a command.116117$future = $interface->getExecFuture(118'git -c user.name=%s -c user.email=%s commit --author %s -F - --',119$committer_info['name'],120$committer_info['email'],121"{$author_name} <{$author_email}>");122123$future->write($commit_message);124125try {126$future->resolvex();127} catch (CommandException $ex) {128$display_command = csprintf('git commit');129130// TODO: One reason this can fail is if the changes have already been131// merged. We could try to detect that.132133$error = DrydockCommandError::newFromCommandException($ex)134->setPhase(self::PHASE_COMMIT)135->setDisplayCommand($display_command);136137$operation->setCommandError($error->toDictionary());138139throw $ex;140}141142try {143$interface->execx(144'git push origin -- %s:%s',145'HEAD',146$push_dst);147} catch (CommandException $ex) {148$display_command = csprintf(149'git push origin %R:%R',150'HEAD',151$push_dst);152153$error = DrydockCommandError::newFromCommandException($ex)154->setPhase(self::PHASE_PUSH)155->setDisplayCommand($display_command);156157$operation->setCommandError($error->toDictionary());158159throw $ex;160}161}162163private function getCommitterInfo(DrydockRepositoryOperation $operation) {164$viewer = $this->getViewer();165166$committer_name = null;167168$author_phid = $operation->getAuthorPHID();169$object = id(new PhabricatorObjectQuery())170->setViewer($viewer)171->withPHIDs(array($author_phid))172->executeOne();173174if ($object) {175if ($object instanceof PhabricatorUser) {176$committer_name = $object->getUsername();177}178}179180if (!strlen($committer_name)) {181$committer_name = pht('autocommitter');182}183184// TODO: Probably let users choose a VCS email address in settings. For185// now just make something up so we don't leak anyone's stuff.186187return array(188'name' => $committer_name,189'email' => '[email protected]',190);191}192193private function loadDiff(DrydockRepositoryOperation $operation) {194$viewer = $this->getViewer();195$revision = $operation->getObject();196197$diff_phid = $operation->getProperty('differential.diffPHID');198199$diff = id(new DifferentialDiffQuery())200->setViewer($viewer)201->withPHIDs(array($diff_phid))202->executeOne();203if (!$diff) {204throw new Exception(205pht(206'Unable to load diff "%s".',207$diff_phid));208}209210$diff_revid = $diff->getRevisionID();211$revision_id = $revision->getID();212if ($diff_revid != $revision_id) {213throw new Exception(214pht(215'Diff ("%s") has wrong revision ID ("%s", expected "%s").',216$diff_phid,217$diff_revid,218$revision_id));219}220221return $diff;222}223224public function getBarrierToLanding(225PhabricatorUser $viewer,226DifferentialRevision $revision) {227228$repository = $revision->getRepository();229if (!$repository) {230return array(231'title' => pht('No Repository'),232'body' => pht(233'This revision is not associated with a known repository. Only '.234'revisions associated with a tracked repository can be landed '.235'automatically.'),236);237}238239if (!$repository->canPerformAutomation()) {240return array(241'title' => pht('No Repository Automation'),242'body' => pht(243'The repository this revision is associated with ("%s") is not '.244'configured to support automation. Configure automation for the '.245'repository to enable revisions to be landed automatically.',246$repository->getMonogram()),247);248}249250// Check if this diff was pushed to a staging area.251$diff = id(new DifferentialDiffQuery())252->setViewer($viewer)253->withIDs(array($revision->getActiveDiff()->getID()))254->needProperties(true)255->executeOne();256257// Older diffs won't have this property. They may still have been pushed.258// At least for now, assume staging changes are present if the property259// is missing. This should smooth the transition to the more formal260// approach.261$has_staging = $diff->hasDiffProperty('arc.staging');262if ($has_staging) {263$staging = $diff->getProperty('arc.staging');264if (!is_array($staging)) {265$staging = array();266}267$status = idx($staging, 'status');268if ($status != ArcanistDiffWorkflow::STAGING_PUSHED) {269return $this->getBarrierToLandingFromStagingStatus($status);270}271}272273// TODO: At some point we should allow installs to give "land reviewed274// code" permission to more users than "push any commit", because it is275// a much less powerful operation. For now, just require push so this276// doesn't do anything users can't do on their own.277$can_push = PhabricatorPolicyFilter::hasCapability(278$viewer,279$repository,280DiffusionPushCapability::CAPABILITY);281if (!$can_push) {282return array(283'title' => pht('Unable to Push'),284'body' => pht(285'You do not have permission to push to the repository this '.286'revision is associated with ("%s"), so you can not land it.',287$repository->getMonogram()),288);289}290291if ($revision->isAccepted()) {292// We can land accepted revisions, so continue below. Otherwise, raise293// an error with tailored messaging for the most common cases.294} else if ($revision->isAbandoned()) {295return array(296'title' => pht('Revision Abandoned'),297'body' => pht(298'This revision has been abandoned. Only accepted revisions '.299'may land.'),300);301} else if ($revision->isClosed()) {302return array(303'title' => pht('Revision Closed'),304'body' => pht(305'This revision has already been closed. Only open, accepted '.306'revisions may land.'),307);308} else {309return array(310'title' => pht('Revision Not Accepted'),311'body' => pht(312'This revision is still under review. Only revisions which '.313'have been accepted may land.'),314);315}316317// Check for other operations. Eventually this should probably be more318// general (e.g., it's OK to land to multiple different branches319// simultaneously) but just put this in as a sanity check for now.320$other_operations = id(new DrydockRepositoryOperationQuery())321->setViewer($viewer)322->withObjectPHIDs(array($revision->getPHID()))323->withOperationTypes(324array(325$this->getOperationConstant(),326))327->withOperationStates(328array(329DrydockRepositoryOperation::STATE_WAIT,330DrydockRepositoryOperation::STATE_WORK,331DrydockRepositoryOperation::STATE_DONE,332))333->execute();334335if ($other_operations) {336$any_done = false;337foreach ($other_operations as $operation) {338if ($operation->isDone()) {339$any_done = true;340break;341}342}343344if ($any_done) {345return array(346'title' => pht('Already Complete'),347'body' => pht('This revision has already landed.'),348);349} else {350return array(351'title' => pht('Already In Flight'),352'body' => pht('This revision is already landing.'),353);354}355}356357return null;358}359360private function getBarrierToLandingFromStagingStatus($status) {361switch ($status) {362case ArcanistDiffWorkflow::STAGING_USER_SKIP:363return array(364'title' => pht('Staging Area Skipped'),365'body' => pht(366'The diff author used the %s flag to skip pushing this change to '.367'staging. Changes must be pushed to staging before they can be '.368'landed from the web.',369phutil_tag('tt', array(), '--skip-staging')),370);371case ArcanistDiffWorkflow::STAGING_DIFF_RAW:372return array(373'title' => pht('Raw Diff Source'),374'body' => pht(375'The diff was generated from a raw input source, so the change '.376'could not be pushed to staging. Changes must be pushed to '.377'staging before they can be landed from the web.'),378);379case ArcanistDiffWorkflow::STAGING_REPOSITORY_UNKNOWN:380return array(381'title' => pht('Unknown Repository'),382'body' => pht(383'When the diff was generated, the client was not able to '.384'determine which repository it belonged to, so the change '.385'was not pushed to staging. Changes must be pushed to staging '.386'before they can be landed from the web.'),387);388case ArcanistDiffWorkflow::STAGING_REPOSITORY_UNAVAILABLE:389return array(390'title' => pht('Staging Unavailable'),391'body' => pht(392'When this diff was generated, the server was running an older '.393'version of the software which did not support staging areas, so '.394'the change was not pushed to staging. Changes must be pushed '.395'to staging before they can be landed from the web.'),396);397case ArcanistDiffWorkflow::STAGING_REPOSITORY_UNSUPPORTED:398return array(399'title' => pht('Repository Unsupported'),400'body' => pht(401'When this diff was generated, the server was running an older '.402'version of the software which did not support staging areas for '.403'this version control system, so the change was not pushed to '.404'staging. Changes must be pushed to staging before they can be '.405'landed from the web.'),406);407408case ArcanistDiffWorkflow::STAGING_REPOSITORY_UNCONFIGURED:409return array(410'title' => pht('Repository Unconfigured'),411'body' => pht(412'When this diff was generated, the repository was not configured '.413'with a staging area, so the change was not pushed to staging. '.414'Changes must be pushed to staging before they can be landed '.415'from the web.'),416);417case ArcanistDiffWorkflow::STAGING_CLIENT_UNSUPPORTED:418return array(419'title' => pht('Client Support Unavailable'),420'body' => pht(421'When this diff was generated, the client did not support '.422'staging areas for this version control system, so the change '.423'was not pushed to staging. Changes must be pushed to staging '.424'before they can be landed from the web. Updating the client '.425'may resolve this issue.'),426);427default:428return array(429'title' => pht('Unknown Error'),430'body' => pht(431'When this diff was generated, it was not pushed to staging for '.432'an unknown reason (the status code was "%s"). Changes must be '.433'pushed to staging before they can be landed from the web. '.434'The server may be running an out-of-date version of this '.435'software, and updating may provide more information about this '.436'error.',437$status),438);439}440}441442}443444445