@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at recaptime-dev/main 443 lines 15 kB view raw
1<?php 2 3final class DrydockLandRepositoryOperation 4 extends DrydockRepositoryOperationType { 5 6 const OPCONST = 'land'; 7 8 const PHASE_PUSH = 'op.land.push'; 9 const PHASE_COMMIT = 'op.land.commit'; 10 11 public function getOperationDescription( 12 DrydockRepositoryOperation $operation, 13 PhabricatorUser $viewer) { 14 return pht('Land Revision'); 15 } 16 17 public function getOperationCurrentStatus( 18 DrydockRepositoryOperation $operation, 19 PhabricatorUser $viewer) { 20 21 $target = $operation->getRepositoryTarget(); 22 $repository = $operation->getRepository(); 23 switch ($operation->getOperationState()) { 24 case DrydockRepositoryOperation::STATE_WAIT: 25 return pht( 26 'Waiting to land revision into %s on %s...', 27 $repository->getMonogram(), 28 $target); 29 case DrydockRepositoryOperation::STATE_WORK: 30 return pht( 31 'Landing revision into %s on %s...', 32 $repository->getMonogram(), 33 $target); 34 case DrydockRepositoryOperation::STATE_DONE: 35 return pht( 36 'Revision landed into %s.', 37 $repository->getMonogram()); 38 } 39 } 40 41 public function getWorkingCopyMerges(DrydockRepositoryOperation $operation) { 42 $repository = $operation->getRepository(); 43 $merges = array(); 44 45 $object = $operation->getObject(); 46 if ($object instanceof DifferentialRevision) { 47 $diff = $this->loadDiff($operation); 48 $merges[] = array( 49 'src.uri' => $repository->getStagingURI(), 50 'src.ref' => $diff->getStagingRef(), 51 ); 52 } else { 53 throw new Exception( 54 pht( 55 'Invalid or unknown object ("%s") for land operation, expected '. 56 'Differential Revision.', 57 $operation->getObjectPHID())); 58 } 59 60 return $merges; 61 } 62 63 public function applyOperation( 64 DrydockRepositoryOperation $operation, 65 DrydockInterface $interface) { 66 $viewer = $this->getViewer(); 67 $repository = $operation->getRepository(); 68 69 $cmd = array(); 70 $arg = array(); 71 72 $object = $operation->getObject(); 73 if ($object instanceof DifferentialRevision) { 74 $revision = $object; 75 76 $diff = $this->loadDiff($operation); 77 78 $dict = $diff->getDiffAuthorshipDict(); 79 $author_name = idx($dict, 'authorName'); 80 $author_email = idx($dict, 'authorEmail'); 81 82 $api_method = 'differential.getcommitmessage'; 83 $api_params = array( 84 'revision_id' => $revision->getID(), 85 ); 86 87 $commit_message = id(new ConduitCall($api_method, $api_params)) 88 ->setUser($viewer) 89 ->execute(); 90 } else { 91 throw new Exception( 92 pht( 93 'Invalid or unknown object ("%s") for land operation, expected '. 94 'Differential Revision.', 95 $operation->getObjectPHID())); 96 } 97 98 $target = $operation->getRepositoryTarget(); 99 list($type, $name) = explode(':', $target, 2); 100 switch ($type) { 101 case 'branch': 102 $push_dst = 'refs/heads/'.$name; 103 break; 104 default: 105 throw new Exception( 106 pht( 107 'Unknown repository operation target type "%s" (in target "%s").', 108 $type, 109 $target)); 110 } 111 112 $committer_info = $this->getCommitterInfo($operation); 113 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. 117 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}>"); 123 124 $future->write($commit_message); 125 126 try { 127 $future->resolvex(); 128 } catch (CommandException $ex) { 129 $display_command = csprintf('git commit'); 130 131 // TODO: One reason this can fail is if the changes have already been 132 // merged. We could try to detect that. 133 134 $error = DrydockCommandError::newFromCommandException($ex) 135 ->setPhase(self::PHASE_COMMIT) 136 ->setDisplayCommand($display_command); 137 138 $operation->setCommandError($error->toDictionary()); 139 140 throw $ex; 141 } 142 143 try { 144 $interface->execx( 145 'git push origin -- %s:%s', 146 'HEAD', 147 $push_dst); 148 } catch (CommandException $ex) { 149 $display_command = csprintf( 150 'git push origin %R:%R', 151 'HEAD', 152 $push_dst); 153 154 $error = DrydockCommandError::newFromCommandException($ex) 155 ->setPhase(self::PHASE_PUSH) 156 ->setDisplayCommand($display_command); 157 158 $operation->setCommandError($error->toDictionary()); 159 160 throw $ex; 161 } 162 } 163 164 private function getCommitterInfo(DrydockRepositoryOperation $operation) { 165 $viewer = $this->getViewer(); 166 167 $committer_name = null; 168 169 $author_phid = $operation->getAuthorPHID(); 170 $object = id(new PhabricatorObjectQuery()) 171 ->setViewer($viewer) 172 ->withPHIDs(array($author_phid)) 173 ->executeOne(); 174 175 if ($object) { 176 if ($object instanceof PhabricatorUser) { 177 $committer_name = $object->getUsername(); 178 } 179 } 180 181 if (!strlen($committer_name)) { 182 $committer_name = pht('autocommitter'); 183 } 184 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. 187 188 return array( 189 'name' => $committer_name, 190 'email' => 'autocommitter@example.com', 191 ); 192 } 193 194 private function loadDiff(DrydockRepositoryOperation $operation) { 195 $viewer = $this->getViewer(); 196 $revision = $operation->getObject(); 197 198 $diff_phid = $operation->getProperty('differential.diffPHID'); 199 200 $diff = id(new DifferentialDiffQuery()) 201 ->setViewer($viewer) 202 ->withPHIDs(array($diff_phid)) 203 ->executeOne(); 204 if (!$diff) { 205 throw new Exception( 206 pht( 207 'Unable to load diff "%s".', 208 $diff_phid)); 209 } 210 211 $diff_revid = $diff->getRevisionID(); 212 $revision_id = $revision->getID(); 213 if ($diff_revid != $revision_id) { 214 throw new Exception( 215 pht( 216 'Diff ("%s") has wrong revision ID ("%s", expected "%s").', 217 $diff_phid, 218 $diff_revid, 219 $revision_id)); 220 } 221 222 return $diff; 223 } 224 225 public function getBarrierToLanding( 226 PhabricatorUser $viewer, 227 DifferentialRevision $revision) { 228 229 $repository = $revision->getRepository(); 230 if (!$repository) { 231 return array( 232 'title' => pht('No Repository'), 233 'body' => pht( 234 'This revision is not associated with a known repository. Only '. 235 'revisions associated with a tracked repository can be landed '. 236 'automatically.'), 237 ); 238 } 239 240 if (!$repository->canPerformAutomation()) { 241 return array( 242 'title' => pht('No Repository Automation'), 243 'body' => pht( 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()), 248 ); 249 } 250 251 // Check if this diff was pushed to a staging area. 252 $diff = id(new DifferentialDiffQuery()) 253 ->setViewer($viewer) 254 ->withIDs(array($revision->getActiveDiff()->getID())) 255 ->needProperties(true) 256 ->executeOne(); 257 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 261 // approach. 262 $has_staging = $diff->hasDiffProperty('arc.staging'); 263 if ($has_staging) { 264 $staging = $diff->getProperty('arc.staging'); 265 if (!is_array($staging)) { 266 $staging = array(); 267 } 268 $status = idx($staging, 'status'); 269 if ($status != ArcanistDiffWorkflow::STAGING_PUSHED) { 270 return $this->getBarrierToLandingFromStagingStatus($status); 271 } 272 } 273 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( 279 $viewer, 280 $repository, 281 DiffusionPushCapability::CAPABILITY); 282 if (!$can_push) { 283 return array( 284 'title' => pht('Unable to Push'), 285 'body' => pht( 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()), 289 ); 290 } 291 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()) { 296 return array( 297 'title' => pht('Revision Abandoned'), 298 'body' => pht( 299 'This revision has been abandoned. Only accepted revisions '. 300 'may land.'), 301 ); 302 } else if ($revision->isClosed()) { 303 return array( 304 'title' => pht('Revision Closed'), 305 'body' => pht( 306 'This revision has already been closed. Only open, accepted '. 307 'revisions may land.'), 308 ); 309 } else { 310 return array( 311 'title' => pht('Revision Not Accepted'), 312 'body' => pht( 313 'This revision is still under review. Only revisions which '. 314 'have been accepted may land.'), 315 ); 316 } 317 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()) 322 ->setViewer($viewer) 323 ->withObjectPHIDs(array($revision->getPHID())) 324 ->withOperationTypes( 325 array( 326 $this->getOperationConstant(), 327 )) 328 ->withOperationStates( 329 array( 330 DrydockRepositoryOperation::STATE_WAIT, 331 DrydockRepositoryOperation::STATE_WORK, 332 DrydockRepositoryOperation::STATE_DONE, 333 )) 334 ->execute(); 335 336 if ($other_operations) { 337 $any_done = false; 338 foreach ($other_operations as $operation) { 339 if ($operation->isDone()) { 340 $any_done = true; 341 break; 342 } 343 } 344 345 if ($any_done) { 346 return array( 347 'title' => pht('Already Complete'), 348 'body' => pht('This revision has already landed.'), 349 ); 350 } else { 351 return array( 352 'title' => pht('Already In Flight'), 353 'body' => pht('This revision is already landing.'), 354 ); 355 } 356 } 357 358 return null; 359 } 360 361 private function getBarrierToLandingFromStagingStatus($status) { 362 switch ($status) { 363 case ArcanistDiffWorkflow::STAGING_USER_SKIP: 364 return array( 365 'title' => pht('Staging Area Skipped'), 366 'body' => pht( 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')), 371 ); 372 case ArcanistDiffWorkflow::STAGING_DIFF_RAW: 373 return array( 374 'title' => pht('Raw Diff Source'), 375 'body' => pht( 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.'), 379 ); 380 case ArcanistDiffWorkflow::STAGING_REPOSITORY_UNKNOWN: 381 return array( 382 'title' => pht('Unknown Repository'), 383 'body' => pht( 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.'), 388 ); 389 case ArcanistDiffWorkflow::STAGING_REPOSITORY_UNAVAILABLE: 390 return array( 391 'title' => pht('Staging Unavailable'), 392 'body' => pht( 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.'), 397 ); 398 case ArcanistDiffWorkflow::STAGING_REPOSITORY_UNSUPPORTED: 399 return array( 400 'title' => pht('Repository Unsupported'), 401 'body' => pht( 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.'), 407 ); 408 409 case ArcanistDiffWorkflow::STAGING_REPOSITORY_UNCONFIGURED: 410 return array( 411 'title' => pht('Repository Unconfigured'), 412 'body' => pht( 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 '. 416 'from the web.'), 417 ); 418 case ArcanistDiffWorkflow::STAGING_CLIENT_UNSUPPORTED: 419 return array( 420 'title' => pht('Client Support Unavailable'), 421 'body' => pht( 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.'), 427 ); 428 default: 429 return array( 430 'title' => pht('Unknown Error'), 431 'body' => pht( 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 '. 437 'error.', 438 $status), 439 ); 440 } 441 } 442 443}