diff --git a/.gitignore b/.gitignore index 620cd6ce..d8c5ef7b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ config.php .htaccess PHPCI/config.yml cache -/loggerconfig.php \ No newline at end of file +/loggerconfig.php +/pluginconfig.php \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md index 7d8d2f31..1df52b9c 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (c) 2013, Block 8 Limited +Copyright (c) 2013-2014, Block 8 Limited All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/PHPCI/Application.php b/PHPCI/Application.php index 3d889967..ecc71ad5 100644 --- a/PHPCI/Application.php +++ b/PHPCI/Application.php @@ -26,11 +26,27 @@ class Application extends b8\Application $route = '/:controller/:action'; $opts = array('controller' => 'Home', 'action' => 'index'); - $this->router->clearRoutes(); - $this->router->register($route, $opts, function (&$route, Response &$response) use (&$request) { + // Inlined as a closure to fix "using $this when not in object context" on 5.3 + $validateSession = function () { + if (!empty($_SESSION['user_id'])) { + $user = b8\Store\Factory::getStore('User')->getByPrimaryKey($_SESSION['user_id']); + + if ($user) { + $_SESSION['user'] = $user; + return true; + } + + unset($_SESSION['user_id']); + } + + return false; + }; + + // Handler for the route we're about to register, checks for a valid session where necessary: + $routeHandler = function (&$route, Response &$response) use (&$request, $validateSession) { $skipValidation = in_array($route['controller'], array('session', 'webhook', 'build-status')); - if (!$skipValidation && !$this->validateSession()) { + if (!$skipValidation && !$validateSession()) { if ($request->isAjax()) { $response->setResponseCode(401); $response->setContent(''); @@ -43,7 +59,10 @@ class Application extends b8\Application } return true; - }); + }; + + $this->router->clearRoutes(); + $this->router->register($route, $opts, $routeHandler); } /** * Handle an incoming web request. @@ -54,29 +73,16 @@ class Application extends b8\Application if (View::exists('layout') && $this->response->hasLayout()) { $view = new View('layout'); + $pageTitle = $this->config->get('page_title', null); + + if (!is_null($pageTitle)) { + $view->title = $pageTitle; + } + $view->content = $this->response->getContent(); $this->response->setContent($view->render()); } return $this->response; } - - /** - * Validate whether or not the remote user has a valid session: - */ - protected function validateSession() - { - if (!empty($_SESSION['user_id'])) { - $user = b8\Store\Factory::getStore('User')->getByPrimaryKey($_SESSION['user_id']); - - if ($user) { - $_SESSION['user'] = $user; - return true; - } - - unset($_SESSION['user_id']); - } - - return false; - } } diff --git a/PHPCI/Builder.php b/PHPCI/Builder.php index e7ea97e3..69db31e1 100644 --- a/PHPCI/Builder.php +++ b/PHPCI/Builder.php @@ -19,6 +19,7 @@ use b8\Store\Factory; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; +use PHPCI\Plugin\Util\Factory as PluginFactory; /** * PHPCI Build Runner @@ -46,11 +47,6 @@ class Builder implements LoggerAwareInterface */ protected $directory; - /** - * @var bool - */ - protected $success = true; - /** * @var bool */ @@ -118,7 +114,9 @@ class Builder implements LoggerAwareInterface $this->buildLogger = new BuildLogger($logger, $build); - $this->pluginExecutor = new Plugin\Util\Executor($this->buildPluginFactory($build), $this->buildLogger); + $pluginFactory = $this->buildPluginFactory($build); + $pluginFactory->addConfigFromFile(PHPCI_DIR . "/pluginconfig.php"); + $this->pluginExecutor = new Plugin\Util\Executor($pluginFactory, $this->buildLogger); $this->commandExecutor = new CommandExecutor( $this->buildLogger, @@ -133,16 +131,22 @@ class Builder implements LoggerAwareInterface /** * Set the config array, as read from phpci.yml - * @param array + * @param array|null $config + * @throws \Exception */ - public function setConfigArray(array $config) + public function setConfigArray($config) { + if (is_null($config) || !is_array($config)) { + throw new \Exception('This project does not contain a phpci.yml file, or it is empty.'); + } + $this->config = $config; } /** * Access a variable from the phpci.yml file. * @param string + * @return mixed */ public function getConfig($key) { @@ -183,38 +187,49 @@ class Builder implements LoggerAwareInterface $this->build->setStarted(new \DateTime()); $this->store->save($this->build); $this->build->sendStatusPostback(); - $this->success = true; + $success = true; - // Set up the build: - $this->setupBuild(); + try { + // Set up the build: + $this->setupBuild(); - // Run the core plugin stages: - foreach (array('setup', 'test') as $stage) { - $this->success &= $this->pluginExecutor->executePlugins($this->config, $stage); - } + // Run the core plugin stages: + foreach (array('setup', 'test') as $stage) { + $success &= $this->pluginExecutor->executePlugins($this->config, $stage); + } - // Set the status so this can be used by complete, success and failure - // stages. - if ($this->success) { - $this->build->setStatus(Build::STATUS_SUCCESS); - } else { + // Set the status so this can be used by complete, success and failure + // stages. + if ($success) { + $this->build->setStatus(Build::STATUS_SUCCESS); + } else { + $this->build->setStatus(Build::STATUS_FAILED); + } + + // Complete stage plugins are always run + $this->pluginExecutor->executePlugins($this->config, 'complete'); + + if ($success) { + $this->pluginExecutor->executePlugins($this->config, 'success'); + $this->buildLogger->logSuccess('BUILD SUCCESSFUL!'); + } else { + $this->pluginExecutor->executePlugins($this->config, 'failure'); + $this->buildLogger->logFailure("BUILD FAILURE"); + } + + // Clean up: + $this->buildLogger->log('Removing build.'); + + $cmd = 'rm -Rf "%s"'; + if (IS_WIN) { + $cmd = 'rmdir /S /Q "%s"'; + } + $this->executeCommand($cmd, $this->buildPath); + } catch (\Exception $ex) { $this->build->setStatus(Build::STATUS_FAILED); + $this->buildLogger->logFailure('Exception: ' . $ex->getMessage()); } - // Complete stage plugins are always run - $this->pluginExecutor->executePlugins($this->config, 'complete'); - - if ($this->success) { - $this->pluginExecutor->executePlugins($this->config, 'success'); - $this->buildLogger->logSuccess('BUILD SUCCESSFUL!'); - } else { - $this->pluginExecutor->executePlugins($this->config, 'failure'); - $this->buildLogger->logFailure("BUILD FAILURE"); - } - - // Clean up: - $this->buildLogger->log('Removing build.'); - shell_exec(sprintf('rm -Rf "%s"', $this->buildPath)); // Update the build in the database, ping any external services, etc. $this->build->sendStatusPostback(); @@ -227,7 +242,7 @@ class Builder implements LoggerAwareInterface */ public function executeCommand() { - return $this->commandExecutor->buildAndExecuteCommand(func_get_args()); + return $this->commandExecutor->executeCommand(func_get_args()); } /** @@ -238,6 +253,11 @@ class Builder implements LoggerAwareInterface return $this->commandExecutor->getLastOutput(); } + public function logExecOutput($enableLog = true) + { + $this->commandExecutor->logExecOutput = $enableLog; + } + /** * Find a binary required by a plugin. * @param $binary @@ -329,10 +349,15 @@ class Builder implements LoggerAwareInterface { $this->buildLogger->logFailure($message, $exception); } - + /** + * Returns a configured instance of the plugin factory. + * + * @param Build $build + * @return PluginFactory + */ private function buildPluginFactory(Build $build) { - $pluginFactory = new Plugin\Util\Factory(); + $pluginFactory = new PluginFactory(); $self = $this; $pluginFactory->registerResource( @@ -351,6 +376,15 @@ class Builder implements LoggerAwareInterface 'PHPCI\Model\Build' ); + $logger = $this->logger; + $pluginFactory->registerResource( + function () use ($logger) { + return $logger; + }, + null, + 'Psr\Log\LoggerInterface' + ); + $pluginFactory->registerResource( function () use ($self) { $factory = new MailerFactory($self->getSystemConfig('phpci')); diff --git a/PHPCI/Command/DaemoniseCommand.php b/PHPCI/Command/DaemoniseCommand.php index f8cbc45b..b4d0295c 100644 --- a/PHPCI/Command/DaemoniseCommand.php +++ b/PHPCI/Command/DaemoniseCommand.php @@ -76,6 +76,7 @@ class DaemoniseCommand extends Command $this->run = true; $this->sleep = 0; $runner = new RunCommand($this->logger); + $runner->setBaxBuilds(1); $emptyInput = new ArgvInput(array()); diff --git a/PHPCI/Command/GenerateCommand.php b/PHPCI/Command/GenerateCommand.php index 44a499de..82c41531 100644 --- a/PHPCI/Command/GenerateCommand.php +++ b/PHPCI/Command/GenerateCommand.php @@ -37,7 +37,13 @@ class GenerateCommand extends Command */ protected function execute(InputInterface $input, OutputInterface $output) { - $gen = new CodeGenerator(Database::getConnection(), ['default' => 'PHPCI'], ['default' => PHPCI_DIR], false); + $gen = new CodeGenerator( + Database::getConnection(), + array('default' => 'PHPCI'), + array('default' => PHPCI_DIR), + false + ); + $gen->generateModels(); $gen->generateStores(); } diff --git a/PHPCI/Command/InstallCommand.php b/PHPCI/Command/InstallCommand.php index 7f9daf6d..aac34dca 100644 --- a/PHPCI/Command/InstallCommand.php +++ b/PHPCI/Command/InstallCommand.php @@ -1,28 +1,34 @@ -* @package PHPCI -* @subpackage Console -*/ + * Install console command - Installs PHPCI. + * @author Dan Cryer + * @package PHPCI + * @subpackage Console + */ class InstallCommand extends Command { protected function configure() @@ -33,137 +39,235 @@ class InstallCommand extends Command } /** - * Installs PHPCI - Can be run more than once as long as you ^C instead of entering an email address. - */ + * Installs PHPCI - Can be run more than once as long as you ^C instead of entering an email address. + */ protected function execute(InputInterface $input, OutputInterface $output) { - // Gather initial data from the user: + $this->verifyNotInstalled($output); + + $output->writeln(''); + $output->writeln('******************'); + $output->writeln(' Welcome to PHPCI'); + $output->writeln('******************'); + $output->writeln(''); + + $this->checkRequirements($output); + + $output->writeln('Please answer the following questions:'); + $output->writeln('-------------------------------------'); + $output->writeln(''); + + + /** + * @var \Symfony\Component\Console\Helper\DialogHelper + */ + $dialog = $this->getHelperSet()->get('dialog'); + + // ---- + // Get MySQL connection information and verify that it works: + // ---- + $connectionVerified = false; + + while (!$connectionVerified) { + $db = array(); + $db['servers']['read'] = $dialog->ask($output, 'Please enter your MySQL host [localhost]: ', 'localhost'); + $db['servers']['write'] = $db['servers']['read']; + $db['name'] = $dialog->ask($output, 'Please enter your database name [phpci]: ', 'phpci'); + $db['username'] = $dialog->ask($output, 'Please enter your database username [phpci]: ', 'phpci'); + $db['password'] = $dialog->askHiddenResponse($output, 'Please enter your database password: '); + + $connectionVerified = $this->verifyDatabaseDetails($db, $output); + } + + $output->writeln(''); + + // ---- + // Get basic installation details (URL, etc) + // ---- + $conf = array(); - $conf['b8']['database']['servers']['read'] = $this->ask('Enter your MySQL host: '); - $conf['b8']['database']['servers']['write'] = $conf['b8']['database']['servers']['read']; - $conf['b8']['database']['name'] = $this->ask('Enter the database name PHPCI should use: '); - $conf['b8']['database']['username'] = $this->ask('Enter your MySQL username: '); - $conf['b8']['database']['password'] = $this->ask('Enter your MySQL password: ', true); - $ask = 'Your PHPCI URL (without trailing slash): '; - $conf['phpci']['url'] = $this->ask($ask, false, array(FILTER_VALIDATE_URL,"/[^\/]$/i")); - $conf['phpci']['github']['id'] = $this->ask('(Optional) Github Application ID: ', true); - $conf['phpci']['github']['secret'] = $this->ask('(Optional) Github Application Secret: ', true); + $conf['b8']['database'] = $db; + $conf['phpci']['url'] = $dialog->askAndValidate( + $output, + 'Your PHPCI URL (without trailing slash): ', + function ($answer) { + if (!filter_var($answer, FILTER_VALIDATE_URL)) { + throw new Exception('Must be a valid URL'); + } - $conf['phpci']['email_settings']['smtp_address'] = $this->ask('(Optional) Smtp server address: ', true); - $conf['phpci']['email_settings']['smtp_port'] = $this->ask('(Optional) Smtp port: ', true); - $conf['phpci']['email_settings']['smtp_encryption'] = $this->ask('(Optional) Smtp encryption: ', true); - $conf['phpci']['email_settings']['smtp_username'] = $this->ask('(Optional) Smtp Username: ', true); - $conf['phpci']['email_settings']['smtp_password'] = $this->ask('(Optional) Smtp Password: ', true); - $conf['phpci']['email_settings']['from_address'] = $this->ask('(Optional) Email address to send from: ', true); + return $answer; + }, + false + ); - $ask = '(Optional) Default address to email notifications to: '; - $conf['phpci']['email_settings']['default_mailto_address'] = $this->ask($ask, true); + $this->writeConfigFile($conf); + $this->setupDatabase($output); + $this->createAdminUser($output, $dialog); + } - $dbUser = $conf['b8']['database']['username']; - $dbPass = $conf['b8']['database']['password']; - $dbHost = $conf['b8']['database']['servers']['write']; - $dbName = $conf['b8']['database']['name']; + /** + * Check PHP version, required modules and for disabled functions. + * @param OutputInterface $output + */ + protected function checkRequirements(OutputInterface $output) + { + $output->write('Checking requirements...'); + $errors = false; - // Create the database if it doesn't exist: - $cmd = 'mysql -u' . $dbUser . (!empty($dbPass) ? ' -p' . $dbPass : '') . ' -h' . $dbHost . - ' -e "CREATE DATABASE IF NOT EXISTS ' . $dbName . '"'; + // Check PHP version: + if (!(version_compare(PHP_VERSION, '5.3.3') >= 0)) { + $output->writeln(''); + $output->writeln('PHPCI requires at least PHP 5.3.3 to function.'); + $errors = true; + } - shell_exec($cmd); + // Check required extensions are present: + $requiredExtensions = array('PDO', 'pdo_mysql', 'mcrypt'); + foreach ($requiredExtensions as $extension) { + if (!extension_loaded($extension)) { + $output->writeln(''); + $output->writeln(''.$extension.' extension must be installed.'); + $errors = true; + } + } + + // Check required functions are callable: + $requiredFunctions = array('exec', 'shell_exec'); + + foreach ($requiredFunctions as $function) { + if (!function_exists($function)) { + $output->writeln(''); + $output->writeln('PHPCI needs to be able to call the '.$function.'() function. Is it disabled in php.ini?'); + $errors = true; + } + } + + if (!function_exists('password_hash')) { + $output->writeln(''); + $output->writeln('PHPCI requires the password_hash() function available in PHP 5.4, or the password_compat library by ircmaxell.'); + $errors = true; + } + + if ($errors) { + throw new Exception('PHPCI cannot be installed, as not all requirements are met. Please review the errors above before continuing.'); + } + + $output->writeln(' OK'); + $output->writeln(''); + } + + /** + * Try and connect to MySQL using the details provided. + * @param array $db + * @param OutputInterface $output + * @return bool + */ + protected function verifyDatabaseDetails(array $db, OutputInterface $output) + { + try { + $pdo = new PDO( + 'mysql:host='.$db['servers']['write'].';dbname='.$db['name'], + $db['username'], + $db['password'], + array( + \PDO::ATTR_PERSISTENT => false, + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, + \PDO::ATTR_TIMEOUT => 2, + \PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\'', + ) + ); + + return true; + + } catch (Exception $ex) { + $output->writeln('PHPCI could not connect to MySQL with the details provided. Please try again.'); + $output->writeln('' . $ex->getMessage() . ''); + } + + return false; + } + + /** + * Write the PHPCI config.yml file. + * @param array $config + */ + protected function writeConfigFile(array $config) + { $dumper = new \Symfony\Component\Yaml\Dumper(); - $yaml = $dumper->dump($conf); + $yaml = $dumper->dump($config, 2); file_put_contents(PHPCI_DIR . 'PHPCI/config.yml', $yaml); + } + protected function setupDatabase(OutputInterface $output) + { + $output->write('Setting up your database... '); + + // Load PHPCI's bootstrap file: require(PHPCI_DIR . 'bootstrap.php'); - // Update the database: - $gen = new \b8\Database\Generator(\b8\Database::getConnection(), 'PHPCI', './PHPCI/Model/Base/'); - $gen->generate(); - - // Try to create a user account: - $adminEmail = $this->ask('Enter your email address (leave blank if updating): ', true, FILTER_VALIDATE_EMAIL); - - if (empty($adminEmail)) { - return; + try { + // Set up the database, based on table data from the models: + $gen = new Database\Generator(Database::getConnection(), 'PHPCI', './PHPCI/Model/Base/'); + $gen->generate(); + } catch (Exception $ex) { + $output->writeln(''); + $output->writeln('PHPCI failed to set up the database.'); + $output->writeln('' . $ex->getMessage() . ''); + die; } - $adminPass = $this->ask('Enter your desired admin password: '); - $adminName = $this->ask('Enter your name: '); + + $output->writeln('OK'); + } + + protected function createAdminUser(OutputInterface $output, DialogHelper $dialog) + { + // Try to create a user account: + $adminEmail = $dialog->askAndValidate( + $output, + 'Your email address: ', + function ($answer) { + if (!filter_var($answer, FILTER_VALIDATE_EMAIL)) { + throw new Exception('Must be a valid email address.'); + } + + return $answer; + }, + false + ); + + $adminPass = $dialog->askHiddenResponse($output, 'Enter your desired admin password: '); + $adminName = $dialog->ask($output, 'Enter your name: '); try { - $user = new \PHPCI\Model\User(); + $user = new User(); $user->setEmail($adminEmail); $user->setName($adminName); $user->setIsAdmin(1); $user->setHash(password_hash($adminPass, PASSWORD_DEFAULT)); - $store = \b8\Store\Factory::getStore('User'); + $store = Factory::getStore('User'); $store->save($user); - print 'User account created!' . PHP_EOL; + $output->writeln('User account created!'); } catch (\Exception $ex) { - print 'There was a problem creating your account. :(' . PHP_EOL; - print $ex->getMessage(); + $output->writeln('PHPCI failed to create your admin account.'); + $output->writeln('' . $ex->getMessage() . ''); + die; } } - protected function ask($question, $emptyOk = false, $validationFilter = null) + protected function verifyNotInstalled(OutputInterface $output) { - print $question . ' '; + if (file_exists(PHPCI_DIR . 'PHPCI/config.yml')) { + $content = file_get_contents(PHPCI_DIR . 'PHPCI/config.yml'); - $rtn = ''; - $stdin = fopen('php://stdin', 'r'); - $rtn = fgets($stdin); - fclose($stdin); - - $rtn = trim($rtn); - - if (!$emptyOk && empty($rtn)) { - $rtn = $this->ask($question, $emptyOk, $validationFilter); - } elseif ($validationFilter != null && ! empty($rtn)) { - if (! $this -> controlFormat($rtn, $validationFilter, $statusMessage)) { - print $statusMessage; - $rtn = $this->ask($question, $emptyOk, $validationFilter); + if (!empty($content)) { + $output->writeln('PHPCI/config.yml exists and is not empty.'); + $output->writeln('If you were trying to update PHPCI, please use phpci:update instead.'); + die; } } - - return $rtn; - } - protected function controlFormat($valueToInspect, $filter, &$statusMessage) - { - $filters = !(is_array($filter))? array($filter) : $filter; - $statusMessage = ''; - $status = true; - $options = array(); - - foreach ($filters as $filter) { - if (! is_int($filter)) { - $regexp = $filter; - $filter = FILTER_VALIDATE_REGEXP; - $options = array( - 'options' => array( - 'regexp' => $regexp, - ) - ); - } - if (! filter_var($valueToInspect, $filter, $options)) { - $status = false; - - switch ($filter) - { - case FILTER_VALIDATE_URL: - $statusMessage = 'Incorrect url format.' . PHP_EOL; - break; - case FILTER_VALIDATE_EMAIL: - $statusMessage = 'Incorrect e-mail format.' . PHP_EOL; - break; - case FILTER_VALIDATE_REGEXP: - $statusMessage = 'Incorrect format.' . PHP_EOL; - break; - } - } - } - - return $status; } } diff --git a/PHPCI/Command/RunCommand.php b/PHPCI/Command/RunCommand.php index 318f9247..dc64aef9 100644 --- a/PHPCI/Command/RunCommand.php +++ b/PHPCI/Command/RunCommand.php @@ -42,6 +42,11 @@ class RunCommand extends Command */ protected $logger; + /** + * @var int + */ + protected $maxBuilds = null; + /** * @param \Monolog\Logger $logger * @param string $name @@ -52,7 +57,6 @@ class RunCommand extends Command $this->logger = $logger; } - protected function configure() { $this @@ -69,7 +73,7 @@ class RunCommand extends Command // For verbose mode we want to output all informational and above // messages to the symphony output interface. - if ($input->getOption('verbose')) { + if ($input->hasOption('verbose')) { $this->logger->pushHandler( new OutputLogHandler($this->output, Logger::INFO) ); @@ -79,7 +83,7 @@ class RunCommand extends Command $this->logger->addInfo("Finding builds to process"); $store = Factory::getStore('Build'); - $result = $store->getByStatus(0); + $result = $store->getByStatus(0, $this->maxBuilds); $this->logger->addInfo(sprintf("Found %d builds", count($result['items']))); $builds = 0; @@ -113,4 +117,9 @@ class RunCommand extends Command return $builds; } + + public function setBaxBuilds($numBuilds) + { + $this->maxBuilds = (int)$numBuilds; + } } diff --git a/PHPCI/Command/UpdateCommand.php b/PHPCI/Command/UpdateCommand.php index 855ca893..127b272e 100644 --- a/PHPCI/Command/UpdateCommand.php +++ b/PHPCI/Command/UpdateCommand.php @@ -48,8 +48,30 @@ class UpdateCommand extends Command */ protected function execute(InputInterface $input, OutputInterface $output) { + $this->verifyInstalled($output); + + $output->writeln('Updating PHPCI database.'); + // Update the database: $gen = new \b8\Database\Generator(\b8\Database::getConnection(), 'PHPCI', './PHPCI/Model/Base/'); $gen->generate(); + + $output->writeln('Done!'); + } + + protected function verifyInstalled(OutputInterface $output) + { + if (!file_exists(PHPCI_DIR . 'PHPCI/config.yml')) { + $output->writeln('PHPCI does not appear to be installed.'); + $output->writeln('Please install PHPCI via phpci:install instead.'); + die; + } + + $content = file_get_contents(PHPCI_DIR . 'PHPCI/config.yml'); + if (empty($content)) { + $output->writeln('PHPCI does not appear to be installed.'); + $output->writeln('Please install PHPCI via phpci:install instead.'); + die; + } } } diff --git a/PHPCI/Controller/BuildController.php b/PHPCI/Controller/BuildController.php index 032ce118..3ebb8432 100644 --- a/PHPCI/Controller/BuildController.php +++ b/PHPCI/Controller/BuildController.php @@ -40,6 +40,9 @@ class BuildController extends \PHPCI\Controller $this->view->plugins = $this->getUiPlugins(); $this->view->build = $build; $this->view->data = $this->getBuildData($build); + + $title = 'Build #' . $build->getId() . ' - ' . $build->getProjectTitle(); + $this->config->set('page_title', $title); } protected function getUiPlugins() diff --git a/PHPCI/Controller/BuildStatusController.php b/PHPCI/Controller/BuildStatusController.php index bc5c188d..74a1fbad 100644 --- a/PHPCI/Controller/BuildStatusController.php +++ b/PHPCI/Controller/BuildStatusController.php @@ -10,7 +10,9 @@ namespace PHPCI\Controller; use b8; +use b8\Exception\HttpException\NotFoundException; use b8\Store; +use PHPCI\BuildFactory; use PHPCI\Model\Project; use PHPCI\Model\Build; @@ -26,10 +28,13 @@ class BuildStatusController extends \PHPCI\Controller * @var \PHPCI\Store\ProjectStore */ protected $projectStore; + protected $buildStore; public function init() { - $this->projectStore = Store\Factory::getStore('Project'); + $this->response->disableLayout(); + $this->buildStore = Store\Factory::getStore('Build'); + $this->projectStore = Store\Factory::getStore('Project'); } /** @@ -41,6 +46,10 @@ class BuildStatusController extends \PHPCI\Controller $project = $this->projectStore->getById($projectId); $status = 'ok'; + if (!$project->getAllowPublicStatus()) { + die(); + } + if (isset($project) && $project instanceof Project) { $build = $project->getLatestBuild($branch, array(2,3)); @@ -52,4 +61,43 @@ class BuildStatusController extends \PHPCI\Controller header('Content-Type: image/png'); die(file_get_contents(APPLICATION_PATH . 'public/assets/img/build-' . $status . '.png')); } + + public function view($projectId) + { + $project = $this->projectStore->getById($projectId); + if (!$project) { + throw new NotFoundException('Project with id: ' . $projectId . ' not found'); + } + + if (!$project->getAllowPublicStatus()) { + throw new NotFoundException('Project with id: ' . $projectId . ' not found'); + } + + $builds = $this->getLatestBuilds($projectId); + + if (count($builds)) { + $this->view->latest = $builds[0]; + } + + $this->view->builds = $builds; + $this->view->project = $project; + + return $this->view->render(); + } + + /** + * Render latest builds for project as HTML table. + */ + protected function getLatestBuilds($projectId) + { + $criteria = array('project_id' => $projectId); + $order = array('id' => 'DESC'); + $builds = $this->buildStore->getWhere($criteria, 10, 0, array(), $order); + + foreach ($builds['items'] as &$build) { + $build = BuildFactory::getBuild($build); + } + + return $builds['items']; + } } diff --git a/PHPCI/Controller/HomeController.php b/PHPCI/Controller/HomeController.php index 95676efc..dc4027ad 100644 --- a/PHPCI/Controller/HomeController.php +++ b/PHPCI/Controller/HomeController.php @@ -41,20 +41,13 @@ class HomeController extends \PHPCI\Controller */ public function index() { - $projects = $this->projectStore->getWhere(array(), 50, 0, array(), array('title' => 'ASC')); - - $summaryBuilds = array(); - foreach ($projects['items'] as $project) { - $summaryBuilds[$project->getId()] = $this->buildStore->getLatestBuilds($project->getId()); - } - - $summaryView = new b8\View('SummaryTable'); - $summaryView->projects = $projects['items']; - $summaryView->builds = $summaryBuilds; + $projects = $this->projectStore->getWhere(array(), 50, 0, array(), array('title' => 'ASC')); $this->view->builds = $this->getLatestBuildsHtml(); $this->view->projects = $projects['items']; - $this->view->summary = $summaryView->render(); + $this->view->summary = $this->getSummaryHtml($projects); + + $this->config->set('page_title', 'Dashboard'); return $this->view->render(); } @@ -67,6 +60,26 @@ class HomeController extends \PHPCI\Controller die($this->getLatestBuildsHtml()); } + public function summary() + { + $projects = $this->projectStore->getWhere(array(), 50, 0, array(), array('title' => 'ASC')); + die($this->getSummaryHtml($projects)); + } + + protected function getSummaryHtml($projects) + { + $summaryBuilds = array(); + foreach ($projects['items'] as $project) { + $summaryBuilds[$project->getId()] = $this->buildStore->getLatestBuilds($project->getId()); + } + + $summaryView = new b8\View('SummaryTable'); + $summaryView->projects = $projects['items']; + $summaryView->builds = $summaryBuilds; + + return $summaryView->render(); + } + /** * Get latest builds and render as a table. */ diff --git a/PHPCI/Controller/PluginController.php b/PHPCI/Controller/PluginController.php index b7e32ddb..4f51e6c9 100644 --- a/PHPCI/Controller/PluginController.php +++ b/PHPCI/Controller/PluginController.php @@ -75,6 +75,8 @@ class PluginController extends \PHPCI\Controller $this->view->plugins = $pluginInfo->getInstalledPlugins(); + $this->config->set('page_title', 'Plugins'); + return $this->view->render(); } diff --git a/PHPCI/Controller/ProjectController.php b/PHPCI/Controller/ProjectController.php index 19fb3149..eea5a7a8 100644 --- a/PHPCI/Controller/ProjectController.php +++ b/PHPCI/Controller/ProjectController.php @@ -10,6 +10,8 @@ namespace PHPCI\Controller; use PHPCI\BuildFactory; +use PHPCI\Helper\Github; +use PHPCI\Helper\SshKey; use PHPCI\Model\Build; use PHPCI\Model\Project; use b8; @@ -17,6 +19,7 @@ use b8\Config; use b8\Controller; use b8\Store; use b8\Form; +use b8\Exception\HttpException\NotFoundException; /** * Project Controller - Allows users to create, edit and view projects. @@ -47,7 +50,11 @@ class ProjectController extends \PHPCI\Controller */ public function view($projectId) { - $project = $this->projectStore->getById($projectId); + $project = $this->projectStore->getById($projectId); + if (!$project) { + throw new NotFoundException('Project with id: ' . $projectId . ' not found'); + } + $page = $this->getParam('p', 1); $builds = $this->getLatestBuildsHtml($projectId, (($page - 1) * 10)); @@ -56,6 +63,8 @@ class ProjectController extends \PHPCI\Controller $this->view->project = $project; $this->view->page = $page; + $this->config->set('page_title', $project->getTitle()); + return $this->view->render(); } @@ -73,6 +82,7 @@ class ProjectController extends \PHPCI\Controller $build->setStatus(Build::STATUS_NEW); $build->setBranch($project->getType() === 'hg' ? 'default' : 'master'); $build->setCreated(new \DateTime()); + $build->setCommitterEmail($_SESSION['user']->getEmail()); $build = $this->buildStore->save($build); @@ -129,38 +139,24 @@ class ProjectController extends \PHPCI\Controller */ public function add() { + $this->config->set('page_title', 'Add Project'); + if (!$_SESSION['user']->getIsAdmin()) { throw new \Exception('You do not have permission to do that.'); } $method = $this->request->getMethod(); - if ($method == 'POST') { - $values = $this->getParams(); - $pub = null; - } else { - $tempPath = sys_get_temp_dir() . '/'; + $pub = null; + $values = $this->getParams(); - // FastCGI fix for Windows machines, where temp path is not available to - // PHP, and defaults to the unwritable system directory. If the temp - // path is pointing to the system directory, shift to the 'TEMP' - // sub-folder, which should also exist, but actually be writable. - if ($tempPath == getenv("SystemRoot") . '/') { - $tempPath = getenv("SystemRoot") . '/TEMP/'; - } + if ($method != 'POST') { + $sshKey = new SshKey(); + $key = $sshKey->generate(); - $keyFile = $tempPath . md5(microtime(true)); - - if (!is_dir($tempPath)) { - mkdir($tempPath); - } - - shell_exec('ssh-keygen -q -t rsa -b 2048 -f '.$keyFile.' -N "" -C "deploy@phpci"'); - - $pub = file_get_contents($keyFile . '.pub'); - $prv = file_get_contents($keyFile); - - $values = array('key' => $prv, 'pubkey' => $pub); + $values['key'] = $key['private_key']; + $values['pubkey'] = $key['public_key']; + $pub = $key['public_key']; } $form = $this->projectForm($values); @@ -179,14 +175,25 @@ class ProjectController extends \PHPCI\Controller if ($values['type'] == "gitlab") { preg_match('`^(.*)@(.*):(.*)/(.*)\.git`', $values['reference'], $matches); + $info = array(); - $info["user"] = $matches[1]; - $info["domain"] = $matches[2]; + if (isset($matches[1])) { + $info["user"] = $matches[1]; + } + + if (isset($matches[2])) { + $info["domain"] = $matches[2]; + } + $values['access_information'] = serialize($info); - $values['reference'] = $matches[3]."/".$matches[4]; + + if (isset($matches[3]) && isset($matches[4])) { + $values['reference'] = $matches[3]."/".$matches[4]; + } } $values['git_key'] = $values['key']; + $values['public_key'] = $values['pubkey']; $project = new Project(); $project->setValues($values); @@ -209,11 +216,15 @@ class ProjectController extends \PHPCI\Controller $method = $this->request->getMethod(); $project = $this->projectStore->getById($projectId); + $this->config->set('page_title', 'Edit: ' . $project->getTitle()); + + if ($method == 'POST') { $values = $this->getParams(); } else { $values = $project->getDataArray(); $values['key'] = $values['git_key']; + $values['pubkey'] = $values['public_key']; if ($values['type'] == "gitlab") { $accessInfo = $project->getAccessInformation(); @@ -237,6 +248,7 @@ class ProjectController extends \PHPCI\Controller $values = $form->getValues(); $values['git_key'] = $values['key']; + $values['public_key'] = $values['pubkey']; if ($values['type'] == "gitlab") { preg_match('`^(.*)@(.*):(.*)/(.*)\.git`', $values['reference'], $matches); @@ -318,6 +330,23 @@ class ProjectController extends \PHPCI\Controller $field->setRows(6); $form->addField($field); + $field = new Form\Element\TextArea('build_config'); + $field->setRequired(false); + $label = 'PHPCI build config for this project (if you cannot add a phpci.yml file in the project repository)'; + $field->setLabel($label); + $field->setClass('form-control'); + $field->setContainerClass('form-group'); + $field->setRows(6); + $form->addField($field); + + $field = new Form\Element\Checkbox('allow_public_status'); + $field->setRequired(false); + $field->setLabel('Enable public status page and image for this project?'); + $field->setContainerClass('form-group'); + $field->setCheckedValue(1); + $field->setValue(1); + $form->addField($field); + $field = new Form\Element\Submit(); $field->setValue('Save Project'); $field->setContainerClass('form-group'); @@ -333,46 +362,8 @@ class ProjectController extends \PHPCI\Controller */ protected function githubRepositories() { - $token = Config::getInstance()->get('phpci.github.token'); - - if (!$token) { - die(json_encode(null)); - } - - $cache = \b8\Cache::getCache(\b8\Cache::TYPE_APC); - $rtn = $cache->get('phpci_github_repos'); - - if (!$rtn) { - $orgs = $this->doGithubApiRequest('/user/orgs', array('access_token' => $token)); - - $params = array('type' => 'all', 'access_token' => $token); - $repos = array(); - $repos['user'] = $this->doGithubApiRequest('/user/repos', $params); - - - foreach ($orgs as $org) { - $repos[$org['login']] = $this->doGithubApiRequest('/orgs/'.$org['login'].'/repos', $params); - } - - $rtn = array(); - foreach ($repos as $repoGroup) { - foreach ($repoGroup as $repo) { - $rtn['repos'][] = $repo['full_name']; - } - } - - $cache->set('phpci_github_repos', $rtn); - } - - die(json_encode($rtn)); - } - - protected function doGithubApiRequest($url, $params) - { - $http = new \b8\HttpClient('https://api.github.com'); - $res = $http->get($url, $params); - - return $res['body']; + $github = new Github(); + die(json_encode($github->getRepositories())); } protected function getReferenceValidator($values) diff --git a/PHPCI/Controller/SessionController.php b/PHPCI/Controller/SessionController.php index 15563b19..def6d4c4 100644 --- a/PHPCI/Controller/SessionController.php +++ b/PHPCI/Controller/SessionController.php @@ -10,6 +10,7 @@ namespace PHPCI\Controller; use b8; +use PHPCI\Helper\Email; /** * Session Controller - Handles user login / logout. @@ -88,4 +89,74 @@ class SessionController extends \PHPCI\Controller header('Location: ' . PHPCI_URL); die; } + + public function forgotPassword() + { + if ($this->request->getMethod() == 'POST') { + $email = $this->getParam('email', null); + $user = $this->userStore->getByEmail($email); + + if (empty($user)) { + $this->view->error = 'No user exists with that email address, please try again.'; + return $this->view->render(); + } + + $key = md5(date('Y-m-d') . $user->getHash()); + $url = PHPCI_URL; + $name = $user->getName(); + $userId = $user->getId(); + + $message = <<setEmailTo($user->getEmail(), $user->getName()); + $email->setSubject('Password reset'); + $email->setBody($message); + $email->send(); + + $this->view->emailed = true; + } + + return $this->view->render(); + } + + public function resetPassword($userId, $key) + { + $user = $this->userStore->getById($userId); + $userKey = md5(date('Y-m-d') . $user->getHash()); + + if (empty($user) || $key != $userKey) { + $this->view->error = 'Invalid password reset request.'; + return $this->view->render(); + } + + if ($this->request->getMethod() == 'POST') { + $hash = password_hash($this->getParam('password'), PASSWORD_DEFAULT); + $user->setHash($hash); + + $_SESSION['user'] = $this->userStore->save($user); + $_SESSION['user_id'] = $user->getId(); + + header('Location: ' . PHPCI_URL); + die; + } + + $this->view->id = $userId; + $this->view->key = $key; + + return $this->view->render(); + } } diff --git a/PHPCI/Controller/SettingsController.php b/PHPCI/Controller/SettingsController.php index 6f8f544c..2e0fc5ec 100644 --- a/PHPCI/Controller/SettingsController.php +++ b/PHPCI/Controller/SettingsController.php @@ -39,7 +39,15 @@ class SettingsController extends Controller public function index() { $this->view->settings = $this->settings; + + $emailSettings = array(); + + if (isset($this->settings['phpci']['email_settings'])) { + $emailSettings = $this->settings['phpci']['email_settings']; + } + $this->view->github = $this->getGithubForm(); + $this->view->emailSettings = $this->getEmailForm($emailSettings); if (!empty($this->settings['phpci']['github']['token'])) { $this->view->githubUser = $this->getGithubUser($this->settings['phpci']['github']['token']); @@ -52,17 +60,28 @@ class SettingsController extends Controller { $this->settings['phpci']['github']['id'] = $this->getParam('githubid', ''); $this->settings['phpci']['github']['secret'] = $this->getParam('githubsecret', ''); - $error = $this->storeSettings(); - if($error) - { + if ($error) { header('Location: ' . PHPCI_URL . 'settings?saved=2'); - } - else - { + } else { header('Location: ' . PHPCI_URL . 'settings?saved=1'); } + + die; + } + + public function email() + { + $this->settings['phpci']['email_settings'] = $this->getParams(); + $error = $this->storeSettings(); + + if ($error) { + header('Location: ' . PHPCI_URL . 'settings?saved=2'); + } else { + header('Location: ' . PHPCI_URL . 'settings?saved=1'); + } + die; } @@ -120,18 +139,23 @@ class SettingsController extends Controller $field->setLabel('Application ID'); $field->setClass('form-control'); $field->setContainerClass('form-group'); - $field->setValue($this->settings['phpci']['github']['id']); $form->addField($field); + if (isset($this->settings['phpci']['github']['id'])) { + $field->setValue($this->settings['phpci']['github']['id']); + } + $field = new Form\Element\Text('githubsecret'); $field->setRequired(true); $field->setPattern('[a-zA-Z0-9]+'); $field->setLabel('Application Secret'); $field->setClass('form-control'); $field->setContainerClass('form-group'); - $field->setValue($this->settings['phpci']['github']['secret']); $form->addField($field); + if (isset($this->settings['phpci']['github']['secret'])) { + $field->setValue($this->settings['phpci']['github']['secret']); + } $field = new Form\Element\Submit(); $field->setValue('Save »'); @@ -141,6 +165,76 @@ class SettingsController extends Controller return $form; } + protected function getEmailForm($values = array()) + { + $form = new Form(); + $form->setMethod('POST'); + $form->setAction(PHPCI_URL . 'settings/email'); + $form->addField(new Form\Element\Csrf('csrf')); + + $field = new Form\Element\Text('smtp_address'); + $field->setRequired(false); + $field->setLabel('SMTP Server'); + $field->setClass('form-control'); + $field->setContainerClass('form-group'); + $field->setValue('localhost'); + $form->addField($field); + + $field = new Form\Element\Text('smtp_port'); + $field->setRequired(false); + $field->setPattern('[0-9]+'); + $field->setLabel('SMTP Port'); + $field->setClass('form-control'); + $field->setContainerClass('form-group'); + $field->setValue(25); + $form->addField($field); + + $field = new Form\Element\Text('smtp_username'); + $field->setRequired(false); + $field->setLabel('SMTP Username'); + $field->setClass('form-control'); + $field->setContainerClass('form-group'); + $form->addField($field); + + $field = new Form\Element\Text('smtp_password'); + $field->setRequired(false); + $field->setLabel('SMTP Password'); + $field->setClass('form-control'); + $field->setContainerClass('form-group'); + $form->addField($field); + + $field = new Form\Element\Email('from_address'); + $field->setRequired(false); + $field->setLabel('From Email Address'); + $field->setClass('form-control'); + $field->setContainerClass('form-group'); + $form->addField($field); + + $field = new Form\Element\Email('default_mailto_address'); + $field->setRequired(false); + $field->setLabel('Default Notification Address'); + $field->setClass('form-control'); + $field->setContainerClass('form-group'); + $form->addField($field); + + $field = new Form\Element\Checkbox('smtp_encryption'); + $field->setCheckedValue(1); + $field->setRequired(false); + $field->setLabel('Use SMTP encryption?'); + $field->setContainerClass('form-group'); + $field->setValue(1); + $form->addField($field); + + $field = new Form\Element\Submit(); + $field->setValue('Save »'); + $field->setClass('btn btn-success pull-right'); + $form->addField($field); + + $form->setValues($values); + + return $form; + } + protected function getGithubUser($token) { $http = new HttpClient('https://api.github.com'); diff --git a/PHPCI/Controller/UserController.php b/PHPCI/Controller/UserController.php index ad41aff6..817cb5e8 100644 --- a/PHPCI/Controller/UserController.php +++ b/PHPCI/Controller/UserController.php @@ -29,7 +29,7 @@ class UserController extends Controller public function init() { - $this->userStore = b8\Store\Factory::getStore('User'); + $this->userStore = b8\Store\Factory::getStore('User'); } /** @@ -40,6 +40,63 @@ class UserController extends Controller $users = $this->userStore->getWhere(array(), 1000, 0, array(), array('email' => 'ASC')); $this->view->users = $users; + $this->config->set('page_title', 'Users'); + + return $this->view->render(); + } + + public function profile() + { + $user = $_SESSION['user']; + $values = $user->getDataArray(); + + if ($this->request->getMethod() == 'POST') { + $values = $this->getParams(); + + if (!empty($values['password'])) { + $values['hash'] = password_hash($values['password'], PASSWORD_DEFAULT); + } + + $this->view->updated = true; + + $user->setValues($values); + $_SESSION['user'] = $this->userStore->save($user); + } + + $form = new Form(); + $form->setAction(PHPCI_URL.'user/profile'); + $form->setMethod('POST'); + + $name = new Form\Element\Text('name'); + $name->setClass('form-control'); + $name->setContainerClass('form-group'); + $name->setLabel('Name'); + $name->setRequired(true); + $form->addField($name); + + $email = new Form\Element\Email('email'); + $email->setClass('form-control'); + $email->setContainerClass('form-group'); + $email->setLabel('Email Address'); + $email->setRequired(true); + $form->addField($email); + + $password = new Form\Element\Password('password'); + $password->setClass('form-control'); + $password->setContainerClass('form-group'); + $password->setLabel('Password (leave blank if you don\'t want to change it)'); + $password->setRequired(false); + $form->addField($password); + + $submit = new Form\Element\Submit(); + $submit->setClass('btn btn-success'); + $submit->setValue('Save »'); + $form->addField($submit); + + $form->setValues($values); + + $this->view->form = $form; + return $this->view->render(); } @@ -52,6 +109,9 @@ class UserController extends Controller throw new \Exception('You do not have permission to do that.'); } + $this->config->set('page_title', 'Add User'); + + $method = $this->request->getMethod(); if ($method == 'POST') { @@ -96,6 +156,9 @@ class UserController extends Controller $method = $this->request->getMethod(); $user = $this->userStore->getById($userId); + $this->config->set('page_title', 'Edit: ' . $user->getName()); + + if ($method == 'POST') { $values = $this->getParams(); } else { diff --git a/PHPCI/Controller/WebhookController.php b/PHPCI/Controller/WebhookController.php index 1b118440..9a9d143b 100644 --- a/PHPCI/Controller/WebhookController.php +++ b/PHPCI/Controller/WebhookController.php @@ -97,7 +97,7 @@ class WebhookController extends \PHPCI\Controller } try { - $this->_buildStore->save($build); + $this->buildStore->save($build); /** bugfix: Errors with PHPCI GitHub hook #296 */ } catch (\Exception $ex) { header('HTTP/1.1 500 Internal Server Error'); header('Ex: ' . $ex->getMessage()); diff --git a/PHPCI/Helper/BuildInterpolator.php b/PHPCI/Helper/BuildInterpolator.php index d4609793..fef51bee 100644 --- a/PHPCI/Helper/BuildInterpolator.php +++ b/PHPCI/Helper/BuildInterpolator.php @@ -58,4 +58,4 @@ class BuildInterpolator $values = array_values($this->interpolation_vars); return str_replace($keys, $values, $input); } -} \ No newline at end of file +} diff --git a/PHPCI/Helper/CommandExecutor.php b/PHPCI/Helper/CommandExecutor.php index a422d970..438829af 100644 --- a/PHPCI/Helper/CommandExecutor.php +++ b/PHPCI/Helper/CommandExecutor.php @@ -25,6 +25,9 @@ class CommandExecutor protected $lastOutput; + public $logExecOutput = true; + + /** * The path which findBinary will look in. * @var string @@ -48,22 +51,12 @@ class CommandExecutor $this->rootDir = $rootDir; } - /** - * Executes shell commands. Accepts multiple arguments the first - * is the template and everything else is inserted in. c.f. sprintf - * @return bool Indicates success - */ - public function executeCommand() - { - return $this->buildAndExecuteCommand(func_get_args()); - } - /** * Executes shell commands. * @param array $args * @return bool Indicates success */ - public function buildAndExecuteCommand($args = array()) + public function executeCommand($args = array()) { $this->lastOutput = array(); @@ -80,7 +73,7 @@ class CommandExecutor $lastOutput = trim($lastOutput, '"'); } - if (!empty($this->lastOutput) && ($this->verbose|| $status != 0)) { + if ($this->logExecOutput && !empty($this->lastOutput) && ($this->verbose|| $status != 0)) { $this->logger->log($this->lastOutput); } @@ -127,7 +120,7 @@ class CommandExecutor } // Use "where" for windows and "which" for other OS - $findCmd = (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') ? 'which' : 'where'; + $findCmd = IS_WIN ? 'where' : 'which'; $findCmdResult = trim(shell_exec($findCmd . ' ' . $bin)); if (!empty($findCmdResult)) { diff --git a/PHPCI/Helper/Email.php b/PHPCI/Helper/Email.php new file mode 100644 index 00000000..3ab57b95 --- /dev/null +++ b/PHPCI/Helper/Email.php @@ -0,0 +1,127 @@ +'; + + protected $emailTo = array(); + protected $emailCc = array(); + protected $subject = 'Email from PHPCI'; + protected $body = ''; + protected $isHtml = false; + protected $config; + + public function __construct() + { + $this->config = Config::getInstance(); + } + + public function setEmailTo($email, $name = null) + { + $this->emailTo[$email] = $name; + + return $this; + } + + public function addCc($email, $name = null) + { + $this->emailCc[$email] = $name; + + return $this; + } + + public function setSubject($subject) + { + $this->subject = $subject; + + return $this; + } + + public function setBody($body) + { + $this->body = $body; + + return $this; + } + + public function setIsHtml($isHtml = false) + { + $this->isHtml = $isHtml; + + return $this; + } + + public function send() + { + $smtpServer = $this->config->get('phpci.email_settings.smtp_address'); + + if (empty($smtpServer)) { + return $this->sendViaMail(); + } else { + return $this->sendViaSwiftMailer(); + } + } + + protected function sendViaMail() + { + $headers = ''; + + if ($this->isHtml) { + $headers = 'Content-Type: text/html' . PHP_EOL; + } + + $headers .= 'From: ' . $this->getFrom() . PHP_EOL; + + $emailTo = array(); + foreach ($this->emailTo as $email => $name) { + $thisTo = $email; + + if (!is_null($name)) { + $thisTo = '"' . $name . '" <' . $thisTo . '>'; + } + + $emailTo[] = $thisTo; + } + + $emailTo = implode(', ', $emailTo); + + return mail($emailTo, $this->subject, $this->body, $headers); + } + + protected function sendViaSwiftMailer() + { + $factory = new MailerFactory($this->config->get('phpci')); + $mailer = $factory->getSwiftMailerFromConfig(); + + $message = \Swift_Message::newInstance($this->subject) + ->setFrom($this->getFrom()) + ->setTo($this->emailTo) + ->setBody($this->body); + + if ($this->isHtml) { + $message->setContentType('text/html'); + } + + if (is_array($this->emailCc) && count($this->emailCc)) { + $message->setCc($this->emailCc); + } + + return $mailer->send($message); + } + + protected function getFrom() + { + $email = $this->config->get('phpci.email_settings.from_address', self::DEFAULT_FROM); + + if (empty($email)) { + $email = self::DEFAULT_FROM; + } + + return $email; + } +} diff --git a/PHPCI/Helper/Github.php b/PHPCI/Helper/Github.php new file mode 100644 index 00000000..6ce2d556 --- /dev/null +++ b/PHPCI/Helper/Github.php @@ -0,0 +1,57 @@ +get($url, $params); + + return $res['body']; + } + + /** + * Get an array of repositories from Github's API. + */ + public function getRepositories() + { + $token = Config::getInstance()->get('phpci.github.token'); + + if (!$token) { + die(json_encode(null)); + } + + $cache = Cache::getCache(Cache::TYPE_APC); + $rtn = $cache->get('phpci_github_repos'); + + if (!$rtn) { + $orgs = $this->makeRequest('/user/orgs', array('access_token' => $token)); + + $params = array('type' => 'all', 'access_token' => $token); + $repos = array(); + $repos['user'] = $this->makeRequest('/user/repos', $params); + + + foreach ($orgs as $org) { + $repos[$org['login']] = $this->makeRequest('/orgs/'.$org['login'].'/repos', $params); + } + + $rtn = array(); + foreach ($repos as $repoGroup) { + foreach ($repoGroup as $repo) { + $rtn['repos'][] = $repo['full_name']; + } + } + + $cache->set('phpci_github_repos', $rtn); + } + + return $rtn; + } +} diff --git a/PHPCI/Helper/MailerFactory.php b/PHPCI/Helper/MailerFactory.php index 33bb8b74..d8625630 100644 --- a/PHPCI/Helper/MailerFactory.php +++ b/PHPCI/Helper/MailerFactory.php @@ -3,16 +3,16 @@ namespace PHPCI\Helper; -class MailerFactory { - +class MailerFactory +{ /** * @var array */ protected $emailConfig; - public function __construct($phpCiConfig = null) + public function __construct($config = null) { - $this->emailConfig = isset($phpCiSettings['email_settings']) ?: array(); + $this->emailConfig = isset($config['email_settings']) ?: array(); } /** @@ -33,7 +33,7 @@ class MailerFactory { return \Swift_Mailer::newInstance($transport); } - protected function getMailConfig($configName) + protected function getMailConfig($configName) { if (isset($this->emailConfig[$configName]) && $this->emailConfig[$configName] != "") { return $this->emailConfig[$configName]; @@ -54,5 +54,4 @@ class MailerFactory { } } } - -} \ No newline at end of file +} diff --git a/PHPCI/Helper/SshKey.php b/PHPCI/Helper/SshKey.php new file mode 100644 index 00000000..7c944e1a --- /dev/null +++ b/PHPCI/Helper/SshKey.php @@ -0,0 +1,46 @@ +canGenerateKeys()) { + shell_exec('ssh-keygen -q -t rsa -b 2048 -f '.$keyFile.' -N "" -C "deploy@phpci"'); + + $pub = file_get_contents($keyFile . '.pub'); + $prv = file_get_contents($keyFile); + + $return = array('private_key' => $prv, 'public_key' => $pub); + } + + return $return; + } + + public function canGenerateKeys() + { + $keygen = @shell_exec('ssh-keygen'); + $canGenerateKeys = !empty($keygen); + + return $canGenerateKeys; + } +} diff --git a/PHPCI/Logging/Handler.php b/PHPCI/Logging/Handler.php new file mode 100644 index 00000000..b38c37a1 --- /dev/null +++ b/PHPCI/Logging/Handler.php @@ -0,0 +1,133 @@ + 'Warning', + E_NOTICE => 'Notice', + E_USER_ERROR => 'User Error', + E_USER_WARNING => 'User Warning', + E_USER_NOTICE => 'User Notice', + E_STRICT => 'Runtime Notice', + E_RECOVERABLE_ERROR => 'Catchable Fatal Error', + E_DEPRECATED => 'Deprecated', + E_USER_DEPRECATED => 'User Deprecated', + ); + + /** + * @var LoggerInterface + */ + protected $logger; + + public function __construct(LoggerInterface $logger = null) + { + $this->logger = $logger; + } + + public static function register(LoggerInterface $logger = null) + { + $handler = new static($logger); + + set_error_handler(array($handler, 'handleError')); + register_shutdown_function(array($handler, 'handleFatalError')); + + set_exception_handler(array($handler, 'handleException')); + } + + /** + * @param integer $level + * @param string $message + * @param string $file + * @param integer $line + * + * @throws \ErrorException + */ + public function handleError($level, $message, $file, $line) + { + if (error_reporting() & $level) { + + $exception_level = isset($this->levels[$level]) ? $this->levels[$level] : $level; + + throw new \ErrorException( + sprintf('%s: %s in %s line %d', $exception_level, $message, $file, $line), + 0, + $level, + $file, + $line + ); + } + } + + /** + * @throws \ErrorException + */ + public function handleFatalError() + { + $fatal_error = error_get_last(); + + try { + if (($error = error_get_last()) !== null) { + $error = new \ErrorException( + sprintf( + '%s: %s in %s line %d', + $fatal_error['type'], + $fatal_error['message'], + $fatal_error['file'], + $fatal_error['line'] + ), + 0, + $fatal_error['type'], + $fatal_error['file'], + $fatal_error['line'] + ); + $this->log($error); + } + } catch (\Exception $e) { + $error = new \ErrorException( + sprintf( + '%s: %s in %s line %d', + $fatal_error['type'], + $fatal_error['message'], + $fatal_error['file'], + $fatal_error['line'] + ), + 0, + $fatal_error['type'], + $fatal_error['file'], + $fatal_error['line'] + ); + $this->log($error); + } + } + + /** + * @param \Exception $exception + */ + public function handleException(\Exception $exception) + { + $this->log($exception); + } + + protected function log(\Exception $exception) + { + if (null !== $this->logger) { + + $message = sprintf( + '%s: %s (uncaught exception) at %s line %s', + get_class($exception), + $exception->getMessage(), + $exception->getFile(), + $exception->getLine() + ); + + $this->logger->error($message, array('exception' => $exception)); + } + } +} diff --git a/PHPCI/Logging/OutputLogHandler.php b/PHPCI/Logging/OutputLogHandler.php index d573b03d..a0b8e5d9 100644 --- a/PHPCI/Logging/OutputLogHandler.php +++ b/PHPCI/Logging/OutputLogHandler.php @@ -28,6 +28,4 @@ class OutputLogHandler extends AbstractProcessingHandler { $this->output->writeln((string)$record['formatted']); } - - } diff --git a/PHPCI/Model/Base/BuildBase.php b/PHPCI/Model/Base/BuildBase.php index 59e4c21e..569c9d6b 100644 --- a/PHPCI/Model/Base/BuildBase.php +++ b/PHPCI/Model/Base/BuildBase.php @@ -631,31 +631,4 @@ class BuildBase extends Model { return Factory::getStore('BuildMeta', 'PHPCI')->getByBuildId($this->getId()); } - - - - - public static function getByPrimaryKey($value, $useConnection = 'read') - { - return Factory::getStore('Build', 'PHPCI')->getByPrimaryKey($value, $useConnection); - } - - - public static function getById($value, $useConnection = 'read') - { - return Factory::getStore('Build', 'PHPCI')->getById($value, $useConnection); - } - - public static function getByProjectId($value, $limit = null, $useConnection = 'read') - { - return Factory::getStore('Build', 'PHPCI')->getByProjectId($value, $limit, $useConnection); - } - - public static function getByStatus($value, $limit = null, $useConnection = 'read') - { - return Factory::getStore('Build', 'PHPCI')->getByStatus($value, $limit, $useConnection); - } - - - } diff --git a/PHPCI/Model/Base/BuildMetaBase.php b/PHPCI/Model/Base/BuildMetaBase.php index 83333102..fc9183b2 100644 --- a/PHPCI/Model/Base/BuildMetaBase.php +++ b/PHPCI/Model/Base/BuildMetaBase.php @@ -98,7 +98,7 @@ class BuildMetaBase extends Model 'default' => null, ), 'meta_value' => array( - 'type' => 'text', + 'type' => 'longtext', 'nullable' => true, 'default' => null, ), @@ -337,26 +337,4 @@ class BuildMetaBase extends Model { return $this->setBuildId($value->getId()); } - - - - - public static function getByPrimaryKey($value, $useConnection = 'read') - { - return Factory::getStore('BuildMeta', 'PHPCI')->getByPrimaryKey($value, $useConnection); - } - - - public static function getById($value, $useConnection = 'read') - { - return Factory::getStore('BuildMeta', 'PHPCI')->getById($value, $useConnection); - } - - public static function getByBuildId($value, $limit = null, $useConnection = 'read') - { - return Factory::getStore('BuildMeta', 'PHPCI')->getByBuildId($value, $limit, $useConnection); - } - - - } diff --git a/PHPCI/Model/Base/ProjectBase.php b/PHPCI/Model/Base/ProjectBase.php index df1c0a34..9e2f5922 100644 --- a/PHPCI/Model/Base/ProjectBase.php +++ b/PHPCI/Model/Base/ProjectBase.php @@ -37,10 +37,13 @@ class ProjectBase extends Model 'title' => null, 'reference' => null, 'git_key' => null, + 'public_key' => null, 'type' => null, 'token' => null, 'access_information' => null, 'last_commit' => null, + 'build_config' => null, + 'allow_public_status' => null, ); /** @@ -52,10 +55,13 @@ class ProjectBase extends Model 'title' => 'getTitle', 'reference' => 'getReference', 'git_key' => 'getGitKey', + 'public_key' => 'getPublicKey', 'type' => 'getType', 'token' => 'getToken', 'access_information' => 'getAccessInformation', 'last_commit' => 'getLastCommit', + 'build_config' => 'getBuildConfig', + 'allow_public_status' => 'getAllowPublicStatus', // Foreign key getters: ); @@ -69,10 +75,13 @@ class ProjectBase extends Model 'title' => 'setTitle', 'reference' => 'setReference', 'git_key' => 'setGitKey', + 'public_key' => 'setPublicKey', 'type' => 'setType', 'token' => 'setToken', 'access_information' => 'setAccessInformation', 'last_commit' => 'setLastCommit', + 'build_config' => 'setBuildConfig', + 'allow_public_status' => 'setAllowPublicStatus', // Foreign key setters: ); @@ -103,6 +112,11 @@ class ProjectBase extends Model 'nullable' => true, 'default' => null, ), + 'public_key' => array( + 'type' => 'text', + 'nullable' => true, + 'default' => null, + ), 'type' => array( 'type' => 'varchar', 'length' => 50, @@ -126,6 +140,15 @@ class ProjectBase extends Model 'nullable' => true, 'default' => null, ), + 'build_config' => array( + 'type' => 'text', + 'nullable' => true, + 'default' => null, + ), + 'allow_public_status' => array( + 'type' => 'tinyint', + 'length' => 4, + ), ); /** @@ -190,6 +213,18 @@ class ProjectBase extends Model return $rtn; } + /** + * Get the value of PublicKey / public_key. + * + * @return string + */ + public function getPublicKey() + { + $rtn = $this->data['public_key']; + + return $rtn; + } + /** * Get the value of Type / type. * @@ -238,6 +273,30 @@ class ProjectBase extends Model return $rtn; } + /** + * Get the value of BuildConfig / build_config. + * + * @return string + */ + public function getBuildConfig() + { + $rtn = $this->data['build_config']; + + return $rtn; + } + + /** + * Get the value of AllowPublicStatus / allow_public_status. + * + * @return int + */ + public function getAllowPublicStatus() + { + $rtn = $this->data['allow_public_status']; + + return $rtn; + } + /** * Set the value of Id / id. * @@ -316,6 +375,24 @@ class ProjectBase extends Model $this->_setModified('git_key'); } + /** + * Set the value of PublicKey / public_key. + * + * @param $value string + */ + public function setPublicKey($value) + { + $this->_validateString('PublicKey', $value); + + if ($this->data['public_key'] === $value) { + return; + } + + $this->data['public_key'] = $value; + + $this->_setModified('public_key'); + } + /** * Set the value of Type / type. * @@ -390,6 +467,44 @@ class ProjectBase extends Model $this->_setModified('last_commit'); } + /** + * Set the value of BuildConfig / build_config. + * + * @param $value string + */ + public function setBuildConfig($value) + { + $this->_validateString('BuildConfig', $value); + + if ($this->data['build_config'] === $value) { + return; + } + + $this->data['build_config'] = $value; + + $this->_setModified('build_config'); + } + + /** + * Set the value of AllowPublicStatus / allow_public_status. + * + * Must not be null. + * @param $value int + */ + public function setAllowPublicStatus($value) + { + $this->_validateNotNull('AllowPublicStatus', $value); + $this->_validateInt('AllowPublicStatus', $value); + + if ($this->data['allow_public_status'] === $value) { + return; + } + + $this->data['allow_public_status'] = $value; + + $this->_setModified('allow_public_status'); + } + /** * Get Build models by ProjectId for this Project. * @@ -401,26 +516,4 @@ class ProjectBase extends Model { return Factory::getStore('Build', 'PHPCI')->getByProjectId($this->getId()); } - - - - - public static function getByPrimaryKey($value, $useConnection = 'read') - { - return Factory::getStore('Project', 'PHPCI')->getByPrimaryKey($value, $useConnection); - } - - - public static function getById($value, $useConnection = 'read') - { - return Factory::getStore('Project', 'PHPCI')->getById($value, $useConnection); - } - - public static function getByTitle($value, $limit = null, $useConnection = 'read') - { - return Factory::getStore('Project', 'PHPCI')->getByTitle($value, $limit, $useConnection); - } - - - } diff --git a/PHPCI/Model/Base/UserBase.php b/PHPCI/Model/Base/UserBase.php index f0b0e393..514a0db9 100644 --- a/PHPCI/Model/Base/UserBase.php +++ b/PHPCI/Model/Base/UserBase.php @@ -273,26 +273,4 @@ class UserBase extends Model $this->_setModified('name'); } - - - - - public static function getByPrimaryKey($value, $useConnection = 'read') - { - return Factory::getStore('User', 'PHPCI')->getByPrimaryKey($value, $useConnection); - } - - - public static function getById($value, $useConnection = 'read') - { - return Factory::getStore('User', 'PHPCI')->getById($value, $useConnection); - } - - public static function getByEmail($value, $useConnection = 'read') - { - return Factory::getStore('User', 'PHPCI')->getByEmail($value, $useConnection); - } - - - } diff --git a/PHPCI/Model/Build.php b/PHPCI/Model/Build.php index ff0a0b23..174126fa 100644 --- a/PHPCI/Model/Build.php +++ b/PHPCI/Model/Build.php @@ -11,6 +11,8 @@ namespace PHPCI\Model; use b8\Store\Factory; use PHPCI\Model\Base\BuildBase; +use PHPCI\Builder; +use Symfony\Component\Yaml\Parser as YamlParser; /** * Build Model @@ -77,4 +79,89 @@ class Build extends BuildBase { return ($this->getStatus() === self::STATUS_SUCCESS); } + + /** + * @param Builder $builder + * @param string $buildPath + * + * @return bool + */ + protected function handleConfig(Builder $builder, $buildPath) + { + $build_config = null; + + // Try phpci.yml first: + if (is_file($buildPath . '/phpci.yml')) { + $build_config = file_get_contents($buildPath . '/phpci.yml'); + } + + // Try getting the project build config from the database: + if (empty($build_config)) { + $build_config = $this->getProject()->getBuildConfig(); + } + + // Fall back to zero config plugins: + if (empty($build_config)) { + $build_config = $this->getZeroConfigPlugins($builder); + } + + if (is_string($build_config)) { + $yamlParser = new YamlParser(); + $build_config = $yamlParser->parse($build_config); + } + + $builder->setConfigArray($build_config); + return true; + } + + protected function getZeroConfigPlugins(Builder $builder) + { + $pluginDir = PHPCI_DIR . 'PHPCI/Plugin/'; + $dir = new \DirectoryIterator($pluginDir); + + $config = array( + 'build_settings' => array( + 'ignore' => array( + 'vendor', + ) + ) + ); + + foreach ($dir as $item) { + if ($item->isDot()) { + continue; + } + + if (!$item->isFile()) { + continue; + } + + if ($item->getExtension() != 'php') { + continue; + } + + $className = '\PHPCI\Plugin\\'.$item->getBasename('.php'); + + $reflectedPlugin = new \ReflectionClass($className); + + if (!$reflectedPlugin->implementsInterface('\PHPCI\ZeroConfigPlugin')) { + continue; + } + + foreach (array('setup', 'test', 'complete', 'success', 'failure') as $stage) { + if ($className::canExecute($stage, $builder, $this)) { + $config[$stage][$className] = array( + 'zero_config' => true + ); + } + } + } + + return $config; + } + + public function getFileLinkTemplate() + { + return null; + } } diff --git a/PHPCI/Model/Build/GithubBuild.php b/PHPCI/Model/Build/GithubBuild.php index 98d7a8eb..f358355a 100644 --- a/PHPCI/Model/Build/GithubBuild.php +++ b/PHPCI/Model/Build/GithubBuild.php @@ -93,4 +93,26 @@ class GithubBuild extends RemoteGitBuild return 'https://github.com/' . $this->getProject()->getReference() . '.git'; } } + + public function getCommitMessage() + { + $rtn = $this->data['commit_message']; + + $reference = $this->getProject()->getReference(); + $commitLink = '#$1'; + $rtn = preg_replace('/\#([0-9]+)/', $commitLink, $rtn); + $rtn = preg_replace('/\@([a-zA-Z0-9_]+)/', '@$1', $rtn); + + return $rtn; + } + + public function getFileLinkTemplate() + { + $link = 'https://github.com/' . $this->getProject()->getReference() . '/'; + $link .= 'blob/' . $this->getBranch() . '/'; + $link .= '{FILE}'; + $link .= '#L{LINE}'; + + return $link; + } } diff --git a/PHPCI/Model/Build/GitlabBuild.php b/PHPCI/Model/Build/GitlabBuild.php index 98be17c8..1ce777ae 100644 --- a/PHPCI/Model/Build/GitlabBuild.php +++ b/PHPCI/Model/Build/GitlabBuild.php @@ -38,6 +38,19 @@ class GitlabBuild extends RemoteGitBuild return 'http://' . $domain . '/' . $this->getProject()->getReference() . '/tree/' . $this->getBranch(); } + /** + * Get link to specific file (and line) in a the repo's branch + */ + public function getFileLinkTemplate() + { + return sprintf( + 'http://%s/%s/blob/%s/{FILE}#L{LINE}', + $this->getProject()->getAccessInformation("domain"), + $this->getProject()->getReference(), + $this->getBranch() + ); + } + /** * Get the URL to be used to clone this remote repository. */ diff --git a/PHPCI/Model/Build/LocalBuild.php b/PHPCI/Model/Build/LocalBuild.php index 8e6984c6..9f2be613 100644 --- a/PHPCI/Model/Build/LocalBuild.php +++ b/PHPCI/Model/Build/LocalBuild.php @@ -11,7 +11,6 @@ namespace PHPCI\Model\Build; use PHPCI\Model\Build; use PHPCI\Builder; -use Symfony\Component\Yaml\Parser as YamlParser; /** * Local Build Model @@ -45,7 +44,11 @@ class LocalBuild extends Build if (isset($buildSettings['prefer_symlink']) && $buildSettings['prefer_symlink'] === true) { return $this->handleSymlink($builder, $reference, $buildPath); } else { - $builder->executeCommand('cp -Rf "%s" "%s/"', $reference, $buildPath); + $cmd = 'cp -Rf "%s" "%s/"'; + if (IS_WIN) { + $cmd = 'xcopy /E /Y "%s" "%s/*"'; + } + $builder->executeCommand($cmd, $reference, $buildPath); } return true; @@ -57,7 +60,8 @@ class LocalBuild extends Build // If it is indeed a bare repository, then extract it into our build path: if ($gitConfig['core']['bare']) { - $builder->executeCommand('mkdir %2$s; git --git-dir="%1$s" archive %3$s | tar -x -C "%2$s"', $reference, $buildPath, $this->getBranch()); + $cmd = 'mkdir %2$s; git --git-dir="%1$s" archive %3$s | tar -x -C "%2$s"'; + $builder->executeCommand($cmd, $reference, $buildPath, $this->getBranch()); return true; } @@ -79,18 +83,4 @@ class LocalBuild extends Build return true; } - - protected function handleConfig(Builder $builder, $reference) - { - /** @todo Add support for database-based yml definition */ - if (!is_file($reference . '/phpci.yml')) { - $builder->logFailure('Project does not contain a phpci.yml file.'); - return false; - } - - $yamlParser = new YamlParser(); - $yamlFile = file_get_contents($reference . '/phpci.yml'); - $builder->setConfigArray($yamlParser->parse($yamlFile)); - return $builder->getConfig('build_settings'); - } } diff --git a/PHPCI/Model/Build/MercurialBuild.php b/PHPCI/Model/Build/MercurialBuild.php index dbfcf4e2..22ba9fb8 100644 --- a/PHPCI/Model/Build/MercurialBuild.php +++ b/PHPCI/Model/Build/MercurialBuild.php @@ -11,7 +11,6 @@ namespace PHPCI\Model\Build; use PHPCI\Model\Build; use PHPCI\Builder; -use Symfony\Component\Yaml\Parser as YamlParser; /** * Mercurial Build Model @@ -34,19 +33,9 @@ class MercurialBuild extends Build */ public function createWorkingCopy(Builder $builder, $buildPath) { - $yamlParser = new YamlParser(); - $this->cloneByHttp($builder, $buildPath); - if (!is_file($buildPath . 'phpci.yml')) { - $builder->logFailure('Project does not contain a phpci.yml file.'); - return false; - } - - $yamlFile = file_get_contents($buildPath . 'phpci.yml'); - $builder->setConfigArray($yamlParser->parse($yamlFile)); - - return true; + return $this->handleConfig($builder, $buildPath); } /** diff --git a/PHPCI/Model/Build/RemoteGitBuild.php b/PHPCI/Model/Build/RemoteGitBuild.php index 35aa8c90..9c7c6a26 100644 --- a/PHPCI/Model/Build/RemoteGitBuild.php +++ b/PHPCI/Model/Build/RemoteGitBuild.php @@ -11,7 +11,6 @@ namespace PHPCI\Model\Build; use PHPCI\Model\Build; use PHPCI\Builder; -use Symfony\Component\Yaml\Parser as YamlParser; /** * Remote Git Build Model @@ -34,7 +33,6 @@ class RemoteGitBuild extends Build */ public function createWorkingCopy(Builder $builder, $buildPath) { - $yamlParser = new YamlParser(); $key = trim($this->getProject()->getGitKey()); if (!empty($key)) { @@ -48,15 +46,7 @@ class RemoteGitBuild extends Build return false; } - if (!is_file($buildPath . 'phpci.yml')) { - $builder->logFailure('Project does not contain a phpci.yml file.'); - return false; - } - - $yamlFile = file_get_contents($buildPath . 'phpci.yml'); - $builder->setConfigArray($yamlParser->parse($yamlFile)); - - return true; + return $this->handleConfig($builder, $buildPath); } /** @@ -64,8 +54,25 @@ class RemoteGitBuild extends Build */ protected function cloneByHttp(Builder $builder, $cloneTo) { - $success = $builder->executeCommand('git clone -b %s %s "%s"', $this->getBranch(), $this->getCloneUrl(), $cloneTo); - $builder->executeCommand('cd "%s" && git checkout %s', $cloneTo, $this->getCommitId()); + $cmd = 'git clone '; + + $depth = $builder->getConfig('clone_depth'); + + if (!is_null($depth)) { + $cmd .= ' --depth ' . intval($depth) . ' '; + } + + $cmd .= ' -b %s %s "%s"'; + $success = $builder->executeCommand($cmd, $this->getBranch(), $this->getCloneUrl(), $cloneTo); + + if (!empty($commit) && $commit != 'Manual') { + $cmd = 'cd "%s" && git checkout %s'; + if (IS_WIN) { + $cmd = 'cd /d "%s" && git checkout %s'; + } + $builder->executeCommand($cmd, $cloneTo, $this->getCommitId()); + } + return $success; } @@ -74,31 +81,88 @@ class RemoteGitBuild extends Build */ protected function cloneBySsh(Builder $builder, $cloneTo) { - // Copy the project's keyfile to disk: - $keyPath = realpath($cloneTo); + $keyFile = $this->writeSshKey($cloneTo); - if ($keyPath === false) { - $keyPath = dirname($cloneTo); + if (!IS_WIN) { + $gitSshWrapper = $this->writeSshWrapper($cloneTo, $keyFile); } - $keyFile = $keyPath . '.key'; + // Do the git clone: + $cmd = 'git clone '; - file_put_contents($keyFile, $this->getProject()->getGitKey()); - chmod($keyFile, 0600); + $depth = $builder->getConfig('clone_depth'); - // Use the key file to do an SSH clone: - $cmd = 'eval `ssh-agent -s` && ssh-add "%s" && git clone -b %s %s "%s" && ssh-agent -k'; - $success = $builder->executeCommand($cmd, $keyFile, $this->getBranch(), $this->getCloneUrl(), $cloneTo); + if (!is_null($depth)) { + $cmd .= ' --depth ' . intval($depth) . ' '; + } + $cmd .= ' -b %s %s "%s"'; + + if (!IS_WIN) { + $cmd = 'export GIT_SSH="'.$gitSshWrapper.'" && ' . $cmd; + } + + $success = $builder->executeCommand($cmd, $this->getBranch(), $this->getCloneUrl(), $cloneTo); + + // Checkout a specific commit if we need to: $commit = $this->getCommitId(); if (!empty($commit) && $commit != 'Manual') { - $builder->executeCommand('cd "%s" && git checkout %s', $cloneTo, $this->getCommitId()); + $cmd = 'cd "%s" && git checkout %s'; + if (IS_WIN) { + $cmd = 'cd /d "%s" && git checkout %s'; + } + $builder->executeCommand($cmd, $cloneTo, $this->getCommitId()); } - // Remove the key file: + // Remove the key file and git wrapper: unlink($keyFile); + unlink($gitSshWrapper); return $success; } + + /** + * Create an SSH key file on disk for this build. + * @param $cloneTo + * @return string + */ + protected function writeSshKey($cloneTo) + { + $keyPath = dirname($cloneTo . '/temp'); + $keyFile = $keyPath . '.key'; + + // Write the contents of this project's git key to the file: + file_put_contents($keyFile, $this->getProject()->getGitKey()); + chmod($keyFile, 0600); + + // Return the filename: + return $keyFile; + } + + /** + * Create an SSH wrapper script for Git to use, to disable host key checking, etc. + * @param $cloneTo + * @param $keyFile + * @return string + */ + protected function writeSshWrapper($cloneTo, $keyFile) + { + $path = dirname($cloneTo . '/temp'); + $wrapperFile = $path . '.sh'; + + $sshFlags = '-o CheckHostIP=no -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o PasswordAuthentication=no'; + + // Write out the wrapper script for this build: + $script = <<phpci = $phpci; + $this->build = $build; $this->features = ''; if (isset($options['executable'])) { diff --git a/PHPCI/Plugin/CleanBuild.php b/PHPCI/Plugin/CleanBuild.php index a7db4fc9..73a7305f 100644 --- a/PHPCI/Plugin/CleanBuild.php +++ b/PHPCI/Plugin/CleanBuild.php @@ -23,10 +23,12 @@ class CleanBuild implements \PHPCI\Plugin { protected $remove; protected $phpci; + protected $build; public function __construct(Builder $phpci, Build $build, array $options = array()) { $this->phpci = $phpci; + $this->build = $build; $this->remove = isset($options['remove']) && is_array($options['remove']) ? $options['remove'] : array(); } diff --git a/PHPCI/Plugin/Codeception.php b/PHPCI/Plugin/Codeception.php index bd168145..5b811896 100644 --- a/PHPCI/Plugin/Codeception.php +++ b/PHPCI/Plugin/Codeception.php @@ -20,9 +20,18 @@ use PHPCI\Model\Build; */ class Codeception implements \PHPCI\Plugin { - protected $args; + /** + * @var string + */ + protected $args = ''; + + /** + * @var Builder + */ protected $phpci; + protected $build; + /** * @var string|string[] $xmlConfigFile The path (or array of paths) of an xml config for PHPUnit */ @@ -30,14 +39,14 @@ class Codeception implements \PHPCI\Plugin public function __construct(Builder $phpci, Build $build, array $options = array()) { - $this->phpci = $phpci; + $this->phpci = $phpci; + $this->build = $build; if (isset($options['config'])) { $this->xmlConfigFile = $options['config']; } - if (isset($options['args'])) { - $this->args = $options['args']; + $this->args = (string) $options['args']; } } @@ -69,8 +78,13 @@ class Codeception implements \PHPCI\Plugin return false; } - $cmd = 'cd "%s" && ' . $codecept . ' run -c "%s"'; - $success = $this->phpci->executeCommand($cmd, $this->phpci->buildPath, $this->phpci->buildPath . $configPath); + $cmd = 'cd "%s" && ' . $codecept . ' run -c "%s" '. $this->args; + if (IS_WIN) { + $cmd = 'cd /d "%s" && ' . $codecept . ' run -c "%s" '. $this->args; + } + + $configPath = $this->phpci->buildPath . $configPath; + $success = $this->phpci->executeCommand($cmd, $this->phpci->buildPath, $configPath); return $success; } diff --git a/PHPCI/Plugin/Composer.php b/PHPCI/Plugin/Composer.php index 0b89afe0..3a13b672 100644 --- a/PHPCI/Plugin/Composer.php +++ b/PHPCI/Plugin/Composer.php @@ -9,6 +9,7 @@ namespace PHPCI\Plugin; +use PHPCI; use PHPCI\Builder; use PHPCI\Model\Build; @@ -18,19 +19,32 @@ use PHPCI\Model\Build; * @package PHPCI * @subpackage Plugins */ -class Composer implements \PHPCI\Plugin +class Composer implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin { protected $directory; protected $action; protected $preferDist; protected $phpci; + protected $build; + + public static function canExecute($stage, Builder $builder, Build $build) + { + $path = $builder->buildPath . '/composer.json'; + + if (file_exists($path) && $stage == 'setup') { + return true; + } + + return false; + } public function __construct(Builder $phpci, Build $build, array $options = array()) { $path = $phpci->buildPath; $this->phpci = $phpci; + $this->build = $build; $this->directory = isset($options['directory']) ? $path . '/' . $options['directory'] : $path; - $this->action = isset($options['action']) ? $options['action'] : 'update'; + $this->action = isset($options['action']) ? $options['action'] : 'install'; $this->preferDist = isset($options['prefer_dist']) ? $options['prefer_dist'] : true; } @@ -46,7 +60,7 @@ class Composer implements \PHPCI\Plugin return false; } $cmd = ''; - if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + if (IS_WIN) { $cmd = 'php '; } $cmd .= $composerLocation . ' --no-ansi --no-interaction '; diff --git a/PHPCI/Plugin/CopyBuild.php b/PHPCI/Plugin/CopyBuild.php index 494a0d3f..021d5e80 100644 --- a/PHPCI/Plugin/CopyBuild.php +++ b/PHPCI/Plugin/CopyBuild.php @@ -21,18 +21,21 @@ use PHPCI\Model\Build; class CopyBuild implements \PHPCI\Plugin { protected $directory; + protected $ignore; protected $phpci; + protected $build; public function __construct(Builder $phpci, Build $build, array $options = array()) { $path = $phpci->buildPath; $this->phpci = $phpci; + $this->build = $build; $this->directory = isset($options['directory']) ? $options['directory'] : $path; $this->ignore = isset($options['respect_ignore']) ? (bool)$options['respect_ignore'] : false; } /** - * Executes Composer and runs a specified command (e.g. install / update) + * Copies files from the root of the build directory into the target folder */ public function execute() { @@ -42,7 +45,7 @@ class CopyBuild implements \PHPCI\Plugin return false; } - $cmd = 'mkdir -p "%s" && ls -1a "%s"* | xargs -r -t "%s/"'; + $cmd = 'mkdir -p "%s" && cp -R "%s" "%s"'; $success = $this->phpci->executeCommand($cmd, $this->directory, $build, $this->directory); if ($this->ignore) { diff --git a/PHPCI/Plugin/Email.php b/PHPCI/Plugin/Email.php index f6317197..0b23a1d7 100644 --- a/PHPCI/Plugin/Email.php +++ b/PHPCI/Plugin/Email.php @@ -46,7 +46,6 @@ class Email implements \PHPCI\Plugin Build $build, \Swift_Mailer $mailer, array $options = array() - ) { $this->phpci = $phpci; $this->build = $build; diff --git a/PHPCI/Plugin/Env.php b/PHPCI/Plugin/Env.php index 09ffbb5a..a55cf47d 100644 --- a/PHPCI/Plugin/Env.php +++ b/PHPCI/Plugin/Env.php @@ -21,11 +21,13 @@ use PHPCI\Model\Build; class Env implements \PHPCI\Plugin { protected $phpci; + protected $build; protected $env_vars; public function __construct(Builder $phpci, Build $build, array $options = array()) { - $this->phpci = $phpci; + $this->phpci = $phpci; + $this->build = $build; $this->env_vars = $options; } diff --git a/PHPCI/Plugin/Git.php b/PHPCI/Plugin/Git.php new file mode 100644 index 00000000..00c1f78f --- /dev/null +++ b/PHPCI/Plugin/Git.php @@ -0,0 +1,136 @@ + + * @package PHPCI + * @subpackage Plugins + */ +class Git implements \PHPCI\Plugin +{ + protected $phpci; + protected $build; + protected $actions = array(); + + public function __construct(Builder $phpci, Build $build, array $options = array()) + { + $this->phpci = $phpci; + $this->build = $build; + $this->actions = $options; + } + + public function execute() + { + $buildPath = $this->phpci->buildPath; + + // Check if there are any actions to be run for the branch we're running on: + if (!array_key_exists($this->build->getBranch(), $this->actions)) { + return true; + } + + // If there are, run them: + $curdir = getcwd(); + chdir($buildPath); + + $success = true; + foreach ($this->actions[$this->build->getBranch()] as $action => $options) { + if (!$this->runAction($action, $options)) { + $success = false; + break; + } + } + + chdir($curdir); + + return $success; + } + + protected function runAction($action, array $options = array()) + { + switch ($action) { + case 'merge': + return $this->runMergeAction($options); + + case 'tag': + return $this->runTagAction($options); + + case 'pull': + return $this->runPullAction($options); + + case 'push': + return $this->runPushAction($options); + } + + + return false; + } + + protected function runMergeAction($options) + { + if (array_key_exists('branch', $options)) { + $cmd = 'git checkout %s && git merge ' . $this->build->getBranch(); + return $this->phpci->executeCommand($cmd, $this->directory, $options['branch']); + } + } + + protected function runTagAction($options) + { + $tagName = date('Ymd-His'); + $message = 'Tag created by PHPCI: ' . date('Y-m-d H:i:s'); + + if (array_key_exists('name', $options)) { + $tagName = $this->phpci->interpolate($options['name']); + } + + if (array_key_exists('message', $options)) { + $message = $this->phpci->interpolate($options['message']); + } + + $cmd = 'git tag %s -m "%s"'; + return $this->phpci->executeCommand($cmd, $tagName, $message); + } + + protected function runPullAction($options) + { + $branch = $this->build->getBranch(); + $remote = 'origin'; + + if (array_key_exists('branch', $options)) { + $branch = $this->phpci->interpolate($options['branch']); + } + + if (array_key_exists('remote', $options)) { + $remote = $this->phpci->interpolate($options['remote']); + } + + return $this->phpci->executeCommand('git pull %s %s', $remote, $branch); + } + + protected function runPushAction($options) + { + $branch = $this->build->getBranch(); + $remote = 'origin'; + + if (array_key_exists('branch', $options)) { + $branch = $this->phpci->interpolate($options['branch']); + } + + if (array_key_exists('remote', $options)) { + $remote = $this->phpci->interpolate($options['remote']); + } + + return $this->phpci->executeCommand('git push %s %s', $remote, $branch); + } +} diff --git a/PHPCI/Plugin/Grunt.php b/PHPCI/Plugin/Grunt.php index 477a4849..0c845184 100644 --- a/PHPCI/Plugin/Grunt.php +++ b/PHPCI/Plugin/Grunt.php @@ -24,12 +24,14 @@ class Grunt implements \PHPCI\Plugin protected $task; protected $preferDist; protected $phpci; + protected $build; protected $grunt; protected $gruntfile; public function __construct(Builder $phpci, Build $build, array $options = array()) { $path = $phpci->buildPath; + $this->build = $build; $this->phpci = $phpci; $this->directory = $path; $this->task = null; @@ -60,12 +62,19 @@ class Grunt implements \PHPCI\Plugin public function execute() { // if npm does not work, we cannot use grunt, so we return false - if (!$this->phpci->executeCommand('cd %s && npm install', $this->directory)) { + $cmd = 'cd %s && npm install'; + if (IS_WIN) { + $cmd = 'cd /d %s && npm install'; + } + if (!$this->phpci->executeCommand($cmd, $this->directory)) { return false; } // build the grunt command $cmd = 'cd %s && ' . $this->grunt; + if (IS_WIN) { + $cmd = 'cd /d %s && ' . $this->grunt; + } $cmd .= ' --no-color'; $cmd .= ' --gruntfile %s'; $cmd .= ' %s'; // the task that will be executed diff --git a/PHPCI/Plugin/Irc.php b/PHPCI/Plugin/Irc.php index 6b294d66..0defc037 100644 --- a/PHPCI/Plugin/Irc.php +++ b/PHPCI/Plugin/Irc.php @@ -13,16 +13,18 @@ use PHPCI\Model\Build; */ class Irc implements \PHPCI\Plugin { - private $phpci; - private $message; - private $server; - private $port; - private $room; - private $nick; + protected $phpci; + protected $build; + protected $message; + protected $server; + protected $port; + protected $room; + protected $nick; public function __construct(Builder $phpci, Build $build, array $options = array()) { $this->phpci = $phpci; + $this->build = $build; $this->message = $options['message']; $buildSettings = $phpci->getConfig('build_settings'); diff --git a/PHPCI/Plugin/Lint.php b/PHPCI/Plugin/Lint.php index ebab146e..3ca785ec 100644 --- a/PHPCI/Plugin/Lint.php +++ b/PHPCI/Plugin/Lint.php @@ -9,6 +9,7 @@ namespace PHPCI\Plugin; +use PHPCI; use PHPCI\Builder; use PHPCI\Model\Build; @@ -18,16 +19,18 @@ use PHPCI\Model\Build; * @package PHPCI * @subpackage Plugins */ -class Lint implements \PHPCI\Plugin +class Lint implements PHPCI\Plugin { protected $directories; protected $recursive = true; protected $ignore; protected $phpci; + protected $build; public function __construct(Builder $phpci, Build $build, array $options = array()) { $this->phpci = $phpci; + $this->build = $build; $this->directories = array(''); $this->ignore = $phpci->ignore; diff --git a/PHPCI/Plugin/Mysql.php b/PHPCI/Plugin/Mysql.php index e2fda714..c66f77cb 100755 --- a/PHPCI/Plugin/Mysql.php +++ b/PHPCI/Plugin/Mysql.php @@ -27,6 +27,7 @@ class Mysql implements \PHPCI\Plugin * @var \PHPCI\Builder */ protected $phpci; + protected $build; protected $queries = array(); protected $host; @@ -42,6 +43,8 @@ class Mysql implements \PHPCI\Plugin public function __construct(Builder $phpci, Build $build, array $options = array()) { $this->phpci = $phpci; + $this->build = $build; + $this->queries = $options; $config = \b8\Database::getConnection('write')->getDetails(); diff --git a/PHPCI/Plugin/Pgsql.php b/PHPCI/Plugin/Pgsql.php index 45155ecf..a05068d2 100644 --- a/PHPCI/Plugin/Pgsql.php +++ b/PHPCI/Plugin/Pgsql.php @@ -22,6 +22,7 @@ use PHPCI\Model\Build; class Pgsql implements \PHPCI\Plugin { protected $phpci; + protected $build; protected $queries = array(); protected $host; @@ -30,13 +31,14 @@ class Pgsql implements \PHPCI\Plugin public function __construct(Builder $phpci, Build $build, array $options = array()) { - $this->phpci = $phpci; - $this->queries = $options; + $this->phpci = $phpci; + $this->build = $build; + $this->queries = $options; $buildSettings = $phpci->getConfig('build_settings'); if (isset($buildSettings['pgsql'])) { - $sql = $buildSettings['pgsql']; + $sql = $buildSettings['pgsql']; $this->host = $sql['host']; $this->user = $sql['user']; $this->pass = $sql['pass']; diff --git a/PHPCI/Plugin/Phing.php b/PHPCI/Plugin/Phing.php index e90d4b4a..270b3aa9 100644 --- a/PHPCI/Plugin/Phing.php +++ b/PHPCI/Plugin/Phing.php @@ -29,10 +29,12 @@ class Phing implements \PHPCI\Plugin private $propertyFile; protected $phpci; + protected $build; public function __construct(Builder $phpci, Build $build, array $options = array()) { $this->setPhpci($phpci); + $this->build = $build; /* * Set working directory diff --git a/PHPCI/Plugin/PhpCodeSniffer.php b/PHPCI/Plugin/PhpCodeSniffer.php index 2f0d7592..7c7f81e0 100755 --- a/PHPCI/Plugin/PhpCodeSniffer.php +++ b/PHPCI/Plugin/PhpCodeSniffer.php @@ -9,6 +9,7 @@ namespace PHPCI\Plugin; +use PHPCI; use PHPCI\Builder; use PHPCI\Model\Build; @@ -18,7 +19,7 @@ use PHPCI\Model\Build; * @package PHPCI * @subpackage Plugins */ -class PhpCodeSniffer implements \PHPCI\Plugin +class PhpCodeSniffer implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin { /** * @var \PHPCI\Builder @@ -50,6 +51,16 @@ class PhpCodeSniffer implements \PHPCI\Plugin */ protected $encoding; + /** + * @var int + */ + protected $allowed_errors; + + /** + * @var int + */ + protected $allowed_warnings; + /** * @var string, based on the assumption the root may not hold the code to be * tested, exteds the base path @@ -61,21 +72,38 @@ class PhpCodeSniffer implements \PHPCI\Plugin */ protected $ignore; + public static function canExecute($stage, Builder $builder, Build $build) + { + if ($stage == 'test') { + return true; + } + + return false; + } + /** * @param \PHPCI\Builder $phpci + * @param \PHPCI\Model\Build $build * @param array $options */ public function __construct(Builder $phpci, Build $build, array $options = array()) { - $this->phpci = $phpci; - $this->build = $build; - $this->suffixes = array('php'); - $this->directory = $phpci->buildPath; - $this->standard = 'PSR2'; - $this->tab_width = ''; - $this->encoding = ''; - $this->path = ''; - $this->ignore = $this->phpci->ignore; + $this->phpci = $phpci; + $this->build = $build; + $this->suffixes = array('php'); + $this->directory = $phpci->buildPath; + $this->standard = 'PSR2'; + $this->tab_width = ''; + $this->encoding = ''; + $this->path = ''; + $this->ignore = $this->phpci->ignore; + $this->allowed_warnings = 0; + $this->allowed_errors = 0; + + if (isset($options['zero_config']) && $options['zero_config']) { + $this->allowed_warnings = -1; + $this->allowed_errors = -1; + } if (isset($options['suffixes'])) { $this->suffixes = (array)$options['suffixes']; @@ -104,6 +132,14 @@ class PhpCodeSniffer implements \PHPCI\Plugin if (isset($options['ignore'])) { $this->ignore = $options['ignore']; } + + if (isset($options['allowed_warnings'])) { + $this->allowed_warnings = (int)$options['allowed_warnings']; + } + + if (isset($options['allowed_errors'])) { + $this->allowed_errors = (int)$options['allowed_errors']; + } } /** @@ -120,8 +156,10 @@ class PhpCodeSniffer implements \PHPCI\Plugin return false; } - $cmd = $phpcs . ' --report=emacs %s %s %s %s %s "%s"'; - $success = $this->phpci->executeCommand( + $this->phpci->logExecOutput(false); + + $cmd = $phpcs . ' --report=json %s %s %s %s %s "%s"'; + $this->phpci->executeCommand( $cmd, $standard, $suffixes, @@ -132,15 +170,21 @@ class PhpCodeSniffer implements \PHPCI\Plugin ); $output = $this->phpci->getLastOutput(); + list($errors, $warnings, $data) = $this->processReport(json_decode(trim($output), true)); - $matches = array(); - if (preg_match_all('/\: warning \-/', $output, $matches)) { - $this->build->storeMeta('phpcs-warnings', count($matches[0])); + $this->phpci->logExecOutput(true); + + $success = true; + $this->build->storeMeta('phpcs-warnings', $warnings); + $this->build->storeMeta('phpcs-errors', $errors); + $this->build->storeMeta('phpcs-data', $data); + + if ($this->allowed_warnings != -1 && $warnings > $this->allowed_warnings) { + $success = false; } - $matches = array(); - if (preg_match_all('/\: error \-/', $output, $matches)) { - $this->build->storeMeta('phpcs-errors', count($matches[0])); + if ($this->allowed_errors != -1 && $errors > $this->allowed_errors) { + $success = false; } return $success; @@ -166,4 +210,31 @@ class PhpCodeSniffer implements \PHPCI\Plugin return array($ignore, $standard, $suffixes); } + + protected function processReport($data) + { + if (!is_array($data)) { + throw new \Exception('Could not process PHPCS report JSON.'); + } + + $errors = $data['totals']['errors']; + $warnings = $data['totals']['warnings']; + + $rtn = array(); + + foreach ($data['files'] as $fileName => $file) { + $fileName = str_replace($this->phpci->buildPath, '', $fileName); + + foreach ($file['messages'] as $message) { + $rtn[] = array( + 'file' => $fileName, + 'line' => $message['line'], + 'type' => $message['type'], + 'message' => $message['message'], + ); + } + } + + return array($errors, $warnings, $rtn); + } } diff --git a/PHPCI/Plugin/PhpCpd.php b/PHPCI/Plugin/PhpCpd.php index bbefd7e6..2e0b908e 100644 --- a/PHPCI/Plugin/PhpCpd.php +++ b/PHPCI/Plugin/PhpCpd.php @@ -23,6 +23,7 @@ class PhpCpd implements \PHPCI\Plugin protected $directory; protected $args; protected $phpci; + protected $build; /** * @var string, based on the assumption the root may not hold the code to be @@ -38,6 +39,8 @@ class PhpCpd implements \PHPCI\Plugin public function __construct(Builder $phpci, Build $build, array $options = array()) { $this->phpci = $phpci; + $this->build = $build; + $this->path = $phpci->buildPath; $this->standard = 'PSR1'; $this->ignore = $phpci->ignore; diff --git a/PHPCI/Plugin/PhpCsFixer.php b/PHPCI/Plugin/PhpCsFixer.php index ca07e9ef..7b1cff2a 100644 --- a/PHPCI/Plugin/PhpCsFixer.php +++ b/PHPCI/Plugin/PhpCsFixer.php @@ -20,8 +20,16 @@ use PHPCI\Model\Build; */ class PhpCsFixer implements \PHPCI\Plugin { + /** + * @var \PHPCI\Builder + */ protected $phpci; + /** + * @var \PHPCI\Model\Build + */ + protected $build; + protected $workingDir = ''; protected $level = ' --level=all'; protected $verbose = ''; @@ -31,6 +39,8 @@ class PhpCsFixer implements \PHPCI\Plugin public function __construct(Builder $phpci, Build $build, array $options = array()) { $this->phpci = $phpci; + $this->build = $build; + $this->workingdir = $this->phpci->buildPath; $this->buildArgs($options); } diff --git a/PHPCI/Plugin/PhpDocblockChecker.php b/PHPCI/Plugin/PhpDocblockChecker.php new file mode 100755 index 00000000..b41b8434 --- /dev/null +++ b/PHPCI/Plugin/PhpDocblockChecker.php @@ -0,0 +1,138 @@ + +* @package PHPCI +* @subpackage Plugins +*/ +class PhpDocblockChecker implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin +{ + /** + * @var \PHPCI\Builder + */ + protected $phpci; + + /** + * @var \PHPCI\Model\Build + */ + protected $build; + + /** + * @var string Based on the assumption the root may not hold the code to be + * tested, extends the build path. + */ + protected $path; + + /** + * @var array - paths to ignore + */ + protected $ignore; + + protected $skipClasses = false; + protected $skipMethods = false; + + public static function canExecute($stage, Builder $builder, Build $build) + { + if ($stage == 'test') { + return true; + } + + return false; + } + + + public function __construct(Builder $phpci, Build $build, array $options = array()) + { + $this->phpci = $phpci; + $this->build = $build; + $this->ignore = $phpci->ignore; + $this->path = ''; + $this->allowed_warnings = 0; + + if (isset($options['zero_config']) && $options['zero_config']) { + $this->allowed_warnings = -1; + } + + if (array_key_exists('skip_classes', $options)) { + $this->skipClasses = true; + } + + if (array_key_exists('skip_methods', $options)) { + $this->skipMethods = true; + } + + if (!empty($options['path'])) { + $this->path = $options['path']; + } + + if (array_key_exists('allowed_warnings', $options)) { + $this->allowed_warnings = (int)$options['allowed_warnings']; + } + } + + /** + * Runs PHP Mess Detector in a specified directory. + */ + public function execute() + { + $ignore = ''; + if (count($this->ignore)) { + $ignore = ' --exclude="' . implode(',', $this->ignore) . '"'; + } + + var_dump($ignore); + + $checker = $this->phpci->findBinary('phpdoccheck'); + + if (!$checker) { + $this->phpci->logFailure('Could not find phpdoccheck.'); + return false; + } + + $path = $this->phpci->buildPath . $this->path; + + $cmd = $checker . ' --json --directory="%s"%s%s%s'; + + // Disable exec output logging, as we don't want the XML report in the log: + $this->phpci->logExecOutput(false); + + // Run checker: + $this->phpci->executeCommand( + $cmd, + $path, + $ignore, + ($this->skipClasses ? ' --skip-classes' : ''), + ($this->skipMethods ? ' --skip-methods' : '') + ); + + // Re-enable exec output logging: + $this->phpci->logExecOutput(true); + + $output = json_decode($this->phpci->getLastOutput()); + $errors = count($output); + $success = true; + + $this->build->storeMeta('phpdoccheck-warnings', $errors); + $this->build->storeMeta('phpdoccheck-data', $output); + + if ($this->allowed_warnings != -1 && $errors > $this->allowed_warnings) { + $success = false; + } + + return $success; + } +} diff --git a/PHPCI/Plugin/PhpLoc.php b/PHPCI/Plugin/PhpLoc.php index 809b6409..a00e6668 100644 --- a/PHPCI/Plugin/PhpLoc.php +++ b/PHPCI/Plugin/PhpLoc.php @@ -9,6 +9,7 @@ namespace PHPCI\Plugin; +use PHPCI; use PHPCI\Builder; use PHPCI\Model\Build; @@ -18,7 +19,7 @@ use PHPCI\Model\Build; * @package PHPCI * @subpackage Plugins */ -class PhpLoc implements \PHPCI\Plugin +class PhpLoc implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin { /** * @var string @@ -29,6 +30,15 @@ class PhpLoc implements \PHPCI\Plugin */ protected $phpci; + public static function canExecute($stage, Builder $builder, Build $build) + { + if ($stage == 'test') { + return true; + } + + return false; + } + public function __construct(Builder $phpci, Build $build, array $options = array()) { $this->phpci = $phpci; @@ -58,7 +68,7 @@ class PhpLoc implements \PHPCI\Plugin return false; } - $success = $this->phpci->executeCommand($phploc . ' %s "%s"', $ignore, $this->phpci->buildPath); + $success = $this->phpci->executeCommand($phploc . ' %s "%s"', $ignore, $this->directory); $output = $this->phpci->getLastOutput(); if (preg_match_all('/\((LOC|CLOC|NCLOC|LLOC)\)\s+([0-9]+)/', $output, $matches)) { diff --git a/PHPCI/Plugin/PhpMessDetector.php b/PHPCI/Plugin/PhpMessDetector.php index 89c22f66..2be7fed4 100755 --- a/PHPCI/Plugin/PhpMessDetector.php +++ b/PHPCI/Plugin/PhpMessDetector.php @@ -9,6 +9,7 @@ namespace PHPCI\Plugin; +use PHPCI; use PHPCI\Builder; use PHPCI\Model\Build; @@ -18,13 +19,18 @@ use PHPCI\Model\Build; * @package PHPCI * @subpackage Plugins */ -class PhpMessDetector implements \PHPCI\Plugin +class PhpMessDetector implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin { /** * @var \PHPCI\Builder */ protected $phpci; + /** + * @var \PHPCI\Model\Build + */ + protected $build; + /** * @var array */ @@ -32,7 +38,8 @@ class PhpMessDetector implements \PHPCI\Plugin /** * @var string, based on the assumption the root may not hold the code to be - * tested, exteds the base path + * tested, exteds the base path only if the provided path is relative. Absolute + * paths are used verbatim */ protected $path; @@ -48,10 +55,16 @@ class PhpMessDetector implements \PHPCI\Plugin */ protected $rules; - /** - * @param \PHPCI\Builder $phpci - * @param array $options - */ + public static function canExecute($stage, Builder $builder, Build $build) + { + if ($stage == 'test') { + return true; + } + + return false; + } + + public function __construct(Builder $phpci, Build $build, array $options = array()) { $this->phpci = $phpci; @@ -60,11 +73,20 @@ class PhpMessDetector implements \PHPCI\Plugin $this->ignore = $phpci->ignore; $this->path = ''; $this->rules = array('codesize', 'unusedcode', 'naming'); + $this->allowed_warnings = 0; + + if (isset($options['zero_config']) && $options['zero_config']) { + $this->allowed_warnings = -1; + } if (!empty($options['path'])) { $this->path = $options['path']; } + if (array_key_exists('allowed_warnings', $options)) { + $this->allowed_warnings = (int)$options['allowed_warnings']; + } + foreach (array('rules', 'ignore', 'suffixes') as $key) { $this->overrideSetting($options, $key); } @@ -85,6 +107,11 @@ class PhpMessDetector implements \PHPCI\Plugin $suffixes = ' --suffixes ' . implode(',', $this->suffixes); } + if (!empty($this->rules) && !is_array($this->rules)) { + $this->phpci->logFailure('The "rules" option must be an array.'); + return false; + } + foreach ($this->rules as &$rule) { if (strpos($rule, '/') !== false) { $rule = $this->phpci->buildPath . $rule; @@ -97,18 +124,38 @@ class PhpMessDetector implements \PHPCI\Plugin $this->phpci->logFailure('Could not find phpmd.'); return false; } + + $path = $this->phpci->buildPath . $this->path; + if (!empty($this->path) && $this->path{0} == '/') { + $path = $this->path; + } - $cmd = $phpmd . ' "%s" text %s %s %s'; - $success = $this->phpci->executeCommand( + $cmd = $phpmd . ' "%s" xml %s %s %s'; + + // Disable exec output logging, as we don't want the XML report in the log: + $this->phpci->logExecOutput(false); + + // Run PHPMD: + $this->phpci->executeCommand( $cmd, - $this->phpci->buildPath . $this->path, + $path, implode(',', $this->rules), $ignore, $suffixes ); - $errors = count(array_filter(explode(PHP_EOL, trim($this->phpci->getLastOutput())))); + // Re-enable exec output logging: + $this->phpci->logExecOutput(true); + + $success = true; + + list($errors, $data) = $this->processReport(trim($this->phpci->getLastOutput())); $this->build->storeMeta('phpmd-warnings', $errors); + $this->build->storeMeta('phpmd-data', $data); + + if ($this->allowed_warnings != -1 && $errors > $this->allowed_warnings) { + $success = false; + } return $success; } @@ -119,4 +166,38 @@ class PhpMessDetector implements \PHPCI\Plugin $this->{$key} = $options[$key]; } } + + protected function processReport($xml) + { + $xml = simplexml_load_string($xml); + + if ($xml === false) { + throw new \Exception('Could not process PHPMD report XML.'); + } + + $warnings = 0; + $data = array(); + + foreach ($xml->file as $file) { + $fileName = (string)$file['name']; + $fileName = str_replace($this->phpci->buildPath, '', $fileName); + + foreach ($file->violation as $violation) { + $warnings++; + $warning = array( + 'file' => $fileName, + 'line_start' => (int)$violation['beginline'], + 'line_end' => (int)$violation['endline'], + 'rule' => (string)$violation['rule'], + 'ruleset' => (string)$violation['ruleset'], + 'priority' => (int)$violation['priority'], + 'message' => (string)$violation, + ); + + $data[] = $warning; + } + } + + return array($warnings, $data); + } } diff --git a/PHPCI/Plugin/PhpSpec.php b/PHPCI/Plugin/PhpSpec.php index afd36699..c8a36253 100644 --- a/PHPCI/Plugin/PhpSpec.php +++ b/PHPCI/Plugin/PhpSpec.php @@ -9,6 +9,7 @@ namespace PHPCI\Plugin; +use PHPCI; use PHPCI\Builder; use PHPCI\Model\Build; @@ -18,18 +19,28 @@ use PHPCI\Model\Build; * @package PHPCI * @subpackage Plugins */ -class PhpSpec implements \PHPCI\Plugin +class PhpSpec implements PHPCI\Plugin { + /** + * @var \PHPCI\Builder + */ protected $phpci; - protected $bootstrap; + + /** + * @var \PHPCI\Model\Build + */ + protected $build; + + /** + * @var array + */ + protected $options; public function __construct(Builder $phpci, Build $build, array $options = array()) { - $this->phpci = $phpci; - - if (!empty($options['bootstrap'])) { - $this->bootstrap = $this->buildPath . $options['bootstrap']; - } + $this->phpci = $phpci; + $this->build = $build; + $this->options = $options; } /** @@ -47,11 +58,7 @@ class PhpSpec implements \PHPCI\Plugin return false; } - if ($this->bootstrap) { - $success = $this->phpci->executeCommand($phpspec . ' -f d'); - } else { - $success = $this->phpci->executeCommand($phpspec . ' -f d --bootstrap "%s"', $this->bootstrap); - } + $success = $this->phpci->executeCommand($phpspec . ' --format=pretty --no-code-generation'); chdir($curdir); diff --git a/PHPCI/Plugin/PhpUnit.php b/PHPCI/Plugin/PhpUnit.php index fde1b2fa..9d3ae323 100755 --- a/PHPCI/Plugin/PhpUnit.php +++ b/PHPCI/Plugin/PhpUnit.php @@ -9,8 +9,10 @@ namespace PHPCI\Plugin; +use PHPCI; use PHPCI\Builder; use PHPCI\Model\Build; +use PHPCI\Plugin\Util\TapParser; /** * PHP Unit Plugin - Allows PHP Unit testing. @@ -18,10 +20,11 @@ use PHPCI\Model\Build; * @package PHPCI * @subpackage Plugins */ -class PhpUnit implements \PHPCI\Plugin +class PhpUnit implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin { protected $args; protected $phpci; + protected $build; /** * @var string|string[] $directory The directory (or array of dirs) to run PHPUnit on @@ -46,9 +49,44 @@ class PhpUnit implements \PHPCI\Plugin */ protected $xmlConfigFile; + public static function canExecute($stage, Builder $builder, Build $build) + { + if ($stage == 'test' && !is_null(self::findConfigFile($builder->buildPath))) { + return true; + } + + return false; + } + + public static function findConfigFile($buildPath) + { + if (file_exists($buildPath . 'phpunit.xml')) { + return 'phpunit.xml'; + } + + if (file_exists($buildPath . 'tests/phpunit.xml')) { + return 'tests/phpunit.xml'; + } + + if (file_exists($buildPath . 'phpunit.xml.dist')) { + return 'phpunit.xml.dist'; + } + + if (file_exists($buildPath . 'tests/phpunit.xml.dist')) { + return 'tests/phpunit.xml.dist'; + } + + return null; + } + public function __construct(Builder $phpci, Build $build, array $options = array()) { - $this->phpci = $phpci; + $this->phpci = $phpci; + $this->build = $build; + + if (empty($options['config']) && empty($options['directory'])) { + $this->xmlConfigFile = self::findConfigFile($phpci->buildPath); + } if (isset($options['directory'])) { $this->directory = $options['directory']; @@ -63,7 +101,7 @@ class PhpUnit implements \PHPCI\Plugin } if (isset($options['args'])) { - $this->args = $options['args']; + $this->args = $this->phpci->interpolate($options['args']); } if (isset($options['path'])) { @@ -82,6 +120,8 @@ class PhpUnit implements \PHPCI\Plugin { $success = true; + $this->phpci->logExecOutput(false); + // Run any config files first. This can be either a single value or an array. if ($this->xmlConfigFile !== null) { $success &= $this->runConfigFile($this->xmlConfigFile); @@ -92,6 +132,23 @@ class PhpUnit implements \PHPCI\Plugin $success &= $this->runDir($this->directory); } + $tapString = $this->phpci->getLastOutput(); + + try { + $tapParser = new TapParser($tapString); + $output = $tapParser->parse(); + } catch (\Exception $ex) { + $this->phpci->logFailure($tapString); + throw $ex; + } + + $failures = $tapParser->getTotalFailures(); + + $this->build->storeMeta('phpunit-errors', $failures); + $this->build->storeMeta('phpunit-data', $output); + + $this->phpci->logExecOutput(true); + return $success; } @@ -114,7 +171,7 @@ class PhpUnit implements \PHPCI\Plugin } - $cmd = $phpunit . ' %s -c "%s" ' . $this->coverage . $this->path; + $cmd = $phpunit . ' --tap %s -c "%s" ' . $this->coverage . $this->path; $success = $this->phpci->executeCommand($cmd, $this->args, $this->phpci->buildPath . $configPath); if ($this->runFrom) { @@ -140,7 +197,7 @@ class PhpUnit implements \PHPCI\Plugin return false; } - $cmd = $phpunit . ' %s "%s"'; + $cmd = $phpunit . ' --tap %s "%s"'; $success = $this->phpci->executeCommand($cmd, $this->args, $this->phpci->buildPath . $dirPath); chdir($curdir); return $success; diff --git a/PHPCI/Plugin/Shell.php b/PHPCI/Plugin/Shell.php index b9949cd3..73439cad 100644 --- a/PHPCI/Plugin/Shell.php +++ b/PHPCI/Plugin/Shell.php @@ -20,9 +20,18 @@ use PHPCI\Model\Build; */ class Shell implements \PHPCI\Plugin { - protected $args; + /** + * @var \PHPCI\Builder + */ protected $phpci; + /** + * @var \PHPCI\Model\Build + */ + protected $build; + + protected $args; + /** * @var string[] $commands The commands to be executed */ @@ -30,7 +39,8 @@ class Shell implements \PHPCI\Plugin public function __construct(Builder $phpci, Build $build, array $options = array()) { - $this->phpci = $phpci; + $this->phpci = $phpci; + $this->build = $build; if (isset($options['command'])) { // Keeping this for backwards compatibility, new projects should use interpolation vars. diff --git a/PHPCI/Plugin/Util/Executor.php b/PHPCI/Plugin/Util/Executor.php index 39a5d35e..15754f84 100644 --- a/PHPCI/Plugin/Util/Executor.php +++ b/PHPCI/Plugin/Util/Executor.php @@ -6,7 +6,6 @@ use \PHPCI\Logging\BuildLogger; class Executor { - /** * @var BuildLogger */ @@ -103,5 +102,4 @@ class Executor return $rtn; } - } diff --git a/PHPCI/Plugin/Util/Factory.php b/PHPCI/Plugin/Util/Factory.php index 781b6d5a..971b3e23 100644 --- a/PHPCI/Plugin/Util/Factory.php +++ b/PHPCI/Plugin/Util/Factory.php @@ -4,7 +4,8 @@ namespace PHPCI\Plugin\Util; -class Factory { +class Factory +{ const TYPE_ARRAY = "array"; const TYPE_CALLABLE = "callable"; @@ -18,12 +19,11 @@ class Factory { */ private $container; - function __construct(\Pimple $container = null) + public function __construct(\Pimple $container = null) { if ($container) { $this->container = $container; - } - else { + } else { $this->container = new \Pimple(); } @@ -37,6 +37,28 @@ class Factory { ); } + /** + * Trys to get a function from the file path specified. If the + * file returns a function then $this will be passed to it. + * This enables the config file to call any public methods. + * + * @param $configPath + * @return bool - true if the function exists else false. + */ + public function addConfigFromFile($configPath) + { + // The file is expected to return a function which can + // act on the pluginFactory to register any resources needed. + if (file_exists($configPath)) { + $configFunction = require($configPath); + if (is_callable($configFunction)) { + $configFunction($this); + return true; + } + } + return false; + } + public function getLastOptions() { return $this->currentPluginOptions; diff --git a/PHPCI/Plugin/Util/PluginInformationCollection.php b/PHPCI/Plugin/Util/PluginInformationCollection.php index 9017ba72..43716867 100644 --- a/PHPCI/Plugin/Util/PluginInformationCollection.php +++ b/PHPCI/Plugin/Util/PluginInformationCollection.php @@ -44,5 +44,4 @@ class PluginInformationCollection implements InstalledPluginInformation } return $arr; } - } diff --git a/PHPCI/Plugin/Util/TapParser.php b/PHPCI/Plugin/Util/TapParser.php new file mode 100644 index 00000000..5b099b03 --- /dev/null +++ b/PHPCI/Plugin/Util/TapParser.php @@ -0,0 +1,107 @@ +tapString = trim($tapString); + } + + /** + * Parse a given TAP format string and return an array of tests and their status. + */ + public function parse() + { + // Split up the TAP string into an array of lines, then + // trim all of the lines so there's no leading or trailing whitespace. + $lines = explode("\n", $this->tapString); + $lines = array_map(function ($line) { + return trim($line); + }, $lines); + + // Check TAP version: + $versionLine = array_shift($lines); + + if ($versionLine != 'TAP version 13') { + throw new \Exception('TapParser only supports TAP version 13'); + } + + if (preg_match(self::TEST_COVERAGE_PATTERN, $lines[count($lines) - 1])) { + array_pop($lines); + if ($lines[count($lines) - 1] == "") { + array_pop($lines); + } + } + + $matches = array(); + $totalTests = 0; + if (preg_match(self::TEST_COUNTS_PATTERN, $lines[0], $matches)) { + array_shift($lines); + $totalTests = (int) $matches[2]; + } + + if (preg_match(self::TEST_COUNTS_PATTERN, $lines[count($lines) - 1], $matches)) { + array_pop($lines); + $totalTests = (int) $matches[2]; + } + + $rtn = $this->processTestLines($lines); + + if ($totalTests != count($rtn)) { + throw new \Exception('Invalid TAP string, number of tests does not match specified test count.'); + } + + return $rtn; + } + + protected function processTestLines($lines) + { + $rtn = array(); + + foreach ($lines as $line) { + $matches = array(); + + if (preg_match(self::TEST_LINE_PATTERN, $line, $matches)) { + $ok = ($matches[1] == 'ok' ? true : false); + + if (!$ok) { + $this->failures++; + } + + $item = array( + 'pass' => $ok, + 'suite' => $matches[2], + 'test' => $matches[3], + ); + + $rtn[] = $item; + } elseif (preg_match(self::TEST_MESSAGE_PATTERN, $line, $matches)) { + $rtn[count($rtn) - 1]['message'] = $matches[1]; + } + } + + return $rtn; + } + + public function getTotalFailures() + { + return $this->failures; + } +} diff --git a/PHPCI/Plugin/Wipe.php b/PHPCI/Plugin/Wipe.php new file mode 100644 index 00000000..7a0c2107 --- /dev/null +++ b/PHPCI/Plugin/Wipe.php @@ -0,0 +1,60 @@ + +* @package PHPCI +* @subpackage Plugins +*/ +class Wipe implements \PHPCI\Plugin +{ + /** + * @var \PHPCI\Builder + */ + protected $phpci; + + /** + * @var \PHPCI\Model\Build + */ + protected $build; + + protected $directory; + + + public function __construct(Builder $phpci, Build $build, array $options = array()) + { + $path = $phpci->buildPath; + $this->phpci = $phpci; + $this->build = $build; + $this->directory = isset($options['directory']) ? $options['directory'] : $path; + } + + /** + * Wipes a directory's contents + */ + public function execute() + { + $build = $this->phpci->buildPath; + + if ($this->directory == $build || empty($this->directory)) { + return true; + } + if (is_dir($this->directory)) { + $cmd = 'rm -rf %s*'; + $success = $this->phpci->executeCommand($cmd, $this->directory); + } + return $success; + } +} diff --git a/PHPCI/View/Build/view.phtml b/PHPCI/View/Build/view.phtml index 6e181444..b73620a5 100644 --- a/PHPCI/View/Build/view.phtml +++ b/PHPCI/View/Build/view.phtml @@ -1,38 +1,59 @@ -
-

getProject()->getTitle(); ?> - Build #getId(); ?>

+
+ -
- Branch: getBranch(); ?>
- Committer: getCommitterEmail(); ?>
- Commit ID: getCommitId() == 'Manual' ? 'HEAD' : $build->getCommitId(); ?>
- Commit Message: getCommitMessage(); ?> +
+

+ + getProject()->getTitle(); ?> + #getId(); ?> + + +

+
+ +
+
+ getCommitMessage()): ?> +
+ getCommitMessage(); ?> +
+ + + Branch: getBranch(); ?>
+ Committer: getCommitterEmail(); ?> + + getCommitId() != 'Manual'): ?> +
Commit ID: getCommitId(); ?>
+ +
- -
Options
- +
+
+

