├── .docksal ├── docksal.yml └── docksal.env ├── .gitignore ├── phpspec.yml.dist ├── src ├── Patch │ ├── LocalPatch.php │ ├── Exception │ │ └── NoIssueFoundException.php │ ├── PatchInterface.php │ ├── HasIssueUriInterface.php │ ├── GithubPatch.php │ ├── DrupalOrgPatch.php │ ├── Factory.php │ └── PatchBase.php ├── Formatter │ ├── Json.php │ ├── FormatterInterface.php │ ├── Csv.php │ └── Factory.php ├── Parser │ ├── ParserInterface.php │ ├── Factory.php │ ├── ComposerJson.php │ ├── DrushMake.php │ └── Drush │ │ └── DrushIniParser.php ├── Application.php ├── Analyze │ └── Patches.php └── AnalyzeCommand.php ├── .travis.yml ├── phpcs-ruleset.xml ├── spec ├── ApplicationSpec.php ├── Formatter │ ├── JsonSpec.php │ └── FactorySpec.php ├── AnalyzeCommandSpec.php ├── Parser │ ├── ComposerJsonSpec.php │ ├── DrushMakeSpec.php │ └── FactorySpec.php ├── Patch │ ├── LocalPatchSpec.php │ ├── FactorySpec.php │ ├── DrupalOrgPatchSpec.php │ └── GithubPatchSpec.php └── Analyze │ └── PatchesSpec.php ├── phpunit.xml.dist ├── README.md ├── bin └── composer-analyze ├── tests ├── AnalyzeCommandTest.php ├── GeneratePatchesTrait.php ├── Patch │ └── GithubPatchTest.php └── Analyze │ └── PatchesTest.php └── composer.json /.docksal/docksal.yml: -------------------------------------------------------------------------------- 1 | version: "2.1" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /reports 2 | /composer.lock 3 | /vendor 4 | .phpunit.result.cache 5 | -------------------------------------------------------------------------------- /phpspec.yml.dist: -------------------------------------------------------------------------------- 1 | suites: 2 | default: 3 | namespace: Phase2\ComposerAnalytics 4 | psr4_prefix: Phase2\ComposerAnalytics 5 | -------------------------------------------------------------------------------- /src/Patch/LocalPatch.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | PHP CodeSniffer configuration Composer Analytics. 4 | ./src 5 | 6 | 7 | ./src/Parser/Drush/* 8 | 9 | -------------------------------------------------------------------------------- /spec/ApplicationSpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType(Application::class); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /spec/Formatter/JsonSpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType(Json::class); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /spec/AnalyzeCommandSpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType(AnalyzeCommand::class); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Formatter/FormatterInterface.php: -------------------------------------------------------------------------------- 1 | insertAll($analyzed); 19 | return (string) $csv; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Formatter/Factory.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./tests/ 7 | 8 | 9 | 10 | 11 | 12 | src/ 13 | 14 | 15 | vendor/ 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /spec/Parser/ComposerJsonSpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType(ComposerJson::class); 14 | } 15 | 16 | function it_has_a_pattern() 17 | { 18 | $this->getPattern()->shouldReturn('composer.json'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /spec/Parser/DrushMakeSpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType(DrushMake::class); 14 | } 15 | 16 | function it_has_a_pattern() 17 | { 18 | $this->getPattern()->shouldReturn('/.+.make(\.(yaml|yml))?$/'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Parser/ParserInterface.php: -------------------------------------------------------------------------------- 1 | beConstructedWith('project', 'src/patches/hackzor.patch', 'description'); 14 | } 15 | 16 | function it_is_initializable() 17 | { 18 | $this->shouldHaveType(LocalPatch::class); 19 | } 20 | 21 | function it_gets_the_project() 22 | { 23 | $this->getProject()->shouldReturn('project'); 24 | } 25 | 26 | function it_gets_the_description() 27 | { 28 | $this->getDescription()->shouldReturn('description'); 29 | } 30 | 31 | function it_gets_the_raw_uri() 32 | { 33 | $this->getPatchUri()->shouldReturn('src/patches/hackzor.patch'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /bin/composer-analyze: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | run(); 27 | 28 | }, '1.0.0'); 29 | -------------------------------------------------------------------------------- /src/Patch/GithubPatch.php: -------------------------------------------------------------------------------- 1 | rawUri, $matches)) { 28 | return sprintf(static::URL_TEMPLATE, $matches[4]); 29 | } 30 | 31 | throw new NoIssueFoundException(sprintf('No issue URI could be extracted from the patch: %s.', $this->rawUri)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Parser/ComposerJson.php: -------------------------------------------------------------------------------- 1 | extra->patches)) { 18 | foreach ($contents->extra->patches as $project => $patches) { 19 | foreach ($patches as $description => $uri) { 20 | // @todo Use a factory to determine patch type. 21 | $found_patches[] = PatchFactory::getPatch($project, $uri, $description); 22 | } 23 | } 24 | } 25 | 26 | return $found_patches; 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function getPattern() 33 | { 34 | return 'composer.json'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /spec/Formatter/FactorySpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType(Factory::class); 17 | } 18 | 19 | function it_gets_formatters() 20 | { 21 | $this->get('csv')->shouldImplement(FormatterInterface::class); 22 | $this->get('csv')->shouldHaveType(Csv::class); 23 | $this->get('json')->shouldImplement(FormatterInterface::class); 24 | $this->get('json')->shouldHaveType(Json::class); 25 | } 26 | 27 | function it_throws_invalid_types() 28 | { 29 | $this->shouldThrow(\LogicException::class)->during('get', ['bad_format']); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /spec/Parser/FactorySpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType(Factory::class); 17 | } 18 | 19 | function it_returns_a_parser() 20 | { 21 | $this->get('composer')->shouldImplement(ParserInterface::class); 22 | $this->get('composer')->shouldHaveType(ComposerJson::class); 23 | $this->get('make')->shouldImplement(ParserInterface::class); 24 | $this->get('make')->shouldHaveType(DrushMake::class); 25 | } 26 | 27 | function it_throws_invalid_types() 28 | { 29 | $this->shouldThrow(\LogicException::class)->during('get', ['bad']); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Application.php: -------------------------------------------------------------------------------- 1 | setArguments(); 37 | return $input; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Patch/DrupalOrgPatch.php: -------------------------------------------------------------------------------- 1 | rawUri, $matches)) { 28 | $issue_number = $matches[1]; 29 | return sprintf(static::URL_TEMPLATE, $issue_number); 30 | } 31 | 32 | throw new NoIssueFoundException(sprintf('No issue URI could be extracted from the patch: %s.', $this->rawUri)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Patch/Factory.php: -------------------------------------------------------------------------------- 1 | shouldHaveType(Factory::class); 17 | } 18 | 19 | function it_gets_local_patches() 20 | { 21 | $this->getPatch('project', 'src/patches/foo.patch', 'description')->shouldHaveType(LocalPatch::class); 22 | } 23 | 24 | function it_gets_drupalorg_patches() 25 | { 26 | $this->getPatch('drupal/core', 'https://www.drupal.org/foo.patch', 'description')->shouldHaveType(DrupalOrgPatch::class); 27 | } 28 | 29 | function it_gets_github_patches() 30 | { 31 | $this->getPatch('drupal/message', 'https://github.com/foo/message/pulls/123.diff', 'description')->shouldHaveType(GithubPatch::class); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /spec/Patch/DrupalOrgPatchSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith('project', 'http://www.drupal.org/files/foo-12345-02.patch', 'description'); 14 | } 15 | 16 | function it_is_initializable() 17 | { 18 | $this->shouldHaveType(DrupalOrgPatch::class); 19 | } 20 | 21 | function it_gets_the_project() 22 | { 23 | $this->getProject()->shouldReturn('project'); 24 | } 25 | 26 | function it_gets_the_description() 27 | { 28 | $this->getDescription()->shouldReturn('description'); 29 | } 30 | 31 | function it_gets_the_raw_uri() 32 | { 33 | $this->getPatchUri()->shouldReturn('http://www.drupal.org/files/foo-12345-02.patch'); 34 | } 35 | 36 | function it_calculates_a_uri() 37 | { 38 | $this->getIssueUri()->shouldReturn('https://www.drupal.org/node/12345'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /spec/Patch/GithubPatchSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith('project', 'https://patch-diff.githubusercontent.com/raw/Gizra/message_subscribe/pull/64.diff', 'description'); 14 | } 15 | 16 | function it_is_initializable() 17 | { 18 | $this->shouldHaveType(GithubPatch::class); 19 | } 20 | 21 | function it_gets_the_project() 22 | { 23 | $this->getProject()->shouldReturn('project'); 24 | } 25 | 26 | function it_gets_the_description() 27 | { 28 | $this->getDescription()->shouldReturn('description'); 29 | } 30 | 31 | function it_gets_the_raw_uri() 32 | { 33 | $this->getPatchUri()->shouldReturn('https://patch-diff.githubusercontent.com/raw/Gizra/message_subscribe/pull/64.diff'); 34 | } 35 | 36 | function it_calculates_a_uri() 37 | { 38 | $this->getIssueUri()->shouldReturn('https://github.com/Gizra/message_subscribe/pull/64'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.docksal/docksal.env: -------------------------------------------------------------------------------- 1 | # This is a shared configuration file that is intended to be stored in the project repo. 2 | # To override a variable locally: 3 | # - create .docksal/docksal-local.env file and local variable overrides there 4 | # - add .docksal/docksal-local.env to .gitignore 5 | 6 | # Use the default Docksal stack 7 | DOCKSAL_STACK=default 8 | 9 | # Lock images versions for LAMP services 10 | # This will prevent images from being updated when Docksal is updated 11 | #WEB_IMAGE='docksal/web:2.1-apache2.4' 12 | #DB_IMAGE='docksal/db:1.1-mysql-5.7' 13 | CLI_IMAGE='docksal/cli:2.11-php7.4' 14 | 15 | # Override virtual host (matches project folder name by default) 16 | #VIRTUAL_HOST=composer-patches.docksal 17 | # Docksal configuration. 18 | DOCROOT=public 19 | 20 | # MySQL settings. 21 | # MySQL will be exposed on a random port. Use "fin ps" to check the port. 22 | # To have a static MySQL port assigned, copy the line below into the .docksal/docksal-local.env file 23 | # and replace the host port "0" with a unique host port number (e.g. MYSQL_PORT_MAPPING='33061:3306') 24 | MYSQL_PORT_MAPPING='0:3306' 25 | 26 | # Enable/disable xdebug 27 | # To override locally, copy the two lines below into .docksal/docksal-local.env and adjust as necessary 28 | XDEBUG_ENABLED=0 29 | -------------------------------------------------------------------------------- /tests/AnalyzeCommandTest.php: -------------------------------------------------------------------------------- 1 | find('composer-analyze'); 31 | $this->commandTester = new CommandTester($command); 32 | } 33 | 34 | /** 35 | * @covers ::execute 36 | */ 37 | public function testEmpty() 38 | { 39 | vfsStream::setup('foo', null, ['bar' => ['composer.json' => '']]); 40 | $this->commandTester->execute(['directory' => vfsStream::url('foo'), '-f' => 'bad']); 41 | $this->assertEquals('No patches found in ' . vfsStream::url('foo') . ".\n", $this->commandTester->getDisplay()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/GeneratePatchesTrait.php: -------------------------------------------------------------------------------- 1 | description = $description; 41 | $this->project = $project; 42 | $this->rawUri = $uri; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function getProject() 49 | { 50 | return $this->project; 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function getDescription() 57 | { 58 | return $this->description; 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | public function getPatchUri() 65 | { 66 | return $this->rawUri; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/Patch/GithubPatchTest.php: -------------------------------------------------------------------------------- 1 | expectException(NoIssueFoundException::class); 23 | } 24 | $patch = new GithubPatch('test_project', $patch_uri, 'test description'); 25 | $this->assertEquals($expected, $patch->getIssueUri()); 26 | } 27 | 28 | /** 29 | * Data provider for ::testGetIssueUri. 30 | */ 31 | public function providerTestGetIssueUri() 32 | { 33 | return [ 34 | ['https://github.com/Gizra/message_subscribe/pull/64.diff', 35 | 'https://github.com/Gizra/message_subscribe/pull/64'], 36 | ['https://github.com/Gizra/message_subscribe/pull/64.patch', 37 | 'https://github.com/Gizra/message_subscribe/pull/64'], 38 | ['https://patch-diff.githubusercontent.com/raw/Gizra/message_subscribe/pull/64.diff', 39 | 'https://github.com/Gizra/message_subscribe/pull/64'], 40 | ['https://patch-diff.githubusercontent.com/raw/Gizra/message_subscribe/pull/64.patch', 41 | 'https://github.com/Gizra/message_subscribe/pull/64'], 42 | ['https://github.com/pull/file.diff', '', true], 43 | ]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /spec/Analyze/PatchesSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($output); 17 | } 18 | 19 | function it_is_initializable() 20 | { 21 | $this->shouldHaveType(Patches::class); 22 | } 23 | 24 | function it_can_set_and_get_patches(DrupalOrgPatch $patch) 25 | { 26 | $patches = [ 27 | $patch, 28 | ]; 29 | $this->setPatches($patches); 30 | $this->getPatches()->shouldReturn($patches); 31 | } 32 | 33 | function it_can_analyze_patches() 34 | { 35 | $drupal = new DrupalOrgPatch( 36 | 'project', 37 | 'http://www.drupal.org/files/foo-12345-02.patch', 38 | 'description' 39 | ); 40 | $bad = new DrupalOrgPatch( 41 | 'project', 42 | 'http://www.drupal.org/files/foo-no-issue.patch', 43 | 'description' 44 | ); 45 | 46 | $patches = [$drupal, $bad]; 47 | $this->setPatches($patches); 48 | $return = [ 49 | ['Project', 'Issue', 'Raw patch', 'Description'], 50 | [$drupal->getProject(), $drupal->getIssueUri(), $drupal->getPatchUri(), $drupal->getDescription()], 51 | [$bad->getProject(), Patches::NO_ISSUE_URI, $bad->getPatchUri(), $bad->getDescription()], 52 | ]; 53 | 54 | $this->analyze()->shouldReturn($return); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phase2/composer-analytics", 3 | "description": "Parses composer files and gathers analytics such as patch use.", 4 | "license": "GPL-2.0+", 5 | "authors": [ 6 | { 7 | "name": "Jonathan Hedstrom", 8 | "email": "jhedstrom@gmail.com" 9 | } 10 | ], 11 | "require": { 12 | "php": ">=7.3", 13 | "ext-json": "*", 14 | "symfony/console": "^5.1", 15 | "symfony/finder": "^5.1", 16 | "league/csv": "^9.6", 17 | "symfony/yaml": "^5.1" 18 | }, 19 | "require-dev": { 20 | "phpunit/phpunit": "^9.3", 21 | "squizlabs/php_codesniffer": "^3.5", 22 | "mikey179/vfsstream": "^1.6", 23 | "jakub-onderka/php-parallel-lint": "^1.0", 24 | "phpspec/phpspec": "^6.2", 25 | "phpspec/prophecy-phpunit": "^2.0" 26 | }, 27 | "autoload": { 28 | "psr-4": {"Phase2\\ComposerAnalytics\\": "src"} 29 | }, 30 | "autoload-dev": { 31 | "psr-4": {"Phase2\\ComposerAnalytics\\Tests\\": "tests"} 32 | }, 33 | "scripts": { 34 | "lint": "parallel-lint src tests spec", 35 | "spec": "phpspec run", 36 | "phpunit":"phpunit --log-junit=reports/unitreport.xml --coverage-text --coverage-html=reports/coverage --coverage-clover=reports/coverage.xml", 37 | "phpcs": "phpcs --encoding=utf-8 --standard=./phpcs-ruleset.xml --report-checkstyle=reports/checkstyle-phpcs.xml --report-full --extensions=php src/* tests/*", 38 | "test": [ 39 | "composer validate --no-interaction", 40 | "@lint", 41 | "@spec", 42 | "@phpunit", 43 | "@phpcs" 44 | ] 45 | }, 46 | "bin": ["bin/composer-analyze"], 47 | "extra": { 48 | "branch-alias": { 49 | "dev-master": "1.0.x-dev" 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Analyze/PatchesTest.php: -------------------------------------------------------------------------------- 1 | prophesize(OutputInterface::class)->reveal(); 36 | $this->patchAnalyzer = new Patches($output); 37 | $this->patchAnalyzer->setPatches($this->generatePatches()); 38 | } 39 | 40 | /** 41 | * @covers ::analyze 42 | */ 43 | public function testAnalyze() 44 | { 45 | $expected = [ 46 | ['Project', 'Issue', 'Raw patch', 'Description'], 47 | ['drupal/core', 'No known issue', 'src/patches/foo.patch', 'A local patch'], 48 | ['drupal/core', 'https://www.drupal.org/node/12345', 'https://www.drupal.org/files/12345-04.patch', 49 | 'Terse'], 50 | ['drupal/core', 'https://www.drupal.org/node/12345', 'https://www.drupal.org/files/12345-277.patch', 51 | 'A different desc'], 52 | ['drupal/message_subscribe', 'https://github.com/foo/bar/pull/123', 53 | 'https://github.com/foo/bar/pull/123.diff', 'A description'], 54 | ['drupal/token', 'Could not determine issue', 'https://www.drupal.org/files/no-context-at-all-patch.patch', 55 | 'Fix it'], 56 | ]; 57 | $this->assertEquals($expected, $this->patchAnalyzer->analyze()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Parser/DrushMake.php: -------------------------------------------------------------------------------- 1 | parse($file); 28 | if (!isset($contents['projects'])) { 29 | return $found_patches; 30 | } 31 | foreach ($contents['projects'] as $project_name => $project_data) { 32 | if (isset($project_data['patch'])) { 33 | foreach ($project_data['patch'] as $description => $patch) { 34 | $found_patches[] = PatchFactory::getPatch($project_name, $patch, $description); 35 | } 36 | } 37 | } 38 | 39 | return $found_patches; 40 | } 41 | 42 | /** 43 | * Determine YAML or INI format, and parse. 44 | * 45 | * @param string $data 46 | * @return array 47 | * 48 | * @throws ParseException 49 | * This will be thrown if the data cannot be parsed as INI or YAML. 50 | */ 51 | protected function parse($data) 52 | { 53 | // Try YAML first. 54 | try { 55 | $parsed = Yaml::parse($data); 56 | return $parsed; 57 | } catch (ParseException $e) { 58 | // Either an INI file, or invalid YAML. 59 | } 60 | 61 | // Try INI. 62 | // @todo Consider using Drush's INI parser which is more robust, but might not be needed for patch analysis. 63 | if ($parsed = DrushIniParser::parse($data)) { 64 | return $parsed; 65 | } 66 | 67 | // Throw YAML exception. 68 | throw $e; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Analyze/Patches.php: -------------------------------------------------------------------------------- 1 | output = $output; 49 | } 50 | 51 | /** 52 | * Set patches to analyze. 53 | * 54 | * @param PatchInterface[] $patches 55 | */ 56 | public function setPatches(array $patches) 57 | { 58 | $this->patches = $patches; 59 | } 60 | 61 | /** 62 | * Get raw patches. 63 | * 64 | * @return \Phase2\ComposerAnalytics\Patch\PatchInterface[] 65 | */ 66 | public function getPatches() 67 | { 68 | return $this->patches; 69 | } 70 | 71 | /** 72 | * Analyze patches. 73 | * 74 | * @return array 75 | */ 76 | public function analyze() 77 | { 78 | $analysis = [static::$header]; 79 | 80 | $bad_patches = []; 81 | foreach ($this->patches as $patch) { 82 | if ($patch instanceof HasIssueUriInterface) { 83 | try { 84 | $issue_uri = $patch->getIssueUri(); 85 | } catch (NoIssueFoundException $e) { 86 | $bad_patches[] = $patch->getPatchUri(); 87 | $issue_uri = static::NO_ISSUE_URI; 88 | } 89 | } else { 90 | $issue_uri = 'No known issue'; 91 | } 92 | 93 | $analysis[] = [ 94 | $patch->getProject(), 95 | $issue_uri, 96 | $patch->getPatchUri(), 97 | $patch->getDescription(), 98 | ]; 99 | } 100 | 101 | if (!empty($bad_patches)) { 102 | $this->output->writeln( 103 | sprintf( 104 | 'Found %s remote patches with no issue number associated:', 105 | count($bad_patches) 106 | ) 107 | ); 108 | $this->output->write(sprintf("\n * %s\n\n", implode("\n * ", $bad_patches))); 109 | } 110 | 111 | // @todo Some sort of sorting, and potentially aggregation. 112 | return $analysis; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Parser/Drush/DrushIniParser.php: -------------------------------------------------------------------------------- 1 | $parent[$key]); 59 | } 60 | if (!isset($parent[$key]) || !is_array($parent[$key])) { 61 | $parent[$key] = array(); 62 | } 63 | $parent = &$parent[$key]; 64 | } 65 | 66 | // Handle PHP constants. 67 | if (defined($value)) { 68 | $value = constant($value); 69 | } 70 | 71 | // Insert actual value. 72 | if ($last == '') { 73 | $last = count($parent); 74 | } 75 | if (isset($merge_item) && isset($parent[$last]) && is_array($parent[$last])) { 76 | $parent[$last][$merge_item] = $value; 77 | } 78 | else { 79 | $parent[$last] = $value; 80 | } 81 | } 82 | return $info; 83 | } 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/AnalyzeCommand.php: -------------------------------------------------------------------------------- 1 | setName('composer-analyze') 24 | ->setDescription('Analyze composer files for patch data') 25 | ->setHelp('This command will find all composer.json files within a given root directory.') 26 | ->addArgument( 27 | 'directory', 28 | InputArgument::OPTIONAL, 29 | 'Root directory to scan for composer.json files', 30 | getcwd() 31 | ) 32 | ->addOption( 33 | 'type', 34 | 't', 35 | InputOption::VALUE_OPTIONAL, 36 | 'File type to process (either `composer` or `make`). Defaults to composer.json', 37 | 'composer' 38 | ) 39 | ->addOption( 40 | 'format', 41 | 'f', 42 | InputOption::VALUE_OPTIONAL, 43 | 'The output format (either `json` or `csv`). Defaults to csv', 44 | 'csv' 45 | ) 46 | ->addOption( 47 | 'output', 48 | 'o', 49 | InputOption::VALUE_OPTIONAL, 50 | 'The output location file path. Defaults to stdout' 51 | ); 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | protected function execute(InputInterface $input, OutputInterface $output) 58 | { 59 | $type = $input->getOption('type'); 60 | $parser = Factory::get($type); 61 | 62 | $finder = new Finder(); 63 | $finder->files(); 64 | $finder->name($parser->getPattern()); 65 | 66 | $directory = $input->getArgument('directory'); 67 | $output->writeln( 68 | sprintf('Scanning for %s files in %s.', $type, $directory), 69 | OutputInterface::VERBOSITY_VERBOSE 70 | ); 71 | $patches = []; 72 | foreach ($finder->in($directory) as $file) { 73 | $output->writeln( 74 | sprintf('Found %s file.', $file->getRealPath()), 75 | OutputInterface::VERBOSITY_VERBOSE 76 | ); 77 | $patches += $parser->findPatches($file->getContents()); 78 | } 79 | if (empty($patches)) { 80 | $output->writeln(sprintf('No patches found in %s.', $directory)); 81 | return 0; 82 | } 83 | 84 | $analyzer = new Patches($output); 85 | $analyzer->setPatches($patches); 86 | $analyzed = $analyzer->analyze(); 87 | 88 | $formatted = FormatterFactory::get($input->getOption('format'))->format($analyzed); 89 | 90 | if ($input->getOption('output')) { 91 | file_put_contents($input->getOption('output'), $formatted); 92 | $output->writeln(sprintf('Report written to %s.', $input->getOption('output'))); 93 | } else { 94 | $output->writeln($formatted); 95 | } 96 | 97 | return 0; 98 | } 99 | } 100 | --------------------------------------------------------------------------------