From f1c4e1010311c18a91e3bf6d7ba631535a2711bb Mon Sep 17 00:00:00 2001 From: "Jonathan H. Wage" Date: Sat, 19 May 2018 18:10:29 +0100 Subject: [PATCH] Allow changelog to be written to a file. --- README.md | 16 ++ phpcs.xml.dist | 4 + .../Command/GenerateChangelogCommand.php | 68 ++++++++- .../Tests/Functional/ConsoleTest.php | 140 +++++++++++++++++- 4 files changed, 221 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1017c85..4a94663 100644 --- a/README.md +++ b/README.md @@ -18,3 +18,19 @@ You can install with composer: Generate a change log based on a GitHub milestone with the following command: $ ./vendor/bin/changelog-generator generate --user=doctrine --repository=migrations --milestone=2.0 + +### Write to File + +Write the generated changelog to a file with the `--file` option. If you don't provide a value, it will be written +to the current working directory in a file named `CHANGELOG.md`: + + $ ./vendor/bin/changelog-generator generate --user=doctrine --repository=migrations --milestone=2.0 --file + +You can pass a value to `--file` to specify where the changelog file should be written: + + $ ./vendor/bin/changelog-generator generate --user=doctrine --repository=migrations --milestone=2.0 --file=changelog.md + +By default it will overwrite the file contents but you can pass the `--append` option to append the changelog to +the existing contents. + + $ ./vendor/bin/changelog-generator generate --user=doctrine --repository=migrations --milestone=2.0 --file=changelog.md --append diff --git a/phpcs.xml.dist b/phpcs.xml.dist index bdf3a68..e06cf24 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -14,6 +14,10 @@ + + */tests/* + + */tests/* diff --git a/src/ChangelogGenerator/Command/GenerateChangelogCommand.php b/src/ChangelogGenerator/Command/GenerateChangelogCommand.php index 84923d7..323bf6e 100644 --- a/src/ChangelogGenerator/Command/GenerateChangelogCommand.php +++ b/src/ChangelogGenerator/Command/GenerateChangelogCommand.php @@ -5,10 +5,15 @@ namespace ChangelogGenerator\Command; use ChangelogGenerator\ChangelogGenerator; +use InvalidArgumentException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Output\StreamOutput; +use function fopen; +use function getcwd; +use function sprintf; class GenerateChangelogCommand extends Command { @@ -51,6 +56,19 @@ protected function configure() : void InputOption::VALUE_REQUIRED, 'The milestone to build the changelog for.' ) + ->addOption( + 'file', + null, + InputOption::VALUE_OPTIONAL, + 'Write the changelog to a file.', + false + ) + ->addOption( + 'append', + null, + InputOption::VALUE_NONE, + 'Append the changelog to the file.' + ) ; } @@ -60,6 +78,54 @@ protected function execute(InputInterface $input, OutputInterface $output) : voi $repository = $input->getOption('repository'); $milestone = $input->getOption('milestone'); - $this->changelogGenerator->generate($user, $repository, $milestone, $output); + $changelogOutput = $this->getChangelogOutput($input, $output); + + $this->changelogGenerator->generate($user, $repository, $milestone, $changelogOutput); + } + + /** + * @return false|resource + */ + protected function fopen(string $file, string $mode) + { + return fopen($file, $mode); + } + + /** + * @throws InvalidArgumentException + */ + protected function createStreamOutput(string $file, bool $append) : StreamOutput + { + $handle = $this->fopen($file, $this->getFileHandleMode($append)); + + if ($handle === false) { + throw new InvalidArgumentException(sprintf('Could not open handle for %s', $file)); + } + + return new StreamOutput($handle); + } + + private function getFileHandleMode(bool $append) : string + { + return $append ? 'a+' : 'w+'; + } + + private function getChangelogOutput(InputInterface $input, OutputInterface $output) : OutputInterface + { + $file = $input->getOption('file'); + $append = (bool) $input->getOption('append'); + + $changelogOutput = $output; + + if ($file !== false) { + $changelogOutput = $this->createStreamOutput($this->getChangelogFilePath($file), $append); + } + + return $changelogOutput; + } + + private function getChangelogFilePath(?string $file) : string + { + return $file === null ? sprintf('%s/CHANGELOG.md', getcwd()) : $file; } } diff --git a/tests/ChangelogGenerator/Tests/Functional/ConsoleTest.php b/tests/ChangelogGenerator/Tests/Functional/ConsoleTest.php index 4778b0e..83deecb 100644 --- a/tests/ChangelogGenerator/Tests/Functional/ConsoleTest.php +++ b/tests/ChangelogGenerator/Tests/Functional/ConsoleTest.php @@ -6,44 +6,172 @@ use ChangelogGenerator\ChangelogGenerator; use ChangelogGenerator\Command\GenerateChangelogCommand; +use InvalidArgumentException; use PackageVersions\Versions; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Output\StreamOutput; +use function sprintf; +use function sys_get_temp_dir; +use function unlink; final class ConsoleTest extends TestCase { /** @var \PHPUnit_Framework_MockObject_MockObject|ChangelogGenerator */ private $changelogGenerator; + /** @var \PHPUnit_Framework_MockObject_MockObject|GenerateChangelogCommand */ + private $generateChangelogCommand; + /** @var Application */ private $application; public function testGenerate() : void { $input = new ArrayInput([ - 'command' => 'generate', - '--user' => 'jwage', - '--repository' => 'changelog-generator', - '--milestone' => '1.0', + 'command' => 'generate', + '--user' => 'jwage', + '--repository' => 'changelog-generator', + '--milestone' => '1.0', ]); $output = $this->createMock(OutputInterface::class); $this->changelogGenerator->expects($this->once()) ->method('generate') - ->with('jwage', 'changelog-generator', '1.0'); + ->with('jwage', 'changelog-generator', '1.0', $output); $this->application->run($input, $output); } + public function testGenerateFile() : void + { + $input = new ArrayInput([ + 'command' => 'generate', + '--user' => 'jwage', + '--repository' => 'changelog-generator', + '--milestone' => '1.0', + '--file' => null, + ]); + + $output = $this->createMock(OutputInterface::class); + $streamOutput = $this->createMock(StreamOutput::class); + + $this->generateChangelogCommand->expects($this->once()) + ->method('createStreamOutput') + ->willReturn($streamOutput); + + $this->changelogGenerator->expects($this->once()) + ->method('generate') + ->with('jwage', 'changelog-generator', '1.0', $streamOutput); + + $this->application->run($input, $output); + } + + public function testGenerateFilePathGiven() : void + { + $input = new ArrayInput([ + 'command' => 'generate', + '--user' => 'jwage', + '--repository' => 'changelog-generator', + '--milestone' => '1.0', + '--file' => 'CHANGELOG.md', + ]); + + $output = $this->createMock(OutputInterface::class); + $streamOutput = $this->createMock(StreamOutput::class); + + $this->generateChangelogCommand->expects($this->once()) + ->method('createStreamOutput') + ->willReturn($streamOutput); + + $this->changelogGenerator->expects($this->once()) + ->method('generate') + ->with('jwage', 'changelog-generator', '1.0', $streamOutput); + + $this->application->run($input, $output); + } + + public function testGenerateFileAppend() : void + { + $input = new ArrayInput([ + 'command' => 'generate', + '--user' => 'jwage', + '--repository' => 'changelog-generator', + '--milestone' => '1.0', + '--file' => 'CHANGELOG.md', + '--append' => true, + ]); + + $output = $this->createMock(OutputInterface::class); + $streamOutput = $this->createMock(StreamOutput::class); + + $this->generateChangelogCommand->expects($this->once()) + ->method('createStreamOutput') + ->willReturn($streamOutput); + + $this->changelogGenerator->expects($this->once()) + ->method('generate') + ->with('jwage', 'changelog-generator', '1.0', $streamOutput); + + $this->application->run($input, $output); + } + + public function testCreateStreamOutput() : void + { + $generateChangelogCommand = new GenerateChangelogCommandStub($this->changelogGenerator); + + $file = sprintf('%s/test.md', sys_get_temp_dir()); + + self::assertInstanceOf(StreamOutput::class, $generateChangelogCommand->createStreamOutputTest($file, true)); + self::assertInstanceOf(StreamOutput::class, $generateChangelogCommand->createStreamOutputTest($file, false)); + + unlink($file); + } + + public function testCreateStreamOutputCouldNotOpenHandleInvalidArgumentException() : void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Could not open handle for /tmp/test.md'); + + $file = sprintf('%s/test.md', sys_get_temp_dir()); + + /** @var \PHPUnit_Framework_MockObject_MockObject|GenerateChangelogCommandStub $generateChangelogCommand */ + $generateChangelogCommand = $this->getMockBuilder(GenerateChangelogCommandStub::class) + ->setConstructorArgs([$this->changelogGenerator]) + ->setMethods(['fopen']) + ->getMock(); + + $generateChangelogCommand->expects($this->once()) + ->method('fopen') + ->with($file, 'a+') + ->willReturn(false); + + $generateChangelogCommand->createStreamOutputTest($file, true); + } + protected function setUp() : void { $this->changelogGenerator = $this->createMock(ChangelogGenerator::class); $this->application = new Application('Changelog Generator', Versions::getVersion('jwage/changelog-generator')); $this->application->setAutoExit(false); - $this->application->add(new GenerateChangelogCommand($this->changelogGenerator)); + + $this->generateChangelogCommand = $this->getMockBuilder(GenerateChangelogCommand::class) + ->setConstructorArgs([$this->changelogGenerator]) + ->setMethods(['createStreamOutput']) + ->getMock(); + + $this->application->add($this->generateChangelogCommand); + } +} + +class GenerateChangelogCommandStub extends GenerateChangelogCommand +{ + public function createStreamOutputTest(string $file, bool $append) : StreamOutput + { + return $this->createStreamOutput($file, $append); } }