Options

+
+ +
-
+
$(document).ready(function() { PHPCI.renderPlugins(); + + $('#delete-build').on('click', function (e) { + e.preventDefault(); + confirmDelete( + "build/delete/getId(); ?>", "Build" + ).onCloseConfirmed = function () {window.location = '/'}; + }); + + $(window).on('build-updated', function(data) { + updateBuildStatus(data.queryData.status); + }); + + updateBuildStatus(getStatus(); ?>); }); + + function updateBuildStatus(status) { + var statusClass = null; + var statusText = null; + + switch (status) { + case 0: + statusClass = 'info'; + statusText = 'Pending'; + break; + case 1: + statusClass = 'warning'; + statusText = 'Running'; + break; + case 2: + statusClass = 'success'; + statusText = 'Success'; + break; + case 3: + statusClass = 'danger'; + statusText = 'Failed'; + break; + } + + $('.build-info-panel') + .removeClass('panel-info') + .removeClass('panel-warning') + .removeClass('panel-success') + .removeClass('panel-danger') + .addClass('panel-' + statusClass); + + $('.build-info-panel .label') + .removeClass('label-info') + .removeClass('label-warning') + .removeClass('label-success') + .removeClass('label-danger') + .addClass('label-' + statusClass) + .text(statusText); + } diff --git a/PHPCI/View/BuildStatus/view.phtml b/PHPCI/View/BuildStatus/view.phtml new file mode 100644 index 00000000..ddc7936a --- /dev/null +++ b/PHPCI/View/BuildStatus/view.phtml @@ -0,0 +1,193 @@ + + + + <?php print $project->getTitle(); ?> - PHPCI + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

