Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a validate command to CLI to validate commit messages #58

Merged
merged 9 commits into from
Oct 7, 2022
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ messages according to Conventional Commits, enter the following in your console:
./vendor/bin/conventional-commits prepare
```

You can also validate the commit message using the following command:

``` bash
./vendor/bin/conventional-commits validate "[commit message]"
```

If you don't provide a commit message in the command line, the command will
prompt you for it.

To see all the features of the console command, enter:

``` bash
Expand Down
2 changes: 2 additions & 0 deletions bin/conventional-commits
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ declare(strict_types=1);

use Ramsey\ConventionalCommits\Console\Command\ConfigCommand;
use Ramsey\ConventionalCommits\Console\Command\PrepareCommand;
use Ramsey\ConventionalCommits\Console\Command\ValidateCommand;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\InputOption;
Expand Down Expand Up @@ -66,5 +67,6 @@ use Symfony\Component\Console\Input\InputOption;

$application->add(new ConfigCommand());
$application->add(new PrepareCommand());
$application->add(new ValidateCommand());
$application->run(new ArgvInput($argv));
})($argv);
105 changes: 105 additions & 0 deletions src/ConventionalCommits/Console/Command/ValidateCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

/**
* This file is part of ramsey/conventional-commits
*
* ramsey/conventional-commits is open source software: you can distribute it
* and/or modify it under the terms of the MIT License (the "License"). You may
* not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* @copyright Copyright (c) Ben Ramsey <[email protected]>
* @license https://opensource.org/licenses/MIT MIT License
*/

declare(strict_types=1);

namespace Ramsey\ConventionalCommits\Console\Command;

use Ramsey\ConventionalCommits\Console\Question\MessageQuestion;
use Ramsey\ConventionalCommits\Console\SymfonyStyleFactory;
use Ramsey\ConventionalCommits\Exception\ConventionalException;
use Ramsey\ConventionalCommits\Parser;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

/**
* A console command that validates a commit message as per the
* Conventional Commits specification
*/
class ValidateCommand extends BaseCommand
{
private SymfonyStyleFactory $styleFactory;

public function __construct(?SymfonyStyleFactory $styleFactory = null)
{
parent::__construct('validate');

$this->styleFactory = $styleFactory ?? new SymfonyStyleFactory();
}

protected function configure(): void
{
$this
->setDescription('Validate a commit message to ensure it conforms to Conventional Commits')
->setHelp(
'This command validates a commit message according to the '
. 'Conventional Commits specification. For more information, '
. 'see https://www.conventionalcommits.org.',
)
->addArgument(
'message',
InputArgument::OPTIONAL,
'The commit message to be validated',
)
->addOption(
'config',
null,
InputOption::VALUE_REQUIRED,
'Path to a file containing Conventional Commits configuration',
);
}

protected function doExecute(InputInterface $input, OutputInterface $output): int
{
$console = $this->styleFactory->factory($input, $output);

/** @var string|null $message */
$message = $input->getArgument('message');
if ($message === null) {
$console->title('Validate Commit Message');
/**
* @psalm-suppress ReservedWord
* @var string|null $message
*/
$message = $console->askQuestion(new MessageQuestion($this->getConfiguration()));
}

if ($message === null) {
$console->error('No commit message was provided');

return self::FAILURE;
}

try {
$parser = new Parser($this->getConfiguration());
$parser->parse($message);
} catch (ConventionalException $exception) {
$console->error($exception->getMessage());

return self::FAILURE;
}

$console->section('Commit Message');
$console->block($message);

return self::SUCCESS;
}
}
62 changes: 62 additions & 0 deletions src/ConventionalCommits/Console/Question/MessageQuestion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

/**
* This file is part of ramsey/conventional-commits
*
* ramsey/conventional-commits is open source software: you can distribute it
* and/or modify it under the terms of the MIT License (the "License"). You may
* not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* @copyright Copyright (c) Ben Ramsey <[email protected]>
* @license https://opensource.org/licenses/MIT MIT License
*/

declare(strict_types=1);

namespace Ramsey\ConventionalCommits\Console\Question;

use Ramsey\ConventionalCommits\Configuration\Configurable;
use Ramsey\ConventionalCommits\Configuration\ConfigurableTool;
use Ramsey\ConventionalCommits\Configuration\Configuration;
use Symfony\Component\Console\Question\Question;

use function method_exists;
use function trim;

