Skip to content

Commit

Permalink
Improvements to the push command:
Browse files Browse the repository at this point in the history
* Allow the current directory not to be mapped to a project - it need only be a
Git repository.
* Also allow pushing to a different project (other than the one mapped to the
directory).
* Only add or change the Git remote if `--set-upstream` is given. Recommend
`set-remote` otherwise.
* State what will happen and then ask for confirmation before pushing in all
interactive cases (not just when pushing to production).
  • Loading branch information
pjcdawkins committed Aug 24, 2023
1 parent c9d1e7f commit 09d3b18
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 68 deletions.
8 changes: 6 additions & 2 deletions src/Command/CommandBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -1630,7 +1630,7 @@ final protected function validateInput(InputInterface $input, $envNotRequired =
*
* @param bool $blankLine Append an extra newline after the message, if any is printed.
*/
private function ensurePrintSelectedProject($blankLine = false) {
protected function ensurePrintSelectedProject($blankLine = false) {
if (!$this->printedSelectedProject && $this->project) {
$this->stdErr->writeln('Selected project: ' . $this->api()->getProjectLabel($this->project));
$this->printedSelectedProject = true;
Expand All @@ -1648,7 +1648,11 @@ private function ensurePrintSelectedProject($blankLine = false) {
* @param bool $blankLine Append an extra newline after the message, if any is printed.
*/
protected function ensurePrintSelectedEnvironment($blankLine = false) {
if (!$this->printedSelectedEnvironment && $this->environment) {
if (!$this->printedSelectedEnvironment) {
if (!$this->environment) {
$this->ensurePrintSelectedProject($blankLine);
return;
}
$this->ensurePrintSelectedProject();
$this->stdErr->writeln('Selected environment: ' . $this->api()->getEnvironmentLabel($this->environment));
$this->printedSelectedEnvironment = true;
Expand Down
182 changes: 118 additions & 64 deletions src/Command/Environment/EnvironmentPushCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
use GuzzleHttp\Exception\BadResponseException;
use Platformsh\Cli\Command\CommandBase;
use Platformsh\Cli\Exception\ProcessFailedException;
use Platformsh\Cli\Exception\RootNotFoundException;
use Platformsh\Cli\Service\Ssh;
use Platformsh\Cli\Util\OsUtil;
use Platformsh\Client\Exception\EnvironmentStateException;
use Platformsh\Client\Model\Environment;
use Platformsh\Client\Model\Project;
Expand All @@ -24,10 +24,10 @@ protected function configure()
->setAliases(['push'])
->setDescription('Push code to an environment')
->addArgument('source', InputArgument::OPTIONAL, 'The source ref: a branch name or commit hash', 'HEAD')
->addOption('target', null, InputOption::VALUE_REQUIRED, 'The target branch name')
->addOption('target', null, InputOption::VALUE_REQUIRED, 'The target branch name. Defaults to the current branch.')
->addOption('force', 'f', InputOption::VALUE_NONE, 'Allow non-fast-forward updates')
->addOption('force-with-lease', null, InputOption::VALUE_NONE, 'Allow non-fast-forward updates, if the remote-tracking branch is up to date')
->addOption('set-upstream', 'u', InputOption::VALUE_NONE, 'Set the target environment as the upstream for the source branch')
->addOption('set-upstream', 'u', InputOption::VALUE_NONE, 'Set the target environment as the upstream for the source branch. This will also set the target project as the remote for the local repository.')
->addOption('activate', null, InputOption::VALUE_NONE, 'Activate the environment before pushing')
->addHiddenOption('branch', null, InputOption::VALUE_NONE, 'DEPRECATED: alias of --activate')
->addOption('parent', null, InputOption::VALUE_REQUIRED, 'Set the new environment parent (only used with --activate)')
Expand All @@ -49,15 +49,37 @@ protected function execute(InputInterface $input, OutputInterface $output)
{
$this->warnAboutDeprecatedOptions(['branch'], 'The option --%s is deprecated and will be removed in future. Use --activate, which has the same effect.');

$this->validateInput($input, true);
$projectRoot = $this->getProjectRoot();
if (!$projectRoot) {
throw new RootNotFoundException();
}

/** @var \Platformsh\Cli\Service\Git $git */
$git = $this->getService('git');
$git->setDefaultRepositoryDir($projectRoot);
$gitRoot = $git->getRoot();

if ($gitRoot === false) {
$this->stdErr->writeln('This command can only be run from inside a Git repository.');
return 1;
}
$git->setDefaultRepositoryDir($gitRoot);

$this->validateInput($input, true);
$project = $this->getSelectedProject();
$currentProject = $this->getCurrentProject();
$this->ensurePrintSelectedProject();
$this->stdErr->writeln('');

if ($currentProject && $currentProject->id !== $project->id) {
$this->stdErr->writeln('The current repository is linked to another project: ' . $this->api()->getProjectLabel($currentProject, 'comment'));
if ($input->getOption('set-upstream')) {
$this->stdErr->writeln('It will be changed to link to the selected project.');
} else {
$this->stdErr->writeln('To link it to the selected project for future actions, use the: <comment>--set-upstream</comment> (<comment>-u</comment>) option');
$this->stdErr->writeln(sprintf(
'Alternatively, run: <comment>%s set-remote %s</comment>',
$this->config()->get('application.executable'),
OsUtil::escapeShellArg($project->id)
));

}
$this->stdErr->writeln('');
}

// Validate the source argument.
$source = $input->getArgument('source');
Expand All @@ -70,61 +92,50 @@ protected function execute(InputInterface $input, OutputInterface $output)
return 1;
}

$this->stdErr->writeln(
sprintf('Source revision: %s', $sourceRevision),
OutputInterface::VERBOSITY_VERY_VERBOSE
);
$this->debug(sprintf('Source revision: %s', $sourceRevision));

/** @var \Platformsh\Cli\Service\QuestionHelper $questionHelper */
$questionHelper = $this->getService('question_helper');

// Find the target branch name (--target, the name of the current
// environment, or the Git branch name).
if ($input->getOption('target')) {
$target = $input->getOption('target');
} elseif ($this->hasSelectedEnvironment()) {
$target = $this->getSelectedEnvironment()->id;
} elseif ($currentBranch = $git->getCurrentBranch()) {
$target = $currentBranch;
} else {
$this->stdErr->writeln('Could not determine target environment name.');
return 1;
$allEnvironments = $this->api()->getEnvironments($project);
$currentBranch = $git->getCurrentBranch();
if ($currentBranch !== false && isset($allEnvironments[$currentBranch])) {
$target = $currentBranch;
} else {
$default = $currentBranch !== false ? $currentBranch : null;
$target = $questionHelper->askInput('Enter the target branch name', $default, array_keys($allEnvironments));
if ($target === null) {
$this->stdErr->writeln('A target branch name (<error>--target</error>) is required.');
return 1;
}
$this->stdErr->writeln('');
}
}

/** @var \Platformsh\Cli\Service\QuestionHelper $questionHelper */
$questionHelper = $this->getService('question_helper');

$project = $this->getSelectedProject();

/** @var Environment|false $targetEnvironment The target environment, which may not exist yet. */
$targetEnvironment = $this->api()->getEnvironment($target, $project);

// Guard against accidental pushing to production.
if (($targetEnvironment && ($targetEnvironment->is_main || $targetEnvironment->type === 'production'))
|| in_array($target, ['main', 'master', 'production', $project->default_branch], true)) {
$questionText = sprintf(
'Are you sure you want to push to the %s branch?',
$targetEnvironment
? $this->api()->getEnvironmentLabel($targetEnvironment, 'comment')
: '<comment>' . $target . '</comment>'
);
if (!$questionHelper->confirm($questionText)) {
return 1;
}
}

$activities = [];
// Determine whether to activate the environment.
$activate = false;
$parentId = $type = null;
if ($target !== $project->default_branch) {
// Determine whether to activate the environment.
$activate = false;
if (!$targetEnvironment || $targetEnvironment->status === 'inactive') {
$activate = $input->getOption('branch')
|| $input->getOption('activate');
if (!$activate && $input->isInteractive()) {
$questionText = $targetEnvironment
? sprintf('Do you want to activate the target environment %s?', $this->api()->getEnvironmentLabel($targetEnvironment))
? sprintf('Do you want to activate the target environment %s?', $this->api()->getEnvironmentLabel($targetEnvironment, 'info', false))
: sprintf('Create <info>%s</info> as an active environment?', $target);
$activate = $questionHelper->confirm($questionText);
}
}

if ($activate) {
// If activating, determine what the environment's parent should be.
$parentId = $input->getOption('parent') ?: $this->findTargetParent($project, $targetEnvironment);
Expand All @@ -137,26 +148,72 @@ protected function execute(InputInterface $input, OutputInterface $output)
} elseif ($type === null && $input->isInteractive()) {
$type = $this->askEnvironmentType($project);
}
}
$this->stdErr->writeln('');
}

// Activate the target environment. The deployment activity
// will queue up behind whatever other activities are created
// here.
$activities = $this->activateTarget($target, $parentId, $project, !$input->getOption('no-clone-parent'), $type);
if ($activities === false) {
return 1;
}
// Check if the environment may be a production one.
$mayBeProduction = $type === 'production'
|| ($targetEnvironment && $targetEnvironment->type === 'production')
|| ($type === null && !$targetEnvironment && in_array($target, ['main', 'master', 'production', $project->default_branch], true));
$otherProject = $currentProject && $currentProject->id !== $project->id;

$projectLabel = $this->api()->getProjectLabel($project, $otherProject ? 'comment' : 'info');
if ($targetEnvironment) {
$environmentLabel = $this->api()->getEnvironmentLabel($targetEnvironment, $mayBeProduction ? 'comment' : 'info');
$this->stdErr->writeln(sprintf('Pushing <info>%s</info> to the environment %s of project %s', $source, $environmentLabel, $projectLabel));
if ($activate) {
$this->stdErr->writeln('The environment will be activated.');
}
} else {
$targetLabel = $mayBeProduction ? '<comment>' . $target . '</comment>' : '<info>' . $target . '</info>';
$this->stdErr->writeln(sprintf('Pushing <info>%s</info> to the branch %s of project %s', $source, $targetLabel, $projectLabel));
if ($activate) {
$this->stdErr->writeln('It will be created as an active environment.');
}
}

$this->stdErr->writeln('');

if (!$questionHelper->confirm('Are you sure you want to continue?')) {
return 1;
}
$this->stdErr->writeln('');

$activities = [];
// Activate the target environment. The deployment activity
// will queue up behind whatever other activities are created
// here.
// TODO perform activation after pushing, when it's possible to set the parent for a new branch via Git push options
if ($activate) {
$activities = $this->activateTarget($target, $parentId, $project, !$input->getOption('no-clone-parent'), $type);
if ($activities === false) {
return 1;
}
$this->stdErr->writeln('');
}

// Ensure the correct Git remote exists.
/** @var \Platformsh\Cli\Local\LocalProject $localProject */
$localProject = $this->getService('local.project');
$localProject->ensureGitRemote($projectRoot, $project->getGitUrl());
// Map the current directory to the project.
if ($input->getOption('set-upstream') && (!$currentProject || $currentProject->id !== $project->id)) {
/** @var \Platformsh\Cli\Local\LocalProject $localProject */
$localProject = $this->getService('local.project');
$this->stdErr->writeln(sprintf('Mapping the directory <info>%s</info> to the project %s', $gitRoot, $this->api()->getProjectLabel($project)));
$this->stdErr->writeln('');
$localProject->mapDirectory($gitRoot, $project);
$currentProject = $project;
}

$remoteName = $this->config()->get('detection.git_remote_name');
if ($git->getConfig("remote.$remoteName.url") === $project->getGitUrl()) {
$remote = $remoteName;
} else {
$remote = $project->getGitUrl();
}

// Build the Git command.
$gitArgs = [
'push',
$this->config()->get('detection.git_remote_name'),
$remote,
$source . ':refs/heads/' . $target,
];
foreach (['force', 'force-with-lease', 'set-upstream'] as $option) {
Expand All @@ -178,12 +235,6 @@ protected function execute(InputInterface $input, OutputInterface $output)
$git->setExtraSshOptions($extraSshOptions);

// Push.
$this->stdErr->writeln(sprintf(
'Pushing <info>%s</info> to the %s environment <info>%s</info>',
$source,
$targetEnvironment ? 'existing' : 'new',
$target
));
try {
$git->execute($gitArgs, null, true, false, $env, true);
} catch (ProcessFailedException $e) {
Expand Down Expand Up @@ -216,6 +267,13 @@ protected function execute(InputInterface $input, OutputInterface $output)
}
}

// Advise the user to set the project as the remote.
if (!$currentProject && !$input->getOption('set-upstream')) {
$this->stdErr->writeln('');
$this->stdErr->writeln('To set the project as the remote for this repository, run:');
$this->stdErr->writeln(sprintf('<info>%s set-remote %s</info>', $this->config()->get('application.executable'), OsUtil::escapeShellArg($project->id)));
}

return 0;
}

Expand Down Expand Up @@ -256,10 +314,6 @@ private function activateTarget($target, $parentId, Project $project, $clonePare
);
}
$activities = array_merge($activities, $targetEnvironment->runOperation('activate')->getActivities());
$this->stdErr->writeln(sprintf(
'Activated environment <info>%s</info>',
$this->api()->getEnvironmentLabel($targetEnvironment)
));
$this->api()->clearEnvironmentsCache($project->id);

return $activities;
Expand Down
4 changes: 2 additions & 2 deletions src/Exception/RootNotFoundException.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ public function __construct(
// then suggest the "project:set-remote" command.
if (is_dir('.git')) {
$config = new Config();
if (is_dir($config->get('service.project_config_dir'))) {
if (is_dir($config->get('service.project_config_dir')) && $config->isCommandEnabled('project:set-remote')) {
$executable = $config->get('application.executable');
$message .= "\n\nTo set the project for this Git repository, run:\n $executable project:set-remote [id]";
$message .= "\n\nTo set the project for this Git repository, run:\n $executable set-remote [id]";
}
}

Expand Down

0 comments on commit 09d3b18

Please sign in to comment.