Adding support for commenting on Github diffs.

This commit is contained in:
Dan Cryer 2015-02-20 13:37:36 +00:00
parent 071e36a4e9
commit f2db45277c
13 changed files with 333 additions and 5 deletions

View file

@ -0,0 +1,92 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Command;
use b8\Store\Factory;
use Monolog\Logger;
use PHPCI\Service\BuildService;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Re-runs the last run build.
* @author Dan Cryer <dan@block8.co.uk>
* @package PHPCI
* @subpackage Console
*/
class RebuildCommand extends Command
{
/**
* @var Logger
*/
protected $logger;
/**
* @var OutputInterface
*/
protected $output;
/**
* @var boolean
*/
protected $run;
/**
* @var int
*/
protected $sleep;
/**
* @param \Monolog\Logger $logger
* @param string $name
*/
public function __construct(Logger $logger, $name = null)
{
parent::__construct($name);
$this->logger = $logger;
}
protected function configure()
{
$this
->setName('phpci:rebuild')
->setDescription('Re-runs the last run build.');
}
/**
* Loops through running.
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$runner = new RunCommand($this->logger);
$runner->setMaxBuilds(1);
$runner->setDaemon(false);
/** @var \PHPCI\Store\BuildStore $store */
$store = Factory::getStore('Build');
$service = new BuildService($store);
$lastBuild = array_shift($store->getLatestBuilds(null, 1));
$service->createDuplicateBuild($lastBuild);
$runner->run(new ArgvInput(array()), $output);
}
/**
* Called when log entries are made in Builder / the plugins.
* @see \PHPCI\Builder::log()
*/
public function logCallback($log)
{
$this->output->writeln($log);
}
}

58
PHPCI/Helper/Diff.php Normal file
View file

@ -0,0 +1,58 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2015, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Helper;
use b8\Cache;
use b8\Config;
use b8\HttpClient;
/**
* Provides some basic diff processing functionality.
* @package PHPCI\Helper
*/
class Diff
{
/**
* Take a diff
* @param string $diff
* @return array
*/
public function getLinePositions($diff)
{
$rtn = array();
$diffLines = explode(PHP_EOL, $diff);
while (1) {
$line = array_shift($diffLines);
if (substr($line, 0, 2) == '@@') {
array_unshift($diffLines, $line);
break;
}
}
$lineNumber = 0;
$position = 0;
foreach ($diffLines as $diffLine) {
if (preg_match('/@@\s+\-[0-9]+\,[0-9]+\s+\+([0-9]+)\,([0-9]+)/', $diffLine, $matches)) {
$lineNumber = (int)$matches[1] - 1;
}
$rtn[$lineNumber] = $position;
$lineNumber++;
$position++;
}
return $rtn;
}
}

View file

@ -108,4 +108,74 @@ class Github
return $rtn;
}
/**
* Create a comment on a specific file (and commit) in a Github Pull Request.
* @param $repo
* @param $pullId
* @param $commitId
* @param $file
* @param $line
* @param $comment
* @return null
*/
public function createPullRequestComment($repo, $pullId, $commitId, $file, $line, $comment)
{
$token = Config::getInstance()->get('phpci.github.token');
if (!$token) {
return null;
}
$url = '/repos/' . strtolower($repo) . '/pulls/' . $pullId . '/comments';
$params = array(
'body' => $comment,
'commit_id' => $commitId,
'path' => $file,
'position' => $line,
);
$http = new HttpClient('https://api.github.com');
$http->setHeaders(array(
'Content-Type: application/x-www-form-urlencoded',
'Authorization: Basic ' . base64_encode($token . ':x-oauth-basic'),
));
$http->post($url, json_encode($params));
}
/**
* Create a comment on a Github commit.
* @param $repo
* @param $commitId
* @param $file
* @param $line
* @param $comment
* @return null
*/
public function createCommitComment($repo, $commitId, $file, $line, $comment)
{
$token = Config::getInstance()->get('phpci.github.token');
if (!$token) {
return null;
}
$url = '/repos/' . strtolower($repo) . '/commits/' . $commitId . '/comments';
$params = array(
'body' => $comment,
'path' => $file,
'position' => $line,
);
$http = new HttpClient('https://api.github.com');
$http->setHeaders(array(
'Content-Type: application/x-www-form-urlencoded',
'Authorization: Basic ' . base64_encode($token . ':x-oauth-basic'),
));
$http->post($url, json_encode($params));
}
}

View file

@ -204,4 +204,17 @@ class Build extends BuildBase
return $rtn;
}
/**
* Allows specific build types (e.g. Github) to report violations back to their respective services.
* @param Builder $builder
* @param $file
* @param $line
* @param $message
* @return mixed
*/
public function reportError(Builder $builder, $file, $line, $message)
{
return array($builder, $file, $line, $message);
}
}

View file