/**
* A prompt that accepts long-form body content for the commit message
*/
class MessageQuestion extends Question implements Configurable
{
use ConfigurableTool;

public function __construct(?Configuration $configuration = null)
{
if (method_exists($this, 'setMultiline')) {
$this->setMultiline(true); // @codeCoverageIgnore
}

$this->configuration = $configuration;

parent::__construct(
'Enter the commit message to be validated',
);
}

public function getValidator(): callable
{
return function (?string $answer): ?string {
if (trim((string) $answer) === '') {
return null;
}

return $answer;
};
}
}
153 changes: 153 additions & 0 deletions tests/ConventionalCommits/Console/Command/ValidateCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<?php

declare(strict_types=1);

namespace Ramsey\Test\ConventionalCommits\Console\Command;

use Hamcrest\Core\IsInstanceOf;
use Mockery\MockInterface;
use Ramsey\ConventionalCommits\Console\Command\ValidateCommand;
use Ramsey\ConventionalCommits\Console\Question\MessageQuestion;
use Ramsey\ConventionalCommits\Console\SymfonyStyleFactory;
use Ramsey\Test\TestCase;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Style\SymfonyStyle;

class ValidateCommandTest extends TestCase
{
public function testCommandName(): void
{
$command = new ValidateCommand();

$this->assertSame('validate', $command->getName());
}

public function testRunWithNoArgs(): void
{
$input = new StringInput('');
$output = new NullOutput();

$console = $this->mockery(SymfonyStyle::class);

$console->expects()->title('Validate Commit Message');

$console
->expects()
->askQuestion(new IsInstanceOf(MessageQuestion::class))
->andReturn(null);

$console
->expects()
->error('No commit message was provided');

/** @var SymfonyStyleFactory & MockInterface $factory */
$factory = $this->mockery(SymfonyStyleFactory::class);
$factory->expects()->factory($input, $output)->andReturn($console);

$command = new ValidateCommand($factory);
$command->run($input, $output);
}

public function testRunWithInvalidCliArg(): void
{
$input = new StringInput("'some issue'");
$output = new NullOutput();

$console = $this->mockery(SymfonyStyle::class);

$console->expects()->title('Validate Commit Message')->never();

$console
->expects()
->askQuestion(new IsInstanceOf(MessageQuestion::class))
->never();

$console
->expects()
->error('Could not find a valid Conventional Commits message.');

/** @var SymfonyStyleFactory & MockInterface $factory */
$factory = $this->mockery(SymfonyStyleFactory::class);
$factory->expects()->factory($input, $output)->andReturn($console);

$command = new ValidateCommand($factory);
$command->run($input, $output);
}

public function testRunWithValidCliArg(): void
{
$input = new StringInput("'fix: some issue'");
$output = new NullOutput();

$console = $this->mockery(SymfonyStyle::class);

$console->expects()->title('Validate Commit Message')->never();

$console
->expects()
->askQuestion(new IsInstanceOf(MessageQuestion::class))
->never();

$console->expects()->section('Commit Message');
$console->expects()->block('fix: some issue');

/** @var SymfonyStyleFactory & MockInterface $factory */
$factory = $this->mockery(SymfonyStyleFactory::class);
$factory->expects()->factory($input, $output)->andReturn($console);

$command = new ValidateCommand($factory);
$command->run($input, $output);
}

public function testRunWithInvalidInput(): void
{
$input = new StringInput('');
$output = new NullOutput();

$console = $this->mockery(SymfonyStyle::class);

$console->expects()->title('Validate Commit Message');

$console
->expects()
->askQuestion(new IsInstanceOf(MessageQuestion::class))
->andReturn('some issue');

$console
->expects()
->error('Could not find a valid Conventional Commits message.');

/** @var SymfonyStyleFactory & MockInterface $factory */
$factory = $this->mockery(SymfonyStyleFactory::class);
$factory->expects()->factory($input, $output)->andReturn($console);

$command = new ValidateCommand($factory);
$command->run($input, $output);
}

public function testRunWithValidInput(): void
{
$input = new StringInput('');
$output = new NullOutput();

$console = $this->mockery(SymfonyStyle::class);

$console->expects()->title('Validate Commit Message');

$console
->expects()
->askQuestion(new IsInstanceOf(MessageQuestion::class))
->andReturn('fix: some issue');

$console->expects()->section('Commit Message');
$console->expects()->block('fix: some issue');

/** @var SymfonyStyleFactory & MockInterface $factory */
$factory = $this->mockery(SymfonyStyleFactory::class);
$factory->expects()->factory($input, $output)->andReturn($console);

$command = new ValidateCommand($factory);
$command->run($input, $output);
}
}
Loading