Commit a8cf1c0e authored by Kosta Harlan's avatar Kosta Harlan
Browse files

Implement command for posting fix suggestions

parent 23f11ec3
## v0.2.0
- Add 'suggestions:post' command
- Rename 'generate-fix-suggestions' to 'suggestions:generate'
- [API] Add Gerrit client
## v0.1.0 ## v0.1.0
- Add `generate-fix-suggestions` command. - Add `generate-fix-suggestions` command.
......
...@@ -4,8 +4,12 @@ ...@@ -4,8 +4,12 @@
require __DIR__.'/vendor/autoload.php'; require __DIR__.'/vendor/autoload.php';
use FixSuggestions\Command\GenerateFixSuggestions; use FixSuggestions\Command\GenerateFixSuggestions;
use FixSuggestions\Command\PostFixSuggestions;
use Symfony\Component\Console\Application; use Symfony\Component\Console\Application;
$application = new Application(); $application = new Application();
$application->add( new GenerateFixSuggestions() ); $application->addCommands( [
new GenerateFixSuggestions(),
new PostFixSuggestions()
] );
$application->run(); $application->run();
{ {
"name": "kostajh/fix-suggestions", "name": "kostajh/fix-suggestions",
"version": "0.1.3", "version": "0.2.0",
"description": "Generate and publish fix suggestions for MediaWiki core and extensions/skins. Currently for Gerrit, later for GitLab.", "description": "Generate and publish fix suggestions for MediaWiki core and extensions/skins. Currently for Gerrit, later for GitLab.",
"type": "library", "type": "library",
"require": { "require": {
"mediawiki/mediawiki-codesniffer": "^37.0",
"symfony/console": "^5.3", "symfony/console": "^5.3",
"symfony/http-client": "^5.3",
"symfony/http-foundation": "^5.3", "symfony/http-foundation": "^5.3",
"mediawiki/mediawiki-codesniffer": "^37.0",
"symfony/process": "^5.3" "symfony/process": "^5.3"
}, },
"autoload": { "autoload": {
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "894b3fb18452f190f29cf3fd9013ae34", "content-hash": "0efd3b9516241b4cf9ce53de8eea8a94",
"packages": [ "packages": [
{ {
"name": "composer/semver", "name": "composer/semver",
...@@ -248,6 +248,53 @@ ...@@ -248,6 +248,53 @@
], ],
"time": "2021-03-05T17:36:06+00:00" "time": "2021-03-05T17:36:06+00:00"
}, },
{
"name": "psr/log",
"version": "1.1.4",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "d49695b909c3b7628b6289db5479a1c204601f11"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
"reference": "d49695b909c3b7628b6289db5479a1c204601f11",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Log\\": "Psr/Log/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"homepage": "https://github.com/php-fig/log",
"keywords": [
"log",
"psr",
"psr-3"
],
"time": "2021-05-03T11:20:27+00:00"
},
{ {
"name": "sebastian/diff", "name": "sebastian/diff",
"version": "3.0.3", "version": "3.0.3",
...@@ -521,6 +568,165 @@ ...@@ -521,6 +568,165 @@
], ],
"time": "2021-03-23T23:28:01+00:00" "time": "2021-03-23T23:28:01+00:00"
}, },
{
"name": "symfony/http-client",
"version": "v5.3.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client.git",
"reference": "67c177d4df8601d9a71f9d615c52171c98d22d74"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client/zipball/67c177d4df8601d9a71f9d615c52171c98d22d74",
"reference": "67c177d4df8601d9a71f9d615c52171c98d22d74",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"psr/log": "^1|^2|^3",
"symfony/deprecation-contracts": "^2.1",
"symfony/http-client-contracts": "^2.4",
"symfony/polyfill-php73": "^1.11",
"symfony/polyfill-php80": "^1.16",
"symfony/service-contracts": "^1.0|^2"
},
"provide": {
"php-http/async-client-implementation": "*",
"php-http/client-implementation": "*",
"psr/http-client-implementation": "1.0",
"symfony/http-client-implementation": "2.4"
},
"require-dev": {
"amphp/amp": "^2.5",
"amphp/http-client": "^4.2.1",
"amphp/http-tunnel": "^1.0",
"amphp/socket": "^1.1",
"guzzlehttp/promises": "^1.4",
"nyholm/psr7": "^1.0",
"php-http/httplug": "^1.0|^2.0",
"psr/http-client": "^1.0",
"symfony/dependency-injection": "^4.4|^5.0",
"symfony/http-kernel": "^4.4.13|^5.1.5",
"symfony/process": "^4.4|^5.0",
"symfony/stopwatch": "^4.4|^5.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\HttpClient\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-07-23T15:55:36+00:00"
},
{
"name": "symfony/http-client-contracts",
"version": "v2.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-client-contracts.git",
"reference": "7e82f6084d7cae521a75ef2cb5c9457bbda785f4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/7e82f6084d7cae521a75ef2cb5c9457bbda785f4",
"reference": "7e82f6084d7cae521a75ef2cb5c9457bbda785f4",
"shasum": ""
},
"require": {
"php": ">=7.2.5"
},
"suggest": {
"symfony/http-client-implementation": ""
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "2.4-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\HttpClient\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to HTTP clients",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
],
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-04-11T23:07:08+00:00"
},
{ {
"name": "symfony/http-foundation", "name": "symfony/http-foundation",
"version": "v5.3.6", "version": "v5.3.6",
......
...@@ -11,7 +11,7 @@ use Symfony\Component\Process\Process; ...@@ -11,7 +11,7 @@ use Symfony\Component\Process\Process;
class GenerateFixSuggestions extends Command { class GenerateFixSuggestions extends Command {
public function configure(): void { public function configure(): void {
$this->setName( 'generate-fix-suggestions' ); $this->setName( 'suggestions:generate' );
$this->setDescription( 'Generate fix suggestions for a patch.' ); $this->setDescription( 'Generate fix suggestions for a patch.' );
$this->addOption( $this->addOption(
'suggestion-tools', 'suggestion-tools',
...@@ -83,6 +83,17 @@ class GenerateFixSuggestions extends Command { ...@@ -83,6 +83,17 @@ class GenerateFixSuggestions extends Command {
// that for consistency. // that for consistency.
getenv( 'GERRIT_URL' ) getenv( 'GERRIT_URL' )
); );
$this->addOption(
'format',
null,
InputOption::VALUE_OPTIONAL,
<<<'TAG'
The output format to return for the command. Default is a JSON object
containing information about generated reports.
TAG
,
'json'
);
} }
/** @inheritDoc */ /** @inheritDoc */
...@@ -106,6 +117,7 @@ class GenerateFixSuggestions extends Command { ...@@ -106,6 +117,7 @@ class GenerateFixSuggestions extends Command {
// TODO: Maybe extract this block into a class once we have GitLab and/or eslint added. // TODO: Maybe extract this block into a class once we have GitLab and/or eslint added.
if ( $input->getOption( 'code-review' ) === 'gerrit' ) { if ( $input->getOption( 'code-review' ) === 'gerrit' ) {
if ( $this->shouldRunPhpcs( $input ) ) { if ( $this->shouldRunPhpcs( $input ) ) {
$response = [];
$cacheFile = sprintf( '%s/%s.json', $cacheFile = sprintf( '%s/%s.json',
$input->getOption( 'cache-directory' ), $input->getOption( 'cache-directory' ),
$input->getOption( 'project' ) ); $input->getOption( 'project' ) );
...@@ -133,8 +145,9 @@ class GenerateFixSuggestions extends Command { ...@@ -133,8 +145,9 @@ class GenerateFixSuggestions extends Command {
$output->write( $process->getOutput() ); $output->write( $process->getOutput() );
return Command::FAILURE; return Command::FAILURE;
} }
$outputFile = sprintf( '%s/%s/%s.json', $outputFile = sprintf( '%s/%s/%s/phpcs.%s.json',
$input->getOption( 'output-directory' ), $input->getOption( 'output-directory' ),
$input->getOption( 'code-review' ),
$input->getOption( 'project' ), $input->getOption( 'project' ),
$input->getOption( 'gerrit-robot-run-id' ) $input->getOption( 'gerrit-robot-run-id' )
); );
...@@ -143,6 +156,11 @@ class GenerateFixSuggestions extends Command { ...@@ -143,6 +156,11 @@ class GenerateFixSuggestions extends Command {
$outputFile, $outputFile,
$process->getOutput() $process->getOutput()
); );
if ( $input->getOption( 'format' ) === 'json' ) {
$response['code_review'] = $input->getOption( 'code-review' );
$response['suggestions']['phpcs'] = $outputFile;
$output->write( json_encode( $response ) );
}
return Command::SUCCESS; return Command::SUCCESS;
} }
} }
......
<?php
namespace FixSuggestions\Command;
use FixSuggestions\Http\Gerrit;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\StreamableInputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
class PostFixSuggestions extends Command {
public function configure() {
$this->setName( 'suggestions:post' )
->setDescription( 'Post fix suggestions to a code review system.' )
->setHelp( <<<'TAG'
Command to take fix suggestions generated by code quality tools and post them
as inline suggestions in a code review system.
TAG
);
$this->addOption(
'gerrit-url',
null,
InputOption::VALUE_OPTIONAL,
'[gerrit] The URL for the Gerrit instance.',
getenv( 'GERRIT_URL' ) ?? 'https://gerrit.wikimedia.org/',
);
$this->addOption(
'gerrit-username',
null,
InputOption::VALUE_OPTIONAL,
'[gerrit] The username to use with the Gerrit instance.',
getenv( 'GERRIT_USERNAME' )
);
$this->addOption(
'gerrit-http-password',
null,
InputOption::VALUE_OPTIONAL,
'[gerrit] The password to use with the Gerrit instance.',
getenv( 'GERRIT_PASSWORD' )
);
$this->addOption(
'zuul-project',
null,
InputOption::VALUE_OPTIONAL,
'[zuul] The Zuul project associated with the suggestions',
);
$this->addOption(
'zuul-change',
null,
InputOption::VALUE_OPTIONAL,
'[zuul] The Zuul change associated with the suggestions',
);
$this->addOption(
'zuul-patchset',
null,
InputOption::VALUE_OPTIONAL,
'[zuul] The Zuul patchset number associated with the suggestions',
);
}
/** @inheritDoc */
public function execute( InputInterface $input, OutputInterface $output ): int {
$inputStream = ( $input instanceof StreamableInputInterface ) ? $input->getStream() : null;
$inputStream = $inputStream ?? STDIN;
$inputStream = stream_get_contents( $inputStream );
$suggestionData = json_decode( $inputStream, true );
if ( !$suggestionData ) {
$output->writeln( '<error>Unable to decode input data.</error>' );
return Command::FAILURE;
}
$codeReview = $suggestionData['code_review'];
foreach ( $suggestionData['suggestions'] as $tool => $suggestionsFile ) {
$output->write( "<info>Posting suggestions for $tool in $suggestionsFile...</info>" );
if ( !file_exists( $suggestionsFile ) ) {
$output->writeln( "$suggestionsFile does not exist." );
return Command::FAILURE;
}
// TODO: Extract this into a class, when we add GitLab support.
if ( $codeReview === 'gerrit' ) {
$gerrit = new Gerrit(
$input->getOption( 'gerrit-url' ),
$input->getOption( 'gerrit-username' ),
$input->getOption( 'gerrit-http-password' )
);
$suggestions = json_decode( file_get_contents( $suggestionsFile ), true );
try {
$response = $gerrit->postFixSuggestions(
$suggestions,
$input->getOption( 'zuul-project' ),
$input->getOption( 'zuul-change' ),
$input->getOption( 'zuul-patchset' )
);
if ( $response->getStatusCode() >= 400 ) {
$output->write( "\n<error>Error:</error> " );
$output->write( $response->getContent( false ), true );
return Command::FAILURE;
} else {
$output->write( '<info>done!</info>', true );
}
} catch ( TransportExceptionInterface $transportException ) {
$output->writeln( $transportException->getMessage() );
return Command::FAILURE;
}
}
}
return Command::SUCCESS;
}
}
<?php
namespace FixSuggestions\Http;
use http\Client\Response;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
class Gerrit {
private HttpClientInterface $client;
/**
* @param string $url
* @param string $username
* @param string $password
*/
public function __construct( string $url, string $username, string $password ) {
$this->client = HttpClient::createForBaseUri( $url, [
'auth_basic' => [ $username, $password ]
] );
}
/**
* @param array $fixSuggestions
* @param string $zuulProject
* @param int $zuulChange
* @param int $zuulPatchset
* @return ResponseInterface
* @throws TransportExceptionInterface
*/
public function postFixSuggestions(
array $fixSuggestions,
string $zuulProject,
int $zuulChange,
int $zuulPatchset
): ResponseInterface {
return $this->client->request( 'POST', self::getGerritUrl(
$zuulProject,
$zuulChange,
$zuulPatchset,
'review'
), [ 'json' => [ 'robot_comments' => $fixSuggestions ] ] );
}
/**
* @param string $gerritProject
* @param int $gerritShortId
* @param int $gerritRevision
* @param string $endpoint
* @return string
*/
private function getGerritUrl(
string $gerritProject, int $gerritShortId, int $gerritRevision, string $endpoint
): string {
return '/r/a/changes/' . urlencode( $gerritProject ) . '~' . $gerritShortId . '/revisions/'
. $gerritRevision . '/' . $endpoint;
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment