├── .gitignore ├── Dockerfile ├── README.md ├── bin └── diffcs ├── composer.json ├── output.png └── src └── Melody └── Diffcs ├── Command └── DiffcsCommand.php ├── DiffcsApplication.php └── Executor.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor/* 3 | .idea/* 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7 2 | 3 | RUN curl -LO https://github.com/marcelsud/diffcs/releases/download/v0.2.1/diffcs.phar && chmod +x diffcs.phar && mv diffcs.phar /usr/local/bin/diffcs 4 | RUN curl -LO https://squizlabs.github.io/PHP_CodeSniffer/phpcs.phar && chmod +x phpcs.phar && mv phpcs.phar /usr/local/bin/phpcs 5 | 6 | ENTRYPOINT ["diffcs"] 7 | CMD ["--help"] 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DiffCS 2 | 3 | A tool to perform code sniffer checks of your pull requests on Github. 4 | 5 | ## How To Install 6 | 7 | You can grab a copy of marcelsud/diffcs in either of the following ways: 8 | 9 | ### As a phar (recommended) 10 | 11 | You can simply download a pre-compiled and ready-to-use version as a Phar to any directory. Simply download the latest diffcs.phar file from our [releases page](https://github.com/marcelsud/diffcs/releases): 12 | 13 | ``` 14 | curl -LO https://github.com/marcelsud/diffcs/releases/download/v0.2.1/diffcs.phar 15 | php diffcs.phar --help 16 | ``` 17 | 18 | Optionally you can install it globally by adding it to your bin folder: 19 | 20 | ``` 21 | chmod +x diffcs.phar 22 | mv diffcs.phar /usr/local/bin/diffcs 23 | ``` 24 | 25 | ### Via composer: 26 | 27 | ``` 28 | composer global require "marcelsud/diffcs":"dev-master" 29 | sudo ln -nfs ~/.composer/vendor/bin/diffcs /usr/local/bin/diffcs 30 | ``` 31 | 32 | ### Via docker: 33 | 34 | ``` 35 | docker run --rm -it marcelsud/diffcs --help 36 | ``` 37 | 38 | ## How To Use 39 | 40 | ### For public repositories: 41 | 42 | Run the following command: `diffcs / `, where: 43 | 44 | - `` is the corporation/user behind the project; 45 | - `` is the project name on Github; 46 | - `` is the pull request id, created by Github. 47 | 48 | **Example**: 49 | 50 | ``` 51 | diffcs symfony/symfony 13342 52 | ``` 53 | 54 | ### For private repositories: 55 | 56 | #### Authenticate with username and password 57 | Execute following command: `diffcs / --github-user=`, where: 58 | 59 | - `` is your Github username. 60 | - the password will be asked afterwards and is only required check private repositories. 61 | 62 | **Example**: 63 | 64 | ``` 65 | diffcs symfony/symfony 13342 --github-user=yourusername 66 | ``` 67 | 68 | #### Authenticate with Github token 69 | Execute following command: `diffcs / --github-token=`, where: 70 | 71 | - you can generate the `` in [your Github account settings](https://github.com/settings/tokens/new?scopes=repo&description=Diffcs%20token)). 72 | 73 | **Example**: 74 | 75 | ``` 76 | diffcs symfony/symfony 13342 --github-token=256199c24f9132f84e9bb06271ff65a3176a2f05 77 | ``` 78 | 79 | ![Image](output.png) 80 | -------------------------------------------------------------------------------- /bin/diffcs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | . 7 | * All rights reserved. 8 | * 9 | * Redistribution and use in source and binary forms, with or without 10 | * modification, are permitted provided that the following conditions 11 | * are met: 12 | * 13 | * * Redistributions of source code must retain the above copyright 14 | * notice, this list of conditions and the following disclaimer. 15 | * 16 | * * Redistributions in binary form must reproduce the above copyright 17 | * notice, this list of conditions and the following disclaimer in 18 | * the documentation and/or other materials provided with the 19 | * distribution. 20 | * 21 | * * Neither the name of Marcelo Santos nor the names of his 22 | * contributors may be used to endorse or promote products derived 23 | * from this software without specific prior written permission. 24 | * 25 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 26 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 27 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 28 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 29 | * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 30 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 31 | * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 32 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 33 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 34 | * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 35 | * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 36 | * POSSIBILITY OF SUCH DAMAGE. 37 | */ 38 | 39 | if (!ini_get('date.timezone')) { 40 | ini_set('date.timezone', 'UTC'); 41 | } 42 | 43 | $files = array( 44 | __DIR__ . '/../../../autoload.php', 45 | __DIR__ . '/../../autoload.php', 46 | __DIR__ . '/../vendor/autoload.php', 47 | __DIR__ . '/vendor/autoload.php' 48 | ); 49 | 50 | foreach ($files as $file) { 51 | if (file_exists($file)) { 52 | define('COMPOSER_AUTOLOAD_FILE', $file); 53 | break; 54 | } 55 | } 56 | 57 | unset($file); 58 | unset($files); 59 | 60 | if (!defined('COMPOSER_AUTOLOAD_FILE')) { 61 | fwrite(STDERR, 62 | 'You need to set up the project dependencies using the following commands:' . PHP_EOL . 63 | 'wget http://getcomposer.org/composer.phar' . PHP_EOL . 64 | 'php composer.phar install' . PHP_EOL 65 | ); 66 | die(1); 67 | } 68 | 69 | require COMPOSER_AUTOLOAD_FILE; 70 | 71 | 72 | use Melody\Diffcs\DiffcsApplication; 73 | 74 | $application = new DiffcsApplication(); 75 | $application->run(); -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "marcelsud/diffcs", 3 | "description": "Run PHP Code Sniffer in pull requests", 4 | "type": "library", 5 | "license": "BSD Style", 6 | "homepage": "https://github.com/marcelsud/diffcs", 7 | "require": { 8 | "knplabs/github-api": "~1.2", 9 | "league/flysystem": "0.5.*", 10 | "symfony/console": "v2.6.3" 11 | }, 12 | "authors": [ 13 | { 14 | "name": "Marcelo Santos", 15 | "email": "marcelsud@gmail.com" 16 | } 17 | ], 18 | "autoload": { 19 | "psr-0": { 20 | "Melody\\": "src/" 21 | } 22 | }, 23 | "bin": ["bin/diffcs"] 24 | } 25 | -------------------------------------------------------------------------------- /output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelsud/diffcs/9016f63103bbf3877742d3e65082d30585493354/output.png -------------------------------------------------------------------------------- /src/Melody/Diffcs/Command/DiffcsCommand.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class DiffcsCommand extends Command 19 | { 20 | /** 21 | * @var string 22 | */ 23 | const PULL_REQUEST_ARGUMENT = "pull-request"; 24 | /** 25 | * @var string 26 | */ 27 | const REPOSITORY_ARGUMENT = "repository"; 28 | /** 29 | * @var string 30 | */ 31 | const CODE_STANDARD_OPTION = "code-standard"; 32 | /** 33 | * @var string 34 | */ 35 | const GITHUB_TOKEN_OPTION = "github-token"; 36 | /** 37 | * @var string 38 | */ 39 | const GITHUB_USER_OPTION = "github-user"; 40 | 41 | /** 42 | * @return void 43 | */ 44 | protected function configure() 45 | { 46 | $this 47 | ->setName('diffcs') 48 | ->setDescription('Used to run phpcs on pull requests') 49 | ->addArgument( 50 | self::REPOSITORY_ARGUMENT, 51 | InputArgument::REQUIRED, 52 | 'The repository name' 53 | ) 54 | ->addArgument( 55 | self::PULL_REQUEST_ARGUMENT, 56 | InputArgument::REQUIRED, 57 | 'The pull request id' 58 | ) 59 | ->addOption( 60 | self::CODE_STANDARD_OPTION, 61 | 'cs', 62 | InputOption::VALUE_OPTIONAL, 63 | 'The github token to access private repositories', 64 | 'PSR2' 65 | ) 66 | ->addOption( 67 | self::GITHUB_TOKEN_OPTION, 68 | null, 69 | InputOption::VALUE_REQUIRED, 70 | 'The github token to access private repositories' 71 | ) 72 | ->addOption( 73 | self::GITHUB_USER_OPTION, 74 | null, 75 | InputOption::VALUE_REQUIRED, 76 | 'The github username to access private repositories' 77 | ) 78 | ; 79 | } 80 | 81 | /** 82 | * @param \Symfony\Component\Console\Input\InputInterface $input 83 | * @param \Symfony\Component\Console\Output\OutputInterface $output 84 | */ 85 | protected function execute(InputInterface $input, OutputInterface $output) 86 | { 87 | $pullRequestId = $input->getArgument(self::PULL_REQUEST_ARGUMENT); 88 | 89 | list($owner, $repository) = explode("/", $input->getArgument(self::REPOSITORY_ARGUMENT)); 90 | $githubToken = $input->getOption(self::GITHUB_TOKEN_OPTION); 91 | $githubUser = $input->getOption(self::GITHUB_USER_OPTION); 92 | $githubPass = false; 93 | 94 | $codeStandard = $input->getOption(self::CODE_STANDARD_OPTION); 95 | 96 | if (!empty($githubUser)) { 97 | $helper = $this->getHelper('question'); 98 | 99 | $question = new Question('Password:'); 100 | $question->setHidden(true); 101 | $question->setHiddenFallback(false); 102 | $question->setValidator(function ($value) { 103 | if (trim($value) == '') { 104 | throw new \Exception('The password can not be empty'); 105 | } 106 | 107 | return $value; 108 | }); 109 | 110 | $githubPass = $helper->ask($input, $output, $question); 111 | } 112 | 113 | $executor = new Executor( 114 | $output, 115 | $owner, 116 | $repository, 117 | $codeStandard, 118 | $githubToken, 119 | $githubUser, 120 | $githubPass 121 | ); 122 | 123 | $output->writeln( 124 | ' ' 125 | ); 126 | $output->writeln( 127 | ' PHP DIFF CS (CODE STANDARD) ' 128 | ); 129 | $output->writeln( 130 | ' ' 131 | ); 132 | 133 | $output->writeln(''); 134 | $output->writeln( 135 | 'Project: ' 136 | .$input->getArgument(self::REPOSITORY_ARGUMENT) 137 | .'' 138 | ); 139 | $output->writeln( 140 | 'Pull Request: #'.$pullRequestId.'' 141 | ); 142 | $output->writeln( 143 | 'Code Standard: '.$codeStandard.'' 144 | ); 145 | $output->writeln(''); 146 | 147 | try { 148 | $results = $executor->execute($pullRequestId); 149 | } catch (\Exception $e) { 150 | $output->writeln('ERROR: '.$e->getMessage()); 151 | 152 | die(); 153 | } 154 | 155 | if (count($results)) { 156 | foreach ($results as $result) { 157 | $output->writeln($result); 158 | } 159 | } 160 | 161 | $output->writeln(PHP_EOL); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Melody/Diffcs/DiffcsApplication.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class DiffcsApplication extends Application 15 | { 16 | /** 17 | * Gets the name of the command based on input. 18 | * 19 | * @param InputInterface $input The input interface 20 | * 21 | * @return string The command name 22 | */ 23 | protected function getCommandName(InputInterface $input) 24 | { 25 | return 'diffcs'; 26 | } 27 | 28 | /** 29 | * Gets the default commands that should always be available. 30 | * 31 | * @return array An array of default Command instances 32 | */ 33 | protected function getDefaultCommands() 34 | { 35 | $defaultCommands = parent::getDefaultCommands(); 36 | 37 | $defaultCommands[] = new DiffcsCommand(); 38 | 39 | return $defaultCommands; 40 | } 41 | 42 | /** 43 | * Overridden so that the application doesn't expect the command 44 | * name to be the first argument. 45 | */ 46 | public function getDefinition() 47 | { 48 | $inputDefinition = parent::getDefinition(); 49 | $inputDefinition->setArguments(); 50 | 51 | return $inputDefinition; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Melody/Diffcs/Executor.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class Executor 15 | { 16 | const PHPCS_PHAR_URL = 'https://squizlabs.github.io/PHP_CodeSniffer/phpcs.phar'; 17 | 18 | /** 19 | * @var type 20 | */ 21 | protected $owner; 22 | /** 23 | * @var type 24 | */ 25 | protected $repository; 26 | /** 27 | * @var type 28 | */ 29 | protected $accessToken; 30 | /** 31 | * @var \Github\Client 32 | */ 33 | protected $client; 34 | /** 35 | * @var \League\Flysystem\Filesystem 36 | */ 37 | protected $filesystem; 38 | /** 39 | * @var type 40 | */ 41 | protected $progress; 42 | /** 43 | * @var string 44 | */ 45 | protected $codeStandard; 46 | 47 | /** 48 | * @param \Symfony\Component\Console\Output\OutputInterface $output 49 | * @param string $owner 50 | * @param string $repository 51 | * @param string $codeStandard 52 | * @param bool $githubToken Optional. 53 | * @param bool $githubUser Optional. 54 | * @param bool $githubPass Optional. 55 | */ 56 | public function __construct( 57 | $output, 58 | $owner, 59 | $repository, 60 | $codeStandard, 61 | $githubToken = false, 62 | $githubUser = false, 63 | $githubPass = false 64 | ) { 65 | $this->output = $output; 66 | $this->owner = $owner; 67 | $this->githubToken = $githubToken; 68 | $this->githubUser = $githubUser; 69 | $this->githubPass = $githubPass; 70 | $this->repository = $repository; 71 | $this->codeStandard = $codeStandard; 72 | $this->client = new \Github\Client(); 73 | $this->filesystem = new Filesystem(new Adapter(sys_get_temp_dir())); 74 | } 75 | 76 | /** 77 | * @param string $pullRequestId 78 | * @return array 79 | */ 80 | public function execute($pullRequestId) 81 | { 82 | if ($this->githubToken) { 83 | $this->authenticateWithToken(); 84 | } 85 | 86 | if ($this->githubUser) { 87 | $this->authenticateWithPassword(); 88 | } 89 | 90 | $pullRequest = $this->client->api('pull_request')->show( 91 | $this->owner, 92 | $this->repository, 93 | $pullRequestId 94 | ); 95 | 96 | $files = $this->client->api('pull_request')->files( 97 | $this->owner, 98 | $this->repository, 99 | $pullRequestId 100 | ); 101 | 102 | $downloadedFiles = $this->downloadFiles($files, $pullRequest["head"]["sha"]); 103 | 104 | return $this->runCodeSniffer($downloadedFiles); 105 | } 106 | 107 | /** 108 | * @return void 109 | */ 110 | public function authenticateWithToken() 111 | { 112 | $this->client->authenticate( 113 | $this->accessToken, 114 | null, 115 | \Github\Client::AUTH_URL_TOKEN 116 | ); 117 | } 118 | 119 | /** 120 | * @return void 121 | */ 122 | public function authenticateWithPassword() 123 | { 124 | $this->client->authenticate( 125 | $this->githubUser, 126 | $this->githubPass, 127 | \Github\Client::AUTH_HTTP_PASSWORD 128 | ); 129 | } 130 | 131 | /** 132 | * @param array $files 133 | * @param string $commitId 134 | * @return array 135 | */ 136 | public function downloadFiles($files, $commitId) 137 | { 138 | $downloadedFiles = []; 139 | 140 | foreach ($files as $file) { 141 | if (!preg_match('/.*\.php$/', $file['filename']) || $file['status'] === "removed") { 142 | continue; 143 | } 144 | 145 | $fileContent = $this->client->api('repo')->contents()->download( 146 | $this->owner, 147 | $this->repository, 148 | $file['filename'], 149 | $commitId 150 | ); 151 | 152 | $file = sys_get_temp_dir() . "/" . $file['filename']; 153 | $this->filesystem->put($file, $fileContent); 154 | 155 | $downloadedFiles[] = $file; 156 | } 157 | 158 | return $downloadedFiles; 159 | } 160 | 161 | /** 162 | * @param array $downloadedFiles 163 | * @return array 164 | */ 165 | public function runCodeSniffer($downloadedFiles) 166 | { 167 | $phpcsBinPath = self::getPhpCsBinPath(); 168 | 169 | $progress = new ProgressBar($this->output, count($downloadedFiles)); 170 | $progress->setProgressCharacter('|'); 171 | $progress->start(); 172 | 173 | $outputs = []; 174 | 175 | foreach ($downloadedFiles as $file) { 176 | if (!preg_match('/.*\.php$/', $file)) { 177 | continue; 178 | } 179 | 180 | $command = sprintf( 181 | "$phpcsBinPath %s/%s --standard=%s", 182 | sys_get_temp_dir(), 183 | $file, 184 | $this->codeStandard 185 | ); 186 | 187 | $output = shell_exec($command); 188 | $output = str_replace('/tmp/tmp/', '', $output); 189 | 190 | if (!empty($output)) { 191 | $outputs[] = $output; 192 | } 193 | 194 | $progress->advance(); 195 | } 196 | 197 | $progress->finish(); 198 | 199 | return $outputs; 200 | } 201 | 202 | private static function getPhpCsBinPath() 203 | { 204 | $phpcsBinPath = trim(shell_exec('which phpcs')); 205 | 206 | if (!$phpcsBinPath) { 207 | $phpcsBinPath = sys_get_temp_dir() . '/.diffcs/phpcs'; 208 | } 209 | 210 | if (!file_exists($phpcsBinPath)) { 211 | shell_exec(sprintf("mkdir -p %s/.diffcs", sys_get_temp_dir())); 212 | copy(self::PHPCS_PHAR_URL, $phpcsBinPath); 213 | shell_exec('chmod +x ' . $phpcsBinPath); 214 | } 215 | 216 | return $phpcsBinPath; 217 | } 218 | } 219 | --------------------------------------------------------------------------------