mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-22 05:20:56 +01:00
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
This commit is contained in:
parent
821ba8b22e
commit
19eee427ad
6 changed files with 204 additions and 72 deletions
|
@ -886,7 +886,7 @@ phutil_register_library_map(array(
|
||||||
'DrydockBlueprintTransactionQuery' => 'applications/drydock/query/DrydockBlueprintTransactionQuery.php',
|
'DrydockBlueprintTransactionQuery' => 'applications/drydock/query/DrydockBlueprintTransactionQuery.php',
|
||||||
'DrydockBlueprintViewController' => 'applications/drydock/controller/DrydockBlueprintViewController.php',
|
'DrydockBlueprintViewController' => 'applications/drydock/controller/DrydockBlueprintViewController.php',
|
||||||
'DrydockCommand' => 'applications/drydock/storage/DrydockCommand.php',
|
'DrydockCommand' => 'applications/drydock/storage/DrydockCommand.php',
|
||||||
'DrydockCommandError' => 'applications/drydock/DrydockCommandError/DrydockCommandError.php',
|
'DrydockCommandError' => 'applications/drydock/exception/DrydockCommandError.php',
|
||||||
'DrydockCommandInterface' => 'applications/drydock/interface/command/DrydockCommandInterface.php',
|
'DrydockCommandInterface' => 'applications/drydock/interface/command/DrydockCommandInterface.php',
|
||||||
'DrydockCommandQuery' => 'applications/drydock/query/DrydockCommandQuery.php',
|
'DrydockCommandQuery' => 'applications/drydock/query/DrydockCommandQuery.php',
|
||||||
'DrydockConsoleController' => 'applications/drydock/controller/DrydockConsoleController.php',
|
'DrydockConsoleController' => 'applications/drydock/controller/DrydockConsoleController.php',
|
||||||
|
@ -5014,6 +5014,7 @@ phutil_register_library_map(array(
|
||||||
'DrydockDAO',
|
'DrydockDAO',
|
||||||
'PhabricatorPolicyInterface',
|
'PhabricatorPolicyInterface',
|
||||||
),
|
),
|
||||||
|
'DrydockCommandError' => 'Phobject',
|
||||||
'DrydockCommandInterface' => 'DrydockInterface',
|
'DrydockCommandInterface' => 'DrydockInterface',
|
||||||
'DrydockCommandQuery' => 'DrydockQuery',
|
'DrydockCommandQuery' => 'DrydockQuery',
|
||||||
'DrydockConsoleController' => 'DrydockController',
|
'DrydockConsoleController' => 'DrydockController',
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
class DrydockCommandError {
|
|
||||||
public static function newFromCommandException(
|
|
||||||
$phase,
|
|
||||||
$command,
|
|
||||||
CommandException $ex) {
|
|
||||||
$error = array(
|
|
||||||
'phase' => $phase,
|
|
||||||
'command' => (string)$command,
|
|
||||||
'raw' => (string)$ex->getCommand(),
|
|
||||||
'err' => $ex->getError(),
|
|
||||||
'stdout' => $ex->getStdout(),
|
|
||||||
'stderr' => $ex->getStderr(),
|
|
||||||
);
|
|
||||||
return $error;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,6 +4,8 @@ final class DrydockWorkingCopyBlueprintImplementation
|
||||||
extends DrydockBlueprintImplementation {
|
extends DrydockBlueprintImplementation {
|
||||||
|
|
||||||
const PHASE_SQUASHMERGE = 'squashmerge';
|
const PHASE_SQUASHMERGE = 'squashmerge';
|
||||||
|
const PHASE_REMOTEFETCH = 'blueprint.workingcopy.fetch.remote';
|
||||||
|
const PHASE_MERGEFETCH = 'blueprint.workingcopy.fetch.staging';
|
||||||
|
|
||||||
public function isEnabled() {
|
public function isEnabled() {
|
||||||
return true;
|
return true;
|
||||||
|
@ -240,11 +242,11 @@ final class DrydockWorkingCopyBlueprintImplementation
|
||||||
|
|
||||||
$default = null;
|
$default = null;
|
||||||
foreach ($map as $directory => $spec) {
|
foreach ($map as $directory => $spec) {
|
||||||
|
$interface->pushWorkingDirectory("{$root}/repo/{$directory}/");
|
||||||
|
|
||||||
$cmd = array();
|
$cmd = array();
|
||||||
$arg = array();
|
$arg = array();
|
||||||
|
|
||||||
$interface->pushWorkingDirectory("{$root}/repo/{$directory}/");
|
|
||||||
|
|
||||||
$cmd[] = 'git clean -d --force';
|
$cmd[] = 'git clean -d --force';
|
||||||
$cmd[] = 'git fetch';
|
$cmd[] = 'git fetch';
|
||||||
|
|
||||||
|
@ -266,7 +268,20 @@ final class DrydockWorkingCopyBlueprintImplementation
|
||||||
|
|
||||||
$cmd[] = 'git reset --hard origin/%s';
|
$cmd[] = 'git reset --hard origin/%s';
|
||||||
$arg[] = $branch;
|
$arg[] = $branch;
|
||||||
} else if ($ref) {
|
}
|
||||||
|
|
||||||
|
$this->execxv($interface, $cmd, $arg);
|
||||||
|
|
||||||
|
if (idx($spec, 'default')) {
|
||||||
|
$default = $directory;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're fetching a ref from a remote, do that separately so we can
|
||||||
|
// raise a more tailored error.
|
||||||
|
if ($ref) {
|
||||||
|
$cmd = array();
|
||||||
|
$arg = array();
|
||||||
|
|
||||||
$ref_uri = $ref['uri'];
|
$ref_uri = $ref['uri'];
|
||||||
$ref_ref = $ref['ref'];
|
$ref_ref = $ref['ref'];
|
||||||
|
|
||||||
|
@ -277,17 +292,25 @@ final class DrydockWorkingCopyBlueprintImplementation
|
||||||
|
|
||||||
$cmd[] = 'git checkout %s --';
|
$cmd[] = 'git checkout %s --';
|
||||||
$arg[] = $ref_ref;
|
$arg[] = $ref_ref;
|
||||||
}
|
|
||||||
|
|
||||||
$cmd = implode(' && ', $cmd);
|
try {
|
||||||
$argv = array_merge(array($cmd), $arg);
|
$this->execxv($interface, $cmd, $arg);
|
||||||
|
} catch (CommandException $ex) {
|
||||||
|
$display_command = csprintf(
|
||||||
|
'git fetch %R %R',
|
||||||
|
$ref_uri,
|
||||||
|
$ref_ref);
|
||||||
|
|
||||||
$result = call_user_func_array(
|
$error = DrydockCommandError::newFromCommandException($ex)
|
||||||
array($interface, 'execx'),
|
->setPhase(self::PHASE_REMOTEFETCH)
|
||||||
$argv);
|
->setDisplayCommand($display_command);
|
||||||
|
|
||||||
if (idx($spec, 'default')) {
|
$lease->setAttribute(
|
||||||
$default = $directory;
|
'workingcopy.vcs.error',
|
||||||
|
$error->toDictionary());
|
||||||
|
|
||||||
|
throw $ex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$merges = idx($spec, 'merges');
|
$merges = idx($spec, 'merges');
|
||||||
|
@ -428,11 +451,29 @@ final class DrydockWorkingCopyBlueprintImplementation
|
||||||
$src_uri = $merge['src.uri'];
|
$src_uri = $merge['src.uri'];
|
||||||
$src_ref = $merge['src.ref'];
|
$src_ref = $merge['src.ref'];
|
||||||
|
|
||||||
$interface->execx(
|
|
||||||
'git fetch --no-tags -- %s +%s:%s',
|
try {
|
||||||
$src_uri,
|
$interface->execx(
|
||||||
$src_ref,
|
'git fetch --no-tags -- %s +%s:%s',
|
||||||
$src_ref);
|
$src_uri,
|
||||||
|
$src_ref,
|
||||||
|
$src_ref);
|
||||||
|
} catch (CommandException $ex) {
|
||||||
|
$display_command = csprintf(
|
||||||
|
'git fetch %R +%R:%R',
|
||||||
|
$src_uri,
|
||||||
|
$src_ref,
|
||||||
|
$src_ref);
|
||||||
|
|
||||||
|
$error = DrydockCommandError::newFromCommandException($ex)
|
||||||
|
->setPhase(self::PHASE_MERGEFETCH)
|
||||||
|
->setDisplayCommand($display_command);
|
||||||
|
|
||||||
|
$lease->setAttribute('workingcopy.vcs.error', $error->toDictionary());
|
||||||
|
|
||||||
|
throw $ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// NOTE: This can never actually generate a commit because we pass
|
// NOTE: This can never actually generate a commit because we pass
|
||||||
// "--squash", but git sometimes runs code to check that a username and
|
// "--squash", but git sometimes runs code to check that a username and
|
||||||
|
@ -443,32 +484,36 @@ final class DrydockWorkingCopyBlueprintImplementation
|
||||||
'drydock@phabricator',
|
'drydock@phabricator',
|
||||||
$src_ref);
|
$src_ref);
|
||||||
|
|
||||||
// Show the user a simplified command if the operation fails and we need to
|
|
||||||
// report an error.
|
|
||||||
$show_command = csprintf(
|
|
||||||
'git merge --squash -- %R',
|
|
||||||
$src_ref);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$interface->execx('%C', $real_command);
|
$interface->execx('%C', $real_command);
|
||||||
} catch (CommandException $ex) {
|
} catch (CommandException $ex) {
|
||||||
$error = DrydockCommandError::newFromCommandException(
|
$display_command = csprintf(
|
||||||
self::PHASE_SQUASHMERGE,
|
'git merge --squash %R',
|
||||||
$show_command,
|
$src_ref);
|
||||||
$ex);
|
|
||||||
|
|
||||||
$lease->setAttribute('workingcopy.vcs.error', $error);
|
$error = DrydockCommandError::newFromCommandException($ex)
|
||||||
|
->setPhase(self::PHASE_SQUASHMERGE)
|
||||||
|
->setDisplayCommand($display_command);
|
||||||
|
|
||||||
|
$lease->setAttribute('workingcopy.vcs.error', $error->toDictionary());
|
||||||
throw $ex;
|
throw $ex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCommandError(DrydockLease $lease) {
|
public function getCommandError(DrydockLease $lease) {
|
||||||
$error = $lease->getAttribute('workingcopy.vcs.error');
|
return $lease->getAttribute('workingcopy.vcs.error');
|
||||||
if (!$error) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return $error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function execxv(
|
||||||
|
DrydockCommandInterface $interface,
|
||||||
|
array $commands,
|
||||||
|
array $arguments) {
|
||||||
|
|
||||||
|
$commands = implode(' && ', $commands);
|
||||||
|
$argv = array_merge(array($commands), $arguments);
|
||||||
|
|
||||||
|
return call_user_func_array(array($interface, 'execx'), $argv);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
58
src/applications/drydock/exception/DrydockCommandError.php
Normal file
58
src/applications/drydock/exception/DrydockCommandError.php
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class DrydockCommandError extends Phobject {
|
||||||
|
|
||||||
|
private $phase;
|
||||||
|
private $displayCommand;
|
||||||
|
private $command;
|
||||||
|
private $error;
|
||||||
|
private $stdout;
|
||||||
|
private $stderr;
|
||||||
|
|
||||||
|
public static function newFromCommandException(CommandException $ex) {
|
||||||
|
$error = new self();
|
||||||
|
|
||||||
|
$error->command = (string)$ex->getCommand();
|
||||||
|
|
||||||
|
$error->error = $ex->getError();
|
||||||
|
$error->stdout = $ex->getStdout();
|
||||||
|
$error->stderr = $ex->getStderr();
|
||||||
|
|
||||||
|
return $error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPhase($phase) {
|
||||||
|
$this->phase = $phase;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPhase() {
|
||||||
|
return $this->phase;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDisplayCommand($display_command) {
|
||||||
|
$this->displayCommand = (string)$display_command;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDisplayCommand() {
|
||||||
|
return $this->displayCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toDictionary() {
|
||||||
|
$display_command = $this->getDisplayCommand();
|
||||||
|
if ($display_command === null) {
|
||||||
|
$display_command = $this->command;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'phase' => $this->getPhase(),
|
||||||
|
'command' => $display_command,
|
||||||
|
'raw' => $this->command,
|
||||||
|
'err' => $this->error,
|
||||||
|
'stdout' => $this->stdout,
|
||||||
|
'stderr' => $this->stderr,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -4,7 +4,9 @@ final class DrydockLandRepositoryOperation
|
||||||
extends DrydockRepositoryOperationType {
|
extends DrydockRepositoryOperationType {
|
||||||
|
|
||||||
const OPCONST = 'land';
|
const OPCONST = 'land';
|
||||||
const PHASE_PUSH = 'push';
|
|
||||||
|
const PHASE_PUSH = 'op.land.push';
|
||||||
|
const PHASE_COMMIT = 'op.land.commit';
|
||||||
|
|
||||||
public function getOperationDescription(
|
public function getOperationDescription(
|
||||||
DrydockRepositoryOperation $operation,
|
DrydockRepositoryOperation $operation,
|
||||||
|
@ -119,25 +121,42 @@ final class DrydockLandRepositoryOperation
|
||||||
$committer_info['email'],
|
$committer_info['email'],
|
||||||
"{$author_name} <{$author_email}>");
|
"{$author_name} <{$author_email}>");
|
||||||
|
|
||||||
$future
|
$future->write($commit_message);
|
||||||
->write($commit_message)
|
|
||||||
->resolvex();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$interface->execx(
|
$future->resolvex();
|
||||||
'git push origin -- %s:%s',
|
|
||||||
'HEAD',
|
|
||||||
$push_dst);
|
|
||||||
} catch (CommandException $ex) {
|
} catch (CommandException $ex) {
|
||||||
$show_command = csprintf(
|
$display_command = csprintf('git commit');
|
||||||
'git push origin -- %s:%s',
|
|
||||||
'HEAD',
|
// TODO: One reason this can fail is if the changes have already been
|
||||||
$push_dst);
|
// merged. We could try to detect that.
|
||||||
$error = DrydockCommandError::newFromCommandException(
|
|
||||||
self::PHASE_PUSH,
|
$error = DrydockCommandError::newFromCommandException($ex)
|
||||||
$show_command,
|
->setPhase(self::PHASE_COMMIT)
|
||||||
$ex);
|
->setDisplayCommand($display_command);
|
||||||
$operation->setCommandError($error);
|
|
||||||
|
$operation->setCommandError($error->toDictionary());
|
||||||
|
|
||||||
|
throw $ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$interface->execx(
|
||||||
|
'git push origin -- %s:%s',
|
||||||
|
'HEAD',
|
||||||
|
$push_dst);
|
||||||
|
} catch (CommandException $ex) {
|
||||||
|
$display_command = csprintf(
|
||||||
|
'git push origin %R:%R',
|
||||||
|
'HEAD',
|
||||||
|
$push_dst);
|
||||||
|
|
||||||
|
$error = DrydockCommandError::newFromCommandException($ex)
|
||||||
|
->setPhase(self::PHASE_PUSH)
|
||||||
|
->setDisplayCommand($display_command);
|
||||||
|
|
||||||
|
$operation->setCommandError($error->toDictionary());
|
||||||
|
|
||||||
throw $ex;
|
throw $ex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,6 +82,20 @@ final class DrydockRepositoryOperationStatusView
|
||||||
'This change did not merge cleanly. This usually indicates '.
|
'This change did not merge cleanly. This usually indicates '.
|
||||||
'that the change is out of date and needs to be updated.');
|
'that the change is out of date and needs to be updated.');
|
||||||
break;
|
break;
|
||||||
|
case DrydockWorkingCopyBlueprintImplementation::PHASE_REMOTEFETCH:
|
||||||
|
$message = pht(
|
||||||
|
'This change could not be fetched from the remote.');
|
||||||
|
break;
|
||||||
|
case DrydockWorkingCopyBlueprintImplementation::PHASE_MERGEFETCH:
|
||||||
|
$message = pht(
|
||||||
|
'This change could not be fetched from the remote staging '.
|
||||||
|
'area. It may not have been pushed, or may have been removed.');
|
||||||
|
break;
|
||||||
|
case DrydockLandRepositoryOperation::PHASE_COMMIT:
|
||||||
|
$message = pht(
|
||||||
|
'Committing this change failed. It may already have been '.
|
||||||
|
'merged.');
|
||||||
|
break;
|
||||||
case DrydockLandRepositoryOperation::PHASE_PUSH:
|
case DrydockLandRepositoryOperation::PHASE_PUSH:
|
||||||
$message = pht(
|
$message = pht(
|
||||||
'The push failed. This usually indicates '.
|
'The push failed. This usually indicates '.
|
||||||
|
@ -123,10 +137,23 @@ final class DrydockRepositoryOperationStatusView
|
||||||
|
|
||||||
private function renderVCSErrorTable(array $vcs_error) {
|
private function renderVCSErrorTable(array $vcs_error) {
|
||||||
$rows = array();
|
$rows = array();
|
||||||
$rows[] = array(pht('Command'), $vcs_error['command']);
|
|
||||||
|
$rows[] = array(
|
||||||
|
pht('Command'),
|
||||||
|
phutil_censor_credentials($vcs_error['command']),
|
||||||
|
);
|
||||||
|
|
||||||
$rows[] = array(pht('Error'), $vcs_error['err']);
|
$rows[] = array(pht('Error'), $vcs_error['err']);
|
||||||
$rows[] = array(pht('Stdout'), $vcs_error['stdout']);
|
|
||||||
$rows[] = array(pht('Stderr'), $vcs_error['stderr']);
|
$rows[] = array(
|
||||||
|
pht('Stdout'),
|
||||||
|
phutil_censor_credentials($vcs_error['stdout']),
|
||||||
|
);
|
||||||
|
|
||||||
|
$rows[] = array(
|
||||||
|
pht('Stderr'),
|
||||||
|
phutil_censor_credentials($vcs_error['stderr']),
|
||||||
|
);
|
||||||
|
|
||||||
$table = id(new AphrontTableView($rows))
|
$table = id(new AphrontTableView($rows))
|
||||||
->setColumnClasses(
|
->setColumnClasses(
|
||||||
|
|
Loading…
Reference in a new issue