@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.

Improve Drydock errors for empty commits and missing changes

Summary: Ref T10093. Show better errors when a commit fails because it has already been merged and when a fetch fails because the ref isn't present in the remote.

Test Plan:
{F1160794}

{F1160795}

Reviewers: chad

Reviewed By: chad

Subscribers: michaeljs1990, yelirekim

Maniphest Tasks: T10093

Differential Revision: https://secure.phabricator.com/D15420

+204 -72
+2 -1
src/__phutil_library_map__.php
··· 886 886 'DrydockBlueprintTransactionQuery' => 'applications/drydock/query/DrydockBlueprintTransactionQuery.php', 887 887 'DrydockBlueprintViewController' => 'applications/drydock/controller/DrydockBlueprintViewController.php', 888 888 'DrydockCommand' => 'applications/drydock/storage/DrydockCommand.php', 889 - 'DrydockCommandError' => 'applications/drydock/DrydockCommandError/DrydockCommandError.php', 889 + 'DrydockCommandError' => 'applications/drydock/exception/DrydockCommandError.php', 890 890 'DrydockCommandInterface' => 'applications/drydock/interface/command/DrydockCommandInterface.php', 891 891 'DrydockCommandQuery' => 'applications/drydock/query/DrydockCommandQuery.php', 892 892 'DrydockConsoleController' => 'applications/drydock/controller/DrydockConsoleController.php', ··· 5014 5014 'DrydockDAO', 5015 5015 'PhabricatorPolicyInterface', 5016 5016 ), 5017 + 'DrydockCommandError' => 'Phobject', 5017 5018 'DrydockCommandInterface' => 'DrydockInterface', 5018 5019 'DrydockCommandQuery' => 'DrydockQuery', 5019 5020 'DrydockConsoleController' => 'DrydockController',
-18
src/applications/drydock/DrydockCommandError/DrydockCommandError.php
··· 1 - <?php 2 - 3 - class DrydockCommandError { 4 - public static function newFromCommandException( 5 - $phase, 6 - $command, 7 - CommandException $ex) { 8 - $error = array( 9 - 'phase' => $phase, 10 - 'command' => (string)$command, 11 - 'raw' => (string)$ex->getCommand(), 12 - 'err' => $ex->getError(), 13 - 'stdout' => $ex->getStdout(), 14 - 'stderr' => $ex->getStderr(), 15 - ); 16 - return $error; 17 - } 18 - }
+78 -33
src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php
··· 4 4 extends DrydockBlueprintImplementation { 5 5 6 6 const PHASE_SQUASHMERGE = 'squashmerge'; 7 + const PHASE_REMOTEFETCH = 'blueprint.workingcopy.fetch.remote'; 8 + const PHASE_MERGEFETCH = 'blueprint.workingcopy.fetch.staging'; 7 9 8 10 public function isEnabled() { 9 11 return true; ··· 240 242 241 243 $default = null; 242 244 foreach ($map as $directory => $spec) { 245 + $interface->pushWorkingDirectory("{$root}/repo/{$directory}/"); 246 + 243 247 $cmd = array(); 244 248 $arg = array(); 245 - 246 - $interface->pushWorkingDirectory("{$root}/repo/{$directory}/"); 247 249 248 250 $cmd[] = 'git clean -d --force'; 249 251 $cmd[] = 'git fetch'; ··· 266 268 267 269 $cmd[] = 'git reset --hard origin/%s'; 268 270 $arg[] = $branch; 269 - } else if ($ref) { 271 + } 272 + 273 + $this->execxv($interface, $cmd, $arg); 274 + 275 + if (idx($spec, 'default')) { 276 + $default = $directory; 277 + } 278 + 279 + // If we're fetching a ref from a remote, do that separately so we can 280 + // raise a more tailored error. 281 + if ($ref) { 282 + $cmd = array(); 283 + $arg = array(); 284 + 270 285 $ref_uri = $ref['uri']; 271 286 $ref_ref = $ref['ref']; 272 287 ··· 277 292 278 293 $cmd[] = 'git checkout %s --'; 279 294 $arg[] = $ref_ref; 280 - } 295 + 296 + try { 297 + $this->execxv($interface, $cmd, $arg); 298 + } catch (CommandException $ex) { 299 + $display_command = csprintf( 300 + 'git fetch %R %R', 301 + $ref_uri, 302 + $ref_ref); 281 303 282 - $cmd = implode(' && ', $cmd); 283 - $argv = array_merge(array($cmd), $arg); 304 + $error = DrydockCommandError::newFromCommandException($ex) 305 + ->setPhase(self::PHASE_REMOTEFETCH) 306 + ->setDisplayCommand($display_command); 284 307 285 - $result = call_user_func_array( 286 - array($interface, 'execx'), 287 - $argv); 308 + $lease->setAttribute( 309 + 'workingcopy.vcs.error', 310 + $error->toDictionary()); 288 311 289 - if (idx($spec, 'default')) { 290 - $default = $directory; 312 + throw $ex; 313 + } 291 314 } 292 315 293 316 $merges = idx($spec, 'merges'); ··· 428 451 $src_uri = $merge['src.uri']; 429 452 $src_ref = $merge['src.ref']; 430 453 431 - $interface->execx( 432 - 'git fetch --no-tags -- %s +%s:%s', 433 - $src_uri, 434 - $src_ref, 435 - $src_ref); 454 + 455 + try { 456 + $interface->execx( 457 + 'git fetch --no-tags -- %s +%s:%s', 458 + $src_uri, 459 + $src_ref, 460 + $src_ref); 461 + } catch (CommandException $ex) { 462 + $display_command = csprintf( 463 + 'git fetch %R +%R:%R', 464 + $src_uri, 465 + $src_ref, 466 + $src_ref); 467 + 468 + $error = DrydockCommandError::newFromCommandException($ex) 469 + ->setPhase(self::PHASE_MERGEFETCH) 470 + ->setDisplayCommand($display_command); 471 + 472 + $lease->setAttribute('workingcopy.vcs.error', $error->toDictionary()); 473 + 474 + throw $ex; 475 + } 476 + 436 477 437 478 // NOTE: This can never actually generate a commit because we pass 438 479 // "--squash", but git sometimes runs code to check that a username and ··· 443 484 'drydock@phabricator', 444 485 $src_ref); 445 486 446 - // Show the user a simplified command if the operation fails and we need to 447 - // report an error. 448 - $show_command = csprintf( 449 - 'git merge --squash -- %R', 450 - $src_ref); 451 - 452 487 try { 453 488 $interface->execx('%C', $real_command); 454 489 } catch (CommandException $ex) { 455 - $error = DrydockCommandError::newFromCommandException( 456 - self::PHASE_SQUASHMERGE, 457 - $show_command, 458 - $ex); 490 + $display_command = csprintf( 491 + 'git merge --squash %R', 492 + $src_ref); 493 + 494 + $error = DrydockCommandError::newFromCommandException($ex) 495 + ->setPhase(self::PHASE_SQUASHMERGE) 496 + ->setDisplayCommand($display_command); 459 497 460 - $lease->setAttribute('workingcopy.vcs.error', $error); 498 + $lease->setAttribute('workingcopy.vcs.error', $error->toDictionary()); 461 499 throw $ex; 462 500 } 463 501 } 464 502 465 503 public function getCommandError(DrydockLease $lease) { 466 - $error = $lease->getAttribute('workingcopy.vcs.error'); 467 - if (!$error) { 468 - return null; 469 - } else { 470 - return $error; 471 - } 504 + return $lease->getAttribute('workingcopy.vcs.error'); 505 + } 506 + 507 + private function execxv( 508 + DrydockCommandInterface $interface, 509 + array $commands, 510 + array $arguments) { 511 + 512 + $commands = implode(' && ', $commands); 513 + $argv = array_merge(array($commands), $arguments); 514 + 515 + return call_user_func_array(array($interface, 'execx'), $argv); 472 516 } 517 + 473 518 474 519 }
+58
src/applications/drydock/exception/DrydockCommandError.php
··· 1 + <?php 2 + 3 + final class DrydockCommandError extends Phobject { 4 + 5 + private $phase; 6 + private $displayCommand; 7 + private $command; 8 + private $error; 9 + private $stdout; 10 + private $stderr; 11 + 12 + public static function newFromCommandException(CommandException $ex) { 13 + $error = new self(); 14 + 15 + $error->command = (string)$ex->getCommand(); 16 + 17 + $error->error = $ex->getError(); 18 + $error->stdout = $ex->getStdout(); 19 + $error->stderr = $ex->getStderr(); 20 + 21 + return $error; 22 + } 23 + 24 + public function setPhase($phase) { 25 + $this->phase = $phase; 26 + return $this; 27 + } 28 + 29 + public function getPhase() { 30 + return $this->phase; 31 + } 32 + 33 + public function setDisplayCommand($display_command) { 34 + $this->displayCommand = (string)$display_command; 35 + return $this; 36 + } 37 + 38 + public function getDisplayCommand() { 39 + return $this->displayCommand; 40 + } 41 + 42 + public function toDictionary() { 43 + $display_command = $this->getDisplayCommand(); 44 + if ($display_command === null) { 45 + $display_command = $this->command; 46 + } 47 + 48 + return array( 49 + 'phase' => $this->getPhase(), 50 + 'command' => $display_command, 51 + 'raw' => $this->command, 52 + 'err' => $this->error, 53 + 'stdout' => $this->stdout, 54 + 'stderr' => $this->stderr, 55 + ); 56 + } 57 + 58 + }
+36 -17
src/applications/drydock/operation/DrydockLandRepositoryOperation.php
··· 4 4 extends DrydockRepositoryOperationType { 5 5 6 6 const OPCONST = 'land'; 7 - const PHASE_PUSH = 'push'; 7 + 8 + const PHASE_PUSH = 'op.land.push'; 9 + const PHASE_COMMIT = 'op.land.commit'; 8 10 9 11 public function getOperationDescription( 10 12 DrydockRepositoryOperation $operation, ··· 119 121 $committer_info['email'], 120 122 "{$author_name} <{$author_email}>"); 121 123 122 - $future 123 - ->write($commit_message) 124 - ->resolvex(); 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 + } 125 142 126 143 try { 127 - $interface->execx( 128 - 'git push origin -- %s:%s', 129 - 'HEAD', 130 - $push_dst); 144 + $interface->execx( 145 + 'git push origin -- %s:%s', 146 + 'HEAD', 147 + $push_dst); 131 148 } catch (CommandException $ex) { 132 - $show_command = csprintf( 133 - 'git push origin -- %s:%s', 134 - 'HEAD', 135 - $push_dst); 136 - $error = DrydockCommandError::newFromCommandException( 137 - self::PHASE_PUSH, 138 - $show_command, 139 - $ex); 140 - $operation->setCommandError($error); 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 + 141 160 throw $ex; 142 161 } 143 162 }
+30 -3
src/applications/drydock/view/DrydockRepositoryOperationStatusView.php
··· 82 82 'This change did not merge cleanly. This usually indicates '. 83 83 'that the change is out of date and needs to be updated.'); 84 84 break; 85 + case DrydockWorkingCopyBlueprintImplementation::PHASE_REMOTEFETCH: 86 + $message = pht( 87 + 'This change could not be fetched from the remote.'); 88 + break; 89 + case DrydockWorkingCopyBlueprintImplementation::PHASE_MERGEFETCH: 90 + $message = pht( 91 + 'This change could not be fetched from the remote staging '. 92 + 'area. It may not have been pushed, or may have been removed.'); 93 + break; 94 + case DrydockLandRepositoryOperation::PHASE_COMMIT: 95 + $message = pht( 96 + 'Committing this change failed. It may already have been '. 97 + 'merged.'); 98 + break; 85 99 case DrydockLandRepositoryOperation::PHASE_PUSH: 86 100 $message = pht( 87 101 'The push failed. This usually indicates '. ··· 123 137 124 138 private function renderVCSErrorTable(array $vcs_error) { 125 139 $rows = array(); 126 - $rows[] = array(pht('Command'), $vcs_error['command']); 140 + 141 + $rows[] = array( 142 + pht('Command'), 143 + phutil_censor_credentials($vcs_error['command']), 144 + ); 145 + 127 146 $rows[] = array(pht('Error'), $vcs_error['err']); 128 - $rows[] = array(pht('Stdout'), $vcs_error['stdout']); 129 - $rows[] = array(pht('Stderr'), $vcs_error['stderr']); 147 + 148 + $rows[] = array( 149 + pht('Stdout'), 150 + phutil_censor_credentials($vcs_error['stdout']), 151 + ); 152 + 153 + $rows[] = array( 154 + pht('Stderr'), 155 + phutil_censor_credentials($vcs_error['stderr']), 156 + ); 130 157 131 158 $table = id(new AphrontTableView($rows)) 132 159 ->setColumnClasses(