@ -10,6 +10,8 @@
namespace PHPCI\Model\Build;
use PHPCI\Builder;
use PHPCI\Helper\Diff;
use PHPCI\Helper\Github;
use PHPCI\Model\Build\RemoteGitBuild;
/**
@ -167,4 +169,56 @@ class GithubBuild extends RemoteGitBuild
return $success;
}
/**
* @inheritDoc
*/
public function reportError(Builder $builder, $file, $line, $message)
{
$diffLineNumber = $this->getDiffLineNumber($builder, $file, $line);
if (!is_null($diffLineNumber)) {
$helper = new Github();
$repo = $this->getProject()->getReference();
$prNumber = $this->getExtra('pull_request_number');
$commit = $this->getCommitId();
if (!empty($prNumber)) {
$helper->createPullRequestComment($repo, $prNumber, $commit, $file, $diffLineNumber, $message);
} else {
$helper->createCommitComment($repo, $commit, $file, $diffLineNumber, $message);
}
}
}
/**
* Uses git diff to figure out what the diff line position is, based on the error line number.
* @param Builder $builder
* @param $file
* @param $line
* @return int|null
*/
protected function getDiffLineNumber(Builder $builder, $file, $line)
{
$builder->logExecOutput(false);
$prNumber = $this->getExtra('pull_request_number');
$path = $builder->buildPath;
if (!empty($prNumber)) {
$builder->executeCommand('cd %s && git diff origin/%s "%s"', $path, $this->getBranch(), $file);
} else {
$builder->executeCommand('cd %s && git diff %s^! "%s"', $path, $this->getCommitId(), $file);
}
$builder->logExecOutput(true);
$diff = $builder->getLastOutput();
$helper = new Diff();
$lines = $helper->getLinePositions($diff);
return $lines[$line];
}
}

View file

@ -100,7 +100,7 @@ class Behat implements \PHPCI\Plugin
$errorCount = 0;
$storeFailures = false;
$data = [];
$data = array();
foreach ($lines as $line) {
$line = trim($line);
@ -119,6 +119,8 @@ class Behat implements \PHPCI\Plugin
'file' => $lineParts[0],
'line' => $lineParts[1]
);
$this->build->reportError($this->phpci, $lineParts[0], $lineParts[1], 'Behat scenario failed.');
}
}

View file

@ -237,6 +237,8 @@ class PhpCodeSniffer implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin
$fileName = str_replace($this->phpci->buildPath, '', $fileName);
foreach ($file['messages'] as $message) {
$this->build->reportError($this->phpci, $fileName, $message['line'], 'PHPCS: ' . $message['message']);
$rtn[] = array(
'file' => $fileName,
'line' => $message['line'],

View file

@ -140,6 +140,17 @@ class PhpCpd implements \PHPCI\Plugin
'line_end' => (int) $file['line'] + (int) $duplication['lines'],
'code' => (string) $duplication->codefragment
);
$message = <<<CPD
Copy and paste detected:
```
{$duplication->codefragment}
```
CPD;
$this->build->reportError($this->phpci, $fileName, $file['line'], $message);
}
$warnings++;

View file

@ -143,12 +143,13 @@ class PhpDocblockChecker implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin
// Re-enable exec output logging:
$this->phpci->logExecOutput(true);
$output = json_decode($this->phpci->getLastOutput());
$output = json_decode($this->phpci->getLastOutput(), true);
$errors = count($output);
$success = true;
$this->build->storeMeta('phpdoccheck-warnings', $errors);
$this->build->storeMeta('phpdoccheck-data', $output);
$this->reportErrors($output);
if ($this->allowed_warnings != -1 && $errors > $this->allowed_warnings) {
$success = false;
@ -156,4 +157,21 @@ class PhpDocblockChecker implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin
return $success;
}
/**
* Report all of the errors we've encountered line-by-line.
* @param $output
*/
protected function reportErrors($output)
{
foreach ($output as $error) {
$message = 'Class ' . $error['class'] . ' does not have a Docblock comment.';
if ($error['type'] == 'method') {
$message = 'Method ' . $error['class'] . '::' . $error['method'] . ' does not have a Docblock comment.';
}
$this->build->reportError($this->phpci, $error['file'], $error['line'], $message);
}
}
}

View file

@ -181,6 +181,7 @@ class PhpMessDetector implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin
'message' => (string)$violation,
);
$this->build->reportError($this->phpci, $fileName, (int)$violation['beginline'], (string)$violation);
$data[] = $warning;
}
}

View file

@ -194,11 +194,16 @@ class TechnicalDebt implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin
$errorCount++;
$this->phpci->log("Found $search on line $lineNumber of $file:\n$content");
$fileName = str_replace($this->directory, '', $file);
$data[] = array(
'file' => str_replace($this->directory, '', $file),
'file' => $fileName,
'line' => $lineNumber,
'message' => $content
);
$this->build->reportError($this->phpci, $fileName, $lineNumber, $content);
}
}
}

View file

@ -76,11 +76,11 @@ class Factory
* be passed along with any resources registered with the factory.
*
* @param $className
* @param array $options
* @param array|null $options
* @throws \InvalidArgumentException if $className doesn't represent a valid plugin
* @return \PHPCI\Plugin
*/
public function buildPlugin($className, array $options = array())
public function buildPlugin($className, $options = array())
{
$this->currentPluginOptions = $options;

View file

@ -13,6 +13,7 @@ define('PHPCI_IS_CONSOLE', true);
require('bootstrap.php');
use PHPCI\Command\RunCommand;
use PHPCI\Command\RebuildCommand;
use PHPCI\Command\GenerateCommand;
use PHPCI\Command\UpdateCommand;
use PHPCI\Command\InstallCommand;
@ -25,6 +26,7 @@ use b8\Store\Factory;
$application = new Application();
$application->add(new RunCommand($loggerConfig->getFor('RunCommand')));
$application->add(new RebuildCommand($loggerConfig->getFor('RunCommand')));
$application->add(new InstallCommand);
$application->add(new UpdateCommand($loggerConfig->getFor('UpdateCommand')));
$application->add(new GenerateCommand);