Skip to content

Commit

Permalink
Merge pull request #7 from timoschinkel/feature/list-owners
Browse files Browse the repository at this point in the history
Introduce command `list-owners`
  • Loading branch information
timoschinkel authored Jan 22, 2020
2 parents 62b1d31 + 45617e7 commit 53f5719
Show file tree
Hide file tree
Showing 6 changed files with 260 additions and 1 deletion.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ codeowners [options] <command>
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:
* `<working_dir>/.github/CODEOWNERS`
* `<working_dir>/.bitbucket/CODEOWNERS`
* `<working_dir>/CODEOWNERS`

### Available commands
#### `owner`
Shows the owner of the path(s) passed as parameter.
Expand Down Expand Up @@ -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 <command>
```

#### `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 <working_dir>/CODEOWNERS
```

For example:

```bash
codeowners list-owners
```

[codeowners]: https://packagist.org/packages/timoschinkel/codeowners
[composer]: https://www.getcomposer.org
3 changes: 3 additions & 0 deletions bin/codeowners
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -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 ([
Expand All @@ -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();
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
85 changes: 85 additions & 0 deletions src/Command/ListOwnersCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

declare(strict_types=1);

namespace CodeOwners\Cli\Command;

use CodeOwners\Cli\FileLocator\FileLocatorFactoryInterface;
use CodeOwners\ParserInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

final class ListOwnersCommand extends Command
{
private const NAME = 'list-owners';

/** @var string */
private $workingDirectory;

/** @var FileLocatorFactoryInterface */
private $fileLocatorFactory;

/** @var ParserInterface */
private $parser;

public function __construct(
string $workingDirectory,
FileLocatorFactoryInterface $fileLocatorFactory,
ParserInterface $parser
) {
$this->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 <working_dir>/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;
}
}
140 changes: 140 additions & 0 deletions tests/Command/ListOwnersCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<?php

declare(strict_types=1);

namespace CodeOwners\Cli\Tests\Command;

use CodeOwners\Cli\Command\ListOwnersCommand;
use CodeOwners\Cli\FileLocator\FileLocatorFactoryInterface;
use CodeOwners\Cli\FileLocator\FileLocatorInterface;
use CodeOwners\Cli\FileLocator\UnableToLocateFileException;
use CodeOwners\Cli\PatternMatcherFactoryInterface;
use CodeOwners\ParserInterface;
use CodeOwners\Pattern;
use org\bovigo\vfs\vfsStream;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Tester\CommandTester;

final class ListOwnersCommandTest extends TestCase
{
/** @var FileLocatorFactoryInterface|ObjectProphecy */
private $fileLocatorFactory;

/** @var PatternMatcherFactoryInterface|ObjectProphecy */
private $patternMatcherFactory;

/** @var ParserInterface|ObjectProphecy */
private $parser;

protected function setUp(): void
{
$this->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();
}
}

0 comments on commit 53f5719

Please sign in to comment.