diff --git a/CHANGELOG.md b/CHANGELOG.md index 935cb9f..543a8d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Added command `list-owners` that lists all owners specified in the CODEOWNERS file + ### Changed - Renamed `\CodeOwners\Cli\Tests\Command\ListCommandTest` to `\CodeOwners\Cli\Tests\Command\ListFilesCommandTest` +- Updated required version of `timoschinkel/codeowners` to `^1.1.0` ## [1.0.0] - 2020-01-08 ### Added diff --git a/README.md b/README.md index 3686151..e13e4ee 100755 --- a/README.md +++ b/README.md @@ -34,6 +34,16 @@ codeowners [options] export PATH=~/.composer/vendor/bin:$PATH ``` +All commands have the options supplied by Symfony Console: + +* `-q`, `--quiet`; Do no output any message +* `-v|vv|vvv`, `--verbose`; Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug + +When no CODEOWNERS file is specified - using `-c` or `--codeowners` - the application will search the CODEOWNERS file in the following locations based on the working directory: +* `/.github/CODEOWNERS` +* `/.bitbucket/CODEOWNERS` +* `/CODEOWNERS` + ### Available commands #### `owner` Shows the owner of the path(s) passed as parameter. @@ -82,5 +92,22 @@ The output of this command can be used to feed into other tools using `xargs`: codeowners list-files @team ./src | xargs ``` +#### `list-owners` +Shows all available owners inside the found CODEOWNERS file. + +```bash +Usage: + list-owners [options] + +Options: + -c, --codeowners=CODEOWNERS Location of code owners file, defaults to /CODEOWNERS +``` + +For example: + +```bash +codeowners list-owners +``` + [codeowners]: https://packagist.org/packages/timoschinkel/codeowners [composer]: https://www.getcomposer.org diff --git a/bin/codeowners b/bin/codeowners index e6171a5..7374fdd 100755 --- a/bin/codeowners +++ b/bin/codeowners @@ -4,9 +4,11 @@ declare(strict_types=1); use CodeOwners\Cli\Command\ListFilesCommand; +use CodeOwners\Cli\Command\ListOwnersCommand; use CodeOwners\Cli\Command\OwnerCommand; use CodeOwners\Cli\FileLocator\FileLocatorFactory; use CodeOwners\Cli\PatternMatcherFactory; +use CodeOwners\Parser; use Symfony\Component\Console\Application; foreach ([ @@ -30,6 +32,7 @@ $app = new Application('Code owners CLI'); $app->addCommands([ new OwnerCommand($workingDir, $fileLocatorFactory, $patternMatcherFactory), new ListFilesCommand($workingDir, $fileLocatorFactory, $patternMatcherFactory), + new ListOwnersCommand($workingDir, $fileLocatorFactory, new Parser()), ]); $app->run(); diff --git a/composer.json b/composer.json index 73e250a..74a871e 100755 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "require": { "php": "^7.2", "symfony/console": "^5.0", - "timoschinkel/codeowners": "^1.0", + "timoschinkel/codeowners": "^1.1.0", "symfony/finder": "^5.0" }, "bin": ["bin/codeowners"], diff --git a/src/Command/ListOwnersCommand.php b/src/Command/ListOwnersCommand.php new file mode 100644 index 0000000..3f312ab --- /dev/null +++ b/src/Command/ListOwnersCommand.php @@ -0,0 +1,85 @@ +workingDirectory = rtrim($workingDirectory, DIRECTORY_SEPARATOR); + $this->fileLocatorFactory = $fileLocatorFactory; + $this->parser = $parser; + + parent::__construct(self::NAME); + } + + public function configure(): void + { + $this + ->setDescription('List all code owners in a CODEOWNERS file') + ->addOption( + 'codeowners', + 'c', + InputArgument::OPTIONAL, + 'Location of code owners file, defaults to /CODEOWNERS' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + // Parsing input parameters: + $codeownersLocation = $input->getOption('codeowners'); + if (is_string($codeownersLocation) !== true) { + $codeownersLocation = null; + } + + $codeownersFile = $this->fileLocatorFactory + ->getFileLocator($this->workingDirectory, $codeownersLocation) + ->locateFile(); + + $output->writeln( + "Using CODEOWNERS definition from {$codeownersFile}" . PHP_EOL, + OutputInterface::VERBOSITY_VERBOSE + ); + + if (is_file($codeownersFile) === false) { + throw new InvalidArgumentException(sprintf('The file "%s" does not exist.', $codeownersFile)); + } + + $owners = []; + + foreach ($this->parser->parse($codeownersFile) as $pattern) { + $owners = array_merge($owners, $pattern->getOwners()); + } + + foreach (array_unique($owners) as $owner) { + $output->writeln($owner); + } + + return 0; + } +} diff --git a/tests/Command/ListOwnersCommandTest.php b/tests/Command/ListOwnersCommandTest.php new file mode 100644 index 0000000..93bb037 --- /dev/null +++ b/tests/Command/ListOwnersCommandTest.php @@ -0,0 +1,140 @@ +fileLocatorFactory = $this->prophesize(FileLocatorFactoryInterface::class); + $this->patternMatcherFactory = $this->prophesize(PatternMatcherFactoryInterface::class); + $this->parser = $this->prophesize(ParserInterface::class); + + parent::setUp(); + } + + public function testCommandDisplaysOwnersFoundInCodeOwnersFile(): void + { + $filesystem = vfsStream::setup('root', 444, [ + 'CODEOWNERS' => '#', + ]); + + $fileLocator = $this->prophesize(FileLocatorInterface::class); + $fileLocator->locateFile() + ->shouldBeCalled() + ->willReturn($filesystem->url() . '/CODEOWNERS'); + + $this->fileLocatorFactory + ->getFileLocator(Argument::type('string'), null) + ->shouldBeCalled() + ->willReturn($fileLocator->reveal()); + + $this->parser + ->parse($filesystem->url() . '/CODEOWNERS') + ->shouldBeCalled() + ->willReturn([ + new Pattern('pattern 01', ['owner 01', 'owner 02']), + new Pattern('pattern 02', ['owner 01', 'owner 03']), + new Pattern('pattern 03', ['owner 04']), + ]); + + $command = new ListOwnersCommand( + $filesystem->url(), + $this->fileLocatorFactory->reveal(), + $this->parser->reveal() + ); + + $output = $this->executeCommand($command, []); + self::assertEquals( + join(PHP_EOL, ['owner 01', 'owner 02', 'owner 03', 'owner 04']) . PHP_EOL, + $output + ); + } + + public function testCommandPassesCodeownerFileLocation(): void + { + $filesystem = vfsStream::setup('root', 444, []); + + $fileLocator = $this->prophesize(FileLocatorInterface::class); + $fileLocator->locateFile() + ->shouldBeCalled() + ->willThrow(UnableToLocateFileException::class); + + $this->fileLocatorFactory + ->getFileLocator(Argument::type('string'), 'CODEOWNERS') + ->shouldBeCalled() + ->willReturn($fileLocator->reveal()); + + $command = new ListOwnersCommand( + $filesystem->url(), + $this->fileLocatorFactory->reveal(), + $this->parser->reveal() + ); + + $this->expectException(UnableToLocateFileException::class); + $this->executeCommand($command, [ + '--codeowners' => 'CODEOWNERS', + ]); + } + + public function testCommandThrowsExceptionIfCodeOwnersFileDoesNotExist(): void + { + $filesystem = vfsStream::setup('root', 444, []); + + $fileLocator = $this->prophesize(FileLocatorInterface::class); + $fileLocator->locateFile() + ->shouldBeCalled() + ->willReturn($filesystem->url() . '/CODEOWNERS'); + + $this->fileLocatorFactory + ->getFileLocator(Argument::type('string'), Argument::any()) + ->willReturn($fileLocator->reveal()); + + $command = new ListOwnersCommand( + $filesystem->url(), + $this->fileLocatorFactory->reveal(), + $this->parser->reveal() + ); + + $this->expectException(InvalidArgumentException::class); + $this->executeCommand($command, []); + } + + private function executeCommand(Command $command, array $parameters): string + { + $application = new Application(); + $application->add($command); + + $tester = new CommandTester($application->find($command->getName())); + $tester->execute($parameters); + + return $tester->getDisplay(); + } +}