├── .gitignore ├── web ├── images │ ├── hr.png │ ├── failed.png │ ├── header.png │ ├── nobuild.png │ ├── success.png │ └── sensio-labs-product.png ├── .htaccess ├── index.php └── css │ └── sismo.css ├── src ├── templates │ ├── error.twig │ ├── ccmonitor.twig.xml │ ├── layout.twig │ ├── projects.twig │ └── project.twig ├── Sismo │ ├── BuildException.php │ ├── SSHProject.php │ ├── Contrib │ │ ├── GoogleTalkNotifier.php │ │ ├── JoliNotifier.php │ │ ├── KDENotifier.php │ │ ├── LoggerNotifier.php │ │ ├── XmppNotifier.php │ │ ├── TwitterNotifier.php │ │ ├── CrossFingerNotifier.php │ │ ├── IrcNotifier.php │ │ ├── GrowlNotifier.php │ │ ├── AndroidPushC2DMNotifier.php │ │ └── GithubNotifier.php │ ├── Notifier │ │ ├── Notifier.php │ │ ├── DBusNotifier.php │ │ ├── MailNotifier.php │ │ └── GrowlNotifier.php │ ├── GithubProject.php │ ├── BitbucketProject.php │ ├── Storage │ │ ├── StorageInterface.php │ │ ├── Storage.php │ │ └── PdoStorage.php │ ├── Sismo.php │ ├── Builder.php │ ├── Commit.php │ └── Project.php ├── controllers.php ├── app.php └── console.php ├── .travis.yml ├── sismo ├── tests ├── Sismo │ └── Tests │ │ ├── BuildExceptionTest.php │ │ ├── SSHProjectTest.php │ │ ├── Notifier │ │ └── NotifierTest.php │ │ ├── Contrib │ │ ├── LoggerNotifierTest.php │ │ └── CrossFingerNotifierTest.php │ │ ├── Storage │ │ ├── PdoStorageTest.php │ │ └── StorageTest.php │ │ ├── GithubProjectTest.php │ │ ├── BitbucketProjectTest.php │ │ ├── CommitTest.php │ │ ├── ProjectTest.php │ │ └── SismoTest.php ├── appTest.php ├── consoleTest.php └── controllersTest.php ├── phpunit.xml.dist ├── LICENSE ├── composer.json ├── README.rst └── compile /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | composer.phar 3 | vendor 4 | -------------------------------------------------------------------------------- /web/images/hr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriendsOfPHP/Sismo/HEAD/web/images/hr.png -------------------------------------------------------------------------------- /web/images/failed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriendsOfPHP/Sismo/HEAD/web/images/failed.png -------------------------------------------------------------------------------- /web/images/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriendsOfPHP/Sismo/HEAD/web/images/header.png -------------------------------------------------------------------------------- /web/images/nobuild.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriendsOfPHP/Sismo/HEAD/web/images/nobuild.png -------------------------------------------------------------------------------- /web/images/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriendsOfPHP/Sismo/HEAD/web/images/success.png -------------------------------------------------------------------------------- /web/images/sensio-labs-product.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FriendsOfPHP/Sismo/HEAD/web/images/sensio-labs-product.png -------------------------------------------------------------------------------- /web/.htaccess: -------------------------------------------------------------------------------- 1 | #SetEnv SISMO_DATA_PATH "/path/to/data" 2 | #SetEnv SISMO_CONFIG_PATH "/path/to/config" 3 | 4 | RewriteEngine On 5 | 6 | RewriteCond %{REQUEST_FILENAME} !-f 7 | RewriteRule ^(.*)$ index.php [QSA,L] 8 | -------------------------------------------------------------------------------- /src/templates/error.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.twig' %} 2 | 3 | {% block title 'Error' %} 4 | 5 | {% block content %} 6 |

Hmmm, looks like something went wrong

7 | 8 |