getTitle(); ?>

+ + + getStatus()) { + case 0: + $statusClass = 'info'; + $statusText = 'Pending'; + break; + case 1: + $statusClass = 'warning'; + $statusText = 'Running'; + break; + case 2: + $statusClass = 'success'; + $statusText = 'Success'; + break; + case 3: + $statusClass = 'danger'; + $statusText = 'Failed'; + break; + } + + ?> + + +
+ + +
+

+ + getProject()->getTitle(); ?> + #getId(); ?> + + +

+
+ +
+
+ getCommitMessage()): ?> +
+ getCommitMessage(); ?> +
+ + + Branch: getBranch(); ?>
+ Committer: getCommitterEmail(); ?> + + getCommitId() != 'Manual'): ?> +
Commit ID: getCommitId(); ?>
+ +
+
+
+ + + + +
+

Builds

+ + + + + + + + + + + + + + + + + + + + + getStatus()) + { + case 0: + $cls = 'active'; + $subcls = 'info'; + $status = 'Pending'; + + break; + + case 1: + $cls = 'warning'; + $subcls = 'warning'; + $status = 'Running'; + break; + + case 2: + $cls = 'success'; + $subcls = 'success'; + $status = 'Success'; + break; + + case 3: + $cls = 'danger'; + $subcls = 'danger'; + $status = 'Failed'; + break; + } + ?> + + + + + + + + + + + + +
IDCommitBranchStatus
No builds yet.
#getId(), 6, '0', STR_PAD_LEFT); ?> + getCommitId() !== 'Manual') { + print ''; + } + print $build->getCommitId(); + if ($build->getCommitId() !== 'Manual') { + print ''; + } + ?> + getBranch(); ?> + getPlugins(), true); + + if ( !is_array($plugins) ) { + $plugins = array(); + } + if ( 0 === count($plugins) ) { + ?> + $pluginstatus): + $subcls = $pluginstatus?'label label-success':'label label-danger'; + ?> Build()->formatPluginName($plugin); ?> +
+
+
+
+ + diff --git a/PHPCI/View/BuildsTable.phtml b/PHPCI/View/BuildsTable.phtml index a9351353..da67e087 100644 --- a/PHPCI/View/BuildsTable.phtml +++ b/PHPCI/View/BuildsTable.phtml @@ -46,7 +46,19 @@ switch($build->getStatus()) print ' - '; } ?> - getCommitId(); ?> + + + getCommitId() !== 'Manual') { + print ''; + } + print $build->getCommitId(); + if ($build->getCommitId() !== 'Manual') { + print ''; + } + ?> + + getBranch(); ?> getStatus())
- View + View User()->getIsAdmin()): ?> -