3 final class DrydockLandRepositoryOperation
4 extends DrydockRepositoryOperationType
{
6 const OPCONST
= 'land';
8 const PHASE_PUSH
= 'op.land.push';
9 const PHASE_COMMIT
= 'op.land.commit';
11 public function getOperationDescription(
12 DrydockRepositoryOperation
$operation,
13 PhabricatorUser
$viewer) {
14 return pht('Land Revision');
17 public function getOperationCurrentStatus(
18 DrydockRepositoryOperation
$operation,
19 PhabricatorUser
$viewer) {
21 $target = $operation->getRepositoryTarget();
22 $repository = $operation->getRepository();
23 switch ($operation->getOperationState()) {
24 case DrydockRepositoryOperation
::STATE_WAIT
:
26 'Waiting to land revision into %s on %s...',
27 $repository->getMonogram(),
29 case DrydockRepositoryOperation
::STATE_WORK
:
31 'Landing revision into %s on %s...',
32 $repository->getMonogram(),
34 case DrydockRepositoryOperation
::STATE_DONE
:
36 'Revision landed into %s.',
37 $repository->getMonogram());
41 public function getWorkingCopyMerges(DrydockRepositoryOperation
$operation) {
42 $repository = $operation->getRepository();
45 $object = $operation->getObject();
46 if ($object instanceof DifferentialRevision
) {
47 $diff = $this->loadDiff($operation);
49 'src.uri' => $repository->getStagingURI(),
50 'src.ref' => $diff->getStagingRef(),
55 'Invalid or unknown object ("%s") for land operation, expected '.
56 'Differential Revision.',
57 $operation->getObjectPHID()));
63 public function applyOperation(
64 DrydockRepositoryOperation
$operation,
65 DrydockInterface
$interface) {
66 $viewer = $this->getViewer();
67 $repository = $operation->getRepository();
72 $object = $operation->getObject();
73 if ($object instanceof DifferentialRevision
) {
76 $diff = $this->loadDiff($operation);
78 $dict = $diff->getDiffAuthorshipDict();
79 $author_name = idx($dict, 'authorName');
80 $author_email = idx($dict, 'authorEmail');
82 $api_method = 'differential.getcommitmessage';
84 'revision_id' => $revision->getID(),
87 $commit_message = id(new ConduitCall($api_method, $api_params))
93 'Invalid or unknown object ("%s") for land operation, expected '.
94 'Differential Revision.',
95 $operation->getObjectPHID()));
98 $target = $operation->getRepositoryTarget();
99 list($type, $name) = explode(':', $target, 2);
102 $push_dst = 'refs/heads/'.$name;
107 'Unknown repository operation target type "%s" (in target "%s").',
112 $committer_info = $this->getCommitterInfo($operation);
114 // NOTE: We're doing this commit with "-F -" so we don't run into trouble
115 // with enormous commit messages which might otherwise exceed the maximum
116 // size of a command.
118 $future = $interface->getExecFuture(
119 'git -c user.name=%s -c user.email=%s commit --author %s -F - --',
120 $committer_info['name'],
121 $committer_info['email'],
122 "{$author_name} <{$author_email}>");
124 $future->write($commit_message);
128 } catch (CommandException
$ex) {
129 $display_command = csprintf('git commit');
131 // TODO: One reason this can fail is if the changes have already been
132 // merged. We could try to detect that.
134 $error = DrydockCommandError
::newFromCommandException($ex)
135 ->setPhase(self
::PHASE_COMMIT
)
136 ->setDisplayCommand($display_command);
138 $operation->setCommandError($error->toDictionary());
145 'git push origin -- %s:%s',
148 } catch (CommandException
$ex) {
149 $display_command = csprintf(
150 'git push origin %R:%R',
154 $error = DrydockCommandError
::newFromCommandException($ex)
155 ->setPhase(self
::PHASE_PUSH
)
156 ->setDisplayCommand($display_command);
158 $operation->setCommandError($error->toDictionary());
164 private function getCommitterInfo(DrydockRepositoryOperation
$operation) {
165 $viewer = $this->getViewer();
167 $committer_name = null;
169 $author_phid = $operation->getAuthorPHID();
170 $object = id(new PhabricatorObjectQuery())
172 ->withPHIDs(array($author_phid))
176 if ($object instanceof PhabricatorUser
) {
177 $committer_name = $object->getUsername();
181 if (!strlen($committer_name)) {
182 $committer_name = pht('autocommitter');
185 // TODO: Probably let users choose a VCS email address in settings. For
186 // now just make something up so we don't leak anyone's stuff.
189 'name' => $committer_name,
190 'email' => 'autocommitter@example.com',
194 private function loadDiff(DrydockRepositoryOperation
$operation) {
195 $viewer = $this->getViewer();
196 $revision = $operation->getObject();
198 $diff_phid = $operation->getProperty('differential.diffPHID');
200 $diff = id(new DifferentialDiffQuery())
202 ->withPHIDs(array($diff_phid))
207 'Unable to load diff "%s".',
211 $diff_revid = $diff->getRevisionID();
212 $revision_id = $revision->getID();
213 if ($diff_revid != $revision_id) {
216 'Diff ("%s") has wrong revision ID ("%s", expected "%s").',
225 public function getBarrierToLanding(
226 PhabricatorUser
$viewer,
227 DifferentialRevision
$revision) {
229 $repository = $revision->getRepository();
232 'title' => pht('No Repository'),
234 'This revision is not associated with a known repository. Only '.
235 'revisions associated with a tracked repository can be landed '.
240 if (!$repository->canPerformAutomation()) {
242 'title' => pht('No Repository Automation'),
244 'The repository this revision is associated with ("%s") is not '.
245 'configured to support automation. Configure automation for the '.
246 'repository to enable revisions to be landed automatically.',
247 $repository->getMonogram()),
251 // Check if this diff was pushed to a staging area.
252 $diff = id(new DifferentialDiffQuery())
254 ->withIDs(array($revision->getActiveDiff()->getID()))
255 ->needProperties(true)
258 // Older diffs won't have this property. They may still have been pushed.
259 // At least for now, assume staging changes are present if the property
260 // is missing. This should smooth the transition to the more formal
262 $has_staging = $diff->hasDiffProperty('arc.staging');
264 $staging = $diff->getProperty('arc.staging');
265 if (!is_array($staging)) {
268 $status = idx($staging, 'status');
269 if ($status != ArcanistDiffWorkflow
::STAGING_PUSHED
) {
270 return $this->getBarrierToLandingFromStagingStatus($status);
274 // TODO: At some point we should allow installs to give "land reviewed
275 // code" permission to more users than "push any commit", because it is
276 // a much less powerful operation. For now, just require push so this
277 // doesn't do anything users can't do on their own.
278 $can_push = PhabricatorPolicyFilter
::hasCapability(
281 DiffusionPushCapability
::CAPABILITY
);
284 'title' => pht('Unable to Push'),
286 'You do not have permission to push to the repository this '.
287 'revision is associated with ("%s"), so you can not land it.',
288 $repository->getMonogram()),
292 if ($revision->isAccepted()) {
293 // We can land accepted revisions, so continue below. Otherwise, raise
294 // an error with tailored messaging for the most common cases.
295 } else if ($revision->isAbandoned()) {
297 'title' => pht('Revision Abandoned'),
299 'This revision has been abandoned. Only accepted revisions '.
302 } else if ($revision->isClosed()) {
304 'title' => pht('Revision Closed'),
306 'This revision has already been closed. Only open, accepted '.
307 'revisions may land.'),
311 'title' => pht('Revision Not Accepted'),
313 'This revision is still under review. Only revisions which '.
314 'have been accepted may land.'),
318 // Check for other operations. Eventually this should probably be more
319 // general (e.g., it's OK to land to multiple different branches
320 // simultaneously) but just put this in as a sanity check for now.
321 $other_operations = id(new DrydockRepositoryOperationQuery())
323 ->withObjectPHIDs(array($revision->getPHID()))
324 ->withOperationTypes(
326 $this->getOperationConstant(),
328 ->withOperationStates(
330 DrydockRepositoryOperation
::STATE_WAIT
,
331 DrydockRepositoryOperation
::STATE_WORK
,
332 DrydockRepositoryOperation
::STATE_DONE
,
336 if ($other_operations) {
338 foreach ($other_operations as $operation) {
339 if ($operation->isDone()) {
347 'title' => pht('Already Complete'),
348 'body' => pht('This revision has already landed.'),
352 'title' => pht('Already In Flight'),
353 'body' => pht('This revision is already landing.'),
361 private function getBarrierToLandingFromStagingStatus($status) {
363 case ArcanistDiffWorkflow
::STAGING_USER_SKIP
:
365 'title' => pht('Staging Area Skipped'),
367 'The diff author used the %s flag to skip pushing this change to '.
368 'staging. Changes must be pushed to staging before they can be '.
369 'landed from the web.',
370 phutil_tag('tt', array(), '--skip-staging')),
372 case ArcanistDiffWorkflow
::STAGING_DIFF_RAW
:
374 'title' => pht('Raw Diff Source'),
376 'The diff was generated from a raw input source, so the change '.
377 'could not be pushed to staging. Changes must be pushed to '.
378 'staging before they can be landed from the web.'),
380 case ArcanistDiffWorkflow
::STAGING_REPOSITORY_UNKNOWN
:
382 'title' => pht('Unknown Repository'),
384 'When the diff was generated, the client was not able to '.
385 'determine which repository it belonged to, so the change '.
386 'was not pushed to staging. Changes must be pushed to staging '.
387 'before they can be landed from the web.'),
389 case ArcanistDiffWorkflow
::STAGING_REPOSITORY_UNAVAILABLE
:
391 'title' => pht('Staging Unavailable'),
393 'When this diff was generated, the server was running an older '.
394 'version of the software which did not support staging areas, so '.
395 'the change was not pushed to staging. Changes must be pushed '.
396 'to staging before they can be landed from the web.'),
398 case ArcanistDiffWorkflow
::STAGING_REPOSITORY_UNSUPPORTED
:
400 'title' => pht('Repository Unsupported'),
402 'When this diff was generated, the server was running an older '.
403 'version of the software which did not support staging areas for '.
404 'this version control system, so the change was not pushed to '.
405 'staging. Changes must be pushed to staging before they can be '.
406 'landed from the web.'),
409 case ArcanistDiffWorkflow
::STAGING_REPOSITORY_UNCONFIGURED
:
411 'title' => pht('Repository Unconfigured'),
413 'When this diff was generated, the repository was not configured '.
414 'with a staging area, so the change was not pushed to staging. '.
415 'Changes must be pushed to staging before they can be landed '.
418 case ArcanistDiffWorkflow
::STAGING_CLIENT_UNSUPPORTED
:
420 'title' => pht('Client Support Unavailable'),
422 'When this diff was generated, the client did not support '.
423 'staging areas for this version control system, so the change '.
424 'was not pushed to staging. Changes must be pushed to staging '.
425 'before they can be landed from the web. Updating the client '.
426 'may resolve this issue.'),
430 'title' => pht('Unknown Error'),
432 'When this diff was generated, it was not pushed to staging for '.
433 'an unknown reason (the status code was "%s"). Changes must be '.
434 'pushed to staging before they can be landed from the web. '.
435 'The server may be running an out-of-date version of this '.
436 'software, and updating may provide more information about this '.