{{ error ? error|replace({"\n": '
' })|raw : 'An error occurred' }}

9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: false 4 | 5 | php: 6 | - 5.3 7 | - 5.4 8 | - 5.5 9 | - 5.6 10 | - 7.0 11 | 12 | matrix: 13 | fast_finish: true 14 | allow_failures: 15 | - php: 7.0 16 | 17 | install: 18 | - travis_retry composer install --no-interaction --prefer-source 19 | 20 | script: 21 | - phpunit --verbose 22 | -------------------------------------------------------------------------------- /web/index.php: -------------------------------------------------------------------------------- 1 | run(); 12 | -------------------------------------------------------------------------------- /sismo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | run(); 16 | -------------------------------------------------------------------------------- /src/Sismo/BuildException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo; 13 | 14 | /** 15 | * Thrown for any problem during a build. 16 | * 17 | * @author Fabien Potencier 18 | */ 19 | class BuildException extends \Exception 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /tests/Sismo/Tests/BuildExceptionTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Tests; 13 | 14 | use Sismo\BuildException; 15 | 16 | class BuildExceptionTest extends \PHPUnit_Framework_TestCase 17 | { 18 | public function test() 19 | { 20 | $this->assertInstanceOf('\Exception', new BuildException()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/templates/ccmonitor.twig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% for project in projects %} 4 | {% set commit = project.getLatestCommit %} 5 | 14 | {% endfor %} 15 | 16 | -------------------------------------------------------------------------------- /src/Sismo/SSHProject.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo; 13 | 14 | /** 15 | * Describes a project accessible via SSH. 16 | * 17 | * @author Toni Uebernickel 18 | */ 19 | class SSHProject extends Project 20 | { 21 | /** 22 | * Sets the project repository URL. 23 | * 24 | * @param string $url The project repository URL 25 | */ 26 | public function setRepository($url) 27 | { 28 | $this->repository = $url; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | ./tests/ 17 | 18 | 19 | 20 | 21 | 22 | ./src/ 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/templates/layout.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title '' %} | Sismo 6 | 7 | 8 | 9 |
10 | {% block content '' %} 11 |
12 |
13 | Powered by Sismo {{ constant('Sismo\\Sismo::VERSION') }}, your Personal Continuous Testing Server 14 | a Sensio Labs product 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /src/templates/projects.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.twig' %} 2 | 3 | {% block title 'Projects' %} 4 | 5 | {% block content %} 6 | {% if not projects %} 7 |

No project yet.

8 | {% else %} 9 | 20 | {% endif %} 21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /src/Sismo/Contrib/GoogleTalkNotifier.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Contrib; 13 | 14 | // @codeCoverageIgnoreStart 15 | /** 16 | * Notifies builds via a Google Talk server. 17 | * 18 | * This notifier needs the XMPPHP library to be required in your configuration. 19 | * 20 | * require '/path/to/XMPPHP/XMPP.php'; 21 | * 22 | * Download it at http://code.google.com/p/xmpphp 23 | * 24 | * @author Fabien Potencier 25 | */ 26 | class GoogleTalkNotifier extends XmppNotifier 27 | { 28 | public function __construct($username, $password, $recipient, $format = '[%STATUS%] %name% %short_sha% -- %message% by %author%') 29 | { 30 | parent::__construct('talk.google.com', 5222, 'gmail.com', $username, $password, $recipient, $format); 31 | } 32 | } 33 | // @codeCoverageIgnoreEnd 34 | -------------------------------------------------------------------------------- /tests/Sismo/Tests/SSHProjectTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Tests; 13 | 14 | use Sismo\SSHProject; 15 | 16 | class SSHProjectTest extends \PHPUnit_Framework_TestCase 17 | { 18 | public function sshRepositoryProvider() 19 | { 20 | return array( 21 | array('git@github.com:twigphp/Twig.git'), 22 | array('git@git.assembla.com:Twig.git'), 23 | array('ssh://git@git.example.com:Twig.git'), 24 | ); 25 | } 26 | 27 | /** 28 | * @dataProvider sshRepositoryProvider 29 | */ 30 | public function testSetRepository($repository) 31 | { 32 | $project = new SSHProject('Project'); 33 | $project->setRepository($repository); 34 | 35 | $this->assertEquals($repository, $project->getRepository()); 36 | $this->assertEquals('master', $project->getBranch()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2013 Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sismo/sismo", 3 | "type": "silex-application", 4 | "description": "Sismo is a personal continuous integration server.", 5 | "keywords": ["sismo", "continuous integration"], 6 | "homepage": "http://sismo.sensiolabs.org", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Fabien Potencier", 11 | "email": "fabpot@symfony.com" 12 | }, 13 | { 14 | "name": "Sismo Community", 15 | "homepage": "https://github.com/FriendsOfPhp/sismo/contributors" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=5.3.3", 20 | "ext-sqlite3": "*", 21 | "silex/silex": "~1.0", 22 | "symfony/console": "~2.3", 23 | "symfony/twig-bridge": "~2.2", 24 | "symfony/process": "~2.2", 25 | "symfony/filesystem": "~2.2", 26 | "symfony/finder": "~2.2", 27 | "sensiolabs/ansi-to-html": "~1.0" 28 | }, 29 | "require-dev": { 30 | "phpunit/phpunit": "3.7.*", 31 | "symfony/browser-kit": "~2.2", 32 | "symfony/css-selector": "~2.2.0", 33 | "symfony/dom-crawler": "~2.2.0", 34 | "symfony/class-loader": "~2.2" 35 | }, 36 | "suggest": { 37 | "jolicode/jolinotif": "Use the best notifier available on your system (Mac, Linux or Windows)", 38 | "monolog/monolog": "Use Monolog handlers as notifiers" 39 | }, 40 | "autoload": { 41 | "psr-0": { "Sismo": "src/" } 42 | }, 43 | "bin": ["sismo"] 44 | } 45 | -------------------------------------------------------------------------------- /tests/Sismo/Tests/Notifier/NotifierTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Tests\Notifier; 13 | 14 | use Sismo\Notifier\Notifier; 15 | use Sismo\Commit; 16 | use Sismo\Project; 17 | 18 | class NotifierTest extends \PHPUnit_Framework_TestCase 19 | { 20 | public function testFormat() 21 | { 22 | $notifier = $this->getMock('Sismo\Notifier\Notifier'); 23 | $r = new \ReflectionObject($notifier); 24 | $m = $r->getMethod('format'); 25 | $m->setAccessible(true); 26 | 27 | $project = new Project('Twig'); 28 | $commit = new Commit($project, '123456'); 29 | $commit->setAuthor('Fabien'); 30 | $commit->setMessage('Foo'); 31 | 32 | $this->assertEquals('twig', $m->invoke($notifier, '%slug%', $commit)); 33 | $this->assertEquals('Twig', $m->invoke($notifier, '%name%', $commit)); 34 | $this->assertEquals('building', $m->invoke($notifier, '%status%', $commit)); 35 | $this->assertEquals('building', $m->invoke($notifier, '%status_code%', $commit)); 36 | $this->assertEquals('123456', $m->invoke($notifier, '%sha%', $commit)); 37 | $this->assertEquals('Fabien', $m->invoke($notifier, '%author%', $commit)); 38 | $this->assertEquals('Foo', $m->invoke($notifier, '%message%', $commit)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Sismo/Tests/Contrib/LoggerNotifierTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Tests\Contrib; 13 | 14 | use Sismo\Commit; 15 | use Sismo\Contrib\LoggerNotifier; 16 | use Sismo\Project; 17 | 18 | class LoggerNotifierTest extends \PHPUnit_Framework_TestCase 19 | { 20 | public function testNotify() 21 | { 22 | $project = new Project('Twig'); 23 | $successCommit = new Commit($project, '123455'); 24 | $successCommit->setAuthor('Fabien'); 25 | $successCommit->setMessage('Bar'); 26 | $successCommit->setStatusCode('success'); 27 | 28 | $failedCommit = new Commit($project, '123456'); 29 | $failedCommit->setAuthor('Fabien'); 30 | $failedCommit->setMessage('Foo'); 31 | $failedCommit->setStatusCode('failed'); 32 | 33 | $project->setCommits(array( 34 | $failedCommit, 35 | $successCommit, 36 | )); 37 | 38 | $logger = $this->getMock('Psr\Log\NullLogger'); 39 | $logger->expects($this->once())->method('info'); 40 | $logger->expects($this->once())->method('critical'); 41 | 42 | $notifier = new LoggerNotifier($logger); 43 | 44 | //notify success commit 45 | $notifier->notify($successCommit); 46 | //notify failed commit 47 | $notifier->notify($failedCommit); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Sismo/Notifier/Notifier.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Notifier; 13 | 14 | use Sismo\Commit; 15 | 16 | /** 17 | * Base class for notifiers. 18 | * 19 | * @author Fabien Potencier 20 | */ 21 | abstract class Notifier 22 | { 23 | /** 24 | * Notifies a commit. 25 | * 26 | * @param Commit $commit A Commit instance 27 | */ 28 | abstract public function notify(Commit $commit); 29 | 30 | protected function format($format, Commit $commit) 31 | { 32 | return strtr($format, $this->getPlaceholders($commit)); 33 | } 34 | 35 | protected function getPlaceholders(Commit $commit) 36 | { 37 | $project = $commit->getProject(); 38 | 39 | return array( 40 | '%slug%' => $project->getSlug(), 41 | '%name%' => $project->getName(), 42 | '%status%' => $commit->getStatus(), 43 | '%status_code%' => $commit->getStatusCode(), 44 | '%STATUS%' => strtoupper($commit->getStatus()), 45 | '%sha%' => $commit->getSha(), 46 | '%short_sha%' => $commit->getShortSha(), 47 | '%author%' => $commit->getAuthor(), 48 | '%message%' => $commit->getMessage(), 49 | '%output%' => $commit->getOutput(), 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Sismo/Contrib/JoliNotifier.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Contrib; 13 | 14 | use Joli\JoliNotif\Notification; 15 | use Joli\JoliNotif\NotifierFactory; 16 | use Sismo\Commit; 17 | use Sismo\Notifier\Notifier; 18 | 19 | // @codeCoverageIgnoreStart 20 | /** 21 | * Notifies builds via the best notifier available on the system (Mac, Linux or Windows). 22 | * 23 | * @author Loïck Piera 24 | */ 25 | class JoliNotifier extends Notifier 26 | { 27 | private $format; 28 | private $notifier; 29 | 30 | public function __construct($format = "[%STATUS%]\n%message%\n%author%") 31 | { 32 | if (!class_exists('Joli\JoliNotif\NotifierFactory')) { 33 | throw new \RuntimeException('This notifier requires the package jolicode/jolinotif to be installed'); 34 | } 35 | 36 | $this->format = $format; 37 | $this->notifier = NotifierFactory::create(); 38 | } 39 | 40 | public function notify(Commit $commit) 41 | { 42 | if (!$this->notifier) { 43 | return; 44 | } 45 | 46 | $notification = new Notification(); 47 | $notification->setTitle($commit->getProject()->getName()); 48 | $notification->setBody($this->format($this->format, $commit)); 49 | 50 | $this->notifier->send($notification); 51 | } 52 | } 53 | // @codeCoverageIgnoreEnd 54 | -------------------------------------------------------------------------------- /src/Sismo/Contrib/KDENotifier.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class KDENotifier extends Notifier 23 | { 24 | protected $titleFormat; 25 | protected $messageFormat; 26 | 27 | public function __construct($titleFormat = '', $messageFormat = '') 28 | { 29 | $this->titleFormat = $titleFormat; 30 | $this->messageFormat = $messageFormat; 31 | } 32 | 33 | public function notify(Commit $commit) 34 | { 35 | // first, try with the kdialog program 36 | $process = new Process(sprintf('kdialog --title "%s" --passivepopup "%s" 5', $this->format($this->titleFormat, $commit), $this->format($this->messageFormat, $commit))); 37 | $process->setTimeout(2); 38 | $process->run(); 39 | if ($process->isSuccessful()) { 40 | return; 41 | } 42 | 43 | // then, try knotify 44 | $process = new Process(sprintf('dcop knotify default notify eventname "%s" "%s" "" "" 16 2 ', $this->format($this->titleFormat, $commit), $this->format($this->messageFormat, $commit))); 45 | $process->setTimeout(2); 46 | $process->run(); 47 | if ($process->isSuccessful()) { 48 | return; 49 | } 50 | } 51 | } 52 | // @codeCoverageIgnoreEnd 53 | -------------------------------------------------------------------------------- /src/Sismo/Notifier/DBusNotifier.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Notifier; 13 | 14 | use Symfony\Component\Process\Process; 15 | use Sismo\Commit; 16 | 17 | // @codeCoverageIgnoreStart 18 | /** 19 | * Notifies builds via DBus. 20 | * 21 | * @author Fabien Potencier 22 | */ 23 | class DBusNotifier extends Notifier 24 | { 25 | public function __construct($format = "[%STATUS%]\n%message%\n%author%") 26 | { 27 | $this->format = $format; 28 | } 29 | 30 | public function notify(Commit $commit) 31 | { 32 | // first, try with the notify-send program 33 | $process = new Process(sprintf('notify-send "%s" "%s"', $commit->getProject()->getName(), $this->format($this->format, $commit))); 34 | $process->setTimeout(2); 35 | $process->run(); 36 | if ($process->isSuccessful()) { 37 | return; 38 | } 39 | 40 | // then, try dbus-send? 41 | $process = new Process(sprintf('dbus-send --print-reply --dest=org.freedesktop.Notifications /org/freedesktop/Notifications org.freedesktop.Notifications.Notify string:"sismo" int32:0 string:"" string:"%s" string:"%s" array:string:"" dict:string:"" int32:-1', $commit->getProject()->getName(), $this->format($this->format, $commit))); 42 | $process->setTimeout(2); 43 | $process->run(); 44 | if ($process->isSuccessful()) { 45 | return; 46 | } 47 | } 48 | } 49 | // @codeCoverageIgnoreEnd 50 | -------------------------------------------------------------------------------- /src/Sismo/GithubProject.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo; 13 | 14 | use Symfony\Component\Process\Process; 15 | 16 | /** 17 | * Describes a project hosted on Github. 18 | * 19 | * @author Fabien Potencier 20 | */ 21 | class GithubProject extends Project 22 | { 23 | public function setRepository($url) 24 | { 25 | parent::setRepository($url); 26 | 27 | if (file_exists($this->getRepository())) { 28 | $process = new Process('git remote -v', $this->getRepository()); 29 | $process->run(); 30 | foreach (explode("\n", $process->getOutput()) as $line) { 31 | $parts = explode("\t", $line); 32 | if ('origin' == $parts[0] && preg_match('#(?:\:|/|@)github.com(?:\:|/)(.*?)/(.*?)\.git#', $parts[1], $matches)) { 33 | $this->setUrlPattern(sprintf('https://github.com/%s/%s/commit/%%commit%%', $matches[1], $matches[2])); 34 | 35 | break; 36 | } 37 | } 38 | } elseif (preg_match('#^[a-z0-9_-]+/[a-z0-9_-]+$#i', $this->getRepository())) { 39 | $this->setUrlPattern(sprintf('https://github.com/%s/commit/%%commit%%', $this->getRepository())); 40 | parent::setRepository(sprintf('https://github.com/%s.git', $this->getRepository())); 41 | } else { 42 | throw new \InvalidArgumentException(sprintf('URL "%s" does not look like a Github repository.', $this->getRepository())); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Sismo/Contrib/LoggerNotifier.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Contrib; 13 | 14 | use Psr\Log\LoggerInterface; 15 | use Sismo\Commit; 16 | use Sismo\Notifier\Notifier; 17 | 18 | /** 19 | * Notifies builds via Monolog or any other PSR logger. 20 | * Logger must have info log level. 21 | * 22 | * Here is a usage example of Monolog with Slack handler: 23 | * 24 | * $message = << 43 | */ 44 | class LoggerNotifier extends Notifier 45 | { 46 | protected $logger; 47 | protected $messageFormat; 48 | 49 | /** 50 | * Constructor. 51 | * 52 | * @param LoggerInterface $logger 53 | * @param string $messageFormat 54 | */ 55 | public function __construct(LoggerInterface $logger, $messageFormat = '') 56 | { 57 | $this->logger = $logger; 58 | $this->messageFormat = $messageFormat; 59 | } 60 | 61 | /** 62 | * @inherit 63 | */ 64 | public function notify(Commit $commit) 65 | { 66 | $message = $this->format($this->messageFormat, $commit); 67 | 68 | return ($commit->getStatusCode() == 'success') ? $this->logger->info($message) : $this->logger->critical($message); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Sismo/BitbucketProject.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the MIT license that is bundled 8 | * with this source code in the file LICENSE. 9 | */ 10 | 11 | namespace Sismo; 12 | 13 | use Symfony\Component\Process\Process; 14 | 15 | /** 16 | * Describes a project hosted on BitBucket. 17 | * 18 | * @author Micah Breedlove 19 | * @author Fabien Potencier 20 | */ 21 | class BitbucketProject extends Project 22 | { 23 | public function setRepository($url) 24 | { 25 | parent::setRepository($url); 26 | 27 | if (file_exists($this->getRepository())) { 28 | $process = new Process('git remote -v', $this->getRepository()); 29 | $process->run(); 30 | foreach (explode("\n", $process->getOutput()) as $line) { 31 | $parts = explode("\t", $line); 32 | if ('origin' == $parts[0] && preg_match('#(?:\:|/|@)bitbucket.org(?:\:|/)(.*?)/(.*?)\.git#', $parts[1], $matches)) { 33 | $this->setUrlPattern(sprintf('https://bitbucket.org/%s/%s/commits/%%commit%%', $matches[1], $matches[2])); 34 | 35 | break; 36 | } 37 | } 38 | } elseif (preg_match('#^[a-z0-9_.-]+/[a-z0-9_.-]+$#i', $this->getRepository())) { 39 | $repo = preg_split('/\//', $this->getRepository()); 40 | 41 | $this->setUrlPattern(sprintf('https://bitbucket.org/%s/commits/%%commit%%', $this->getRepository())); 42 | $this->setBitBucketRepository(sprintf('git@bitbucket.org:/%s.git', $this->getRepository())); 43 | } else { 44 | throw new \InvalidArgumentException(sprintf('URL "%s" does not look like a BitBucket repository.', $this->getRepository())); 45 | } 46 | } 47 | 48 | public function setBitBucketRepository($url) 49 | { 50 | $this->repository = $url; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Sismo/Contrib/XmppNotifier.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Contrib; 13 | 14 | use Sismo\Notifier\Notifier; 15 | use Sismo\Commit; 16 | 17 | // @codeCoverageIgnoreStart 18 | /** 19 | * Notifies builds via a XMPP server. 20 | * 21 | * This notifier needs the XMPPHP library to be required in your configuration. 22 | * 23 | * require '/path/to/XMPPHP/XMPP.php'; 24 | * 25 | * Download it at http://code.google.com/p/xmpphp 26 | * 27 | * @author Fabien Potencier 28 | */ 29 | class XmppNotifier extends Notifier 30 | { 31 | private $format; 32 | private $host; 33 | private $port; 34 | private $username; 35 | private $password; 36 | private $server; 37 | private $recipient; 38 | 39 | public function __construct($host, $port, $server, $username, $password, $recipient, $format = '[%STATUS%] %name% %short_sha% -- %message% by %author%') 40 | { 41 | $this->host = $host; 42 | $this->port = $port; 43 | $this->server = $server; 44 | $this->username = $username; 45 | $this->password = $password; 46 | $this->recipient = $recipient; 47 | $this->format = $format; 48 | } 49 | 50 | public function notify(Commit $commit) 51 | { 52 | $old = error_reporting(0); 53 | $conn = new \XMPPHP_XMPP($this->host, $this->port, $this->username, $this->password, 'sismo', $this->server); 54 | $conn->connect(); 55 | $conn->processUntil('session_start'); 56 | $conn->presence(); 57 | foreach (explode(',', $this->recipient) as $user) { 58 | $conn->message($user, $this->format($this->format, $commit)); 59 | } 60 | $conn->disconnect(); 61 | error_reporting($old); 62 | } 63 | } 64 | // @codeCoverageIgnoreEnd 65 | -------------------------------------------------------------------------------- /src/templates/project.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.twig' %} 2 | 3 | {% from _self import commit_block %} 4 | 5 | {% block title 'Project ' ~ project %} 6 | 7 | {% block content %} 8 | 9 | 10 |

11 | 12 | {{ project.shortname }} 13 | {% if project.subname %} — {{ project.subname }}{% endif %} 14 | 15 |

16 | 17 | {% if not commit %} 18 |

Never built yet.

19 | {% else %} 20 |
{{ commit_block(commit) }}
21 | 22 | {% if commit.isbuilt %} 23 |
24 |
{{ commit.output ? ansi_to_html.convert(commit.output)|raw : 'No output' }}
25 |
26 | {% endif %} 27 | 28 | {% if commits is defined and commits %} 29 |

Builds History

30 |
    31 | {% for commit in commits %} 32 |
  • {{ commit_block(commit) }}
  • 33 | {% endfor %} 34 |
35 | {% endif %} 36 | {% endif %} 37 | {% endblock %} 38 | 39 | {% macro commit_block(commit) %} 40 |
{{ _self.commit_link(commit) }}
41 |

42 | {{ commit.message }} 43 | 44 |

45 |
46 | by {{ commit.author }} on {{ commit.date|date('j M Y H:i') }} 47 |
48 | {% endmacro %} 49 | 50 | {% macro commit_link(commit) %} 51 | {% if not commit.project.urlpattern %} 52 | #{{ commit.shortsha }} {{ commit.status }} 53 | {% else %} 54 | #{{ commit.shortsha }} {{ commit.status }} 55 | {% endif %} 56 | {% endmacro %} 57 | -------------------------------------------------------------------------------- /src/Sismo/Contrib/TwitterNotifier.php: -------------------------------------------------------------------------------- 1 | 37 | */ 38 | class TwitterNotifier extends Notifier 39 | { 40 | protected $consumerKey; 41 | protected $consumerSecret; 42 | protected $accessToken; 43 | protected $accessTokenSecret; 44 | protected $messageFormat; 45 | 46 | public function __construct($consumerKey, $consumerSecret, $accessToken, $accessTokenSecret, $messageFormat = "[%STATUS%]\n%message%\n%author%") 47 | { 48 | $this->consumerKey = $consumerKey; 49 | $this->consumerSecret = $consumerSecret; 50 | $this->accessToken = $accessToken; 51 | $this->accessTokenSecret = $accessTokenSecret; 52 | $this->messageFormat = $messageFormat; 53 | } 54 | 55 | public function notify(Commit $commit) 56 | { 57 | $conn = new \TwitterOAuth($this->consumerKey, $this->consumerSecret, $this->accessToken, $this->accessTokenSecret); 58 | $content = $conn->get('account/verify_credentials'); 59 | $conn->post('statuses/update', array('status' => $this->format($this->messageFormat, $commit))); 60 | } 61 | } 62 | // @codeCoverageIgnoreEnd 63 | -------------------------------------------------------------------------------- /src/Sismo/Storage/StorageInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Storage; 13 | 14 | use Sismo\Project; 15 | use Sismo\Commit; 16 | 17 | /** 18 | * Stores projects and builds information. 19 | * 20 | * @author Toni Uebernickel 21 | */ 22 | interface StorageInterface 23 | { 24 | /** 25 | * Retrieves a commit out of a project. 26 | * 27 | * @throws \RuntimeException 28 | * 29 | * @param Project $project The project this commit is part of. 30 | * @param string $sha The hash of the commit to retrieve. 31 | * 32 | * @return Commit 33 | */ 34 | public function getCommit(Project $project, $sha); 35 | 36 | /** 37 | * Initiate, create and save a new commit. 38 | * 39 | * @param Project $project The project of the new commit. 40 | * @param string $sha The hash of the commit. 41 | * @param string $author The name of the author of the new commit. 42 | * @param \DateTime $date The date the new commit was created originally (e.g. by external resources). 43 | * @param string $message The commit message. 44 | * 45 | * @return Commit The newly created commit. 46 | */ 47 | public function initCommit(Project $project, $sha, $author, \DateTime $date, $message); 48 | 49 | /** 50 | * Create or update the information of a project. 51 | * 52 | * If the project is already available, the information of the existing project will be updated. 53 | * 54 | * @param Project $project The project to create or update. 55 | */ 56 | public function updateProject(Project $project); 57 | 58 | /** 59 | * Update the commits information. 60 | * 61 | * The commit is identified by its sha hash. 62 | * 63 | * @param Commit $commit 64 | */ 65 | public function updateCommit(Commit $commit); 66 | 67 | /** 68 | * Shutdown the storage and all of its external resources. 69 | */ 70 | public function close(); 71 | } 72 | -------------------------------------------------------------------------------- /tests/Sismo/Tests/Storage/PdoStorageTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Tests\Storage; 13 | 14 | use Sismo\Storage\PdoStorage; 15 | 16 | require_once __DIR__.'/StorageTest.php'; 17 | 18 | class PdoStorageTest extends StorageTest 19 | { 20 | private $db; 21 | private $run = false; 22 | 23 | public function setUp() 24 | { 25 | // Skip checks once everything is fine. 26 | if (!$this->run) { 27 | if (!class_exists('\PDO')) { 28 | $this->markTestSkipped('PDO is not available.'); 29 | } 30 | 31 | if (!in_array('sqlite', \PDO::getAvailableDrivers())) { 32 | $this->markTestSkipped('SQLite PDO driver is not available.'); 33 | } 34 | 35 | $this->run = true; 36 | } 37 | 38 | $app = require __DIR__.'/../../../../src/app.php'; 39 | 40 | // sqlite with file is tested by StorageTest, so we use memory here 41 | $this->db = new \PDO('sqlite::memory:'); 42 | $this->db->exec($app['db.schema']); 43 | } 44 | 45 | public function testStaticCreate() 46 | { 47 | $this->assertInstanceOf('\Sismo\Storage\PdoStorage', PdoStorage::create('sqlite::memory:')); 48 | } 49 | 50 | public function testInitExistingCommit() 51 | { 52 | $project = $this->getProject(); 53 | 54 | $storage = $this->getStorage(); 55 | $commit = $storage->initCommit($project, '7d78d5', 'fabien', new \DateTime(), 'foo'); 56 | $this->assertInstanceOf('\Sismo\Commit', $commit); 57 | 58 | $commit2 = $storage->initCommit($project, '7d78d5', 'fabien', new \DateTime(), 'foo-amended'); 59 | $this->assertInstanceOf('\Sismo\Commit', $commit2); 60 | 61 | $this->assertEquals('foo-amended', $storage->getCommit($project, '7d78d5')->getMessage()); 62 | } 63 | 64 | public function tearDown() 65 | { 66 | unset($this->db); 67 | } 68 | 69 | protected function getStorage() 70 | { 71 | return new PdoStorage($this->db); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Sismo/Contrib/CrossFingerNotifier.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Contrib; 13 | 14 | use Sismo\Commit; 15 | use Sismo\Notifier\Notifier; 16 | 17 | /** 18 | * A cross finger notifier. 19 | * Launch notification on an array of Notifier instance 20 | * only if the commit needs it. 21 | * 22 | * @author Tugdual Saunier 23 | */ 24 | class CrossFingerNotifier extends Notifier 25 | { 26 | protected $notifiers; 27 | 28 | /** 29 | * Constructor 30 | * 31 | * @param array|Notifier $notifiers An array or a single Notifier instance 32 | */ 33 | public function __construct($notifiers = array()) 34 | { 35 | if (!is_array($notifiers)) { 36 | $notifiers = array($notifiers); 37 | } 38 | 39 | foreach ($notifiers as $notifier) { 40 | if (!$notifier instanceof Notifier) { 41 | throw new \InvalidArgumentException("Only Sismo\Notifier instance supported"); 42 | } 43 | 44 | $this->notifiers[] = $notifier; 45 | } 46 | } 47 | 48 | /** 49 | * Notifies a commit. 50 | * 51 | * @param Commit $commit Then Commit instance 52 | * 53 | * @return bool whether notification has been sent or not 54 | */ 55 | public function notify(Commit $commit) 56 | { 57 | if ($this->commitNeedNotification($commit)) { 58 | foreach ($this->notifiers as $notifier) { 59 | $notifier->notify($commit); 60 | } 61 | 62 | return true; 63 | } 64 | 65 | return false; 66 | } 67 | 68 | /** 69 | * Determines if a build needs to be notify 70 | * based on his status and his predecessor's one 71 | * 72 | * @param Commit $commit The commit to analyse 73 | * 74 | * @return bool whether the commit need notification or not 75 | */ 76 | protected function commitNeedNotification(Commit $commit) 77 | { 78 | if (!$commit->isSuccessful()) { 79 | return true; 80 | } 81 | 82 | //getProject()->getLatestCommit() actually contains the previous build 83 | $previousCommit = $commit->getProject()->getLatestCommit(); 84 | 85 | return !$previousCommit || $previousCommit->getStatusCode() != $commit->getStatusCode(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/Sismo/Tests/GithubProjectTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Tests; 13 | 14 | use Sismo\GithubProject; 15 | use Symfony\Component\Filesystem\Filesystem; 16 | use Symfony\Component\Process\Process; 17 | 18 | class GithubProjectTest extends \PHPUnit_Framework_TestCase 19 | { 20 | public function testSetRepository() 21 | { 22 | $project = new GithubProject('Twig', 'twigphp/Twig'); 23 | $this->assertEquals('master', $project->getBranch()); 24 | $this->assertEquals('https://github.com/twigphp/Twig.git', $project->getRepository()); 25 | $this->assertEquals('https://github.com/twigphp/Twig/commit/%commit%', $project->getUrlPattern()); 26 | 27 | $project = new GithubProject('Twig', 'twigphp/Twig@foo'); 28 | $this->assertEquals('foo', $project->getBranch()); 29 | $this->assertEquals('https://github.com/twigphp/Twig.git', $project->getRepository()); 30 | $this->assertEquals('https://github.com/twigphp/Twig/commit/%commit%', $project->getUrlPattern()); 31 | } 32 | 33 | public function localRepositoryProvider() 34 | { 35 | return array( 36 | array('https://github.com/twigphp/Twig.git'), 37 | array('git@github.com:twigphp/Twig.git'), 38 | ); 39 | } 40 | 41 | /** 42 | * @dataProvider localRepositoryProvider 43 | */ 44 | public function testSetRepositoryLocal($url) 45 | { 46 | $fs = new Filesystem(); 47 | $repository = sys_get_temp_dir().'/sismo/twigphp/Twig'; 48 | $fs->remove($repository); 49 | $fs->mkdir($repository); 50 | 51 | $process = new Process('git init && git remote add origin '.$url, $repository); 52 | $process->run(); 53 | 54 | $project = new GithubProject('Twig', $repository); 55 | $this->assertEquals('master', $project->getBranch()); 56 | $this->assertEquals($repository, $project->getRepository()); 57 | $this->assertEquals('https://github.com/twigphp/Twig/commit/%commit%', $project->getUrlPattern()); 58 | 59 | $fs->remove($repository); 60 | } 61 | 62 | /** 63 | * @expectedException \InvalidArgumentException 64 | */ 65 | public function testSetRepositoryThrowsAnExceptionIfRepositoryIsNotAGithubOne() 66 | { 67 | $project = new GithubProject('Twig', 'twigphp/Twig/foobar'); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/controllers.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | use Symfony\Component\HttpFoundation\Response; 13 | use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; 14 | use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; 15 | 16 | $app->get('/', function () use ($app) { 17 | return $app['twig']->render('projects.twig', array('projects' => $app['sismo']->getProjects())); 18 | })->bind('projects'); 19 | 20 | $app->get('/{slug}', function ($slug) use ($app) { 21 | if (!$app['sismo']->hasProject($slug)) { 22 | throw new NotFoundHttpException(sprintf('Project "%s" not found.', $slug)); 23 | } 24 | 25 | $project = $app['sismo']->getProject($slug); 26 | $commits = $project->getCommits(); 27 | $latest = array_shift($commits); 28 | 29 | return $app['twig']->render('project.twig', array( 30 | 'project' => $project, 31 | 'commit' => $latest, 32 | 'commits' => $commits, 33 | )); 34 | })->bind('project'); 35 | 36 | $app->get('/dashboard/cctray.xml', function () use ($app) { 37 | $content = $app['twig']->render('ccmonitor.twig.xml', array('projects' => $app['sismo']->getProjects())); 38 | 39 | return new Response($content, 200, array('content-type' => 'text/xml')); 40 | })->bind('ccmonitor'); 41 | 42 | $app->get('/{slug}/{sha}', function ($slug, $sha) use ($app) { 43 | if (!$app['sismo']->hasProject($slug)) { 44 | throw new NotFoundHttpException(sprintf('Project "%s" not found.', $slug)); 45 | } 46 | 47 | $project = $app['sismo']->getProject($slug); 48 | 49 | if (!$commit = $app['storage']->getCommit($project, $sha)) { 50 | throw new NotFoundHttpException(sprintf('Commit "%s" for project "%s" not found.', $sha, $slug)); 51 | } 52 | 53 | return $app['twig']->render('project.twig', array( 54 | 'project' => $project, 55 | 'commit' => $commit, 56 | )); 57 | })->bind('commit'); 58 | 59 | $app->post('/{slug}/build/{token}', function ($slug, $token) use ($app) { 60 | // Boot sismo 61 | $app['sismo']; 62 | 63 | if (!$server_token = $app['build.token']) { 64 | throw new NotFoundHttpException('Not found.'); 65 | } 66 | if ($token != $server_token) { 67 | throw new AccessDeniedHttpException(); 68 | } 69 | if (!$app['sismo']->hasProject($slug)) { 70 | throw new NotFoundHttpException(sprintf('Project "%s" not found.', $slug)); 71 | } 72 | 73 | $project = $app['sismo']->getProject($slug); 74 | $app['sismo']->build($project); 75 | 76 | return sprintf('Triggered build for project "%s".', $slug); 77 | })->bind('build'); 78 | -------------------------------------------------------------------------------- /tests/appTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | use Symfony\Component\Filesystem\Filesystem; 13 | use Sismo\Project; 14 | 15 | class appTest extends \PHPUnit_Framework_TestCase 16 | { 17 | private $app; 18 | 19 | public function setUp() 20 | { 21 | $this->app = require __DIR__.'/../src/app.php'; 22 | 23 | $this->baseDir = sys_get_temp_dir().'/sismo'; 24 | $fs = new Filesystem(); 25 | $fs->mkdir($this->baseDir); 26 | $this->app['data.path'] = $this->baseDir.'/db'; 27 | $this->app['config.file'] = $this->baseDir.'/config.php'; 28 | 29 | // This file does not exist, so app will use default sqlite storage. 30 | $app['config.storage.file'] = $this->baseDir.'/storage.php'; 31 | 32 | @unlink($this->app['db.path']); 33 | file_put_contents($app['config.file'], 'app['storage']->close(); 41 | 42 | $fs = new Filesystem(); 43 | $fs->remove($this->baseDir); 44 | } 45 | 46 | public function testServices() 47 | { 48 | $this->assertInstanceOf('SQLite3', $this->app['db']); 49 | $this->assertInstanceOf('Sismo\Storage\StorageInterface', $this->app['storage']); 50 | $this->assertInstanceOf('Sismo\Builder', $this->app['builder']); 51 | $this->assertInstanceOf('Sismo\Sismo', $this->app['sismo']); 52 | } 53 | 54 | public function testMissingGit() 55 | { 56 | $this->app['git.path'] = 'gitinvalidcommand'; 57 | 58 | $this->setExpectedException('\RuntimeException'); 59 | $builder = $this->app['builder']->init(new Project('foo'), null); 60 | } 61 | 62 | public function testMissingConfigFile() 63 | { 64 | $this->app['config.file'] = $this->baseDir.'/missing-config.php'; 65 | 66 | $this->setExpectedException('\RuntimeException'); 67 | $sismo = $this->app['sismo']; 68 | } 69 | 70 | public function invalidConfigProvider() 71 | { 72 | return array( 73 | array('app['config.file'], $config); 85 | 86 | $this->setExpectedException('\RuntimeException'); 87 | $sismo = $this->app['sismo']; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Sismo/Notifier/MailNotifier.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Notifier; 13 | 14 | use Sismo\Commit; 15 | 16 | // @codeCoverageIgnoreStart 17 | /** 18 | * A base email notifier using the native mail() function. 19 | * 20 | * Here is a usage example: 21 | * 22 | * $subject = '[%status_code%] %name% (%short_sha%)'; 23 | * $message = << 39 | */ 40 | class MailNotifier extends Notifier 41 | { 42 | protected $recipients; 43 | protected $subjectFormat; 44 | protected $messageFormat; 45 | protected $headers; 46 | protected $params; 47 | 48 | /** 49 | * Constructor. 50 | * 51 | * @param array|string $recipients 52 | * @param string $subjectFormat 53 | * @param string $messageFormat 54 | * @param string $headers Additional headers applied to the email. 55 | * @param string $params Additional params to be used on mail() 56 | */ 57 | public function __construct($recipients, $subjectFormat = '', $messageFormat = '', $headers = '', $params = '') 58 | { 59 | $this->recipients = $recipients; 60 | $this->subjectFormat = $subjectFormat; 61 | $this->messageFormat = $messageFormat; 62 | $this->headers = $headers; 63 | $this->params = $params; 64 | } 65 | 66 | public function notify(Commit $commit) 67 | { 68 | $subject = $this->format($this->subjectFormat, $commit); 69 | $message = $this->format($this->messageFormat, $commit); 70 | 71 | return $this->sendEmail($this->recipients, $subject, $message, $this->headers, $this->params); 72 | } 73 | 74 | /** 75 | * Send the email. 76 | * 77 | * @param array|string $to 78 | * @param string $subject 79 | * @param string $message 80 | * @param string $headers Additional headers to send. 81 | * @param string $params Additional params for the mailer in use. 82 | * 83 | * @return bool Whether the mail has been sent. 84 | */ 85 | protected function sendEmail($to, $subject, $message, $headers = '', $params = '') 86 | { 87 | if (is_array($to)) { 88 | $to = implode(',', $to); 89 | } 90 | 91 | return mail($to, $subject, $message, $headers, $params); 92 | } 93 | } 94 | // @codeCoverageIgnoreEnd 95 | -------------------------------------------------------------------------------- /src/Sismo/Notifier/GrowlNotifier.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Notifier; 13 | 14 | use Sismo\Commit; 15 | 16 | // @codeCoverageIgnoreStart 17 | /** 18 | * Notifies builds via a Growl (Mac only). 19 | * 20 | * @author Fabien Potencier 21 | */ 22 | class GrowlNotifier extends Notifier 23 | { 24 | private $application; 25 | private $address; 26 | private $notifications; 27 | private $password; 28 | private $port; 29 | private $registered; 30 | private $format; 31 | 32 | public function __construct($password, $application = 'sismo', $address = 'localhost', $format = "[%STATUS%]\n%message%\n%author%", $port = 9887) 33 | { 34 | $this->application = $application; 35 | $this->address = $address; 36 | $this->password = $password; 37 | $this->format = $format; 38 | $this->port = $port; 39 | $this->registered = false; 40 | $this->notifications = array( 41 | array('name' => 'Success', 'enabled' => true), 42 | array('name' => 'Fail', 'enabled' => true), 43 | ); 44 | } 45 | 46 | public function notify(Commit $commit) 47 | { 48 | $this->register(); 49 | 50 | return $this->doNotify($commit->isSuccessful() ? 'Success' : 'Fail', $commit->getProject()->getName(), $this->format($this->format, $commit)); 51 | } 52 | 53 | private function register() 54 | { 55 | if (true === $this->registered) { 56 | return; 57 | } 58 | 59 | $this->registered = true; 60 | $data = ''; 61 | $defaults = ''; 62 | $nbDefaults = 0; 63 | foreach ($this->notifications as $i => $notification) { 64 | $data .= pack('n', strlen($notification['name'])).$notification['name']; 65 | if ($notification['enabled']) { 66 | $defaults .= pack('c', $i); 67 | ++$nbDefaults; 68 | } 69 | } 70 | 71 | // pack(Protocol version, type, app name, number of notifications to register) 72 | $data = pack('c2nc2', 1, 0, strlen($this->application), count($this->notifications), $nbDefaults).$this->application.$data.$defaults; 73 | 74 | $this->send($data); 75 | } 76 | 77 | private function doNotify($name, $title, $message) 78 | { 79 | // pack(protocol version, type, priority/sticky flags, notification name length, title length, message length. app name length) 80 | $data = pack('c2n5', 1, 1, 0, strlen($name), strlen($title), strlen($message), strlen($this->application)).$name.$title.$message.$this->application; 81 | 82 | $this->send($data); 83 | } 84 | 85 | private function send($data) 86 | { 87 | $data .= pack('H32', md5($data.$this->password)); 88 | 89 | $fp = fsockopen('udp://'.$this->address, $this->port); 90 | fwrite($fp, $data); 91 | fclose($fp); 92 | } 93 | } 94 | // @codeCoverageIgnoreEnd 95 | -------------------------------------------------------------------------------- /tests/Sismo/Tests/BitbucketProjectTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Tests; 13 | 14 | use Sismo\BitbucketProject; 15 | use Symfony\Component\Filesystem\Filesystem; 16 | use Symfony\Component\Process\Process; 17 | 18 | class BitbucketProjectTest extends \PHPUnit_Framework_TestCase 19 | { 20 | /** 21 | * @dataProvider getRepositoryProvider 22 | */ 23 | public function testThatItSetRepository($repositoryPath, $branch, $repository, $urlPattern) 24 | { 25 | $project = new BitbucketProject('Twig', $repositoryPath); 26 | $this->assertEquals($branch, $project->getBranch()); 27 | $this->assertEquals($repository, $project->getRepository()); 28 | $this->assertEquals($urlPattern, $project->getUrlPattern()); 29 | } 30 | 31 | public function getRepositoryProvider() 32 | { 33 | return array( 34 | array('acme/Demo', 'master', 'git@bitbucket.org:/acme/Demo.git', 'https://bitbucket.org/acme/Demo/commits/%commit%'), 35 | array('acme/Demo@develop', 'develop', 'git@bitbucket.org:/acme/Demo.git', 'https://bitbucket.org/acme/Demo/commits/%commit%'), 36 | array('acme/no.no1', 'master', 'git@bitbucket.org:/acme/no.no1.git', 'https://bitbucket.org/acme/no.no1/commits/%commit%'), 37 | array('no.no/acme', 'master', 'git@bitbucket.org:/no.no/acme.git', 'https://bitbucket.org/no.no/acme/commits/%commit%'), 38 | array('acme/no_no1', 'master', 'git@bitbucket.org:/acme/no_no1.git', 'https://bitbucket.org/acme/no_no1/commits/%commit%'), 39 | array('acme/no-no1', 'master', 'git@bitbucket.org:/acme/no-no1.git', 'https://bitbucket.org/acme/no-no1/commits/%commit%'), 40 | ); 41 | } 42 | 43 | /** 44 | * @expectedException \InvalidArgumentException 45 | */ 46 | public function testSetRepositoryThrowsAnExceptionIfRepositoryIsNotAGithubOne() 47 | { 48 | $project = new BitbucketProject('Twig', 'twigphp/Twig/foobar'); 49 | } 50 | 51 | public function localRepositoryProvider() 52 | { 53 | return array( 54 | array('https://bitbucket.org/atlassian/stash-example-plugin.git'), 55 | array('git@bitbucket.org:atlassian/stash-example-plugin.git'), 56 | ); 57 | } 58 | 59 | /** 60 | * @dataProvider localRepositoryProvider 61 | */ 62 | public function testSetRepositoryLocal($url) 63 | { 64 | $fs = new Filesystem(); 65 | $repository = sys_get_temp_dir().'/sismo/atlassian/stash-example-plugin'; 66 | $fs->remove($repository); 67 | $fs->mkdir($repository); 68 | 69 | $process = new Process('git init && git remote add origin '.$url, $repository); 70 | $process->run(); 71 | 72 | $project = new BitbucketProject('Stash', $repository); 73 | $this->assertEquals('master', $project->getBranch()); 74 | $this->assertEquals($repository, $project->getRepository()); 75 | $this->assertEquals('https://bitbucket.org/atlassian/stash-example-plugin/commits/%commit%', $project->getUrlPattern()); 76 | 77 | $fs->remove($repository); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/Sismo/Tests/Contrib/CrossFingerNotifierTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Tests\Contrib; 13 | 14 | use Sismo\Notifier\Notifier; 15 | use Sismo\Contrib\CrossFingerNotifier; 16 | use Sismo\Commit; 17 | use Sismo\Project; 18 | 19 | class CrossFingerNotifierTest extends \PHPUnit_Framework_TestCase 20 | { 21 | /** 22 | * @expectedException InvalidArgumentException 23 | */ 24 | public function testConstruct() 25 | { 26 | $notifier = new CrossFingerNotifier(new \stdClass()); 27 | } 28 | 29 | public function testCommitNeedNotification() 30 | { 31 | $notifier = $this->getMock('Sismo\Contrib\CrossFingerNotifier'); 32 | $r = new \ReflectionObject($notifier); 33 | $m = $r->getMethod('commitNeedNotification'); 34 | $m->setAccessible(true); 35 | 36 | $project = new Project('Twig'); 37 | $commit = new Commit($project, '123456'); 38 | $commit->setAuthor('Fabien'); 39 | $commit->setMessage('Foo'); 40 | 41 | $commit2 = new Commit($project, '123455'); 42 | $commit2->setAuthor('Fabien'); 43 | $commit2->setMessage('Bar'); 44 | $commit2->setStatusCode('success'); 45 | 46 | $commit3 = clone $commit2; 47 | 48 | //a failed commit should be notified 49 | $this->assertTrue($m->invoke($notifier, $commit)); 50 | 51 | //a successful commit without predecessor should be notified 52 | $this->assertTrue($m->invoke($notifier, $commit2)); 53 | 54 | $project->setCommits(array( 55 | $commit3, 56 | )); 57 | //a successful commit with a successful predecessor should NOT be notified 58 | $this->assertFalse($m->invoke($notifier, $commit2)); 59 | 60 | $project->setCommits(array( 61 | $commit2, 62 | $commit3, 63 | )); 64 | //a failed commit with a successful predecessor should be notified 65 | $this->assertTrue($m->invoke($notifier, $commit)); 66 | } 67 | 68 | public function testNotify() 69 | { 70 | $project = new Project('Twig'); 71 | $failedCommit = new Commit($project, '123456'); 72 | $failedCommit->setAuthor('Fabien'); 73 | $failedCommit->setMessage('Foo'); 74 | 75 | $successCommit = new Commit($project, '123455'); 76 | $successCommit->setAuthor('Fabien'); 77 | $successCommit->setMessage('Bar'); 78 | $successCommit->setStatusCode('success'); 79 | 80 | $baseNotifier = $this->getMock('Sismo\Notifier\Notifier'); 81 | $baseNotifier->expects($this->once()) 82 | ->method('notify') 83 | ->will($this->returnValue('foo')); 84 | 85 | $notifier = new CrossFingerNotifier(array($baseNotifier)); 86 | 87 | //a failed commit should call notify on real notifier 88 | $this->assertTrue($notifier->notify($failedCommit)); 89 | 90 | $project->setCommits(array( 91 | $successCommit, 92 | )); 93 | //a success commit should not call notify on real notifier 94 | $this->assertFalse($notifier->notify($successCommit)); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Sismo/Contrib/IrcNotifier.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Contrib; 13 | 14 | use Sismo\Notifier\Notifier; 15 | use Sismo\Commit; 16 | 17 | /** 18 | * Delivers a connection via socket to the IRC server. 19 | * Extends Sismo\Notifier\Notifier for notifying users via Irc 20 | * 21 | * @package IRCBot 22 | * @subpackage Library 23 | * @author Daniel Siepmann 24 | * @author Brent Shaffer 25 | */ 26 | class IrcNotifier extends Notifier 27 | { 28 | private $server; 29 | private $port; 30 | private $nick; 31 | private $channel; 32 | private $format; 33 | 34 | /** 35 | * Example: 36 | * // basic usage 37 | * $irc = new Sismo\Contrib\IrcNotifier('#mychannel'); 38 | * 39 | * // more advanced usage 40 | * $irc = new Sismo\Contrib\IrcNotifier('#mychannel', 'sismo-bot', 'chat.mysite.com', '6668'); 41 | */ 42 | public function __construct($channel, $nick = 'Sismo', $server = 'irc.freenode.com', $port = 6667, $format = '[%STATUS%] %name% %short_sha% -- %message% by %author%') 43 | { 44 | $this->server = $server; 45 | $this->port = $port; 46 | $this->nick = $nick; 47 | $this->channel = $channel; 48 | $this->format = $format; 49 | } 50 | 51 | public function notify(Commit $commit) 52 | { 53 | $old = error_reporting(0); 54 | $this->connect(); 55 | $channels = explode(',', $this->channel); 56 | $this->join($channels); 57 | foreach ($channels as $channel) { 58 | $this->say($channel, $this->format($this->format, $commit)); 59 | } 60 | $this->disconnect(); 61 | error_reporting($old); 62 | } 63 | 64 | private function say($channel, $message) 65 | { 66 | $this->sendData(sprintf('PRIVMSG %s :%s', $channel, $message)); 67 | } 68 | 69 | private function connect() 70 | { 71 | $this->socket = fsockopen($this->server, $this->port); 72 | if (!$this->isConnected()) { 73 | throw new \RuntimeException('Unable to connect to server via fsockopen with server: "'.$this->server.'" and port: "'.$this->port.'".'); 74 | } 75 | // USER username hostname servername :realname 76 | $this->sendData(sprintf('USER %s Sismo Sismo :%s', $this->nick, $this->nick)); 77 | $this->sendData(sprintf('NICK %s', $this->nick)); 78 | } 79 | 80 | private function disconnect() 81 | { 82 | if ($this->socket) { 83 | return fclose($this->socket); 84 | } 85 | 86 | return false; 87 | } 88 | 89 | private function sendData($data) 90 | { 91 | return fwrite($this->socket, $data."\r\n"); 92 | } 93 | 94 | private function isConnected() 95 | { 96 | if (is_resource($this->socket)) { 97 | return true; 98 | } 99 | 100 | return false; 101 | } 102 | 103 | private function join($channel) 104 | { 105 | foreach ((array) $channel as $chan) { 106 | $this->sendData(sprintf('JOIN %s', $chan)); 107 | } 108 | } 109 | 110 | public function __destruct() 111 | { 112 | $this->disconnect(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Sismo/Contrib/GrowlNotifier.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Contrib; 13 | 14 | use Sismo\Notifier\Notifier; 15 | use Sismo\Commit; 16 | 17 | require_once 'Net/Growl/Autoload.php'; 18 | 19 | // @codeCoverageIgnoreStart 20 | /** 21 | * Notifies builds via a Growl (Mac or Windows, but not Linux) 22 | * 23 | * Requires PEAR::Net_Growl package 24 | * 25 | * @author Laurent Laville 26 | * @link http://growl.laurent-laville.org/ 27 | * @link http://pear.php.net/package/Net_Growl 28 | */ 29 | class GrowlNotifier extends Notifier 30 | { 31 | const NOTIFY_SUCCESS = 'Success'; 32 | const NOTIFY_FAILURE = 'Fail'; 33 | 34 | /** 35 | * Net_Growl instance 36 | * @var object 37 | */ 38 | private $growl; 39 | 40 | /** 41 | * Message format with predefined place holders 42 | * @var string 43 | * @see Sismo\Notifier\Notifier::getPlaceholders() for known place holders 44 | */ 45 | private $format; 46 | 47 | /** 48 | * Class constructor 49 | * 50 | * @param string $application (optional) Identify an application by a string 51 | * @param array $notifications (optional) Options to configure the 52 | * notification channels 53 | * @param string $password (optional) Password to protect your Growl client 54 | * for notification spamming 55 | * @param array $options (optional) Options to configure the Growl comm. 56 | * Choose either UDP or GNTP protocol, 57 | * host URL, and more ... 58 | */ 59 | public function __construct($application = 'sismo', $notifications = array(), 60 | $password = '', $options = array() 61 | ) { 62 | $this->format = "[%STATUS%]\n%message%\n%author%"; 63 | 64 | $notifications = array_merge( 65 | // default notifications (channels Success and Fail are enabled) 66 | array( 67 | self::NOTIFY_SUCCESS => array(), 68 | self::NOTIFY_FAILURE => array(), 69 | ), 70 | // custom notifications 71 | $notifications 72 | ); 73 | 74 | $this->growl = \Net_Growl::singleton( 75 | $application, $notifications, $password, $options 76 | ); 77 | } 78 | 79 | /** 80 | * Defines the new message format 81 | * 82 | * @param string $format The message format with predefined place holders 83 | * 84 | * @return $this 85 | * @see Sismo\Notifier\Notifier::getPlaceholders() for known place holders 86 | */ 87 | public function setMessageFormat($format) 88 | { 89 | if (is_string($format) && !empty($format)) { 90 | $this->format = $format; 91 | } 92 | 93 | return $this; 94 | } 95 | 96 | /** 97 | * Notify a project commit 98 | * 99 | * @param Sismo\Commit $commit The latest project commit 100 | * 101 | * @return bool TRUE on a succesfull notification, FALSE on failure 102 | */ 103 | public function notify(Commit $commit) 104 | { 105 | try { 106 | $this->growl->register(); 107 | 108 | $name = $commit->isSuccessful() 109 | ? self::NOTIFY_SUCCESS : self::NOTIFY_FAILURE; 110 | $notifications = $this->growl->getApplication()->getGrowlNotifications(); 111 | 112 | $this->growl->publish( 113 | $name, 114 | $commit->getProject()->getName(), 115 | $this->format($this->format, $commit), 116 | $notifications[$name] 117 | ); 118 | } catch (\Net_Growl_Exception $e) { 119 | return false; 120 | } 121 | 122 | return true; 123 | } 124 | } 125 | // @codeCoverageIgnoreEnd 126 | -------------------------------------------------------------------------------- /tests/consoleTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | use Sismo\Project; 13 | use Symfony\Component\Console\Tester\ApplicationTester; 14 | use Sismo\BuildException; 15 | 16 | class consoleTest extends \PHPUnit_Framework_TestCase 17 | { 18 | protected $app; 19 | protected $console; 20 | protected $baseDir; 21 | 22 | public function setUp() 23 | { 24 | $this->app = require __DIR__.'/../src/app.php'; 25 | 26 | $this->app['sismo'] = $this->getMockBuilder('Sismo\Sismo')->disableOriginalConstructor()->getMock(); 27 | 28 | $this->console = require __DIR__.'/../src/console.php'; 29 | $this->console->setAutoExit(false); 30 | $this->console->setCatchExceptions(false); 31 | } 32 | 33 | public function testBuildForNonExistentProject() 34 | { 35 | $tester = new ApplicationTester($this->console); 36 | 37 | $this->assertEquals(1, $tester->run(array('command' => 'build', 'slug' => 'Twig'))); 38 | $this->assertEquals('Project "Twig" does not exist.', trim($tester->getDisplay())); 39 | } 40 | 41 | public function testBuildForProject() 42 | { 43 | $project = $this->getMockBuilder('Sismo\Project')->disableOriginalConstructor()->getMock(); 44 | 45 | $this->app['sismo']->expects($this->once())->method('hasProject')->will($this->returnValue(true)); 46 | $this->app['sismo']->expects($this->once())->method('getProject')->will($this->returnValue($project)); 47 | $this->app['sismo']->expects($this->once())->method('build'); 48 | 49 | $tester = new ApplicationTester($this->console); 50 | 51 | $this->assertEquals(0, $tester->run(array('command' => 'build', 'slug' => 'Twig'))); 52 | $this->assertEquals('Building Project "" (into "d41d8c")', trim($tester->getDisplay())); 53 | } 54 | 55 | public function testBuildForProjects() 56 | { 57 | $project1 = $this->getMock('Sismo\Project', null, array('Twig')); 58 | $project2 = $this->getMock('Sismo\Project', null, array('Silex')); 59 | 60 | $this->app['sismo']->expects($this->once())->method('getProjects')->will($this->returnValue(array($project1, $project2))); 61 | $this->app['sismo']->expects($this->exactly(2))->method('build'); 62 | 63 | $tester = new ApplicationTester($this->console); 64 | 65 | $this->assertEquals(0, $tester->run(array('command' => 'build'))); 66 | $this->assertEquals('Building Project "Twig" (into "eb0a19")'.PHP_EOL.PHP_EOL.'Building Project "Silex" (into "eb0a19")', trim($tester->getDisplay())); 67 | } 68 | 69 | public function testBuildForProjectsWithBuildExceptions() 70 | { 71 | $project1 = $this->getMock('Sismo\Project', null, array('Twig')); 72 | $project2 = $this->getMock('Sismo\Project', null, array('Silex')); 73 | 74 | $this->app['sismo']->expects($this->once())->method('getProjects')->will($this->returnValue(array($project1, $project2))); 75 | $this->app['sismo']->expects($this->exactly(2))->method('build')->will($this->throwException(new BuildException())); 76 | 77 | $tester = new ApplicationTester($this->console); 78 | $this->assertEquals(1, $tester->run(array('command' => 'build'))); 79 | } 80 | 81 | public function testVerboseBuildForProject() 82 | { 83 | $project = $this->getMockBuilder('Sismo\Project')->disableOriginalConstructor()->getMock(); 84 | 85 | $this->app['sismo']->expects($this->once())->method('hasProject')->will($this->returnValue(true)); 86 | $this->app['sismo']->expects($this->once())->method('getProject')->will($this->returnValue($project)); 87 | $this->app['sismo']->expects($this->once())->method('build'); 88 | 89 | $tester = new ApplicationTester($this->console); 90 | 91 | $this->assertEquals(0, $tester->run(array('command' => 'build', 'slug' => 'Twig', '--verbose' => true))); 92 | $this->assertEquals('Building Project "" (into "d41d8c")', trim($tester->getDisplay())); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/Sismo/Tests/Storage/StorageTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Tests\Storage; 13 | 14 | use Sismo\Storage; 15 | use Sismo\Commit; 16 | use Sismo\Project; 17 | 18 | class StorageTest extends \PHPUnit_Framework_TestCase 19 | { 20 | /** 21 | * A SQLite3 reference. 22 | * 23 | * @var \SQLite3 24 | */ 25 | private $db; 26 | private $path; 27 | 28 | public function setUp() 29 | { 30 | $app = require __DIR__.'/../../../../src/app.php'; 31 | 32 | $this->path = sys_get_temp_dir().'/sismo.db'; 33 | @unlink($this->path); 34 | 35 | $this->db = new \SQLite3($this->path); 36 | $this->db->busyTimeout(1000); 37 | $this->db->exec($app['db.schema']); 38 | } 39 | 40 | public function tearDown() 41 | { 42 | @unlink($this->path); 43 | } 44 | 45 | public function testInterfaceIsValid() 46 | { 47 | $project = $this->getProject(); 48 | 49 | $storage = $this->getStorage(); 50 | 51 | $commit = $storage->initCommit($project, '7d78d5', 'fabien', new \DateTime(), 'foo'); 52 | $this->assertInstanceOf('Sismo\Commit', $commit); 53 | 54 | $commit->setMessage('foo, amended with stuff'); 55 | $storage->updateCommit($commit); 56 | 57 | $commit = $storage->getCommit($project, '7d78d5'); 58 | $this->assertInstanceOf('Sismo\Commit', $commit); 59 | } 60 | 61 | public function testGetCommitReturnsFalseIfNotInDatabase() 62 | { 63 | $storage = $this->getStorage(); 64 | $project = $this->getProject(); 65 | $this->assertFalse($storage->getCommit($project, '7d78d5')); 66 | } 67 | 68 | public function testGetCommitReturnsCommitIfInDatabase() 69 | { 70 | $project = $this->getProject(); 71 | 72 | $storage = $this->getStorage(); 73 | $storage->initCommit($project, '7d78d5', 'fabien', new \DateTime(), 'foo'); 74 | 75 | $commit = $storage->getCommit($project, '7d78d5'); 76 | $this->assertInstanceOf('Sismo\Commit', $commit); 77 | $this->assertEquals('7d78d5', $commit->getSha()); 78 | } 79 | 80 | public function testUpdateCommit() 81 | { 82 | $project = $this->getProject(); 83 | 84 | $storage = $this->getStorage(); 85 | $storage->initCommit($project, '7d78d5', 'fabien', new \DateTime(), 'foo'); 86 | 87 | $commit = new Commit($project, '7d78d5'); 88 | $commit->setOutput('foo'); 89 | $commit->setStatusCode('success'); 90 | 91 | $storage->updateCommit($commit); 92 | 93 | $this->assertEquals('foo', $commit->getOutput()); 94 | $this->assertEquals('success', $commit->getStatusCode()); 95 | 96 | $commit = $storage->getCommit($project, '7d78d5'); 97 | $this->assertNotNull($commit->getBuildDate()); 98 | } 99 | 100 | public function testUpdateProject() 101 | { 102 | $project = new Project('Twig'); 103 | 104 | $storage = $this->getStorage(); 105 | 106 | $storage->updateProject($project); 107 | $this->assertEquals(false, $project->isBuilding()); 108 | $this->assertEquals(array(), $project->getCommits()); 109 | 110 | $commit = $storage->initCommit($project, '7d78d5', 'fabien', new \DateTime(), 'foo'); 111 | $storage->updateProject($project); 112 | $this->assertEquals(true, $project->isBuilding()); 113 | $this->assertEquals(array($commit), $project->getCommits()); 114 | } 115 | 116 | protected function getProject() 117 | { 118 | $project = $this->getMockBuilder('Sismo\Project')->disableOriginalConstructor()->getMock(); 119 | $project->expects($this->any())->method('getSlug')->will($this->returnValue('twig')); 120 | 121 | return $project; 122 | } 123 | 124 | protected function getStorage() 125 | { 126 | return new Storage($this->db); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Sismo/Sismo.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo; 13 | 14 | use Sismo\Storage\StorageInterface; 15 | 16 | /** 17 | * Main entry point for Sismo. 18 | * 19 | * @author Fabien Potencier 20 | */ 21 | class Sismo 22 | { 23 | const VERSION = '1.0.0'; 24 | 25 | const FORCE_BUILD = 1; 26 | const LOCAL_BUILD = 2; 27 | const SILENT_BUILD = 4; 28 | 29 | private $storage; 30 | private $builder; 31 | private $projects = array(); 32 | 33 | /** 34 | * Constructor. 35 | * 36 | * @param StorageInterface $storage A StorageInterface instance 37 | * @param Builder $builder A Builder instance 38 | */ 39 | public function __construct(StorageInterface $storage, Builder $builder) 40 | { 41 | $this->storage = $storage; 42 | $this->builder = $builder; 43 | } 44 | 45 | /** 46 | * Builds a project. 47 | * 48 | * @param Project $project A Project instance 49 | * @param string $revision The revision to build (or null for the latest revision) 50 | * @param integer $flags Flags (a combinaison of FORCE_BUILD, LOCAL_BUILD, and SILENT_BUILD) 51 | * @param mixed $callback A PHP callback 52 | */ 53 | public function build(Project $project, $revision = null, $flags = 0, $callback = null) 54 | { 55 | // project already has a running build 56 | if ($project->isBuilding() && Sismo::FORCE_BUILD !== ($flags & Sismo::FORCE_BUILD)) { 57 | return; 58 | } 59 | 60 | $this->builder->init($project, $callback); 61 | 62 | list($sha, $author, $date, $message) = $this->builder->prepare($revision, Sismo::LOCAL_BUILD !== ($flags & Sismo::LOCAL_BUILD)); 63 | 64 | $commit = $this->storage->getCommit($project, $sha); 65 | 66 | // commit has already been built 67 | if ($commit && $commit->isBuilt() && Sismo::FORCE_BUILD !== ($flags & Sismo::FORCE_BUILD)) { 68 | return; 69 | } 70 | 71 | $commit = $this->storage->initCommit($project, $sha, $author, \DateTime::createFromFormat('Y-m-d H:i:s O', $date), $message); 72 | 73 | $process = $this->builder->build(); 74 | 75 | if (!$process->isSuccessful()) { 76 | $commit->setStatusCode('failed'); 77 | $commit->setOutput(sprintf("\033[31mBuild failed\033[0m\n\n\033[33mOutput\033[0m\n%s\n\n\033[33m Error\033[0m%s", $process->getOutput(), $process->getErrorOutput())); 78 | } else { 79 | $commit->setStatusCode('success'); 80 | $commit->setOutput($process->getOutput()); 81 | } 82 | 83 | $this->storage->updateCommit($commit); 84 | 85 | if (Sismo::SILENT_BUILD !== ($flags & Sismo::SILENT_BUILD)) { 86 | foreach ($project->getNotifiers() as $notifier) { 87 | $notifier->notify($commit); 88 | } 89 | } 90 | } 91 | 92 | /** 93 | * Checks if Sismo knows about a given project. 94 | * 95 | * @param string $slug A project slug 96 | */ 97 | public function hasProject($slug) 98 | { 99 | return isset($this->projects[$slug]); 100 | } 101 | 102 | /** 103 | * Gets a project. 104 | * 105 | * @param string $slug A project slug 106 | */ 107 | public function getProject($slug) 108 | { 109 | if (!isset($this->projects[$slug])) { 110 | throw new \InvalidArgumentException(sprintf('Project "%s" does not exist.', $slug)); 111 | } 112 | 113 | return $this->projects[$slug]; 114 | } 115 | 116 | /** 117 | * Adds a project. 118 | * 119 | * @param Project $project A Project instance 120 | */ 121 | public function addProject(Project $project) 122 | { 123 | $this->storage->updateProject($project); 124 | 125 | $this->projects[$project->getSlug()] = $project; 126 | } 127 | 128 | /** 129 | * Gets all projects. 130 | * 131 | * @return array An array of Project instances 132 | */ 133 | public function getProjects() 134 | { 135 | return $this->projects; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/app.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | use Silex\Application; 13 | use Silex\Provider\TwigServiceProvider; 14 | use Silex\Provider\UrlGeneratorServiceProvider; 15 | use Sismo\Sismo; 16 | use Sismo\Project; 17 | use Sismo\Storage\Storage; 18 | use Sismo\Builder; 19 | use Symfony\Component\HttpFoundation\Response; 20 | use SensioLabs\AnsiConverter\AnsiToHtmlConverter; 21 | 22 | $app = new Application(); 23 | $app->register(new UrlGeneratorServiceProvider()); 24 | $app->register(new TwigServiceProvider(), array( 25 | 'twig.path' => __DIR__.'/templates', 26 | )); 27 | $app['twig'] = $app->share($app->extend('twig', function ($twig, $app) { 28 | $twig->setCache($app['twig.cache.path']); 29 | $twig->addGlobal('ansi_to_html', new AnsiToHtmlConverter()); 30 | 31 | return $twig; 32 | })); 33 | 34 | $app['data.path'] = getenv('SISMO_DATA_PATH') ?: getenv('HOME').'/.sismo/data'; 35 | $app['config.file'] = getenv('SISMO_CONFIG_PATH') ?: getenv('HOME').'/.sismo/config.php'; 36 | $app['config.storage.file'] = getenv('SISMO_STORAGE_PATH') ?: getenv('HOME').'/.sismo/storage.php'; 37 | $app['build.path'] = $app->share(function ($app) { return $app['data.path'].'/build'; }); 38 | $app['db.path'] = $app->share(function ($app) { 39 | if (!is_dir($app['data.path'])) { 40 | mkdir($app['data.path'], 0777, true); 41 | } 42 | 43 | return $app['data.path'].'/sismo.db'; 44 | }); 45 | $app['build.token'] = getenv('SISMO_BUILD_TOKEN'); 46 | $app['twig.cache.path'] = $app->share(function ($app) { return $app['data.path'].'/cache'; }); 47 | $app['git.path'] = getenv('SISMO_GIT_PATH') ?: 'git'; 48 | $app['git.cmds'] = array(); 49 | $app['db.schema'] = <<share(function () use ($app) { 75 | $db = new \SQLite3($app['db.path']); 76 | $db->busyTimeout(1000); 77 | $db->exec($app['db.schema']); 78 | 79 | return $db; 80 | }); 81 | 82 | $app['storage'] = $app->share(function () use ($app) { 83 | if (is_file($app['config.storage.file'])) { 84 | $storage = require $app['config.storage.file']; 85 | } else { 86 | $storage = new Storage($app['db']); 87 | } 88 | 89 | return $storage; 90 | }); 91 | 92 | $app['builder'] = $app->share(function () use ($app) { 93 | return new Builder($app['build.path'], $app['git.path'], $app['git.cmds']); 94 | }); 95 | 96 | $app['sismo'] = $app->share(function () use ($app) { 97 | $sismo = new Sismo($app['storage'], $app['builder']); 98 | if (!is_file($app['config.file'])) { 99 | throw new \RuntimeException(sprintf("Looks like you forgot to define your projects.\nSismo looked into \"%s\".", $app['config.file'])); 100 | } 101 | $projects = require $app['config.file']; 102 | 103 | if (null === $projects) { 104 | throw new \RuntimeException(sprintf('The "%s" configuration file must return an array of Projects (returns null).', $app['config.file'])); 105 | } 106 | 107 | if (!is_array($projects)) { 108 | throw new \RuntimeException(sprintf('The "%s" configuration file must return an array of Projects (returns a non-array).', $app['config.file'])); 109 | } 110 | 111 | foreach ($projects as $project) { 112 | if (!$project instanceof Project) { 113 | throw new \RuntimeException(sprintf('The "%s" configuration file must return an array of Project instances.', $app['config.file'])); 114 | } 115 | 116 | $sismo->addProject($project); 117 | } 118 | 119 | return $sismo; 120 | }); 121 | 122 | $app->error(function (\Exception $e, $code) use ($app) { 123 | if ($app['debug']) { 124 | return; 125 | } 126 | 127 | $error = 404 == $code ? $e->getMessage() : null; 128 | 129 | return new Response($app['twig']->render('error.twig', array('error' => $error)), $code); 130 | }); 131 | 132 | return $app; 133 | -------------------------------------------------------------------------------- /tests/Sismo/Tests/CommitTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Tests; 13 | 14 | use Sismo\Project; 15 | use Sismo\Commit; 16 | 17 | class CommitTest extends \PHPUnit_Framework_TestCase 18 | { 19 | public function testToString() 20 | { 21 | $commit = new Commit(new Project('Twig'), '7d78d5f7a8c039059046d6c5e1d7f66765bd91c7'); 22 | $this->assertEquals('Twig@7d78d5', (string) $commit); 23 | } 24 | 25 | public function testSha() 26 | { 27 | $commit = new Commit(new Project('Twig'), '7d78d5f7a8c039059046d6c5e1d7f66765bd91c7'); 28 | $this->assertEquals('7d78d5f7a8c039059046d6c5e1d7f66765bd91c7', $commit->getSha()); 29 | $this->assertEquals('7d78d5', $commit->getShortSha()); 30 | } 31 | 32 | public function testStatus() 33 | { 34 | $commit = new Commit(new Project('Twig'), '7d78d5f7a8c039059046d6c5e1d7f66765bd91c7'); 35 | 36 | $this->assertEquals('building', $commit->getStatusCode()); 37 | $this->assertEquals('building', $commit->getStatus()); 38 | 39 | $commit->setStatusCode('failed'); 40 | $this->assertEquals('failed', $commit->getStatusCode()); 41 | $this->assertEquals('failed', $commit->getStatus()); 42 | 43 | $commit->setStatusCode('success'); 44 | $this->assertEquals('success', $commit->getStatusCode()); 45 | $this->assertEquals('succeeded', $commit->getStatus()); 46 | } 47 | 48 | /** 49 | * @expectedException \InvalidArgumentException 50 | */ 51 | public function testSetStatus() 52 | { 53 | $commit = new Commit(new Project('Twig'), '7d78d5f7a8c039059046d6c5e1d7f66765bd91c7'); 54 | $commit->setStatusCode('weird'); 55 | } 56 | 57 | public function testIsBuilding() 58 | { 59 | $commit = new Commit(new Project('Twig'), '7d78d5f7a8c039059046d6c5e1d7f66765bd91c7'); 60 | $this->assertTrue($commit->isBuilding()); 61 | 62 | $commit->setStatusCode('failed'); 63 | $this->assertFalse($commit->isBuilding()); 64 | } 65 | 66 | public function testIsBuilt() 67 | { 68 | $commit = new Commit(new Project('Twig'), '7d78d5f7a8c039059046d6c5e1d7f66765bd91c7'); 69 | $this->assertFalse($commit->isBuilt()); 70 | 71 | $commit->setStatusCode('failed'); 72 | $this->assertTrue($commit->isBuilt()); 73 | 74 | $commit->setStatusCode('success'); 75 | $this->assertTrue($commit->isBuilt()); 76 | } 77 | 78 | public function testIsSuccessful() 79 | { 80 | $commit = new Commit(new Project('Twig'), '7d78d5f7a8c039059046d6c5e1d7f66765bd91c7'); 81 | $this->assertFalse($commit->isSuccessful()); 82 | 83 | $commit->setStatusCode('failed'); 84 | $this->assertFalse($commit->isSuccessful()); 85 | 86 | $commit->setStatusCode('success'); 87 | $this->assertTrue($commit->isSuccessful()); 88 | } 89 | 90 | public function testOutput() 91 | { 92 | $commit = new Commit(new Project('Twig'), '7d78d5f7a8c039059046d6c5e1d7f66765bd91c7'); 93 | $commit->setOutput('foo'); 94 | $this->assertEquals('foo', $commit->getOutput()); 95 | } 96 | 97 | public function testMessage() 98 | { 99 | $commit = new Commit(new Project('Twig'), '7d78d5f7a8c039059046d6c5e1d7f66765bd91c7'); 100 | $commit->setMessage('foo'); 101 | $this->assertEquals('foo', $commit->getMessage()); 102 | } 103 | 104 | public function testProject() 105 | { 106 | $commit = new Commit($project = new Project('Twig'), '7d78d5f7a8c039059046d6c5e1d7f66765bd91c7'); 107 | $this->assertEquals($project, $commit->getProject()); 108 | } 109 | 110 | public function testAuthor() 111 | { 112 | $commit = new Commit(new Project('Twig'), '7d78d5f7a8c039059046d6c5e1d7f66765bd91c7'); 113 | $commit->setAuthor('foo'); 114 | $this->assertEquals('foo', $commit->getAuthor()); 115 | } 116 | 117 | public function testDate() 118 | { 119 | $commit = new Commit(new Project('Twig'), '7d78d5f7a8c039059046d6c5e1d7f66765bd91c7'); 120 | $commit->setDate($date = new \DateTime()); 121 | $this->assertEquals($date, $commit->getDate()); 122 | } 123 | 124 | public function testBuildDate() 125 | { 126 | $commit = new Commit(new Project('Twig'), '7d78d5f7a8c039059046d6c5e1d7f66765bd91c7'); 127 | $commit->setBuildDate($date = new \DateTime()); 128 | $this->assertEquals($date, $commit->getBuildDate()); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Sismo/Contrib/AndroidPushC2DMNotifier.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Contrib; 13 | 14 | use Sismo\Commit; 15 | 16 | // @codeCoverageIgnoreStart 17 | /** 18 | * A base C2DM (Cloud 2 Device Messaging) function that will send a small push text message to an 19 | * Android device via Google service. 20 | * 21 | * Here is a usage example: 22 | * 23 | * $messageFormat = '[%status_code%] %name% (%short_sha%)'; 24 | * $c2dmNotifier = new \Sismo\Notifier\AndroidPushC2DMNotifier('C2DM_PUSH_USER', 'C2DM_PUSH_PASSWORD', 25 | * 'DEVICE_REGISTRATION_ID', $messageFormat, 'SismoNotifier'); 26 | * 27 | * 28 | * @author Michael Kliewe 29 | */ 30 | class AndroidPushC2DMNotifier extends Notifier 31 | { 32 | protected $authCode; 33 | protected $deviceRegistrationId; 34 | protected $messageFormat; 35 | protected $msgType; 36 | 37 | /** 38 | * Constructor 39 | * 40 | * @param string $username 41 | * @param string $password 42 | * @param string $deviceRegistrationId 43 | * @param string $messageFormat 44 | * @param string $source 45 | * @param string $msgType 46 | * @param string $service 47 | */ 48 | public function __construct($username, $password, $deviceRegistrationId, $messageFormat = '', 49 | $source = 'Company-AppName-Version', $msgType = 'SismoNotifierMsgType', $service = 'ac2dm') 50 | { 51 | $this->authCode = $this->getGoogleAuthCodeHelper($username, $password, $source, $service); 52 | $this->deviceRegistrationId = $deviceRegistrationId; 53 | $this->messageFormat = $messageFormat; 54 | $this->msgType = $msgType; 55 | } 56 | 57 | public function notify(Commit $commit) 58 | { 59 | $message = $this->format($this->messageFormat, $commit); 60 | 61 | $headers = array('Authorization: GoogleLogin auth='.$this->authCode); 62 | $data = array( 63 | 'registration_id' => $this->deviceRegistrationId, 64 | 'collapse_key' => $this->msgType, 65 | 'data.message' => $message, 66 | ); 67 | 68 | $ch = curl_init(); 69 | 70 | curl_setopt($ch, CURLOPT_URL, "https://android.apis.google.com/c2dm/send"); 71 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 72 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 73 | curl_setopt($ch, CURLOPT_POST, true); 74 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 75 | curl_setopt($ch, CURLOPT_POSTFIELDS, $data); 76 | 77 | $response = curl_exec($ch); 78 | curl_close($ch); 79 | 80 | // Check the response. If the exact error message is needed it can be parsed here 81 | $responseArray = preg_split('/=/', $response); 82 | if (!isset($responseArray[0]) || !isset($responseArray[1])) { 83 | return false; 84 | } 85 | if (strtolower($responseArray[0]) == 'error') { 86 | return false; 87 | } 88 | 89 | return true; 90 | } 91 | 92 | public function getGoogleAuthCodeHelper($username, $password, $source = 'Company-AppName-Version', $service = 'ac2dm') 93 | { 94 | $ch = curl_init(); 95 | if (!$ch) { 96 | return false; 97 | } 98 | 99 | curl_setopt($ch, CURLOPT_URL, "https://www.google.com/accounts/ClientLogin"); 100 | $postFields = "accountType=".urlencode('HOSTED_OR_GOOGLE') 101 | ."&Email=".urlencode($username) 102 | ."&Passwd=".urlencode($password) 103 | ."&source=".urlencode($source) 104 | ."&service=".urlencode($service); 105 | curl_setopt($ch, CURLOPT_HEADER, true); 106 | curl_setopt($ch, CURLOPT_POST, true); 107 | curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields); 108 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 109 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); 110 | curl_setopt($ch, CURLOPT_FRESH_CONNECT, true); 111 | curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_ANY); 112 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 113 | 114 | $response = curl_exec($ch); 115 | 116 | curl_close($ch); 117 | 118 | if (strpos($response, '200 OK') === false) { 119 | return false; 120 | } 121 | 122 | // find the auth code 123 | preg_match("/(Auth=)([\w|-]+)/", $response, $matches); 124 | 125 | if (!$matches[2]) { 126 | return false; 127 | } 128 | 129 | return $matches[2]; 130 | } 131 | } 132 | // @codeCoverageIgnoreEnd 133 | -------------------------------------------------------------------------------- /src/Sismo/Contrib/GithubNotifier.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Contrib; 13 | 14 | use Sismo\Commit; 15 | use Sismo\Notifier\Notifier; 16 | 17 | // @codeCoverageIgnoreStart 18 | /** 19 | * Class GithubNotifier 20 | * @package Sismo\Contrib 21 | */ 22 | class GithubNotifier extends Notifier 23 | { 24 | /** @var string */ 25 | protected $apikey; 26 | 27 | /** @var string */ 28 | protected $host = 'https://api.github.com'; 29 | 30 | /** @var string */ 31 | protected $repo; 32 | 33 | /** @var string */ 34 | protected $context = 'fabpot/sismo'; 35 | 36 | /** @var string */ 37 | protected $description = "Sismo"; 38 | 39 | /** @var string */ 40 | protected $targetUrlPattern = null; 41 | 42 | /** 43 | * @param string $apikey personal API key 44 | * @param string $repo repository name, e.g. fabpot/Sismo 45 | * @param string $targetUrlPattern status target URL pattern, e.g. http://sismo/%slug%/%sha% 46 | */ 47 | public function __construct($apikey, $repo, $targetUrlPattern = null) 48 | { 49 | $this->apikey = $apikey; 50 | $this->repo = trim($repo, '/'); 51 | 52 | $this->setTargetUrlPattern($targetUrlPattern); 53 | } 54 | 55 | /** 56 | * @param string $context 57 | * 58 | * @return $this 59 | */ 60 | public function setContext($context) 61 | { 62 | $this->context = $context; 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * @param string $description 69 | * 70 | * @return $this 71 | */ 72 | public function setDescription($description) 73 | { 74 | $this->description = $description; 75 | 76 | return $this; 77 | } 78 | 79 | /** 80 | * @param string $host 81 | * 82 | * @return $this 83 | */ 84 | public function setHost($host) 85 | { 86 | $this->host = rtrim($host, '/'); 87 | 88 | return $this; 89 | } 90 | 91 | /** 92 | * @param string $targetUrlPattern 93 | * 94 | * @return $this 95 | */ 96 | public function setTargetUrlPattern($targetUrlPattern) 97 | { 98 | $this->targetUrlPattern = $targetUrlPattern; 99 | 100 | return $this; 101 | } 102 | 103 | /** 104 | * Notifies a commit. 105 | * 106 | * @param Commit $commit A Commit instance 107 | */ 108 | public function notify(Commit $commit) 109 | { 110 | $this->request( 111 | $this->getStatusEndpointUrl($commit), 112 | array( 113 | 'state' => $this->getGitHubState($commit), 114 | 'target_url' => $this->format($this->targetUrlPattern, $commit), 115 | 'description' => $this->format($this->description, $commit), 116 | 'context' => $this->context, 117 | ), 118 | $this->getGitHubHeaders() 119 | ); 120 | } 121 | 122 | /** 123 | * @param Commit $commit 124 | * 125 | * @return string 126 | */ 127 | protected function getStatusEndpointUrl(Commit $commit) 128 | { 129 | return $this->host."/repos/".$this->repo."/statuses/".$commit->getSha(); 130 | } 131 | 132 | /** 133 | * @param Commit $commit 134 | * 135 | * @return string 136 | */ 137 | protected function getGitHubState(Commit $commit) 138 | { 139 | switch ($commit->getStatusCode()) { 140 | case 'building': 141 | return 'pending'; 142 | case 'success': 143 | return 'success'; 144 | case 'failed': 145 | return 'failure'; 146 | default: 147 | return 'error'; 148 | } 149 | } 150 | 151 | /** 152 | * @return array 153 | */ 154 | protected function getGitHubHeaders() 155 | { 156 | return array( 157 | "User-Agent: Sismo GitHub notifier", 158 | "Authorization: Basic ".base64_encode($this->apikey.":x-oauth-basic"), 159 | ); 160 | } 161 | 162 | /** 163 | * @param $url 164 | * @param $data 165 | * @param array $headers 166 | * 167 | * @return bool 168 | */ 169 | protected function request($url, $data, array $headers = array()) 170 | { 171 | $ch = curl_init(); 172 | 173 | curl_setopt($ch, CURLOPT_URL, $url); 174 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 175 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 176 | curl_setopt($ch, CURLOPT_POST, true); 177 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 178 | curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); 179 | 180 | curl_exec($ch); 181 | $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); 182 | curl_close($ch); 183 | 184 | return floor($statusCode / 100) == 2; 185 | } 186 | } 187 | // @codeCoverageIgnoreEnd 188 | -------------------------------------------------------------------------------- /src/Sismo/Builder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo; 13 | 14 | use Symfony\Component\Process\Process; 15 | 16 | // @codeCoverageIgnoreStart 17 | /** 18 | * Builds commits. 19 | * 20 | * @author Fabien Potencier 21 | */ 22 | class Builder 23 | { 24 | private $project; 25 | private $baseBuildDir; 26 | private $buildDir; 27 | private $callback; 28 | private $gitPath; 29 | private $gitCmds; 30 | 31 | public function __construct($buildDir, $gitPath, array $gitCmds) 32 | { 33 | $this->baseBuildDir = $buildDir; 34 | $this->gitPath = $gitPath; 35 | $this->gitCmds = array_replace(array( 36 | 'clone' => 'clone --progress --recursive %repo% %dir% --branch %localbranch%', 37 | 'fetch' => 'fetch origin', 38 | 'prepare' => 'submodule update --init --recursive', 39 | 'checkout' => 'checkout -q -f %branch%', 40 | 'reset' => 'reset --hard %revision%', 41 | 'show' => 'show -s --pretty=format:%format% %revision%', 42 | ), $gitCmds); 43 | } 44 | 45 | public function init(Project $project, $callback = null) 46 | { 47 | $process = new Process(sprintf('%s --version', $this->gitPath)); 48 | if ($process->run() > 0) { 49 | throw new \RuntimeException(sprintf('The git binary cannot be found (%s).', $this->gitPath)); 50 | } 51 | 52 | $this->project = $project; 53 | $this->callback = $callback; 54 | $this->buildDir = $this->baseBuildDir.'/'.$this->getBuildDir($project); 55 | } 56 | 57 | public function build() 58 | { 59 | file_put_contents($this->buildDir.'/sismo-run-tests.sh', str_replace(array("\r\n", "\r"), "\n", $this->project->getCommand())); 60 | 61 | $process = new Process('sh sismo-run-tests.sh', $this->buildDir); 62 | $process->setTimeout(3600); 63 | $process->run($this->callback); 64 | 65 | return $process; 66 | } 67 | 68 | public function getBuildDir(Project $project) 69 | { 70 | return substr(md5($project->getRepository().$project->getBranch()), 0, 6); 71 | } 72 | 73 | public function prepare($revision, $sync) 74 | { 75 | if (!file_exists($this->buildDir)) { 76 | mkdir($this->buildDir, 0777, true); 77 | } 78 | 79 | if (!file_exists($this->buildDir.'/.git')) { 80 | $this->execute($this->getGitCommand('clone'), sprintf('Unable to clone repository for project "%s".', $this->project)); 81 | } 82 | 83 | if ($sync) { 84 | $this->execute($this->gitPath.' '.$this->gitCmds['fetch'], sprintf('Unable to fetch repository for project "%s".', $this->project)); 85 | } 86 | 87 | $this->execute($this->getGitCommand('checkout'), sprintf('Unable to checkout branch "%s" for project "%s".', $this->project->getBranch(), $this->project)); 88 | 89 | if ($sync) { 90 | $this->execute($this->gitPath.' '.$this->gitCmds['prepare'], sprintf('Unable to update submodules for project "%s".', $this->project)); 91 | } 92 | 93 | if (null === $revision || 'HEAD' === $revision) { 94 | $revision = null; 95 | if (file_exists($file = $this->buildDir.'/.git/HEAD')) { 96 | $revision = trim(file_get_contents($file)); 97 | if (0 === strpos($revision, 'ref: ')) { 98 | if (file_exists($file = $this->buildDir.'/.git/'.substr($revision, 5))) { 99 | $revision = trim(file_get_contents($file)); 100 | } else { 101 | $revision = null; 102 | } 103 | } 104 | } 105 | 106 | if (null === $revision) { 107 | throw new BuildException(sprintf('Unable to get HEAD for branch "%s" for project "%s".', $this->project->getBranch(), $this->project)); 108 | } 109 | } 110 | 111 | $this->execute($this->getGitCommand('reset', array('%revision%' => escapeshellarg($revision))), sprintf('Revision "%s" for project "%s" does not exist.', $revision, $this->project)); 112 | 113 | $process = $this->execute($this->getGitCommand('show', array('%revision%' => escapeshellarg($revision))), sprintf('Unable to get logs for project "%s".', $this->project)); 114 | 115 | return explode("\n", trim($process->getOutput()), 4); 116 | } 117 | 118 | protected function getGitCommand($command, array $replace = array()) 119 | { 120 | $replace = array_merge(array( 121 | '%repo%' => escapeshellarg($this->project->getRepository()), 122 | '%dir%' => escapeshellarg($this->buildDir), 123 | '%branch%' => escapeshellarg('origin/'.$this->project->getBranch()), 124 | '%localbranch%' => escapeshellarg($this->project->getBranch()), 125 | '%format%' => '"%H%n%an%n%ci%n%s%n"', 126 | ), $replace); 127 | 128 | return strtr($this->gitPath.' '.$this->gitCmds[$command], $replace); 129 | } 130 | 131 | private function execute($command, $message) 132 | { 133 | if (null !== $this->callback) { 134 | call_user_func($this->callback, 'out', sprintf("Running \"%s\"\n", $command)); 135 | } 136 | $process = new Process($command, $this->buildDir); 137 | $process->setTimeout(3600); 138 | $process->run($this->callback); 139 | if (!$process->isSuccessful()) { 140 | throw new BuildException($message); 141 | } 142 | 143 | return $process; 144 | } 145 | } 146 | // @codeCoverageIgnoreEnd 147 | 148 | -------------------------------------------------------------------------------- /web/css/sismo.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2009, Yahoo! Inc. All rights reserved. 3 | Code licensed under the BSD License: 4 | http://developer.yahoo.net/yui/license.txt 5 | version: 2.7.0 6 | */ 7 | html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var,optgroup{font-style:inherit;font-weight:inherit;}del,ins{text-decoration:none;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:baseline;}sub{vertical-align:baseline;}legend{color:#000;}input,button,textarea,select,optgroup,option{font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;}input,button,textarea,select{*font-size:100%;} 8 | 9 | /* Sismo CSS */ 10 | html { 11 | background-color: #182d33; 12 | } 13 | 14 | body { 15 | font-family: Georgia, serif; 16 | color: #fff; 17 | font-size: 16px; 18 | } 19 | 20 | em { 21 | font-style: italic; 22 | } 23 | 24 | strong { 25 | font-weight: bold; 26 | } 27 | 28 | h1 { 29 | font-size: 30px; 30 | margin-top: 30px; 31 | margin-bottom: 10px; 32 | } 33 | 34 | h1 a { 35 | color: #fff; 36 | } 37 | 38 | h2 { 39 | font-size: 26px; 40 | } 41 | 42 | #builds h2 { 43 | font-size: 21px; 44 | } 45 | 46 | #content, footer { 47 | width: 780px; 48 | margin: 0 auto; 49 | background: #274751; 50 | padding: 15px 24px; 51 | } 52 | 53 | #content { 54 | padding-top: 160px; 55 | background: #274751 url(../images/header.png) no-repeat right top; 56 | } 57 | 58 | a { 59 | text-decoration: none; 60 | } 61 | 62 | a:hover { 63 | text-decoration: underline; 64 | } 65 | 66 | .success { 67 | background: url(../images/success.png) repeat-y !important; 68 | } 69 | 70 | .failed { 71 | background: url(../images/failed.png) repeat-y !important; 72 | } 73 | 74 | .success a, .failed a { 75 | color: #fff !important; 76 | } 77 | 78 | #projects { 79 | margin: 15px 0; 80 | margin-top: 30px; 81 | } 82 | 83 | #projects li { 84 | position: relative; 85 | padding: 12px; 86 | margin-bottom: 5px; 87 | font-size: 30px; 88 | vertical-align: bottom; 89 | border: 3px solid #ccc; 90 | } 91 | 92 | #projects li a { 93 | color: #555; 94 | } 95 | 96 | #build { 97 | padding: 10px 15px; 98 | margin-bottom: 15px; 99 | border: 3px solid #ccc; 100 | background: url(../images/nobuild.png) repeat-y; 101 | } 102 | 103 | #build a { 104 | color: #555; 105 | } 106 | 107 | #output { 108 | margin-bottom: 15px; 109 | } 110 | 111 | #output pre { 112 | background: #000; 113 | color: #fff; 114 | padding: 8px; 115 | overflow: auto; 116 | max-height: 300px; 117 | width: 764px; 118 | font-size: 14px; 119 | } 120 | 121 | .meta { 122 | font-size: 13px; 123 | font-family: Georgia, serif; 124 | } 125 | 126 | #builds li { 127 | padding: 8px 12px; 128 | margin-bottom: 5px; 129 | border: 3px solid #ccc; 130 | background: url(../images/nobuild.png) repeat-y; 131 | } 132 | 133 | #builds li a { 134 | color: #fff; 135 | } 136 | 137 | #builds .meta { 138 | font-size: 14px; 139 | } 140 | 141 | .commit { 142 | float: right; 143 | padding-left: 15px; 144 | padding-bottom: 15px; 145 | color: #333 !important; 146 | } 147 | 148 | .commit a { 149 | color: #333 !important; 150 | } 151 | 152 | .status { 153 | float: right; 154 | padding-left: 15px; 155 | padding-bottom: 15px; 156 | color: #333 !important; 157 | font-size: 15px; 158 | } 159 | 160 | footer { 161 | text-align: center; 162 | font-size: 12px; 163 | font-family: Arial; 164 | background: #274751 url(../images/hr.png) no-repeat; 165 | display: block; 166 | } 167 | 168 | footer span { 169 | margin-right: 40px; 170 | } 171 | 172 | footer img { 173 | margin: 0 4px; 174 | vertical-align: middle; 175 | } 176 | 177 | footer a { 178 | color: #fff; 179 | } 180 | 181 | .clearfix:after { content: "\0020"; display: block; height: 0; clear: both; visibility: hidden; overflow: hidden; } 182 | .clearfix { display: inline-block; } 183 | * html .clearfix { height: 1%; } 184 | .clearfix { display: block; } 185 | 186 | #project_command_test { 187 | width: 100%; 188 | height: 150px; 189 | } 190 | 191 | .permalink { 192 | font-size: 60%; 193 | } 194 | 195 | .permalink a { 196 | color: #eee !important; 197 | } 198 | 199 | #back { 200 | float: right; 201 | } 202 | 203 | #back a { 204 | color: #ddd; 205 | } 206 | 207 | #error { 208 | margin-top: 30px; 209 | margin-bottom: 30px; 210 | position: relative; 211 | padding: 12px; 212 | font-size: 22px; 213 | vertical-align: bottom; 214 | border: 3px solid #ccc; 215 | } 216 | 217 | .ansi_color_fg_black { color: black; } 218 | .ansi_color_fg_red { color: red; } 219 | .ansi_color_fg_green { color: green; } 220 | .ansi_color_fg_yellow { color: yellow; } 221 | .ansi_color_fg_blue { color: blue; } 222 | .ansi_color_fg_magenta { color: magenta; } 223 | .ansi_color_fg_cyan { color: cyan; } 224 | .ansi_color_fg_white { color: white; } 225 | .ansi_color_bg_black { background-color: black; padding: 2px 0; } 226 | .ansi_color_bg_red { background-color: red; padding: 2px 0; } 227 | .ansi_color_bg_green { background-color: green; padding: 2px 0; } 228 | .ansi_color_bg_yellow { background-color: yellow; padding: 2px 0; } 229 | .ansi_color_bg_blue { background-color: blue; padding: 2px 0; } 230 | .ansi_color_bg_magenta { background-color: magenta; padding: 2px 0; } 231 | .ansi_color_bg_cyan { background-color: cyan; padding: 2px 0; } 232 | .ansi_color_bg_white { background-color: white; padding: 2px 0; } 233 | -------------------------------------------------------------------------------- /src/Sismo/Commit.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo; 13 | 14 | /** 15 | * Represents a project commit. 16 | * 17 | * @author Fabien Potencier 18 | */ 19 | class Commit 20 | { 21 | private $project; 22 | private $sha; 23 | private $message; 24 | private $author; 25 | private $date; 26 | private $build; 27 | private $output; 28 | private $buildDate; 29 | private $status = 'building'; 30 | private $statuses = array('building' => 'building', 'success' => 'succeeded', 'failed' => 'failed'); 31 | 32 | /** 33 | * Constructor. 34 | * 35 | * @param Project $project A Project instance 36 | * @param string $sha The sha of the commit 37 | */ 38 | public function __construct(Project $project, $sha) 39 | { 40 | $this->project = $project; 41 | $this->sha = $sha; 42 | } 43 | 44 | /** 45 | * Returns a string representation of the Commit. 46 | * 47 | * @return string The string representation of the Commit 48 | */ 49 | public function __toString() 50 | { 51 | return sprintf('%s@%s', $this->project, $this->getShortSha()); 52 | } 53 | 54 | /** 55 | * Returns true if the commit is being built. 56 | * 57 | * @return Boolean true of the commit is being built, false otherwise 58 | */ 59 | public function isBuilding() 60 | { 61 | return 'building' === $this->status; 62 | } 63 | 64 | /** 65 | * Returns true if the commit has already been built. 66 | * 67 | * @return Boolean true of the commit has already been built, false otherwise 68 | */ 69 | public function isBuilt() 70 | { 71 | return in_array($this->status, array('success', 'failed')); 72 | } 73 | 74 | /** 75 | * Returns true if the commit was built successfully. 76 | * 77 | * @return Boolean true of the commit was built successfully, false otherwise 78 | */ 79 | public function isSuccessful() 80 | { 81 | return 'success' === $this->status; 82 | } 83 | 84 | /** 85 | * Sets the build status code of the commit. 86 | * 87 | * Can be one of "building", "success", or "failed". 88 | * 89 | * @param string $status The commit build status code 90 | */ 91 | public function setStatusCode($status) 92 | { 93 | if (!in_array($status, array('building', 'success', 'failed'))) { 94 | throw new \InvalidArgumentException(sprintf('Invalid status code "%s".', $status)); 95 | } 96 | 97 | $this->status = $status; 98 | } 99 | 100 | /** 101 | * Gets the build status code of the commit. 102 | * 103 | * Can be one of "building", "success", or "failed". 104 | * 105 | * @return string The commit build status code 106 | */ 107 | public function getStatusCode() 108 | { 109 | return $this->status; 110 | } 111 | 112 | /** 113 | * Gets the build status of the commit. 114 | * 115 | * Can be one of "building", "succeeded", or "failed". 116 | * 117 | * @return string The commit build status 118 | */ 119 | public function getStatus() 120 | { 121 | return $this->statuses[$this->status]; 122 | } 123 | 124 | /** 125 | * Sets the build output. 126 | * 127 | * @param string $output The build output 128 | */ 129 | public function setOutput($output) 130 | { 131 | $this->output = $output; 132 | } 133 | 134 | /** 135 | * Gets the raw build output. 136 | * 137 | * The output can contain ANSI code characters. 138 | * 139 | * @return string The raw build output 140 | * 141 | * @see getDecoratedOutput() 142 | */ 143 | public function getOutput() 144 | { 145 | return $this->output; 146 | } 147 | 148 | /** 149 | * Gets the commit message. 150 | * 151 | * @return string The commit message 152 | */ 153 | public function getMessage() 154 | { 155 | return $this->message; 156 | } 157 | 158 | /** 159 | * Sets the commit message. 160 | * 161 | * @param string $message The commit message 162 | */ 163 | public function setMessage($message) 164 | { 165 | $this->message = $message; 166 | } 167 | 168 | /** 169 | * Gets the commit SHA1. 170 | * 171 | * @return string The commit SHA1 172 | */ 173 | public function getSha() 174 | { 175 | return $this->sha; 176 | } 177 | 178 | /** 179 | * Gets the short commit SHA1 (6 first characters). 180 | * 181 | * @return string The short commit SHA1 182 | */ 183 | public function getShortSha() 184 | { 185 | return substr($this->sha, 0, 6); 186 | } 187 | 188 | /** 189 | * Gets the Project associated with this Commit. 190 | * 191 | * @return Project A Project instance 192 | */ 193 | public function getProject() 194 | { 195 | return $this->project; 196 | } 197 | 198 | /** 199 | * Gets the author associated with this commit. 200 | * 201 | * @return string The commit author 202 | */ 203 | public function getAuthor() 204 | { 205 | return $this->author; 206 | } 207 | 208 | /** 209 | * Sets the author associated with this commit. 210 | * 211 | * @return string The commit author 212 | */ 213 | public function setAuthor($author) 214 | { 215 | $this->author = $author; 216 | } 217 | 218 | /** 219 | * Gets the creation date of this commit. 220 | * 221 | * @return \DateTime A \DateTime instance 222 | */ 223 | public function getDate() 224 | { 225 | return $this->date; 226 | } 227 | 228 | /** 229 | * Sets the creation date of this commit. 230 | * 231 | * @param \DateTime $date A \DateTime instance 232 | */ 233 | public function setDate(\DateTime $date) 234 | { 235 | $this->date = $date; 236 | } 237 | 238 | /** 239 | * Gets the build date of this commit. 240 | * 241 | * @return \DateTime A \DateTime instance 242 | */ 243 | public function getBuildDate() 244 | { 245 | return $this->buildDate; 246 | } 247 | 248 | /** 249 | * Sets the build date of this commit. 250 | * 251 | * @param \DateTime $date A \DateTime instance 252 | */ 253 | public function setBuildDate(\DateTime $date) 254 | { 255 | $this->buildDate = $date; 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/Sismo/Storage/Storage.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Storage; 13 | 14 | use Sismo\Project; 15 | use Sismo\Commit; 16 | 17 | /** 18 | * Stores projects and builds information. 19 | * 20 | * @author Fabien Potencier 21 | */ 22 | class Storage implements StorageInterface 23 | { 24 | private $db; 25 | 26 | public function __construct(\SQLite3 $db) 27 | { 28 | $this->db = $db; 29 | } 30 | 31 | public function getCommit(Project $project, $sha) 32 | { 33 | $stmt = $this->db->prepare('SELECT slug, sha, author, date, build_date, message, status, output FROM `commit` WHERE slug = :slug AND sha = :sha'); 34 | $stmt->bindValue(':slug', $project->getSlug(), SQLITE3_TEXT); 35 | $stmt->bindValue(':sha', $sha, SQLITE3_TEXT); 36 | 37 | if (false !== $result = $stmt->execute()) { 38 | if (false !== $result = $result->fetchArray(\SQLITE3_ASSOC)) { 39 | return $this->createCommit($project, $result); 40 | } 41 | } 42 | 43 | return false; 44 | } 45 | 46 | public function initCommit(Project $project, $sha, $author, \DateTime $date, $message) 47 | { 48 | $stmt = $this->db->prepare('INSERT OR REPLACE INTO `commit` (slug, sha, author, date, message, status, output, build_date) VALUES (:slug, :sha, :author, :date, :message, :status, :output, :build_date)'); 49 | $stmt->bindValue(':slug', $project->getSlug(), SQLITE3_TEXT); 50 | $stmt->bindValue(':sha', $sha, SQLITE3_TEXT); 51 | $stmt->bindValue(':author', $author, SQLITE3_TEXT); 52 | $stmt->bindValue(':date', $date->format('Y-m-d H:i:s'), SQLITE3_TEXT); 53 | $stmt->bindValue(':message', $message, SQLITE3_TEXT); 54 | $stmt->bindValue(':status', 'building', SQLITE3_TEXT); 55 | $stmt->bindValue(':output', '', SQLITE3_TEXT); 56 | $stmt->bindValue(':build_date', '', SQLITE3_TEXT); 57 | 58 | if (false === $result = $stmt->execute()) { 59 | // @codeCoverageIgnoreStart 60 | throw new \RuntimeException(sprintf('Unable to save commit "%s" from project "%s".', $sha, $project->getName())); 61 | // @codeCoverageIgnoreEnd 62 | } 63 | 64 | $commit = new Commit($project, $sha); 65 | $commit->setAuthor($author); 66 | $commit->setMessage($message); 67 | $commit->setDate($date); 68 | 69 | return $commit; 70 | } 71 | 72 | public function updateProject(Project $project) 73 | { 74 | $stmt = $this->db->prepare('INSERT OR REPLACE INTO project (slug, name, repository, branch, command, url_pattern) VALUES (:slug, :name, :repository, :branch, :command, :url_pattern)'); 75 | $stmt->bindValue(':slug', $project->getSlug(), SQLITE3_TEXT); 76 | $stmt->bindValue(':name', $project->getName(), SQLITE3_TEXT); 77 | $stmt->bindValue(':repository', $project->getRepository(), SQLITE3_TEXT); 78 | $stmt->bindValue(':branch', $project->getBranch(), SQLITE3_TEXT); 79 | $stmt->bindValue(':command', $project->getCommand(), SQLITE3_TEXT); 80 | $stmt->bindValue(':url_pattern', $project->getUrlPattern(), SQLITE3_TEXT); 81 | 82 | if (false === $stmt->execute()) { 83 | // @codeCoverageIgnoreStart 84 | throw new \RuntimeException(sprintf('Unable to save project "%s".', $project->getName())); 85 | // @codeCoverageIgnoreEnd 86 | } 87 | 88 | // related commits 89 | $stmt = $this->db->prepare('SELECT sha, author, date, build_date, message, status, output FROM `commit` WHERE slug = :slug ORDER BY `status` = "building" DESC, build_date DESC LIMIT 100'); 90 | $stmt->bindValue(':slug', $project->getSlug(), SQLITE3_TEXT); 91 | 92 | if (false === $results = $stmt->execute()) { 93 | // @codeCoverageIgnoreStart 94 | throw new \RuntimeException(sprintf('Unable to get latest commit for project "%s".', $project->getName())); 95 | // @codeCoverageIgnoreEnd 96 | } 97 | 98 | $commits = array(); 99 | while ($result = $results->fetchArray(\SQLITE3_ASSOC)) { 100 | $commits[] = $this->createCommit($project, $result); 101 | } 102 | 103 | $project->setCommits($commits); 104 | 105 | // project building? 106 | $stmt = $this->db->prepare('SELECT COUNT(*) AS count FROM `commit` WHERE slug = :slug AND status = "building"'); 107 | $stmt->bindValue(':slug', $project->getSlug(), SQLITE3_TEXT); 108 | 109 | $building = false; 110 | if (false !== $result = $stmt->execute()) { 111 | if (false !== $result = $result->fetchArray(\SQLITE3_ASSOC)) { 112 | if ($result['count'] > 0) { 113 | $building = true; 114 | } 115 | } 116 | } 117 | 118 | $project->setBuilding($building); 119 | } 120 | 121 | public function updateCommit(Commit $commit) 122 | { 123 | $stmt = $this->db->prepare('UPDATE `commit` SET status = :status, output = :output, build_date = CURRENT_TIMESTAMP WHERE slug = :slug AND sha = :sha'); 124 | $stmt->bindValue(':slug', $commit->getProject()->getSlug(), SQLITE3_TEXT); 125 | $stmt->bindValue(':sha', $commit->getSha(), SQLITE3_TEXT); 126 | $stmt->bindValue(':status', $commit->getStatusCode(), SQLITE3_TEXT); 127 | $stmt->bindValue(':output', $commit->getOutput(), SQLITE3_TEXT); 128 | 129 | if (false === $stmt->execute()) { 130 | // @codeCoverageIgnoreStart 131 | throw new \RuntimeException(sprintf('Unable to save build "%s@%s".', $commit->getProject()->getName(), $commit->getSha())); 132 | // @codeCoverageIgnoreEnd 133 | } 134 | } 135 | 136 | private function createCommit($project, $result) 137 | { 138 | $commit = new Commit($project, $result['sha']); 139 | $commit->setAuthor($result['author']); 140 | $commit->setMessage($result['message']); 141 | $commit->setDate(\DateTime::createFromFormat('Y-m-d H:i:s', $result['date'])); 142 | if ($result['build_date']) { 143 | $commit->setBuildDate(\DateTime::createFromFormat('Y-m-d H:i:s', $result['build_date'])); 144 | } 145 | $commit->setStatusCode($result['status']); 146 | $commit->setOutput($result['output']); 147 | 148 | return $commit; 149 | } 150 | 151 | public function close() 152 | { 153 | $this->db->close(); 154 | } 155 | 156 | public function __destruct() 157 | { 158 | $this->close(); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /tests/controllersTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | use Silex\WebTestCase; 13 | use Sismo\Project; 14 | use Symfony\Component\Filesystem\Filesystem; 15 | 16 | class controllersTest extends WebTestCase 17 | { 18 | protected $baseDir; 19 | 20 | public function createApplication() 21 | { 22 | $app = require __DIR__.'/../src/app.php'; 23 | 24 | $this->baseDir = realpath(sys_get_temp_dir()).'/sismo'; 25 | $fs = new Filesystem(); 26 | $fs->mkdir($this->baseDir); 27 | $fs->mkdir($this->baseDir.'/config'); 28 | $app['data.path'] = $this->baseDir.'/db'; 29 | $app['config.file'] = $this->baseDir.'/config.php'; 30 | 31 | // This file does not exist, so app will use default sqlite storage. 32 | $app['config.storage.file'] = $this->baseDir.'/storage.php'; 33 | 34 | @unlink($this->app['db.path']); 35 | file_put_contents($app['config.file'], 'app['storage']->close(); 47 | 48 | $fs = new Filesystem(); 49 | $fs->remove($this->baseDir); 50 | } 51 | 52 | public function testGetProjectsEmpty() 53 | { 54 | $crawler = $this->createClient()->request('GET', '/'); 55 | 56 | $this->assertEquals(1, count($crawler->filter('p:contains("No project yet.")'))); 57 | } 58 | 59 | public function testPages() 60 | { 61 | $sismo = $this->app['sismo']; 62 | $storage = $this->app['storage']; 63 | 64 | $sismo->addProject(new Project('Twig')); 65 | 66 | $sismo->addProject($project1 = new Project('Silex1')); 67 | $commit = $storage->initCommit($project1, '7d78d5', 'fabien', new \DateTime(), 'foo'); 68 | $storage->updateProject($project1); 69 | 70 | $sismo->addProject($project2 = new Project('Silex2')); 71 | $commit = $storage->initCommit($project2, '7d78d5', 'fabien', new \DateTime(), 'foo'); 72 | $commit->setStatusCode('success'); 73 | $storage->updateCommit($commit); 74 | $storage->updateProject($project2); 75 | 76 | $sismo->addProject($project3 = new Project('Silex3')); 77 | $commit = $storage->initCommit($project3, '7d78d5', 'fabien', new \DateTime(), 'foo'); 78 | $commit->setStatusCode('failed'); 79 | $storage->updateCommit($commit); 80 | $storage->updateProject($project3); 81 | 82 | // projects page 83 | $client = $this->createClient(); 84 | $crawler = $client->request('GET', '/'); 85 | 86 | $this->assertEquals(array('Twig', 'Silex1', 'Silex2', 'Silex3'), $crawler->filter('ul#projects li a')->each(function ($node) { return trim($node->nodeValue); })); 87 | $this->assertEquals(array('not built yet', 'building', 'succeeded', 'failed'), $crawler->filter('ul#projects li div')->each(function ($node) { return trim($node->nodeValue); })); 88 | $this->assertEquals(array('no_build', 'building', 'success', 'failed'), $crawler->filter('ul#projects li')->extract('class')); 89 | 90 | $links = $crawler->filter('ul#projects li a')->links(); 91 | 92 | // project page 93 | $crawler = $client->click($links[0]); 94 | $this->assertEquals(1, count($crawler->filter('p:contains("Never built yet.")'))); 95 | 96 | $crawler = $client->click($links[1]); 97 | $this->assertEquals('#7d78d5 building', trim($crawler->filter('div.commit')->text())); 98 | 99 | $crawler = $client->click($links[2]); 100 | $this->assertEquals('#7d78d5 succeeded', trim($crawler->filter('div.commit')->text())); 101 | 102 | $crawler = $client->click($links[3]); 103 | $this->assertEquals('#7d78d5 failed', trim($crawler->filter('div.commit')->text())); 104 | 105 | // sha page 106 | $crawler = $client->request('GET', '/silex2/7d78d5'); 107 | $this->assertEquals('#7d78d5 succeeded', trim($crawler->filter('div.commit')->text())); 108 | 109 | // cc tray 110 | $crawler = $client->request('GET', '/dashboard/cctray.xml'); 111 | $this->assertEquals(4, count($crawler->filter('Project'))); 112 | 113 | $this->assertEquals(array('Unknown', 'Unknown', 'Success', 'Failure'), $crawler->filter('Project')->extract('lastBuildStatus')); 114 | $this->assertEquals(array('Sleeping', 'Building', 'Sleeping', 'Sleeping'), $crawler->filter('Project')->extract('activity')); 115 | } 116 | 117 | public function testGetNonExistentProject() 118 | { 119 | $crawler = $this->createClient()->request('GET', '/foobar'); 120 | 121 | $this->assertEquals('Project "foobar" not found.', $crawler->filter('p')->text()); 122 | } 123 | 124 | public function testGetNonExistentProjectOnShaPage() 125 | { 126 | $crawler = $this->createClient()->request('GET', '/foo/bar'); 127 | 128 | $this->assertEquals('Project "foo" not found.', $crawler->filter('p')->text()); 129 | } 130 | 131 | public function testGetNonExistentSha() 132 | { 133 | $sismo = $this->app['sismo']; 134 | $sismo->addProject(new Project('Twig')); 135 | 136 | $crawler = $this->createClient()->request('GET', '/twig/bar'); 137 | 138 | $this->assertEquals('Commit "bar" for project "twig" not found.', $crawler->filter('p')->text()); 139 | } 140 | 141 | public function testBuildPage() 142 | { 143 | $token = md5(mt_rand()); 144 | $this->app['build.token'] = $token; 145 | 146 | $project = new Project('Twig'); 147 | 148 | $this->app['sismo'] = $this->getMockBuilder('Sismo\Sismo')->disableOriginalConstructor()->getMock(); 149 | $this->app['sismo']->expects($this->once())->method('hasProject')->will($this->returnValue(true)); 150 | $this->app['sismo']->expects($this->once())->method('getProject')->will($this->returnValue($project)); 151 | $this->app['sismo']->expects($this->once())->method('build'); 152 | 153 | $storage = $this->app['storage']; 154 | 155 | $this->app['sismo']->addProject($project); 156 | $commit = $storage->initCommit($project, '7d78d5', 'fabien', new \DateTime(), 'foo'); 157 | $commit->setStatusCode('success'); 158 | $storage->updateCommit($commit); 159 | $storage->updateProject($project); 160 | 161 | $client = $this->createClient(); 162 | $crawler = $client->request('POST', sprintf('/twig/build/%s', urlencode($token))); 163 | 164 | $this->assertEquals('Triggered build for project "twig".', $crawler->filter('p')->text()); 165 | } 166 | 167 | public function testBuildPageTokenNotSet() 168 | { 169 | $this->app['build.token'] = null; 170 | 171 | $client = $this->createClient(); 172 | $crawler = $client->request('POST', '/twig/build/foo'); 173 | 174 | $this->assertEquals('Not found.', $crawler->filter('p')->text()); 175 | } 176 | 177 | public function testBuildPageInvalidToken() 178 | { 179 | $this->app['build.token'] = md5(mt_rand()); 180 | 181 | $client = $this->createClient(); 182 | $crawler = $client->request('POST', '/twig/build/foo'); 183 | 184 | $this->assertEquals('An error occurred', $crawler->filter('p')->text()); 185 | } 186 | 187 | public function testBuildPageNonExistentProject() 188 | { 189 | $token = md5(mt_rand()); 190 | $this->app['build.token'] = $token; 191 | 192 | $client = $this->createClient(); 193 | $crawler = $client->request('POST', sprintf('/foobar/build/%s', urlencode($token))); 194 | 195 | $this->assertEquals('Project "foobar" not found.', $crawler->filter('p')->text()); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /tests/Sismo/Tests/ProjectTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Tests; 13 | 14 | use Sismo\Project; 15 | 16 | class ProjectTest extends \PHPUnit_Framework_TestCase 17 | { 18 | public function testConstructor() 19 | { 20 | $project = new Project('Twig Local'); 21 | $this->assertEquals('Twig Local', $project->getName()); 22 | $this->assertEquals('twig-local', $project->getSlug()); 23 | $this->assertEquals('master', $project->getBranch()); 24 | $this->assertEquals(array(), $project->getNotifiers()); 25 | 26 | $project = new Project('Twig Local', 'repo', array(), 'twig'); 27 | $this->assertEquals('twig', $project->getSlug()); 28 | $this->assertEquals(array(), $project->getNotifiers()); 29 | 30 | $project = new Project('Twig Local', 'repo'); 31 | $this->assertEquals('repo', $project->getRepository()); 32 | $this->assertEquals('master', $project->getBranch()); 33 | 34 | $project = new Project('Twig Local', 'repo@feat'); 35 | $this->assertEquals('repo', $project->getRepository()); 36 | $this->assertEquals('feat', $project->getBranch()); 37 | 38 | $project = new Project('Twig Local', 'repo', array( 39 | $notifier1 = $this->getMock('Sismo\Notifier\Notifier'), 40 | $notifier2 = $this->getMock('Sismo\Notifier\Notifier'), 41 | )); 42 | $this->assertSame(array($notifier1, $notifier2), $project->getNotifiers()); 43 | 44 | $project = new Project('Twig Local', 'repo', $notifier3 = $this->getMock('Sismo\Notifier\Notifier')); 45 | $this->assertSame(array($notifier3), $project->getNotifiers()); 46 | } 47 | 48 | public function testSlug() 49 | { 50 | $project = new Project('Twig Local'); 51 | $this->assertEquals('twig-local', $project->getSlug()); 52 | 53 | $project->setSlug('twig-local-my-slug'); 54 | $this->assertEquals('twig-local-my-slug', $project->getSlug()); 55 | } 56 | 57 | public function testToString() 58 | { 59 | $project = new Project('Twig Local'); 60 | $this->assertEquals('Twig Local', (string) $project); 61 | } 62 | 63 | public function testBuildingFlag() 64 | { 65 | $project = new Project('Twig Local'); 66 | $this->assertFalse($project->isBuilding()); 67 | 68 | $project->setBuilding(true); 69 | $this->assertTrue($project->isBuilding()); 70 | 71 | $project->setBuilding('foo'); 72 | $this->assertTrue($project->isBuilding()); 73 | 74 | $project->setBuilding(false); 75 | $this->assertFalse($project->isBuilding()); 76 | } 77 | 78 | public function testNotifiers() 79 | { 80 | $project = new Project('Twig Local'); 81 | $this->assertEquals(array(), $project->getNotifiers()); 82 | 83 | $project->addNotifier($notifier1 = $this->getMock('Sismo\Notifier\Notifier')); 84 | $this->assertSame(array($notifier1), $project->getNotifiers()); 85 | 86 | $project->addNotifier($notifier2 = $this->getMock('Sismo\Notifier\Notifier')); 87 | $this->assertSame(array($notifier1, $notifier2), $project->getNotifiers()); 88 | } 89 | 90 | public function testBranch() 91 | { 92 | $project = new Project('Twig Local'); 93 | $this->assertEquals('master', $project->getBranch()); 94 | 95 | $project->setBranch('new-feature'); 96 | $this->assertEquals('new-feature', $project->getBranch()); 97 | } 98 | 99 | public function testCommits() 100 | { 101 | $project = new Project('Twig Local'); 102 | $this->assertEquals(array(), $project->getCommits()); 103 | 104 | $project->setCommits(array( 105 | $commit1 = $this->getMockBuilder('Sismo\Commit')->disableOriginalConstructor()->getMock(), 106 | $commit2 = $this->getMockBuilder('Sismo\Commit')->disableOriginalConstructor()->getMock(), 107 | $commit3 = $this->getMockBuilder('Sismo\Commit')->disableOriginalConstructor()->getMock(), 108 | )); 109 | $this->assertEquals(array($commit1, $commit2, $commit3), $project->getCommits()); 110 | $this->assertEquals($commit1, $project->getLatestCommit()); 111 | } 112 | 113 | public function testName() 114 | { 115 | $project = new Project('Twig (Local)'); 116 | $this->assertEquals('Twig (Local)', $project->getName()); 117 | $this->assertEquals('Twig', $project->getShortName()); 118 | $this->assertEquals('Local', $project->getSubName()); 119 | 120 | $project = new Project('Twig'); 121 | $this->assertEquals('Twig', $project->getName()); 122 | $this->assertEquals('Twig', $project->getShortName()); 123 | $this->assertEquals('', $project->getSubName()); 124 | } 125 | 126 | public function testStatus() 127 | { 128 | $project = new Project('Twig Local'); 129 | $this->assertEquals('no_build', $project->getStatusCode()); 130 | $this->assertEquals('not built yet', $project->getStatus()); 131 | 132 | $commit = $this->getMockBuilder('Sismo\Commit')->disableOriginalConstructor()->getMock(); 133 | $commit->expects($this->once())->method('getStatusCode')->will($this->returnValue('success')); 134 | $project->setCommits(array($commit)); 135 | $this->assertEquals('success', $project->getStatusCode()); 136 | 137 | $commit = $this->getMockBuilder('Sismo\Commit')->disableOriginalConstructor()->getMock(); 138 | $commit->expects($this->once())->method('getStatus')->will($this->returnValue('success')); 139 | $project->setCommits(array($commit)); 140 | $this->assertEquals('success', $project->getStatus()); 141 | } 142 | 143 | public function testCCStatus() 144 | { 145 | $project = new Project('Twig Local'); 146 | $this->assertEquals('Unknown', $project->getCCStatus()); 147 | 148 | $commit = $this->getMockBuilder('Sismo\Commit')->disableOriginalConstructor()->getMock(); 149 | $commit->expects($this->once())->method('isBuilt')->will($this->returnValue(true)); 150 | $commit->expects($this->once())->method('isSuccessful')->will($this->returnValue(true)); 151 | $project->setCommits(array($commit)); 152 | $this->assertEquals('Success', $project->getCCStatus()); 153 | 154 | $commit = $this->getMockBuilder('Sismo\Commit')->disableOriginalConstructor()->getMock(); 155 | $commit->expects($this->once())->method('isBuilt')->will($this->returnValue(true)); 156 | $commit->expects($this->once())->method('isSuccessful')->will($this->returnValue(false)); 157 | $project->setCommits(array($commit)); 158 | $this->assertEquals('Failure', $project->getCCStatus()); 159 | } 160 | 161 | public function testCCActivity() 162 | { 163 | $project = new Project('Twig Local'); 164 | $this->assertEquals('Sleeping', $project->getCCActivity()); 165 | 166 | $commit = $this->getMockBuilder('Sismo\Commit')->disableOriginalConstructor()->getMock(); 167 | $commit->expects($this->once())->method('isBuilding')->will($this->returnValue(true)); 168 | $project->setCommits(array($commit)); 169 | $this->assertEquals('Building', $project->getCCActivity()); 170 | 171 | $commit = $this->getMockBuilder('Sismo\Commit')->disableOriginalConstructor()->getMock(); 172 | $commit->expects($this->once())->method('isBuilding')->will($this->returnValue(false)); 173 | $project->setCommits(array($commit)); 174 | $this->assertEquals('Sleeping', $project->getCCActivity()); 175 | } 176 | 177 | public function testRepository() 178 | { 179 | $project = new Project('Twig Local'); 180 | 181 | $project->setRepository('https://github.com/twigphp/Twig.git'); 182 | $this->assertEquals('https://github.com/twigphp/Twig.git', $project->getRepository()); 183 | 184 | $project->setRepository('https://github.com/twigphp/Twig.git@feat'); 185 | $this->assertEquals('https://github.com/twigphp/Twig.git', $project->getRepository()); 186 | $this->assertEquals('feat', $project->getBranch()); 187 | } 188 | 189 | public function testCommand() 190 | { 191 | $project = new Project('Twig Local'); 192 | $this->assertEquals('phpunit', $project->getCommand()); 193 | 194 | $project->setCommand('/path/to/phpunit'); 195 | $this->assertEquals('/path/to/phpunit', $project->getCommand()); 196 | } 197 | 198 | public function testDefaultCommand() 199 | { 200 | $project = new Project('Twig Local'); 201 | $this->assertEquals('phpunit', $project->getCommand()); 202 | 203 | Project::setDefaultCommand('phpunit --colors --strict'); 204 | $project2 = new Project('Twig Local'); 205 | $this->assertEquals('phpunit', $project->getCommand()); 206 | $this->assertEquals('phpunit --colors --strict', $project2->getCommand()); 207 | 208 | $project2->setCommand('phpunit'); 209 | $this->assertEquals('phpunit', $project2->getCommand()); 210 | } 211 | 212 | public function testUrlPattern() 213 | { 214 | $project = new Project('Twig Local'); 215 | 216 | $project->setUrlPattern('https://github.com/twigphp/Twig/commit/%commit%'); 217 | $this->assertEquals('https://github.com/twigphp/Twig/commit/%commit%', $project->getUrlPattern()); 218 | } 219 | 220 | /** 221 | * @expectedException \RuntimeException 222 | */ 223 | public function testSlugify() 224 | { 225 | $project = new Project(''); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Sismo: Your Continuous Testing Server 2 | ===================================== 3 | 4 | `Sismo`_ is a *Continuous Testing Server* written in PHP. 5 | 6 | Unlike more "advanced" Continuous Integration Servers (like Jenkins), Sismo 7 | does not try to do more than getting your code, running your tests, and send 8 | you notifications. 9 | 10 | What makes Sismo special? 11 | ------------------------- 12 | 13 | Sismo has been optimized to run *locally* on your computer for your *Git* 14 | projects. Even if it can test *remote* repositories, Sismo is better used as a 15 | local ``post-commit`` hook. Whenever you commit changes locally, Sismo runs 16 | the tests and give you *immediate* feedback *before* you actually push your 17 | modifications to the remote repository. So, Sismo is a nice *complement* to 18 | your Continuous Integration Server. 19 | 20 | Sismo is *language and tool agnostic*. Just give it a command that knows how 21 | to run your tests and returns a non-zero exit code when tests do not pass. 22 | 23 | Sounds good? There is more. Sismo is insanely *easy to install* (there is only 24 | one PHP file to download), *easy to configure*, and it comes with a *gorgeous 25 | web interface*. 26 | 27 | .. image:: http://sismo.sensiolabs.org/images/sismo-home.png 28 | 29 | Installation 30 | ------------ 31 | 32 | Installing Sismo is as easy as downloading the `sismo.php`_ file and put it 33 | somewhere under your web root directory. That's it, the CLI tool and the web 34 | interface is packed into a single PHP file. 35 | 36 | Note that Sismo needs at least PHP 5.3.3 to run. 37 | 38 | Configuration 39 | ------------- 40 | 41 | By default, Sismo reads its configuration from ``~/.sismo/config.php``: 42 | 43 | .. code-block:: php 44 | 45 | setRepository('https://github.com/symfony/symfony.git'); 69 | $sf2->setBranch('master'); 70 | $sf2->setCommand('./vendors.sh; phpunit'); 71 | $sf2->setSlug('symfony-local'); 72 | $sf2->setUrlPattern('https://github.com/symfony/symfony/commit/%commit%'); 73 | $sf2->addNotifier($notifier); 74 | $projects[] = $sf2; 75 | 76 | return $projects; 77 | 78 | For notifications, you can also use any Cruise Control "tray" software as 79 | Sismo also exposes an XML file in the Cruise Control format:: 80 | 81 | http://path/to/sismo.php/dashboard/cctray.xml 82 | 83 | Use `CCMenu`_ on Mac, `CCTray`_ on Windows, `JCCTray`_ on Windows or Linux, or 84 | `CCMonitor`_ for Firefox. 85 | 86 | Using Sismo 87 | ----------- 88 | 89 | Build all configured projects by running the ``build`` command: 90 | 91 | .. code-block:: text 92 | 93 | $ php sismo.php build --verbose 94 | 95 | If a build fails, Sismo will send notifications. Use the ``output`` command to 96 | see the latest build output of a project: 97 | 98 | .. code-block:: text 99 | 100 | $ php sismo.php output twig 101 | 102 | If you have configured Sismo to be accessible from the web interface, you can 103 | also check the build outputs there: 104 | 105 | .. image:: http://sismo.sensiolabs.org/images/sismo-project.png 106 | 107 | If your web server runs under a different user than the one you use on the 108 | CLI, you will need to set some environment variables in your virtual host 109 | configuration: 110 | 111 | .. code-block:: apache 112 | 113 | SetEnv SISMO_DATA_PATH "/path/to/sismo/data" 114 | SetEnv SISMO_CONFIG_PATH "/path/to/sismo/config.php" 115 | 116 | The ``build`` command is quite powerful and has many options. Learn more by 117 | appending ``--help``: 118 | 119 | .. code-block:: bash 120 | 121 | $ php sismo.php build --help 122 | 123 | To make Sismo run whenever you commit some changes, save this script in your 124 | project as ``.git/hooks/post-commit`` and make sure it's executable: 125 | 126 | .. code-block:: bash 127 | 128 | #!/bin/sh 129 | 130 | nohup php /path/to/sismo.php --quiet --force build symfony-local `git log -1 HEAD --pretty="%H"` &>/dev/null & 131 | 132 | ``symfony-local`` is the slug of the project. You can also create a 133 | ``post-merge`` script if you want to run Sismo when you merge branches. 134 | 135 | If you are running Sismo (with the single PHP file) with PHP 5.4.0, you can 136 | use the Sismo build-in web server: 137 | 138 | .. code-block:: bash 139 | 140 | $ php sismo.php run localhost:9000 141 | 142 | And then open the browser and point it to http://localhost:9000/sismo.php 143 | 144 | Limitations 145 | ----------- 146 | 147 | Sismo is small and simple and it will stay that way. Sismo will never have the 148 | following: 149 | 150 | * a queue (if a project is already being built, newer commits are ignored); 151 | * a web interface for configuration; 152 | * metrics support; 153 | * plugin support; 154 | * other SCM support; 155 | * slaves support; 156 | * built-in authentication. 157 | 158 | ... and probably the feature you have in mind right now and all the ones you 159 | will think of later on ;) 160 | 161 | Tips and Recipes 162 | ---------------- 163 | 164 | Change the default Location 165 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 166 | 167 | Set the following environment variables to customize the default locations 168 | used by Sismo: 169 | 170 | .. code-block:: apache 171 | 172 | # in a .htaccess or httpd.conf Apache configuration file 173 | 174 | SetEnv SISMO_DATA_PATH "/path/to/sismo/data" 175 | SetEnv SISMO_CONFIG_PATH "/path/to/sismo/config.php" 176 | 177 | # for the CLI tool 178 | 179 | export SISMO_DATA_PATH=/path/to/sismo/data/ 180 | export SISMO_CONFIG_PATH=/path/to/sismo/config.php 181 | 182 | Tracking multiple Branches 183 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 184 | 185 | To track multiple branches of a project, just make their names unique and set 186 | the branch name:: 187 | 188 | $projects[] = new Sismo\GithubProject('Twig (master branch)', '/Users/fabien/Twig'); 189 | 190 | $projects[] = new Sismo\GithubProject('Twig (feat-awesome branch)', '/Users/fabien/Twig@feat-awesome'); 191 | 192 | Note that Sismo uses the same clone for projects sharing the same repositories 193 | URL. 194 | 195 | Running Sismo for Remote Repositories 196 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 197 | 198 | Using Sismo for remote repositories is as simple as adding the Sismo building 199 | tool in a crontab entry: 200 | 201 | .. code-block:: text 202 | 203 | 0 12 * * * php /path/to/sismo.php --quiet 204 | 205 | For GitHub projects, and other systems that support post-receive URL hooks, 206 | you can set up Sismo to build automatically when a new revision is pushed. 207 | You need to set an environment variable in your Apache configuration: 208 | 209 | .. code-block:: apache 210 | 211 | # in a .htaccess or httpd.conf Apache configuration file 212 | 213 | SetEnv SISMO_BUILD_TOKEN "YOUR_TOKEN" 214 | 215 | You can also set an environment variable in your config file 216 | (``~/.sismo/config.php``): 217 | 218 | .. code-block:: php 219 | 220 | putenv('SISMO_BUILD_TOKEN=YOUR_TOKEN'); 221 | 222 | Replace YOUR_TOKEN with something more secure, as anyone with this token 223 | could use it to trigger builds. Then set your post-receive URL appropriately. 224 | For example:: 225 | 226 | http://path/to/sismo.php/your_project/build/YOUR_TOKEN 227 | 228 | History in the Web Interface 229 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 230 | 231 | The build history for a project in the web interface is different from the 232 | project history. It is sorted in the order of the builds so that the latest 233 | build output is always at your fingertips. 234 | 235 | Adding a Notifier 236 | ~~~~~~~~~~~~~~~~~ 237 | 238 | Sismo comes with the most common notifiers but you can create new ones very 239 | easily: extend the `Sismo\Notifier\Notifier` abstract class and implement the 240 | `notify()` method: 241 | 242 | .. code-block:: php 243 | 244 | public function notify(Commit $commit) 245 | { 246 | // do something with the commit 247 | } 248 | 249 | The `Commit`_ object has many methods that gives you a lot of information 250 | about the commit and its build. You can also get general information about the 251 | project by calling `getProject()`_. 252 | 253 | Use Sismo with composer 254 | ~~~~~~~~~~~~~~~~~~~~~~~ 255 | 256 | If a majority of your projects use `composer`_, you can configure Sismo 257 | to install dependencies before running `phpunit`. Add the following code 258 | to your config file: 259 | 260 | .. code-block:: php 261 | 262 | Sismo\Project::setDefaultCommand('if [ -f composer.json ]; then composer install; fi && phpunit'); 263 | 264 | .. _Sismo: http://sismo.sensiolabs.org/ 265 | .. _sismo.php: http://sismo.sensiolabs.org/get/sismo.php 266 | .. _CCMenu: http://ccmenu.sourceforge.net/ 267 | .. _CCTray: http://confluence.public.thoughtworks.org/display/CCNET/CCTray 268 | .. _CCMonitor: http://code.google.com/p/cc-monitor/ 269 | .. _JCCTray: http://sourceforge.net/projects/jcctray/ 270 | .. _Commit: http://sismo.sensiolabs.org/api/index.html?q=Sismo\Commit 271 | .. _getProject(): http://sismo.sensiolabs.org/api/index.html?q=Sismo\Project 272 | .. _composer: https://getcomposer.org/doc/00-intro.md#globally 273 | -------------------------------------------------------------------------------- /src/Sismo/Project.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo; 13 | 14 | use Sismo\Notifier\Notifier; 15 | 16 | /** 17 | * Represents a project. 18 | * 19 | * @author Fabien Potencier 20 | */ 21 | class Project 22 | { 23 | protected static $defaultCommand = 'phpunit'; 24 | 25 | protected $name; 26 | protected $slug; 27 | protected $repository; 28 | protected $branch = 'master'; 29 | protected $command; 30 | protected $urlPattern; 31 | protected $commits = array(); 32 | protected $building = false; 33 | protected $notifiers = array(); 34 | 35 | /** 36 | * Constructor. 37 | * 38 | * @param string $name The project name 39 | * @param string $repository The repository URL 40 | * @param array $notifiers An array of Notifier instances 41 | * @param string $slug The project slug 42 | */ 43 | public function __construct($name, $repository = null, $notifiers = array(), $slug = null) 44 | { 45 | $this->name = $name; 46 | $this->slug = $slug ?: $this->slugify($name); 47 | $this->command = static::$defaultCommand; 48 | 49 | if (null !== $repository) { 50 | $this->setRepository($repository); 51 | } 52 | 53 | if (!is_array($notifiers)) { 54 | $notifiers = array($notifiers); 55 | } 56 | 57 | foreach ($notifiers as $notifier) { 58 | $this->addNotifier($notifier); 59 | } 60 | } 61 | 62 | /** 63 | * Returns a string representation of the Project. 64 | * 65 | * @return string The string representation of the Project 66 | */ 67 | public function __toString() 68 | { 69 | return $this->name; 70 | } 71 | 72 | /** 73 | * Toggles the building status flag. 74 | * 75 | * @param bool $bool The build status flag 76 | */ 77 | public function setBuilding($bool) 78 | { 79 | $this->building = (bool) $bool; 80 | } 81 | 82 | /** 83 | * Returns true if the project is currently being built. 84 | * 85 | * @return bool true if the project is currently being built, false otherwise 86 | */ 87 | public function isBuilding() 88 | { 89 | return $this->building; 90 | } 91 | 92 | /** 93 | * Adds a notifier. 94 | * 95 | * @param Notifier $notifier A Notifier instance 96 | */ 97 | public function addNotifier(Notifier $notifier) 98 | { 99 | $this->notifiers[] = $notifier; 100 | 101 | return $this; 102 | } 103 | 104 | /** 105 | * Gets the notifiers associated with this project. 106 | * 107 | * @return array An array of Notifier instances 108 | */ 109 | public function getNotifiers() 110 | { 111 | return $this->notifiers; 112 | } 113 | 114 | /** 115 | * Sets the branch of the project we are interested in. 116 | * 117 | * @param string $branch The branch name 118 | */ 119 | public function setBranch($branch) 120 | { 121 | $this->branch = $branch; 122 | 123 | return $this; 124 | } 125 | 126 | /** 127 | * Gets the project branch name. 128 | * 129 | * @return string The branch name 130 | */ 131 | public function getBranch() 132 | { 133 | return $this->branch; 134 | } 135 | 136 | /** 137 | * Sets the commits associated with the project. 138 | * 139 | * @param array $commits An array of Commit instances 140 | */ 141 | public function setCommits(array $commits = array()) 142 | { 143 | $this->commits = $commits; 144 | } 145 | 146 | /** 147 | * Gets the commits associated with the project. 148 | * 149 | * @return array An array of Commit instances 150 | */ 151 | public function getCommits() 152 | { 153 | return $this->commits; 154 | } 155 | 156 | /** 157 | * Gets the latest commit of the project. 158 | * 159 | * @return Commit A Commit instance 160 | */ 161 | public function getLatestCommit() 162 | { 163 | return $this->commits ? $this->commits[0] : null; 164 | } 165 | 166 | /** 167 | * Gets the build status code of the latest commit. 168 | * 169 | * If the commit has been built, it returns the Commit::getStatusCode() 170 | * value; if not, it returns "no_build". 171 | * 172 | * @return string The build status code for the latest commit 173 | * 174 | * @see Commit::getStatusCode() 175 | */ 176 | public function getStatusCode() 177 | { 178 | return !$this->commits ? 'no_build' : $this->commits[0]->getStatusCode(); 179 | } 180 | 181 | /** 182 | * Gets the build status of the latest commit. 183 | * 184 | * If the commit has been built, it returns the Commit::getStatus() 185 | * value; if not, it returns "not built yet". 186 | * 187 | * @return string The build status for the latest commit 188 | * 189 | * @see Commit::getStatus() 190 | */ 191 | public function getStatus() 192 | { 193 | return !$this->commits ? 'not built yet' : $this->commits[0]->getStatus(); 194 | } 195 | 196 | /** 197 | * Gets the build status of the latest commit as a Cruise Control string. 198 | * 199 | * The value is one of "Unknown", "Success", or "Failure". 200 | * 201 | * @return string The build status for the latest commit 202 | */ 203 | public function getCCStatus() 204 | { 205 | if (!$this->commits || !$this->commits[0]->isBuilt()) { 206 | return 'Unknown'; 207 | } 208 | 209 | return $this->commits[0]->isSuccessful() ? 'Success' : 'Failure'; 210 | } 211 | 212 | /** 213 | * Gets the build status activity of the latest commit as a Cruise Control string. 214 | * 215 | * The value is one of "Building" or "Sleeping". 216 | * 217 | * @return string The build status activity for the latest commit 218 | */ 219 | public function getCCActivity() 220 | { 221 | return $this->commits && $this->commits[0]->isBuilding() ? 'Building' : 'Sleeping'; 222 | } 223 | 224 | /** 225 | * Gets the project name. 226 | * 227 | * @return string The project name 228 | */ 229 | public function getName() 230 | { 231 | return $this->name; 232 | } 233 | 234 | /** 235 | * Gets the project short name. 236 | * 237 | * @return string The project short name 238 | */ 239 | public function getShortName() 240 | { 241 | list($name) = explode('(', $this->name); 242 | 243 | return trim($name); 244 | } 245 | 246 | /** 247 | * Gets the project sub name. 248 | * 249 | * @return string The project sub name 250 | */ 251 | public function getSubName() 252 | { 253 | if (false !== $pos = strpos($this->name, '(')) { 254 | return trim(substr($this->name, $pos + 1, -1)); 255 | } 256 | 257 | return ''; 258 | } 259 | 260 | /** 261 | * Gets the project slug. 262 | * 263 | * @return string The project slug 264 | */ 265 | public function getSlug() 266 | { 267 | return $this->slug; 268 | } 269 | 270 | /** 271 | * Sets the project slug. 272 | * 273 | * @param string $slug The project slug 274 | */ 275 | public function setSlug($slug) 276 | { 277 | $this->slug = $slug; 278 | 279 | return $this; 280 | } 281 | 282 | /** 283 | * Gets the project repository URL. 284 | * 285 | * @return string The project repository URL 286 | */ 287 | public function getRepository() 288 | { 289 | return $this->repository; 290 | } 291 | 292 | /** 293 | * Sets the project repository URL. 294 | * 295 | * @param string $url The project repository URL 296 | */ 297 | public function setRepository($url) 298 | { 299 | if (false !== strpos($url, '@')) { 300 | list($url, $branch) = explode('@', $url); 301 | $this->branch = $branch; 302 | } 303 | 304 | $this->repository = $url; 305 | 306 | return $this; 307 | } 308 | 309 | /** 310 | * Gets the command to use to build the project. 311 | * 312 | * @return string The build command 313 | */ 314 | public function getCommand() 315 | { 316 | return $this->command; 317 | } 318 | 319 | /** 320 | * Sets the command to use to build the project. 321 | * 322 | * @param string $command The build command 323 | */ 324 | public function setCommand($command) 325 | { 326 | $this->command = $command; 327 | 328 | return $this; 329 | } 330 | 331 | public static function setDefaultCommand($command) 332 | { 333 | self::$defaultCommand = $command; 334 | } 335 | 336 | /** 337 | * Gets the URL pattern to use to link to commits. 338 | * 339 | * @return string The URL pattern 340 | */ 341 | public function getUrlPattern() 342 | { 343 | return $this->urlPattern; 344 | } 345 | 346 | /** 347 | * Sets the URL pattern to use to link to commits. 348 | * 349 | * In a pattern, you can use the "%commit%" placeholder to reference 350 | * the commit SHA1. 351 | * 352 | * @return string The URL pattern 353 | */ 354 | public function setUrlPattern($pattern) 355 | { 356 | $this->urlPattern = $pattern; 357 | 358 | return $this; 359 | } 360 | 361 | // code derived from http://php.vrana.cz/vytvoreni-pratelskeho-url.php 362 | private function slugify($text) 363 | { 364 | // replace non letter or digits by - 365 | $text = preg_replace('#[^\\pL\d]+#u', '-', $text); 366 | 367 | // trim 368 | $text = trim($text, '-'); 369 | 370 | // transliterate 371 | if (function_exists('iconv')) { 372 | $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text); 373 | } 374 | 375 | // lowercase 376 | $text = strtolower($text); 377 | 378 | // remove unwanted characters 379 | $text = preg_replace('#[^-\w]+#', '', $text); 380 | 381 | if (empty($text)) { 382 | throw new \RuntimeException(sprintf('Unable to compute a slug for "%s". Define it explicitly.', $text)); 383 | } 384 | 385 | return $text; 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | load($classes, __DIR__.'/build', 'sismo', false); 155 | 156 | $classes = str_replace(' '$base/ccmonitor.twig.xml', 171 | 'error.twig' => '$base/error.twig', 172 | 'layout.twig' => '$base/layout.twig', 173 | 'project.twig' => '$base/project.twig', 174 | 'projects.twig' => '$base/projects.twig', 175 | ); 176 | 177 | return \$templates[\$name]; 178 | } 179 | public function isFresh(\$name, \$time) { return true; } 180 | } 181 | 182 | \$app['twig.loader'] = \$app->share(function () { return new FakeTwigLoader(); }); 183 | } 184 | EOF; 185 | 186 | $templates = ''; 187 | $twig = new Twig_Environment(new Twig_Loader_Filesystem(__DIR__.'/src/templates'), array('cache' => sys_get_temp_dir())); 188 | $twig->addExtension(new Symfony\Bridge\Twig\Extension\RoutingExtension(new Symfony\Component\Routing\Generator\UrlGenerator(new Symfony\Component\Routing\RouteCollection(), new Symfony\Component\Routing\RequestContext()))); 189 | foreach (array('ccmonitor.twig.xml', 'error.twig', 'layout.twig', 'project.twig', 'projects.twig') as $name) { 190 | $templates .= str_replace('compileSource($twig->getLoader()->getSource($name), $name)); 191 | } 192 | $templates = 'namespace {'.str_replace('basepath', 'baseurl', $templates).'}'; 193 | 194 | $cli = 'namespace {'.str_replace('get(\'/css/sismo.css\', function() { return new Response(\'%s\', 200, array(\'Content-Type\' => \'text/css\')); });', str_replace("'", "\\'", file_get_contents(__DIR__.'/web/css/sismo.css'))); 201 | foreach (array('failed.png', 'header.png', 'hr.png', 'nobuild.png', 'success.png', 'sensio-labs-product.png') as $image) { 202 | $assets .= sprintf('$app->get(\'/images/%s\', function() { return new Response(base64_decode(\'%s\'), 200, array(\'Content-Type\' => \'image/png\')); });', $image, base64_encode(file_get_contents(__DIR__.'/web/images/'.$image))); 203 | } 204 | $assets = 'namespace { use Symfony\Component\HttpFoundation\Response; '.$assets.'}'; 205 | 206 | $content = "run(); 217 | } else { 218 | \$app->run(); 219 | } 220 | } 221 | "; 222 | 223 | // remove require_once calls 224 | $content = preg_replace('#require_once[^;]+?;#', '', $content); 225 | 226 | file_put_contents(__DIR__.'/build/sismo.php', $content); 227 | file_put_contents(__DIR__.'/build/sismo.php', 228 | " 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Storage; 13 | 14 | use Sismo\Project; 15 | use Sismo\Commit; 16 | 17 | /** 18 | * Stores projects and builds information. 19 | * 20 | * @author Toni Uebernickel 21 | */ 22 | class PdoStorage implements StorageInterface 23 | { 24 | /** 25 | * An established database connection. 26 | * 27 | * @var \PDO 28 | */ 29 | private $db; 30 | 31 | /** 32 | * Constructor. 33 | * 34 | * @param \PDO $con An established PDO connection. 35 | */ 36 | public function __construct(\PDO $con) 37 | { 38 | $this->db = $con; 39 | $this->db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); 40 | } 41 | 42 | /** 43 | * Create a PdoStorage by establishing a PDO connection. 44 | * 45 | * @throws \PDOException If the attempt to connect to the requested database fails. 46 | * 47 | * @param string $dsn The data source name. 48 | * @param string $username The username to login with. 49 | * @param string $passwd The password of the given user. 50 | * @param array $options Additional options to pass to the PDO driver. 51 | * 52 | * @return PdoStorage The created storage on the defined connection. 53 | */ 54 | public static function create($dsn, $username = null, $passwd = null, array $options = array()) 55 | { 56 | return new self(new \PDO($dsn, $username, $passwd, $options)); 57 | } 58 | 59 | /** 60 | * Retrieves a commit out of a project. 61 | * 62 | * @param Project $project The project this commit is part of. 63 | * @param string $sha The hash of the commit to retrieve. 64 | * 65 | * @return Commit 66 | */ 67 | public function getCommit(Project $project, $sha) 68 | { 69 | $stmt = $this->db->prepare('SELECT slug, sha, author, date, build_date, message, status, output FROM `commit` WHERE slug = :slug AND sha = :sha'); 70 | $stmt->bindValue(':slug', $project->getSlug(), \PDO::PARAM_STR); 71 | $stmt->bindValue(':sha', $sha, \PDO::PARAM_STR); 72 | 73 | if ($stmt->execute()) { 74 | if (false !== $result = $stmt->fetch(\PDO::FETCH_ASSOC)) { 75 | return $this->createCommit($project, $result); 76 | } 77 | } else { 78 | // @codeCoverageIgnoreStart 79 | throw new \RuntimeException(sprintf('Unable to retrieve commit "%s" from project "%s".', $sha, $project), 1); 80 | // @codeCoverageIgnoreEnd 81 | } 82 | 83 | return false; 84 | } 85 | 86 | /** 87 | * Initiate, create and save a new commit. 88 | * 89 | * @param Project $project The project of the new commit. 90 | * @param string $sha The hash of the commit. 91 | * @param string $author The name of the author of the new commit. 92 | * @param \DateTime $date The date the new commit was created originally (e.g. by external resources). 93 | * @param string $message The commit message. 94 | * 95 | * @return Commit The newly created commit. 96 | */ 97 | public function initCommit(Project $project, $sha, $author, \DateTime $date, $message) 98 | { 99 | $stmt = $this->db->prepare('SELECT COUNT(*) FROM `commit` WHERE slug = :slug'); 100 | $stmt->bindValue(':slug', $project->getSlug(), \PDO::PARAM_STR); 101 | 102 | if (false === $stmt->execute()) { 103 | // @codeCoverageIgnoreStart 104 | throw new \RuntimeException(sprintf('Unable to verify existence of commit "%s" from project "%s".', $sha, $project->getName())); 105 | // @codeCoverageIgnoreEnd 106 | } 107 | 108 | if ($stmt->fetchColumn(0)) { 109 | $stmt = $this->db->prepare('UPDATE `commit` SET slug = :slug, sha = :sha, author = :author, date = :date, message = :message, status = :status, output = :output, build_date = :build_date WHERE slug = :slug'); 110 | } else { 111 | $stmt = $this->db->prepare('INSERT INTO `commit` (slug, sha, author, date, message, status, output, build_date) VALUES (:slug, :sha, :author, :date, :message, :status, :output, :build_date)'); 112 | } 113 | 114 | $stmt->bindValue(':slug', $project->getSlug(), \PDO::PARAM_STR); 115 | $stmt->bindValue(':sha', $sha, \PDO::PARAM_STR); 116 | $stmt->bindValue(':author', $author, \PDO::PARAM_STR); 117 | $stmt->bindValue(':date', $date->format('Y-m-d H:i:s'), \PDO::PARAM_STR); 118 | $stmt->bindValue(':message', $message, \PDO::PARAM_STR); 119 | $stmt->bindValue(':status', 'building', \PDO::PARAM_STR); 120 | $stmt->bindValue(':output', '', \PDO::PARAM_STR); 121 | $stmt->bindValue(':build_date', '', \PDO::PARAM_STR); 122 | 123 | if (false === $stmt->execute()) { 124 | // @codeCoverageIgnoreStart 125 | throw new \RuntimeException(sprintf('Unable to save commit "%s" from project "%s".', $sha, $project->getName())); 126 | // @codeCoverageIgnoreEnd 127 | } 128 | 129 | $commit = new Commit($project, $sha); 130 | $commit->setAuthor($author); 131 | $commit->setMessage($message); 132 | $commit->setDate($date); 133 | 134 | return $commit; 135 | } 136 | 137 | /** 138 | * Create or update the information of a project. 139 | * 140 | * If the project is already available, the information of the existing project will be updated. 141 | * 142 | * @param Project $project The project to create or update. 143 | * 144 | * @return StorageInterface $this 145 | */ 146 | public function updateProject(Project $project) 147 | { 148 | $stmt = $this->db->prepare('SELECT COUNT(*) FROM project WHERE slug = :slug'); 149 | $stmt->bindValue(':slug', $project->getSlug(), \PDO::PARAM_STR); 150 | 151 | if (false === $stmt->execute()) { 152 | // @codeCoverageIgnoreStart 153 | throw new \RuntimeException(sprintf('Unable to verify existence of project "%s".', $project->getName())); 154 | // @codeCoverageIgnoreEnd 155 | } 156 | 157 | if ($stmt->fetchColumn(0)) { 158 | $stmt = $this->db->prepare('UPDATE project SET slug = :slug, name = :name, repository = :repository, branch = :branch, command = :command, url_pattern = :url_pattern WHERE slug = :slug'); 159 | } else { 160 | $stmt = $this->db->prepare('INSERT INTO project (slug, name, repository, branch, command, url_pattern) VALUES (:slug, :name, :repository, :branch, :command, :url_pattern)'); 161 | } 162 | 163 | $stmt->bindValue(':slug', $project->getSlug(), \PDO::PARAM_STR); 164 | $stmt->bindValue(':name', $project->getName(), \PDO::PARAM_STR); 165 | $stmt->bindValue(':repository', $project->getRepository(), \PDO::PARAM_STR); 166 | $stmt->bindValue(':branch', $project->getBranch(), \PDO::PARAM_STR); 167 | $stmt->bindValue(':command', $project->getCommand(), \PDO::PARAM_STR); 168 | $stmt->bindValue(':url_pattern', $project->getUrlPattern(), \PDO::PARAM_STR); 169 | 170 | if (false === $stmt->execute()) { 171 | // @codeCoverageIgnoreStart 172 | throw new \RuntimeException(sprintf('Unable to save project "%s".', $project->getName())); 173 | // @codeCoverageIgnoreEnd 174 | } 175 | 176 | // related commits 177 | $stmt = $this->db->prepare('SELECT sha, author, date, build_date, message, status, output FROM `commit` WHERE slug = :slug ORDER BY `status` = "building" DESC, build_date DESC LIMIT 100'); 178 | $stmt->bindValue(':slug', $project->getSlug(), \PDO::PARAM_STR); 179 | 180 | if (false === $stmt->execute()) { 181 | // @codeCoverageIgnoreStart 182 | throw new \RuntimeException(sprintf('Unable to get latest commit for project "%s".', $project->getName())); 183 | // @codeCoverageIgnoreEnd 184 | } 185 | 186 | $commits = array(); 187 | while ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) { 188 | $commits[] = $this->createCommit($project, $result); 189 | } 190 | 191 | $project->setCommits($commits); 192 | 193 | // project building? 194 | $stmt = $this->db->prepare('SELECT COUNT(*) AS count FROM `commit` WHERE slug = :slug AND status = "building"'); 195 | $stmt->bindValue(':slug', $project->getSlug(), \PDO::PARAM_STR); 196 | 197 | $building = false; 198 | if ($stmt->execute() and intval($stmt->fetchColumn(0))) { 199 | $building = true; 200 | } 201 | 202 | $project->setBuilding($building); 203 | } 204 | 205 | /** 206 | * Update the commits information. 207 | * 208 | * The commit is identified by its sha hash. 209 | * 210 | * @param Commit $commit 211 | * 212 | * @return StorageInterface $this 213 | */ 214 | public function updateCommit(Commit $commit) 215 | { 216 | $stmt = $this->db->prepare('UPDATE `commit` SET status = :status, output = :output, build_date = :current_date WHERE slug = :slug AND sha = :sha'); 217 | $stmt->bindValue(':slug', $commit->getProject()->getSlug(), \PDO::PARAM_STR); 218 | $stmt->bindValue(':sha', $commit->getSha(), \PDO::PARAM_STR); 219 | $stmt->bindValue(':status', $commit->getStatusCode(), \PDO::PARAM_STR); 220 | $stmt->bindValue(':output', $commit->getOutput(), \PDO::PARAM_STR); 221 | $stmt->bindValue(':current_date', date('Y-m-d H:i:s'), \PDO::PARAM_STR); 222 | 223 | if (false === $stmt->execute()) { 224 | // @codeCoverageIgnoreStart 225 | throw new \RuntimeException(sprintf('Unable to save build "%s@%s".', $commit->getProject()->getName(), $commit->getSha())); 226 | // @codeCoverageIgnoreEnd 227 | } 228 | } 229 | 230 | private function createCommit($project, $result) 231 | { 232 | $commit = new Commit($project, $result['sha']); 233 | $commit->setAuthor($result['author']); 234 | $commit->setMessage($result['message']); 235 | $commit->setDate(\DateTime::createFromFormat('Y-m-d H:i:s', $result['date'])); 236 | if ($result['build_date']) { 237 | $commit->setBuildDate(\DateTime::createFromFormat('Y-m-d H:i:s', $result['build_date'])); 238 | } 239 | $commit->setStatusCode($result['status']); 240 | $commit->setOutput($result['output']); 241 | 242 | return $commit; 243 | } 244 | 245 | /** 246 | * Shutdown the storage and all of its external resources. 247 | * 248 | * @return StorageInterface $this 249 | */ 250 | public function close() 251 | { 252 | unset($this->db); 253 | } 254 | 255 | public function __destruct() 256 | { 257 | $this->close(); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/console.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | use Sismo\Sismo; 13 | use Sismo\BuildException; 14 | use Symfony\Component\Console\Application; 15 | use Symfony\Component\Console\Input\InputInterface; 16 | use Symfony\Component\Console\Input\InputArgument; 17 | use Symfony\Component\Console\Input\InputOption; 18 | use Symfony\Component\Console\Output\OutputInterface; 19 | use Symfony\Component\Process\ProcessBuilder; 20 | 21 | $console = new Application('Sismo', Sismo::VERSION); 22 | $console 23 | ->register('output') 24 | ->setDefinition(array( 25 | new InputArgument('slug', InputArgument::REQUIRED, 'Project slug'), 26 | )) 27 | ->setDescription('Displays the latest output for a project') 28 | ->setHelp(<<%command.name% command displays the latest output for a project: 30 | 31 | php %command.full_name% twig 32 | EOF 33 | ) 34 | ->setCode(function (InputInterface $input, OutputInterface $output) use ($app) { 35 | $sismo = $app['sismo']; 36 | $slug = $input->getArgument('slug'); 37 | if (!$sismo->hasProject($slug)) { 38 | $output->writeln(sprintf('Project "%s" does not exist.', $slug)); 39 | 40 | return 1; 41 | } 42 | 43 | $project = $sismo->getProject($slug); 44 | 45 | if (!$project->getLatestCommit()) { 46 | $output->writeln(sprintf('Project "%s" has never been built yet.', $slug)); 47 | 48 | return 2; 49 | } 50 | 51 | $output->write($project->getLatestCommit()->getOutput()); 52 | 53 | $now = new \DateTime(); 54 | $diff = $now->diff($project->getLatestCommit()->getBuildDate()); 55 | if ($m = $diff->format('%i')) { 56 | $time = $m.' minutes'; 57 | } else { 58 | $time = $diff->format('%s').' seconds'; 59 | } 60 | $output->writeln(''); 61 | $output->writeln(sprintf('This output was generated by Sismo %s ago', $time)); 62 | }) 63 | ; 64 | 65 | $console 66 | ->register('projects') 67 | ->setDescription('List available projects') 68 | ->setHelp(<<%command.name% command displays the available projects Sismo can build. 70 | EOF 71 | ) 72 | ->setCode(function (InputInterface $input, OutputInterface $output) use ($app) { 73 | $sismo = $app['sismo']; 74 | 75 | $projects = array(); 76 | $width = 0; 77 | foreach ($sismo->getProjects() as $slug => $project) { 78 | $projects[$slug] = $project->getName(); 79 | $width = strlen($project->getName()) > $width ? strlen($project->getName()) : $width; 80 | } 81 | $width += 2; 82 | 83 | $output->writeln(''); 84 | $output->writeln('Available projects:'); 85 | foreach ($projects as $slug => $project) { 86 | $output->writeln(sprintf(" %-${width}s %s", $slug, $project)); 87 | } 88 | 89 | $output->writeln(''); 90 | }) 91 | ; 92 | 93 | $console 94 | ->register('build') 95 | ->setDefinition(array( 96 | new InputArgument('slug', InputArgument::OPTIONAL, 'Project slug'), 97 | new InputArgument('sha', InputArgument::OPTIONAL, 'Commit sha'), 98 | new InputOption('force', '', InputOption::VALUE_NONE, 'Force the build'), 99 | new InputOption('local', '', InputOption::VALUE_NONE, 'Disable remote sync'), 100 | new InputOption('silent', '', InputOption::VALUE_NONE, 'Disable notifications'), 101 | new InputOption('timeout', '', InputOption::VALUE_REQUIRED, 'Time limit'), 102 | new InputOption('data-path', '', InputOption::VALUE_REQUIRED, 'The data path'), 103 | new InputOption('config-file', '', InputOption::VALUE_REQUIRED, 'The config file'), 104 | )) 105 | ->setDescription('Build projects') 106 | ->setHelp(<<%command.name% command builds the latest commit 108 | of all configured projects one after the other: 109 | 110 | php %command.full_name% 111 | 112 | The command loads project configurations from 113 | ~/.sismo/config.php. Change it with the 114 | --config-file option: 115 | 116 | php %command.full_name% --config-file=/path/to/config.php 117 | 118 | Data (repository, DB, ...) are stored in ~/.sismo/data/. 119 | The --data-path option allows you to change the default: 120 | 121 | php %command.full_name% --data-path=/path/to/data 122 | 123 | Pass the project slug to build a specific project: 124 | 125 | php %command.full_name% twig 126 | 127 | Force a specific commit to be built by passing the SHA: 128 | 129 | php %command.full_name% twig a1ef34 130 | 131 | Use --force to force the built even if it has already been 132 | built previously: 133 | 134 | php %command.full_name% twig a1ef34 --force 135 | 136 | Disable notifications with --silent: 137 | 138 | php %command.full_name% twig a1ef34 --silent 139 | 140 | Disable repository synchonization with --local: 141 | 142 | php %command.full_name% twig a1ef34 --local 143 | 144 | Limit the time (in seconds) spent by the command building projects by using 145 | the --timeout option: 146 | 147 | php %command.full_name% twig --timeout 3600 148 | 149 | When you use this command as a cron job, --timeout can avoid 150 | the command to be run concurrently. Be warned that this is a rough estimate as 151 | the time is only checked between two builds. When a build is started, it won't 152 | be stopped if the time limit is over. 153 | 154 | Use the --verbose option to debug builds in case of a 155 | problem. 156 | EOF 157 | ) 158 | ->setCode(function (InputInterface $input, OutputInterface $output) use ($app) { 159 | if ($input->getOption('data-path')) { 160 | $app['data.path'] = $input->getOption('data-path'); 161 | } 162 | if ($input->getOption('config-file')) { 163 | $app['config.file'] = $input->getOption('config-file'); 164 | } 165 | $sismo = $app['sismo']; 166 | 167 | if ($slug = $input->getArgument('slug')) { 168 | if (!$sismo->hasProject($slug)) { 169 | $output->writeln(sprintf('Project "%s" does not exist.', $slug)); 170 | 171 | return 1; 172 | } 173 | 174 | $projects = array($sismo->getProject($slug)); 175 | } else { 176 | $projects = $sismo->getProjects(); 177 | } 178 | 179 | $start = time(); 180 | $startedOut = false; 181 | $startedErr = false; 182 | $callback = null; 183 | if (OutputInterface::VERBOSITY_VERBOSE === $output->getVerbosity()) { 184 | $callback = function ($type, $buffer) use ($output, &$startedOut, &$startedErr) { 185 | if ('err' === $type) { 186 | if (!$startedErr) { 187 | $output->write("\n ERR "); 188 | $startedErr = true; 189 | $startedOut = false; 190 | } 191 | 192 | $output->write(str_replace("\n", "\n ERR ", $buffer)); 193 | } else { 194 | if (!$startedOut) { 195 | $output->write("\n OUT "); 196 | $startedOut = true; 197 | $startedErr = false; 198 | } 199 | 200 | $output->write(str_replace("\n", "\n OUT ", $buffer)); 201 | } 202 | }; 203 | } 204 | 205 | $flags = 0; 206 | if ($input->getOption('force')) { 207 | $flags = $flags | Sismo::FORCE_BUILD; 208 | } 209 | if ($input->getOption('local')) { 210 | $flags = $flags | Sismo::LOCAL_BUILD; 211 | } 212 | if ($input->getOption('silent')) { 213 | $flags = $flags | Sismo::SILENT_BUILD; 214 | } 215 | 216 | $returnValue = 0; 217 | foreach ($projects as $project) { 218 | // out of time? 219 | if ($input->getOption('timeout') && time() - $start > $input->getOption('timeout')) { 220 | break; 221 | } 222 | 223 | try { 224 | $output->writeln(sprintf('Building Project "%s" (into "%s")', $project, $app['builder']->getBuildDir($project))); 225 | $sismo->build($project, $input->getArgument('sha'), $flags, $callback); 226 | 227 | $output->writeln(''); 228 | } catch (BuildException $e) { 229 | $output->writeln("\n".sprintf('%s', $e->getMessage())); 230 | 231 | $returnValue = 1; 232 | } 233 | } 234 | 235 | return $returnValue; 236 | }) 237 | ; 238 | 239 | $console 240 | ->register('run') 241 | ->setDefinition(array( 242 | new InputArgument('address', InputArgument::OPTIONAL, 'Address:port', 'localhost:9000'), 243 | )) 244 | ->setDescription('Runs Sismo with PHP built-in web server') 245 | ->setHelp(<<%command.name% command runs the embedded Sismo web server: 247 | 248 | %command.full_name% 249 | 250 | You can also customize the default address and port the web server listens to: 251 | 252 | %command.full_name% 127.0.0.1:8080 253 | EOF 254 | ) 255 | ->setCode(function (InputInterface $input, OutputInterface $output) use ($console) { 256 | 257 | if (version_compare(PHP_VERSION, '5.4.0') < 0) { 258 | throw new \Exception('This feature only runs with PHP 5.4.0 or higher.'); 259 | } 260 | 261 | $sismo = __DIR__.'/sismo.php'; 262 | while (!file_exists($sismo)) { 263 | $dialog = $console->getHelperSet()->get('dialog'); 264 | $sismo = $dialog->ask($output, sprintf('I cannot find "%s". What\'s the absoulte path of "sismo.php"? ', $sismo), __DIR__.'/sismo.php'); 265 | } 266 | 267 | $output->writeln(sprintf("Sismo running on %s\n", $input->getArgument('address'))); 268 | 269 | $builder = new ProcessBuilder(array(PHP_BINARY, '-S', $input->getArgument('address'), $sismo)); 270 | 271 | $builder->setWorkingDirectory(getcwd()); 272 | $builder->setTimeout(null); 273 | $builder->getProcess()->run(function ($type, $buffer) use ($output) { 274 | if (OutputInterface::VERBOSITY_VERBOSE === $output->getVerbosity()) { 275 | $output->write($buffer); 276 | } 277 | }); 278 | }) 279 | ; 280 | 281 | return $console; 282 | -------------------------------------------------------------------------------- /tests/Sismo/Tests/SismoTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * This source file is subject to the MIT license that is bundled 9 | * with this source code in the file LICENSE. 10 | */ 11 | 12 | namespace Sismo\Tests; 13 | 14 | use Sismo\Sismo; 15 | use Sismo\Project; 16 | 17 | class SismoTest extends \PHPUnit_Framework_TestCase 18 | { 19 | public function testProject() 20 | { 21 | $sismo = new Sismo($this->getStorage(), $this->getBuilder()); 22 | $this->assertEquals(array(), $sismo->getProjects()); 23 | $this->assertFalse($sismo->hasProject('twig')); 24 | 25 | $sismo->addProject($project = new Project('Twig')); 26 | $this->assertTrue($sismo->hasProject('twig')); 27 | $this->assertSame($project, $sismo->getProject('twig')); 28 | 29 | $sismo->addProject($project1 = new Project('Silex')); 30 | 31 | $this->assertSame(array('twig' => $project, 'silex' => $project1), $sismo->getProjects()); 32 | } 33 | 34 | public function testAddProjectSaveIt() 35 | { 36 | $storage = $this->getStorage(); 37 | $storage->expects($this->once())->method('updateProject'); 38 | 39 | $sismo = new Sismo($storage, $this->getBuilder()); 40 | $sismo->addProject($project = new Project('Twig')); 41 | $sismo->getProject('twig'); 42 | } 43 | 44 | /** 45 | * @expectedException \InvalidArgumentException 46 | */ 47 | public function testGetProjectWhenProjectDoesNotExist() 48 | { 49 | $sismo = new Sismo($this->getStorage(), $this->getBuilder()); 50 | $sismo->getProject('twig'); 51 | } 52 | 53 | public function testBuildIsNotCalledWhenCommitIsAlreadyBuilt() 54 | { 55 | // an already built commit 56 | $commit = $this->getCommit(); 57 | $commit->expects($this->once())->method('isBuilt')->will($this->returnValue(true)); 58 | 59 | // build won't be triggered 60 | $builder = $this->getBuilder(); 61 | $builder->expects($this->never())->method('build'); 62 | 63 | $sismo = new Sismo($this->getStorage($commit), $builder); 64 | $sismo->build($this->getProject()); 65 | } 66 | 67 | public function testBuildIsForcedWhenCommitIsAlreadyBuiltAndForceIsTrue() 68 | { 69 | // an already built commit 70 | $commit = $this->getCommit(); 71 | $commit->expects($this->once())->method('isBuilt')->will($this->returnValue(true)); 72 | 73 | // build is triggered because of FORCE_BUILD flags 74 | $builder = $this->getBuilder(); 75 | $builder->expects($this->once())->method('prepare')->will($this->returnValue(array('sha1', 'fabien', '2011-01-01 01:01:01 +0200', 'initial commit'))); 76 | $builder->expects($this->once())->method('build')->will($this->returnValue($this->getProcess())); 77 | 78 | $sismo = new Sismo($this->getStorage($commit), $builder); 79 | $sismo->build($this->getProject(), null, Sismo::FORCE_BUILD); 80 | } 81 | 82 | public function testBuildIsNotCalledWhenProjectIsBuilding() 83 | { 84 | // a project with a running build 85 | $project = $this->getProject(); 86 | $project->expects($this->once())->method('isBuilding')->will($this->returnValue(true)); 87 | 88 | // build won't be triggered 89 | $builder = $this->getBuilder(); 90 | $builder->expects($this->never())->method('build'); 91 | 92 | $sismo = new Sismo($this->getStorage(), $builder); 93 | $sismo->build($project); 94 | } 95 | 96 | public function testBuildIsForcedWhenProjectIsBuildingAndForceIsTrue() 97 | { 98 | // a project with a running build 99 | $project = $this->getProject(); 100 | $project->expects($this->once())->method('isBuilding')->will($this->returnValue(true)); 101 | 102 | // build is triggered because of FORCE_BUILD flags 103 | $builder = $this->getBuilder(); 104 | $builder->expects($this->once())->method('prepare')->will($this->returnValue(array('sha1', 'fabien', '2011-01-01 01:01:01 +0200', 'initial commit'))); 105 | $builder->expects($this->once())->method('build')->will($this->returnValue($this->getProcess())); 106 | 107 | $commit = $this->getCommit(); 108 | $commit->expects($this->once())->method('isBuilt')->will($this->returnValue(false)); 109 | 110 | $sismo = new Sismo($this->getStorage($commit), $builder); 111 | $sismo->build($project, null, Sismo::FORCE_BUILD); 112 | } 113 | 114 | public function testBuildIsForcedWhenCommitDoesNotExist() 115 | { 116 | // build is triggered as commit does not exist 117 | $builder = $this->getBuilder(); 118 | $builder->expects($this->once())->method('prepare')->will($this->returnValue(array('sha1', 'fabien', '2011-01-01 01:01:01 +0200', 'initial commit'))); 119 | $builder->expects($this->once())->method('build')->will($this->returnValue($this->getProcess())); 120 | 121 | $storage = $this->getStorage(); 122 | $storage->expects($this->any())->method('initCommit')->will($this->returnValue($this->getCommit())); 123 | 124 | $sismo = new Sismo($storage, $builder); 125 | $sismo->build($this->getProject()); 126 | } 127 | 128 | public function testBuildWithNotifiers() 129 | { 130 | // build is triggered as commit does not exist 131 | $builder = $this->getBuilder(); 132 | $builder->expects($this->once())->method('prepare')->will($this->returnValue(array('sha1', 'fabien', '2011-01-01 01:01:01 +0200', 'initial commit'))); 133 | $builder->expects($this->once())->method('build')->will($this->returnValue($this->getProcess())); 134 | 135 | $storage = $this->getStorage(); 136 | $storage->expects($this->any())->method('initCommit')->will($this->returnValue($this->getCommit())); 137 | 138 | // notifier will be called 139 | $notifier = $this->getNotifier(); 140 | $notifier->expects($this->once())->method('notify'); 141 | 142 | $project = $this->getMockBuilder('Sismo\Project')->disableOriginalConstructor()->getMock(); 143 | $project->expects($this->once())->method('getNotifiers')->will($this->returnValue(array($notifier))); 144 | 145 | $sismo = new Sismo($storage, $builder); 146 | $sismo->build($project); 147 | } 148 | 149 | public function testBuildWithNotifiersWhenSilentIsTrue() 150 | { 151 | // build is triggered as commit does not exist 152 | $builder = $this->getBuilder(); 153 | $builder->expects($this->once())->method('prepare')->will($this->returnValue(array('sha1', 'fabien', '2011-01-01 01:01:01 +0200', 'initial commit'))); 154 | $builder->expects($this->once())->method('build')->will($this->returnValue($this->getProcess())); 155 | 156 | $storage = $this->getStorage(); 157 | $storage->expects($this->any())->method('initCommit')->will($this->returnValue($this->getCommit())); 158 | 159 | // notifier won't be called 160 | $notifier = $this->getNotifier(); 161 | $notifier->expects($this->never())->method('notify'); 162 | 163 | // notifiers won't be get from project 164 | $project = $this->getMockBuilder('Sismo\Project')->disableOriginalConstructor()->getMock(); 165 | $project->expects($this->never())->method('getNotifiers')->will($this->returnValue(array($notifier))); 166 | 167 | $sismo = new Sismo($storage, $builder); 168 | $sismo->build($project, null, Sismo::SILENT_BUILD); 169 | } 170 | 171 | public function testCommitResultForSuccessBuild() 172 | { 173 | // build is a success 174 | $process = $this->getProcess(); 175 | $process->expects($this->once())->method('isSuccessful')->will($this->returnValue(true)); 176 | $process->expects($this->once())->method('getOutput')->will($this->returnValue('foo')); 177 | 178 | // build is triggered as commit does not exist 179 | $builder = $this->getBuilder(); 180 | $builder->expects($this->once())->method('prepare')->will($this->returnValue(array('sha1', 'fabien', '2011-01-01 01:01:01 +0200', 'initial commit'))); 181 | $builder->expects($this->once())->method('build')->will($this->returnValue($process)); 182 | 183 | // check commit status 184 | $commit = $this->getCommit(); 185 | $commit->expects($this->once())->method('setStatusCode')->with($this->equalTo('success')); 186 | $commit->expects($this->once())->method('setOutput')->with($this->equalTo('foo')); 187 | 188 | // check that storage is updated 189 | $storage = $this->getStorage(); 190 | $storage->expects($this->any())->method('initCommit')->will($this->returnValue($commit)); 191 | $storage->expects($this->once())->method('updateCommit')->with($this->equalTo($commit)); 192 | 193 | $sismo = new Sismo($storage, $builder); 194 | $sismo->build($this->getProject()); 195 | } 196 | 197 | public function testCommitResultForSuccessFail() 198 | { 199 | // build is a fail 200 | $process = $this->getProcess(); 201 | $process->expects($this->once())->method('isSuccessful')->will($this->returnValue(false)); 202 | $process->expects($this->once())->method('getOutput')->will($this->returnValue('foo')); 203 | $process->expects($this->once())->method('getErrorOutput')->will($this->returnValue('bar')); 204 | 205 | // build is triggered as commit does not exist 206 | $builder = $this->getBuilder(); 207 | $builder->expects($this->once())->method('prepare')->will($this->returnValue(array('sha1', 'fabien', '2011-01-01 01:01:01 +0200', 'initial commit'))); 208 | $builder->expects($this->once())->method('build')->will($this->returnValue($process)); 209 | 210 | // check commit status 211 | $commit = $this->getCommit(); 212 | $commit->expects($this->once())->method('setStatusCode')->with($this->equalTo('failed')); 213 | $commit->expects($this->once())->method('setOutput')->with($this->matchesRegularExpression('/foo.*bar/s')); 214 | 215 | // check that storage is updated 216 | $storage = $this->getStorage(); 217 | $storage->expects($this->any())->method('initCommit')->will($this->returnValue($commit)); 218 | $storage->expects($this->once())->method('updateCommit')->with($this->equalTo($commit)); 219 | 220 | $sismo = new Sismo($storage, $builder); 221 | $sismo->build($this->getProject()); 222 | } 223 | 224 | private function getBuilder() 225 | { 226 | return $this->getMockBuilder('Sismo\Builder')->disableOriginalConstructor()->getMock(); 227 | } 228 | 229 | private function getNotifier() 230 | { 231 | return $this->getMockBuilder('Sismo\Notifier\Notifier')->disableOriginalConstructor()->getMock(); 232 | } 233 | 234 | private function getProject() 235 | { 236 | $project = $this->getMockBuilder('Sismo\Project')->disableOriginalConstructor()->getMock(); 237 | $project->expects($this->any())->method('getNotifiers')->will($this->returnValue(array())); 238 | 239 | return $project; 240 | } 241 | 242 | private function getCommit() 243 | { 244 | return $this->getMockBuilder('Sismo\Commit')->disableOriginalConstructor()->getMock(); 245 | } 246 | 247 | private function getProcess() 248 | { 249 | return $this->getMockBuilder('Symfony\Component\Process\Process')->disableOriginalConstructor()->getMock(); 250 | } 251 | 252 | private function getStorage($commit = null) 253 | { 254 | $storage = $this->getMockBuilder('Sismo\Storage\Storage')->disableOriginalConstructor()->getMock(); 255 | if (null !== $commit) { 256 | $storage->expects($this->once())->method('getCommit')->will($this->returnValue($commit)); 257 | $storage->expects($this->any())->method('initCommit')->will($this->returnValue($commit)); 258 | } 259 | 260 | return $storage; 261 | } 262 | } 263 | --------------------------------------------------------------------------------