diff options
127 files changed, 18238 insertions, 17 deletions
diff --git a/Zotlabs/Module/Admin.php b/Zotlabs/Module/Admin.php index e1eaa6e0e..d23b508f0 100644 --- a/Zotlabs/Module/Admin.php +++ b/Zotlabs/Module/Admin.php @@ -1,5 +1,8 @@ <?php namespace Zotlabs\Module; + +use PHPGit\Git as Git; + /** * @file mod/admin.php * @brief Hubzilla's admin controller. @@ -36,6 +39,10 @@ class Admin extends \Zotlabs\Web\Controller { $this->admin_page_channels_post($a); break; case 'plugins': + if (argc() > 2 && argv(2) === 'addrepo') { + $this->admin_page_plugins_post('addrepo'); + break; + } if (argc() > 2 && is_file("addon/" . argv(2) . "/" . argv(2) . ".php")){ @include_once("addon/" . argv(2) . "/" . argv(2) . ".php"); @@ -1343,6 +1350,16 @@ class Admin extends \Zotlabs\Web\Controller { usort($plugins,'self::plugin_sort'); + + $admin_plugins_add_repo_form= replace_macros( + get_markup_template('admin_plugins_addrepo.tpl'), array( + '$post' => 'admin/plugins/addrepo', + '$desc' => t('Enter the public git repository URL of the plugin repo.'), + '$repoURL' => array('repoURL', t('Plugin repo git URL'), '', ''), + '$submit' => t('Download Plugin Repo') + ) + ); + $t = get_markup_template('admin_plugins.tpl'); return replace_macros($t, array( '$title' => t('Administration'), @@ -1353,6 +1370,9 @@ class Admin extends \Zotlabs\Web\Controller { '$plugins' => $plugins, '$disabled' => t('Disabled - version incompatibility'), '$form_security_token' => get_form_security_token('admin_plugins'), + '$addrepo' => t('Add Plugin Repo'), + '$expandform' => false, + '$form' => $admin_plugins_add_repo_form )); } @@ -1647,6 +1667,58 @@ class Admin extends \Zotlabs\Web\Controller { )); } + function admin_page_plugins_post($action) { + switch($action) { + case 'addrepo': + + if(array_key_exists('repoURL',$_REQUEST)) { + require __DIR__ . '/../../library/PHPGit.autoload.php'; // Load PHPGit dependencies + logger('Repo URL submitted: ' . $_REQUEST['repoURL']); + $repoURL = $_REQUEST['repoURL']; + $urlpath = parse_url($repoURL, PHP_URL_PATH); + $lastslash = strrpos($urlpath, '/') + 1; + $gitext = strrpos($urlpath, '.'); + if ($gitext) { + $reponame = substr($urlpath, $lastslash, $gitext - $lastslash); + } else { + logger('invalid git repo URL'); + notice('Invalid git repo URL'); + break; + } + $storepath = realpath(__DIR__ . '/../../store/'); + //logger('storepath: ' . $storepath); + $repopath = $storepath . '/pluginrepos/' . $reponame; + + if (!file_exists($repopath)) { + //logger('repopath does not exist'); + if (mkdir($repopath, 0770, true)) { + //logger('repopath created'); + $git = new Git(); + //logger('new git object created'); + $cloned = $git->clone($repoURL, $repopath); + if (!$cloned) { + logger('git clone failed'); + notice('Repo coule not be cloned. Filesystem path error.'); + json_return_and_die(array('message' => 'Repo coule not be cloned. Filesystem path error.', 'success' => false)); + } + json_return_and_die(array('message' => 'Successfully cloned to: ' . $repopath , 'success' => true)); + } else { + logger('repopath could not be created'); + notice('Repo coule not be cloned. Filesystem path error.'); + json_return_and_die(array('message' => 'Repo coule not be cloned. Filesystem path error', 'success' => false)); + } + } else { + json_return_and_die(array('message' => 'Repo already exists at: ' . $repopath, 'success' => true)); + } + } else { + json_return_and_die(array('message' => 'No repo URL provided', 'success' => false)); + } + break; + default: + break; + } + } + function admin_page_profs_post(&$a) { if(array_key_exists('basic',$_REQUEST)) { diff --git a/Zotlabs/Storage/Git.php b/Zotlabs/Storage/Git.php new file mode 100644 index 000000000..f9b5a6c79 --- /dev/null +++ b/Zotlabs/Storage/Git.php @@ -0,0 +1,19 @@ +<?php + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +namespace Zotlabs\Storage; + + +require __DIR__ . '/../../library/PHPGit.autoload.php'; // Load PHPGit dependencies + +/** + * Description of Git + * + * @author andrew + */ +class Git {} diff --git a/library/PHPGit.autoload.php b/library/PHPGit.autoload.php new file mode 100644 index 000000000..7ad2971da --- /dev/null +++ b/library/PHPGit.autoload.php @@ -0,0 +1,7 @@ +<?php + +// autoload.php @generated by Composer + +require_once __DIR__ . '/composer' . '/autoload_real.php'; + +return ComposerAutoloaderInit929ad7f4e2f44798d12d2ab06db7b39b::getLoader();
\ No newline at end of file diff --git a/library/composer/ClassLoader.php b/library/composer/ClassLoader.php new file mode 100644 index 000000000..4e05d3b15 --- /dev/null +++ b/library/composer/ClassLoader.php @@ -0,0 +1,413 @@ +<?php + +/* + * This file is part of Composer. + * + * (c) Nils Adermann <naderman@naderman.de> + * Jordi Boggiano <j.boggiano@seld.be> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0 class loader + * + * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier <fabien@symfony.com> + * @author Jordi Boggiano <j.boggiano@seld.be> + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + + private $classMapAuthoritative = false; + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-0 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731 + if ('\\' == $class[0]) { + $class = substr($class, 1); + } + + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative) { + return false; + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if ($file === null && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if ($file === null) { + // Remember that this class does not exist. + return $this->classMap[$class] = false; + } + + return $file; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) { + if (0 === strpos($class, $prefix)) { + foreach ($this->prefixDirsPsr4[$prefix] as $dir) { + if (is_file($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (is_file($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (is_file($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (is_file($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/library/composer/autoload_classmap.php b/library/composer/autoload_classmap.php new file mode 100644 index 000000000..7a91153b0 --- /dev/null +++ b/library/composer/autoload_classmap.php @@ -0,0 +1,9 @@ +<?php + +// autoload_classmap.php @generated by Composer + +$vendorDir = dirname(dirname(__FILE__)); +$baseDir = dirname($vendorDir); + +return array( +); diff --git a/library/composer/autoload_namespaces.php b/library/composer/autoload_namespaces.php new file mode 100644 index 000000000..6c63a8ed1 --- /dev/null +++ b/library/composer/autoload_namespaces.php @@ -0,0 +1,11 @@ +<?php + +// autoload_namespaces.php @generated by Composer + +$vendorDir = dirname(dirname(__FILE__)); +$baseDir = dirname($vendorDir); + +return array( + 'PHPGit' => array($baseDir . '/src'), + '' => array($vendorDir . '/kzykhys/git/src'), +); diff --git a/library/composer/autoload_psr4.php b/library/composer/autoload_psr4.php new file mode 100644 index 000000000..1ccbde8ed --- /dev/null +++ b/library/composer/autoload_psr4.php @@ -0,0 +1,11 @@ +<?php + +// autoload_psr4.php @generated by Composer + +$vendorDir = dirname(dirname(__FILE__)); +$baseDir = dirname($vendorDir); + +return array( + 'Symfony\\Component\\Process\\' => array($vendorDir . '/symfony/process'), + 'Symfony\\Component\\OptionsResolver\\' => array($vendorDir . '/symfony/options-resolver'), +); diff --git a/library/composer/autoload_real.php b/library/composer/autoload_real.php new file mode 100644 index 000000000..59b376b09 --- /dev/null +++ b/library/composer/autoload_real.php @@ -0,0 +1,50 @@ +<?php + +// autoload_real.php @generated by Composer + +class ComposerAutoloaderInit929ad7f4e2f44798d12d2ab06db7b39b +{ + private static $loader; + + public static function loadClassLoader($class) + { + if ('Composer\Autoload\ClassLoader' === $class) { + require __DIR__ . '/ClassLoader.php'; + } + } + + public static function getLoader() + { + if (null !== self::$loader) { + return self::$loader; + } + + spl_autoload_register(array('ComposerAutoloaderInit929ad7f4e2f44798d12d2ab06db7b39b', 'loadClassLoader'), true, true); + self::$loader = $loader = new \Composer\Autoload\ClassLoader(); + spl_autoload_unregister(array('ComposerAutoloaderInit929ad7f4e2f44798d12d2ab06db7b39b', 'loadClassLoader')); + + $map = require __DIR__ . '/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + + $loader->register(true); + + return $loader; + } +} + +function composerRequire929ad7f4e2f44798d12d2ab06db7b39b($file) +{ + require $file; +} diff --git a/library/composer/installed.json b/library/composer/installed.json new file mode 100644 index 000000000..23dbdb9c7 --- /dev/null +++ b/library/composer/installed.json @@ -0,0 +1,157 @@ +[ + { + "name": "symfony/process", + "version": "v2.8.0", + "version_normalized": "2.8.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "1b988a88e3551102f3c2d9e1d47a18c3a78d6312" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/1b988a88e3551102f3c2d9e1d47a18c3a78d6312", + "reference": "1b988a88e3551102f3c2d9e1d47a18c3a78d6312", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "time": "2015-11-30 12:35:10", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com" + }, + { + "name": "symfony/options-resolver", + "version": "v2.8.0", + "version_normalized": "2.8.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "e7f62cf7d9e48238299cfa5d0556aee8cff1e075" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/e7f62cf7d9e48238299cfa5d0556aee8cff1e075", + "reference": "e7f62cf7d9e48238299cfa5d0556aee8cff1e075", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "time": "2015-11-18 13:45:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony OptionsResolver Component", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ] + }, + { + "name": "kzykhys/git", + "version": "dev-master", + "version_normalized": "9999999-dev", + "source": { + "type": "git", + "url": "https://github.com/kzykhys/PHPGit.git", + "reference": "8b3ee3147d58e0aee62610f0aebcd30eb076ad5d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kzykhys/PHPGit/zipball/8b3ee3147d58e0aee62610f0aebcd30eb076ad5d", + "reference": "8b3ee3147d58e0aee62610f0aebcd30eb076ad5d", + "shasum": "" + }, + "require": { + "php": ">=5.3.2", + "symfony/options-resolver": ">=2.3", + "symfony/process": ">=2.3" + }, + "require-dev": { + "symfony/filesystem": ">=2.3" + }, + "time": "2014-02-27 13:14:59", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "source", + "autoload": { + "psr-0": { + "": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kazuyuki Hayashi", + "email": "hayashi@valnur.net" + } + ], + "description": "A Git wrapper for PHP5.3+" + } +] diff --git a/library/kzykhys/git/.travis.yml b/library/kzykhys/git/.travis.yml new file mode 100644 index 000000000..ff4164104 --- /dev/null +++ b/library/kzykhys/git/.travis.yml @@ -0,0 +1,20 @@ +language: php + +php: + - 5.5 + - 5.4 + - 5.3 + +before_script: + - composer require --no-update satooshi/php-coveralls:dev-master@dev sensiolabs/security-checker:dev-master + - composer update --dev --no-interaction + - git config --global user.name "John Doe" + - git config --global user.email "example@example.com" + +script: + - mkdir -p build/logs + - phpunit -c phpunit.xml.dist --coverage-clover build/logs/clover.xml + - php vendor/bin/security-checker security:check composer.lock + +after_script: + - php vendor/bin/coveralls
\ No newline at end of file diff --git a/library/kzykhys/git/README.md b/library/kzykhys/git/README.md new file mode 100644 index 000000000..b2f6635af --- /dev/null +++ b/library/kzykhys/git/README.md @@ -0,0 +1,1347 @@ +PHPGit - A Git wrapper for PHP5.3+ +================================== + +[![Latest Unstable Version](https://poser.pugx.org/kzykhys/git/v/unstable.png)](https://packagist.org/packages/kzykhys/git) +[![Build Status](https://travis-ci.org/kzykhys/PHPGit.png?branch=master)](https://travis-ci.org/kzykhys/PHPGit) +[![Coverage Status](https://coveralls.io/repos/kzykhys/PHPGit/badge.png)](https://coveralls.io/r/kzykhys/PHPGit) +[![SensioLabsInsight](https://insight.sensiolabs.com/projects/04f10b57-a113-47ad-8dda-9a6dacbb079f/mini.png)](https://insight.sensiolabs.com/projects/04f10b57-a113-47ad-8dda-9a6dacbb079f) + +Requirements +------------ + +* PHP5.3 +* Git + +Installation +------------ + +Update your composer.json and run `composer update` + +``` json +{ + "require": { + "kzykhys/git": "dev-master" + } +} +``` + +Basic Usage +----------- + +``` php +<?php + +require __DIR__ . '/vendor/autoload.php'; + +$git = new PHPGit\Git(); +$git->clone('https://github.com/kzykhys/PHPGit.git', '/path/to/repo'); +$git->setRepository('/path/to/repo'); +$git->remote->add('production', 'git://example.com/your/repo.git'); +$git->add('README.md'); +$git->commit('Adds README.md'); +$git->checkout('release'); +$git->merge('master'); +$git->push(); +$git->push('production', 'release'); +$git->tag->create('v1.0.1', 'release'); + +foreach ($git->tree('release') as $object) { + if ($object['type'] == 'blob') { + echo $git->show($object['file']); + } +} +``` + +API +--- + +### Git commands + +* [git add](#git-add) + * $git->[add](#git-addstringarraytraversable-file-array-options--)(_string|array|\Traversable_ $file, _array_ $options = []) +* [git archive](#git-archive) + * $git->[archive](#git-archivestring-file-string-tree--null-stringarraytraversable-path--null-array-options--)(_string_ $file, _string_ $tree = null, _string|array|\Traversable_ $path = null, _array_ $options = []) +* [git branch](#git-branch) + * $git->[branch](#git-brancharray-options--)(_array_ $options = []) + * $git->branch->[create](#git-branch-createstring-branch-string-startpoint--null-array-options--)(_string_ $branch, _string_ $startPoint = null, _array_ $options = []) + * $git->branch->[move](#git-branch-movestring-branch-string-newbranch-array-options--)(_string_ $branch, _string_ $newBranch, _array_ $options = []) + * $git->branch->[delete](#git-branch-deletestring-branch-array-options--)(_string_ $branch, _array_ $options = []) +* [git cat-file](#git-cat-file) + * $git->cat->[blob](#git-cat-blobstring-object)(_string_ $object) + * $git->cat->[type](#git-cat-typestring-object)(_string_ $object) + * $git->cat->[size](#git-cat-sizestring-object)(_string_ $object) +* [git checkout](#git-checkout) + * $git->[checkout](#git-checkoutstring-branch-array-options--)(_string_ $branch, _array_ $options = []) + * $git->checkout->[create](#git-checkout-createstring-branch-string-startpoint--null-array-options--)(_string_ $branch, _string_ $startPoint = null, _array_ $options = []) + * $git->checkout->[orphan](#git-checkout-orphanstring-branch-string-startpoint--null-array-options--)(_string_ $branch, _string_ $startPoint = null, _array_ $options = []) +* [git clone](#git-clone) + * $git->[clone](#git-clonestring-repository-string-path--null-array-options--)(_string_ $repository, _string_ $path = null, _array_ $options = []) +* [git commit](#git-commit) + * $git->[commit](#git-commitstring-message-array-options--)(_string_ $message, _array_ $options = []) +* [git config](#git-config) + * $git->[config](#git-configarray-options--)(_array_ $options = []) + * $git->config->[set](#git-config-setstring-name-string-value-array-options--)(_string_ $name, _string_ $value, _array_ $options = []) + * $git->config->[add](#git-config-addstring-name-string-value-array-options--)(_string_ $name, _string_ $value, _array_ $options = []) +* [git describe](#git-describe) + * $git->[describe](#git-describestring-committish--null-array-options--)(_string_ $committish = null, _array_ $options = []) + * $git->describe->[tags](#git-describe-tagsstring-committish--null-array-options--)(_string_ $committish = null, _array_ $options = []) +* [git fetch](#git-fetch) + * $git->[fetch](#git-fetchstring-repository-string-refspec--null-array-options--)(_string_ $repository, _string_ $refspec = null, _array_ $options = []) + * $git->fetch->[all](#git-fetch-allarray-options--)(_array_ $options = []) +* [git init](#git-init) + * $git->[init](#git-initstring-path-array-options--)(_string_ $path, _array_ $options = []) +* [git log](#git-log) + * $git->[log](#git-logstring-revrange---string-path--null-array-options--)(_string_ $revRange = '', _string_ $path = null, _array_ $options = []) +* [git merge](#git-merge) + * $git->[merge](#git-mergestringarraytraversable-commit-string-message--null-array-options--)(_string|array|\Traversable_ $commit, _string_ $message = null, _array_ $options = []) + * $git->merge->[abort](#git-merge-abort)() +* [git mv](#git-mv) + * $git->[mv](#git-mvstringarrayiterator-source-string-destination-array-options--)(_string|array|\Iterator_ $source, _string_ $destination, _array_ $options = []) +* [git pull](#git-pull) + * $git->[pull](#git-pullstring-repository--null-string-refspec--null-array-options--)(_string_ $repository = null, _string_ $refspec = null, _array_ $options = []) +* [git push](#git-push) + * $git->[push](#git-pushstring-repository--null-string-refspec--null-array-options--)(_string_ $repository = null, _string_ $refspec = null, _array_ $options = []) +* [git rebase](#git-rebase) + * $git->[rebase](#git-rebasestring-upstream--null-string-branch--null-array-options--)(_string_ $upstream = null, _string_ $branch = null, _array_ $options = []) + * $git->rebase->[continues](#git-rebase-continues)() + * $git->rebase->[abort](#git-rebase-abort)() + * $git->rebase->[skip](#git-rebase-skip)() +* [git remote](#git-remote) + * $git->[remote](#git-remote)() + * $git->remote->[add](#git-remote-addstring-name-string-url-array-options--)(_string_ $name, _string_ $url, _array_ $options = []) + * $git->remote->[rename](#git-remote-renamestring-name-string-newname)(_string_ $name, _string_ $newName) + * $git->remote->[rm](#git-remote-rmstring-name)(_string_ $name) + * $git->remote->[show](#git-remote-showstring-name)(_string_ $name) + * $git->remote->[prune](#git-remote-prunestring-name--null)(_string_ $name = null) + * $git->remote->[head](#git-remote-headstring-name-string-branch--null)(_string_ $name, _string_ $branch = null) + * $git->remote->head->[set](#git-remote-head-setstring-name-string-branch)(_string_ $name, _string_ $branch) + * $git->remote->head->[delete](#git-remote-head-deletestring-name)(_string_ $name) + * $git->remote->head->[remote](#git-remote-head-remotestring-name)(_string_ $name) + * $git->remote->[branches](#git-remote-branchesstring-name-array-branches)(_string_ $name, _array_ $branches) + * $git->remote->branches->[set](#git-remote-branches-setstring-name-array-branches)(_string_ $name, _array_ $branches) + * $git->remote->branches->[add](#git-remote-branches-addstring-name-array-branches)(_string_ $name, _array_ $branches) + * $git->remote->[url](#git-remote-urlstring-name-string-newurl-string-oldurl--null-array-options--)(_string_ $name, _string_ $newUrl, _string_ $oldUrl = null, _array_ $options = []) + * $git->remote->url->[set](#git-remote-url-setstring-name-string-newurl-string-oldurl--null-array-options--)(_string_ $name, _string_ $newUrl, _string_ $oldUrl = null, _array_ $options = []) + * $git->remote->url->[add](#git-remote-url-addstring-name-string-newurl-array-options--)(_string_ $name, _string_ $newUrl, _array_ $options = []) + * $git->remote->url->[delete](#git-remote-url-deletestring-name-string-url-array-options--)(_string_ $name, _string_ $url, _array_ $options = []) +* [git reset](#git-reset) + * $git->[reset](#git-resetstringarraytraversable-paths-string-commit--null)(_string|array|\Traversable_ $paths, _string_ $commit = null) + * $git->reset->[soft](#git-reset-softstring-commit--null)(_string_ $commit = null) + * $git->reset->[mixed](#git-reset-mixedstring-commit--null)(_string_ $commit = null) + * $git->reset->[hard](#git-reset-hardstring-commit--null)(_string_ $commit = null) + * $git->reset->[merge](#git-reset-mergestring-commit--null)(_string_ $commit = null) + * $git->reset->[keep](#git-reset-keepstring-commit--null)(_string_ $commit = null) + * $git->reset->[mode](#git-reset-modestring-mode-string-commit--null)(_string_ $mode, _string_ $commit = null) +* [git rm](#git-rm) + * $git->[rm](#git-rmstringarraytraversable-file-array-options--)(_string|array|\Traversable_ $file, _array_ $options = []) + * $git->rm->[cached](#git-rm-cachedstringarraytraversable-file-array-options--)(_string|array|\Traversable_ $file, _array_ $options = []) +* [git shortlog](#git-shortlog) + * $git->[shortlog](#git-shortlogstringarraytraversable-commits--head)(_string|array|\Traversable_ $commits = HEAD) + * $git->shortlog->[summary](#git-shortlog-summarystring-commits--head)(_string_ $commits = HEAD) +* [git show](#git-show) + * $git->[show](#git-showstring-object-array-options--)(_string_ $object, _array_ $options = []) +* [git stash](#git-stash) + * $git->[stash](#git-stash)() + * $git->stash->[save](#git-stash-savestring-message--null-array-options--)(_string_ $message = null, _array_ $options = []) + * $git->stash->[lists](#git-stash-listsarray-options--)(_array_ $options = []) + * $git->stash->[show](#git-stash-showstring-stash--null)(_string_ $stash = null) + * $git->stash->[drop](#git-stash-dropstring-stash--null)(_string_ $stash = null) + * $git->stash->[pop](#git-stash-popstring-stash--null-array-options--)(_string_ $stash = null, _array_ $options = []) + * $git->stash->[apply](#git-stash-applystring-stash--null-array-options--)(_string_ $stash = null, _array_ $options = []) + * $git->stash->[branch](#git-stash-branchstring-name-string-stash--null)(_string_ $name, _string_ $stash = null) + * $git->stash->[clear](#git-stash-clear)() + * $git->stash->[create](#git-stash-create)() +* [git status](#git-status) + * $git->[status](#git-statusarray-options--)(_array_ $options = []) +* [git tag](#git-tag) + * $git->[tag](#git-tag)() + * $git->tag->[create](#git-tag-createstring-tag-string-commit--null-array-options--)(_string_ $tag, _string_ $commit = null, _array_ $options = []) + * $git->tag->[delete](#git-tag-deletestringarraytraversable-tag)(_string|array|\Traversable_ $tag) + * $git->tag->[verify](#git-tag-verifystringarraytraversable-tag)(_string|array|\Traversable_ $tag) +* [git ls-tree](#git-ls-tree) + * $git->[tree](#git-treestring-branch--master-string-path--)(_string_ $branch = master, _string_ $path = '') + +* * * * * + +### git add + +#### $git->add(_string|array|\Traversable_ $file, _array_ $options = []) + +Add file contents to the index + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->add('file.txt'); +$git->add('file.txt', ['force' => false, 'ignore-errors' => false); +``` + +##### Options + +- **force** (_boolean_) Allow adding otherwise ignored files +- **ignore-errors** (_boolean_) Do not abort the operation + +* * * * * + +### git archive + +#### $git->archive(_string_ $file, _string_ $tree = null, _string|array|\Traversable_ $path = null, _array_ $options = []) + +Create an archive of files from a named tree + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->archive('repo.zip', 'master', null, ['format' => 'zip']); +``` + +##### Options + +- **format** (_boolean_) Format of the resulting archive: tar or zip +- **prefix** (_boolean_) Prepend prefix/ to each filename in the archive + +* * * * * + +### git branch + +#### $git->branch(_array_ $options = []) + +Returns an array of both remote-tracking branches and local branches + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$branches = $git->branch(); +``` + +##### Output Example + +``` +[ + 'master' => ['current' => true, 'name' => 'master', 'hash' => 'bf231bb', 'title' => 'Initial Commit'], + 'origin/master' => ['current' => false, 'name' => 'origin/master', 'alias' => 'remotes/origin/master'] +] +``` + +##### Options + +- **all** (_boolean_) List both remote-tracking branches and local branches +- **remotes** (_boolean_) List the remote-tracking branches + +#### $git->branch->create(_string_ $branch, _string_ $startPoint = null, _array_ $options = []) + +Creates a new branch head named **$branch** which points to the current HEAD, or **$startPoint** if given + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->branch->create('bugfix'); // from current HEAD +$git->branch->create('patch-1', 'a092bf7s'); // from commit +$git->branch->create('1.0.x-fix', 'v1.0.2'); // from tag +``` + +##### Options + +- **force** (_boolean_) Reset **$branch** to **$startPoint** if **$branch** exists already + +#### $git->branch->move(_string_ $branch, _string_ $newBranch, _array_ $options = []) + +Move/rename a branch and the corresponding reflog + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->branch->move('bugfix', '2.0'); +``` + +##### Options + +- **force** (_boolean_) Move/rename a branch even if the new branch name already exists + +#### $git->branch->delete(_string_ $branch, _array_ $options = []) + +Delete a branch + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->branch->delete('2.0'); +``` + +The branch must be fully merged in its upstream branch, or in HEAD if no upstream was set with --track or --set-upstream. + +##### Options + +- **force** (_boolean_) Delete a branch irrespective of its merged status + +* * * * * + +### git cat-file + +#### $git->cat->blob(_string_ $object) + +Returns the contents of blob object + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$contents = $git->cat->blob('e69de29bb2d1d6434b8b29ae775ad8'); +``` + +#### $git->cat->type(_string_ $object) + +Returns the object type identified by **$object** + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$type = $git->cat->type('e69de29bb2d1d6434b8b29ae775ad8'); +``` + +#### $git->cat->size(_string_ $object) + +Returns the object size identified by **$object** + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$type = $git->cat->size('e69de29bb2d1d6434b8b29ae775ad8'); +``` + +* * * * * + +### git checkout + +#### $git->checkout(_string_ $branch, _array_ $options = []) + +Switches branches by updating the index, working tree, and HEAD to reflect the specified branch or commit + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->checkout('develop'); +``` + +##### Options + +- **force** (_boolean_) Proceed even if the index or the working tree differs from HEAD +- **merge** (_boolean_) Merges local modification + +#### $git->checkout->create(_string_ $branch, _string_ $startPoint = null, _array_ $options = []) + +Create a new branch and checkout + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->checkout->create('patch-1'); +$git->checkout->create('patch-2', 'develop'); +``` + +##### Options + +- **force** (_boolean_) Proceed even if the index or the working tree differs from HEAD + +#### $git->checkout->orphan(_string_ $branch, _string_ $startPoint = null, _array_ $options = []) + +Create a new orphan branch, named <new_branch>, started from <start_point> and switch to it + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->checkout->orphan('gh-pages'); +``` + +##### Options + +- **force** (_boolean_) Proceed even if the index or the working tree differs from HEAD + +* * * * * + +### git clone + +#### $git->clone(_string_ $repository, _string_ $path = null, _array_ $options = []) + +Clone a repository into a new directory + +``` php +$git = new PHPGit\Git(); +$git->clone('https://github.com/kzykhys/PHPGit.git', '/path/to/repo'); +``` + +##### Options + +- **shared** (_boolean_) Starts out without any object of its own +- **bare** (_boolean_) Make a bare GIT repository + +* * * * * + +### git commit + +#### $git->commit(_string_ $message, _array_ $options = []) + +Record changes to the repository + +``` php +$git = new PHPGit\Git(); +$git->clone('https://github.com/kzykhys/PHPGit.git', '/path/to/repo'); +$git->setRepository('/path/to/repo'); +$git->add('README.md'); +$git->commit('Fixes README.md'); +``` + +##### Options + +- **all** (_boolean_) Stage files that have been modified and deleted +- **reuse-message** (_string_) Take an existing commit object, and reuse the log message and the authorship information (including the timestamp) when creating the commit +- **squash** (_string_) Construct a commit message for use with rebase --autosquash +- **author** (_string_) Override the commit author +- **date** (_string_) Override the author date used in the commit +- **cleanup** (_string_) Can be one of verbatim, whitespace, strip, and default +- **amend** (_boolean_) Used to amend the tip of the current branch + +* * * * * + +### git config + +#### $git->config(_array_ $options = []) + +Returns all variables set in config file + + +##### Options + +- **global** (_boolean_) Read or write configuration options for the current user +- **system** (_boolean_) Read or write configuration options for all users on the current machine + +#### $git->config->set(_string_ $name, _string_ $value, _array_ $options = []) + +Set an option + +##### Options + +- **global** (_boolean_) Read or write configuration options for the current user +- **system** (_boolean_) Read or write configuration options for all users on the current machine + +#### $git->config->add(_string_ $name, _string_ $value, _array_ $options = []) + +Adds a new line to the option without altering any existing values + +##### Options + +- **global** (_boolean_) Read or write configuration options for the current user +- **system** (_boolean_) Read or write configuration options for all users on the current machine + +* * * * * + +### git describe + +#### $git->describe(_string_ $committish = null, _array_ $options = []) + +Returns the most recent tag that is reachable from a commit + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->tag->create('v1.0.0'); +$git->commit('Fixes #14'); +echo $git->describe('HEAD', ['tags' => true]); +``` + +##### Output Example + +``` +v1.0.0-1-g7049efc +``` + +##### Options + +- **all** (_boolean_) Enables matching any known branch, remote-tracking branch, or lightweight tag +- **tags** (_boolean_) Enables matching a lightweight (non-annotated) tag +- **always** (_boolean_) Show uniquely abbreviated commit object as fallback + +#### $git->describe->tags(_string_ $committish = null, _array_ $options = []) + +Equivalent to $git->describe($committish, ['tags' => true]); + +* * * * * + +### git fetch + +#### $git->fetch(_string_ $repository, _string_ $refspec = null, _array_ $options = []) + +Fetches named heads or tags from one or more other repositories, along with the objects necessary to complete them + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->remote->add('origin', 'git://your/repo.git'); +$git->fetch('origin'); +``` + +##### Options + +- **append** (_boolean_) Append ref names and object names of fetched refs to the existing contents of .git/FETCH_HEAD +- **keep** (_boolean_) Keep downloaded pack +- **prune** (_boolean_) After fetching, remove any remote-tracking branches which no longer exist on the remote + +#### $git->fetch->all(_array_ $options = []) + +Fetch all remotes + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->remote->add('origin', 'git://your/repo.git'); +$git->remote->add('release', 'git://your/another_repo.git'); +$git->fetch->all(); +``` + +##### Options + +- **append** (_boolean_) Append ref names and object names of fetched refs to the existing contents of .git/FETCH_HEAD +- **keep** (_boolean_) Keep downloaded pack +- **prune** (_boolean_) After fetching, remove any remote-tracking branches which no longer exist on the remote + +* * * * * + +### git init + +#### $git->init(_string_ $path, _array_ $options = []) + +Create an empty git repository or reinitialize an existing one + +``` php +$git = new PHPGit\Git(); +$git->init('/path/to/repo1'); +$git->init('/path/to/repo2', array('shared' => true, 'bare' => true)); +``` + +##### Options + +- **shared** (_boolean_) Specify that the git repository is to be shared amongst several users +- **bare** (_boolean_) Create a bare repository + +* * * * * + +### git log + +#### $git->log(_string_ $revRange = '', _string_ $path = null, _array_ $options = []) + +Returns the commit logs + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$logs = $git->log(array('limit' => 10)); +``` + +##### Output Example + +``` php +[ + 0 => [ + 'hash' => '1a821f3f8483747fd045eb1f5a31c3cc3063b02b', + 'name' => 'John Doe', + 'email' => 'john@example.com', + 'date' => 'Fri Jan 17 16:32:49 2014 +0900', + 'title' => 'Initial Commit' + ], + 1 => [ + //... + ] +] +``` + +##### Options + +- **limit** (_integer_) Limits the number of commits to show +- **skip** (_integer_) Skip number commits before starting to show the commit output + +* * * * * + +### git merge + +#### $git->merge(_string|array|\Traversable_ $commit, _string_ $message = null, _array_ $options = []) + +Incorporates changes from the named commits into the current branch + +```php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->merge('1.0'); +$git->merge('1.1', 'Merge message', ['strategy' => 'ours']); +``` + +##### Options + +- **no-ff** (_boolean_) Do not generate a merge commit if the merge resolved as a fast-forward, only update the branch pointer +- **rerere-autoupdate** (_boolean_) Allow the rerere mechanism to update the index with the result of auto-conflict resolution if possible +- **squash** (_boolean_) Allows you to create a single commit on top of the current branch whose effect is the same as merging another branch +- **strategy** (_string_) Use the given merge strategy +- **strategy-option** (_string_) Pass merge strategy specific option through to the merge strategy + +#### $git->merge->abort() + +Abort the merge process and try to reconstruct the pre-merge state + +```php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +try { + $git->merge('dev'); +} catch (PHPGit\Exception\GitException $e) { + $git->merge->abort(); +} +``` + +* * * * * + +### git mv + +#### $git->mv(_string|array|\Iterator_ $source, _string_ $destination, _array_ $options = []) + +Move or rename a file, a directory, or a symlink + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->mv('UPGRADE-1.0.md', 'UPGRADE-1.1.md'); +``` + +##### Options + +- **force** (_boolean_) Force renaming or moving of a file even if the target exists + +* * * * * + +### git pull + +#### $git->pull(_string_ $repository = null, _string_ $refspec = null, _array_ $options = []) + +Fetch from and merge with another repository or a local branch + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->pull('origin', 'master'); +``` + +* * * * * + +### git push + +#### $git->push(_string_ $repository = null, _string_ $refspec = null, _array_ $options = []) + +Update remote refs along with associated objects + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->push('origin', 'master'); +``` + +* * * * * + +### git rebase + +#### $git->rebase(_string_ $upstream = null, _string_ $branch = null, _array_ $options = []) + +Forward-port local commits to the updated upstream head + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->fetch('origin'); +$git->rebase('origin/master'); +``` + +##### Options + +- **onto** (_string_) Starting point at which to create the new commits +- **no-verify** (_boolean_) Bypasses the pre-rebase hook +- **force-rebase** (_boolean_) Force the rebase even if the current branch is a descendant of the commit you are rebasing onto + +#### $git->rebase->continues() + +Restart the rebasing process after having resolved a merge conflict + +#### $git->rebase->abort() + +Abort the rebase operation and reset HEAD to the original branch + +#### $git->rebase->skip() + +Restart the rebasing process by skipping the current patch + +* * * * * + +### git remote + +#### $git->remote() + +Returns an array of existing remotes + +``` php +$git = new PHPGit\Git(); +$git->clone('https://github.com/kzykhys/Text.git', '/path/to/repo'); +$git->setRepository('/path/to/repo'); +$remotes = $git->remote(); +``` + +##### Output Example + +``` php +[ + 'origin' => [ + 'fetch' => 'https://github.com/kzykhys/Text.git', + 'push' => 'https://github.com/kzykhys/Text.git' + ] +] +``` + +#### $git->remote->add(_string_ $name, _string_ $url, _array_ $options = []) + +Adds a remote named **$name** for the repository at **$url** + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); +$git->fetch('origin'); +``` + +##### Options + +- **tags** (_boolean_) With this option, `git fetch <name>` imports every tag from the remote repository +- **no-tags** (_boolean_) With this option, `git fetch <name>` does not import tags from the remote repository + +#### $git->remote->rename(_string_ $name, _string_ $newName) + +Rename the remote named **$name** to **$newName** + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); +$git->remote->rename('origin', 'upstream'); +``` + +#### $git->remote->rm(_string_ $name) + +Remove the remote named **$name** + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); +$git->remote->rm('origin'); +``` + +#### $git->remote->show(_string_ $name) + +Gives some information about the remote **$name** + +``` php +$git = new PHPGit\Git(); +$git->clone('https://github.com/kzykhys/Text.git', '/path/to/repo'); +$git->setRepository('/path/to/repo'); +echo $git->remote->show('origin'); +``` + +##### Output Example + +``` +\* remote origin + Fetch URL: https://github.com/kzykhys/Text.git + Push URL: https://github.com/kzykhys/Text.git + HEAD branch: master + Remote branch: + master tracked + Local branch configured for 'git pull': + master merges with remote master + Local ref configured for 'git push': + master pushes to master (up to date) +``` + +#### $git->remote->prune(_string_ $name = null) + +Deletes all stale remote-tracking branches under **$name** + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->remote->prune('origin'); +``` + +#### $git->remote->head(_string_ $name, _string_ $branch = null) + +Alias of set() + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); +$git->remote->head('origin'); +``` + +#### $git->remote->head->set(_string_ $name, _string_ $branch) + +Sets the default branch for the named remote + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); +$git->remote->head->set('origin'); +``` + +#### $git->remote->head->delete(_string_ $name) + +Deletes the default branch for the named remote + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); +$git->remote->head->delete('origin'); +``` + +#### $git->remote->head->remote(_string_ $name) + +Determine the default branch by querying remote + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); +$git->remote->head->remote('origin'); +``` + +#### $git->remote->branches(_string_ $name, _array_ $branches) + +Alias of set() + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); +$git->remote->branches('origin', array('master', 'develop')); +``` + +#### $git->remote->branches->set(_string_ $name, _array_ $branches) + +Changes the list of branches tracked by the named remote + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); +$git->remote->branches->set('origin', array('master', 'develop')); +``` + +#### $git->remote->branches->add(_string_ $name, _array_ $branches) + +Adds to the list of branches tracked by the named remote + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); +$git->remote->branches->add('origin', array('master', 'develop')); +``` + +#### $git->remote->url(_string_ $name, _string_ $newUrl, _string_ $oldUrl = null, _array_ $options = []) + +Alias of set() + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); +$git->remote->url('origin', 'https://github.com/text/Text.git'); +``` + +##### Options + +- **push** (_boolean_) Push URLs are manipulated instead of fetch URLs + +#### $git->remote->url->set(_string_ $name, _string_ $newUrl, _string_ $oldUrl = null, _array_ $options = []) + +Sets the URL remote to $newUrl + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); +$git->remote->url->set('origin', 'https://github.com/text/Text.git'); +``` + +##### Options + +- **push** (_boolean_) Push URLs are manipulated instead of fetch URLs + +#### $git->remote->url->add(_string_ $name, _string_ $newUrl, _array_ $options = []) + +Adds new URL to remote + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); +$git->remote->url->add('origin', 'https://github.com/text/Text.git'); +``` + +##### Options + +- **push** (_boolean_) Push URLs are manipulated instead of fetch URLs + +#### $git->remote->url->delete(_string_ $name, _string_ $url, _array_ $options = []) + +Deletes all URLs matching regex $url + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); +$git->remote->url->delete('origin', 'https://github.com'); +``` + +##### Options + +- **push** (_boolean_) Push URLs are manipulated instead of fetch URLs + +* * * * * + +### git reset + +#### $git->reset(_string|array|\Traversable_ $paths, _string_ $commit = null) + +Resets the index entries for all **$paths** to their state at **$commit** + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->reset(); +``` + +#### $git->reset->soft(_string_ $commit = null) + +Resets the current branch head to **$commit** + +Does not touch the index file nor the working tree at all (but resets the head to **$commit**, +just like all modes do). +This leaves all your changed files "Changes to be committed", as git status would put it. + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->reset->soft(); +``` + +#### $git->reset->mixed(_string_ $commit = null) + +Resets the current branch head to **$commit** + +Resets the index but not the working tree (i.e., the changed files are preserved but not marked for commit) +and reports what has not been updated. This is the default action. + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->reset->mixed(); +``` + +#### $git->reset->hard(_string_ $commit = null) + +Resets the current branch head to **$commit** + +Resets the index and working tree. Any changes to tracked files in the working tree since **$commit** are discarded + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->reset->hard(); +``` + +#### $git->reset->merge(_string_ $commit = null) + +Resets the current branch head to **$commit** + +Resets the index and updates the files in the working tree that are different between **$commit** and HEAD, +but keeps those which are different between the index and working tree +(i.e. which have changes which have not been added). If a file that is different between **$commit** and +the index has unstaged changes, reset is aborted + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->reset->merge(); +``` + +#### $git->reset->keep(_string_ $commit = null) + +Resets the current branch head to **$commit** + +Resets index entries and updates files in the working tree that are different between **$commit** and HEAD. +If a file that is different between **$commit** and HEAD has local changes, reset is aborted. + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->reset->keep(); +``` + +#### $git->reset->mode(_string_ $mode, _string_ $commit = null) + +Resets the current branch head to **$commit** + +Possibly updates the index (resetting it to the tree of **$commit**) and the working tree depending on **$mode** + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->reset->mode('hard'); +``` + +* * * * * + +### git rm + +#### $git->rm(_string|array|\Traversable_ $file, _array_ $options = []) + +Remove files from the working tree and from the index + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->rm('CHANGELOG-1.0-1.1.txt', ['force' => true]); +``` + +##### Options + +- **force** (_boolean_) Override the up-to-date check +- **cached** (_boolean_) Unstage and remove paths only from the index +- **recursive** (_boolean_) Allow recursive removal when a leading directory name is given + +#### $git->rm->cached(_string|array|\Traversable_ $file, _array_ $options = []) + +Equivalent to $git->rm($file, ['cached' => true]); + +##### Options + +- **force** (_boolean_) Override the up-to-date check +- **recursive** (_boolean_) Allow recursive removal when a leading directory name is given + +* * * * * + +### git shortlog + +#### $git->shortlog(_string|array|\Traversable_ $commits = HEAD) + +Summarize 'git log' output + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$shortlog = $git->shortlog(); +``` + +##### Output Example + +``` php +[ + 'John Doe <john@example.com>' => [ + 0 => ['commit' => '589de67', 'date' => new \DateTime('2014-02-10 12:56:15 +0300'), 'subject' => 'Update README'], + 1 => ['commit' => '589de67', 'date' => new \DateTime('2014-02-15 12:56:15 +0300'), 'subject' => 'Update README'], + ], + //... +] +``` + +#### $git->shortlog->summary(_string_ $commits = HEAD) + +Suppress commit description and provide a commit count summary only + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$shortlog = $git->shortlog->summary(); +``` + +##### Output Example + +``` php +[ + 'John Doe <john@example.com>' => 153, + //... +] +``` + +* * * * * + +### git show + +#### $git->show(_string_ $object, _array_ $options = []) + +Shows one or more objects (blobs, trees, tags and commits) + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +echo $git->show('3ddee587e209661c8265d5bfd0df999836f6dfa2'); +``` + +##### Options + +- **format** (_string_) Pretty-print the contents of the commit logs in a given format, where <format> can be one of oneline, short, medium, full, fuller, email, raw and format:<string> +- **abbrev-commit** (_boolean_) Instead of showing the full 40-byte hexadecimal commit object name, show only a partial prefix + +* * * * * + +### git stash + +#### $git->stash() + +Save your local modifications to a new stash, and run git reset --hard to revert them + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->stash(); +``` + +#### $git->stash->save(_string_ $message = null, _array_ $options = []) + +Save your local modifications to a new stash, and run git reset --hard to revert them. + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->stash->save('My stash'); +``` + +#### $git->stash->lists(_array_ $options = []) + +Returns the stashes that you currently have + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$stashes = $git->stash->lists(); +``` + +##### Output Example + +``` php +[ + 0 => ['branch' => 'master', 'message' => '0e2f473 Fixes README.md'], + 1 => ['branch' => 'master', 'message' => 'ce1ddde Initial commit'], +] +``` + +#### $git->stash->show(_string_ $stash = null) + +Show the changes recorded in the stash as a diff between the stashed state and its original parent + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +echo $git->stash->show('stash@{0}'); +``` + +##### Output Example + +``` + REAMDE.md | 2 +- + 1 files changed, 1 insertions(+), 1 deletions(-) +``` + +#### $git->stash->drop(_string_ $stash = null) + +Remove a single stashed state from the stash list + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->stash->drop('stash@{0}'); +``` + +#### $git->stash->pop(_string_ $stash = null, _array_ $options = []) + +Remove a single stashed state from the stash list and apply it on top of the current working tree state + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->stash->pop('stash@{0}'); +``` + +#### $git->stash->apply(_string_ $stash = null, _array_ $options = []) + +Like pop, but do not remove the state from the stash list + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->stash->apply('stash@{0}'); +``` + +#### $git->stash->branch(_string_ $name, _string_ $stash = null) + +Creates and checks out a new branch named <branchname> starting from the commit at which the <stash> was originally created, applies the changes recorded in <stash> to the new working tree and index + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->stash->branch('hotfix', 'stash@{0}'); +``` + +#### $git->stash->clear() + +Remove all the stashed states + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->stash->clear(); +``` + +#### $git->stash->create() + +Create a stash (which is a regular commit object) and return its object name, without storing it anywhere in the ref namespace + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$commit = $git->stash->create(); +``` + +##### Output Example + +``` +877316ea6f95c43b7ccc2c2a362eeedfa78b597d +``` + +* * * * * + +### git status + +#### $git->status(_array_ $options = []) + +Returns the working tree status + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$status = $git->status(); +``` + +##### Constants + +- StatusCommand::UNMODIFIED [=' '] unmodified +- StatusCommand::MODIFIED [='M'] modified +- StatusCommand::ADDED [='A'] added +- StatusCommand::DELETED [='D'] deleted +- StatusCommand::RENAMED [='R'] renamed +- StatusCommand::COPIED [='C'] copied +- StatusCommand::UPDATED_BUT_UNMERGED [='U'] updated but unmerged +- StatusCommand::UNTRACKED [='?'] untracked +- StatusCommand::IGNORED [='!'] ignored + +##### Output Example + +``` php +[ + 'branch' => 'master', + 'changes' => [ + ['file' => 'item1.txt', 'index' => 'A', 'work_tree' => 'M'], + ['file' => 'item2.txt', 'index' => 'A', 'work_tree' => ' '], + ['file' => 'item3.txt', 'index' => '?', 'work_tree' => '?'], + ] +] +``` + +##### Options + +- **ignored** (_boolean_) Show ignored files as well + +* * * * * + +### git tag + +#### $git->tag() + +Returns an array of tags + +``` php +$git = new PHPGit\Git(); +$git->clone('https://github.com/kzykhys/PHPGit.git', '/path/to/repo'); +$git->setRepository('/path/to/repo'); +$tags = $git->tag(); +``` + +##### Output Example + +``` +['v1.0.0', 'v1.0.1', 'v1.0.2'] +``` + +#### $git->tag->create(_string_ $tag, _string_ $commit = null, _array_ $options = []) + +Creates a tag object + +``` php +$git = new PHPGit\Git(); +$git->setRepository('/path/to/repo'); +$git->tag->create('v1.0.0'); +``` + +##### Options + +- **annotate** (_boolean_) Make an unsigned, annotated tag object +- **sign** (_boolean_) Make a GPG-signed tag, using the default e-mail address’s key +- **force** (_boolean_) Replace an existing tag with the given name (instead of failing) + +#### $git->tag->delete(_string|array|\Traversable_ $tag) + +Delete existing tags with the given names + +#### $git->tag->verify(_string|array|\Traversable_ $tag) + +Verify the gpg signature of the given tag names + +* * * * * + +### git ls-tree + +#### $git->tree(_string_ $branch = master, _string_ $path = '') + +Returns the contents of a tree object + +``` php +$git = new PHPGit\Git(); +$git->clone('https://github.com/kzykhys/PHPGit.git', '/path/to/repo'); +$git->setRepository('/path/to/repo'); +$tree = $git->tree('master'); +``` + +##### Output Example + +``` php +[ + ['mode' => '100644', 'type' => 'blob', 'hash' => '1f100ce9855b66111d34b9807e47a73a9e7359f3', 'file' => '.gitignore', 'sort' => '2:.gitignore'], + ['mode' => '100644', 'type' => 'blob', 'hash' => 'e0bfe494537037451b09c32636c8c2c9795c05c0', 'file' => '.travis.yml', 'sort' => '2:.travis.yml'], + ['mode' => '040000', 'type' => 'tree', 'hash' => '8d5438e79f77cd72de80c49a413f4edde1f3e291', 'file' => 'bin', 'sort' => '1:.bin'], +] +``` + +License +------- + +The MIT License + +Author +------ + +Kazuyuki Hayashi (@kzykhys) diff --git a/library/kzykhys/git/phpunit.xml.dist b/library/kzykhys/git/phpunit.xml.dist new file mode 100644 index 000000000..ad8dd7b00 --- /dev/null +++ b/library/kzykhys/git/phpunit.xml.dist @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- http://www.phpunit.de/manual/current/en/appendixes.configuration.html --> +<phpunit + backupGlobals = "false" + backupStaticAttributes = "false" + colors = "true" + convertErrorsToExceptions = "true" + convertNoticesToExceptions = "true" + convertWarningsToExceptions = "true" + processIsolation = "false" + stopOnFailure = "false" + syntaxCheck = "false" + bootstrap = "vendor/autoload.php" > + + <testsuites> + <testsuite name="Project Test Suite"> + <directory>test</directory> + </testsuite> + </testsuites> + + <filter> + <whitelist> + <directory>src</directory> + </whitelist> + </filter> + +</phpunit>
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command.php b/library/kzykhys/git/src/PHPGit/Command.php new file mode 100644 index 000000000..9238a5454 --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command.php @@ -0,0 +1,123 @@ +<?php + +namespace PHPGit; + +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; +use Symfony\Component\Process\ProcessBuilder; + +/** + * Base class for git commands + * + * @author Kazuyuki Hayashi <hayashi@valnur.net> + */ +abstract class Command +{ + + /** + * @var Git + */ + protected $git; + + /** + * @param Git $git + */ + public function __construct(Git $git) + { + $this->git = $git; + } + + /** + * Returns the combination of the default and the passed options + * + * @param array $options An array of options + * + * @return array + */ + public function resolve(array $options = array()) + { + $resolver = new OptionsResolver(); + $this->setDefaultOptions($resolver); + + return $resolver->resolve($options); + } + + /** + * Sets the default options + * + * @param OptionsResolverInterface $resolver The resolver for the options + * + * @codeCoverageIgnore + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + } + + /** + * Split string by new line or null(\0) + * + * @param string $input The string to split + * @param bool $useNull True to split by new line, otherwise null + * + * @return array + */ + protected function split($input, $useNull = false) + { + if ($useNull) { + $pattern = '/\0/'; + } else { + $pattern = '/\r?\n/'; + } + + return preg_split($pattern, rtrim($input), -1, PREG_SPLIT_NO_EMPTY); + } + + /** + * Adds boolean options to command arguments + * + * @param ProcessBuilder $builder A ProcessBuilder object + * @param array $options An array of options + * @param array $optionNames The names of options to add + */ + protected function addFlags(ProcessBuilder $builder, array $options = array(), array $optionNames = null) + { + if ($optionNames) { + foreach ($optionNames as $name) { + if (isset($options[$name]) && is_bool($options[$name]) && $options[$name]) { + $builder->add('--' . $name); + } + } + } else { + foreach ($options as $name => $option) { + if ($option) { + $builder->add('--' . $name); + } + } + } + } + + /** + * Adds options with values to command arguments + * + * @param ProcessBuilder $builder A ProcessBuilder object + * @param array $options An array of options + * @param array $optionNames The names of options to add + */ + protected function addValues(ProcessBuilder $builder, array $options = array(), array $optionNames = null) + { + if ($optionNames) { + foreach ($optionNames as $name) { + if (isset($options[$name]) && $options[$name]) { + $builder->add('--' . $name . '=' . $options[$name]); + } + } + } else { + foreach ($options as $name => $option) { + if ($option) { + $builder->add('--' . $name . '=' . $option); + } + } + } + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/AddCommand.php b/library/kzykhys/git/src/PHPGit/Command/AddCommand.php new file mode 100644 index 000000000..c035b2412 --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/AddCommand.php @@ -0,0 +1,75 @@ +<?php + +namespace PHPGit\Command; + +use PHPGit\Command; +use PHPGit\Exception\GitException; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; + +/** + * Add file contents to the index - `git add` + * + * @author Kazuyuki Hayashi <hayashi@valnur.net> + */ +class AddCommand extends Command +{ + + /** + * Add file contents to the index + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->add('file.txt'); + * $git->add('file.txt', ['force' => false, 'ignore-errors' => false); + * ``` + * + * ##### Options + * + * - **force** (_boolean_) Allow adding otherwise ignored files + * - **ignore-errors** (_boolean_) Do not abort the operation + * + * @param string|array|\Traversable $file Files to add content from + * @param array $options [optional] An array of options {@see AddCommand::setDefaultOptions} + * + * @throws GitException + * @return bool + */ + public function __invoke($file, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('add'); + + $this->addFlags($builder, $options); + + if (!is_array($file) && !($file instanceof \Traversable)) { + $file = array($file); + } + + foreach ($file as $value) { + $builder->add($value); + } + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * {@inheritdoc} + * + * - **force** (_boolean_) Allow adding otherwise ignored files + * - **ignore-errors** (_boolean_) Do not abort the operation + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + //'dry-run' => false, + 'force' => false, + 'ignore-errors' => false, + //'ignore-missing' => false, + )); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/ArchiveCommand.php b/library/kzykhys/git/src/PHPGit/Command/ArchiveCommand.php new file mode 100644 index 000000000..a6c9bd0d9 --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/ArchiveCommand.php @@ -0,0 +1,96 @@ +<?php + +namespace PHPGit\Command; + +use PHPGit\Command; +use PHPGit\Exception\GitException; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; + +/** + * Create an archive of files from a named tree - `git archive` + * + * @author Kazuyuki Hayashi <hayashi@valnur.net> + */ +class ArchiveCommand extends Command +{ + + /** + * Create an archive of files from a named tree + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->archive('repo.zip', 'master', null, ['format' => 'zip']); + * ``` + * + * ##### Options + * + * - **format** (_boolean_) Format of the resulting archive: tar or zip + * - **prefix** (_boolean_) Prepend prefix/ to each filename in the archive + * + * @param string $file The filename + * @param string $tree [optional] The tree or commit to produce an archive for + * @param string|array|\Traversable $path [optional] If one or more paths are specified, only these are included + * @param array $options [optional] An array of options {@see ArchiveCommand::setDefaultOptions} + * + * @throws GitException + * @return bool + */ + public function __invoke($file, $tree = null, $path = null, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('archive'); + + if ($options['format']) { + $builder->add('--format=' . $options['format']); + } + + if ($options['prefix']) { + $builder->add('--prefix=' . $options['prefix']); + } + + $builder->add('-o')->add($file); + + if ($tree) { + $builder->add($tree); + } + + if (!is_array($path) && !($path instanceof \Traversable)) { + $path = array($path); + } + + foreach ($path as $value) { + $builder->add($value); + } + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * {@inheritdoc} + * + * - **format** (_boolean_) Format of the resulting archive: tar or zip + * - **prefix** (_boolean_) Prepend prefix/ to each filename in the archive + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + 'format' => null, + 'prefix' => null + )); + + $resolver->setAllowedTypes(array( + 'format' => array('null', 'string'), + 'prefix' => array('null', 'string') + )); + + $resolver->setAllowedValues(array( + 'format' => array('tar', 'zip') + )); + } + + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/BranchCommand.php b/library/kzykhys/git/src/PHPGit/Command/BranchCommand.php new file mode 100644 index 000000000..4b42f5048 --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/BranchCommand.php @@ -0,0 +1,229 @@ +<?php + +namespace PHPGit\Command; + +use PHPGit\Command; +use PHPGit\Exception\GitException; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; + +/** + * List, create, or delete branches - `git branch` + * + * @author Kazuyuki Hayashi <hayashi@valnur.net> + */ +class BranchCommand extends Command +{ + + /** + * Returns an array of both remote-tracking branches and local branches + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $branches = $git->branch(); + * ``` + * + * ##### Output Example + * + * ``` + * [ + * 'master' => ['current' => true, 'name' => 'master', 'hash' => 'bf231bb', 'title' => 'Initial Commit'], + * 'origin/master' => ['current' => false, 'name' => 'origin/master', 'alias' => 'remotes/origin/master'] + * ] + * ``` + * + * ##### Options + * + * - **all** (_boolean_) List both remote-tracking branches and local branches + * - **remotes** (_boolean_) List the remote-tracking branches + * + * @param array $options [optional] An array of options {@see BranchCommand::setDefaultOptions} + * + * @throws GitException + * @return array + */ + public function __invoke(array $options = array()) + { + $options = $this->resolve($options); + $branches = array(); + $builder = $this->getProcessBuilder() + ->add('-v')->add('--abbrev=7'); + + if ($options['remotes']) { + $builder->add('--remotes'); + } + + if ($options['all']) { + $builder->add('--all'); + } + + $process = $builder->getProcess(); + $this->git->run($process); + + $lines = preg_split('/\r?\n/', rtrim($process->getOutput()), -1, PREG_SPLIT_NO_EMPTY); + + foreach ($lines as $line) { + $branch = array(); + preg_match('/(?<current>\*| ) (?<name>[^\s]+) +((?:->) (?<alias>[^\s]+)|(?<hash>[0-9a-z]{7}) (?<title>.*))/', $line, $matches); + + $branch['current'] = ($matches['current'] == '*'); + $branch['name'] = $matches['name']; + + if (isset($matches['hash'])) { + $branch['hash'] = $matches['hash']; + $branch['title'] = $matches['title']; + } else { + $branch['alias'] = $matches['alias']; + } + + $branches[$matches['name']] = $branch; + } + + return $branches; + } + + /** + * Creates a new branch head named **$branch** which points to the current HEAD, or **$startPoint** if given + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->branch->create('bugfix'); // from current HEAD + * $git->branch->create('patch-1', 'a092bf7s'); // from commit + * $git->branch->create('1.0.x-fix', 'v1.0.2'); // from tag + * ``` + * + * ##### Options + * + * - **force** (_boolean_) Reset **$branch** to **$startPoint** if **$branch** exists already + * + * @param string $branch The name of the branch to create + * @param string $startPoint [optional] The new branch head will point to this commit. + * It may be given as a branch name, a commit-id, or a tag. + * If this option is omitted, the current HEAD will be used instead. + * @param array $options [optional] An array of options {@see BranchCommand::setDefaultOptions} + * + * @throws GitException + * @return bool + */ + public function create($branch, $startPoint = null, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->getProcessBuilder(); + + if ($options['force']) { + $builder->add('-f'); + } + + $builder->add($branch); + + if ($startPoint) { + $builder->add($startPoint); + } + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * Move/rename a branch and the corresponding reflog + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->branch->move('bugfix', '2.0'); + * ``` + * + * ##### Options + * + * - **force** (_boolean_) Move/rename a branch even if the new branch name already exists + * + * @param string $branch The name of an existing branch to rename + * @param string $newBranch The new name for an existing branch + * @param array $options [optional] An array of options {@see BranchCommand::setDefaultOptions} + * + * @throws GitException + * @return bool + */ + public function move($branch, $newBranch, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->getProcessBuilder(); + + if ($options['force']) { + $builder->add('-M'); + } else { + $builder->add('-m'); + } + + $builder->add($branch)->add($newBranch); + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * Delete a branch + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->branch->delete('2.0'); + * ``` + * + * The branch must be fully merged in its upstream branch, or in HEAD if no upstream was set with --track or --set-upstream. + * + * ##### Options + * + * - **force** (_boolean_) Delete a branch irrespective of its merged status + * + * @param string $branch The name of the branch to delete + * @param array $options [optional] An array of options {@see BranchCommand::setDefaultOptions} + * + * @throws GitException + * @return bool + */ + public function delete($branch, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->getProcessBuilder(); + + if ($options['force']) { + $builder->add('-D'); + } else { + $builder->add('-d'); + } + + $builder->add($branch); + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * {@inheritdoc} + * + * - **force** (_boolean_) Reset <branchname> to <startpoint> if <branchname> exists already + * - **all** (_boolean_) List both remote-tracking branches and local branches + * - **remotes** (_boolean_) List or delete (if used with delete()) the remote-tracking branches + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + 'force' => false, + 'all' => false, + 'remotes' => false, + )); + } + + /** + * @return \Symfony\Component\Process\ProcessBuilder + */ + protected function getProcessBuilder() + { + return $this->git->getProcessBuilder() + ->add('branch'); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/CatCommand.php b/library/kzykhys/git/src/PHPGit/Command/CatCommand.php new file mode 100644 index 000000000..0c4fc1f41 --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/CatCommand.php @@ -0,0 +1,91 @@ +<?php + +namespace PHPGit\Command; + +use PHPGit\Command; +use PHPGit\Exception\GitException; + +/** + * Provide content or type and size information for repository objects - `git cat-file` + * + * @author Kazuyuki Hayashi <hayashi@valnur.net> + */ +class CatCommand extends Command +{ + + /** + * Returns the contents of blob object + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $contents = $git->cat->blob('e69de29bb2d1d6434b8b29ae775ad8'); + * ``` + * + * @param string $object The name of the blob object to show + * + * @throws GitException + * @return string + */ + public function blob($object) + { + $process = $this->git->getProcessBuilder() + ->add('cat-file') + ->add('blob') + ->add($object) + ->getProcess(); + + return $this->git->run($process); + } + + /** + * Returns the object type identified by **$object** + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $type = $git->cat->type('e69de29bb2d1d6434b8b29ae775ad8'); + * ``` + * + * @param string $object The name of the object to show + * + * @throws GitException + * @return string + */ + public function type($object) + { + $process = $this->git->getProcessBuilder() + ->add('cat-file') + ->add('-t') + ->add($object) + ->getProcess(); + + return trim($this->git->run($process)); + } + + /** + * Returns the object size identified by **$object** + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $type = $git->cat->size('e69de29bb2d1d6434b8b29ae775ad8'); + * ``` + * + * @param string $object The name of the object to show + * + * @throws GitException + * @return string + */ + public function size($object) + { + $process = $this->git->getProcessBuilder() + ->add('cat-file') + ->add('-s') + ->add($object) + ->getProcess(); + + return trim($this->git->run($process)); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/CheckoutCommand.php b/library/kzykhys/git/src/PHPGit/Command/CheckoutCommand.php new file mode 100644 index 000000000..caddf07bd --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/CheckoutCommand.php @@ -0,0 +1,145 @@ +<?php + +namespace PHPGit\Command; + +use PHPGit\Command; +use PHPGit\Exception\GitException; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; + +/** + * Checkout a branch or paths to the working tree - `git checkout` + * + * @author Kazuyuki Hayashi <hayashi@valnur.net> + */ +class CheckoutCommand extends Command +{ + + /** + * Switches branches by updating the index, working tree, and HEAD to reflect the specified branch or commit + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->checkout('develop'); + * ``` + * + * ##### Options + * + * - **force** (_boolean_) Proceed even if the index or the working tree differs from HEAD + * - **merge** (_boolean_) Merges local modification + * + * @param string $branch Branch to checkout + * @param array $options [optional] An array of options {@see CheckoutCommand::setDefaultOptions} + * + * @throws GitException + * @return bool + */ + public function __invoke($branch, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('checkout'); + + $this->addFlags($builder, $options, array('force', 'merge')); + + $builder->add($branch); + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * Create a new branch and checkout + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->checkout->create('patch-1'); + * $git->checkout->create('patch-2', 'develop'); + * ``` + * + * ##### Options + * + * - **force** (_boolean_) Proceed even if the index or the working tree differs from HEAD + * + * @param string $branch Branch to checkout + * @param string $startPoint The name of a commit at which to start the new branch + * @param array $options [optional] An array of options {@see CheckoutCommand::setDefaultOptions} + * + * @throws GitException + * @return bool + */ + public function create($branch, $startPoint = null, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('checkout') + ->add('-b'); + + $this->addFlags($builder, $options, array('force', 'merge')); + + $builder->add($branch); + + if ($startPoint) { + $builder->add($startPoint); + } + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * Create a new orphan branch, named <new_branch>, started from <start_point> and switch to it + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->checkout->orphan('gh-pages'); + * ``` + * + * ##### Options + * + * - **force** (_boolean_) Proceed even if the index or the working tree differs from HEAD + * + * @param string $branch Branch to checkout + * @param string $startPoint [optional] The name of a commit at which to start the new branch + * @param array $options [optional] An array of options {@see CheckoutCommand::setDefaultOptions} + * + * @throws GitException + * @return bool + */ + public function orphan($branch, $startPoint = null, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('checkout'); + + $this->addFlags($builder, $options, array('force', 'merge')); + + $builder->add('--orphan')->add($branch); + + if ($startPoint) { + $builder->add($startPoint); + } + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * {@inheritdoc} + * + * - **force** (_boolean_) Proceed even if the index or the working tree differs from HEAD + * - **merge** (_boolean_) Merges local modification + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + 'force' => false, + 'merge' => false + )); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/CloneCommand.php b/library/kzykhys/git/src/PHPGit/Command/CloneCommand.php new file mode 100644 index 000000000..cc6b4ab37 --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/CloneCommand.php @@ -0,0 +1,71 @@ +<?php + +namespace PHPGit\Command; + +use PHPGit\Command; +use PHPGit\Exception\GitException; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; + +/** + * Clone a repository into a new directory - `git clone` + * + * @author Kazuyuki Hayashi <hayashi@valnur.net> + */ +class CloneCommand extends Command +{ + + /** + * Clone a repository into a new directory + * + * ``` php + * $git = new PHPGit\Git(); + * $git->clone('https://github.com/kzykhys/PHPGit.git', '/path/to/repo'); + * ``` + * + * ##### Options + * + * - **shared** (_boolean_) Starts out without any object of its own + * - **bare** (_boolean_) Make a bare GIT repository + * + * @param string $repository The repository to clone from + * @param string $path [optional] The name of a new directory to clone into + * @param array $options [optional] An array of options {@see CloneCommand::setDefaultOptions} + * + * @throws GitException + * @return bool + */ + public function __invoke($repository, $path = null, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('clone') + ->add('--quiet'); + + $this->addFlags($builder, $options); + + $builder->add($repository); + + if ($path) { + $builder->add($path); + } + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * {@inheritdoc} + * + * - **shared** (_boolean_) Starts out without any object of its own + * - **bare** (_boolean_) Make a bare GIT repository + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + 'shared' => false, + 'bare' => false + )); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/CommitCommand.php b/library/kzykhys/git/src/PHPGit/Command/CommitCommand.php new file mode 100644 index 000000000..a4f2bdd95 --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/CommitCommand.php @@ -0,0 +1,87 @@ +<?php + +namespace PHPGit\Command; + +use PHPGit\Command; +use PHPGit\Exception\GitException; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; + +/** + * Record changes to the repository - `git commit` + * + * @author Kazuyuki Hayashi <hayashi@valnur.net> + */ +class CommitCommand extends Command +{ + + /** + * Record changes to the repository + * + * ``` php + * $git = new PHPGit\Git(); + * $git->clone('https://github.com/kzykhys/PHPGit.git', '/path/to/repo'); + * $git->setRepository('/path/to/repo'); + * $git->add('README.md'); + * $git->commit('Fixes README.md'); + * ``` + * + * ##### Options + * + * - **all** (_boolean_) Stage files that have been modified and deleted + * - **reuse-message** (_string_) Take an existing commit object, and reuse the log message and the authorship information (including the timestamp) when creating the commit + * - **squash** (_string_) Construct a commit message for use with rebase --autosquash + * - **author** (_string_) Override the commit author + * - **date** (_string_) Override the author date used in the commit + * - **cleanup** (_string_) Can be one of verbatim, whitespace, strip, and default + * - **amend** (_boolean_) Used to amend the tip of the current branch + * + * @param string $message Use the given <$msg> as the commit message + * @param array $options [optional] An array of options {@see CloneCommand::setDefaultOptions} + * + * @throws GitException + * @return bool + */ + public function __invoke($message, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('commit') + ->add('-m')->add($message); + + $this->addFlags($builder, $options, array('all', 'amend')); + $this->addValues($builder, $options, array('reuse-message', 'squash', 'author', 'date', 'cleanup')); + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * {@inheritdoc} + * + * - **all** (_boolean_) Stage files that have been modified and deleted + * - **reuse-message** (_string_) Take an existing commit object, and reuse the log message and the authorship information (including the timestamp) when creating the commit + * - **squash** (_string_) Construct a commit message for use with rebase --autosquash + * - **author** (_string_) Override the commit author + * - **date** (_string_) Override the author date used in the commit + * - **cleanup** (_string_) Can be one of verbatim, whitespace, strip, and default + * - **amend** (_boolean_) Used to amend the tip of the current branch + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + 'all' => false, + 'reuse-message' => null, + 'squash' => null, + 'author' => null, + 'date' => null, + 'cleanup' => null, + 'amend' => false + )); + + $resolver->setAllowedValues(array( + 'cleanup' => array(null, 'default', 'verbatim', 'whitespace', 'strip') + )); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/ConfigCommand.php b/library/kzykhys/git/src/PHPGit/Command/ConfigCommand.php new file mode 100644 index 000000000..cb8bb625f --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/ConfigCommand.php @@ -0,0 +1,132 @@ +<?php + +namespace PHPGit\Command; + +use PHPGit\Command; +use PHPGit\Exception\GitException; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; + +/** + * Get and set repository or global options - `git config` + * + * @author Kazuyuki Hayashi <hayashi@valnur.net> + */ +class ConfigCommand extends Command +{ + + /** + * Returns all variables set in config file + * + * + * ##### Options + * + * - **global** (_boolean_) Read or write configuration options for the current user + * - **system** (_boolean_) Read or write configuration options for all users on the current machine + * + * @param array $options [optional] An array of options {@see ConfigCommand::setDefaultOptions} + * + * @throws GitException + * @return array + */ + public function __invoke(array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('config') + ->add('--list') + ->add('--null'); + + $this->addFlags($builder, $options, array('global', 'system')); + + $config = array(); + $output = $this->git->run($builder->getProcess()); + $lines = $this->split($output, true); + + foreach ($lines as $line) { + list($name, $value) = explode("\n", $line, 2); + + if (isset($config[$name])) { + $config[$name] .= "\n" . $value; + } else { + $config[$name] = $value; + } + } + + return $config; + } + + /** + * Set an option + * + * ##### Options + * + * - **global** (_boolean_) Read or write configuration options for the current user + * - **system** (_boolean_) Read or write configuration options for all users on the current machine + * + * @param string $name The name of the option + * @param string $value The value to set + * @param array $options [optional] An array of options {@see ConfigCommand::setDefaultOptions} + * + * @throws GitException + * @return bool + */ + public function set($name, $value, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('config'); + + $this->addFlags($builder, $options, array('global', 'system')); + + $builder->add($name)->add($value); + $process = $builder->getProcess(); + $this->git->run($process); + + return true; + } + + /** + * Adds a new line to the option without altering any existing values + * + * ##### Options + * + * - **global** (_boolean_) Read or write configuration options for the current user + * - **system** (_boolean_) Read or write configuration options for all users on the current machine + * + * @param string $name The name of the option + * @param string $value The value to add + * @param array $options [optional] An array of options {@see ConfigCommand::setDefaultOptions} + * + * @throws GitException + * @return bool + */ + public function add($name, $value, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('config'); + + $this->addFlags($builder, $options, array('global', 'system')); + + $builder->add('--add')->add($name)->add($value); + $process = $builder->getProcess(); + $this->git->run($process); + + return true; + } + + /** + * {@inheritdoc} + * + * - **global** (_boolean_) Read or write configuration options for the current user + * - **system** (_boolean_) Read or write configuration options for all users on the current machine + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + 'global' => false, + 'system' => false, + )); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/DescribeCommand.php b/library/kzykhys/git/src/PHPGit/Command/DescribeCommand.php new file mode 100644 index 000000000..affdd009b --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/DescribeCommand.php @@ -0,0 +1,90 @@ +<?php + +namespace PHPGit\Command; + +use PHPGit\Command; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; + +/** + * Show the most recent tag that is reachable from a commit - `git describe` + * + * @author Kazuyuki Hayashi <hayashi@valnur.net> + */ +class DescribeCommand extends Command +{ + + /** + * Returns the most recent tag that is reachable from a commit + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->tag->create('v1.0.0'); + * $git->commit('Fixes #14'); + * echo $git->describe('HEAD', ['tags' => true]); + * ``` + * + * ##### Output Example + * + * ``` + * v1.0.0-1-g7049efc + * ``` + * + * ##### Options + * + * - **all** (_boolean_) Enables matching any known branch, remote-tracking branch, or lightweight tag + * - **tags** (_boolean_) Enables matching a lightweight (non-annotated) tag + * - **always** (_boolean_) Show uniquely abbreviated commit object as fallback + * + * @param string $committish [optional] Committish object names to describe. + * @param array $options [optional] An array of options {@see DescribeCommand::setDefaultOptions} + * + * @return string + */ + public function __invoke($committish = null, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('describe'); + + $this->addFlags($builder, $options, array()); + + if ($committish) { + $builder->add($committish); + } + + return trim($this->git->run($builder->getProcess())); + } + + /** + * Equivalent to $git->describe($committish, ['tags' => true]); + * + * @param string $committish [optional] Committish object names to describe. + * @param array $options [optional] An array of options {@see DescribeCommand::setDefaultOptions} + * + * @return string + */ + public function tags($committish = null, array $options = array()) + { + $options['tags'] = true; + + return $this->__invoke($committish, $options); + } + + /** + * {@inheritdoc} + * + * - **all** (_boolean_) Enables matching any known branch, remote-tracking branch, or lightweight tag + * - **tags** (_boolean_) Enables matching a lightweight (non-annotated) tag + * - **always** (_boolean_) Show uniquely abbreviated commit object as fallback + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + 'all' => false, + 'tags' => false, + 'always' => false, + )); + } + +} diff --git a/library/kzykhys/git/src/PHPGit/Command/FetchCommand.php b/library/kzykhys/git/src/PHPGit/Command/FetchCommand.php new file mode 100644 index 000000000..1302038e8 --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/FetchCommand.php @@ -0,0 +1,112 @@ +<?php + +namespace PHPGit\Command; + +use PHPGit\Command; +use PHPGit\Exception\GitException; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; + +/** + * Download objects and refs from another repository - `git fetch` + * + * @author Kazuyuki Hayashi <hayashi@valnur.net> + */ +class FetchCommand extends Command +{ + + /** + * Fetches named heads or tags from one or more other repositories, along with the objects necessary to complete them + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->remote->add('origin', 'git://your/repo.git'); + * $git->fetch('origin'); + * ``` + * + * ##### Options + * + * - **append** (_boolean_) Append ref names and object names of fetched refs to the existing contents of .git/FETCH_HEAD + * - **keep** (_boolean_) Keep downloaded pack + * - **prune** (_boolean_) After fetching, remove any remote-tracking branches which no longer exist on the remote + * + * @param string $repository The "remote" repository that is the source of a fetch or pull operation + * @param string $refspec The format of a <refspec> parameter is an optional plus +, followed by the source ref <src>, + * followed by a colon :, followed by the destination ref <dst> + * @param array $options [optional] An array of options {@see FetchCommand::setDefaultOptions} + * + * @throws GitException + * @return bool + */ + public function __invoke($repository, $refspec = null, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('fetch'); + + $this->addFlags($builder, $options); + $builder->add($repository); + + if ($refspec) { + $builder->add($refspec); + } + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * Fetch all remotes + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->remote->add('origin', 'git://your/repo.git'); + * $git->remote->add('release', 'git://your/another_repo.git'); + * $git->fetch->all(); + * ``` + * + * ##### Options + * + * - **append** (_boolean_) Append ref names and object names of fetched refs to the existing contents of .git/FETCH_HEAD + * - **keep** (_boolean_) Keep downloaded pack + * - **prune** (_boolean_) After fetching, remove any remote-tracking branches which no longer exist on the remote + * + * @param array $options [optional] An array of options {@see FetchCommand::setDefaultOptions} + * + * @throws GitException + * @return bool + */ + public function all(array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('fetch') + ->add('--all'); + + $this->addFlags($builder, $options); + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * {@inheritdoc} + * + * - **append** (_boolean_) Append ref names and object names of fetched refs to the existing contents of .git/FETCH_HEAD + * - **keep** (_boolean_) Keep downloaded pack + * - **prune** (_boolean_) After fetching, remove any remote-tracking branches which no longer exist on the remote + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + 'append' => false, + //'force' => false, + 'keep' => false, + 'prune' => false, + )); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/InitCommand.php b/library/kzykhys/git/src/PHPGit/Command/InitCommand.php new file mode 100644 index 000000000..1ff56fa5b --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/InitCommand.php @@ -0,0 +1,65 @@ +<?php + +namespace PHPGit\Command; + +use PHPGit\Command; +use PHPGit\Exception\GitException; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; + +/** + * Create an empty git repository or reinitialize an existing one - `git init` + * + * @author Kazuyuki Hayashi <hayashi@valnur.net> + */ +class InitCommand extends Command +{ + + /** + * Create an empty git repository or reinitialize an existing one + * + * ``` php + * $git = new PHPGit\Git(); + * $git->init('/path/to/repo1'); + * $git->init('/path/to/repo2', array('shared' => true, 'bare' => true)); + * ``` + * + * ##### Options + * + * - **shared** (_boolean_) Specify that the git repository is to be shared amongst several users + * - **bare** (_boolean_) Create a bare repository + * + * @param string $path The directory to create an empty repository + * @param array $options [optional] An array of options {@see InitCommand::setDefaultOptions} + * + * @throws GitException + * @return bool + */ + public function __invoke($path, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('init'); + + $this->addFlags($builder, $options, array('shared', 'bare')); + + $process = $builder->add($path)->getProcess(); + $this->git->run($process); + + return true; + } + + /** + * {@inheritdoc} + * + * - **shared** (_boolean_) Specify that the git repository is to be shared amongst several users + * - **bare** (_boolean_) Create a bare repository + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + 'shared' => false, + 'bare' => false + )); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/LogCommand.php b/library/kzykhys/git/src/PHPGit/Command/LogCommand.php new file mode 100644 index 000000000..c116550f7 --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/LogCommand.php @@ -0,0 +1,105 @@ +<?php + +namespace PHPGit\Command; + +use PHPGit\Command; +use PHPGit\Exception\GitException; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; + +/** + * Show commit logs - `git log` + * + * @author Kazuyuki Hayashi <hayashi@valnur.net> + */ +class LogCommand extends Command +{ + + /** + * Returns the commit logs + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $logs = $git->log(array('limit' => 10)); + * ``` + * + * ##### Output Example + * + * ``` php + * [ + * 0 => [ + * 'hash' => '1a821f3f8483747fd045eb1f5a31c3cc3063b02b', + * 'name' => 'John Doe', + * 'email' => 'john@example.com', + * 'date' => 'Fri Jan 17 16:32:49 2014 +0900', + * 'title' => 'Initial Commit' + * ], + * 1 => [ + * //... + * ] + * ] + * ``` + * + * ##### Options + * + * - **limit** (_integer_) Limits the number of commits to show + * - **skip** (_integer_) Skip number commits before starting to show the commit output + * + * @param string $revRange [optional] Show only commits in the specified revision range + * @param string $path [optional] Show only commits that are enough to explain how the files that match the specified paths came to be + * @param array $options [optional] An array of options {@see LogCommand::setDefaultOptions} + * + * @throws GitException + * @return array + */ + public function __invoke($revRange = '', $path = null, array $options = array()) + { + $commits = array(); + $options = $this->resolve($options); + + $builder = $this->git->getProcessBuilder() + ->add('log') + ->add('-n')->add($options['limit']) + ->add('--skip=' . $options['skip']) + ->add('--format=%H||%aN||%aE||%aD||%s'); + + if ($revRange) { + $builder->add($revRange); + } + + if ($path) { + $builder->add('--')->add($path); + } + + $output = $this->git->run($builder->getProcess()); + $lines = $this->split($output); + + foreach ($lines as $line) { + list($hash, $name, $email, $date, $title) = preg_split('/\|\|/', $line, -1, PREG_SPLIT_NO_EMPTY); + $commits[] = array( + 'hash' => $hash, + 'name' => $name, + 'email' => $email, + 'date' => $date, + 'title' => $title + ); + } + + return $commits; + } + + /** + * {@inheritdoc} + * + * - **limit** (_integer_) Limits the number of commits to show + * - **skip** (_integer_) Skip number commits before starting to show the commit output + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + 'limit' => 10, + 'skip' => 0 + )); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/MergeCommand.php b/library/kzykhys/git/src/PHPGit/Command/MergeCommand.php new file mode 100644 index 000000000..e1987151f --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/MergeCommand.php @@ -0,0 +1,110 @@ +<?php + +namespace PHPGit\Command; + +use PHPGit\Command; +use PHPGit\Exception\GitException; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; + +/** + * Join two or more development histories together - `git merge` + * + * @author Kazuyuki Hayashi <hayashi@valnur.net> + */ +class MergeCommand extends Command +{ + + /** + * Incorporates changes from the named commits into the current branch + * + * ```php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->merge('1.0'); + * $git->merge('1.1', 'Merge message', ['strategy' => 'ours']); + * ``` + * + * ##### Options + * + * - **no-ff** (_boolean_) Do not generate a merge commit if the merge resolved as a fast-forward, only update the branch pointer + * - **rerere-autoupdate** (_boolean_) Allow the rerere mechanism to update the index with the result of auto-conflict resolution if possible + * - **squash** (_boolean_) Allows you to create a single commit on top of the current branch whose effect is the same as merging another branch + * - **strategy** (_string_) Use the given merge strategy + * - **strategy-option** (_string_) Pass merge strategy specific option through to the merge strategy + * + * @param string|array|\Traversable $commit Commits to merge into our branch + * @param string $message [optional] Commit message to be used for the merge commit + * @param array $options [optional] An array of options {@see MergeCommand::setDefaultOptions} + * + * @throws GitException + * @return bool + */ + public function __invoke($commit, $message = null, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('merge'); + + $this->addFlags($builder, $options, array('no-ff', 'rerere-autoupdate', 'squash')); + + if (!is_array($commit) && !($commit instanceof \Traversable)) { + $commit = array($commit); + } + foreach ($commit as $value) { + $builder->add($value); + } + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * Abort the merge process and try to reconstruct the pre-merge state + * + * ```php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * try { + * $git->merge('dev'); + * } catch (PHPGit\Exception\GitException $e) { + * $git->merge->abort(); + * } + * ``` + * + * @throws GitException + * @return bool + */ + public function abort() + { + $builder = $this->git->getProcessBuilder() + ->add('merge') + ->add('--abort'); + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * {@inheritdoc} + * + * - **no-ff** (_boolean_) Do not generate a merge commit if the merge resolved as a fast-forward, only update the branch pointer + * - **rerere-autoupdate** (_boolean_) Allow the rerere mechanism to update the index with the result of auto-conflict resolution if possible + * - **squash** (_boolean_) Allows you to create a single commit on top of the current branch whose effect is the same as merging another branch + * - **strategy** (_string_) Use the given merge strategy + * - **strategy-option** (_string_) Pass merge strategy specific option through to the merge strategy + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + 'no-ff' => false, + 'rerere-autoupdate' => false, + 'squash' => false, + + 'strategy' => null, + 'strategy-option' => null + )); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/MvCommand.php b/library/kzykhys/git/src/PHPGit/Command/MvCommand.php new file mode 100644 index 000000000..fe7ce6af6 --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/MvCommand.php @@ -0,0 +1,70 @@ +<?php + +namespace PHPGit\Command; + +use PHPGit\Command; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; + +/** + * Move or rename a file, a directory, or a symlink - `git mv` + * + * @author Kazuyuki Hayashi <hayashi@valnur.net> + */ +class MvCommand extends Command +{ + + /** + * Move or rename a file, a directory, or a symlink + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->mv('UPGRADE-1.0.md', 'UPGRADE-1.1.md'); + * ``` + * + * ##### Options + * + * - **force** (_boolean_) Force renaming or moving of a file even if the target exists + * + * @param string|array|\Iterator $source The files to move + * @param string $destination The destination + * @param array $options [optional] An array of options {@see MvCommand::setDefaultOptions} + * + * @return bool + */ + public function __invoke($source, $destination, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('mv'); + + $this->addFlags($builder, $options, array('force')); + + if (!is_array($source) && !($source instanceof \Traversable)) { + $source = array($source); + } + + foreach ($source as $value) { + $builder->add($value); + } + + $builder->add($destination); + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * {@inheritdoc} + * + * - **force** (_boolean_) Force renaming or moving of a file even if the target exists + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + 'force' => false + )); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/PullCommand.php b/library/kzykhys/git/src/PHPGit/Command/PullCommand.php new file mode 100644 index 000000000..a7cea0025 --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/PullCommand.php @@ -0,0 +1,59 @@ +<?php + +namespace PHPGit\Command; + +use PHPGit\Command; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; + +/** + * Fetch from and merge with another repository or a local branch - `git pull` + * + * @author Kazuyuki Hayashi <hayashi@valnur.net> + */ +class PullCommand extends Command +{ + + /** + * Fetch from and merge with another repository or a local branch + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->pull('origin', 'master'); + * ``` + * + * @param string $repository The "remote" repository that is the source of a fetch or pull operation + * @param string $refspec The format of a <refspec> parameter is an optional plus +, + * followed by the source ref <src>, followed by a colon :, followed by the destination ref <dst> + * @param array $options [optional] An array of options {@see PullCommand::setDefaultOptions} + * + * @return bool + */ + public function __invoke($repository = null, $refspec = null, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('pull'); + + if ($repository) { + $builder->add($repository); + + if ($refspec) { + $builder->add($refspec); + } + } + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * {@inheritdoc} + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/PushCommand.php b/library/kzykhys/git/src/PHPGit/Command/PushCommand.php new file mode 100644 index 000000000..d0665d735 --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/PushCommand.php @@ -0,0 +1,65 @@ +<?php + +namespace PHPGit\Command; + +use PHPGit\Command; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; + +/** + * Update remote refs along with associated objects - `git push` + * + * @author Kazuyuki Hayashi <hayashi@valnur.net> + */ +class PushCommand extends Command +{ + + /** + * Update remote refs along with associated objects + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->push('origin', 'master'); + * ``` + * + * @param string $repository The "remote" repository that is destination of a push operation + * @param string $refspec Specify what destination ref to update with what source object + * @param array $options [optional] An array of options {@see PushCommand::setDefaultOptions} + * + * @return bool + */ + public function __invoke($repository = null, $refspec = null, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('push'); + + $this->addFlags($builder, $options); + + if ($repository) { + $builder->add($repository); + + if ($refspec) { + $builder->add($refspec); + } + } + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * {@inheritdoc} + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + 'all' => false, + 'mirror' => false, + 'tags' => false, + 'force' => false + )); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/RebaseCommand.php b/library/kzykhys/git/src/PHPGit/Command/RebaseCommand.php new file mode 100644 index 000000000..7516b360c --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/RebaseCommand.php @@ -0,0 +1,129 @@ +<?php + +namespace PHPGit\Command; + +use PHPGit\Command; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; + +/** + * Forward-port local commits to the updated upstream head - `git rebase` + * + * @author Kazuyuki Hayashi <hayashi@valnur.net> + */ +class RebaseCommand extends Command +{ + + /** + * Forward-port local commits to the updated upstream head + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->fetch('origin'); + * $git->rebase('origin/master'); + * ``` + * + * ##### Options + * + * - **onto** (_string_) Starting point at which to create the new commits + * - **no-verify** (_boolean_) Bypasses the pre-rebase hook + * - **force-rebase** (_boolean_) Force the rebase even if the current branch is a descendant of the commit you are rebasing onto + * + * @param string $upstream [optional] Upstream branch to compare against + * @param string $branch [optional] Working branch; defaults to HEAD + * @param array $options [optional] An array of options {@see RebaseCommand::setDefaultOptions} + * + * @return bool + */ + public function __invoke($upstream = null, $branch = null, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('rebase'); + + if ($options['onto']) { + $builder->add('--onto')->add($options['onto']); + } + + if ($upstream) { + $builder->add($upstream); + } + + if ($branch) { + $builder->add($branch); + } + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * Restart the rebasing process after having resolved a merge conflict + * + * @return bool + */ + public function continues() + { + $builder = $this->git->getProcessBuilder() + ->add('rebase') + ->add('--continue'); + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * Abort the rebase operation and reset HEAD to the original branch + * + * @return bool + */ + public function abort() + { + $builder = $this->git->getProcessBuilder() + ->add('rebase') + ->add('--abort'); + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * Restart the rebasing process by skipping the current patch + * + * @return bool + */ + public function skip() + { + $builder = $this->git->getProcessBuilder() + ->add('rebase') + ->add('--skip'); + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * {@inheritdoc} + * + * - **onto** (_string_) Starting point at which to create the new commits + * - **no-verify** (_boolean_) Bypasses the pre-rebase hook + * - **force-rebase** (_boolean_) Force the rebase even if the current branch is a descendant of the commit you are rebasing onto + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + 'onto' => null, + 'no-verify' => false, + 'force-rebase' => false + )); + + $resolver->setAllowedTypes(array( + 'onto' => array('null', 'string') + )); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/Remote/SetBranchesCommand.php b/library/kzykhys/git/src/PHPGit/Command/Remote/SetBranchesCommand.php new file mode 100644 index 000000000..4e17a4d48 --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/Remote/SetBranchesCommand.php @@ -0,0 +1,98 @@ +<?php + +namespace PHPGit\Command\Remote; + +use PHPGit\Command; + +/** + * Changes the list of branches tracked by the named remote + * + * @author Kazuyuki Hayashi + */ +class SetBranchesCommand extends Command +{ + + /** + * Alias of set() + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); + * $git->remote->branches('origin', array('master', 'develop')); + * ``` + * + * @param string $name The remote name + * @param array $branches The names of the tracked branch + * + * @return bool + */ + public function __invoke($name, array $branches) + { + return $this->set($name, $branches); + } + + /** + * Changes the list of branches tracked by the named remote + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); + * $git->remote->branches->set('origin', array('master', 'develop')); + * ``` + * + * @param string $name The remote name + * @param array $branches The names of the tracked branch + * + * @return bool + */ + public function set($name, array $branches) + { + $builder = $this->git->getProcessBuilder() + ->add('remote') + ->add('set-branches') + ->add($name); + + foreach ($branches as $branch) { + $builder->add($branch); + } + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * Adds to the list of branches tracked by the named remote + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); + * $git->remote->branches->add('origin', array('master', 'develop')); + * ``` + * + * @param string $name The remote name + * @param array $branches The names of the tracked branch + * + * @return bool + */ + public function add($name, array $branches) + { + $builder = $this->git->getProcessBuilder() + ->add('remote') + ->add('set-branches') + ->add($name) + ->add('--add'); + + foreach ($branches as $branch) { + $builder->add($branch); + } + + $this->git->run($builder->getProcess()); + + return true; + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/Remote/SetHeadCommand.php b/library/kzykhys/git/src/PHPGit/Command/Remote/SetHeadCommand.php new file mode 100644 index 000000000..9241ef5b7 --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/Remote/SetHeadCommand.php @@ -0,0 +1,120 @@ +<?php + +namespace PHPGit\Command\Remote; + +use PHPGit\Command; + +/** + * Sets or deletes the default branch (i.e. the target of the symbolic-ref refs/remotes/<name>/HEAD) for the named remote + * + * @author Kazuyuki Hayashi + */ +class SetHeadCommand extends Command +{ + + /** + * Alias of set() + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); + * $git->remote->head('origin'); + * ``` + * + * @param string $name The remote name + * @param string $branch [optional] The symbolic-ref to set + * + * @return bool + */ + public function __invoke($name, $branch = null) + { + return $this->set($name, $branch); + } + + /** + * Sets the default branch for the named remote + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); + * $git->remote->head->set('origin'); + * ``` + * + * @param string $name The remote name + * @param string $branch [optional] The symbolic-ref to set + * + * @return bool + */ + public function set($name, $branch) + { + $builder = $this->git->getProcessBuilder() + ->add('remote') + ->add('set-head') + ->add($name); + + if ($branch) { + $builder->add($branch); + } + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * Deletes the default branch for the named remote + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); + * $git->remote->head->delete('origin'); + * ``` + * + * @param string $name The remote name + * + * @return bool + */ + public function delete($name) + { + $builder = $this->git->getProcessBuilder() + ->add('remote') + ->add('set-head') + ->add($name) + ->add('-d'); + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * Determine the default branch by querying remote + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); + * $git->remote->head->remote('origin'); + * ``` + * + * @param string $name The remote name + * + * @return bool + */ + public function remote($name) + { + $builder = $this->git->getProcessBuilder() + ->add('remote') + ->add('set-head') + ->add($name) + ->add('-a'); + + $this->git->run($builder->getProcess()); + + return true; + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/Remote/SetUrlCommand.php b/library/kzykhys/git/src/PHPGit/Command/Remote/SetUrlCommand.php new file mode 100644 index 000000000..7b7d84ff5 --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/Remote/SetUrlCommand.php @@ -0,0 +1,175 @@ +<?php + +namespace PHPGit\Command\Remote; + +use PHPGit\Command; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; + +/** + * Changes URL remote points to + * + * @author Kazuyuki Hayashi + */ +class SetUrlCommand extends Command +{ + + /** + * Alias of set() + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); + * $git->remote->url('origin', 'https://github.com/text/Text.git'); + * ``` + * + * ##### Options + * + * - **push** (_boolean_) Push URLs are manipulated instead of fetch URLs + * + * @param string $name The name of remote + * @param string $newUrl The new URL + * @param string $oldUrl [optional] The old URL + * @param array $options [optional] An array of options {@see SetUrlCommand::setDefaultOptions} + * + * @return bool + */ + public function __invoke($name, $newUrl, $oldUrl = null, array $options = array()) + { + return $this->set($name, $newUrl, $oldUrl, $options); + } + + /** + * Sets the URL remote to $newUrl + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); + * $git->remote->url->set('origin', 'https://github.com/text/Text.git'); + * ``` + * + * ##### Options + * + * - **push** (_boolean_) Push URLs are manipulated instead of fetch URLs + * + * @param string $name The name of remote + * @param string $newUrl The new URL + * @param string $oldUrl [optional] The old URL + * @param array $options [optional] An array of options {@see SetUrlCommand::setDefaultOptions} + * + * @return bool + */ + public function set($name, $newUrl, $oldUrl = null, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('remote') + ->add('set-url'); + + $this->addFlags($builder, $options); + + $builder + ->add($name) + ->add($newUrl); + + if ($oldUrl) { + $builder->add($oldUrl); + } + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * Adds new URL to remote + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); + * $git->remote->url->add('origin', 'https://github.com/text/Text.git'); + * ``` + * + * ##### Options + * + * - **push** (_boolean_) Push URLs are manipulated instead of fetch URLs + * + * @param string $name The name of remote + * @param string $newUrl The new URL + * @param array $options [optional] An array of options {@see SetUrlCommand::setDefaultOptions} + * + * @return bool + */ + public function add($name, $newUrl, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('remote') + ->add('set-url') + ->add('--add'); + + $this->addFlags($builder, $options); + + $builder + ->add($name) + ->add($newUrl); + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * Deletes all URLs matching regex $url + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); + * $git->remote->url->delete('origin', 'https://github.com'); + * ``` + * + * ##### Options + * + * - **push** (_boolean_) Push URLs are manipulated instead of fetch URLs + * + * @param string $name The remote name + * @param string $url The URL to delete + * @param array $options [optional] An array of options {@see SetUrlCommand::setDefaultOptions} + * + * @return bool + */ + public function delete($name, $url, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('remote') + ->add('set-url') + ->add('--delete'); + + $this->addFlags($builder, $options); + + $builder + ->add($name) + ->add($url); + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * {@inheritdoc} + * + * - **push** (_boolean_) Push URLs are manipulated instead of fetch URLs + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + 'push' => false + )); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/RemoteCommand.php b/library/kzykhys/git/src/PHPGit/Command/RemoteCommand.php new file mode 100644 index 000000000..08220a551 --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/RemoteCommand.php @@ -0,0 +1,278 @@ +<?php + +namespace PHPGit\Command; + +use PHPGit\Command; +use PHPGit\Git; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; + +/** + * Manage set of tracked repositories - `git remote` + * + * @author Kazuyuki Hayashi <hayashi@valnur.net> + * + * @method head($name, $branch) Sets the default branch for the named remote + * @method branches($name, $branches) Changes the list of branches tracked by the named remote + * @method url($name, $newUrl, $oldUrl = null, $options = array()) Sets the URL remote to $newUrl + */ +class RemoteCommand extends Command +{ + + /** @var Remote\SetHeadCommand */ + public $head; + + /** @var Remote\SetBranchesCommand */ + public $branches; + + /** @var Remote\SetUrlCommand */ + public $url; + + /** + * @param Git $git + */ + public function __construct(Git $git) + { + parent::__construct($git); + + $this->head = new Remote\SetHeadCommand($git); + $this->branches = new Remote\SetBranchesCommand($git); + $this->url = new Remote\SetUrlCommand($git); + } + + /** + * Calls sub-commands + * + * @param string $name The name of a property + * @param array $arguments An array of arguments + * + * @throws \BadMethodCallException + * @return mixed + */ + public function __call($name, $arguments) + { + if (isset($this->{$name}) && is_callable($this->{$name})) { + return call_user_func_array($this->{$name}, $arguments); + } + + throw new \BadMethodCallException(sprintf('Call to undefined method %s::%s()', __CLASS__, $name)); + } + + /** + * Returns an array of existing remotes + * + * ``` php + * $git = new PHPGit\Git(); + * $git->clone('https://github.com/kzykhys/Text.git', '/path/to/repo'); + * $git->setRepository('/path/to/repo'); + * $remotes = $git->remote(); + * ``` + * + * ##### Output Example + * + * ``` php + * [ + * 'origin' => [ + * 'fetch' => 'https://github.com/kzykhys/Text.git', + * 'push' => 'https://github.com/kzykhys/Text.git' + * ] + * ] + * ``` + * + * @return array + */ + public function __invoke() + { + $builder = $this->git->getProcessBuilder() + ->add('remote') + ->add('-v'); + + $remotes = array(); + $output = $this->git->run($builder->getProcess()); + $lines = $this->split($output); + + foreach ($lines as $line) { + if (preg_match('/^(.*)\t(.*)\s\((.*)\)$/', $line, $matches)) { + if (!isset($remotes[$matches[1]])) { + $remotes[$matches[1]] = array(); + } + + $remotes[$matches[1]][$matches[3]] = $matches[2]; + } + } + + return $remotes; + } + + /** + * Adds a remote named **$name** for the repository at **$url** + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); + * $git->fetch('origin'); + * ``` + * + * ##### Options + * + * - **tags** (_boolean_) With this option, `git fetch <name>` imports every tag from the remote repository + * - **no-tags** (_boolean_) With this option, `git fetch <name>` does not import tags from the remote repository + * + * @param string $name The name of the remote + * @param string $url The url of the remote + * @param array $options [optional] An array of options {@see RemoteCommand::setDefaultOptions} + * + * @return bool + */ + public function add($name, $url, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('remote') + ->add('add'); + + $this->addFlags($builder, $options, array('tags', 'no-tags')); + + $builder->add($name)->add($url); + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * Rename the remote named **$name** to **$newName** + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); + * $git->remote->rename('origin', 'upstream'); + * ``` + * + * @param string $name The remote name to rename + * @param string $newName The new remote name + * + * @return bool + */ + public function rename($name, $newName) + { + $builder = $this->git->getProcessBuilder() + ->add('remote') + ->add('rename') + ->add($name) + ->add($newName); + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * Remove the remote named **$name** + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); + * $git->remote->rm('origin'); + * ``` + * + * @param string $name The remote name to remove + * + * @return bool + */ + public function rm($name) + { + $builder = $this->git->getProcessBuilder() + ->add('remote') + ->add('rm') + ->add($name); + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * Gives some information about the remote **$name** + * + * ``` php + * $git = new PHPGit\Git(); + * $git->clone('https://github.com/kzykhys/Text.git', '/path/to/repo'); + * $git->setRepository('/path/to/repo'); + * echo $git->remote->show('origin'); + * ``` + * + * ##### Output Example + * + * ``` + * \* remote origin + * Fetch URL: https://github.com/kzykhys/Text.git + * Push URL: https://github.com/kzykhys/Text.git + * HEAD branch: master + * Remote branch: + * master tracked + * Local branch configured for 'git pull': + * master merges with remote master + * Local ref configured for 'git push': + * master pushes to master (up to date) + * ``` + * + * @param string $name The remote name to show + * + * @return string + */ + public function show($name) + { + $builder = $this->git->getProcessBuilder() + ->add('remote') + ->add('show') + ->add($name); + + return $this->git->run($builder->getProcess()); + } + + /** + * Deletes all stale remote-tracking branches under **$name** + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->remote->prune('origin'); + * ``` + * + * @param string $name The remote name + * + * @return bool + */ + public function prune($name = null) + { + $builder = $this->git->getProcessBuilder() + ->add('remote') + ->add('prune'); + + if ($name) { + $builder->add($name); + } + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * {@inheritdoc} + * + * - **tags** (_boolean_) With this option, `git fetch <name>` imports every tag from the remote repository + * - **no-tags** (_boolean_) With this option, `git fetch <name>` does not import tags from the remote repository + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + 'tags' => false, + 'no-tags' => false + )); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/ResetCommand.php b/library/kzykhys/git/src/PHPGit/Command/ResetCommand.php new file mode 100644 index 000000000..f70f53e2e --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/ResetCommand.php @@ -0,0 +1,199 @@ +<?php + +namespace PHPGit\Command; + +use PHPGit\Command; +use PHPGit\Exception\GitException; + +/** + * Reset current HEAD to the specified state - `git reset` + * + * @author Kazuyuki Hayashi <hayashi@valnur.net> + */ +class ResetCommand extends Command +{ + + /** + * Resets the index entries for all **$paths** to their state at **$commit** + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->reset(); + * ``` + * + * @param string|array|\Traversable $paths The paths to reset + * @param string $commit The commit + * + * @return bool + */ + public function __invoke($paths, $commit = null) + { + $builder = $this->git->getProcessBuilder() + ->add('reset'); + + if ($commit) { + $builder->add($commit)->add('--'); + } + + if (!is_array($paths) && !($paths instanceof \Traversable)) { + $paths = array($paths); + } + + foreach ($paths as $path) { + $builder->add($path); + } + + try { + $this->git->run($builder->getProcess()); + } catch (GitException $e) { + // Confirm exit code + } + + return true; + } + + /** + * Resets the current branch head to **$commit** + * + * Does not touch the index file nor the working tree at all (but resets the head to **$commit**, + * just like all modes do). + * This leaves all your changed files "Changes to be committed", as git status would put it. + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->reset->soft(); + * ``` + * + * @param string $commit The commit + * + * @return bool + */ + public function soft($commit = null) + { + return $this->mode('soft', $commit); + } + + /** + * Resets the current branch head to **$commit** + * + * Resets the index but not the working tree (i.e., the changed files are preserved but not marked for commit) + * and reports what has not been updated. This is the default action. + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->reset->mixed(); + * ``` + * + * @param string $commit The commit + * + * @return bool + */ + public function mixed($commit = null) + { + return $this->mode('mixed', $commit); + } + + /** + * Resets the current branch head to **$commit** + * + * Resets the index and working tree. Any changes to tracked files in the working tree since **$commit** are discarded + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->reset->hard(); + * ``` + * + * @param string $commit The commit + * + * @return bool + */ + public function hard($commit = null) + { + return $this->mode('hard', $commit); + } + + /** + * Resets the current branch head to **$commit** + * + * Resets the index and updates the files in the working tree that are different between **$commit** and HEAD, + * but keeps those which are different between the index and working tree + * (i.e. which have changes which have not been added). If a file that is different between **$commit** and + * the index has unstaged changes, reset is aborted + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->reset->merge(); + * ``` + * + * @param string $commit The commit + * + * @return bool + */ + public function merge($commit = null) + { + return $this->mode('merge', $commit); + } + + /** + * Resets the current branch head to **$commit** + * + * Resets index entries and updates files in the working tree that are different between **$commit** and HEAD. + * If a file that is different between **$commit** and HEAD has local changes, reset is aborted. + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->reset->keep(); + * ``` + * + * @param string $commit The commit + * + * @return bool + */ + public function keep($commit = null) + { + return $this->mode('keep', $commit); + } + + /** + * Resets the current branch head to **$commit** + * + * Possibly updates the index (resetting it to the tree of **$commit**) and the working tree depending on **$mode** + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->reset->mode('hard'); + * ``` + * + * @param string $mode --<mode> + * @param string $commit The commit + * + * @throws \InvalidArgumentException + * @return bool + */ + public function mode($mode, $commit = null) + { + if (!in_array($mode, array('soft', 'mixed', 'hard', 'merge', 'keep'))) { + throw new \InvalidArgumentException('$mode must be one of the following: soft, mixed, hard, merge, keep'); + } + + $builder = $this->git->getProcessBuilder() + ->add('reset') + ->add('--' . $mode); + + if ($commit) { + $builder->add($commit); + } + + $this->git->run($builder->getProcess()); + + return true; + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/RmCommand.php b/library/kzykhys/git/src/PHPGit/Command/RmCommand.php new file mode 100644 index 000000000..d6da31230 --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/RmCommand.php @@ -0,0 +1,97 @@ +<?php + +namespace PHPGit\Command; + +use PHPGit\Command; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; + +/** + * Remove files from the working tree and from the index - `git rm` + * + * @author Kazuyuki Hayashi + */ +class RmCommand extends Command +{ + + /** + * Remove files from the working tree and from the index + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->rm('CHANGELOG-1.0-1.1.txt', ['force' => true]); + * ``` + * + * ##### Options + * + * - **force** (_boolean_) Override the up-to-date check + * - **cached** (_boolean_) Unstage and remove paths only from the index + * - **recursive** (_boolean_) Allow recursive removal when a leading directory name is given + * + * @param string|array|\Traversable $file Files to remove. Fileglobs (e.g. *.c) can be given to remove all matching files. + * @param array $options [optional] An array of options {@see RmCommand::setDefaultOptions} + * + * @return bool + */ + public function __invoke($file, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('rm'); + + $this->addFlags($builder, $options, array('force', 'cached')); + + if ($options['recursive']) { + $builder->add('-r'); + } + + if (!is_array($file) && !($file instanceof \Traversable)) { + $file = array($file); + } + + foreach ($file as $value) { + $builder->add($value); + } + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * Equivalent to $git->rm($file, ['cached' => true]); + * + * ##### Options + * + * - **force** (_boolean_) Override the up-to-date check + * - **recursive** (_boolean_) Allow recursive removal when a leading directory name is given + * + * @param string|array|\Traversable $file Files to remove. Fileglobs (e.g. *.c) can be given to remove all matching files. + * @param array $options [optional] An array of options {@see RmCommand::setDefaultOptions} + * + * @return bool + */ + public function cached($file, array $options = array()) + { + $options['cached'] = true; + + return $this->__invoke($file, $options); + } + + /** + * {@inheritdoc} + * + * - **force** (_boolean_) Override the up-to-date check + * - **cached** (_boolean_) Unstage and remove paths only from the index + * - **recursive** (_boolean_) Allow recursive removal when a leading directory name is given + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + 'force' => false, + 'cached' => false, + 'recursive' => false + )); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/ShortlogCommand.php b/library/kzykhys/git/src/PHPGit/Command/ShortlogCommand.php new file mode 100644 index 000000000..23c66e464 --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/ShortlogCommand.php @@ -0,0 +1,134 @@ +<?php + +namespace PHPGit\Command; + +use PHPGit\Command; + +/** + * Summarize 'git log' output - `git shortlog` + * + * @author Kazuyuki Hayashi <hayashi@valnur.net> + */ +class ShortlogCommand extends Command +{ + + /** + * Summarize 'git log' output + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $shortlog = $git->shortlog(); + * ``` + * + * ##### Output Example + * + * ``` php + * [ + * 'John Doe <john@example.com>' => [ + * 0 => ['commit' => '589de67', 'date' => new \DateTime('2014-02-10 12:56:15 +0300'), 'subject' => 'Update README'], + * 1 => ['commit' => '589de67', 'date' => new \DateTime('2014-02-15 12:56:15 +0300'), 'subject' => 'Update README'], + * ], + * //... + * ] + * ``` + * @param string|array|\Traversable $commits [optional] Defaults to HEAD + * + * @return array + */ + public function __invoke($commits = 'HEAD') + { + $builder = $this->git->getProcessBuilder() + ->add('shortlog') + ->add('--numbered') + ->add('--format=') + ->add('-w256,2,2') + ->add('-e'); + + if (!is_array($commits) && !($commits instanceof \Traversable)) { + $commits = array($commits); + } + + foreach ($commits as $commit) { + $builder->add($commit); + } + + $process = $builder->getProcess(); + $process->setCommandLine(str_replace('--format=', '--format=%h|%ci|%s', $process->getCommandLine())); + + $output = $this->git->run($process); + $lines = $this->split($output); + $result = array(); + $author = null; + + foreach ($lines as $line) { + if (substr($line, 0, 1) != ' ') { + if (preg_match('/([^<>]*? <[^<>]+>)/', $line, $matches)) { + $author = $matches[1]; + $result[$author] = array(); + } + continue; + } + + list ($commit, $date, $subject) = explode('|', trim($line), 3); + $result[$author][] = array( + 'commit' => $commit, + 'date' => new \DateTime($date), + 'subject' => $subject + ); + } + + return $result; + } + + /** + * Suppress commit description and provide a commit count summary only + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $shortlog = $git->shortlog->summary(); + * ``` + * + * ##### Output Example + * + * ``` php + * [ + * 'John Doe <john@example.com>' => 153, + * //... + * ] + * ``` + * + * @param string $commits [optional] Defaults to HEAD + * + * @return array + */ + public function summary($commits = 'HEAD') + { + $builder = $this->git->getProcessBuilder() + ->add('shortlog') + ->add('--numbered') + ->add('--summary') + ->add('-e'); + + if (!is_array($commits) && !($commits instanceof \Traversable)) { + $commits = array($commits); + } + + foreach ($commits as $commit) { + $builder->add($commit); + } + + $output = $this->git->run($builder->getProcess()); + $lines = $this->split($output); + $result = array(); + + foreach ($lines as $line) { + list ($commits, $author) = explode("\t", trim($line), 2); + $result[$author] = (int) $commits; + } + + return $result; + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/ShowCommand.php b/library/kzykhys/git/src/PHPGit/Command/ShowCommand.php new file mode 100644 index 000000000..866388357 --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/ShowCommand.php @@ -0,0 +1,70 @@ +<?php + +namespace PHPGit\Command; + +use PHPGit\Command; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; + +/** + * Show various types of objects - `git show` + * + * @author Kazuyuki Hayashi + */ +class ShowCommand extends Command +{ + + /** + * Shows one or more objects (blobs, trees, tags and commits) + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * echo $git->show('3ddee587e209661c8265d5bfd0df999836f6dfa2'); + * ``` + * + * ##### Options + * + * - **format** (_string_) Pretty-print the contents of the commit logs in a given format, where <format> can be one of oneline, short, medium, full, fuller, email, raw and format:<string> + * - **abbrev-commit** (_boolean_) Instead of showing the full 40-byte hexadecimal commit object name, show only a partial prefix + * + * @param string $object The names of objects to show + * @param array $options [optional] An array of options {@see ShowCommand::setDefaultOptions} + * + * @return string + */ + public function __invoke($object, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('show'); + + $this->addFlags($builder, $options, array('abbrev-commit')); + + if ($options['format']) { + $builder->add('--format=' . $options['format']); + } + + $builder->add($object); + + return $this->git->run($builder->getProcess()); + } + + /** + * {@inheritdoc} + * + * - **format** (_string_) Pretty-print the contents of the commit logs in a given format, where <format> can be one of oneline, short, medium, full, fuller, email, raw and format:<string> + * - **abbrev-commit** (_boolean_) Instead of showing the full 40-byte hexadecimal commit object name, show only a partial prefix + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + 'format' => null, + 'abbrev-commit' => false + )); + + $resolver->setAllowedTypes(array( + 'format' => array('null', 'string'), + )); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/StashCommand.php b/library/kzykhys/git/src/PHPGit/Command/StashCommand.php new file mode 100644 index 000000000..52dceaa6b --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/StashCommand.php @@ -0,0 +1,309 @@ +<?php + +namespace PHPGit\Command; + +use PHPGit\Command; + +/** + * Stash the changes in a dirty working directory away - `git stash` + * + * @author Kazuyuki Hayashi + */ +class StashCommand extends Command +{ + + /** + * Save your local modifications to a new stash, and run git reset --hard to revert them + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->stash(); + * ``` + * + * @return bool + */ + public function __invoke() + { + $builder = $this->git->getProcessBuilder() + ->add('stash'); + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * Save your local modifications to a new stash, and run git reset --hard to revert them. + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->stash->save('My stash'); + * ``` + * + * @param string $message [optional] The description along with the stashed state + * @param array $options [optional] An array of options {@see StashCommand::setDefaultOptions} + * + * @return bool + */ + public function save($message = null, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('stash') + ->add('save'); + + $builder->add($message); + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * Returns the stashes that you currently have + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $stashes = $git->stash->lists(); + * ``` + * + * ##### Output Example + * + * ``` php + * [ + * 0 => ['branch' => 'master', 'message' => '0e2f473 Fixes README.md'], + * 1 => ['branch' => 'master', 'message' => 'ce1ddde Initial commit'], + * ] + * ``` + * + * @param array $options [optional] An array of options {@see StashCommand::setDefaultOptions} + * + * @return array + */ + public function lists(array $options = array()) + { + $builder = $this->git->getProcessBuilder() + ->add('stash') + ->add('list'); + + $output = $this->git->run($builder->getProcess()); + $lines = $this->split($output); + $list = array(); + + foreach ($lines as $line) { + if (preg_match('/stash@{(\d+)}:.* [Oo]n (.*): (.*)/', $line, $matches)) { + $list[$matches[1]] = array( + 'branch' => $matches[2], + 'message' => $matches[3] + ); + } + } + + return $list; + } + + /** + * Show the changes recorded in the stash as a diff between the stashed state and its original parent + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * echo $git->stash->show('stash@{0}'); + * ``` + * + * ##### Output Example + * + * ``` + * REAMDE.md | 2 +- + * 1 files changed, 1 insertions(+), 1 deletions(-) + * ``` + * + * @param string $stash The stash to show + * + * @return string + */ + public function show($stash = null) + { + $builder = $this->git->getProcessBuilder() + ->add('stash') + ->add('show'); + + if ($stash) { + $builder->add($stash); + } + + return $this->git->run($builder->getProcess()); + } + + /** + * Remove a single stashed state from the stash list + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->stash->drop('stash@{0}'); + * ``` + * + * @param string $stash The stash to drop + * + * @return mixed + */ + public function drop($stash = null) + { + $builder = $this->git->getProcessBuilder() + ->add('stash') + ->add('drop'); + + if ($stash) { + $builder->add($stash); + } + + return $this->git->run($builder->getProcess()); + } + + /** + * Remove a single stashed state from the stash list and apply it on top of the current working tree state + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->stash->pop('stash@{0}'); + * ``` + * + * @param string $stash The stash to pop + * @param array $options [optional] An array of options {@see StashCommand::setDefaultOptions} + * + * @return bool + */ + public function pop($stash = null, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('stash') + ->add('pop'); + + $this->addFlags($builder, $options, array('index')); + + if ($stash) { + $builder->add($stash); + } + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * Like pop, but do not remove the state from the stash list + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->stash->apply('stash@{0}'); + * ``` + * + * @param string $stash The stash to apply + * @param array $options [optional] An array of options {@see StashCommand::setDefaultOptions} + * + * @return bool + */ + public function apply($stash = null, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('stash') + ->add('apply'); + + $this->addFlags($builder, $options, array('index')); + + if ($stash) { + $builder->add($stash); + } + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * Creates and checks out a new branch named <branchname> starting from the commit at which the <stash> was originally created, applies the changes recorded in <stash> to the new working tree and index + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->stash->branch('hotfix', 'stash@{0}'); + * ``` + * + * @param string $name The name of the branch + * @param string $stash The stash + * + * @return bool + */ + public function branch($name, $stash = null) + { + $builder = $this->git->getProcessBuilder() + ->add('stash') + ->add('branch') + ->add($name); + + if ($stash) { + $builder->add($stash); + } + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * Remove all the stashed states + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->stash->clear(); + * ``` + * + * @return bool + */ + public function clear() + { + $builder = $this->git->getProcessBuilder() + ->add('stash') + ->add('clear'); + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * Create a stash (which is a regular commit object) and return its object name, without storing it anywhere in the ref namespace + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $commit = $git->stash->create(); + * ``` + * + * ##### Output Example + * + * ``` + * 877316ea6f95c43b7ccc2c2a362eeedfa78b597d + * ``` + * + * @return string + */ + public function create() + { + $builder = $this->git->getProcessBuilder() + ->add('stash') + ->add('create'); + + return $this->git->run($builder->getProcess()); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/StatusCommand.php b/library/kzykhys/git/src/PHPGit/Command/StatusCommand.php new file mode 100644 index 000000000..c2bc983fe --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/StatusCommand.php @@ -0,0 +1,147 @@ +<?php + +namespace PHPGit\Command; + +use PHPGit\Command; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; + +/** + * Show the working tree status - `git status` + * + * = unmodified + * M = modified + * A = added + * D = deleted + * R = renamed + * C = copied + * U = updated but unmerged + * + * X Y Meaning + * ------------------------------------------------- + * [MD] not updated + * M [ MD] updated in index + * A [ MD] added to index + * D [ M] deleted from index + * R [ MD] renamed in index + * C [ MD] copied in index + * [MARC] index and work tree matches + * [ MARC] M work tree changed since index + * [ MARC] D deleted in work tree + * ------------------------------------------------- + * D D unmerged, both deleted + * A U unmerged, added by us + * U D unmerged, deleted by them + * U A unmerged, added by them + * D U unmerged, deleted by us + * A A unmerged, both added + * U U unmerged, both modified + * ------------------------------------------------- + * ? ? untracked + * ! ! ignored + * ------------------------------------------------- + * + * @author Kazuyuki Hayashi <hayashi@valnur.net> + */ +class StatusCommand extends Command +{ + + const UNMODIFIED = ' '; + const MODIFIED = 'M'; + const ADDED = 'A'; + const DELETED = 'D'; + const RENAMED = 'R'; + const COPIED = 'C'; + const UPDATED_BUT_UNMERGED = 'U'; + const UNTRACKED = '?'; + const IGNORED = '!'; + + /** + * Returns the working tree status + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $status = $git->status(); + * ``` + * + * ##### Constants + * + * - StatusCommand::UNMODIFIED [=' '] unmodified + * - StatusCommand::MODIFIED [='M'] modified + * - StatusCommand::ADDED [='A'] added + * - StatusCommand::DELETED [='D'] deleted + * - StatusCommand::RENAMED [='R'] renamed + * - StatusCommand::COPIED [='C'] copied + * - StatusCommand::UPDATED_BUT_UNMERGED [='U'] updated but unmerged + * - StatusCommand::UNTRACKED [='?'] untracked + * - StatusCommand::IGNORED [='!'] ignored + * + * ##### Output Example + * + * ``` php + * [ + * 'branch' => 'master', + * 'changes' => [ + * ['file' => 'item1.txt', 'index' => 'A', 'work_tree' => 'M'], + * ['file' => 'item2.txt', 'index' => 'A', 'work_tree' => ' '], + * ['file' => 'item3.txt', 'index' => '?', 'work_tree' => '?'], + * ] + * ] + * ``` + * + * ##### Options + * + * - **ignored** (_boolean_) Show ignored files as well + * + * @param array $options [optional] An array of options {@see StatusCommand::setDefaultOptions} + * + * @return mixed + */ + public function __invoke(array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('status') + ->add('--porcelain')->add('-s')->add('-b')->add('--null'); + + $this->addFlags($builder, $options); + + $process = $builder->getProcess(); + $result = array('branch' => null, 'changes' => array()); + $output = $this->git->run($process); + + list($branch, $changes) = preg_split('/(\0|\n)/', $output, 2); + $lines = $this->split($changes, true); + + if (substr($branch, -11) == '(no branch)') { + $result['branch'] = null; + } elseif (preg_match('/([^ ]*)\.\.\..*?\[.*?\]$/', $branch, $matches)) { + $result['branch'] = $matches[1]; + } elseif (preg_match('/ ([^ ]*)$/', $branch, $matches)) { + $result['branch'] = $matches[1]; + } + + foreach ($lines as $line) { + $result['changes'][] = array( + 'file' => substr($line, 3), + 'index' => substr($line, 0, 1), + 'work_tree' => substr($line, 1, 1) + ); + } + + return $result; + } + + /** + * {@inheritdoc} + * + * - **ignored** (_boolean_) Show ignored files as well + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + 'ignored' => false + )); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/TagCommand.php b/library/kzykhys/git/src/PHPGit/Command/TagCommand.php new file mode 100644 index 000000000..197d3e887 --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/TagCommand.php @@ -0,0 +1,156 @@ +<?php + +namespace PHPGit\Command; + +use PHPGit\Command; +use PHPGit\Exception\GitException; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; + +/** + * Create, list, delete or verify a tag object signed with GPG - `git tag` + * + * @author Kazuyuki Hayashi + */ +class TagCommand extends Command +{ + + /** + * Returns an array of tags + * + * ``` php + * $git = new PHPGit\Git(); + * $git->clone('https://github.com/kzykhys/PHPGit.git', '/path/to/repo'); + * $git->setRepository('/path/to/repo'); + * $tags = $git->tag(); + * ``` + * + * ##### Output Example + * + * ``` + * ['v1.0.0', 'v1.0.1', 'v1.0.2'] + * ``` + * + * @throws GitException + * @return array + */ + public function __invoke() + { + $builder = $this->git->getProcessBuilder() + ->add('tag'); + + $output = $this->git->run($builder->getProcess()); + + return $this->split($output); + } + + /** + * Creates a tag object + * + * ``` php + * $git = new PHPGit\Git(); + * $git->setRepository('/path/to/repo'); + * $git->tag->create('v1.0.0'); + * ``` + * + * ##### Options + * + * - **annotate** (_boolean_) Make an unsigned, annotated tag object + * - **sign** (_boolean_) Make a GPG-signed tag, using the default e-mail address’s key + * - **force** (_boolean_) Replace an existing tag with the given name (instead of failing) + * + * @param string $tag The name of the tag to create + * @param string $commit The SHA1 object name of the commit object + * @param array $options [optional] An array of options {@see TagCommand::setDefaultOptions} + * + * @throws GitException + * @return bool + */ + public function create($tag, $commit = null, array $options = array()) + { + $options = $this->resolve($options); + $builder = $this->git->getProcessBuilder() + ->add('tag') + ->add($tag); + + $this->addFlags($builder, $options, array('annotate', 'sign', 'force')); + + if ($commit) { + $builder->add($commit); + } + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * Delete existing tags with the given names + * + * @param string|array|\Traversable $tag The name of the tag to create + * + * @throws GitException + * @return bool + */ + public function delete($tag) + { + $builder = $this->git->getProcessBuilder() + ->add('tag') + ->add('-d'); + + if (!is_array($tag) && !($tag instanceof \Traversable)) { + $tag = array($tag); + } + + foreach ($tag as $value) { + $builder->add($value); + } + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * Verify the gpg signature of the given tag names + * + * @param string|array|\Traversable $tag The name of the tag to create + * + * @throws GitException + * @return bool + */ + public function verify($tag) + { + $builder = $this->git->getProcessBuilder() + ->add('tag') + ->add('-v'); + + if (!is_array($tag) && !($tag instanceof \Traversable)) { + $tag = array($tag); + } + + foreach ($tag as $value) { + $builder->add($value); + } + + $this->git->run($builder->getProcess()); + + return true; + } + + /** + * {@inheritdoc} + * + * - **annotate** (_boolean_) Make an unsigned, annotated tag object + * - **sign** (_boolean_) Make a GPG-signed tag, using the default e-mail address’s key + * - **force** (_boolean_) Replace an existing tag with the given name (instead of failing) + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults(array( + 'annotate' => false, + 'sign' => false, + 'force' => false, + )); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Command/TreeCommand.php b/library/kzykhys/git/src/PHPGit/Command/TreeCommand.php new file mode 100644 index 000000000..ea1040a6a --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Command/TreeCommand.php @@ -0,0 +1,70 @@ +<?php + +namespace PHPGit\Command; + +use PHPGit\Command; + +/** + * List the contents of a tree object - `git ls-tree` + * + * @author Kazuyuki Hayashi <hayashi@valnur.net> + */ +class TreeCommand extends Command +{ + + /** + * Returns the contents of a tree object + * + * ``` php + * $git = new PHPGit\Git(); + * $git->clone('https://github.com/kzykhys/PHPGit.git', '/path/to/repo'); + * $git->setRepository('/path/to/repo'); + * $tree = $git->tree('master'); + * ``` + * + * ##### Output Example + * + * ``` php + * [ + * ['mode' => '100644', 'type' => 'blob', 'hash' => '1f100ce9855b66111d34b9807e47a73a9e7359f3', 'file' => '.gitignore', 'sort' => '2:.gitignore'], + * ['mode' => '100644', 'type' => 'blob', 'hash' => 'e0bfe494537037451b09c32636c8c2c9795c05c0', 'file' => '.travis.yml', 'sort' => '2:.travis.yml'], + * ['mode' => '040000', 'type' => 'tree', 'hash' => '8d5438e79f77cd72de80c49a413f4edde1f3e291', 'file' => 'bin', 'sort' => '1:.bin'], + * ] + * ``` + * + * @param string $branch The commit + * @param string $path The path + * + * @return array + */ + public function __invoke($branch = 'master', $path = '') + { + $objects = array(); + $builder = $this->git->getProcessBuilder(); + $process = $builder->add('ls-tree')->add($branch . ':' . $path)->getProcess(); + $output = $this->git->run($process); + $lines = $this->split($output); + + $types = array( + 'submodule' => 0, + 'tree' => 1, + 'blob' => 2 + ); + + foreach ($lines as $line) { + list($meta, $file) = explode("\t", $line); + list($mode, $type, $hash) = explode(" ", $meta); + + $objects[] = array( + 'sort' => sprintf('%d:%s', $types[$type], $file), + 'mode' => $mode, + 'type' => $type, + 'hash' => $hash, + 'file' => $file + ); + } + + return $objects; + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Exception/GitException.php b/library/kzykhys/git/src/PHPGit/Exception/GitException.php new file mode 100644 index 000000000..f5901010c --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Exception/GitException.php @@ -0,0 +1,39 @@ +<?php + +namespace PHPGit\Exception; + +/** + * @author Kazuyuki Hayashi <hayashi@valnur.net> + */ +class GitException extends \Exception +{ + + /** + * @var string + */ + protected $commandLine; + + /** + * Construct the exception. Note: The message is NOT binary safe. + * + * @param string $message [optional] The Exception message to throw. + * @param int $code [optional] The Exception code. + * @param string $commandLine [optional] Command-line + * @param \Exception $previous [optional] The previous exception used for the exception chaining. Since 5.3.0 + */ + public function __construct($message = "", $code = 0, $commandLine = null, \Exception $previous = null) + { + parent::__construct($message, $code, $previous); + + $this->commandLine = $commandLine; + } + + /** + * @return null|string + */ + public function getCommandLine() + { + return $this->commandLine; + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/src/PHPGit/Git.php b/library/kzykhys/git/src/PHPGit/Git.php new file mode 100644 index 000000000..f952d6f1f --- /dev/null +++ b/library/kzykhys/git/src/PHPGit/Git.php @@ -0,0 +1,308 @@ +<?php + +namespace PHPGit; + +use PHPGit\Command; +use PHPGit\Exception\GitException; +use Symfony\Component\Process\Process; +use Symfony\Component\Process\ProcessBuilder; + +/** + * PHPGit - A Git wrapper for PHP5.3+ + * ================================== + * + * [![Latest Unstable Version](https://poser.pugx.org/kzykhys/git/v/unstable.png)](https://packagist.org/packages/kzykhys/git) + * [![Build Status](https://travis-ci.org/kzykhys/PHPGit.png?branch=master)](https://travis-ci.org/kzykhys/PHPGit) + * [![Coverage Status](https://coveralls.io/repos/kzykhys/PHPGit/badge.png)](https://coveralls.io/r/kzykhys/PHPGit) + * [![SensioLabsInsight](https://insight.sensiolabs.com/projects/04f10b57-a113-47ad-8dda-9a6dacbb079f/mini.png)](https://insight.sensiolabs.com/projects/04f10b57-a113-47ad-8dda-9a6dacbb079f) + * + * Requirements + * ------------ + * + * * PHP5.3 + * * Git + * + * Installation + * ------------ + * + * Update your composer.json and run `composer update` + * + * ``` json + * { + * "require": { + * "kzykhys/git": "dev-master" + * } + * } + * ``` + * + * Basic Usage + * ----------- + * + * ``` php + * <?php + * + * require __DIR__ . '/vendor/autoload.php'; + * + * $git = new PHPGit\Git(); + * $git->clone('https://github.com/kzykhys/PHPGit.git', '/path/to/repo'); + * $git->setRepository('/path/to/repo'); + * $git->remote->add('production', 'git://example.com/your/repo.git'); + * $git->add('README.md'); + * $git->commit('Adds README.md'); + * $git->checkout('release'); + * $git->merge('master'); + * $git->push(); + * $git->push('production', 'release'); + * $git->tag->create('v1.0.1', 'release'); + * + * foreach ($git->tree('release') as $object) { + * if ($object['type'] == 'blob') { + * echo $git->show($object['file']); + * } + * } + * ``` + * + * @author Kazuyuki Hayashi <hayashi@valnur.net> + * @license MIT + * + * @method add($file, $options = array()) Add file contents to the index + * @method archive($file, $tree = null, $path = null, $options = array()) Create an archive of files from a named tree + * @method branch($options = array()) List both remote-tracking branches and local branches + * @method checkout($branch, $options = array()) Checkout a branch or paths to the working tree + * @method clone($repository, $path = null, $options = array()) Clone a repository into a new directory + * @method commit($message = '', $options = array()) Record changes to the repository + * @method config($options = array()) List all variables set in config file + * @method describe($committish = null, $options = array()) Returns the most recent tag that is reachable from a commit + * @method fetch($repository, $refspec = null, $options = array()) Fetches named heads or tags from one or more other repositories + * @method init($path, $options = array()) Create an empty git repository or reinitialize an existing one + * @method log($path = null, $options = array()) Returns the commit logs + * @method merge($commit, $message = null, $options = array()) Incorporates changes from the named commits into the current branch + * @method mv($source, $destination, $options = array()) Move or rename a file, a directory, or a symlink + * @method pull($repository = null, $refspec = null, $options = array()) Fetch from and merge with another repository or a local branch + * @method push($repository = null, $refspec = null, $options = array()) Update remote refs along with associated objects + * @method rebase($upstream = null, $branch = null, $options = array()) Forward-port local commits to the updated upstream head + * @method remote() Returns an array of existing remotes + * @method reset($commit = null, $paths = array()) Resets the index entries for all <paths> to their state at <commit> + * @method rm($file, $options = array()) Remove files from the working tree and from the index + * @method shortlog($commits = array()) Summarize 'git log' output + * @method show($object, $options = array()) Shows one or more objects (blobs, trees, tags and commits) + * @method stash() Save your local modifications to a new stash, and run git reset --hard to revert them + * @method status($options = array()) Show the working tree status + * @method tag() Returns an array of tags + * @method tree($branch = 'master', $path = '') List the contents of a tree object + */ +class Git +{ + + /** @var Command\AddCommand */ + public $add; + + /** @var Command\ArchiveCommand */ + public $archive; + + /** @var Command\BranchCommand */ + public $branch; + + /** @var Command\CatCommand */ + public $cat; + + /** @var Command\CheckoutCommand */ + public $checkout; + + /** @var Command\CloneCommand */ + public $clone; + + /** @var Command\CommitCommand */ + public $commit; + + /** @var Command\ConfigCommand */ + public $config; + + /** @var Command\DescribeCommand */ + public $describe; + + // Not implemented yet + public $diff; + + /** @var Command\FetchCommand */ + public $fetch; + + /** @var Command\InitCommand */ + public $init; + + /** @var Command\LogCommand */ + public $log; + + /** @var Command\MergeCommand */ + public $merge; + + /** @var Command\MvCommand */ + public $mv; + + /** @var Command\PullCommand */ + public $pull; + + /** @var Command\PushCommand */ + public $push; + + /** @var Command\RebaseCommand */ + public $rebase; + + /** @var Command\RemoteCommand */ + public $remote; + + /** @var Command\ResetCommand */ + public $reset; + + /** @var Command\RmCommand */ + public $rm; + + /** @var Command\ShortlogCommand */ + public $shortlog; + + /** @var Command\ShowCommand */ + public $show; + + /** @var Command\StashCommand */ + public $stash; + + /** @var Command\StatusCommand */ + public $status; + + /** @var Command\TagCommand */ + public $tag; + + /** @var Command\TreeCommand */ + public $tree; + + /** @var string */ + private $bin = 'git'; + + /** @var string */ + private $directory = '.'; + + /** + * Initializes sub-commands + */ + public function __construct() + { + $this->add = new Command\AddCommand($this); + $this->archive = new Command\ArchiveCommand($this); + $this->branch = new Command\BranchCommand($this); + $this->cat = new Command\CatCommand($this); + $this->checkout = new Command\CheckoutCommand($this); + $this->clone = new Command\CloneCommand($this); + $this->commit = new Command\CommitCommand($this); + $this->config = new Command\ConfigCommand($this); + $this->describe = new Command\DescribeCommand($this); + $this->fetch = new Command\FetchCommand($this); + $this->init = new Command\InitCommand($this); + $this->log = new Command\LogCommand($this); + $this->merge = new Command\MergeCommand($this); + $this->mv = new Command\MvCommand($this); + $this->pull = new Command\PullCommand($this); + $this->push = new Command\PushCommand($this); + $this->rebase = new Command\RebaseCommand($this); + $this->remote = new Command\RemoteCommand($this); + $this->reset = new Command\ResetCommand($this); + $this->rm = new Command\RmCommand($this); + $this->shortlog = new Command\ShortlogCommand($this); + $this->show = new Command\ShowCommand($this); + $this->stash = new Command\StashCommand($this); + $this->status = new Command\StatusCommand($this); + $this->tag = new Command\TagCommand($this); + $this->tree = new Command\TreeCommand($this); + } + + /** + * Calls sub-commands + * + * @param string $name The name of a property + * @param array $arguments An array of arguments + * + * @throws \BadMethodCallException + * @return mixed + */ + public function __call($name, $arguments) + { + if (isset($this->{$name}) && is_callable($this->{$name})) { + return call_user_func_array($this->{$name}, $arguments); + } + + throw new \BadMethodCallException(sprintf('Call to undefined method PHPGit\Git::%s()', $name)); + } + + /** + * Sets the Git binary path + * + * @param string $bin + * + * @return Git + */ + public function setBin($bin) + { + $this->bin = $bin; + + return $this; + } + + /** + * Sets the Git repository path + * + * @var string $directory + * + * @return Git + */ + public function setRepository($directory) + { + $this->directory = $directory; + + return $this; + } + + /** + * Returns version number + * + * @return mixed + */ + public function getVersion() + { + $process = $this->getProcessBuilder() + ->add('--version') + ->getProcess(); + + return $this->run($process); + } + + /** + * Returns an instance of ProcessBuilder + * + * @return ProcessBuilder + */ + public function getProcessBuilder() + { + return ProcessBuilder::create() + ->setPrefix($this->bin) + ->setWorkingDirectory($this->directory); + } + + /** + * Executes a process + * + * @param Process $process The process to run + * + * @throws Exception\GitException + * @return mixed + */ + public function run(Process $process) + { + $process->run(); + + if (!$process->isSuccessful()) { + throw new GitException($process->getErrorOutput(), $process->getExitCode(), $process->getCommandLine()); + } + + return $process->getOutput(); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/BaseTestCase.php b/library/kzykhys/git/test/PHPGit/BaseTestCase.php new file mode 100644 index 000000000..df69b216e --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/BaseTestCase.php @@ -0,0 +1,33 @@ +<?php + +use Symfony\Component\Filesystem\Filesystem; + +/** + * @author Kazuyuki Hayashi <hayashi@siance.co.jp> + */ +abstract class BaseTestCase extends PHPUnit_Framework_TestCase +{ + + /** + * @var string + */ + protected $directory; + + /** + * {@inheritdoc} + */ + public function setUp() + { + $this->directory = __DIR__.'/../../build/' . strtolower(get_class($this)); + } + + /** + * {@inheritdoc} + */ + public function tearDown() + { + $filesystem = new Filesystem(); + $filesystem->remove($this->directory); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Command/AddCommandTest.php b/library/kzykhys/git/test/PHPGit/Command/AddCommandTest.php new file mode 100644 index 000000000..1fad08417 --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Command/AddCommandTest.php @@ -0,0 +1,42 @@ +<?php + +use PHPGit\Git; +use Symfony\Component\Filesystem\Filesystem; + +require_once __DIR__ . '/../BaseTestCase.php'; + +/** + * @author Kazuyuki Hayashi <hayashi@valnur.net> + */ +class AddCommandTest extends BaseTestCase +{ + + public function testAdd() + { + $filesystem = new Filesystem(); + $filesystem->mkdir($this->directory); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + + $filesystem->dumpFile($this->directory . '/test.txt', 'foo'); + $filesystem->dumpFile($this->directory . '/test.md', '**foo**'); + + $this->assertTrue($git->add('test.txt')); + $this->assertTrue($git->add(array('test.md'), array('force' => true))); + } + + /** + * @expectedException \PHPGit\Exception\GitException + * @expectedExceptionCode 128 + */ + public function testException() + { + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $git->add('foo'); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Command/ArchiveCommandTest.php b/library/kzykhys/git/test/PHPGit/Command/ArchiveCommandTest.php new file mode 100644 index 000000000..982c8f7c7 --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Command/ArchiveCommandTest.php @@ -0,0 +1,32 @@ +<?php + +use PHPGit\Git; +use Symfony\Component\Filesystem\Filesystem; + +require_once __DIR__ . '/../BaseTestCase.php'; + +/** + * @author Kazuyuki Hayashi <hayashi@valnur.net> + */ +class ArchiveCommandTest extends BaseTestCase +{ + + public function testArchive() + { + $filesystem = new Filesystem(); + $filesystem->mkdir($this->directory); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + + $filesystem->dumpFile($this->directory . '/test.txt', 'hello'); + $git->add('test.txt'); + $git->commit('Initial commit'); + + $git->archive($this->directory . '/test.zip', 'master', null, array('format' => 'zip', 'prefix' => 'test/')); + + $this->assertFileExists($this->directory . '/test.zip'); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Command/BranchCommandTest.php b/library/kzykhys/git/test/PHPGit/Command/BranchCommandTest.php new file mode 100644 index 000000000..4deeea367 --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Command/BranchCommandTest.php @@ -0,0 +1,106 @@ +<?php + +use PHPGit\Git; +use Symfony\Component\Filesystem\Filesystem; + +require_once __DIR__ . '/../BaseTestCase.php'; + +class BranchCommandTest extends BaseTestCase +{ + + public function setUp() + { + parent::setUp(); + + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + + $filesystem->dumpFile($this->directory . '/test.txt', ''); + $git->add('test.txt'); + $git->commit('Initial commit'); + } + + public function testBranch() + { + $git = new Git(); + $git->setRepository($this->directory); + + $branches = $git->branch(); + + $this->assertCount(1, $branches); + $this->assertEquals('master', $branches['master']['name']); + $this->assertTrue($branches['master']['current']); + $this->assertEquals('Initial commit', $branches['master']['title']); + } + + public function testAllBranch() + { + $git = new Git(); + $git->clone('file://' . realpath($this->directory), $this->directory.'2'); + $git->setRepository($this->directory.'2'); + + $branches = $git->branch(array('remotes' => true)); + $this->assertArrayHasKey('origin/master', $branches); + + $branches = $git->branch(array('all' => true)); + $this->assertArrayHasKey('master', $branches); + $this->assertArrayHasKey('remotes/origin/master', $branches); + + $filesystem = new Filesystem(); + $filesystem->remove($this->directory.'2'); + } + + public function testBranchCreate() + { + $git = new Git(); + $git->setRepository($this->directory); + + $git->branch->create('1.0'); + $branches = $git->branch(); + $this->assertCount(2, $branches); + + $git->branch->create('1.0-fix', '1.0', array('force' => true)); + $branches = $git->branch(); + $this->assertCount(3, $branches); + $this->assertArrayHasKey('1.0', $branches); + $this->assertArrayHasKey('1.0-fix', $branches); + } + + public function testBranchMove() + { + $git = new Git(); + $git->setRepository($this->directory); + $git->branch->create('1.0'); + $git->branch->move('1.0', '1.0.x'); + $branches = $git->branch(); + $this->assertCount(2, $branches); + $this->assertArrayHasKey('1.0.x', $branches); + + $git->branch->move('1.0.x', '2.x', array('force' => true)); + $branches = $git->branch(); + $this->assertCount(2, $branches); + $this->assertArrayHasKey('2.x', $branches); + } + + public function testBranchDelete() + { + $git = new Git(); + $git->setRepository($this->directory); + $git->branch->create('1.0'); + $git->branch->create('2.0'); + $branches = $git->branch(); + $this->assertCount(3, $branches); + + $git->branch->delete('1.0'); + $branches = $git->branch(); + $this->assertCount(2, $branches); + + $git->branch->delete('2.0', array('force' => true)); + $branches = $git->branch(); + $this->assertCount(1, $branches); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Command/CatCommandTest.php b/library/kzykhys/git/test/PHPGit/Command/CatCommandTest.php new file mode 100644 index 000000000..945924ccb --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Command/CatCommandTest.php @@ -0,0 +1,65 @@ +<?php + +use PHPGit\Git; +use Symfony\Component\Filesystem\Filesystem; + +require_once __DIR__ . '/../BaseTestCase.php'; + +class CatCommandTest extends BaseTestCase +{ + + public function testCatBlob() + { + $filesystem = new Filesystem(); + $filesystem->mkdir($this->directory); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + + $filesystem->dumpFile($this->directory . '/test.txt', 'foo'); + $git->add('test.txt'); + $git->commit('Initial commit'); + + $tree = $git->tree(); + + $this->assertEquals('foo', $git->cat->blob($tree[0]['hash'])); + } + + public function testCatType() + { + $filesystem = new Filesystem(); + $filesystem->mkdir($this->directory); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + + $filesystem->dumpFile($this->directory . '/test.txt', 'foo'); + $git->add('test.txt'); + $git->commit('Initial commit'); + + $tree = $git->tree(); + + $this->assertEquals('blob', $git->cat->type($tree[0]['hash'])); + } + + public function testCatSize() + { + $filesystem = new Filesystem(); + $filesystem->mkdir($this->directory); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + + $filesystem->dumpFile($this->directory . '/test.txt', 'foo'); + $git->add('test.txt'); + $git->commit('Initial commit'); + + $tree = $git->tree(); + + $this->assertEquals(3, $git->cat->size($tree[0]['hash'])); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Command/CheckoutCommandTest.php b/library/kzykhys/git/test/PHPGit/Command/CheckoutCommandTest.php new file mode 100644 index 000000000..c306ea407 --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Command/CheckoutCommandTest.php @@ -0,0 +1,65 @@ +<?php + +use PHPGit\Git; +use Symfony\Component\Filesystem\Filesystem; + +require_once __DIR__ . '/../BaseTestCase.php'; + +class CheckoutCommandTest extends BaseTestCase +{ + + public function setUp() + { + parent::setUp(); + + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + + $filesystem->dumpFile($this->directory . '/test.txt', ''); + $git->add('test.txt'); + $git->commit('Initial commit'); + } + + public function testCheckout() + { + $git = new Git(); + $git->setRepository($this->directory); + $git->branch->create('next'); + $git->checkout('next'); + + $branches = $git->branch(); + $this->assertArrayHasKey('next', $branches); + $this->assertTrue($branches['next']['current']); + } + + public function testCheckoutCreate() + { + $git = new Git(); + $git->setRepository($this->directory); + $git->checkout->create('next'); + + $branches = $git->branch(); + $this->assertArrayHasKey('next', $branches); + $this->assertTrue($branches['next']['current']); + + $git->checkout->create('develop', 'next'); + + $branches = $git->branch(); + $this->assertArrayHasKey('develop', $branches); + $this->assertTrue($branches['develop']['current']); + } + + public function testCheckoutOrphan() + { + $git = new Git(); + $git->setRepository($this->directory); + $git->checkout->orphan('gh-pages', 'master', array('force' => true)); + + $status = $git->status(); + $this->assertEquals('gh-pages', $status['branch']); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Command/CloneCommandTest.php b/library/kzykhys/git/test/PHPGit/Command/CloneCommandTest.php new file mode 100644 index 000000000..d6a4d26ff --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Command/CloneCommandTest.php @@ -0,0 +1,28 @@ +<?php + +use PHPGit\Git; +use Symfony\Component\Filesystem\Filesystem; + +require_once __DIR__ . '/../BaseTestCase.php'; + +class CloneCommandTest extends BaseTestCase +{ + + public function testClone() + { + $git = new Git(); + $git->clone('https://github.com/kzykhys/Text.git', $this->directory); + $git->setRepository($this->directory); + + $this->assertFileExists($this->directory . '/.git'); + + $filesystem = new Filesystem(); + $filesystem->remove($this->directory); + + $git->setRepository('.'); + $git->clone('https://github.com/kzykhys/Text.git', $this->directory, array('shared' => true)); + + $this->assertFileExists($this->directory . '/.git'); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Command/CommitCommandTest.php b/library/kzykhys/git/test/PHPGit/Command/CommitCommandTest.php new file mode 100644 index 000000000..01b50ad8d --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Command/CommitCommandTest.php @@ -0,0 +1,26 @@ +<?php + +use PHPGit\Git; +use Symfony\Component\Filesystem\Filesystem; + +require_once __DIR__ . '/../BaseTestCase.php'; + +class CommitCommandTest extends BaseTestCase +{ + + public function testCommit() + { + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + + $filesystem = new Filesystem(); + $filesystem->dumpFile($this->directory . '/test.txt', ''); + $git->add('test.txt'); + $git->commit('Initial commit'); + $logs = $git->log('test.txt'); + + $this->assertCount(1, $logs); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Command/ConfigCommandTest.php b/library/kzykhys/git/test/PHPGit/Command/ConfigCommandTest.php new file mode 100644 index 000000000..fba2fbf76 --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Command/ConfigCommandTest.php @@ -0,0 +1,55 @@ +<?php + +use PHPGit\Git; + +require_once __DIR__ . '/../BaseTestCase.php'; + +class ConfigCommandTest extends BaseTestCase +{ + + public function testConfigSetAndList() + { + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + + $before = $git->config(); + + $git->config->set('user.name', 'John Doe'); + + $config = $git->config(); + $this->assertArrayHasKey('user.name', $config); + + $expected = 'John Doe'; + + if (isset($before['user.name'])) { + $expected = $before['user.name'] . "\n" . $expected; + } + + $this->assertEquals($expected, $config['user.name']); + } + + public function testConfigAdd() + { + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + + $before = $git->config(); + + $git->config->set('user.name', 'John Doe'); + $git->config->add('user.name', 'Foo'); + + $config = $git->config(); + $this->assertArrayHasKey('user.name', $config); + + $expected = "John Doe\nFoo"; + + if (isset($before['user.name'])) { + $expected = $before['user.name'] . "\n" . $expected; + } + + $this->assertEquals($expected, $config['user.name']); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Command/DescribeCommandTest.php b/library/kzykhys/git/test/PHPGit/Command/DescribeCommandTest.php new file mode 100644 index 000000000..04d3bd3b0 --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Command/DescribeCommandTest.php @@ -0,0 +1,36 @@ +<?php + +use PHPGit\Git; +use Symfony\Component\Filesystem\Filesystem; + +require_once __DIR__ . '/../BaseTestCase.php'; + +class DescribeCommandTest extends BaseTestCase +{ + + public function testDescribeTags() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + + $filesystem->dumpFile($this->directory . '/README.md', 'hello'); + $git->add('README.md'); + $git->commit('Initial commit'); + $git->tag->create('v1.0.0'); + $version = $git->describe->tags('HEAD'); + + $this->assertEquals('v1.0.0', $version); + + $filesystem->dumpFile($this->directory . '/README.md', 'hello2'); + $git->add('README.md'); + $git->commit('Fixes README'); + $version = $git->describe->tags('HEAD'); + + $this->assertStringStartsWith('v1.0.0', $version); + $this->assertStringEndsNotWith('v1.0.0', $version); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Command/FetchCommandTest.php b/library/kzykhys/git/test/PHPGit/Command/FetchCommandTest.php new file mode 100644 index 000000000..f52943099 --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Command/FetchCommandTest.php @@ -0,0 +1,36 @@ +<?php + +use PHPGit\Git; + +require_once __DIR__ . '/../BaseTestCase.php'; + +class FetchCommandTest extends BaseTestCase +{ + + public function testFetch() + { + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + + $git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); + $git->fetch('origin', '+refs/heads/*:refs/remotes/origin/*'); + + $tags = $git->tag(); + $this->assertContains('v1.0.0', $tags); + } + + public function testFetchAll() + { + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + + $git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); + $git->fetch->all(); + + $tags = $git->tag(); + $this->assertContains('v1.0.0', $tags); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Command/MergeCommandTest.php b/library/kzykhys/git/test/PHPGit/Command/MergeCommandTest.php new file mode 100644 index 000000000..208461523 --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Command/MergeCommandTest.php @@ -0,0 +1,101 @@ +<?php + +use PHPGit\Git; +use Symfony\Component\Filesystem\Filesystem; + +require_once __DIR__ . '/../BaseTestCase.php'; + +class MergeCommandTest extends BaseTestCase +{ + + public function testMerge() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + + $filesystem->dumpFile($this->directory . '/test.txt', 'foo'); + $git->add('test.txt'); + $git->commit('master'); + + $git->checkout->create('develop'); + $filesystem->dumpFile($this->directory . '/test.txt', 'bar'); + $git->add('test.txt'); + $git->commit('develop'); + + $git->checkout('master'); + + $this->assertEquals('foo', file_get_contents($this->directory . '/test.txt')); + + $git->merge('develop'); + + $this->assertEquals('bar', file_get_contents($this->directory . '/test.txt')); + } + + /** + * @expectedException \PHPGit\Exception\GitException + */ + public function testMergeFail() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + + // branch:master + $filesystem->dumpFile($this->directory . '/test.txt', 'foo'); + $git->add('test.txt'); + $git->commit('master'); + + // branch:develop + $git->checkout->create('develop'); + $filesystem->dumpFile($this->directory . '/test.txt', 'bar'); + $git->add('test.txt'); + $git->commit('develop'); + + // branch:master + $git->checkout('master'); + $filesystem->dumpFile($this->directory . '/test.txt', 'baz'); + $git->merge('develop'); + } + + public function testMergeAbort() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + + // branch:master + $filesystem->dumpFile($this->directory . '/test.txt', 'foo'); + $git->add('test.txt'); + $git->commit('master'); + + // branch:develop + $git->checkout->create('develop'); + $filesystem->dumpFile($this->directory . '/test.txt', 'bar'); + $git->add('test.txt'); + $git->commit('develop'); + + // branch:master + $git->checkout('master'); + $filesystem->dumpFile($this->directory . '/test.txt', 'baz'); + $git->add('test.txt'); + $git->commit('master'); + + try { + $git->merge('develop'); + $this->fail('$git->merge("develop") should fail'); + } catch (Exception $e) { + } + + $git->merge->abort(); + + $this->assertEquals('baz', file_get_contents($this->directory . '/test.txt')); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Command/MvCommandTest.php b/library/kzykhys/git/test/PHPGit/Command/MvCommandTest.php new file mode 100644 index 000000000..dd5f46c55 --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Command/MvCommandTest.php @@ -0,0 +1,26 @@ +<?php + +use PHPGit\Git; +use Symfony\Component\Filesystem\Filesystem; + +require_once __DIR__ . '/../BaseTestCase.php'; + +class MvCommandTest extends BaseTestCase +{ + + public function testMv() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $filesystem->dumpFile($this->directory . '/test.txt', 'foo'); + $git->add('test.txt'); + $git->commit('Initial commit'); + $git->mv('test.txt', 'test2.txt'); + + $this->assertFileExists($this->directory . '/test2.txt'); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Command/PullCommandTest.php b/library/kzykhys/git/test/PHPGit/Command/PullCommandTest.php new file mode 100644 index 000000000..89dec75a9 --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Command/PullCommandTest.php @@ -0,0 +1,22 @@ +<?php + +use PHPGit\Git; + +require_once __DIR__ . '/../BaseTestCase.php'; + +class PullCommandTest extends BaseTestCase +{ + + public function testPull() + { + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + + $git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); + $git->pull('origin', 'master'); + + $this->assertFileExists($this->directory . '/README.md'); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Command/PushCommandTest.php b/library/kzykhys/git/test/PHPGit/Command/PushCommandTest.php new file mode 100644 index 000000000..11424cebc --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Command/PushCommandTest.php @@ -0,0 +1,34 @@ +<?php + +use PHPGit\Git; +use Symfony\Component\Filesystem\Filesystem; + +require_once __DIR__ . '/../BaseTestCase.php'; + +class PushCommandTest extends BaseTestCase +{ + + public function testPush() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory, array('shared' => true, 'bare' => true)); + + $git->clone('file://' . realpath($this->directory), $this->directory.'2'); + $git->setRepository($this->directory.'2'); + + $filesystem->dumpFile($this->directory.'2/test.txt', 'foobar'); + $git->add('test.txt'); + $git->commit('test'); + $git->push('origin', 'master'); + + $git->clone('file://' . realpath($this->directory), $this->directory.'3'); + + $this->assertFileExists($this->directory.'3/test.txt'); + + $filesystem->remove($this->directory.'2'); + $filesystem->remove($this->directory.'3'); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Command/RebaseCommandTest.php b/library/kzykhys/git/test/PHPGit/Command/RebaseCommandTest.php new file mode 100644 index 000000000..af7e87a19 --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Command/RebaseCommandTest.php @@ -0,0 +1,154 @@ +<?php + +use PHPGit\Git; +use Symfony\Component\Filesystem\Filesystem; + +require_once __DIR__ . '/../BaseTestCase.php'; + +class RebaseCommandTest extends BaseTestCase +{ + + public function testRebase() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + + $filesystem->dumpFile($this->directory . '/test.txt', '123'); + $git->add('test.txt'); + $git->commit('initial commit'); + + $git->checkout->create('next'); + $filesystem->dumpFile($this->directory . '/test2.txt', '123'); + $git->add('test2.txt'); + $git->commit('test'); + + $git->checkout('master'); + $git->rebase('next', 'master'); + + $this->assertFileExists($this->directory. '/test2.txt'); + } + + public function testRebaseOnto() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $filesystem->dumpFile($this->directory . '/test.txt', '123'); + $git->add('test.txt'); + $git->commit('initial commit'); + + $git->checkout->create('next'); + $filesystem->dumpFile($this->directory . '/test2.txt', '123'); + $git->add('test2.txt'); + $git->commit('test'); + + $git->checkout->create('topic', 'next'); + $filesystem->dumpFile($this->directory . '/test3.txt', '123'); + $git->add('test3.txt'); + $git->commit('test'); + + $git->rebase('next', null, array('onto' => 'master')); + $this->assertFileNotExists($this->directory . '/test2.txt'); + } + + public function testRebaseContinue() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + + $filesystem->dumpFile($this->directory . '/test.txt', 'foo'); + $git->add('test.txt'); + $git->commit('initial commit'); + + $git->checkout->create('next'); + $filesystem->dumpFile($this->directory . '/test.txt', 'bar'); + $git->add('test.txt'); + $git->commit('next commit'); + + $git->checkout('master'); + $filesystem->dumpFile($this->directory . '/test.txt', 'baz'); + $git->add('test.txt'); + $git->commit('master commit'); + + try { + $git->rebase('next'); + $this->fail('GitException should be thrown'); + } catch (\PHPGit\Exception\GitException $e) { + } + + $filesystem->dumpFile($this->directory . '/test.txt', 'foobar'); + $git->add('test.txt'); + $git->rebase->continues(); + } + + public function testRebaseAbort() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + + $filesystem->dumpFile($this->directory . '/test.txt', 'foo'); + $git->add('test.txt'); + $git->commit('initial commit'); + + $git->checkout->create('next'); + $filesystem->dumpFile($this->directory . '/test.txt', 'bar'); + $git->add('test.txt'); + $git->commit('next commit'); + + $git->checkout('master'); + $filesystem->dumpFile($this->directory . '/test.txt', 'baz'); + $git->add('test.txt'); + $git->commit('master commit'); + + try { + $git->rebase('next'); + $this->fail('GitException should be thrown'); + } catch (\PHPGit\Exception\GitException $e) { + } + + $git->rebase->abort(); + } + + public function testRebaseSkip() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + + $filesystem->dumpFile($this->directory . '/test.txt', 'foo'); + $git->add('test.txt'); + $git->commit('initial commit'); + + $git->checkout->create('next'); + $filesystem->dumpFile($this->directory . '/test.txt', 'bar'); + $git->add('test.txt'); + $git->commit('next commit'); + + $git->checkout('master'); + $filesystem->dumpFile($this->directory . '/test.txt', 'baz'); + $git->add('test.txt'); + $git->commit('master commit'); + + try { + $git->rebase('next'); + $this->fail('GitException should be thrown'); + } catch (\PHPGit\Exception\GitException $e) { + } + + $git->rebase->skip(); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Command/Remote/SetBranchesCommandTest.php b/library/kzykhys/git/test/PHPGit/Command/Remote/SetBranchesCommandTest.php new file mode 100644 index 000000000..4f428f832 --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Command/Remote/SetBranchesCommandTest.php @@ -0,0 +1,28 @@ +<?php + +use PHPGit\Git; + +require_once __DIR__ . '/../../BaseTestCase.php'; + +class SetBranchesCommandTest extends BaseTestCase +{ + + public function testSetBranches() + { + $git = new Git(); + $git->clone('https://github.com/kzykhys/Text.git', $this->directory); + $git->setRepository($this->directory); + + $git->remote->branches('origin', array('master')); + } + + public function testSetBranchesAdd() + { + $git = new Git(); + $git->clone('https://github.com/kzykhys/Text.git', $this->directory); + $git->setRepository($this->directory); + + $git->remote->branches->add('origin', array('gh-pages')); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Command/Remote/SetHeadCommandTest.php b/library/kzykhys/git/test/PHPGit/Command/Remote/SetHeadCommandTest.php new file mode 100644 index 000000000..679c2976f --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Command/Remote/SetHeadCommandTest.php @@ -0,0 +1,56 @@ +<?php + +use PHPGit\Git; + +require_once __DIR__ . '/../../BaseTestCase.php'; + +class SetHeadCommandTest extends BaseTestCase +{ + + public function testSetHead() + { + $git = new Git(); + $git->clone('https://github.com/kzykhys/Text.git', $this->directory); + $git->setRepository($this->directory); + + $before = $git->branch(array('all' => true)); + + $git->remote->head('origin', 'master'); + + $after = $git->branch(array('all' => true)); + + $this->assertEquals($before, $after); + } + + public function testSetHeadDelete() + { + $git = new Git(); + $git->clone('https://github.com/kzykhys/Text.git', $this->directory); + $git->setRepository($this->directory); + + $before = $git->branch(array('all' => true)); + + $git->remote->head->delete('origin'); + + $after = $git->branch(array('all' => true)); + + $this->assertNotEquals($before, $after); + } + + public function testSetHeadRemote() + { + $git = new Git(); + $git->clone('https://github.com/kzykhys/Text.git', $this->directory); + $git->setRepository($this->directory); + + $before = $git->branch(array('all' => true)); + + $git->remote->head->delete('origin'); + $git->remote->head->remote('origin'); + + $after = $git->branch(array('all' => true)); + + $this->assertEquals($before, $after); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Command/Remote/SetUrlCommandTest.php b/library/kzykhys/git/test/PHPGit/Command/Remote/SetUrlCommandTest.php new file mode 100644 index 000000000..b70b67d40 --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Command/Remote/SetUrlCommandTest.php @@ -0,0 +1,46 @@ +<?php + +use PHPGit\Git; + +require_once __DIR__ . '/../../BaseTestCase.php'; + +class SetUrlCommandTest extends BaseTestCase +{ + + public function testSetUrl() + { + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $git->remote->add('origin', 'http://example.com/test.git'); + $git->remote->url('origin', 'https://github.com/kzykhys/Text.git', 'http://example.com/test.git'); + + $remotes = $git->remote(); + + $this->assertEquals('https://github.com/kzykhys/Text.git', $remotes['origin']['fetch']); + } + + public function testSetUrlAdd() + { + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $git->remote->add('origin', 'http://example.com/test.git'); + $git->remote->url->add('origin', 'https://github.com/kzykhys/Text.git'); + } + + public function testSetUrlDelete() + { + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $git->remote->add('origin', 'http://example.com/test.git'); + $git->remote->url->add('origin', 'https://github.com/kzykhys/Text.git'); + $git->remote->url->delete('origin', 'https://github.com'); + + $remotes = $git->remote(); + + $this->assertEquals('http://example.com/test.git', $remotes['origin']['fetch']); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Command/RemoteCommandTest.php b/library/kzykhys/git/test/PHPGit/Command/RemoteCommandTest.php new file mode 100644 index 000000000..78aa81309 --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Command/RemoteCommandTest.php @@ -0,0 +1,100 @@ +<?php + +use PHPGit\Git; + +require_once __DIR__ . '/../BaseTestCase.php'; + +class RemoteCommandTest extends BaseTestCase +{ + + public function testRemote() + { + $git = new Git(); + $git->clone('https://github.com/kzykhys/Text.git', $this->directory); + $git->setRepository($this->directory); + + $remotes = $git->remote(); + + $this->assertEquals(array( + 'origin' => array( + 'fetch' => 'https://github.com/kzykhys/Text.git', + 'push' => 'https://github.com/kzykhys/Text.git' + ) + ), $remotes); + } + + public function testRemoteAdd() + { + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); + + $remotes = $git->remote(); + + $this->assertEquals(array( + 'origin' => array( + 'fetch' => 'https://github.com/kzykhys/Text.git', + 'push' => 'https://github.com/kzykhys/Text.git' + ) + ), $remotes); + } + + public function testRemoteRename() + { + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); + $git->remote->rename('origin', 'upstream'); + + $remotes = $git->remote(); + $this->assertEquals(array( + 'upstream' => array( + 'fetch' => 'https://github.com/kzykhys/Text.git', + 'push' => 'https://github.com/kzykhys/Text.git' + ) + ), $remotes); + } + + public function testRemoteRm() + { + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); + $git->remote->rm('origin'); + + $remotes = $git->remote(); + $this->assertEquals(array(), $remotes); + } + + public function testRemoteShow() + { + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); + + $this->assertNotEmpty($git->remote->show('origin')); + } + + public function testRemotePrune() + { + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $git->remote->add('origin', 'https://github.com/kzykhys/Text.git'); + $git->remote->prune('origin'); + } + + /** + * @expectedException \BadMethodCallException + */ + public function testBadMethodCall() + { + $git = new Git(); + $git->remote->foo(); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Command/ResetCommandTest.php b/library/kzykhys/git/test/PHPGit/Command/ResetCommandTest.php new file mode 100644 index 000000000..777e48512 --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Command/ResetCommandTest.php @@ -0,0 +1,128 @@ +<?php + +use PHPGit\Git; +use Symfony\Component\Filesystem\Filesystem; + +require_once __DIR__ . '/../BaseTestCase.php'; + + +class ResetCommandTest extends BaseTestCase +{ + + public function testReset() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $filesystem->dumpFile($this->directory . '/README.md', 'foo'); + $git->add('README.md'); + $git->commit('Initial commit'); + + $filesystem->dumpFile($this->directory . '/README.md', 'hello'); + $git->add('README.md'); + + $git->reset('README.md', 'HEAD'); + } + + public function testResetSoft() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $filesystem->dumpFile($this->directory . '/README.md', 'foo'); + $git->add('README.md'); + $git->commit('Initial commit'); + + $filesystem->dumpFile($this->directory . '/README.md', 'hello'); + + $git->reset->soft(); + } + + public function testResetMixed() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $filesystem->dumpFile($this->directory . '/README.md', 'foo'); + $git->add('README.md'); + $git->commit('Initial commit'); + + $filesystem->dumpFile($this->directory . '/README.md', 'hello'); + + $git->reset->mixed(); + } + + public function testResetHard() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $filesystem->dumpFile($this->directory . '/README.md', 'foo'); + $git->add('README.md'); + $git->commit('Initial commit'); + + $filesystem->dumpFile($this->directory . '/README.md', 'hello'); + + $git->reset->hard('HEAD'); + } + + public function testResetMerge() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $filesystem->dumpFile($this->directory . '/README.md', 'foo'); + $git->add('README.md'); + $git->commit('Initial commit'); + + $filesystem->dumpFile($this->directory . '/README.md', 'hello'); + + $git->reset->merge(); + } + + public function testResetKeep() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $filesystem->dumpFile($this->directory . '/README.md', 'foo'); + $git->add('README.md'); + $git->commit('Initial commit'); + + $filesystem->dumpFile($this->directory . '/README.md', 'hello'); + + $git->reset->keep(); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testResetInvalidMode() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $filesystem->dumpFile($this->directory . '/README.md', 'foo'); + $git->add('README.md'); + $git->commit('Initial commit'); + + $filesystem->dumpFile($this->directory . '/README.md', 'hello'); + + $git->reset->mode('foo'); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Command/RmCommandTest.php b/library/kzykhys/git/test/PHPGit/Command/RmCommandTest.php new file mode 100644 index 000000000..34996cc12 --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Command/RmCommandTest.php @@ -0,0 +1,51 @@ +<?php + +use PHPGit\Git; +use Symfony\Component\Filesystem\Filesystem; + +require_once __DIR__ . '/../BaseTestCase.php'; + +class RmCommandTest extends BaseTestCase +{ + + public function testRm() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + + $filesystem->dumpFile($this->directory . '/README.md', 'foo'); + $filesystem->dumpFile($this->directory . '/bin/test.php', 'foo'); + $git->add(array('README.md', 'bin/test.php')); + $git->commit('Initial commit'); + + $git->rm('README.md'); + $git->rm('bin', array('recursive' => true)); + + $this->assertFileNotExists($this->directory . '/README.md'); + } + + public function testRmCached() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + + $filesystem->dumpFile($this->directory . '/README.md', 'foo'); + $git->add('README.md'); + $git->commit('Initial commit'); + + $git->rm->cached('README.md'); + $git->commit('Delete README.md'); + + $this->assertFileExists($this->directory . '/README.md'); + + $tree = $git->tree(); + $this->assertEquals(array(), $tree); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Command/ShortlogCommandTest.php b/library/kzykhys/git/test/PHPGit/Command/ShortlogCommandTest.php new file mode 100755 index 000000000..48967cab1 --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Command/ShortlogCommandTest.php @@ -0,0 +1,71 @@ +<?php + +use PHPGit\Git; +use Symfony\Component\Filesystem\Filesystem; + +require_once __DIR__ . '/../BaseTestCase.php'; + +class ShortlogCommandTest extends BaseTestCase +{ + + public function testShortlog() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $git->config->set('user.name', 'Name One'); + $git->config->set('user.email', 'one@example.com'); + $filesystem->dumpFile($this->directory . '/test.txt', ''); + $git->add('test.txt'); + $git->commit('1'); + $filesystem->dumpFile($this->directory . '/test2.txt', ''); + $git->add('test2.txt'); + $git->commit('2'); + + $git->config->set('user.name', 'Name Two'); + $git->config->set('user.email', 'two@example.com'); + $filesystem->dumpFile($this->directory . '/test3.txt', ''); + $git->add('test3.txt'); + $git->commit('3'); + + $shortlog = $git->shortlog(); + + $this->assertCount(2, $shortlog); + $this->assertCount(2, $shortlog['Name One <one@example.com>']); + $this->assertCount(1, $shortlog['Name Two <two@example.com>']); + $this->assertEquals('1', $shortlog['Name One <one@example.com>'][0]['subject']); + } + + public function testShortlogSummary() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $git->config->set('user.name', 'Name One'); + $git->config->set('user.email', 'one@example.com'); + $filesystem->dumpFile($this->directory . '/test.txt', ''); + $git->add('test.txt'); + $git->commit('1'); + $filesystem->dumpFile($this->directory . '/test2.txt', ''); + $git->add('test2.txt'); + $git->commit('2'); + + $git->config->set('user.name', 'Name Two'); + $git->config->set('user.email', 'two@example.com'); + $filesystem->dumpFile($this->directory . '/test3.txt', ''); + $git->add('test3.txt'); + $git->commit('3'); + + $summary = $git->shortlog->summary(); + + $this->assertEquals(array( + 'Name One <one@example.com>' => 2, + 'Name Two <two@example.com>' => 1 + ), $summary); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Command/ShowCommandTest.php b/library/kzykhys/git/test/PHPGit/Command/ShowCommandTest.php new file mode 100644 index 000000000..25b22fe4f --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Command/ShowCommandTest.php @@ -0,0 +1,26 @@ +<?php + +use PHPGit\Git; +use Symfony\Component\Filesystem\Filesystem; + +require_once __DIR__ . '/../BaseTestCase.php'; + +class ShowCommandTest extends BaseTestCase +{ + + public function testShow() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + + $filesystem->dumpFile($this->directory . '/README.md', 'foobar'); + $git->add('README.md'); + $git->commit('Initial commit'); + + $git->show('master', array('format' => 'oneline')); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Command/StashCommandTest.php b/library/kzykhys/git/test/PHPGit/Command/StashCommandTest.php new file mode 100644 index 000000000..bb87e48ea --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Command/StashCommandTest.php @@ -0,0 +1,204 @@ +<?php + +use PHPGit\Git; +use Symfony\Component\Filesystem\Filesystem; + +require_once __DIR__ . '/../BaseTestCase.php'; + +class StashCommandTest extends BaseTestCase +{ + + public function testStash() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $filesystem->dumpFile($this->directory . '/README.md', 'hello'); + $git->add('.'); + $git->commit('Initial commit'); + + $filesystem->dumpFile($this->directory . '/README.md', 'hi!'); + $git->stash(); + + $this->assertEquals('hello', file_get_contents($this->directory.'/README.md')); + } + + public function testStashSave() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $filesystem->dumpFile($this->directory . '/README.md', 'hello'); + $git->add('.'); + $git->commit('Initial commit'); + + $filesystem->dumpFile($this->directory . '/README.md', 'hi!'); + $git->stash->save('stash test'); + + $this->assertEquals('hello', file_get_contents($this->directory.'/README.md')); + } + + public function testStashList() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $filesystem->dumpFile($this->directory . '/README.md', 'hello'); + $git->add('.'); + $git->commit('Initial commit'); + + $filesystem->dumpFile($this->directory . '/README.md', 'hi!'); + $git->stash(); + + $stashes = $git->stash->lists(); + + $this->assertCount(1, $stashes); + $this->assertEquals('master', $stashes[0]['branch']); + $this->assertStringEndsWith('Initial commit', $stashes[0]['message']); + } + + public function testStashShow() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $filesystem->dumpFile($this->directory . '/README.md', 'hello'); + $git->add('.'); + $git->commit('Initial commit'); + + $filesystem->dumpFile($this->directory . '/README.md', 'hi!'); + $git->stash(); + $git->stash->show('stash@{0}'); + } + + public function testStashDrop() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $filesystem->dumpFile($this->directory . '/README.md', 'hello'); + $git->add('.'); + $git->commit('Initial commit'); + + $filesystem->dumpFile($this->directory . '/README.md', 'hi!'); + $git->stash(); + $git->stash->drop(); + + $filesystem->dumpFile($this->directory . '/README.md', 'hi!'); + $git->stash(); + $git->stash->drop('stash@{0}'); + + $this->assertCount(0, $git->stash->lists()); + } + + public function testStashPop() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $filesystem->dumpFile($this->directory . '/README.md', 'hello'); + $git->add('.'); + $git->commit('Initial commit'); + + $filesystem->dumpFile($this->directory . '/README.md', 'hi!'); + $git->stash->save('stash#1'); + + $filesystem->dumpFile($this->directory . '/README.md', 'bar'); + $git->stash->save('stash#2'); + $git->stash->pop('stash@{1}'); + + $this->assertEquals('hi!', file_get_contents($this->directory.'/README.md')); + $this->assertCount(1, $git->stash->lists()); + } + + public function testStashApply() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $filesystem->dumpFile($this->directory . '/README.md', 'hello'); + $git->add('.'); + $git->commit('Initial commit'); + + $filesystem->dumpFile($this->directory . '/README.md', 'hi!'); + $git->stash->save('stash#1'); + + $filesystem->dumpFile($this->directory . '/README.md', 'bar'); + $git->stash->save('stash#2'); + $git->stash->apply('stash@{1}'); + + $this->assertEquals('hi!', file_get_contents($this->directory.'/README.md')); + $this->assertCount(2, $git->stash->lists()); + } + + public function testStashBranch() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $filesystem->dumpFile($this->directory . '/README.md', 'hello'); + $git->add('.'); + $git->commit('Initial commit'); + + $filesystem->dumpFile($this->directory . '/README.md', 'hi!'); + $git->stash(); + + $git->stash->branch('dev', 'stash@{0}'); + $status = $git->status(); + + $this->assertEquals('dev', $status['branch']); + $this->assertEquals('hi!', file_get_contents($this->directory.'/README.md')); + } + + public function testStashClear() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $filesystem->dumpFile($this->directory . '/README.md', 'hello'); + $git->add('.'); + $git->commit('Initial commit'); + + $filesystem->dumpFile($this->directory . '/README.md', 'hi!'); + $git->stash(); + $git->stash->clear(); + + $this->assertCount(0, $git->stash->lists()); + } + + public function testStashCreate() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $filesystem->dumpFile($this->directory . '/README.md', 'hello'); + $git->add('.'); + $git->commit('Initial commit'); + + $filesystem->dumpFile($this->directory . '/README.md', 'hi!'); + $object = $git->stash->create(); + + $this->assertNotEmpty($object); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Command/StatusCommandTest.php b/library/kzykhys/git/test/PHPGit/Command/StatusCommandTest.php new file mode 100644 index 000000000..ad04a74c0 --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Command/StatusCommandTest.php @@ -0,0 +1,76 @@ +<?php + +use PHPGit\Command\StatusCommand; +use PHPGit\Git; +use Symfony\Component\Filesystem\Filesystem; + +require_once __DIR__ . '/../BaseTestCase.php'; + +class StatusCommandTest extends BaseTestCase +{ + + public function testStatus() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + + $filesystem->dumpFile($this->directory . '/item1.txt', '1'); + $filesystem->dumpFile($this->directory . '/item2.txt', '2'); + $filesystem->dumpFile($this->directory . '/item3.txt', '3'); + + $git->add('item1.txt'); + $git->add('item2.txt'); + + $filesystem->dumpFile($this->directory . '/item1.txt', '1-1'); + + $status = $git->status(); + + $this->assertEquals(array( + 'branch' => 'master', + 'changes' => array( + array('file' => 'item1.txt', 'index' => StatusCommand::ADDED, 'work_tree' => StatusCommand::MODIFIED), + array('file' => 'item2.txt', 'index' => StatusCommand::ADDED, 'work_tree' => StatusCommand::UNMODIFIED), + array('file' => 'item3.txt', 'index' => StatusCommand::UNTRACKED, 'work_tree' => StatusCommand::UNTRACKED), + ) + ), $status); + } + + public function testDetachedHeadStatus() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + + $filesystem->dumpFile($this->directory . '/item1.txt', '1'); + $git->add('item1.txt'); + $git->commit('initial commit'); + $logs = $git->log(); + $hash = $logs[0]['hash']; + + $git->checkout($hash); + $status = $git->status(); + $this->assertEquals(null, $status['branch']); + } + + public function testTrackingBranchStatus() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->clone('https://github.com/kzykhys/Text.git', $this->directory); + $git->setRepository($this->directory); + + $filesystem->dumpFile($this->directory . '/test.txt', '1'); + $git->add('test.txt'); + $git->commit('test'); + + $status = $git->status(); + $this->assertEquals('master', $status['branch']); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Command/TagCommandTest.php b/library/kzykhys/git/test/PHPGit/Command/TagCommandTest.php new file mode 100644 index 000000000..5715ba92e --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Command/TagCommandTest.php @@ -0,0 +1,58 @@ +<?php + +use PHPGit\Git; +use Symfony\Component\Filesystem\Filesystem; + +require_once __DIR__ . '/../BaseTestCase.php'; + +class TagCommandTest extends BaseTestCase +{ + + public function testTagDelete() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $filesystem->dumpFile($this->directory . '/README.md', 'hello'); + $git->add('.'); + $git->commit('Initial commit'); + $git->tag->create('v1.0.0'); + $git->tag->delete('v1.0.0'); + $this->assertCount(0, $git->tag()); + } + + /** + * @expectedException \PHPGit\Exception\GitException + */ + public function testTagVerify() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $filesystem->dumpFile($this->directory . '/README.md', 'hello'); + $git->add('.'); + $git->commit('Initial commit'); + $git->tag->create('v1.0.0'); + $git->tag->verify('v1.0.0'); + } + + public function testCreateTagFromCommit() + { + $filesystem = new Filesystem(); + + $git = new Git(); + $git->init($this->directory); + $git->setRepository($this->directory); + $filesystem->dumpFile($this->directory . '/README.md', 'hello'); + $git->add('.'); + $git->commit('Initial commit'); + $log = $git->log(null, null, array('limit' => 1)); + $git->tag->create('v1.0.0', $log[0]['hash']); + $this->assertCount(1, $git->tag()); + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/Exception/GitExceptionTest.php b/library/kzykhys/git/test/PHPGit/Exception/GitExceptionTest.php new file mode 100644 index 000000000..154d6b906 --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/Exception/GitExceptionTest.php @@ -0,0 +1,23 @@ +<?php + +use PHPGit\Exception\GitException; +use PHPGit\Git; + +class GitExceptionTest extends PHPUnit_Framework_TestCase +{ + + public function testException() + { + $git = new Git(); + $git->setRepository(sys_get_temp_dir()); + try { + $git->status(); + $this->fail('Previous operation should fail'); + } catch (GitException $e) { + $command = $e->getCommandLine(); + $command = str_replace(array('"', "'"), '', $command); + $this->assertStringEndsWith('status --porcelain -s -b --null', $command); + } + } + +}
\ No newline at end of file diff --git a/library/kzykhys/git/test/PHPGit/GitTest.php b/library/kzykhys/git/test/PHPGit/GitTest.php new file mode 100644 index 000000000..c9aceeb13 --- /dev/null +++ b/library/kzykhys/git/test/PHPGit/GitTest.php @@ -0,0 +1,33 @@ +<?php + +use PHPGit\Git; + +class GitTest extends PHPUnit_Framework_TestCase +{ + + public function testGetVersion() + { + $git = new Git(); + $this->assertNotEmpty($git->getVersion()); + } + + /** + * @expectedException \PHPGit\Exception\GitException + */ + public function testInvalidGitBinary() + { + $git = new Git(); + $git->setBin('/foo/bar'); + $git->getVersion(); + } + + /** + * @expectedException \BadMethodCallException + */ + public function testBadMethodCall() + { + $git = new Git(); + $git->foo(); + } + +}
\ No newline at end of file diff --git a/library/symfony/options-resolver/CHANGELOG.md b/library/symfony/options-resolver/CHANGELOG.md new file mode 100644 index 000000000..5f6d15b2c --- /dev/null +++ b/library/symfony/options-resolver/CHANGELOG.md @@ -0,0 +1,46 @@ +CHANGELOG +========= + +2.6.0 +----- + + * deprecated OptionsResolverInterface + * [BC BREAK] removed "array" type hint from OptionsResolverInterface methods + setRequired(), setAllowedValues(), addAllowedValues(), setAllowedTypes() and + addAllowedTypes() + * added OptionsResolver::setDefault() + * added OptionsResolver::hasDefault() + * added OptionsResolver::setNormalizer() + * added OptionsResolver::isRequired() + * added OptionsResolver::getRequiredOptions() + * added OptionsResolver::isMissing() + * added OptionsResolver::getMissingOptions() + * added OptionsResolver::setDefined() + * added OptionsResolver::isDefined() + * added OptionsResolver::getDefinedOptions() + * added OptionsResolver::remove() + * added OptionsResolver::clear() + * deprecated OptionsResolver::replaceDefaults() + * deprecated OptionsResolver::setOptional() in favor of setDefined() + * deprecated OptionsResolver::isKnown() in favor of isDefined() + * [BC BREAK] OptionsResolver::isRequired() returns true now if a required + option has a default value set + * [BC BREAK] merged Options into OptionsResolver and turned Options into an + interface + * deprecated Options::overload() (now in OptionsResolver) + * deprecated Options::set() (now in OptionsResolver) + * deprecated Options::get() (now in OptionsResolver) + * deprecated Options::has() (now in OptionsResolver) + * deprecated Options::replace() (now in OptionsResolver) + * [BC BREAK] Options::get() (now in OptionsResolver) can only be used within + lazy option/normalizer closures now + * [BC BREAK] removed Traversable interface from Options since using within + lazy option/normalizer closures resulted in exceptions + * [BC BREAK] removed Options::all() since using within lazy option/normalizer + closures resulted in exceptions + * [BC BREAK] OptionDefinitionException now extends LogicException instead of + RuntimeException + * [BC BREAK] normalizers are not executed anymore for unset options + * normalizers are executed after validating the options now + * [BC BREAK] an UndefinedOptionsException is now thrown instead of an + InvalidOptionsException when non-existing options are passed diff --git a/library/symfony/options-resolver/Exception/AccessException.php b/library/symfony/options-resolver/Exception/AccessException.php new file mode 100644 index 000000000..c12b68064 --- /dev/null +++ b/library/symfony/options-resolver/Exception/AccessException.php @@ -0,0 +1,22 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when trying to read an option outside of or write it inside of + * {@link \Symfony\Component\OptionsResolver\Options::resolve()}. + * + * @author Bernhard Schussek <bschussek@gmail.com> + */ +class AccessException extends \LogicException implements ExceptionInterface +{ +} diff --git a/library/symfony/options-resolver/Exception/ExceptionInterface.php b/library/symfony/options-resolver/Exception/ExceptionInterface.php new file mode 100644 index 000000000..b62bb51d4 --- /dev/null +++ b/library/symfony/options-resolver/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Marker interface for all exceptions thrown by the OptionsResolver component. + * + * @author Bernhard Schussek <bschussek@gmail.com> + */ +interface ExceptionInterface +{ +} diff --git a/library/symfony/options-resolver/Exception/InvalidArgumentException.php b/library/symfony/options-resolver/Exception/InvalidArgumentException.php new file mode 100644 index 000000000..6d421d68b --- /dev/null +++ b/library/symfony/options-resolver/Exception/InvalidArgumentException.php @@ -0,0 +1,21 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when an argument is invalid. + * + * @author Bernhard Schussek <bschussek@gmail.com> + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/library/symfony/options-resolver/Exception/InvalidOptionsException.php b/library/symfony/options-resolver/Exception/InvalidOptionsException.php new file mode 100644 index 000000000..6fd4f125f --- /dev/null +++ b/library/symfony/options-resolver/Exception/InvalidOptionsException.php @@ -0,0 +1,23 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when the value of an option does not match its validation rules. + * + * You should make sure a valid value is passed to the option. + * + * @author Bernhard Schussek <bschussek@gmail.com> + */ +class InvalidOptionsException extends InvalidArgumentException +{ +} diff --git a/library/symfony/options-resolver/Exception/MissingOptionsException.php b/library/symfony/options-resolver/Exception/MissingOptionsException.php new file mode 100644 index 000000000..faa487f16 --- /dev/null +++ b/library/symfony/options-resolver/Exception/MissingOptionsException.php @@ -0,0 +1,23 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Exception thrown when a required option is missing. + * + * Add the option to the passed options array. + * + * @author Bernhard Schussek <bschussek@gmail.com> + */ +class MissingOptionsException extends InvalidArgumentException +{ +} diff --git a/library/symfony/options-resolver/Exception/NoSuchOptionException.php b/library/symfony/options-resolver/Exception/NoSuchOptionException.php new file mode 100644 index 000000000..4c3280f4c --- /dev/null +++ b/library/symfony/options-resolver/Exception/NoSuchOptionException.php @@ -0,0 +1,26 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when trying to read an option that has no value set. + * + * When accessing optional options from within a lazy option or normalizer you should first + * check whether the optional option is set. You can do this with `isset($options['optional'])`. + * In contrast to the {@link UndefinedOptionsException}, this is a runtime exception that can + * occur when evaluating lazy options. + * + * @author Tobias Schultze <http://tobion.de> + */ +class NoSuchOptionException extends \OutOfBoundsException implements ExceptionInterface +{ +} diff --git a/library/symfony/options-resolver/Exception/OptionDefinitionException.php b/library/symfony/options-resolver/Exception/OptionDefinitionException.php new file mode 100644 index 000000000..e8e339d44 --- /dev/null +++ b/library/symfony/options-resolver/Exception/OptionDefinitionException.php @@ -0,0 +1,21 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when two lazy options have a cyclic dependency. + * + * @author Bernhard Schussek <bschussek@gmail.com> + */ +class OptionDefinitionException extends \LogicException implements ExceptionInterface +{ +} diff --git a/library/symfony/options-resolver/Exception/UndefinedOptionsException.php b/library/symfony/options-resolver/Exception/UndefinedOptionsException.php new file mode 100644 index 000000000..6ca3fce47 --- /dev/null +++ b/library/symfony/options-resolver/Exception/UndefinedOptionsException.php @@ -0,0 +1,24 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Exception thrown when an undefined option is passed. + * + * You should remove the options in question from your code or define them + * beforehand. + * + * @author Bernhard Schussek <bschussek@gmail.com> + */ +class UndefinedOptionsException extends InvalidArgumentException +{ +} diff --git a/library/symfony/options-resolver/LICENSE b/library/symfony/options-resolver/LICENSE new file mode 100644 index 000000000..43028bc60 --- /dev/null +++ b/library/symfony/options-resolver/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2015 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/library/symfony/options-resolver/Options.php b/library/symfony/options-resolver/Options.php new file mode 100644 index 000000000..d444ec423 --- /dev/null +++ b/library/symfony/options-resolver/Options.php @@ -0,0 +1,22 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver; + +/** + * Contains resolved option values. + * + * @author Bernhard Schussek <bschussek@gmail.com> + * @author Tobias Schultze <http://tobion.de> + */ +interface Options extends \ArrayAccess, \Countable +{ +} diff --git a/library/symfony/options-resolver/OptionsResolver.php b/library/symfony/options-resolver/OptionsResolver.php new file mode 100644 index 000000000..e0578af71 --- /dev/null +++ b/library/symfony/options-resolver/OptionsResolver.php @@ -0,0 +1,1218 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver; + +use Symfony\Component\OptionsResolver\Exception\AccessException; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; +use Symfony\Component\OptionsResolver\Exception\NoSuchOptionException; +use Symfony\Component\OptionsResolver\Exception\OptionDefinitionException; +use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; + +/** + * Validates options and merges them with default values. + * + * @author Bernhard Schussek <bschussek@gmail.com> + * @author Tobias Schultze <http://tobion.de> + */ +class OptionsResolver implements Options, OptionsResolverInterface +{ + /** + * The fully qualified name of the {@link Options} interface. + * + * @internal + */ + const OPTIONS_INTERFACE = 'Symfony\\Component\\OptionsResolver\\Options'; + + /** + * The names of all defined options. + * + * @var array + */ + private $defined = array(); + + /** + * The default option values. + * + * @var array + */ + private $defaults = array(); + + /** + * The names of required options. + * + * @var array + */ + private $required = array(); + + /** + * The resolved option values. + * + * @var array + */ + private $resolved = array(); + + /** + * A list of normalizer closures. + * + * @var \Closure[] + */ + private $normalizers = array(); + + /** + * A list of accepted values for each option. + * + * @var array + */ + private $allowedValues = array(); + + /** + * A list of accepted types for each option. + * + * @var array + */ + private $allowedTypes = array(); + + /** + * A list of closures for evaluating lazy options. + * + * @var array + */ + private $lazy = array(); + + /** + * A list of lazy options whose closure is currently being called. + * + * This list helps detecting circular dependencies between lazy options. + * + * @var array + */ + private $calling = array(); + + /** + * Whether the instance is locked for reading. + * + * Once locked, the options cannot be changed anymore. This is + * necessary in order to avoid inconsistencies during the resolving + * process. If any option is changed after being read, all evaluated + * lazy options that depend on this option would become invalid. + * + * @var bool + */ + private $locked = false; + + private static $typeAliases = array( + 'boolean' => 'bool', + 'integer' => 'int', + 'double' => 'float', + ); + + /** + * Sets the default value of a given option. + * + * If the default value should be set based on other options, you can pass + * a closure with the following signature: + * + * function (Options $options) { + * // ... + * } + * + * The closure will be evaluated when {@link resolve()} is called. The + * closure has access to the resolved values of other options through the + * passed {@link Options} instance: + * + * function (Options $options) { + * if (isset($options['port'])) { + * // ... + * } + * } + * + * If you want to access the previously set default value, add a second + * argument to the closure's signature: + * + * $options->setDefault('name', 'Default Name'); + * + * $options->setDefault('name', function (Options $options, $previousValue) { + * // 'Default Name' === $previousValue + * }); + * + * This is mostly useful if the configuration of the {@link Options} object + * is spread across different locations of your code, such as base and + * sub-classes. + * + * @param string $option The name of the option + * @param mixed $value The default value of the option + * + * @return OptionsResolver This instance + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function setDefault($option, $value) + { + // Setting is not possible once resolving starts, because then lazy + // options could manipulate the state of the object, leading to + // inconsistent results. + if ($this->locked) { + throw new AccessException('Default values cannot be set from a lazy option or normalizer.'); + } + + // If an option is a closure that should be evaluated lazily, store it + // in the "lazy" property. + if ($value instanceof \Closure) { + $reflClosure = new \ReflectionFunction($value); + $params = $reflClosure->getParameters(); + + if (isset($params[0]) && null !== ($class = $params[0]->getClass()) && self::OPTIONS_INTERFACE === $class->name) { + // Initialize the option if no previous value exists + if (!isset($this->defaults[$option])) { + $this->defaults[$option] = null; + } + + // Ignore previous lazy options if the closure has no second parameter + if (!isset($this->lazy[$option]) || !isset($params[1])) { + $this->lazy[$option] = array(); + } + + // Store closure for later evaluation + $this->lazy[$option][] = $value; + $this->defined[$option] = true; + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + } + + // This option is not lazy anymore + unset($this->lazy[$option]); + + // Yet undefined options can be marked as resolved, because we only need + // to resolve options with lazy closures, normalizers or validation + // rules, none of which can exist for undefined options + // If the option was resolved before, update the resolved value + if (!isset($this->defined[$option]) || array_key_exists($option, $this->resolved)) { + $this->resolved[$option] = $value; + } + + $this->defaults[$option] = $value; + $this->defined[$option] = true; + + return $this; + } + + /** + * Sets a list of default values. + * + * @param array $defaults The default values to set + * + * @return OptionsResolver This instance + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function setDefaults(array $defaults) + { + foreach ($defaults as $option => $value) { + $this->setDefault($option, $value); + } + + return $this; + } + + /** + * Returns whether a default value is set for an option. + * + * Returns true if {@link setDefault()} was called for this option. + * An option is also considered set if it was set to null. + * + * @param string $option The option name + * + * @return bool Whether a default value is set + */ + public function hasDefault($option) + { + return array_key_exists($option, $this->defaults); + } + + /** + * Marks one or more options as required. + * + * @param string|string[] $optionNames One or more option names + * + * @return OptionsResolver This instance + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function setRequired($optionNames) + { + if ($this->locked) { + throw new AccessException('Options cannot be made required from a lazy option or normalizer.'); + } + + foreach ((array) $optionNames as $option) { + $this->defined[$option] = true; + $this->required[$option] = true; + } + + return $this; + } + + /** + * Returns whether an option is required. + * + * An option is required if it was passed to {@link setRequired()}. + * + * @param string $option The name of the option + * + * @return bool Whether the option is required + */ + public function isRequired($option) + { + return isset($this->required[$option]); + } + + /** + * Returns the names of all required options. + * + * @return string[] The names of the required options + * + * @see isRequired() + */ + public function getRequiredOptions() + { + return array_keys($this->required); + } + + /** + * Returns whether an option is missing a default value. + * + * An option is missing if it was passed to {@link setRequired()}, but not + * to {@link setDefault()}. This option must be passed explicitly to + * {@link resolve()}, otherwise an exception will be thrown. + * + * @param string $option The name of the option + * + * @return bool Whether the option is missing + */ + public function isMissing($option) + { + return isset($this->required[$option]) && !array_key_exists($option, $this->defaults); + } + + /** + * Returns the names of all options missing a default value. + * + * @return string[] The names of the missing options + * + * @see isMissing() + */ + public function getMissingOptions() + { + return array_keys(array_diff_key($this->required, $this->defaults)); + } + + /** + * Defines a valid option name. + * + * Defines an option name without setting a default value. The option will + * be accepted when passed to {@link resolve()}. When not passed, the + * option will not be included in the resolved options. + * + * @param string|string[] $optionNames One or more option names + * + * @return OptionsResolver This instance + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function setDefined($optionNames) + { + if ($this->locked) { + throw new AccessException('Options cannot be defined from a lazy option or normalizer.'); + } + + foreach ((array) $optionNames as $option) { + $this->defined[$option] = true; + } + + return $this; + } + + /** + * Returns whether an option is defined. + * + * Returns true for any option passed to {@link setDefault()}, + * {@link setRequired()} or {@link setDefined()}. + * + * @param string $option The option name + * + * @return bool Whether the option is defined + */ + public function isDefined($option) + { + return isset($this->defined[$option]); + } + + /** + * Returns the names of all defined options. + * + * @return string[] The names of the defined options + * + * @see isDefined() + */ + public function getDefinedOptions() + { + return array_keys($this->defined); + } + + /** + * Sets the normalizer for an option. + * + * The normalizer should be a closure with the following signature: + * + * ```php + * function (Options $options, $value) { + * // ... + * } + * ``` + * + * The closure is invoked when {@link resolve()} is called. The closure + * has access to the resolved values of other options through the passed + * {@link Options} instance. + * + * The second parameter passed to the closure is the value of + * the option. + * + * The resolved option value is set to the return value of the closure. + * + * @param string $option The option name + * @param \Closure $normalizer The normalizer + * + * @return OptionsResolver This instance + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function setNormalizer($option, \Closure $normalizer) + { + if ($this->locked) { + throw new AccessException('Normalizers cannot be set from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf( + 'The option "%s" does not exist. Defined options are: "%s".', + $option, + implode('", "', array_keys($this->defined)) + )); + } + + $this->normalizers[$option] = $normalizer; + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Sets the normalizers for an array of options. + * + * @param array $normalizers An array of closures + * + * @return OptionsResolver This instance + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + * + * @see setNormalizer() + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function setNormalizers(array $normalizers) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use setNormalizer() instead.', E_USER_DEPRECATED); + + foreach ($normalizers as $option => $normalizer) { + $this->setNormalizer($option, $normalizer); + } + + return $this; + } + + /** + * Sets allowed values for an option. + * + * Instead of passing values, you may also pass a closures with the + * following signature: + * + * function ($value) { + * // return true or false + * } + * + * The closure receives the value as argument and should return true to + * accept the value and false to reject the value. + * + * @param string $option The option name + * @param mixed $allowedValues One or more acceptable values/closures + * + * @return OptionsResolver This instance + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function setAllowedValues($option, $allowedValues = null) + { + if ($this->locked) { + throw new AccessException('Allowed values cannot be set from a lazy option or normalizer.'); + } + + // BC + if (is_array($option) && null === $allowedValues) { + @trigger_error('Calling the '.__METHOD__.' method with an array of options is deprecated since version 2.6 and will be removed in 3.0. Use the new signature with a single option instead.', E_USER_DEPRECATED); + + foreach ($option as $optionName => $optionValues) { + $this->setAllowedValues($optionName, $optionValues); + } + + return $this; + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf( + 'The option "%s" does not exist. Defined options are: "%s".', + $option, + implode('", "', array_keys($this->defined)) + )); + } + + $this->allowedValues[$option] = is_array($allowedValues) ? $allowedValues : array($allowedValues); + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Adds allowed values for an option. + * + * The values are merged with the allowed values defined previously. + * + * Instead of passing values, you may also pass a closures with the + * following signature: + * + * function ($value) { + * // return true or false + * } + * + * The closure receives the value as argument and should return true to + * accept the value and false to reject the value. + * + * @param string $option The option name + * @param mixed $allowedValues One or more acceptable values/closures + * + * @return OptionsResolver This instance + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function addAllowedValues($option, $allowedValues = null) + { + if ($this->locked) { + throw new AccessException('Allowed values cannot be added from a lazy option or normalizer.'); + } + + // BC + if (is_array($option) && null === $allowedValues) { + @trigger_error('Calling the '.__METHOD__.' method with an array of options is deprecated since version 2.6 and will be removed in 3.0. Use the new signature with a single option instead.', E_USER_DEPRECATED); + + foreach ($option as $optionName => $optionValues) { + $this->addAllowedValues($optionName, $optionValues); + } + + return $this; + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf( + 'The option "%s" does not exist. Defined options are: "%s".', + $option, + implode('", "', array_keys($this->defined)) + )); + } + + if (!is_array($allowedValues)) { + $allowedValues = array($allowedValues); + } + + if (!isset($this->allowedValues[$option])) { + $this->allowedValues[$option] = $allowedValues; + } else { + $this->allowedValues[$option] = array_merge($this->allowedValues[$option], $allowedValues); + } + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Sets allowed types for an option. + * + * Any type for which a corresponding is_<type>() function exists is + * acceptable. Additionally, fully-qualified class or interface names may + * be passed. + * + * @param string $option The option name + * @param string|string[] $allowedTypes One or more accepted types + * + * @return OptionsResolver This instance + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function setAllowedTypes($option, $allowedTypes = null) + { + if ($this->locked) { + throw new AccessException('Allowed types cannot be set from a lazy option or normalizer.'); + } + + // BC + if (is_array($option) && null === $allowedTypes) { + @trigger_error('Calling the '.__METHOD__.' method with an array of options is deprecated since version 2.6 and will be removed in 3.0. Use the new signature with a single option instead.', E_USER_DEPRECATED); + + foreach ($option as $optionName => $optionTypes) { + $this->setAllowedTypes($optionName, $optionTypes); + } + + return $this; + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf( + 'The option "%s" does not exist. Defined options are: "%s".', + $option, + implode('", "', array_keys($this->defined)) + )); + } + + $this->allowedTypes[$option] = (array) $allowedTypes; + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Adds allowed types for an option. + * + * The types are merged with the allowed types defined previously. + * + * Any type for which a corresponding is_<type>() function exists is + * acceptable. Additionally, fully-qualified class or interface names may + * be passed. + * + * @param string $option The option name + * @param string|string[] $allowedTypes One or more accepted types + * + * @return OptionsResolver This instance + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function addAllowedTypes($option, $allowedTypes = null) + { + if ($this->locked) { + throw new AccessException('Allowed types cannot be added from a lazy option or normalizer.'); + } + + // BC + if (is_array($option) && null === $allowedTypes) { + @trigger_error('Calling the '.__METHOD__.' method with an array of options is deprecated since version 2.6 and will be removed in 3.0. Use the new signature with a single option instead.', E_USER_DEPRECATED); + + foreach ($option as $optionName => $optionTypes) { + $this->addAllowedTypes($optionName, $optionTypes); + } + + return $this; + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf( + 'The option "%s" does not exist. Defined options are: "%s".', + $option, + implode('", "', array_keys($this->defined)) + )); + } + + if (!isset($this->allowedTypes[$option])) { + $this->allowedTypes[$option] = (array) $allowedTypes; + } else { + $this->allowedTypes[$option] = array_merge($this->allowedTypes[$option], (array) $allowedTypes); + } + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Removes the option with the given name. + * + * Undefined options are ignored. + * + * @param string|string[] $optionNames One or more option names + * + * @return OptionsResolver This instance + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function remove($optionNames) + { + if ($this->locked) { + throw new AccessException('Options cannot be removed from a lazy option or normalizer.'); + } + + foreach ((array) $optionNames as $option) { + unset($this->defined[$option], $this->defaults[$option], $this->required[$option], $this->resolved[$option]); + unset($this->lazy[$option], $this->normalizers[$option], $this->allowedTypes[$option], $this->allowedValues[$option]); + } + + return $this; + } + + /** + * Removes all options. + * + * @return OptionsResolver This instance + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function clear() + { + if ($this->locked) { + throw new AccessException('Options cannot be cleared from a lazy option or normalizer.'); + } + + $this->defined = array(); + $this->defaults = array(); + $this->required = array(); + $this->resolved = array(); + $this->lazy = array(); + $this->normalizers = array(); + $this->allowedTypes = array(); + $this->allowedValues = array(); + + return $this; + } + + /** + * Merges options with the default values stored in the container and + * validates them. + * + * Exceptions are thrown if: + * + * - Undefined options are passed; + * - Required options are missing; + * - Options have invalid types; + * - Options have invalid values. + * + * @param array $options A map of option names to values + * + * @return array The merged and validated options + * + * @throws UndefinedOptionsException If an option name is undefined + * @throws InvalidOptionsException If an option doesn't fulfill the + * specified validation rules + * @throws MissingOptionsException If a required option is missing + * @throws OptionDefinitionException If there is a cyclic dependency between + * lazy options and/or normalizers + * @throws NoSuchOptionException If a lazy option reads an unavailable option + * @throws AccessException If called from a lazy option or normalizer + */ + public function resolve(array $options = array()) + { + if ($this->locked) { + throw new AccessException('Options cannot be resolved from a lazy option or normalizer.'); + } + + // Allow this method to be called multiple times + $clone = clone $this; + + // Make sure that no unknown options are passed + $diff = array_diff_key($options, $clone->defined); + + if (count($diff) > 0) { + ksort($clone->defined); + ksort($diff); + + throw new UndefinedOptionsException(sprintf( + (count($diff) > 1 ? 'The options "%s" do not exist.' : 'The option "%s" does not exist.').' Defined options are: "%s".', + implode('", "', array_keys($diff)), + implode('", "', array_keys($clone->defined)) + )); + } + + // Override options set by the user + foreach ($options as $option => $value) { + $clone->defaults[$option] = $value; + unset($clone->resolved[$option], $clone->lazy[$option]); + } + + // Check whether any required option is missing + $diff = array_diff_key($clone->required, $clone->defaults); + + if (count($diff) > 0) { + ksort($diff); + + throw new MissingOptionsException(sprintf( + count($diff) > 1 ? 'The required options "%s" are missing.' : 'The required option "%s" is missing.', + implode('", "', array_keys($diff)) + )); + } + + // Lock the container + $clone->locked = true; + + // Now process the individual options. Use offsetGet(), which resolves + // the option itself and any options that the option depends on + foreach ($clone->defaults as $option => $_) { + $clone->offsetGet($option); + } + + return $clone->resolved; + } + + /** + * Returns the resolved value of an option. + * + * @param string $option The option name + * + * @return mixed The option value + * + * @throws AccessException If accessing this method outside of + * {@link resolve()} + * @throws NoSuchOptionException If the option is not set + * @throws InvalidOptionsException If the option doesn't fulfill the + * specified validation rules + * @throws OptionDefinitionException If there is a cyclic dependency between + * lazy options and/or normalizers + */ + public function offsetGet($option) + { + if (!$this->locked) { + throw new AccessException('Array access is only supported within closures of lazy options and normalizers.'); + } + + // Shortcut for resolved options + if (array_key_exists($option, $this->resolved)) { + return $this->resolved[$option]; + } + + // Check whether the option is set at all + if (!array_key_exists($option, $this->defaults)) { + if (!isset($this->defined[$option])) { + throw new NoSuchOptionException(sprintf( + 'The option "%s" does not exist. Defined options are: "%s".', + $option, + implode('", "', array_keys($this->defined)) + )); + } + + throw new NoSuchOptionException(sprintf( + 'The optional option "%s" has no value set. You should make sure it is set with "isset" before reading it.', + $option + )); + } + + $value = $this->defaults[$option]; + + // Resolve the option if the default value is lazily evaluated + if (isset($this->lazy[$option])) { + // If the closure is already being called, we have a cyclic + // dependency + if (isset($this->calling[$option])) { + throw new OptionDefinitionException(sprintf( + 'The options "%s" have a cyclic dependency.', + implode('", "', array_keys($this->calling)) + )); + } + + // The following section must be protected from cyclic + // calls. Set $calling for the current $option to detect a cyclic + // dependency + // BEGIN + $this->calling[$option] = true; + try { + foreach ($this->lazy[$option] as $closure) { + $value = $closure($this, $value); + } + } catch (\Exception $e) { + unset($this->calling[$option]); + throw $e; + } + unset($this->calling[$option]); + // END + } + + // Validate the type of the resolved option + if (isset($this->allowedTypes[$option])) { + $valid = false; + + foreach ($this->allowedTypes[$option] as $type) { + $type = isset(self::$typeAliases[$type]) ? self::$typeAliases[$type] : $type; + + if (function_exists($isFunction = 'is_'.$type)) { + if ($isFunction($value)) { + $valid = true; + break; + } + + continue; + } + + if ($value instanceof $type) { + $valid = true; + break; + } + } + + if (!$valid) { + throw new InvalidOptionsException(sprintf( + 'The option "%s" with value %s is expected to be of type '. + '"%s", but is of type "%s".', + $option, + $this->formatValue($value), + implode('" or "', $this->allowedTypes[$option]), + $this->formatTypeOf($value) + )); + } + } + + // Validate the value of the resolved option + if (isset($this->allowedValues[$option])) { + $success = false; + $printableAllowedValues = array(); + + foreach ($this->allowedValues[$option] as $allowedValue) { + if ($allowedValue instanceof \Closure) { + if ($allowedValue($value)) { + $success = true; + break; + } + + // Don't include closures in the exception message + continue; + } elseif ($value === $allowedValue) { + $success = true; + break; + } + + $printableAllowedValues[] = $allowedValue; + } + + if (!$success) { + $message = sprintf( + 'The option "%s" with value %s is invalid.', + $option, + $this->formatValue($value) + ); + + if (count($printableAllowedValues) > 0) { + $message .= sprintf( + ' Accepted values are: %s.', + $this->formatValues($printableAllowedValues) + ); + } + + throw new InvalidOptionsException($message); + } + } + + // Normalize the validated option + if (isset($this->normalizers[$option])) { + // If the closure is already being called, we have a cyclic + // dependency + if (isset($this->calling[$option])) { + throw new OptionDefinitionException(sprintf( + 'The options "%s" have a cyclic dependency.', + implode('", "', array_keys($this->calling)) + )); + } + + $normalizer = $this->normalizers[$option]; + + // The following section must be protected from cyclic + // calls. Set $calling for the current $option to detect a cyclic + // dependency + // BEGIN + $this->calling[$option] = true; + try { + $value = $normalizer($this, $value); + } catch (\Exception $e) { + unset($this->calling[$option]); + throw $e; + } + unset($this->calling[$option]); + // END + } + + // Mark as resolved + $this->resolved[$option] = $value; + + return $value; + } + + /** + * Returns whether a resolved option with the given name exists. + * + * @param string $option The option name + * + * @return bool Whether the option is set + * + * @throws AccessException If accessing this method outside of {@link resolve()} + * + * @see \ArrayAccess::offsetExists() + */ + public function offsetExists($option) + { + if (!$this->locked) { + throw new AccessException('Array access is only supported within closures of lazy options and normalizers.'); + } + + return array_key_exists($option, $this->defaults); + } + + /** + * Not supported. + * + * @throws AccessException + */ + public function offsetSet($option, $value) + { + throw new AccessException('Setting options via array access is not supported. Use setDefault() instead.'); + } + + /** + * Not supported. + * + * @throws AccessException + */ + public function offsetUnset($option) + { + throw new AccessException('Removing options via array access is not supported. Use remove() instead.'); + } + + /** + * Returns the number of set options. + * + * This may be only a subset of the defined options. + * + * @return int Number of options + * + * @throws AccessException If accessing this method outside of {@link resolve()} + * + * @see \Countable::count() + */ + public function count() + { + if (!$this->locked) { + throw new AccessException('Counting is only supported within closures of lazy options and normalizers.'); + } + + return count($this->defaults); + } + + /** + * Alias of {@link setDefault()}. + * + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function set($option, $value) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the setDefaults() method instead.', E_USER_DEPRECATED); + + return $this->setDefault($option, $value); + } + + /** + * Shortcut for {@link clear()} and {@link setDefaults()}. + * + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function replace(array $defaults) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the clear() and setDefaults() methods instead.', E_USER_DEPRECATED); + + $this->clear(); + + return $this->setDefaults($defaults); + } + + /** + * Alias of {@link setDefault()}. + * + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function overload($option, $value) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the setDefault() method instead.', E_USER_DEPRECATED); + + return $this->setDefault($option, $value); + } + + /** + * Alias of {@link offsetGet()}. + * + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function get($option) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the ArrayAccess syntax instead to get an option value.', E_USER_DEPRECATED); + + return $this->offsetGet($option); + } + + /** + * Alias of {@link offsetExists()}. + * + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function has($option) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the ArrayAccess syntax instead to get an option value.', E_USER_DEPRECATED); + + return $this->offsetExists($option); + } + + /** + * Shortcut for {@link clear()} and {@link setDefaults()}. + * + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function replaceDefaults(array $defaultValues) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the clear() and setDefaults() methods instead.', E_USER_DEPRECATED); + + $this->clear(); + + return $this->setDefaults($defaultValues); + } + + /** + * Alias of {@link setDefined()}. + * + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function setOptional(array $optionNames) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the setDefined() method instead.', E_USER_DEPRECATED); + + return $this->setDefined($optionNames); + } + + /** + * Alias of {@link isDefined()}. + * + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function isKnown($option) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the isDefined() method instead.', E_USER_DEPRECATED); + + return $this->isDefined($option); + } + + /** + * Returns a string representation of the type of the value. + * + * This method should be used if you pass the type of a value as + * message parameter to a constraint violation. Note that such + * parameters should usually not be included in messages aimed at + * non-technical people. + * + * @param mixed $value The value to return the type of + * + * @return string The type of the value + */ + private function formatTypeOf($value) + { + return is_object($value) ? get_class($value) : gettype($value); + } + + /** + * Returns a string representation of the value. + * + * This method returns the equivalent PHP tokens for most scalar types + * (i.e. "false" for false, "1" for 1 etc.). Strings are always wrapped + * in double quotes ("). + * + * @param mixed $value The value to format as string + * + * @return string The string representation of the passed value + */ + private function formatValue($value) + { + if (is_object($value)) { + return get_class($value); + } + + if (is_array($value)) { + return 'array'; + } + + if (is_string($value)) { + return '"'.$value.'"'; + } + + if (is_resource($value)) { + return 'resource'; + } + + if (null === $value) { + return 'null'; + } + + if (false === $value) { + return 'false'; + } + + if (true === $value) { + return 'true'; + } + + return (string) $value; + } + + /** + * Returns a string representation of a list of values. + * + * Each of the values is converted to a string using + * {@link formatValue()}. The values are then concatenated with commas. + * + * @param array $values A list of values + * + * @return string The string representation of the value list + * + * @see formatValue() + */ + private function formatValues(array $values) + { + foreach ($values as $key => $value) { + $values[$key] = $this->formatValue($value); + } + + return implode(', ', $values); + } +} diff --git a/library/symfony/options-resolver/OptionsResolverInterface.php b/library/symfony/options-resolver/OptionsResolverInterface.php new file mode 100644 index 000000000..aebc8df22 --- /dev/null +++ b/library/symfony/options-resolver/OptionsResolverInterface.php @@ -0,0 +1,212 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver; + +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; +use Symfony\Component\OptionsResolver\Exception\OptionDefinitionException; + +/** + * @author Bernhard Schussek <bschussek@gmail.com> + * + * @deprecated since version 2.6, to be removed in 3.0. Use {@link OptionsResolver} instead. + */ +interface OptionsResolverInterface +{ + /** + * Sets default option values. + * + * The options can either be values of any types or closures that + * evaluate the option value lazily. These closures must have one + * of the following signatures: + * + * <code> + * function (Options $options) + * function (Options $options, $value) + * </code> + * + * The second parameter passed to the closure is the previously + * set default value, in case you are overwriting an existing + * default value. + * + * The closures should return the lazily created option value. + * + * @param array $defaultValues A list of option names as keys and default + * values or closures as values. + * + * @return OptionsResolverInterface The resolver instance. + */ + public function setDefaults(array $defaultValues); + + /** + * Replaces default option values. + * + * Old defaults are erased, which means that closures passed here cannot + * access the previous default value. This may be useful to improve + * performance if the previous default value is calculated by an expensive + * closure. + * + * @param array $defaultValues A list of option names as keys and default + * values or closures as values. + * + * @return OptionsResolverInterface The resolver instance. + */ + public function replaceDefaults(array $defaultValues); + + /** + * Sets optional options. + * + * This method declares valid option names without setting default values for them. + * If these options are not passed to {@link resolve()} and no default has been set + * for them, they will be missing in the final options array. This can be helpful + * if you want to determine whether an option has been set or not because otherwise + * {@link resolve()} would trigger an exception for unknown options. + * + * @param array $optionNames A list of option names. + * + * @return OptionsResolverInterface The resolver instance. + */ + public function setOptional(array $optionNames); + + /** + * Sets required options. + * + * If these options are not passed to {@link resolve()} and no default has been set for + * them, an exception will be thrown. + * + * @param array $optionNames A list of option names. + * + * @return OptionsResolverInterface The resolver instance. + */ + public function setRequired($optionNames); + + /** + * Sets allowed values for a list of options. + * + * @param array $allowedValues A list of option names as keys and arrays + * with values acceptable for that option as + * values. + * + * @return OptionsResolverInterface The resolver instance. + * + * @throws InvalidOptionsException If an option has not been defined + * (see {@link isKnown()}) for which + * an allowed value is set. + */ + public function setAllowedValues($allowedValues); + + /** + * Adds allowed values for a list of options. + * + * The values are merged with the allowed values defined previously. + * + * @param array $allowedValues A list of option names as keys and arrays + * with values acceptable for that option as + * values. + * + * @return OptionsResolverInterface The resolver instance. + * + * @throws InvalidOptionsException If an option has not been defined + * (see {@link isKnown()}) for which + * an allowed value is set. + */ + public function addAllowedValues($allowedValues); + + /** + * Sets allowed types for a list of options. + * + * @param array $allowedTypes A list of option names as keys and type + * names passed as string or array as values. + * + * @return OptionsResolverInterface The resolver instance. + * + * @throws InvalidOptionsException If an option has not been defined for + * which an allowed type is set. + */ + public function setAllowedTypes($allowedTypes); + + /** + * Adds allowed types for a list of options. + * + * The types are merged with the allowed types defined previously. + * + * @param array $allowedTypes A list of option names as keys and type + * names passed as string or array as values. + * + * @return OptionsResolverInterface The resolver instance. + * + * @throws InvalidOptionsException If an option has not been defined for + * which an allowed type is set. + */ + public function addAllowedTypes($allowedTypes); + + /** + * Sets normalizers that are applied on resolved options. + * + * The normalizers should be closures with the following signature: + * + * <code> + * function (Options $options, $value) + * </code> + * + * The second parameter passed to the closure is the value of + * the option. + * + * The closure should return the normalized value. + * + * @param array $normalizers An array of closures. + * + * @return OptionsResolverInterface The resolver instance. + */ + public function setNormalizers(array $normalizers); + + /** + * Returns whether an option is known. + * + * An option is known if it has been passed to either {@link setDefaults()}, + * {@link setRequired()} or {@link setOptional()} before. + * + * @param string $option The name of the option. + * + * @return bool Whether the option is known. + */ + public function isKnown($option); + + /** + * Returns whether an option is required. + * + * An option is required if it has been passed to {@link setRequired()}, + * but not to {@link setDefaults()}. That is, the option has been declared + * as required and no default value has been set. + * + * @param string $option The name of the option. + * + * @return bool Whether the option is required. + */ + public function isRequired($option); + + /** + * Returns the combination of the default and the passed options. + * + * @param array $options The custom option values. + * + * @return array A list of options and their values. + * + * @throws InvalidOptionsException If any of the passed options has not + * been defined or does not contain an + * allowed value. + * @throws MissingOptionsException If a required option is missing. + * @throws OptionDefinitionException If a cyclic dependency is detected + * between two lazy options. + */ + public function resolve(array $options = array()); +} diff --git a/library/symfony/options-resolver/README.md b/library/symfony/options-resolver/README.md new file mode 100644 index 000000000..cd7a7405d --- /dev/null +++ b/library/symfony/options-resolver/README.md @@ -0,0 +1,20 @@ +OptionsResolver Component +========================= + +This component processes and validates option arrays. + +Documentation +------------- + +The documentation for the component can be found [online] [1]. + +Resources +--------- + +You can run the unit tests with the following command: + + $ cd path/to/Symfony/Component/OptionsResolver/ + $ composer install + $ phpunit + +[1]: https://symfony.com/doc/current/components/options_resolver.html diff --git a/library/symfony/options-resolver/Tests/LegacyOptionsResolverTest.php b/library/symfony/options-resolver/Tests/LegacyOptionsResolverTest.php new file mode 100644 index 000000000..ee89f5279 --- /dev/null +++ b/library/symfony/options-resolver/Tests/LegacyOptionsResolverTest.php @@ -0,0 +1,733 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Tests; + +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\OptionsResolver\Options; + +/** + * @group legacy + */ +class LegacyOptionsResolverTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var OptionsResolver + */ + private $resolver; + + protected function setUp() + { + $this->resolver = new OptionsResolver(); + } + + public function testResolve() + { + $this->resolver->setDefaults(array( + 'one' => '1', + 'two' => '2', + )); + + $options = array( + 'two' => '20', + ); + + $this->assertEquals(array( + 'one' => '1', + 'two' => '20', + ), $this->resolver->resolve($options)); + } + + public function testResolveNumericOptions() + { + $this->resolver->setDefaults(array( + '1' => '1', + '2' => '2', + )); + + $options = array( + '2' => '20', + ); + + $this->assertEquals(array( + '1' => '1', + '2' => '20', + ), $this->resolver->resolve($options)); + } + + public function testResolveLazy() + { + $this->resolver->setDefaults(array( + 'one' => '1', + 'two' => function (Options $options) { + return '20'; + }, + )); + + $this->assertEquals(array( + 'one' => '1', + 'two' => '20', + ), $this->resolver->resolve(array())); + } + + public function testTypeAliasesForAllowedTypes() + { + $this->resolver->setDefaults(array( + 'force' => false, + )); + + $this->resolver->setAllowedTypes(array( + 'force' => 'boolean', + )); + + $this->resolver->resolve(array( + 'force' => true, + )); + } + + public function testResolveLazyDependencyOnOptional() + { + $this->resolver->setDefaults(array( + 'one' => '1', + 'two' => function (Options $options) { + return $options['one'].'2'; + }, + )); + + $options = array( + 'one' => '10', + ); + + $this->assertEquals(array( + 'one' => '10', + 'two' => '102', + ), $this->resolver->resolve($options)); + } + + public function testResolveLazyDependencyOnMissingOptionalWithoutDefault() + { + $test = $this; + + $this->resolver->setOptional(array( + 'one', + )); + + $this->resolver->setDefaults(array( + 'two' => function (Options $options) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertFalse(isset($options['one'])); + + return '2'; + }, + )); + + $options = array(); + + $this->assertEquals(array( + 'two' => '2', + ), $this->resolver->resolve($options)); + } + + public function testResolveLazyDependencyOnOptionalWithoutDefault() + { + $test = $this; + + $this->resolver->setOptional(array( + 'one', + )); + + $this->resolver->setDefaults(array( + 'two' => function (Options $options) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertTrue(isset($options['one'])); + + return $options['one'].'2'; + }, + )); + + $options = array( + 'one' => '10', + ); + + $this->assertEquals(array( + 'one' => '10', + 'two' => '102', + ), $this->resolver->resolve($options)); + } + + public function testResolveLazyDependencyOnRequired() + { + $this->resolver->setRequired(array( + 'one', + )); + $this->resolver->setDefaults(array( + 'two' => function (Options $options) { + return $options['one'].'2'; + }, + )); + + $options = array( + 'one' => '10', + ); + + $this->assertEquals(array( + 'one' => '10', + 'two' => '102', + ), $this->resolver->resolve($options)); + } + + public function testResolveLazyReplaceDefaults() + { + $test = $this; + + $this->resolver->setDefaults(array( + 'one' => function (Options $options) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->fail('Previous closure should not be executed'); + }, + )); + + $this->resolver->replaceDefaults(array( + 'one' => function (Options $options, $previousValue) { + return '1'; + }, + )); + + $this->assertEquals(array( + 'one' => '1', + ), $this->resolver->resolve(array())); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + * @expectedExceptionMessage The option "foo" does not exist. Defined options are: "one", "three", "two". + */ + public function testResolveFailsIfNonExistingOption() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setRequired(array( + 'two', + )); + + $this->resolver->setOptional(array( + 'three', + )); + + $this->resolver->resolve(array( + 'foo' => 'bar', + )); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\MissingOptionsException + */ + public function testResolveFailsIfMissingRequiredOption() + { + $this->resolver->setRequired(array( + 'one', + )); + + $this->resolver->setDefaults(array( + 'two' => '2', + )); + + $this->resolver->resolve(array( + 'two' => '20', + )); + } + + public function testResolveSucceedsIfOptionValueAllowed() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setAllowedValues(array( + 'one' => array('1', 'one'), + )); + + $options = array( + 'one' => 'one', + ); + + $this->assertEquals(array( + 'one' => 'one', + ), $this->resolver->resolve($options)); + } + + public function testResolveSucceedsIfOptionValueAllowed2() + { + $this->resolver->setDefaults(array( + 'one' => '1', + 'two' => '2', + )); + + $this->resolver->setAllowedValues(array( + 'one' => '1', + 'two' => '2', + )); + $this->resolver->addAllowedValues(array( + 'one' => 'one', + 'two' => 'two', + )); + + $options = array( + 'one' => '1', + 'two' => 'two', + ); + + $this->assertEquals(array( + 'one' => '1', + 'two' => 'two', + ), $this->resolver->resolve($options)); + } + + public function testResolveSucceedsIfOptionalWithAllowedValuesNotSet() + { + $this->resolver->setRequired(array( + 'one', + )); + + $this->resolver->setOptional(array( + 'two', + )); + + $this->resolver->setAllowedValues(array( + 'one' => array('1', 'one'), + 'two' => array('2', 'two'), + )); + + $options = array( + 'one' => '1', + ); + + $this->assertEquals(array( + 'one' => '1', + ), $this->resolver->resolve($options)); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfOptionValueNotAllowed() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setAllowedValues(array( + 'one' => array('1', 'one'), + )); + + $this->resolver->resolve(array( + 'one' => '2', + )); + } + + public function testResolveSucceedsIfOptionTypeAllowed() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => 'string', + )); + + $options = array( + 'one' => 'one', + ); + + $this->assertEquals(array( + 'one' => 'one', + ), $this->resolver->resolve($options)); + } + + public function testResolveSucceedsIfOptionTypeAllowedPassArray() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => array('string', 'bool'), + )); + + $options = array( + 'one' => true, + ); + + $this->assertEquals(array( + 'one' => true, + ), $this->resolver->resolve($options)); + } + + public function testResolveSucceedsIfOptionTypeAllowedPassObject() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => 'object', + )); + + $object = new \stdClass(); + $options = array( + 'one' => $object, + ); + + $this->assertEquals(array( + 'one' => $object, + ), $this->resolver->resolve($options)); + } + + public function testResolveSucceedsIfOptionTypeAllowedPassClass() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => '\stdClass', + )); + + $object = new \stdClass(); + $options = array( + 'one' => $object, + ); + + $this->assertEquals(array( + 'one' => $object, + ), $this->resolver->resolve($options)); + } + + public function testResolveSucceedsIfOptionTypeAllowedAddTypes() + { + $this->resolver->setDefaults(array( + 'one' => '1', + 'two' => '2', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => 'string', + 'two' => 'bool', + )); + $this->resolver->addAllowedTypes(array( + 'one' => 'float', + 'two' => 'integer', + )); + + $options = array( + 'one' => 1.23, + 'two' => false, + ); + + $this->assertEquals(array( + 'one' => 1.23, + 'two' => false, + ), $this->resolver->resolve($options)); + } + + public function testResolveSucceedsIfOptionalWithTypeAndWithoutValue() + { + $this->resolver->setOptional(array( + 'one', + 'two', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => 'string', + 'two' => 'int', + )); + + $options = array( + 'two' => 1, + ); + + $this->assertEquals(array( + 'two' => 1, + ), $this->resolver->resolve($options)); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfOptionTypeNotAllowed() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => array('string', 'bool'), + )); + + $this->resolver->resolve(array( + 'one' => 1.23, + )); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfOptionTypeNotAllowedMultipleOptions() + { + $this->resolver->setDefaults(array( + 'one' => '1', + 'two' => '2', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => 'string', + 'two' => 'bool', + )); + + $this->resolver->resolve(array( + 'one' => 'foo', + 'two' => 1.23, + )); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfOptionTypeNotAllowedAddTypes() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => 'string', + )); + $this->resolver->addAllowedTypes(array( + 'one' => 'bool', + )); + + $this->resolver->resolve(array( + 'one' => 1.23, + )); + } + + public function testFluidInterface() + { + $this->resolver->setDefaults(array('one' => '1')) + ->replaceDefaults(array('one' => '2')) + ->setAllowedValues(array('one' => array('1', '2'))) + ->addAllowedValues(array('one' => array('3'))) + ->setRequired(array('two')) + ->setOptional(array('three')); + + $options = array( + 'two' => '2', + ); + + $this->assertEquals(array( + 'one' => '2', + 'two' => '2', + ), $this->resolver->resolve($options)); + } + + public function testKnownIfDefaultWasSet() + { + $this->assertFalse($this->resolver->isKnown('foo')); + + $this->resolver->setDefaults(array( + 'foo' => 'bar', + )); + + $this->assertTrue($this->resolver->isKnown('foo')); + } + + public function testKnownIfRequired() + { + $this->assertFalse($this->resolver->isKnown('foo')); + + $this->resolver->setRequired(array( + 'foo', + )); + + $this->assertTrue($this->resolver->isKnown('foo')); + } + + public function testKnownIfOptional() + { + $this->assertFalse($this->resolver->isKnown('foo')); + + $this->resolver->setOptional(array( + 'foo', + )); + + $this->assertTrue($this->resolver->isKnown('foo')); + } + + public function testRequiredIfRequired() + { + $this->assertFalse($this->resolver->isRequired('foo')); + + $this->resolver->setRequired(array( + 'foo', + )); + + $this->assertTrue($this->resolver->isRequired('foo')); + } + + public function testNormalizersTransformFinalOptions() + { + $this->resolver->setDefaults(array( + 'foo' => 'bar', + 'bam' => 'baz', + )); + $this->resolver->setNormalizers(array( + 'foo' => function (Options $options, $value) { + return $options['bam'].'['.$value.']'; + }, + )); + + $expected = array( + 'foo' => 'baz[bar]', + 'bam' => 'baz', + ); + + $this->assertEquals($expected, $this->resolver->resolve(array())); + + $expected = array( + 'foo' => 'boo[custom]', + 'bam' => 'boo', + ); + + $this->assertEquals($expected, $this->resolver->resolve(array( + 'foo' => 'custom', + 'bam' => 'boo', + ))); + } + + public function testResolveWithoutOptionSucceedsIfRequiredAndDefaultValue() + { + $this->resolver->setRequired(array( + 'foo', + )); + $this->resolver->setDefaults(array( + 'foo' => 'bar', + )); + + $this->assertEquals(array( + 'foo' => 'bar', + ), $this->resolver->resolve(array())); + } + + public function testResolveWithoutOptionSucceedsIfDefaultValueAndRequired() + { + $this->resolver->setDefaults(array( + 'foo' => 'bar', + )); + $this->resolver->setRequired(array( + 'foo', + )); + + $this->assertEquals(array( + 'foo' => 'bar', + ), $this->resolver->resolve(array())); + } + + public function testResolveSucceedsIfOptionRequiredAndValueAllowed() + { + $this->resolver->setRequired(array( + 'one', 'two', + )); + $this->resolver->setAllowedValues(array( + 'two' => array('2'), + )); + + $options = array( + 'one' => '1', + 'two' => '2', + ); + + $this->assertEquals($options, $this->resolver->resolve($options)); + } + + public function testResolveSucceedsIfValueAllowedCallbackReturnsTrue() + { + $this->resolver->setRequired(array( + 'test', + )); + $this->resolver->setAllowedValues(array( + 'test' => function ($value) { + return true; + }, + )); + + $options = array( + 'test' => true, + ); + + $this->assertEquals($options, $this->resolver->resolve($options)); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfValueAllowedCallbackReturnsFalse() + { + $this->resolver->setRequired(array( + 'test', + )); + $this->resolver->setAllowedValues(array( + 'test' => function ($value) { + return false; + }, + )); + + $options = array( + 'test' => true, + ); + + $this->assertEquals($options, $this->resolver->resolve($options)); + } + + public function testClone() + { + $this->resolver->setDefaults(array('one' => '1')); + + $clone = clone $this->resolver; + + // Changes after cloning don't affect each other + $this->resolver->setDefaults(array('two' => '2')); + $clone->setDefaults(array('three' => '3')); + + $this->assertEquals(array( + 'one' => '1', + 'two' => '2', + ), $this->resolver->resolve()); + + $this->assertEquals(array( + 'one' => '1', + 'three' => '3', + ), $clone->resolve()); + } + + public function testOverloadReturnsThis() + { + $this->assertSame($this->resolver, $this->resolver->overload('foo', 'bar')); + } + + public function testOverloadCallsSet() + { + $this->resolver->overload('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } +} diff --git a/library/symfony/options-resolver/Tests/LegacyOptionsTest.php b/library/symfony/options-resolver/Tests/LegacyOptionsTest.php new file mode 100644 index 000000000..b65a09002 --- /dev/null +++ b/library/symfony/options-resolver/Tests/LegacyOptionsTest.php @@ -0,0 +1,337 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Tests; + +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @group legacy + */ +class LegacyOptionsTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var OptionsResolver + */ + private $options; + + protected function setUp() + { + $this->options = new OptionsResolver(); + } + + public function testSetLazyOption() + { + $test = $this; + + $this->options->set('foo', function (Options $options) use ($test) { + return 'dynamic'; + }); + + $this->assertEquals(array('foo' => 'dynamic'), $this->options->resolve()); + } + + public function testOverloadKeepsPreviousValue() + { + $test = $this; + + // defined by superclass + $this->options->set('foo', 'bar'); + + // defined by subclass + $this->options->overload('foo', function (Options $options, $previousValue) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertEquals('bar', $previousValue); + + return 'dynamic'; + }); + + $this->assertEquals(array('foo' => 'dynamic'), $this->options->resolve()); + } + + public function testPreviousValueIsEvaluatedIfLazy() + { + $test = $this; + + // defined by superclass + $this->options->set('foo', function (Options $options) { + return 'bar'; + }); + + // defined by subclass + $this->options->overload('foo', function (Options $options, $previousValue) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertEquals('bar', $previousValue); + + return 'dynamic'; + }); + + $this->assertEquals(array('foo' => 'dynamic'), $this->options->resolve()); + } + + public function testPreviousValueIsNotEvaluatedIfNoSecondArgument() + { + $test = $this; + + // defined by superclass + $this->options->set('foo', function (Options $options) use ($test) { + $test->fail('Should not be called'); + }); + + // defined by subclass, no $previousValue argument defined! + $this->options->overload('foo', function (Options $options) use ($test) { + return 'dynamic'; + }); + + $this->assertEquals(array('foo' => 'dynamic'), $this->options->resolve()); + } + + public function testLazyOptionCanAccessOtherOptions() + { + $test = $this; + + $this->options->set('foo', 'bar'); + + $this->options->set('bam', function (Options $options) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertEquals('bar', $options->get('foo')); + + return 'dynamic'; + }); + + $this->assertEquals(array('foo' => 'bar', 'bam' => 'dynamic'), $this->options->resolve()); + } + + public function testLazyOptionCanAccessOtherLazyOptions() + { + $test = $this; + + $this->options->set('foo', function (Options $options) { + return 'bar'; + }); + + $this->options->set('bam', function (Options $options) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertEquals('bar', $options->get('foo')); + + return 'dynamic'; + }); + + $this->assertEquals(array('foo' => 'bar', 'bam' => 'dynamic'), $this->options->resolve()); + } + + public function testNormalizer() + { + $this->options->set('foo', 'bar'); + + $this->options->setNormalizer('foo', function () { + return 'normalized'; + }); + + $this->assertEquals(array('foo' => 'normalized'), $this->options->resolve()); + } + + public function testNormalizerReceivesUnnormalizedValue() + { + $this->options->set('foo', 'bar'); + + $this->options->setNormalizer('foo', function (Options $options, $value) { + return 'normalized['.$value.']'; + }); + + $this->assertEquals(array('foo' => 'normalized[bar]'), $this->options->resolve()); + } + + public function testNormalizerCanAccessOtherOptions() + { + $test = $this; + + $this->options->set('foo', 'bar'); + $this->options->set('bam', 'baz'); + + $this->options->setNormalizer('bam', function (Options $options) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertEquals('bar', $options->get('foo')); + + return 'normalized'; + }); + + $this->assertEquals(array('foo' => 'bar', 'bam' => 'normalized'), $this->options->resolve()); + } + + public function testNormalizerCanAccessOtherLazyOptions() + { + $test = $this; + + $this->options->set('foo', function (Options $options) { + return 'bar'; + }); + $this->options->set('bam', 'baz'); + + $this->options->setNormalizer('bam', function (Options $options) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertEquals('bar', $options->get('foo')); + + return 'normalized'; + }); + + $this->assertEquals(array('foo' => 'bar', 'bam' => 'normalized'), $this->options->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException + */ + public function testFailForCyclicDependencies() + { + $this->options->set('foo', function (Options $options) { + $options->get('bam'); + }); + + $this->options->set('bam', function (Options $options) { + $options->get('foo'); + }); + + $this->options->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException + */ + public function testFailForCyclicDependenciesBetweenNormalizers() + { + $this->options->set('foo', 'bar'); + $this->options->set('bam', 'baz'); + + $this->options->setNormalizer('foo', function (Options $options) { + $options->get('bam'); + }); + + $this->options->setNormalizer('bam', function (Options $options) { + $options->get('foo'); + }); + + $this->options->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException + */ + public function testFailForCyclicDependenciesBetweenNormalizerAndLazyOption() + { + $this->options->set('foo', function (Options $options) { + $options->get('bam'); + }); + $this->options->set('bam', 'baz'); + + $this->options->setNormalizer('bam', function (Options $options) { + $options->get('foo'); + }); + + $this->options->resolve(); + } + + public function testReplaceClearsAndSets() + { + $this->options->set('one', '1'); + + $this->options->replace(array( + 'two' => '2', + 'three' => function (Options $options) { + return '2' === $options['two'] ? '3' : 'foo'; + }, + )); + + $this->assertEquals(array( + 'two' => '2', + 'three' => '3', + ), $this->options->resolve()); + } + + public function testClearRemovesAllOptions() + { + $this->options->set('one', 1); + $this->options->set('two', 2); + + $this->options->clear(); + + $this->assertEmpty($this->options->resolve()); + } + + public function testOverloadCannotBeEvaluatedLazilyWithoutExpectedClosureParams() + { + $this->options->set('foo', 'bar'); + + $this->options->overload('foo', function () { + return 'test'; + }); + + $resolved = $this->options->resolve(); + $this->assertTrue(is_callable($resolved['foo'])); + } + + public function testOverloadCannotBeEvaluatedLazilyWithoutFirstParamTypeHint() + { + $this->options->set('foo', 'bar'); + + $this->options->overload('foo', function ($object) { + return 'test'; + }); + + $resolved = $this->options->resolve(); + $this->assertTrue(is_callable($resolved['foo'])); + } + + public function testRemoveOptionAndNormalizer() + { + $this->options->set('foo1', 'bar'); + $this->options->setNormalizer('foo1', function (Options $options) { + return ''; + }); + $this->options->set('foo2', 'bar'); + $this->options->setNormalizer('foo2', function (Options $options) { + return ''; + }); + + $this->options->remove('foo2'); + $this->assertEquals(array('foo1' => ''), $this->options->resolve()); + } + + public function testReplaceOptionAndNormalizer() + { + $this->options->set('foo1', 'bar'); + $this->options->setNormalizer('foo1', function (Options $options) { + return ''; + }); + $this->options->set('foo2', 'bar'); + $this->options->setNormalizer('foo2', function (Options $options) { + return ''; + }); + + $this->options->replace(array('foo1' => 'new')); + $this->assertEquals(array('foo1' => 'new'), $this->options->resolve()); + } + + public function testClearOptionAndNormalizer() + { + $this->options->set('foo1', 'bar'); + $this->options->setNormalizer('foo1', function (Options $options) { + return ''; + }); + $this->options->set('foo2', 'bar'); + $this->options->setNormalizer('foo2', function (Options $options) { + return ''; + }); + + $this->options->clear(); + $this->assertEmpty($this->options->resolve()); + } +} diff --git a/library/symfony/options-resolver/Tests/OptionsResolver2Dot6Test.php b/library/symfony/options-resolver/Tests/OptionsResolver2Dot6Test.php new file mode 100644 index 000000000..9158c5ba0 --- /dev/null +++ b/library/symfony/options-resolver/Tests/OptionsResolver2Dot6Test.php @@ -0,0 +1,1550 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Tests; + +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class OptionsResolver2Dot6Test extends \PHPUnit_Framework_TestCase +{ + /** + * @var OptionsResolver + */ + private $resolver; + + protected function setUp() + { + $this->resolver = new OptionsResolver(); + } + + //////////////////////////////////////////////////////////////////////////// + // resolve() + //////////////////////////////////////////////////////////////////////////// + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + * @expectedExceptionMessage The option "foo" does not exist. Defined options are: "a", "z". + */ + public function testResolveFailsIfNonExistingOption() + { + $this->resolver->setDefault('z', '1'); + $this->resolver->setDefault('a', '2'); + + $this->resolver->resolve(array('foo' => 'bar')); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + * @expectedExceptionMessage The options "baz", "foo", "ping" do not exist. Defined options are: "a", "z". + */ + public function testResolveFailsIfMultipleNonExistingOptions() + { + $this->resolver->setDefault('z', '1'); + $this->resolver->setDefault('a', '2'); + + $this->resolver->resolve(array('ping' => 'pong', 'foo' => 'bar', 'baz' => 'bam')); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testResolveFailsFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->resolve(array()); + }); + + $this->resolver->resolve(); + } + + //////////////////////////////////////////////////////////////////////////// + // setDefault()/hasDefault() + //////////////////////////////////////////////////////////////////////////// + + public function testSetDefaultReturnsThis() + { + $this->assertSame($this->resolver, $this->resolver->setDefault('foo', 'bar')); + } + + public function testSetDefault() + { + $this->resolver->setDefault('one', '1'); + $this->resolver->setDefault('two', '20'); + + $this->assertEquals(array( + 'one' => '1', + 'two' => '20', + ), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetDefaultFromLazyOption() + { + $this->resolver->setDefault('lazy', function (Options $options) { + $options->setDefault('default', 42); + }); + + $this->resolver->resolve(); + } + + public function testHasDefault() + { + $this->assertFalse($this->resolver->hasDefault('foo')); + $this->resolver->setDefault('foo', 42); + $this->assertTrue($this->resolver->hasDefault('foo')); + } + + public function testHasDefaultWithNullValue() + { + $this->assertFalse($this->resolver->hasDefault('foo')); + $this->resolver->setDefault('foo', null); + $this->assertTrue($this->resolver->hasDefault('foo')); + } + + //////////////////////////////////////////////////////////////////////////// + // lazy setDefault() + //////////////////////////////////////////////////////////////////////////// + + public function testSetLazyReturnsThis() + { + $this->assertSame($this->resolver, $this->resolver->setDefault('foo', function (Options $options) {})); + } + + public function testSetLazyClosure() + { + $this->resolver->setDefault('foo', function (Options $options) { + return 'lazy'; + }); + + $this->assertEquals(array('foo' => 'lazy'), $this->resolver->resolve()); + } + + public function testClosureWithoutTypeHintNotInvoked() + { + $closure = function ($options) { + \PHPUnit_Framework_Assert::fail('Should not be called'); + }; + + $this->resolver->setDefault('foo', $closure); + + $this->assertSame(array('foo' => $closure), $this->resolver->resolve()); + } + + public function testClosureWithoutParametersNotInvoked() + { + $closure = function () { + \PHPUnit_Framework_Assert::fail('Should not be called'); + }; + + $this->resolver->setDefault('foo', $closure); + + $this->assertSame(array('foo' => $closure), $this->resolver->resolve()); + } + + public function testAccessPreviousDefaultValue() + { + // defined by superclass + $this->resolver->setDefault('foo', 'bar'); + + // defined by subclass + $this->resolver->setDefault('foo', function (Options $options, $previousValue) { + \PHPUnit_Framework_Assert::assertEquals('bar', $previousValue); + + return 'lazy'; + }); + + $this->assertEquals(array('foo' => 'lazy'), $this->resolver->resolve()); + } + + public function testAccessPreviousLazyDefaultValue() + { + // defined by superclass + $this->resolver->setDefault('foo', function (Options $options) { + return 'bar'; + }); + + // defined by subclass + $this->resolver->setDefault('foo', function (Options $options, $previousValue) { + \PHPUnit_Framework_Assert::assertEquals('bar', $previousValue); + + return 'lazy'; + }); + + $this->assertEquals(array('foo' => 'lazy'), $this->resolver->resolve()); + } + + public function testPreviousValueIsNotEvaluatedIfNoSecondArgument() + { + // defined by superclass + $this->resolver->setDefault('foo', function () { + \PHPUnit_Framework_Assert::fail('Should not be called'); + }); + + // defined by subclass, no $previousValue argument defined! + $this->resolver->setDefault('foo', function (Options $options) { + return 'lazy'; + }); + + $this->assertEquals(array('foo' => 'lazy'), $this->resolver->resolve()); + } + + public function testOverwrittenLazyOptionNotEvaluated() + { + $this->resolver->setDefault('foo', function (Options $options) { + \PHPUnit_Framework_Assert::fail('Should not be called'); + }); + + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testInvokeEachLazyOptionOnlyOnce() + { + $calls = 0; + + $this->resolver->setDefault('lazy1', function (Options $options) use (&$calls) { + \PHPUnit_Framework_Assert::assertSame(1, ++$calls); + + $options['lazy2']; + }); + + $this->resolver->setDefault('lazy2', function (Options $options) use (&$calls) { + \PHPUnit_Framework_Assert::assertSame(2, ++$calls); + }); + + $this->resolver->resolve(); + + $this->assertSame(2, $calls); + } + + //////////////////////////////////////////////////////////////////////////// + // setRequired()/isRequired()/getRequiredOptions() + //////////////////////////////////////////////////////////////////////////// + + public function testSetRequiredReturnsThis() + { + $this->assertSame($this->resolver, $this->resolver->setRequired('foo')); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetRequiredFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->setRequired('bar'); + }); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\MissingOptionsException + */ + public function testResolveFailsIfRequiredOptionMissing() + { + $this->resolver->setRequired('foo'); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfRequiredOptionSet() + { + $this->resolver->setRequired('foo'); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertNotEmpty($this->resolver->resolve()); + } + + public function testResolveSucceedsIfRequiredOptionPassed() + { + $this->resolver->setRequired('foo'); + + $this->assertNotEmpty($this->resolver->resolve(array('foo' => 'bar'))); + } + + public function testIsRequired() + { + $this->assertFalse($this->resolver->isRequired('foo')); + $this->resolver->setRequired('foo'); + $this->assertTrue($this->resolver->isRequired('foo')); + } + + public function testRequiredIfSetBefore() + { + $this->assertFalse($this->resolver->isRequired('foo')); + + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setRequired('foo'); + + $this->assertTrue($this->resolver->isRequired('foo')); + } + + public function testStillRequiredAfterSet() + { + $this->assertFalse($this->resolver->isRequired('foo')); + + $this->resolver->setRequired('foo'); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertTrue($this->resolver->isRequired('foo')); + } + + public function testIsNotRequiredAfterRemove() + { + $this->assertFalse($this->resolver->isRequired('foo')); + $this->resolver->setRequired('foo'); + $this->resolver->remove('foo'); + $this->assertFalse($this->resolver->isRequired('foo')); + } + + public function testIsNotRequiredAfterClear() + { + $this->assertFalse($this->resolver->isRequired('foo')); + $this->resolver->setRequired('foo'); + $this->resolver->clear(); + $this->assertFalse($this->resolver->isRequired('foo')); + } + + public function testGetRequiredOptions() + { + $this->resolver->setRequired(array('foo', 'bar')); + $this->resolver->setDefault('bam', 'baz'); + $this->resolver->setDefault('foo', 'boo'); + + $this->assertSame(array('foo', 'bar'), $this->resolver->getRequiredOptions()); + } + + //////////////////////////////////////////////////////////////////////////// + // isMissing()/getMissingOptions() + //////////////////////////////////////////////////////////////////////////// + + public function testIsMissingIfNotSet() + { + $this->assertFalse($this->resolver->isMissing('foo')); + $this->resolver->setRequired('foo'); + $this->assertTrue($this->resolver->isMissing('foo')); + } + + public function testIsNotMissingIfSet() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->assertFalse($this->resolver->isMissing('foo')); + $this->resolver->setRequired('foo'); + $this->assertFalse($this->resolver->isMissing('foo')); + } + + public function testIsNotMissingAfterRemove() + { + $this->resolver->setRequired('foo'); + $this->resolver->remove('foo'); + $this->assertFalse($this->resolver->isMissing('foo')); + } + + public function testIsNotMissingAfterClear() + { + $this->resolver->setRequired('foo'); + $this->resolver->clear(); + $this->assertFalse($this->resolver->isRequired('foo')); + } + + public function testGetMissingOptions() + { + $this->resolver->setRequired(array('foo', 'bar')); + $this->resolver->setDefault('bam', 'baz'); + $this->resolver->setDefault('foo', 'boo'); + + $this->assertSame(array('bar'), $this->resolver->getMissingOptions()); + } + + //////////////////////////////////////////////////////////////////////////// + // setDefined()/isDefined()/getDefinedOptions() + //////////////////////////////////////////////////////////////////////////// + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetDefinedFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->setDefined('bar'); + }); + + $this->resolver->resolve(); + } + + public function testDefinedOptionsNotIncludedInResolvedOptions() + { + $this->resolver->setDefined('foo'); + + $this->assertSame(array(), $this->resolver->resolve()); + } + + public function testDefinedOptionsIncludedIfDefaultSetBefore() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setDefined('foo'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testDefinedOptionsIncludedIfDefaultSetAfter() + { + $this->resolver->setDefined('foo'); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testDefinedOptionsIncludedIfPassedToResolve() + { + $this->resolver->setDefined('foo'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve(array('foo' => 'bar'))); + } + + public function testIsDefined() + { + $this->assertFalse($this->resolver->isDefined('foo')); + $this->resolver->setDefined('foo'); + $this->assertTrue($this->resolver->isDefined('foo')); + } + + public function testLazyOptionsAreDefined() + { + $this->assertFalse($this->resolver->isDefined('foo')); + $this->resolver->setDefault('foo', function (Options $options) {}); + $this->assertTrue($this->resolver->isDefined('foo')); + } + + public function testRequiredOptionsAreDefined() + { + $this->assertFalse($this->resolver->isDefined('foo')); + $this->resolver->setRequired('foo'); + $this->assertTrue($this->resolver->isDefined('foo')); + } + + public function testSetOptionsAreDefined() + { + $this->assertFalse($this->resolver->isDefined('foo')); + $this->resolver->setDefault('foo', 'bar'); + $this->assertTrue($this->resolver->isDefined('foo')); + } + + public function testGetDefinedOptions() + { + $this->resolver->setDefined(array('foo', 'bar')); + $this->resolver->setDefault('baz', 'bam'); + $this->resolver->setRequired('boo'); + + $this->assertSame(array('foo', 'bar', 'baz', 'boo'), $this->resolver->getDefinedOptions()); + } + + public function testRemovedOptionsAreNotDefined() + { + $this->assertFalse($this->resolver->isDefined('foo')); + $this->resolver->setDefined('foo'); + $this->assertTrue($this->resolver->isDefined('foo')); + $this->resolver->remove('foo'); + $this->assertFalse($this->resolver->isDefined('foo')); + } + + public function testClearedOptionsAreNotDefined() + { + $this->assertFalse($this->resolver->isDefined('foo')); + $this->resolver->setDefined('foo'); + $this->assertTrue($this->resolver->isDefined('foo')); + $this->resolver->clear(); + $this->assertFalse($this->resolver->isDefined('foo')); + } + + //////////////////////////////////////////////////////////////////////////// + // setAllowedTypes() + //////////////////////////////////////////////////////////////////////////// + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + */ + public function testSetAllowedTypesFailsIfUnknownOption() + { + $this->resolver->setAllowedTypes('foo', 'string'); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetAllowedTypesFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->setAllowedTypes('bar', 'string'); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + /** + * @dataProvider provideInvalidTypes + */ + public function testResolveFailsIfInvalidType($actualType, $allowedType, $exceptionMessage) + { + $this->resolver->setDefined('option'); + $this->resolver->setAllowedTypes('option', $allowedType); + $this->setExpectedException('Symfony\Component\OptionsResolver\Exception\InvalidOptionsException', $exceptionMessage); + $this->resolver->resolve(array('option' => $actualType)); + } + + public function provideInvalidTypes() + { + return array( + array(true, 'string', 'The option "option" with value true is expected to be of type "string", but is of type "boolean".'), + array(false, 'string', 'The option "option" with value false is expected to be of type "string", but is of type "boolean".'), + array(fopen(__FILE__, 'r'), 'string', 'The option "option" with value resource is expected to be of type "string", but is of type "resource".'), + array(array(), 'string', 'The option "option" with value array is expected to be of type "string", but is of type "array".'), + array(new OptionsResolver(), 'string', 'The option "option" with value Symfony\Component\OptionsResolver\OptionsResolver is expected to be of type "string", but is of type "Symfony\Component\OptionsResolver\OptionsResolver".'), + array(42, 'string', 'The option "option" with value 42 is expected to be of type "string", but is of type "integer".'), + array(null, 'string', 'The option "option" with value null is expected to be of type "string", but is of type "NULL".'), + array('bar', '\stdClass', 'The option "option" with value "bar" is expected to be of type "\stdClass", but is of type "string".'), + ); + } + + public function testResolveSucceedsIfValidType() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedTypes('foo', 'string'); + + $this->assertNotEmpty($this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value 42 is expected to be of type "string" or "bool", but is of type "integer". + */ + public function testResolveFailsIfInvalidTypeMultiple() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedTypes('foo', array('string', 'bool')); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidTypeMultiple() + { + $this->resolver->setDefault('foo', true); + $this->resolver->setAllowedTypes('foo', array('string', 'bool')); + + $this->assertNotEmpty($this->resolver->resolve()); + } + + public function testResolveSucceedsIfInstanceOfClass() + { + $this->resolver->setDefault('foo', new \stdClass()); + $this->resolver->setAllowedTypes('foo', '\stdClass'); + + $this->assertNotEmpty($this->resolver->resolve()); + } + + //////////////////////////////////////////////////////////////////////////// + // addAllowedTypes() + //////////////////////////////////////////////////////////////////////////// + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + */ + public function testAddAllowedTypesFailsIfUnknownOption() + { + $this->resolver->addAllowedTypes('foo', 'string'); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfAddAllowedTypesFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->addAllowedTypes('bar', 'string'); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfInvalidAddedType() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->addAllowedTypes('foo', 'string'); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidAddedType() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->addAllowedTypes('foo', 'string'); + + $this->assertNotEmpty($this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfInvalidAddedTypeMultiple() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->addAllowedTypes('foo', array('string', 'bool')); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidAddedTypeMultiple() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->addAllowedTypes('foo', array('string', 'bool')); + + $this->assertNotEmpty($this->resolver->resolve()); + } + + public function testAddAllowedTypesDoesNotOverwrite() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedTypes('foo', 'string'); + $this->resolver->addAllowedTypes('foo', 'bool'); + + $this->resolver->setDefault('foo', 'bar'); + + $this->assertNotEmpty($this->resolver->resolve()); + } + + public function testAddAllowedTypesDoesNotOverwrite2() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedTypes('foo', 'string'); + $this->resolver->addAllowedTypes('foo', 'bool'); + + $this->resolver->setDefault('foo', false); + + $this->assertNotEmpty($this->resolver->resolve()); + } + + //////////////////////////////////////////////////////////////////////////// + // setAllowedValues() + //////////////////////////////////////////////////////////////////////////// + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + */ + public function testSetAllowedValuesFailsIfUnknownOption() + { + $this->resolver->setAllowedValues('foo', 'bar'); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetAllowedValuesFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->setAllowedValues('bar', 'baz'); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value 42 is invalid. Accepted values are: "bar". + */ + public function testResolveFailsIfInvalidValue() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedValues('foo', 'bar'); + + $this->resolver->resolve(array('foo' => 42)); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value null is invalid. Accepted values are: "bar". + */ + public function testResolveFailsIfInvalidValueIsNull() + { + $this->resolver->setDefault('foo', null); + $this->resolver->setAllowedValues('foo', 'bar'); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfInvalidValueStrict() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedValues('foo', '42'); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidValue() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', 'bar'); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testResolveSucceedsIfValidValueIsNull() + { + $this->resolver->setDefault('foo', null); + $this->resolver->setAllowedValues('foo', null); + + $this->assertEquals(array('foo' => null), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value 42 is invalid. Accepted values are: "bar", false, null. + */ + public function testResolveFailsIfInvalidValueMultiple() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedValues('foo', array('bar', false, null)); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidValueMultiple() + { + $this->resolver->setDefault('foo', 'baz'); + $this->resolver->setAllowedValues('foo', array('bar', 'baz')); + + $this->assertEquals(array('foo' => 'baz'), $this->resolver->resolve()); + } + + public function testResolveFailsIfClosureReturnsFalse() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedValues('foo', function ($value) use (&$passedValue) { + $passedValue = $value; + + return false; + }); + + try { + $this->resolver->resolve(); + $this->fail('Should fail'); + } catch (InvalidOptionsException $e) { + } + + $this->assertSame(42, $passedValue); + } + + public function testResolveSucceedsIfClosureReturnsTrue() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', function ($value) use (&$passedValue) { + $passedValue = $value; + + return true; + }); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + $this->assertSame('bar', $passedValue); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfAllClosuresReturnFalse() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedValues('foo', array( + function () { return false; }, + function () { return false; }, + function () { return false; }, + )); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfAnyClosureReturnsTrue() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', array( + function () { return false; }, + function () { return true; }, + function () { return false; }, + )); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + //////////////////////////////////////////////////////////////////////////// + // addAllowedValues() + //////////////////////////////////////////////////////////////////////////// + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + */ + public function testAddAllowedValuesFailsIfUnknownOption() + { + $this->resolver->addAllowedValues('foo', 'bar'); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfAddAllowedValuesFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->addAllowedValues('bar', 'baz'); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfInvalidAddedValue() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->addAllowedValues('foo', 'bar'); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidAddedValue() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->addAllowedValues('foo', 'bar'); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testResolveSucceedsIfValidAddedValueIsNull() + { + $this->resolver->setDefault('foo', null); + $this->resolver->addAllowedValues('foo', null); + + $this->assertEquals(array('foo' => null), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfInvalidAddedValueMultiple() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->addAllowedValues('foo', array('bar', 'baz')); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidAddedValueMultiple() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->addAllowedValues('foo', array('bar', 'baz')); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testAddAllowedValuesDoesNotOverwrite() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', 'bar'); + $this->resolver->addAllowedValues('foo', 'baz'); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testAddAllowedValuesDoesNotOverwrite2() + { + $this->resolver->setDefault('foo', 'baz'); + $this->resolver->setAllowedValues('foo', 'bar'); + $this->resolver->addAllowedValues('foo', 'baz'); + + $this->assertEquals(array('foo' => 'baz'), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfAllAddedClosuresReturnFalse() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedValues('foo', function () { return false; }); + $this->resolver->addAllowedValues('foo', function () { return false; }); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfAnyAddedClosureReturnsTrue() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', function () { return false; }); + $this->resolver->addAllowedValues('foo', function () { return true; }); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testResolveSucceedsIfAnyAddedClosureReturnsTrue2() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', function () { return true; }); + $this->resolver->addAllowedValues('foo', function () { return false; }); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + //////////////////////////////////////////////////////////////////////////// + // setNormalizer() + //////////////////////////////////////////////////////////////////////////// + + public function testSetNormalizerReturnsThis() + { + $this->resolver->setDefault('foo', 'bar'); + $this->assertSame($this->resolver, $this->resolver->setNormalizer('foo', function () {})); + } + + public function testSetNormalizerClosure() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setNormalizer('foo', function () { + return 'normalized'; + }); + + $this->assertEquals(array('foo' => 'normalized'), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + */ + public function testSetNormalizerFailsIfUnknownOption() + { + $this->resolver->setNormalizer('foo', function () {}); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetNormalizerFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->setNormalizer('foo', function () {}); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + public function testNormalizerReceivesSetOption() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->resolver->setNormalizer('foo', function (Options $options, $value) { + return 'normalized['.$value.']'; + }); + + $this->assertEquals(array('foo' => 'normalized[bar]'), $this->resolver->resolve()); + } + + public function testNormalizerReceivesPassedOption() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->resolver->setNormalizer('foo', function (Options $options, $value) { + return 'normalized['.$value.']'; + }); + + $resolved = $this->resolver->resolve(array('foo' => 'baz')); + + $this->assertEquals(array('foo' => 'normalized[baz]'), $resolved); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testValidateTypeBeforeNormalization() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->resolver->setAllowedTypes('foo', 'int'); + + $this->resolver->setNormalizer('foo', function () { + \PHPUnit_Framework_Assert::fail('Should not be called.'); + }); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testValidateValueBeforeNormalization() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->resolver->setAllowedValues('foo', 'baz'); + + $this->resolver->setNormalizer('foo', function () { + \PHPUnit_Framework_Assert::fail('Should not be called.'); + }); + + $this->resolver->resolve(); + } + + public function testNormalizerCanAccessOtherOptions() + { + $this->resolver->setDefault('default', 'bar'); + $this->resolver->setDefault('norm', 'baz'); + + $this->resolver->setNormalizer('norm', function (Options $options) { + /* @var \PHPUnit_Framework_TestCase $test */ + \PHPUnit_Framework_Assert::assertSame('bar', $options['default']); + + return 'normalized'; + }); + + $this->assertEquals(array( + 'default' => 'bar', + 'norm' => 'normalized', + ), $this->resolver->resolve()); + } + + public function testNormalizerCanAccessLazyOptions() + { + $this->resolver->setDefault('lazy', function (Options $options) { + return 'bar'; + }); + $this->resolver->setDefault('norm', 'baz'); + + $this->resolver->setNormalizer('norm', function (Options $options) { + /* @var \PHPUnit_Framework_TestCase $test */ + \PHPUnit_Framework_Assert::assertEquals('bar', $options['lazy']); + + return 'normalized'; + }); + + $this->assertEquals(array( + 'lazy' => 'bar', + 'norm' => 'normalized', + ), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException + */ + public function testFailIfCyclicDependencyBetweenNormalizers() + { + $this->resolver->setDefault('norm1', 'bar'); + $this->resolver->setDefault('norm2', 'baz'); + + $this->resolver->setNormalizer('norm1', function (Options $options) { + $options['norm2']; + }); + + $this->resolver->setNormalizer('norm2', function (Options $options) { + $options['norm1']; + }); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException + */ + public function testFailIfCyclicDependencyBetweenNormalizerAndLazyOption() + { + $this->resolver->setDefault('lazy', function (Options $options) { + $options['norm']; + }); + + $this->resolver->setDefault('norm', 'baz'); + + $this->resolver->setNormalizer('norm', function (Options $options) { + $options['lazy']; + }); + + $this->resolver->resolve(); + } + + public function testCatchedExceptionFromNormalizerDoesNotCrashOptionResolver() + { + $throw = true; + + $this->resolver->setDefaults(array('catcher' => null, 'thrower' => null)); + + $this->resolver->setNormalizer('catcher', function (Options $options) { + try { + return $options['thrower']; + } catch(\Exception $e) { + return false; + } + }); + + $this->resolver->setNormalizer('thrower', function (Options $options) use (&$throw) { + if ($throw) { + $throw = false; + throw new \UnexpectedValueException('throwing'); + } + + return true; + }); + + $this->resolver->resolve(); + } + + public function testCatchedExceptionFromLazyDoesNotCrashOptionResolver() + { + $throw = true; + + $this->resolver->setDefault('catcher', function (Options $options) { + try { + return $options['thrower']; + } catch(\Exception $e) { + return false; + } + }); + + $this->resolver->setDefault('thrower', function (Options $options) use (&$throw) { + if ($throw) { + $throw = false; + throw new \UnexpectedValueException('throwing'); + } + + return true; + }); + + $this->resolver->resolve(); + } + + public function testInvokeEachNormalizerOnlyOnce() + { + $calls = 0; + + $this->resolver->setDefault('norm1', 'bar'); + $this->resolver->setDefault('norm2', 'baz'); + + $this->resolver->setNormalizer('norm1', function ($options) use (&$calls) { + \PHPUnit_Framework_Assert::assertSame(1, ++$calls); + + $options['norm2']; + }); + $this->resolver->setNormalizer('norm2', function () use (&$calls) { + \PHPUnit_Framework_Assert::assertSame(2, ++$calls); + }); + + $this->resolver->resolve(); + + $this->assertSame(2, $calls); + } + + public function testNormalizerNotCalledForUnsetOptions() + { + $this->resolver->setDefined('norm'); + + $this->resolver->setNormalizer('norm', function () { + \PHPUnit_Framework_Assert::fail('Should not be called.'); + }); + + $this->assertEmpty($this->resolver->resolve()); + } + + //////////////////////////////////////////////////////////////////////////// + // setDefaults() + //////////////////////////////////////////////////////////////////////////// + + public function testSetDefaultsReturnsThis() + { + $this->assertSame($this->resolver, $this->resolver->setDefaults(array('foo', 'bar'))); + } + + public function testSetDefaults() + { + $this->resolver->setDefault('one', '1'); + $this->resolver->setDefault('two', 'bar'); + + $this->resolver->setDefaults(array( + 'two' => '2', + 'three' => '3', + )); + + $this->assertEquals(array( + 'one' => '1', + 'two' => '2', + 'three' => '3', + ), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetDefaultsFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->setDefaults(array('two' => '2')); + }); + + $this->resolver->resolve(); + } + + //////////////////////////////////////////////////////////////////////////// + // remove() + //////////////////////////////////////////////////////////////////////////// + + public function testRemoveReturnsThis() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame($this->resolver, $this->resolver->remove('foo')); + } + + public function testRemoveSingleOption() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setDefault('baz', 'boo'); + $this->resolver->remove('foo'); + + $this->assertSame(array('baz' => 'boo'), $this->resolver->resolve()); + } + + public function testRemoveMultipleOptions() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setDefault('baz', 'boo'); + $this->resolver->setDefault('doo', 'dam'); + + $this->resolver->remove(array('foo', 'doo')); + + $this->assertSame(array('baz' => 'boo'), $this->resolver->resolve()); + } + + public function testRemoveLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + return 'lazy'; + }); + $this->resolver->remove('foo'); + + $this->assertSame(array(), $this->resolver->resolve()); + } + + public function testRemoveNormalizer() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setNormalizer('foo', function (Options $options, $value) { + return 'normalized'; + }); + $this->resolver->remove('foo'); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testRemoveAllowedTypes() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedTypes('foo', 'int'); + $this->resolver->remove('foo'); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testRemoveAllowedValues() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', array('baz', 'boo')); + $this->resolver->remove('foo'); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfRemoveFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->remove('bar'); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + public function testRemoveUnknownOptionIgnored() + { + $this->assertNotNull($this->resolver->remove('foo')); + } + + //////////////////////////////////////////////////////////////////////////// + // clear() + //////////////////////////////////////////////////////////////////////////// + + public function testClearReturnsThis() + { + $this->assertSame($this->resolver, $this->resolver->clear()); + } + + public function testClearRemovesAllOptions() + { + $this->resolver->setDefault('one', 1); + $this->resolver->setDefault('two', 2); + + $this->resolver->clear(); + + $this->assertEmpty($this->resolver->resolve()); + } + + public function testClearLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + return 'lazy'; + }); + $this->resolver->clear(); + + $this->assertSame(array(), $this->resolver->resolve()); + } + + public function testClearNormalizer() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setNormalizer('foo', function (Options $options, $value) { + return 'normalized'; + }); + $this->resolver->clear(); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testClearAllowedTypes() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedTypes('foo', 'int'); + $this->resolver->clear(); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testClearAllowedValues() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', 'baz'); + $this->resolver->clear(); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfClearFromLazyption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->clear(); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + public function testClearOptionAndNormalizer() + { + $this->resolver->setDefault('foo1', 'bar'); + $this->resolver->setNormalizer('foo1', function (Options $options) { + return ''; + }); + $this->resolver->setDefault('foo2', 'bar'); + $this->resolver->setNormalizer('foo2', function (Options $options) { + return ''; + }); + + $this->resolver->clear(); + $this->assertEmpty($this->resolver->resolve()); + } + + //////////////////////////////////////////////////////////////////////////// + // ArrayAccess + //////////////////////////////////////////////////////////////////////////// + + public function testArrayAccess() + { + $this->resolver->setDefault('default1', 0); + $this->resolver->setDefault('default2', 1); + $this->resolver->setRequired('required'); + $this->resolver->setDefined('defined'); + $this->resolver->setDefault('lazy1', function (Options $options) { + return 'lazy'; + }); + + $this->resolver->setDefault('lazy2', function (Options $options) { + \PHPUnit_Framework_Assert::assertTrue(isset($options['default1'])); + \PHPUnit_Framework_Assert::assertTrue(isset($options['default2'])); + \PHPUnit_Framework_Assert::assertTrue(isset($options['required'])); + \PHPUnit_Framework_Assert::assertTrue(isset($options['lazy1'])); + \PHPUnit_Framework_Assert::assertTrue(isset($options['lazy2'])); + \PHPUnit_Framework_Assert::assertFalse(isset($options['defined'])); + + \PHPUnit_Framework_Assert::assertSame(0, $options['default1']); + \PHPUnit_Framework_Assert::assertSame(42, $options['default2']); + \PHPUnit_Framework_Assert::assertSame('value', $options['required']); + \PHPUnit_Framework_Assert::assertSame('lazy', $options['lazy1']); + + // Obviously $options['lazy'] and $options['defined'] cannot be + // accessed + }); + + $this->resolver->resolve(array('default2' => 42, 'required' => 'value')); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testArrayAccessGetFailsOutsideResolve() + { + $this->resolver->setDefault('default', 0); + + $this->resolver['default']; + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testArrayAccessExistsFailsOutsideResolve() + { + $this->resolver->setDefault('default', 0); + + isset($this->resolver['default']); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testArrayAccessSetNotSupported() + { + $this->resolver['default'] = 0; + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testArrayAccessUnsetNotSupported() + { + $this->resolver->setDefault('default', 0); + + unset($this->resolver['default']); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\NoSuchOptionException + * @expectedExceptionMessage The option "undefined" does not exist. Defined options are: "foo", "lazy". + */ + public function testFailIfGetNonExisting() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->resolver->setDefault('lazy', function (Options $options) { + $options['undefined']; + }); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\NoSuchOptionException + * @expectedExceptionMessage The optional option "defined" has no value set. You should make sure it is set with "isset" before reading it. + */ + public function testFailIfGetDefinedButUnset() + { + $this->resolver->setDefined('defined'); + + $this->resolver->setDefault('lazy', function (Options $options) { + $options['defined']; + }); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException + */ + public function testFailIfCyclicDependency() + { + $this->resolver->setDefault('lazy1', function (Options $options) { + $options['lazy2']; + }); + + $this->resolver->setDefault('lazy2', function (Options $options) { + $options['lazy1']; + }); + + $this->resolver->resolve(); + } + + //////////////////////////////////////////////////////////////////////////// + // Countable + //////////////////////////////////////////////////////////////////////////// + + public function testCount() + { + $this->resolver->setDefault('default', 0); + $this->resolver->setRequired('required'); + $this->resolver->setDefined('defined'); + $this->resolver->setDefault('lazy1', function () {}); + + $this->resolver->setDefault('lazy2', function (Options $options) { + \PHPUnit_Framework_Assert::assertCount(4, $options); + }); + + $this->assertCount(4, $this->resolver->resolve(array('required' => 'value'))); + } + + /** + * In resolve() we count the options that are actually set (which may be + * only a subset of the defined options). Outside of resolve(), it's not + * clear what is counted. + * + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testCountFailsOutsideResolve() + { + $this->resolver->setDefault('foo', 0); + $this->resolver->setRequired('bar'); + $this->resolver->setDefined('bar'); + $this->resolver->setDefault('lazy1', function () {}); + + count($this->resolver); + } +} diff --git a/library/symfony/options-resolver/phpunit.xml.dist b/library/symfony/options-resolver/phpunit.xml.dist new file mode 100644 index 000000000..abf84614b --- /dev/null +++ b/library/symfony/options-resolver/phpunit.xml.dist @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd" + backupGlobals="false" + colors="true" + bootstrap="vendor/autoload.php" +> + <php> + <ini name="error_reporting" value="-1" /> + </php> + + <testsuites> + <testsuite name="Symfony OptionsResolver Component Test Suite"> + <directory>./Tests/</directory> + </testsuite> + </testsuites> + + <filter> + <whitelist> + <directory>./</directory> + <exclude> + <directory>./Resources</directory> + <directory>./Tests</directory> + <directory>./vendor</directory> + </exclude> + </whitelist> + </filter> +</phpunit> diff --git a/library/symfony/process/CHANGELOG.md b/library/symfony/process/CHANGELOG.md new file mode 100644 index 000000000..2f3c1beb7 --- /dev/null +++ b/library/symfony/process/CHANGELOG.md @@ -0,0 +1,40 @@ +CHANGELOG +========= + +2.5.0 +----- + + * added support for PTY mode + * added the convenience method "mustRun" + * deprecation: Process::setStdin() is deprecated in favor of Process::setInput() + * deprecation: Process::getStdin() is deprecated in favor of Process::getInput() + * deprecation: Process::setInput() and ProcessBuilder::setInput() do not accept non-scalar types + +2.4.0 +----- + + * added the ability to define an idle timeout + +2.3.0 +----- + + * added ProcessUtils::escapeArgument() to fix the bug in escapeshellarg() function on Windows + * added Process::signal() + * added Process::getPid() + * added support for a TTY mode + +2.2.0 +----- + + * added ProcessBuilder::setArguments() to reset the arguments on a builder + * added a way to retrieve the standard and error output incrementally + * added Process:restart() + +2.1.0 +----- + + * added support for non-blocking processes (start(), wait(), isRunning(), stop()) + * enhanced Windows compatibility + * added Process::getExitCodeText() that returns a string representation for + the exit code returned by the process + * added ProcessBuilder diff --git a/library/symfony/process/Exception/ExceptionInterface.php b/library/symfony/process/Exception/ExceptionInterface.php new file mode 100644 index 000000000..75c1c9e5d --- /dev/null +++ b/library/symfony/process/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +/** + * Marker Interface for the Process Component. + * + * @author Johannes M. Schmitt <schmittjoh@gmail.com> + */ +interface ExceptionInterface +{ +} diff --git a/library/symfony/process/Exception/InvalidArgumentException.php b/library/symfony/process/Exception/InvalidArgumentException.php new file mode 100644 index 000000000..926ee2118 --- /dev/null +++ b/library/symfony/process/Exception/InvalidArgumentException.php @@ -0,0 +1,21 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +/** + * InvalidArgumentException for the Process Component. + * + * @author Romain Neutron <imprec@gmail.com> + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/library/symfony/process/Exception/LogicException.php b/library/symfony/process/Exception/LogicException.php new file mode 100644 index 000000000..be3d490dd --- /dev/null +++ b/library/symfony/process/Exception/LogicException.php @@ -0,0 +1,21 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +/** + * LogicException for the Process Component. + * + * @author Romain Neutron <imprec@gmail.com> + */ +class LogicException extends \LogicException implements ExceptionInterface +{ +} diff --git a/library/symfony/process/Exception/ProcessFailedException.php b/library/symfony/process/Exception/ProcessFailedException.php new file mode 100644 index 000000000..328acfde5 --- /dev/null +++ b/library/symfony/process/Exception/ProcessFailedException.php @@ -0,0 +1,54 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +use Symfony\Component\Process\Process; + +/** + * Exception for failed processes. + * + * @author Johannes M. Schmitt <schmittjoh@gmail.com> + */ +class ProcessFailedException extends RuntimeException +{ + private $process; + + public function __construct(Process $process) + { + if ($process->isSuccessful()) { + throw new InvalidArgumentException('Expected a failed process, but the given process was successful.'); + } + + $error = sprintf('The command "%s" failed.'."\n\nExit Code: %s(%s)\n\nWorking directory: %s", + $process->getCommandLine(), + $process->getExitCode(), + $process->getExitCodeText(), + $process->getWorkingDirectory() + ); + + if (!$process->isOutputDisabled()) { + $error .= sprintf("\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s", + $process->getOutput(), + $process->getErrorOutput() + ); + } + + parent::__construct($error); + + $this->process = $process; + } + + public function getProcess() + { + return $this->process; + } +} diff --git a/library/symfony/process/Exception/ProcessTimedOutException.php b/library/symfony/process/Exception/ProcessTimedOutException.php new file mode 100644 index 000000000..d45114696 --- /dev/null +++ b/library/symfony/process/Exception/ProcessTimedOutException.php @@ -0,0 +1,69 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +use Symfony\Component\Process\Process; + +/** + * Exception that is thrown when a process times out. + * + * @author Johannes M. Schmitt <schmittjoh@gmail.com> + */ +class ProcessTimedOutException extends RuntimeException +{ + const TYPE_GENERAL = 1; + const TYPE_IDLE = 2; + + private $process; + private $timeoutType; + + public function __construct(Process $process, $timeoutType) + { + $this->process = $process; + $this->timeoutType = $timeoutType; + + parent::__construct(sprintf( + 'The process "%s" exceeded the timeout of %s seconds.', + $process->getCommandLine(), + $this->getExceededTimeout() + )); + } + + public function getProcess() + { + return $this->process; + } + + public function isGeneralTimeout() + { + return $this->timeoutType === self::TYPE_GENERAL; + } + + public function isIdleTimeout() + { + return $this->timeoutType === self::TYPE_IDLE; + } + + public function getExceededTimeout() + { + switch ($this->timeoutType) { + case self::TYPE_GENERAL: + return $this->process->getTimeout(); + + case self::TYPE_IDLE: + return $this->process->getIdleTimeout(); + + default: + throw new \LogicException(sprintf('Unknown timeout type "%d".', $this->timeoutType)); + } + } +} diff --git a/library/symfony/process/Exception/RuntimeException.php b/library/symfony/process/Exception/RuntimeException.php new file mode 100644 index 000000000..adead2536 --- /dev/null +++ b/library/symfony/process/Exception/RuntimeException.php @@ -0,0 +1,21 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +/** + * RuntimeException for the Process Component. + * + * @author Johannes M. Schmitt <schmittjoh@gmail.com> + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/library/symfony/process/ExecutableFinder.php b/library/symfony/process/ExecutableFinder.php new file mode 100644 index 000000000..fa11cb6e4 --- /dev/null +++ b/library/symfony/process/ExecutableFinder.php @@ -0,0 +1,90 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +/** + * Generic executable finder. + * + * @author Fabien Potencier <fabien@symfony.com> + * @author Johannes M. Schmitt <schmittjoh@gmail.com> + */ +class ExecutableFinder +{ + private $suffixes = array('.exe', '.bat', '.cmd', '.com'); + + /** + * Replaces default suffixes of executable. + * + * @param array $suffixes + */ + public function setSuffixes(array $suffixes) + { + $this->suffixes = $suffixes; + } + + /** + * Adds new possible suffix to check for executable. + * + * @param string $suffix + */ + public function addSuffix($suffix) + { + $this->suffixes[] = $suffix; + } + + /** + * Finds an executable by name. + * + * @param string $name The executable name (without the extension) + * @param string $default The default to return if no executable is found + * @param array $extraDirs Additional dirs to check into + * + * @return string The executable path or default value + */ + public function find($name, $default = null, array $extraDirs = array()) + { + if (ini_get('open_basedir')) { + $searchPath = explode(PATH_SEPARATOR, ini_get('open_basedir')); + $dirs = array(); + foreach ($searchPath as $path) { + // Silencing against https://bugs.php.net/69240 + if (@is_dir($path)) { + $dirs[] = $path; + } else { + if (basename($path) == $name && is_executable($path)) { + return $path; + } + } + } + } else { + $dirs = array_merge( + explode(PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')), + $extraDirs + ); + } + + $suffixes = array(''); + if ('\\' === DIRECTORY_SEPARATOR) { + $pathExt = getenv('PATHEXT'); + $suffixes = $pathExt ? explode(PATH_SEPARATOR, $pathExt) : $this->suffixes; + } + foreach ($suffixes as $suffix) { + foreach ($dirs as $dir) { + if (is_file($file = $dir.DIRECTORY_SEPARATOR.$name.$suffix) && ('\\' === DIRECTORY_SEPARATOR || is_executable($file))) { + return $file; + } + } + } + + return $default; + } +} diff --git a/library/symfony/process/LICENSE b/library/symfony/process/LICENSE new file mode 100644 index 000000000..43028bc60 --- /dev/null +++ b/library/symfony/process/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2015 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/library/symfony/process/PhpExecutableFinder.php b/library/symfony/process/PhpExecutableFinder.php new file mode 100644 index 000000000..fb297825f --- /dev/null +++ b/library/symfony/process/PhpExecutableFinder.php @@ -0,0 +1,90 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +/** + * An executable finder specifically designed for the PHP executable. + * + * @author Fabien Potencier <fabien@symfony.com> + * @author Johannes M. Schmitt <schmittjoh@gmail.com> + */ +class PhpExecutableFinder +{ + private $executableFinder; + + public function __construct() + { + $this->executableFinder = new ExecutableFinder(); + } + + /** + * Finds The PHP executable. + * + * @param bool $includeArgs Whether or not include command arguments + * + * @return string|false The PHP executable path or false if it cannot be found + */ + public function find($includeArgs = true) + { + $args = $this->findArguments(); + $args = $includeArgs && $args ? ' '.implode(' ', $args) : ''; + + // HHVM support + if (defined('HHVM_VERSION')) { + return (getenv('PHP_BINARY') ?: PHP_BINARY).$args; + } + + // PHP_BINARY return the current sapi executable + if (defined('PHP_BINARY') && PHP_BINARY && in_array(PHP_SAPI, array('cli', 'cli-server', 'phpdbg')) && is_file(PHP_BINARY)) { + return PHP_BINARY.$args; + } + + if ($php = getenv('PHP_PATH')) { + if (!is_executable($php)) { + return false; + } + + return $php; + } + + if ($php = getenv('PHP_PEAR_PHP_BIN')) { + if (is_executable($php)) { + return $php; + } + } + + $dirs = array(PHP_BINDIR); + if ('\\' === DIRECTORY_SEPARATOR) { + $dirs[] = 'C:\xampp\php\\'; + } + + return $this->executableFinder->find('php', false, $dirs); + } + + /** + * Finds the PHP executable arguments. + * + * @return array The PHP executable arguments + */ + public function findArguments() + { + $arguments = array(); + + if (defined('HHVM_VERSION')) { + $arguments[] = '--php'; + } elseif ('phpdbg' === PHP_SAPI) { + $arguments[] = '-qrr'; + } + + return $arguments; + } +} diff --git a/library/symfony/process/PhpProcess.php b/library/symfony/process/PhpProcess.php new file mode 100644 index 000000000..4a2a2625f --- /dev/null +++ b/library/symfony/process/PhpProcess.php @@ -0,0 +1,72 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +use Symfony\Component\Process\Exception\RuntimeException; + +/** + * PhpProcess runs a PHP script in an independent process. + * + * $p = new PhpProcess('<?php echo "foo"; ?>'); + * $p->run(); + * print $p->getOutput()."\n"; + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class PhpProcess extends Process +{ + /** + * Constructor. + * + * @param string $script The PHP script to run (as a string) + * @param string|null $cwd The working directory or null to use the working dir of the current PHP process + * @param array|null $env The environment variables or null to use the same environment as the current PHP process + * @param int $timeout The timeout in seconds + * @param array $options An array of options for proc_open + */ + public function __construct($script, $cwd = null, array $env = null, $timeout = 60, array $options = array()) + { + $executableFinder = new PhpExecutableFinder(); + if (false === $php = $executableFinder->find()) { + $php = null; + } + if ('phpdbg' === PHP_SAPI) { + $file = tempnam(sys_get_temp_dir(), 'dbg'); + file_put_contents($file, $script); + register_shutdown_function('unlink', $file); + $php .= ' '.ProcessUtils::escapeArgument($file); + $script = null; + } + + parent::__construct($php, $cwd, $env, $script, $timeout, $options); + } + + /** + * Sets the path to the PHP binary to use. + */ + public function setPhpBinary($php) + { + $this->setCommandLine($php); + } + + /** + * {@inheritdoc} + */ + public function start($callback = null) + { + if (null === $this->getCommandLine()) { + throw new RuntimeException('Unable to find the PHP executable.'); + } + + parent::start($callback); + } +} diff --git a/library/symfony/process/Pipes/AbstractPipes.php b/library/symfony/process/Pipes/AbstractPipes.php new file mode 100644 index 000000000..1ca85739f --- /dev/null +++ b/library/symfony/process/Pipes/AbstractPipes.php @@ -0,0 +1,74 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Pipes; + +/** + * @author Romain Neutron <imprec@gmail.com> + * + * @internal + */ +abstract class AbstractPipes implements PipesInterface +{ + /** @var array */ + public $pipes = array(); + + /** @var string */ + protected $inputBuffer = ''; + /** @var resource|null */ + protected $input; + + /** @var bool */ + private $blocked = true; + + /** + * {@inheritdoc} + */ + public function close() + { + foreach ($this->pipes as $pipe) { + fclose($pipe); + } + $this->pipes = array(); + } + + /** + * Returns true if a system call has been interrupted. + * + * @return bool + */ + protected function hasSystemCallBeenInterrupted() + { + $lastError = error_get_last(); + + // stream_select returns false when the `select` system call is interrupted by an incoming signal + return isset($lastError['message']) && false !== stripos($lastError['message'], 'interrupted system call'); + } + + /** + * Unblocks streams. + */ + protected function unblock() + { + if (!$this->blocked) { + return; + } + + foreach ($this->pipes as $pipe) { + stream_set_blocking($pipe, 0); + } + if (null !== $this->input) { + stream_set_blocking($this->input, 0); + } + + $this->blocked = false; + } +} diff --git a/library/symfony/process/Pipes/PipesInterface.php b/library/symfony/process/Pipes/PipesInterface.php new file mode 100644 index 000000000..09d3f61d6 --- /dev/null +++ b/library/symfony/process/Pipes/PipesInterface.php @@ -0,0 +1,60 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Pipes; + +/** + * PipesInterface manages descriptors and pipes for the use of proc_open. + * + * @author Romain Neutron <imprec@gmail.com> + * + * @internal + */ +interface PipesInterface +{ + const CHUNK_SIZE = 16384; + + /** + * Returns an array of descriptors for the use of proc_open. + * + * @return array + */ + public function getDescriptors(); + + /** + * Returns an array of filenames indexed by their related stream in case these pipes use temporary files. + * + * @return string[] + */ + public function getFiles(); + + /** + * Reads data in file handles and pipes. + * + * @param bool $blocking Whether to use blocking calls or not. + * @param bool $close Whether to close pipes if they've reached EOF. + * + * @return string[] An array of read data indexed by their fd. + */ + public function readAndWrite($blocking, $close = false); + + /** + * Returns if the current state has open file handles or pipes. + * + * @return bool + */ + public function areOpen(); + + /** + * Closes file handles and pipes. + */ + public function close(); +} diff --git a/library/symfony/process/Pipes/UnixPipes.php b/library/symfony/process/Pipes/UnixPipes.php new file mode 100644 index 000000000..f8a0d1997 --- /dev/null +++ b/library/symfony/process/Pipes/UnixPipes.php @@ -0,0 +1,214 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Pipes; + +use Symfony\Component\Process\Process; + +/** + * UnixPipes implementation uses unix pipes as handles. + * + * @author Romain Neutron <imprec@gmail.com> + * + * @internal + */ +class UnixPipes extends AbstractPipes +{ + /** @var bool */ + private $ttyMode; + /** @var bool */ + private $ptyMode; + /** @var bool */ + private $disableOutput; + + public function __construct($ttyMode, $ptyMode, $input, $disableOutput) + { + $this->ttyMode = (bool) $ttyMode; + $this->ptyMode = (bool) $ptyMode; + $this->disableOutput = (bool) $disableOutput; + + if (is_resource($input)) { + $this->input = $input; + } else { + $this->inputBuffer = (string) $input; + } + } + + public function __destruct() + { + $this->close(); + } + + /** + * {@inheritdoc} + */ + public function getDescriptors() + { + if ($this->disableOutput) { + $nullstream = fopen('/dev/null', 'c'); + + return array( + array('pipe', 'r'), + $nullstream, + $nullstream, + ); + } + + if ($this->ttyMode) { + return array( + array('file', '/dev/tty', 'r'), + array('file', '/dev/tty', 'w'), + array('file', '/dev/tty', 'w'), + ); + } + + if ($this->ptyMode && Process::isPtySupported()) { + return array( + array('pty'), + array('pty'), + array('pty'), + ); + } + + return array( + array('pipe', 'r'), + array('pipe', 'w'), // stdout + array('pipe', 'w'), // stderr + ); + } + + /** + * {@inheritdoc} + */ + public function getFiles() + { + return array(); + } + + /** + * {@inheritdoc} + */ + public function readAndWrite($blocking, $close = false) + { + // only stdin is left open, job has been done ! + // we can now close it + if (1 === count($this->pipes) && array(0) === array_keys($this->pipes)) { + fclose($this->pipes[0]); + unset($this->pipes[0]); + } + + if (empty($this->pipes)) { + return array(); + } + + $this->unblock(); + + $read = array(); + + if (null !== $this->input) { + // if input is a resource, let's add it to stream_select argument to + // fill a buffer + $r = array_merge($this->pipes, array('input' => $this->input)); + } else { + $r = $this->pipes; + } + // discard read on stdin + unset($r[0]); + + $w = isset($this->pipes[0]) ? array($this->pipes[0]) : null; + $e = null; + + // let's have a look if something changed in streams + if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) { + // if a system call has been interrupted, forget about it, let's try again + // otherwise, an error occurred, let's reset pipes + if (!$this->hasSystemCallBeenInterrupted()) { + $this->pipes = array(); + } + + return $read; + } + + // nothing has changed + if (0 === $n) { + return $read; + } + + foreach ($r as $pipe) { + // prior PHP 5.4 the array passed to stream_select is modified and + // lose key association, we have to find back the key + $type = (false !== $found = array_search($pipe, $this->pipes)) ? $found : 'input'; + $data = ''; + while ('' !== $dataread = (string) fread($pipe, self::CHUNK_SIZE)) { + $data .= $dataread; + } + + if ('' !== $data) { + if ($type === 'input') { + $this->inputBuffer .= $data; + } else { + $read[$type] = $data; + } + } + + if (false === $data || (true === $close && feof($pipe) && '' === $data)) { + if ($type === 'input') { + // no more data to read on input resource + // use an empty buffer in the next reads + $this->input = null; + } else { + fclose($this->pipes[$type]); + unset($this->pipes[$type]); + } + } + } + + if (null !== $w && 0 < count($w)) { + while (strlen($this->inputBuffer)) { + $written = fwrite($w[0], $this->inputBuffer, 2 << 18); // write 512k + if ($written > 0) { + $this->inputBuffer = (string) substr($this->inputBuffer, $written); + } else { + break; + } + } + } + + // no input to read on resource, buffer is empty and stdin still open + if ('' === $this->inputBuffer && null === $this->input && isset($this->pipes[0])) { + fclose($this->pipes[0]); + unset($this->pipes[0]); + } + + return $read; + } + + /** + * {@inheritdoc} + */ + public function areOpen() + { + return (bool) $this->pipes; + } + + /** + * Creates a new UnixPipes instance. + * + * @param Process $process + * @param string|resource $input + * + * @return UnixPipes + */ + public static function create(Process $process, $input) + { + return new static($process->isTty(), $process->isPty(), $input, $process->isOutputDisabled()); + } +} diff --git a/library/symfony/process/Pipes/WindowsPipes.php b/library/symfony/process/Pipes/WindowsPipes.php new file mode 100644 index 000000000..1472f8c6c --- /dev/null +++ b/library/symfony/process/Pipes/WindowsPipes.php @@ -0,0 +1,253 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Pipes; + +use Symfony\Component\Process\Process; +use Symfony\Component\Process\Exception\RuntimeException; + +/** + * WindowsPipes implementation uses temporary files as handles. + * + * @see https://bugs.php.net/bug.php?id=51800 + * @see https://bugs.php.net/bug.php?id=65650 + * + * @author Romain Neutron <imprec@gmail.com> + * + * @internal + */ +class WindowsPipes extends AbstractPipes +{ + /** @var array */ + private $files = array(); + /** @var array */ + private $fileHandles = array(); + /** @var array */ + private $readBytes = array( + Process::STDOUT => 0, + Process::STDERR => 0, + ); + /** @var bool */ + private $disableOutput; + + public function __construct($disableOutput, $input) + { + $this->disableOutput = (bool) $disableOutput; + + if (!$this->disableOutput) { + // Fix for PHP bug #51800: reading from STDOUT pipe hangs forever on Windows if the output is too big. + // Workaround for this problem is to use temporary files instead of pipes on Windows platform. + // + // @see https://bugs.php.net/bug.php?id=51800 + $this->files = array( + Process::STDOUT => tempnam(sys_get_temp_dir(), 'out_sf_proc'), + Process::STDERR => tempnam(sys_get_temp_dir(), 'err_sf_proc'), + ); + foreach ($this->files as $offset => $file) { + if (false === $file || false === $this->fileHandles[$offset] = fopen($file, 'rb')) { + throw new RuntimeException('A temporary file could not be opened to write the process output to, verify that your TEMP environment variable is writable'); + } + } + } + + if (is_resource($input)) { + $this->input = $input; + } else { + $this->inputBuffer = $input; + } + } + + public function __destruct() + { + $this->close(); + $this->removeFiles(); + } + + /** + * {@inheritdoc} + */ + public function getDescriptors() + { + if ($this->disableOutput) { + $nullstream = fopen('NUL', 'c'); + + return array( + array('pipe', 'r'), + $nullstream, + $nullstream, + ); + } + + // We're not using pipe on Windows platform as it hangs (https://bugs.php.net/bug.php?id=51800) + // We're not using file handles as it can produce corrupted output https://bugs.php.net/bug.php?id=65650 + // So we redirect output within the commandline and pass the nul device to the process + return array( + array('pipe', 'r'), + array('file', 'NUL', 'w'), + array('file', 'NUL', 'w'), + ); + } + + /** + * {@inheritdoc} + */ + public function getFiles() + { + return $this->files; + } + + /** + * {@inheritdoc} + */ + public function readAndWrite($blocking, $close = false) + { + $this->write($blocking, $close); + + $read = array(); + $fh = $this->fileHandles; + foreach ($fh as $type => $fileHandle) { + if (0 !== fseek($fileHandle, $this->readBytes[$type])) { + continue; + } + $data = ''; + $dataread = null; + while (!feof($fileHandle)) { + if (false !== $dataread = fread($fileHandle, self::CHUNK_SIZE)) { + $data .= $dataread; + } + } + if (0 < $length = strlen($data)) { + $this->readBytes[$type] += $length; + $read[$type] = $data; + } + + if (false === $dataread || (true === $close && feof($fileHandle) && '' === $data)) { + fclose($this->fileHandles[$type]); + unset($this->fileHandles[$type]); + } + } + + return $read; + } + + /** + * {@inheritdoc} + */ + public function areOpen() + { + return (bool) $this->pipes && (bool) $this->fileHandles; + } + + /** + * {@inheritdoc} + */ + public function close() + { + parent::close(); + foreach ($this->fileHandles as $handle) { + fclose($handle); + } + $this->fileHandles = array(); + } + + /** + * Creates a new WindowsPipes instance. + * + * @param Process $process The process + * @param $input + * + * @return WindowsPipes + */ + public static function create(Process $process, $input) + { + return new static($process->isOutputDisabled(), $input); + } + + /** + * Removes temporary files. + */ + private function removeFiles() + { + foreach ($this->files as $filename) { + if (file_exists($filename)) { + @unlink($filename); + } + } + $this->files = array(); + } + + /** + * Writes input to stdin. + * + * @param bool $blocking + * @param bool $close + */ + private function write($blocking, $close) + { + if (empty($this->pipes)) { + return; + } + + $this->unblock(); + + $r = null !== $this->input ? array('input' => $this->input) : null; + $w = isset($this->pipes[0]) ? array($this->pipes[0]) : null; + $e = null; + + // let's have a look if something changed in streams + if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) { + // if a system call has been interrupted, forget about it, let's try again + // otherwise, an error occurred, let's reset pipes + if (!$this->hasSystemCallBeenInterrupted()) { + $this->pipes = array(); + } + + return; + } + + // nothing has changed + if (0 === $n) { + return; + } + + if (null !== $w && 0 < count($r)) { + $data = ''; + while ($dataread = fread($r['input'], self::CHUNK_SIZE)) { + $data .= $dataread; + } + + $this->inputBuffer .= $data; + + if (false === $data || (true === $close && feof($r['input']) && '' === $data)) { + // no more data to read on input resource + // use an empty buffer in the next reads + $this->input = null; + } + } + + if (null !== $w && 0 < count($w)) { + while (strlen($this->inputBuffer)) { + $written = fwrite($w[0], $this->inputBuffer, 2 << 18); + if ($written > 0) { + $this->inputBuffer = (string) substr($this->inputBuffer, $written); + } else { + break; + } + } + } + + // no input to read on resource, buffer is empty and stdin still open + if ('' === $this->inputBuffer && null === $this->input && isset($this->pipes[0])) { + fclose($this->pipes[0]); + unset($this->pipes[0]); + } + } +} diff --git a/library/symfony/process/Process.php b/library/symfony/process/Process.php new file mode 100644 index 000000000..c1e732170 --- /dev/null +++ b/library/symfony/process/Process.php @@ -0,0 +1,1515 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +use Symfony\Component\Process\Exception\InvalidArgumentException; +use Symfony\Component\Process\Exception\LogicException; +use Symfony\Component\Process\Exception\ProcessFailedException; +use Symfony\Component\Process\Exception\ProcessTimedOutException; +use Symfony\Component\Process\Exception\RuntimeException; +use Symfony\Component\Process\Pipes\PipesInterface; +use Symfony\Component\Process\Pipes\UnixPipes; +use Symfony\Component\Process\Pipes\WindowsPipes; + +/** + * Process is a thin wrapper around proc_* functions to easily + * start independent PHP processes. + * + * @author Fabien Potencier <fabien@symfony.com> + * @author Romain Neutron <imprec@gmail.com> + */ +class Process +{ + const ERR = 'err'; + const OUT = 'out'; + + const STATUS_READY = 'ready'; + const STATUS_STARTED = 'started'; + const STATUS_TERMINATED = 'terminated'; + + const STDIN = 0; + const STDOUT = 1; + const STDERR = 2; + + // Timeout Precision in seconds. + const TIMEOUT_PRECISION = 0.2; + + private $callback; + private $commandline; + private $cwd; + private $env; + private $input; + private $starttime; + private $lastOutputTime; + private $timeout; + private $idleTimeout; + private $options; + private $exitcode; + private $fallbackExitcode; + private $processInformation; + private $outputDisabled = false; + private $stdout; + private $stderr; + private $enhanceWindowsCompatibility = true; + private $enhanceSigchildCompatibility; + private $process; + private $status = self::STATUS_READY; + private $incrementalOutputOffset = 0; + private $incrementalErrorOutputOffset = 0; + private $tty; + private $pty; + + private $useFileHandles = false; + /** @var PipesInterface */ + private $processPipes; + + private $latestSignal; + + private static $sigchild; + + /** + * Exit codes translation table. + * + * User-defined errors must use exit codes in the 64-113 range. + * + * @var array + */ + public static $exitCodes = array( + 0 => 'OK', + 1 => 'General error', + 2 => 'Misuse of shell builtins', + + 126 => 'Invoked command cannot execute', + 127 => 'Command not found', + 128 => 'Invalid exit argument', + + // signals + 129 => 'Hangup', + 130 => 'Interrupt', + 131 => 'Quit and dump core', + 132 => 'Illegal instruction', + 133 => 'Trace/breakpoint trap', + 134 => 'Process aborted', + 135 => 'Bus error: "access to undefined portion of memory object"', + 136 => 'Floating point exception: "erroneous arithmetic operation"', + 137 => 'Kill (terminate immediately)', + 138 => 'User-defined 1', + 139 => 'Segmentation violation', + 140 => 'User-defined 2', + 141 => 'Write to pipe with no one reading', + 142 => 'Signal raised by alarm', + 143 => 'Termination (request to terminate)', + // 144 - not defined + 145 => 'Child process terminated, stopped (or continued*)', + 146 => 'Continue if stopped', + 147 => 'Stop executing temporarily', + 148 => 'Terminal stop signal', + 149 => 'Background process attempting to read from tty ("in")', + 150 => 'Background process attempting to write to tty ("out")', + 151 => 'Urgent data available on socket', + 152 => 'CPU time limit exceeded', + 153 => 'File size limit exceeded', + 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"', + 155 => 'Profiling timer expired', + // 156 - not defined + 157 => 'Pollable event', + // 158 - not defined + 159 => 'Bad syscall', + ); + + /** + * Constructor. + * + * @param string $commandline The command line to run + * @param string|null $cwd The working directory or null to use the working dir of the current PHP process + * @param array|null $env The environment variables or null to use the same environment as the current PHP process + * @param string|null $input The input + * @param int|float|null $timeout The timeout in seconds or null to disable + * @param array $options An array of options for proc_open + * + * @throws RuntimeException When proc_open is not installed + */ + public function __construct($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = array()) + { + if (!function_exists('proc_open')) { + throw new RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.'); + } + + $this->commandline = $commandline; + $this->cwd = $cwd; + + // on Windows, if the cwd changed via chdir(), proc_open defaults to the dir where PHP was started + // on Gnu/Linux, PHP builds with --enable-maintainer-zts are also affected + // @see : https://bugs.php.net/bug.php?id=51800 + // @see : https://bugs.php.net/bug.php?id=50524 + if (null === $this->cwd && (defined('ZEND_THREAD_SAFE') || '\\' === DIRECTORY_SEPARATOR)) { + $this->cwd = getcwd(); + } + if (null !== $env) { + $this->setEnv($env); + } + + $this->input = $input; + $this->setTimeout($timeout); + $this->useFileHandles = '\\' === DIRECTORY_SEPARATOR; + $this->pty = false; + $this->enhanceWindowsCompatibility = true; + $this->enhanceSigchildCompatibility = '\\' !== DIRECTORY_SEPARATOR && $this->isSigchildEnabled(); + $this->options = array_replace(array('suppress_errors' => true, 'binary_pipes' => true), $options); + } + + public function __destruct() + { + // stop() will check if we have a process running. + $this->stop(); + } + + public function __clone() + { + $this->resetProcessData(); + } + + /** + * Runs the process. + * + * The callback receives the type of output (out or err) and + * some bytes from the output in real-time. It allows to have feedback + * from the independent process during execution. + * + * The STDOUT and STDERR are also available after the process is finished + * via the getOutput() and getErrorOutput() methods. + * + * @param callable|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR + * + * @return int The exit status code + * + * @throws RuntimeException When process can't be launched + * @throws RuntimeException When process stopped after receiving signal + * @throws LogicException In case a callback is provided and output has been disabled + */ + public function run($callback = null) + { + $this->start($callback); + + return $this->wait(); + } + + /** + * Runs the process. + * + * This is identical to run() except that an exception is thrown if the process + * exits with a non-zero exit code. + * + * @param callable|null $callback + * + * @return self + * + * @throws RuntimeException if PHP was compiled with --enable-sigchild and the enhanced sigchild compatibility mode is not enabled + * @throws ProcessFailedException if the process didn't terminate successfully + */ + public function mustRun($callback = null) + { + if ($this->isSigchildEnabled() && !$this->enhanceSigchildCompatibility) { + throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.'); + } + + if (0 !== $this->run($callback)) { + throw new ProcessFailedException($this); + } + + return $this; + } + + /** + * Starts the process and returns after writing the input to STDIN. + * + * This method blocks until all STDIN data is sent to the process then it + * returns while the process runs in the background. + * + * The termination of the process can be awaited with wait(). + * + * The callback receives the type of output (out or err) and some bytes from + * the output in real-time while writing the standard input to the process. + * It allows to have feedback from the independent process during execution. + * If there is no callback passed, the wait() method can be called + * with true as a second parameter then the callback will get all data occurred + * in (and since) the start call. + * + * @param callable|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR + * + * @throws RuntimeException When process can't be launched + * @throws RuntimeException When process is already running + * @throws LogicException In case a callback is provided and output has been disabled + */ + public function start($callback = null) + { + if ($this->isRunning()) { + throw new RuntimeException('Process is already running'); + } + if ($this->outputDisabled && null !== $callback) { + throw new LogicException('Output has been disabled, enable it to allow the use of a callback.'); + } + + $this->resetProcessData(); + $this->starttime = $this->lastOutputTime = microtime(true); + $this->callback = $this->buildCallback($callback); + $descriptors = $this->getDescriptors(); + + $commandline = $this->commandline; + + if ('\\' === DIRECTORY_SEPARATOR && $this->enhanceWindowsCompatibility) { + $commandline = 'cmd /V:ON /E:ON /D /C "('.$commandline.')'; + foreach ($this->processPipes->getFiles() as $offset => $filename) { + $commandline .= ' '.$offset.'>'.ProcessUtils::escapeArgument($filename); + } + $commandline .= '"'; + + if (!isset($this->options['bypass_shell'])) { + $this->options['bypass_shell'] = true; + } + } + + $ptsWorkaround = null; + + if (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { + // Workaround for the bug, when PTS functionality is enabled. + // @see : https://bugs.php.net/69442 + $ptsWorkaround = fopen(__FILE__, 'r'); + } + + $this->process = proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $this->env, $this->options); + + if ($ptsWorkaround) { + fclose($ptsWorkaround); + } + + if (!is_resource($this->process)) { + throw new RuntimeException('Unable to launch a new process.'); + } + $this->status = self::STATUS_STARTED; + + if ($this->tty) { + return; + } + + $this->updateStatus(false); + $this->checkTimeout(); + } + + /** + * Restarts the process. + * + * Be warned that the process is cloned before being started. + * + * @param callable|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR + * + * @return Process The new process + * + * @throws RuntimeException When process can't be launched + * @throws RuntimeException When process is already running + * + * @see start() + */ + public function restart($callback = null) + { + if ($this->isRunning()) { + throw new RuntimeException('Process is already running'); + } + + $process = clone $this; + $process->start($callback); + + return $process; + } + + /** + * Waits for the process to terminate. + * + * The callback receives the type of output (out or err) and some bytes + * from the output in real-time while writing the standard input to the process. + * It allows to have feedback from the independent process during execution. + * + * @param callable|null $callback A valid PHP callback + * + * @return int The exitcode of the process + * + * @throws RuntimeException When process timed out + * @throws RuntimeException When process stopped after receiving signal + * @throws LogicException When process is not yet started + */ + public function wait($callback = null) + { + $this->requireProcessIsStarted(__FUNCTION__); + + $this->updateStatus(false); + if (null !== $callback) { + $this->callback = $this->buildCallback($callback); + } + + do { + $this->checkTimeout(); + $running = '\\' === DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen(); + $close = '\\' !== DIRECTORY_SEPARATOR || !$running; + $this->readPipes(true, $close); + } while ($running); + + while ($this->isRunning()) { + usleep(1000); + } + + if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) { + throw new RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation['termsig'])); + } + + return $this->exitcode; + } + + /** + * Returns the Pid (process identifier), if applicable. + * + * @return int|null The process id if running, null otherwise + * + * @throws RuntimeException In case --enable-sigchild is activated + */ + public function getPid() + { + if ($this->isSigchildEnabled()) { + throw new RuntimeException('This PHP has been compiled with --enable-sigchild. The process identifier can not be retrieved.'); + } + + $this->updateStatus(false); + + return $this->isRunning() ? $this->processInformation['pid'] : null; + } + + /** + * Sends a POSIX signal to the process. + * + * @param int $signal A valid POSIX signal (see http://www.php.net/manual/en/pcntl.constants.php) + * + * @return Process + * + * @throws LogicException In case the process is not running + * @throws RuntimeException In case --enable-sigchild is activated + * @throws RuntimeException In case of failure + */ + public function signal($signal) + { + $this->doSignal($signal, true); + + return $this; + } + + /** + * Disables fetching output and error output from the underlying process. + * + * @return Process + * + * @throws RuntimeException In case the process is already running + * @throws LogicException if an idle timeout is set + */ + public function disableOutput() + { + if ($this->isRunning()) { + throw new RuntimeException('Disabling output while the process is running is not possible.'); + } + if (null !== $this->idleTimeout) { + throw new LogicException('Output can not be disabled while an idle timeout is set.'); + } + + $this->outputDisabled = true; + + return $this; + } + + /** + * Enables fetching output and error output from the underlying process. + * + * @return Process + * + * @throws RuntimeException In case the process is already running + */ + public function enableOutput() + { + if ($this->isRunning()) { + throw new RuntimeException('Enabling output while the process is running is not possible.'); + } + + $this->outputDisabled = false; + + return $this; + } + + /** + * Returns true in case the output is disabled, false otherwise. + * + * @return bool + */ + public function isOutputDisabled() + { + return $this->outputDisabled; + } + + /** + * Returns the current output of the process (STDOUT). + * + * @return string The process output + * + * @throws LogicException in case the output has been disabled + * @throws LogicException In case the process is not started + */ + public function getOutput() + { + if ($this->outputDisabled) { + throw new LogicException('Output has been disabled.'); + } + + $this->requireProcessIsStarted(__FUNCTION__); + + $this->readPipes(false, '\\' === DIRECTORY_SEPARATOR ? !$this->processInformation['running'] : true); + + return $this->stdout; + } + + /** + * Returns the output incrementally. + * + * In comparison with the getOutput method which always return the whole + * output, this one returns the new output since the last call. + * + * @throws LogicException in case the output has been disabled + * @throws LogicException In case the process is not started + * + * @return string The process output since the last call + */ + public function getIncrementalOutput() + { + $this->requireProcessIsStarted(__FUNCTION__); + + $data = $this->getOutput(); + + $latest = substr($data, $this->incrementalOutputOffset); + + if (false === $latest) { + return ''; + } + + $this->incrementalOutputOffset = strlen($data); + + return $latest; + } + + /** + * Clears the process output. + * + * @return Process + */ + public function clearOutput() + { + $this->stdout = ''; + $this->incrementalOutputOffset = 0; + + return $this; + } + + /** + * Returns the current error output of the process (STDERR). + * + * @return string The process error output + * + * @throws LogicException in case the output has been disabled + * @throws LogicException In case the process is not started + */ + public function getErrorOutput() + { + if ($this->outputDisabled) { + throw new LogicException('Output has been disabled.'); + } + + $this->requireProcessIsStarted(__FUNCTION__); + + $this->readPipes(false, '\\' === DIRECTORY_SEPARATOR ? !$this->processInformation['running'] : true); + + return $this->stderr; + } + + /** + * Returns the errorOutput incrementally. + * + * In comparison with the getErrorOutput method which always return the + * whole error output, this one returns the new error output since the last + * call. + * + * @throws LogicException in case the output has been disabled + * @throws LogicException In case the process is not started + * + * @return string The process error output since the last call + */ + public function getIncrementalErrorOutput() + { + $this->requireProcessIsStarted(__FUNCTION__); + + $data = $this->getErrorOutput(); + + $latest = substr($data, $this->incrementalErrorOutputOffset); + + if (false === $latest) { + return ''; + } + + $this->incrementalErrorOutputOffset = strlen($data); + + return $latest; + } + + /** + * Clears the process output. + * + * @return Process + */ + public function clearErrorOutput() + { + $this->stderr = ''; + $this->incrementalErrorOutputOffset = 0; + + return $this; + } + + /** + * Returns the exit code returned by the process. + * + * @return null|int The exit status code, null if the Process is not terminated + * + * @throws RuntimeException In case --enable-sigchild is activated and the sigchild compatibility mode is disabled + */ + public function getExitCode() + { + if ($this->isSigchildEnabled() && !$this->enhanceSigchildCompatibility) { + throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.'); + } + + $this->updateStatus(false); + + return $this->exitcode; + } + + /** + * Returns a string representation for the exit code returned by the process. + * + * This method relies on the Unix exit code status standardization + * and might not be relevant for other operating systems. + * + * @return null|string A string representation for the exit status code, null if the Process is not terminated. + * + * @throws RuntimeException In case --enable-sigchild is activated and the sigchild compatibility mode is disabled + * + * @see http://tldp.org/LDP/abs/html/exitcodes.html + * @see http://en.wikipedia.org/wiki/Unix_signal + */ + public function getExitCodeText() + { + if (null === $exitcode = $this->getExitCode()) { + return; + } + + return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error'; + } + + /** + * Checks if the process ended successfully. + * + * @return bool true if the process ended successfully, false otherwise + */ + public function isSuccessful() + { + return 0 === $this->getExitCode(); + } + + /** + * Returns true if the child process has been terminated by an uncaught signal. + * + * It always returns false on Windows. + * + * @return bool + * + * @throws RuntimeException In case --enable-sigchild is activated + * @throws LogicException In case the process is not terminated + */ + public function hasBeenSignaled() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + if ($this->isSigchildEnabled()) { + throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.'); + } + + $this->updateStatus(false); + + return $this->processInformation['signaled']; + } + + /** + * Returns the number of the signal that caused the child process to terminate its execution. + * + * It is only meaningful if hasBeenSignaled() returns true. + * + * @return int + * + * @throws RuntimeException In case --enable-sigchild is activated + * @throws LogicException In case the process is not terminated + */ + public function getTermSignal() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + if ($this->isSigchildEnabled()) { + throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.'); + } + + $this->updateStatus(false); + + return $this->processInformation['termsig']; + } + + /** + * Returns true if the child process has been stopped by a signal. + * + * It always returns false on Windows. + * + * @return bool + * + * @throws LogicException In case the process is not terminated + */ + public function hasBeenStopped() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + $this->updateStatus(false); + + return $this->processInformation['stopped']; + } + + /** + * Returns the number of the signal that caused the child process to stop its execution. + * + * It is only meaningful if hasBeenStopped() returns true. + * + * @return int + * + * @throws LogicException In case the process is not terminated + */ + public function getStopSignal() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + $this->updateStatus(false); + + return $this->processInformation['stopsig']; + } + + /** + * Checks if the process is currently running. + * + * @return bool true if the process is currently running, false otherwise + */ + public function isRunning() + { + if (self::STATUS_STARTED !== $this->status) { + return false; + } + + $this->updateStatus(false); + + return $this->processInformation['running']; + } + + /** + * Checks if the process has been started with no regard to the current state. + * + * @return bool true if status is ready, false otherwise + */ + public function isStarted() + { + return $this->status != self::STATUS_READY; + } + + /** + * Checks if the process is terminated. + * + * @return bool true if process is terminated, false otherwise + */ + public function isTerminated() + { + $this->updateStatus(false); + + return $this->status == self::STATUS_TERMINATED; + } + + /** + * Gets the process status. + * + * The status is one of: ready, started, terminated. + * + * @return string The current process status + */ + public function getStatus() + { + $this->updateStatus(false); + + return $this->status; + } + + /** + * Stops the process. + * + * @param int|float $timeout The timeout in seconds + * @param int $signal A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9) + * + * @return int The exit-code of the process + */ + public function stop($timeout = 10, $signal = null) + { + $timeoutMicro = microtime(true) + $timeout; + if ($this->isRunning()) { + // given `SIGTERM` may not be defined and that `proc_terminate` uses the constant value and not the constant itself, we use the same here + $this->doSignal(15, false); + do { + usleep(1000); + } while ($this->isRunning() && microtime(true) < $timeoutMicro); + + if ($this->isRunning() && !$this->isSigchildEnabled()) { + // Avoid exception here: process is supposed to be running, but it might have stopped just + // after this line. In any case, let's silently discard the error, we cannot do anything. + $this->doSignal($signal ?: 9, false); + } + } + + $this->updateStatus(false); + if ($this->processInformation['running']) { + $this->close(); + } + + return $this->exitcode; + } + + /** + * Adds a line to the STDOUT stream. + * + * @param string $line The line to append + */ + public function addOutput($line) + { + $this->lastOutputTime = microtime(true); + $this->stdout .= $line; + } + + /** + * Adds a line to the STDERR stream. + * + * @param string $line The line to append + */ + public function addErrorOutput($line) + { + $this->lastOutputTime = microtime(true); + $this->stderr .= $line; + } + + /** + * Gets the command line to be executed. + * + * @return string The command to execute + */ + public function getCommandLine() + { + return $this->commandline; + } + + /** + * Sets the command line to be executed. + * + * @param string $commandline The command to execute + * + * @return self The current Process instance + */ + public function setCommandLine($commandline) + { + $this->commandline = $commandline; + + return $this; + } + + /** + * Gets the process timeout (max. runtime). + * + * @return float|null The timeout in seconds or null if it's disabled + */ + public function getTimeout() + { + return $this->timeout; + } + + /** + * Gets the process idle timeout (max. time since last output). + * + * @return float|null The timeout in seconds or null if it's disabled + */ + public function getIdleTimeout() + { + return $this->idleTimeout; + } + + /** + * Sets the process timeout (max. runtime). + * + * To disable the timeout, set this value to null. + * + * @param int|float|null $timeout The timeout in seconds + * + * @return self The current Process instance + * + * @throws InvalidArgumentException if the timeout is negative + */ + public function setTimeout($timeout) + { + $this->timeout = $this->validateTimeout($timeout); + + return $this; + } + + /** + * Sets the process idle timeout (max. time since last output). + * + * To disable the timeout, set this value to null. + * + * @param int|float|null $timeout The timeout in seconds + * + * @return self The current Process instance. + * + * @throws LogicException if the output is disabled + * @throws InvalidArgumentException if the timeout is negative + */ + public function setIdleTimeout($timeout) + { + if (null !== $timeout && $this->outputDisabled) { + throw new LogicException('Idle timeout can not be set while the output is disabled.'); + } + + $this->idleTimeout = $this->validateTimeout($timeout); + + return $this; + } + + /** + * Enables or disables the TTY mode. + * + * @param bool $tty True to enabled and false to disable + * + * @return self The current Process instance + * + * @throws RuntimeException In case the TTY mode is not supported + */ + public function setTty($tty) + { + if ('\\' === DIRECTORY_SEPARATOR && $tty) { + throw new RuntimeException('TTY mode is not supported on Windows platform.'); + } + if ($tty && (!file_exists('/dev/tty') || !is_readable('/dev/tty'))) { + throw new RuntimeException('TTY mode requires /dev/tty to be readable.'); + } + + $this->tty = (bool) $tty; + + return $this; + } + + /** + * Checks if the TTY mode is enabled. + * + * @return bool true if the TTY mode is enabled, false otherwise + */ + public function isTty() + { + return $this->tty; + } + + /** + * Sets PTY mode. + * + * @param bool $bool + * + * @return self + */ + public function setPty($bool) + { + $this->pty = (bool) $bool; + + return $this; + } + + /** + * Returns PTY state. + * + * @return bool + */ + public function isPty() + { + return $this->pty; + } + + /** + * Gets the working directory. + * + * @return string|null The current working directory or null on failure + */ + public function getWorkingDirectory() + { + if (null === $this->cwd) { + // getcwd() will return false if any one of the parent directories does not have + // the readable or search mode set, even if the current directory does + return getcwd() ?: null; + } + + return $this->cwd; + } + + /** + * Sets the current working directory. + * + * @param string $cwd The new working directory + * + * @return self The current Process instance + */ + public function setWorkingDirectory($cwd) + { + $this->cwd = $cwd; + + return $this; + } + + /** + * Gets the environment variables. + * + * @return array The current environment variables + */ + public function getEnv() + { + return $this->env; + } + + /** + * Sets the environment variables. + * + * An environment variable value should be a string. + * If it is an array, the variable is ignored. + * + * That happens in PHP when 'argv' is registered into + * the $_ENV array for instance. + * + * @param array $env The new environment variables + * + * @return self The current Process instance + */ + public function setEnv(array $env) + { + // Process can not handle env values that are arrays + $env = array_filter($env, function ($value) { + return !is_array($value); + }); + + $this->env = array(); + foreach ($env as $key => $value) { + $this->env[$key] = (string) $value; + } + + return $this; + } + + /** + * Gets the contents of STDIN. + * + * @return string|null The current contents + * + * @deprecated since version 2.5, to be removed in 3.0. + * Use setInput() instead. + * This method is deprecated in favor of getInput. + */ + public function getStdin() + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.5 and will be removed in 3.0. Use the getInput() method instead.', E_USER_DEPRECATED); + + return $this->getInput(); + } + + /** + * Gets the Process input. + * + * @return null|string The Process input + */ + public function getInput() + { + return $this->input; + } + + /** + * Sets the contents of STDIN. + * + * @param string|null $stdin The new contents + * + * @return self The current Process instance + * + * @deprecated since version 2.5, to be removed in 3.0. + * Use setInput() instead. + * + * @throws LogicException In case the process is running + * @throws InvalidArgumentException In case the argument is invalid + */ + public function setStdin($stdin) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.5 and will be removed in 3.0. Use the setInput() method instead.', E_USER_DEPRECATED); + + return $this->setInput($stdin); + } + + /** + * Sets the input. + * + * This content will be passed to the underlying process standard input. + * + * @param mixed $input The content + * + * @return self The current Process instance + * + * @throws LogicException In case the process is running + * + * Passing an object as an input is deprecated since version 2.5 and will be removed in 3.0. + */ + public function setInput($input) + { + if ($this->isRunning()) { + throw new LogicException('Input can not be set while the process is running.'); + } + + $this->input = ProcessUtils::validateInput(sprintf('%s::%s', __CLASS__, __FUNCTION__), $input); + + return $this; + } + + /** + * Gets the options for proc_open. + * + * @return array The current options + */ + public function getOptions() + { + return $this->options; + } + + /** + * Sets the options for proc_open. + * + * @param array $options The new options + * + * @return self The current Process instance + */ + public function setOptions(array $options) + { + $this->options = $options; + + return $this; + } + + /** + * Gets whether or not Windows compatibility is enabled. + * + * This is true by default. + * + * @return bool + */ + public function getEnhanceWindowsCompatibility() + { + return $this->enhanceWindowsCompatibility; + } + + /** + * Sets whether or not Windows compatibility is enabled. + * + * @param bool $enhance + * + * @return self The current Process instance + */ + public function setEnhanceWindowsCompatibility($enhance) + { + $this->enhanceWindowsCompatibility = (bool) $enhance; + + return $this; + } + + /** + * Returns whether sigchild compatibility mode is activated or not. + * + * @return bool + */ + public function getEnhanceSigchildCompatibility() + { + return $this->enhanceSigchildCompatibility; + } + + /** + * Activates sigchild compatibility mode. + * + * Sigchild compatibility mode is required to get the exit code and + * determine the success of a process when PHP has been compiled with + * the --enable-sigchild option + * + * @param bool $enhance + * + * @return self The current Process instance + */ + public function setEnhanceSigchildCompatibility($enhance) + { + $this->enhanceSigchildCompatibility = (bool) $enhance; + + return $this; + } + + /** + * Performs a check between the timeout definition and the time the process started. + * + * In case you run a background process (with the start method), you should + * trigger this method regularly to ensure the process timeout + * + * @throws ProcessTimedOutException In case the timeout was reached + */ + public function checkTimeout() + { + if ($this->status !== self::STATUS_STARTED) { + return; + } + + if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) { + $this->stop(0); + + throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_GENERAL); + } + + if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) { + $this->stop(0); + + throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_IDLE); + } + } + + /** + * Returns whether PTY is supported on the current operating system. + * + * @return bool + */ + public static function isPtySupported() + { + static $result; + + if (null !== $result) { + return $result; + } + + if ('\\' === DIRECTORY_SEPARATOR) { + return $result = false; + } + + $proc = @proc_open('echo 1', array(array('pty'), array('pty'), array('pty')), $pipes); + if (is_resource($proc)) { + proc_close($proc); + + return $result = true; + } + + return $result = false; + } + + /** + * Creates the descriptors needed by the proc_open. + * + * @return array + */ + private function getDescriptors() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->processPipes = WindowsPipes::create($this, $this->input); + } else { + $this->processPipes = UnixPipes::create($this, $this->input); + } + $descriptors = $this->processPipes->getDescriptors($this->outputDisabled); + + if (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { + // last exit code is output on the fourth pipe and caught to work around --enable-sigchild + $descriptors = array_merge($descriptors, array(array('pipe', 'w'))); + + $this->commandline = '('.$this->commandline.') 3>/dev/null; code=$?; echo $code >&3; exit $code'; + } + + return $descriptors; + } + + /** + * Builds up the callback used by wait(). + * + * The callbacks adds all occurred output to the specific buffer and calls + * the user callback (if present) with the received output. + * + * @param callable|null $callback The user defined PHP callback + * + * @return \Closure A PHP closure + */ + protected function buildCallback($callback) + { + $that = $this; + $out = self::OUT; + $callback = function ($type, $data) use ($that, $callback, $out) { + if ($out == $type) { + $that->addOutput($data); + } else { + $that->addErrorOutput($data); + } + + if (null !== $callback) { + call_user_func($callback, $type, $data); + } + }; + + return $callback; + } + + /** + * Updates the status of the process, reads pipes. + * + * @param bool $blocking Whether to use a blocking read call. + */ + protected function updateStatus($blocking) + { + if (self::STATUS_STARTED !== $this->status) { + return; + } + + $this->processInformation = proc_get_status($this->process); + $this->captureExitCode(); + + $this->readPipes($blocking, '\\' === DIRECTORY_SEPARATOR ? !$this->processInformation['running'] : true); + + if (!$this->processInformation['running']) { + $this->close(); + } + } + + /** + * Returns whether PHP has been compiled with the '--enable-sigchild' option or not. + * + * @return bool + */ + protected function isSigchildEnabled() + { + if (null !== self::$sigchild) { + return self::$sigchild; + } + + if (!function_exists('phpinfo')) { + return self::$sigchild = false; + } + + ob_start(); + phpinfo(INFO_GENERAL); + + return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild'); + } + + /** + * Validates and returns the filtered timeout. + * + * @param int|float|null $timeout + * + * @return float|null + * + * @throws InvalidArgumentException if the given timeout is a negative number + */ + private function validateTimeout($timeout) + { + $timeout = (float) $timeout; + + if (0.0 === $timeout) { + $timeout = null; + } elseif ($timeout < 0) { + throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); + } + + return $timeout; + } + + /** + * Reads pipes, executes callback. + * + * @param bool $blocking Whether to use blocking calls or not. + * @param bool $close Whether to close file handles or not. + */ + private function readPipes($blocking, $close) + { + $result = $this->processPipes->readAndWrite($blocking, $close); + + $callback = $this->callback; + foreach ($result as $type => $data) { + if (3 == $type) { + $this->fallbackExitcode = (int) $data; + } else { + $callback($type === self::STDOUT ? self::OUT : self::ERR, $data); + } + } + } + + /** + * Captures the exitcode if mentioned in the process information. + */ + private function captureExitCode() + { + if (isset($this->processInformation['exitcode']) && -1 != $this->processInformation['exitcode']) { + $this->exitcode = $this->processInformation['exitcode']; + } + } + + /** + * Closes process resource, closes file handles, sets the exitcode. + * + * @return int The exitcode + */ + private function close() + { + $this->processPipes->close(); + if (is_resource($this->process)) { + $exitcode = proc_close($this->process); + } else { + $exitcode = -1; + } + + $this->exitcode = -1 !== $exitcode ? $exitcode : (null !== $this->exitcode ? $this->exitcode : -1); + $this->status = self::STATUS_TERMINATED; + + if (-1 === $this->exitcode && null !== $this->fallbackExitcode) { + $this->exitcode = $this->fallbackExitcode; + } elseif (-1 === $this->exitcode && $this->processInformation['signaled'] && 0 < $this->processInformation['termsig']) { + // if process has been signaled, no exitcode but a valid termsig, apply Unix convention + $this->exitcode = 128 + $this->processInformation['termsig']; + } + + return $this->exitcode; + } + + /** + * Resets data related to the latest run of the process. + */ + private function resetProcessData() + { + $this->starttime = null; + $this->callback = null; + $this->exitcode = null; + $this->fallbackExitcode = null; + $this->processInformation = null; + $this->stdout = null; + $this->stderr = null; + $this->process = null; + $this->latestSignal = null; + $this->status = self::STATUS_READY; + $this->incrementalOutputOffset = 0; + $this->incrementalErrorOutputOffset = 0; + } + + /** + * Sends a POSIX signal to the process. + * + * @param int $signal A valid POSIX signal (see http://www.php.net/manual/en/pcntl.constants.php) + * @param bool $throwException Whether to throw exception in case signal failed + * + * @return bool True if the signal was sent successfully, false otherwise + * + * @throws LogicException In case the process is not running + * @throws RuntimeException In case --enable-sigchild is activated + * @throws RuntimeException In case of failure + */ + private function doSignal($signal, $throwException) + { + if (!$this->isRunning()) { + if ($throwException) { + throw new LogicException('Can not send signal on a non running process.'); + } + + return false; + } + + if ($this->isSigchildEnabled()) { + if ($throwException) { + throw new RuntimeException('This PHP has been compiled with --enable-sigchild. The process can not be signaled.'); + } + + return false; + } + + if ('\\' === DIRECTORY_SEPARATOR) { + exec(sprintf('taskkill /F /T /PID %d 2>&1', $this->getPid()), $output, $exitCode); + if ($exitCode) { + if ($throwException) { + throw new RuntimeException(sprintf('Unable to kill the process (%s).', implode(' ', $output))); + } + + return false; + } + } + + if (true !== @proc_terminate($this->process, $signal) && '\\' !== DIRECTORY_SEPARATOR) { + if ($throwException) { + throw new RuntimeException(sprintf('Error while sending signal `%s`.', $signal)); + } + + return false; + } + + $this->latestSignal = $signal; + + return true; + } + + /** + * Ensures the process is running or terminated, throws a LogicException if the process has a not started. + * + * @param string $functionName The function name that was called. + * + * @throws LogicException If the process has not run. + */ + private function requireProcessIsStarted($functionName) + { + if (!$this->isStarted()) { + throw new LogicException(sprintf('Process must be started before calling %s.', $functionName)); + } + } + + /** + * Ensures the process is terminated, throws a LogicException if the process has a status different than `terminated`. + * + * @param string $functionName The function name that was called. + * + * @throws LogicException If the process is not yet terminated. + */ + private function requireProcessIsTerminated($functionName) + { + if (!$this->isTerminated()) { + throw new LogicException(sprintf('Process must be terminated before calling %s.', $functionName)); + } + } +} diff --git a/library/symfony/process/ProcessBuilder.php b/library/symfony/process/ProcessBuilder.php new file mode 100644 index 000000000..a782fd69e --- /dev/null +++ b/library/symfony/process/ProcessBuilder.php @@ -0,0 +1,287 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +use Symfony\Component\Process\Exception\InvalidArgumentException; +use Symfony\Component\Process\Exception\LogicException; + +/** + * Process builder. + * + * @author Kris Wallsmith <kris@symfony.com> + */ +class ProcessBuilder +{ + private $arguments; + private $cwd; + private $env = array(); + private $input; + private $timeout = 60; + private $options = array(); + private $inheritEnv = true; + private $prefix = array(); + private $outputDisabled = false; + + /** + * Constructor. + * + * @param string[] $arguments An array of arguments + */ + public function __construct(array $arguments = array()) + { + $this->arguments = $arguments; + } + + /** + * Creates a process builder instance. + * + * @param string[] $arguments An array of arguments + * + * @return ProcessBuilder + */ + public static function create(array $arguments = array()) + { + return new static($arguments); + } + + /** + * Adds an unescaped argument to the command string. + * + * @param string $argument A command argument + * + * @return ProcessBuilder + */ + public function add($argument) + { + $this->arguments[] = $argument; + + return $this; + } + + /** + * Adds a prefix to the command string. + * + * The prefix is preserved when resetting arguments. + * + * @param string|array $prefix A command prefix or an array of command prefixes + * + * @return ProcessBuilder + */ + public function setPrefix($prefix) + { + $this->prefix = is_array($prefix) ? $prefix : array($prefix); + + return $this; + } + + /** + * Sets the arguments of the process. + * + * Arguments must not be escaped. + * Previous arguments are removed. + * + * @param string[] $arguments + * + * @return ProcessBuilder + */ + public function setArguments(array $arguments) + { + $this->arguments = $arguments; + + return $this; + } + + /** + * Sets the working directory. + * + * @param null|string $cwd The working directory + * + * @return ProcessBuilder + */ + public function setWorkingDirectory($cwd) + { + $this->cwd = $cwd; + + return $this; + } + + /** + * Sets whether environment variables will be inherited or not. + * + * @param bool $inheritEnv + * + * @return ProcessBuilder + */ + public function inheritEnvironmentVariables($inheritEnv = true) + { + $this->inheritEnv = $inheritEnv; + + return $this; + } + + /** + * Sets an environment variable. + * + * Setting a variable overrides its previous value. Use `null` to unset a + * defined environment variable. + * + * @param string $name The variable name + * @param null|string $value The variable value + * + * @return ProcessBuilder + */ + public function setEnv($name, $value) + { + $this->env[$name] = $value; + + return $this; + } + + /** + * Adds a set of environment variables. + * + * Already existing environment variables with the same name will be + * overridden by the new values passed to this method. Pass `null` to unset + * a variable. + * + * @param array $variables The variables + * + * @return ProcessBuilder + */ + public function addEnvironmentVariables(array $variables) + { + $this->env = array_replace($this->env, $variables); + + return $this; + } + + /** + * Sets the input of the process. + * + * @param mixed $input The input as a string + * + * @return ProcessBuilder + * + * @throws InvalidArgumentException In case the argument is invalid + * + * Passing an object as an input is deprecated since version 2.5 and will be removed in 3.0. + */ + public function setInput($input) + { + $this->input = ProcessUtils::validateInput(sprintf('%s::%s', __CLASS__, __FUNCTION__), $input); + + return $this; + } + + /** + * Sets the process timeout. + * + * To disable the timeout, set this value to null. + * + * @param float|null $timeout + * + * @return ProcessBuilder + * + * @throws InvalidArgumentException + */ + public function setTimeout($timeout) + { + if (null === $timeout) { + $this->timeout = null; + + return $this; + } + + $timeout = (float) $timeout; + + if ($timeout < 0) { + throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); + } + + $this->timeout = $timeout; + + return $this; + } + + /** + * Adds a proc_open option. + * + * @param string $name The option name + * @param string $value The option value + * + * @return ProcessBuilder + */ + public function setOption($name, $value) + { + $this->options[$name] = $value; + + return $this; + } + + /** + * Disables fetching output and error output from the underlying process. + * + * @return ProcessBuilder + */ + public function disableOutput() + { + $this->outputDisabled = true; + + return $this; + } + + /** + * Enables fetching output and error output from the underlying process. + * + * @return ProcessBuilder + */ + public function enableOutput() + { + $this->outputDisabled = false; + + return $this; + } + + /** + * Creates a Process instance and returns it. + * + * @return Process + * + * @throws LogicException In case no arguments have been provided + */ + public function getProcess() + { + if (0 === count($this->prefix) && 0 === count($this->arguments)) { + throw new LogicException('You must add() command arguments before calling getProcess().'); + } + + $options = $this->options; + + $arguments = array_merge($this->prefix, $this->arguments); + $script = implode(' ', array_map(array(__NAMESPACE__.'\\ProcessUtils', 'escapeArgument'), $arguments)); + + if ($this->inheritEnv) { + // include $_ENV for BC purposes + $env = array_replace($_ENV, $_SERVER, $this->env); + } else { + $env = $this->env; + } + + $process = new Process($script, $this->cwd, $env, $this->input, $this->timeout, $options); + + if ($this->outputDisabled) { + $process->disableOutput(); + } + + return $process; + } +} diff --git a/library/symfony/process/ProcessUtils.php b/library/symfony/process/ProcessUtils.php new file mode 100644 index 000000000..4f30b630d --- /dev/null +++ b/library/symfony/process/ProcessUtils.php @@ -0,0 +1,115 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +use Symfony\Component\Process\Exception\InvalidArgumentException; + +/** + * ProcessUtils is a bunch of utility methods. + * + * This class contains static methods only and is not meant to be instantiated. + * + * @author Martin Hasoň <martin.hason@gmail.com> + */ +class ProcessUtils +{ + /** + * This class should not be instantiated. + */ + private function __construct() + { + } + + /** + * Escapes a string to be used as a shell argument. + * + * @param string $argument The argument that will be escaped + * + * @return string The escaped argument + */ + public static function escapeArgument($argument) + { + //Fix for PHP bug #43784 escapeshellarg removes % from given string + //Fix for PHP bug #49446 escapeshellarg doesn't work on Windows + //@see https://bugs.php.net/bug.php?id=43784 + //@see https://bugs.php.net/bug.php?id=49446 + if ('\\' === DIRECTORY_SEPARATOR) { + if ('' === $argument) { + return escapeshellarg($argument); + } + + $escapedArgument = ''; + $quote = false; + foreach (preg_split('/(")/', $argument, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) as $part) { + if ('"' === $part) { + $escapedArgument .= '\\"'; + } elseif (self::isSurroundedBy($part, '%')) { + // Avoid environment variable expansion + $escapedArgument .= '^%"'.substr($part, 1, -1).'"^%'; + } else { + // escape trailing backslash + if ('\\' === substr($part, -1)) { + $part .= '\\'; + } + $quote = true; + $escapedArgument .= $part; + } + } + if ($quote) { + $escapedArgument = '"'.$escapedArgument.'"'; + } + + return $escapedArgument; + } + + return escapeshellarg($argument); + } + + /** + * Validates and normalizes a Process input. + * + * @param string $caller The name of method call that validates the input + * @param mixed $input The input to validate + * + * @return string The validated input + * + * @throws InvalidArgumentException In case the input is not valid + * + * Passing an object as an input is deprecated since version 2.5 and will be removed in 3.0. + */ + public static function validateInput($caller, $input) + { + if (null !== $input) { + if (is_resource($input)) { + return $input; + } + if (is_scalar($input)) { + return (string) $input; + } + // deprecated as of Symfony 2.5, to be removed in 3.0 + if (is_object($input) && method_exists($input, '__toString')) { + @trigger_error('Passing an object as an input is deprecated since version 2.5 and will be removed in 3.0.', E_USER_DEPRECATED); + + return (string) $input; + } + + throw new InvalidArgumentException(sprintf('%s only accepts strings or stream resources.', $caller)); + } + + return $input; + } + + private static function isSurroundedBy($arg, $char) + { + return 2 < strlen($arg) && $char === $arg[0] && $char === $arg[strlen($arg) - 1]; + } +} diff --git a/library/symfony/process/README.md b/library/symfony/process/README.md new file mode 100644 index 000000000..7222fe895 --- /dev/null +++ b/library/symfony/process/README.md @@ -0,0 +1,65 @@ +Process Component +================= + +Process executes commands in sub-processes. + +In this example, we run a simple directory listing and get the result back: + +```php +use Symfony\Component\Process\Process; +use Symfony\Component\Process\Exception\ProcessFailedException; + +$process = new Process('ls -lsa'); +$process->setTimeout(3600); +$process->run(); +if (!$process->isSuccessful()) { + throw new ProcessFailedException($process); +} + +print $process->getOutput(); +``` + +You can think that this is easy to achieve with plain PHP but it's not especially +if you want to take care of the subtle differences between the different platforms. + +You can simplify the code by using `mustRun()` instead of `run()`, which will +throw a `ProcessFailedException` automatically in case of a problem: + +```php +use Symfony\Component\Process\Process; + +$process = new Process('ls -lsa'); +$process->setTimeout(3600); +$process->mustRun(); + +print $process->getOutput(); +``` + +And if you want to be able to get some feedback in real-time, just pass an +anonymous function to the ``run()`` method and you will get the output buffer +as it becomes available: + +```php +use Symfony\Component\Process\Process; + +$process = new Process('ls -lsa'); +$process->run(function ($type, $buffer) { + if (Process::ERR === $type) { + echo 'ERR > '.$buffer; + } else { + echo 'OUT > '.$buffer; + } +}); +``` + +That's great if you want to execute a long running command (like rsync-ing files to a +remote server) and give feedback to the user in real-time. + +Resources +--------- + +You can run the unit tests with the following command: + + $ cd path/to/Symfony/Component/Process/ + $ composer install + $ phpunit diff --git a/library/symfony/process/Tests/AbstractProcessTest.php b/library/symfony/process/Tests/AbstractProcessTest.php new file mode 100644 index 000000000..fca3729be --- /dev/null +++ b/library/symfony/process/Tests/AbstractProcessTest.php @@ -0,0 +1,1196 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use Symfony\Component\Process\Exception\LogicException; +use Symfony\Component\Process\Exception\ProcessTimedOutException; +use Symfony\Component\Process\Exception\RuntimeException; +use Symfony\Component\Process\PhpExecutableFinder; +use Symfony\Component\Process\Pipes\PipesInterface; +use Symfony\Component\Process\Process; + +/** + * @author Robert Schönthal <seroscho@googlemail.com> + */ +abstract class AbstractProcessTest extends \PHPUnit_Framework_TestCase +{ + protected static $phpBin; + + public static function setUpBeforeClass() + { + $phpBin = new PhpExecutableFinder(); + self::$phpBin = 'phpdbg' === PHP_SAPI ? 'php' : $phpBin->find(); + } + + public function testThatProcessDoesNotThrowWarningDuringRun() + { + @trigger_error('Test Error', E_USER_NOTICE); + $process = $this->getProcess(self::$phpBin." -r 'sleep(3)'"); + $process->run(); + $actualError = error_get_last(); + $this->assertEquals('Test Error', $actualError['message']); + $this->assertEquals(E_USER_NOTICE, $actualError['type']); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException + */ + public function testNegativeTimeoutFromConstructor() + { + $this->getProcess('', null, null, null, -1); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException + */ + public function testNegativeTimeoutFromSetter() + { + $p = $this->getProcess(''); + $p->setTimeout(-1); + } + + public function testFloatAndNullTimeout() + { + $p = $this->getProcess(''); + + $p->setTimeout(10); + $this->assertSame(10.0, $p->getTimeout()); + + $p->setTimeout(null); + $this->assertNull($p->getTimeout()); + + $p->setTimeout(0.0); + $this->assertNull($p->getTimeout()); + } + + public function testStopWithTimeoutIsActuallyWorking() + { + if (!extension_loaded('pcntl')) { + $this->markTestSkipped('Extension pcntl is required.'); + } + + // exec is mandatory here since we send a signal to the process + // see https://github.com/symfony/symfony/issues/5030 about prepending + // command with exec + $p = $this->getProcess('exec '.self::$phpBin.' '.__DIR__.'/NonStopableProcess.php 3'); + $p->start(); + usleep(100000); + $start = microtime(true); + $p->stop(1.1, SIGKILL); + while ($p->isRunning()) { + usleep(1000); + } + $duration = microtime(true) - $start; + + $this->assertLessThan(4, $duration); + } + + public function testAllOutputIsActuallyReadOnTermination() + { + // this code will result in a maximum of 2 reads of 8192 bytes by calling + // start() and isRunning(). by the time getOutput() is called the process + // has terminated so the internal pipes array is already empty. normally + // the call to start() will not read any data as the process will not have + // generated output, but this is non-deterministic so we must count it as + // a possibility. therefore we need 2 * PipesInterface::CHUNK_SIZE plus + // another byte which will never be read. + $expectedOutputSize = PipesInterface::CHUNK_SIZE * 2 + 2; + + $code = sprintf('echo str_repeat(\'*\', %d);', $expectedOutputSize); + $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code))); + + $p->start(); + // Let's wait enough time for process to finish... + // Here we don't call Process::run or Process::wait to avoid any read of pipes + usleep(500000); + + if ($p->isRunning()) { + $this->markTestSkipped('Process execution did not complete in the required time frame'); + } + + $o = $p->getOutput(); + + $this->assertEquals($expectedOutputSize, strlen($o)); + } + + public function testCallbacksAreExecutedWithStart() + { + $data = ''; + + $process = $this->getProcess('echo foo && php -r "sleep(1);" && echo foo'); + $process->start(function ($type, $buffer) use (&$data) { + $data .= $buffer; + }); + + while ($process->isRunning()) { + usleep(10000); + } + + $this->assertEquals(2, preg_match_all('/foo/', $data, $matches)); + } + + /** + * tests results from sub processes. + * + * @dataProvider responsesCodeProvider + */ + public function testProcessResponses($expected, $getter, $code) + { + $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code))); + $p->run(); + + $this->assertSame($expected, $p->$getter()); + } + + /** + * tests results from sub processes. + * + * @dataProvider pipesCodeProvider + */ + public function testProcessPipes($code, $size) + { + $expected = str_repeat(str_repeat('*', 1024), $size).'!'; + $expectedLength = (1024 * $size) + 1; + + $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code))); + $p->setInput($expected); + $p->run(); + + $this->assertEquals($expectedLength, strlen($p->getOutput())); + $this->assertEquals($expectedLength, strlen($p->getErrorOutput())); + } + + /** + * @dataProvider pipesCodeProvider + */ + public function testSetStreamAsInput($code, $size) + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestIncomplete('This test fails with a timeout on Windows, can someone investigate please?'); + } + $expected = str_repeat(str_repeat('*', 1024), $size).'!'; + $expectedLength = (1024 * $size) + 1; + + $stream = fopen('php://temporary', 'w+'); + fwrite($stream, $expected); + rewind($stream); + + $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code)), null, null, null, 5); + $p->setInput($stream); + $p->run(); + + fclose($stream); + + $this->assertEquals($expectedLength, strlen($p->getOutput())); + $this->assertEquals($expectedLength, strlen($p->getErrorOutput())); + } + + public function testSetInputWhileRunningThrowsAnException() + { + $process = $this->getProcess(self::$phpBin.' -r "usleep(500000);"'); + $process->start(); + try { + $process->setInput('foobar'); + $process->stop(); + $this->fail('A LogicException should have been raised.'); + } catch (LogicException $e) { + $this->assertEquals('Input can not be set while the process is running.', $e->getMessage()); + } + $process->stop(); + } + + /** + * @dataProvider provideInvalidInputValues + * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException + * @expectedExceptionMessage Symfony\Component\Process\Process::setInput only accepts strings or stream resources. + */ + public function testInvalidInput($value) + { + $process = $this->getProcess(self::$phpBin.' -v'); + $process->setInput($value); + } + + public function provideInvalidInputValues() + { + return array( + array(array()), + array(new NonStringifiable()), + ); + } + + /** + * @dataProvider provideInputValues + */ + public function testValidInput($expected, $value) + { + $process = $this->getProcess(self::$phpBin.' -v'); + $process->setInput($value); + $this->assertSame($expected, $process->getInput()); + } + + public function provideInputValues() + { + return array( + array(null, null), + array('24.5', 24.5), + array('input data', 'input data'), + ); + } + + /** + * @dataProvider provideLegacyInputValues + * @group legacy + */ + public function testLegacyValidInput($expected, $value) + { + $process = $this->getProcess(self::$phpBin.' -v'); + $process->setInput($value); + $this->assertSame($expected, $process->getInput()); + } + + public function provideLegacyInputValues() + { + return array( + array('stringifiable', new Stringifiable()), + ); + } + + public function chainedCommandsOutputProvider() + { + if ('\\' === DIRECTORY_SEPARATOR) { + return array( + array("2 \r\n2\r\n", '&&', '2'), + ); + } + + return array( + array("1\n1\n", ';', '1'), + array("2\n2\n", '&&', '2'), + ); + } + + /** + * @dataProvider chainedCommandsOutputProvider + */ + public function testChainedCommandsOutput($expected, $operator, $input) + { + $process = $this->getProcess(sprintf('echo %s %s echo %s', $input, $operator, $input)); + $process->run(); + $this->assertEquals($expected, $process->getOutput()); + } + + public function testCallbackIsExecutedForOutput() + { + $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('echo \'foo\';'))); + + $called = false; + $p->run(function ($type, $buffer) use (&$called) { + $called = $buffer === 'foo'; + }); + + $this->assertTrue($called, 'The callback should be executed with the output'); + } + + public function testGetErrorOutput() + { + $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }'))); + + $p->run(); + $this->assertEquals(3, preg_match_all('/ERROR/', $p->getErrorOutput(), $matches)); + } + + public function testGetIncrementalErrorOutput() + { + // use a lock file to toggle between writing ("W") and reading ("R") the + // error stream + $lock = tempnam(sys_get_temp_dir(), get_class($this).'Lock'); + file_put_contents($lock, 'W'); + + $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { if (\'W\' === file_get_contents('.var_export($lock, true).')) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; file_put_contents('.var_export($lock, true).', \'R\'); } usleep(100); }'))); + + $p->start(); + while ($p->isRunning()) { + if ('R' === file_get_contents($lock)) { + $this->assertLessThanOrEqual(1, preg_match_all('/ERROR/', $p->getIncrementalErrorOutput(), $matches)); + file_put_contents($lock, 'W'); + } + usleep(100); + } + + unlink($lock); + } + + public function testFlushErrorOutput() + { + $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }'))); + + $p->run(); + $p->clearErrorOutput(); + $this->assertEmpty($p->getErrorOutput()); + } + + public function testGetEmptyIncrementalErrorOutput() + { + // use a lock file to toggle between writing ("W") and reading ("R") the + // output stream + $lock = tempnam(sys_get_temp_dir(), get_class($this).'Lock'); + file_put_contents($lock, 'W'); + + $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { if (\'W\' === file_get_contents('.var_export($lock, true).')) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; file_put_contents('.var_export($lock, true).', \'R\'); } usleep(100); }'))); + + $p->start(); + + $shouldWrite = false; + + while ($p->isRunning()) { + if ('R' === file_get_contents($lock)) { + if (!$shouldWrite) { + $this->assertLessThanOrEqual(1, preg_match_all('/ERROR/', $p->getIncrementalOutput(), $matches)); + $shouldWrite = true; + } else { + $this->assertSame('', $p->getIncrementalOutput()); + + file_put_contents($lock, 'W'); + $shouldWrite = false; + } + } + usleep(100); + } + + unlink($lock); + } + + public function testGetOutput() + { + $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { echo \' foo \'; $n++; }'))); + + $p->run(); + $this->assertEquals(3, preg_match_all('/foo/', $p->getOutput(), $matches)); + } + + public function testGetIncrementalOutput() + { + // use a lock file to toggle between writing ("W") and reading ("R") the + // output stream + $lock = tempnam(sys_get_temp_dir(), get_class($this).'Lock'); + file_put_contents($lock, 'W'); + + $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { if (\'W\' === file_get_contents('.var_export($lock, true).')) { echo \' foo \'; $n++; file_put_contents('.var_export($lock, true).', \'R\'); } usleep(100); }'))); + + $p->start(); + while ($p->isRunning()) { + if ('R' === file_get_contents($lock)) { + $this->assertLessThanOrEqual(1, preg_match_all('/foo/', $p->getIncrementalOutput(), $matches)); + file_put_contents($lock, 'W'); + } + usleep(100); + } + + unlink($lock); + } + + public function testFlushOutput() + { + $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n=0;while ($n<3) {echo \' foo \';$n++;}'))); + + $p->run(); + $p->clearOutput(); + $this->assertEmpty($p->getOutput()); + } + + public function testGetEmptyIncrementalOutput() + { + // use a lock file to toggle between writing ("W") and reading ("R") the + // output stream + $lock = tempnam(sys_get_temp_dir(), get_class($this).'Lock'); + file_put_contents($lock, 'W'); + + $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { if (\'W\' === file_get_contents('.var_export($lock, true).')) { echo \' foo \'; $n++; file_put_contents('.var_export($lock, true).', \'R\'); } usleep(100); }'))); + + $p->start(); + + $shouldWrite = false; + + while ($p->isRunning()) { + if ('R' === file_get_contents($lock)) { + if (!$shouldWrite) { + $this->assertLessThanOrEqual(1, preg_match_all('/foo/', $p->getIncrementalOutput(), $matches)); + $shouldWrite = true; + } else { + $this->assertSame('', $p->getIncrementalOutput()); + + file_put_contents($lock, 'W'); + $shouldWrite = false; + } + } + usleep(100); + } + + unlink($lock); + } + + public function testZeroAsOutput() + { + if ('\\' === DIRECTORY_SEPARATOR) { + // see http://stackoverflow.com/questions/7105433/windows-batch-echo-without-new-line + $p = $this->getProcess('echo | set /p dummyName=0'); + } else { + $p = $this->getProcess('printf 0'); + } + + $p->run(); + $this->assertSame('0', $p->getOutput()); + } + + public function testExitCodeCommandFailed() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does not support POSIX exit code'); + } + + // such command run in bash return an exitcode 127 + $process = $this->getProcess('nonexistingcommandIhopeneversomeonewouldnameacommandlikethis'); + $process->run(); + + $this->assertGreaterThan(0, $process->getExitCode()); + } + + public function testTTYCommand() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does have /dev/tty support'); + } + + $process = $this->getProcess('echo "foo" >> /dev/null && '.self::$phpBin.' -r "usleep(100000);"'); + $process->setTty(true); + $process->start(); + $this->assertTrue($process->isRunning()); + $process->wait(); + + $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus()); + } + + public function testTTYCommandExitCode() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does have /dev/tty support'); + } + + $process = $this->getProcess('echo "foo" >> /dev/null'); + $process->setTty(true); + $process->run(); + + $this->assertTrue($process->isSuccessful()); + } + + public function testTTYInWindowsEnvironment() + { + if ('\\' !== DIRECTORY_SEPARATOR) { + $this->markTestSkipped('This test is for Windows platform only'); + } + + $process = $this->getProcess('echo "foo" >> /dev/null'); + $process->setTty(false); + $this->setExpectedException('Symfony\Component\Process\Exception\RuntimeException', 'TTY mode is not supported on Windows platform.'); + $process->setTty(true); + } + + public function testExitCodeTextIsNullWhenExitCodeIsNull() + { + $process = $this->getProcess(''); + $this->assertNull($process->getExitCodeText()); + } + + public function testPTYCommand() + { + if (!Process::isPtySupported()) { + $this->markTestSkipped('PTY is not supported on this operating system.'); + } + + $process = $this->getProcess('echo "foo"'); + $process->setPty(true); + $process->run(); + + $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus()); + $this->assertEquals("foo\r\n", $process->getOutput()); + } + + public function testMustRun() + { + $process = $this->getProcess('echo foo'); + + $this->assertSame($process, $process->mustRun()); + $this->assertEquals('foo'.PHP_EOL, $process->getOutput()); + } + + public function testSuccessfulMustRunHasCorrectExitCode() + { + $process = $this->getProcess('echo foo')->mustRun(); + $this->assertEquals(0, $process->getExitCode()); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\ProcessFailedException + */ + public function testMustRunThrowsException() + { + $process = $this->getProcess('exit 1'); + $process->mustRun(); + } + + public function testExitCodeText() + { + $process = $this->getProcess(''); + $r = new \ReflectionObject($process); + $p = $r->getProperty('exitcode'); + $p->setAccessible(true); + + $p->setValue($process, 2); + $this->assertEquals('Misuse of shell builtins', $process->getExitCodeText()); + } + + public function testStartIsNonBlocking() + { + $process = $this->getProcess(self::$phpBin.' -r "usleep(500000);"'); + $start = microtime(true); + $process->start(); + $end = microtime(true); + $this->assertLessThan(0.4, $end - $start); + $process->wait(); + } + + public function testUpdateStatus() + { + $process = $this->getProcess(self::$phpBin.' -v'); + $process->run(); + $this->assertTrue(strlen($process->getOutput()) > 0); + } + + public function testGetExitCodeIsNullOnStart() + { + $process = $this->getProcess(self::$phpBin.' -r "usleep(200000);"'); + $this->assertNull($process->getExitCode()); + $process->start(); + $this->assertNull($process->getExitCode()); + $process->wait(); + $this->assertEquals(0, $process->getExitCode()); + } + + public function testGetExitCodeIsNullOnWhenStartingAgain() + { + $process = $this->getProcess(self::$phpBin.' -r "usleep(200000);"'); + $process->run(); + $this->assertEquals(0, $process->getExitCode()); + $process->start(); + $this->assertNull($process->getExitCode()); + $process->wait(); + $this->assertEquals(0, $process->getExitCode()); + } + + public function testGetExitCode() + { + $process = $this->getProcess(self::$phpBin.' -v'); + $process->run(); + $this->assertSame(0, $process->getExitCode()); + } + + public function testStatus() + { + $process = $this->getProcess(self::$phpBin.' -r "usleep(500000);"'); + $this->assertFalse($process->isRunning()); + $this->assertFalse($process->isStarted()); + $this->assertFalse($process->isTerminated()); + $this->assertSame(Process::STATUS_READY, $process->getStatus()); + $process->start(); + $this->assertTrue($process->isRunning()); + $this->assertTrue($process->isStarted()); + $this->assertFalse($process->isTerminated()); + $this->assertSame(Process::STATUS_STARTED, $process->getStatus()); + $process->wait(); + $this->assertFalse($process->isRunning()); + $this->assertTrue($process->isStarted()); + $this->assertTrue($process->isTerminated()); + $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus()); + } + + public function testStop() + { + $process = $this->getProcess(self::$phpBin.' -r "sleep(4);"'); + $process->start(); + $this->assertTrue($process->isRunning()); + $process->stop(); + $this->assertFalse($process->isRunning()); + } + + public function testIsSuccessful() + { + $process = $this->getProcess(self::$phpBin.' -v'); + $process->run(); + $this->assertTrue($process->isSuccessful()); + } + + public function testIsSuccessfulOnlyAfterTerminated() + { + $process = $this->getProcess(self::$phpBin.' -r "sleep(1);"'); + $process->start(); + + $this->assertFalse($process->isSuccessful()); + + while ($process->isRunning()) { + usleep(300000); + } + + $this->assertTrue($process->isSuccessful()); + } + + public function testIsNotSuccessful() + { + $process = $this->getProcess(self::$phpBin.' -r "usleep(500000);throw new \Exception(\'BOUM\');"'); + $process->start(); + $this->assertTrue($process->isRunning()); + $process->wait(); + $this->assertFalse($process->isSuccessful()); + } + + public function testProcessIsNotSignaled() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does not support POSIX signals'); + } + + $process = $this->getProcess(self::$phpBin.' -v'); + $process->run(); + $this->assertFalse($process->hasBeenSignaled()); + } + + public function testProcessWithoutTermSignalIsNotSignaled() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does not support POSIX signals'); + } + + $process = $this->getProcess(self::$phpBin.' -v'); + $process->run(); + $this->assertFalse($process->hasBeenSignaled()); + } + + public function testProcessWithoutTermSignal() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does not support POSIX signals'); + } + + $process = $this->getProcess(self::$phpBin.' -v'); + $process->run(); + $this->assertEquals(0, $process->getTermSignal()); + } + + public function testProcessIsSignaledIfStopped() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does not support POSIX signals'); + } + + $process = $this->getProcess(self::$phpBin.' -r "sleep(4);"'); + $process->start(); + $process->stop(); + $this->assertTrue($process->hasBeenSignaled()); + } + + public function testProcessWithTermSignal() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does not support POSIX signals'); + } + + // SIGTERM is only defined if pcntl extension is present + $termSignal = defined('SIGTERM') ? SIGTERM : 15; + + $process = $this->getProcess(self::$phpBin.' -r "sleep(4);"'); + $process->start(); + $process->stop(); + + $this->assertEquals($termSignal, $process->getTermSignal()); + } + + public function testProcessThrowsExceptionWhenExternallySignaled() + { + if (!function_exists('posix_kill')) { + $this->markTestSkipped('Function posix_kill is required.'); + } + + $termSignal = defined('SIGKILL') ? SIGKILL : 9; + + $process = $this->getProcess('exec '.self::$phpBin.' -r "while (true) {}"'); + $process->start(); + posix_kill($process->getPid(), $termSignal); + + $this->setExpectedException('Symfony\Component\Process\Exception\RuntimeException', 'The process has been signaled with signal "9".'); + $process->wait(); + } + + public function testRestart() + { + $process1 = $this->getProcess(self::$phpBin.' -r "echo getmypid();"'); + $process1->run(); + $process2 = $process1->restart(); + + $process2->wait(); // wait for output + + // Ensure that both processed finished and the output is numeric + $this->assertFalse($process1->isRunning()); + $this->assertFalse($process2->isRunning()); + $this->assertTrue(is_numeric($process1->getOutput())); + $this->assertTrue(is_numeric($process2->getOutput())); + + // Ensure that restart returned a new process by check that the output is different + $this->assertNotEquals($process1->getOutput(), $process2->getOutput()); + } + + public function testRunProcessWithTimeout() + { + $timeout = 0.5; + $process = $this->getProcess(self::$phpBin.' -r "usleep(600000);"'); + $process->setTimeout($timeout); + $start = microtime(true); + try { + $process->run(); + $this->fail('A RuntimeException should have been raised'); + } catch (RuntimeException $e) { + } + $duration = microtime(true) - $start; + + if ('\\' === DIRECTORY_SEPARATOR) { + // Windows is a bit slower as it read file handles, then allow twice the precision + $maxDuration = $timeout + 2 * Process::TIMEOUT_PRECISION; + } else { + $maxDuration = $timeout + Process::TIMEOUT_PRECISION; + } + + $this->assertLessThan($maxDuration, $duration); + } + + public function testCheckTimeoutOnNonStartedProcess() + { + $process = $this->getProcess(self::$phpBin.' -r "sleep(3);"'); + $process->checkTimeout(); + } + + public function testCheckTimeoutOnTerminatedProcess() + { + $process = $this->getProcess(self::$phpBin.' -v'); + $process->run(); + $process->checkTimeout(); + } + + public function testCheckTimeoutOnStartedProcess() + { + $timeout = 0.5; + $precision = 100000; + $process = $this->getProcess(self::$phpBin.' -r "sleep(3);"'); + $process->setTimeout($timeout); + $start = microtime(true); + + $process->start(); + + try { + while ($process->isRunning()) { + $process->checkTimeout(); + usleep($precision); + } + $this->fail('A RuntimeException should have been raised'); + } catch (RuntimeException $e) { + } + $duration = microtime(true) - $start; + + $this->assertLessThan($timeout + $precision, $duration); + $this->assertFalse($process->isSuccessful()); + } + + public function testIdleTimeout() + { + $process = $this->getProcess(self::$phpBin.' -r "sleep(3);"'); + $process->setTimeout(10); + $process->setIdleTimeout(0.5); + + try { + $process->run(); + + $this->fail('A timeout exception was expected.'); + } catch (ProcessTimedOutException $ex) { + $this->assertTrue($ex->isIdleTimeout()); + $this->assertFalse($ex->isGeneralTimeout()); + $this->assertEquals(0.5, $ex->getExceededTimeout()); + } + } + + public function testIdleTimeoutNotExceededWhenOutputIsSent() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestIncomplete('This test fails with a timeout on Windows, can someone investigate please?'); + } + $process = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 30; while ($n--) {echo "foo\n"; usleep(100000); }'))); + $process->setTimeout(2); + $process->setIdleTimeout(1); + + try { + $process->run(); + $this->fail('A timeout exception was expected.'); + } catch (ProcessTimedOutException $ex) { + $this->assertTrue($ex->isGeneralTimeout(), 'A general timeout is expected.'); + $this->assertFalse($ex->isIdleTimeout(), 'No idle timeout is expected.'); + $this->assertEquals(2, $ex->getExceededTimeout()); + } + } + + public function testStartAfterATimeout() + { + $process = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 1000; while ($n--) {echo \'\'; usleep(1000); }'))); + $process->setTimeout(0.1); + + try { + $process->run(); + $this->fail('A RuntimeException should have been raised.'); + } catch (RuntimeException $e) { + } + $process->start(); + usleep(1000); + $process->stop(); + } + + public function testGetPid() + { + $process = $this->getProcess(self::$phpBin.' -r "usleep(500000);"'); + $process->start(); + $this->assertGreaterThan(0, $process->getPid()); + $process->wait(); + } + + public function testGetPidIsNullBeforeStart() + { + $process = $this->getProcess(self::$phpBin.' -r "sleep(1);"'); + $this->assertNull($process->getPid()); + } + + public function testGetPidIsNullAfterRun() + { + $process = $this->getProcess(self::$phpBin.' -v'); + $process->run(); + $this->assertNull($process->getPid()); + } + + public function testSignal() + { + if (!extension_loaded('pcntl')) { + $this->markTestSkipped('Extension pcntl is required.'); + } + + $process = $this->getProcess('exec php -f '.__DIR__.'/SignalListener.php'); + $process->start(); + usleep(500000); + $process->signal(SIGUSR1); + + while ($process->isRunning() && false === strpos($process->getOutput(), 'Caught SIGUSR1')) { + usleep(10000); + } + + $this->assertEquals('Caught SIGUSR1', $process->getOutput()); + } + + public function testExitCodeIsAvailableAfterSignal() + { + if (!extension_loaded('pcntl')) { + $this->markTestSkipped('Extension pcntl is required.'); + } + + $process = $this->getProcess('sleep 4'); + $process->start(); + $process->signal(SIGKILL); + + while ($process->isRunning()) { + usleep(10000); + } + + $this->assertFalse($process->isRunning()); + $this->assertTrue($process->hasBeenSignaled()); + $this->assertFalse($process->isSuccessful()); + $this->assertEquals(137, $process->getExitCode()); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\LogicException + */ + public function testSignalProcessNotRunning() + { + if (!extension_loaded('pcntl')) { + $this->markTestSkipped('Extension pcntl is required.'); + } + + $process = $this->getProcess(self::$phpBin.' -v'); + $process->signal(SIGHUP); + } + + /** + * @dataProvider provideMethodsThatNeedARunningProcess + */ + public function testMethodsThatNeedARunningProcess($method) + { + $process = $this->getProcess(self::$phpBin.' -v'); + $this->setExpectedException('Symfony\Component\Process\Exception\LogicException', sprintf('Process must be started before calling %s.', $method)); + $process->{$method}(); + } + + public function provideMethodsThatNeedARunningProcess() + { + return array( + array('getOutput'), + array('getIncrementalOutput'), + array('getErrorOutput'), + array('getIncrementalErrorOutput'), + array('wait'), + ); + } + + /** + * @dataProvider provideMethodsThatNeedATerminatedProcess + */ + public function testMethodsThatNeedATerminatedProcess($method) + { + $process = $this->getProcess(self::$phpBin.' -r "sleep(1);"'); + $process->start(); + try { + $process->{$method}(); + $process->stop(0); + $this->fail('A LogicException must have been thrown'); + } catch (\Exception $e) { + $this->assertInstanceOf('Symfony\Component\Process\Exception\LogicException', $e); + $this->assertEquals(sprintf('Process must be terminated before calling %s.', $method), $e->getMessage()); + } + $process->stop(0); + } + + public function provideMethodsThatNeedATerminatedProcess() + { + return array( + array('hasBeenSignaled'), + array('getTermSignal'), + array('hasBeenStopped'), + array('getStopSignal'), + ); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + */ + public function testSignalWithWrongIntSignal() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('POSIX signals do not work on Windows'); + } + + $process = $this->getProcess(self::$phpBin.' -r "sleep(3);"'); + $process->start(); + $process->signal(-4); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + */ + public function testSignalWithWrongNonIntSignal() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('POSIX signals do not work on Windows'); + } + + $process = $this->getProcess(self::$phpBin.' -r "sleep(3);"'); + $process->start(); + $process->signal('Céphalopodes'); + } + + public function testDisableOutputDisablesTheOutput() + { + $p = $this->getProcess(self::$phpBin.' -r "usleep(500000);"'); + $this->assertFalse($p->isOutputDisabled()); + $p->disableOutput(); + $this->assertTrue($p->isOutputDisabled()); + $p->enableOutput(); + $this->assertFalse($p->isOutputDisabled()); + } + + public function testDisableOutputWhileRunningThrowsException() + { + $p = $this->getProcess(self::$phpBin.' -r "usleep(500000);"'); + $p->start(); + $this->setExpectedException('Symfony\Component\Process\Exception\RuntimeException', 'Disabling output while the process is running is not possible.'); + $p->disableOutput(); + } + + public function testEnableOutputWhileRunningThrowsException() + { + $p = $this->getProcess(self::$phpBin.' -r "usleep(500000);"'); + $p->disableOutput(); + $p->start(); + $this->setExpectedException('Symfony\Component\Process\Exception\RuntimeException', 'Enabling output while the process is running is not possible.'); + $p->enableOutput(); + } + + public function testEnableOrDisableOutputAfterRunDoesNotThrowException() + { + $p = $this->getProcess(self::$phpBin.' -r "usleep(500000);"'); + $p->disableOutput(); + $p->start(); + $p->wait(); + $p->enableOutput(); + $p->disableOutput(); + } + + public function testDisableOutputWhileIdleTimeoutIsSet() + { + $process = $this->getProcess('sleep 3'); + $process->setIdleTimeout(1); + $this->setExpectedException('Symfony\Component\Process\Exception\LogicException', 'Output can not be disabled while an idle timeout is set.'); + $process->disableOutput(); + } + + public function testSetIdleTimeoutWhileOutputIsDisabled() + { + $process = $this->getProcess('sleep 3'); + $process->disableOutput(); + $this->setExpectedException('Symfony\Component\Process\Exception\LogicException', 'Idle timeout can not be set while the output is disabled.'); + $process->setIdleTimeout(1); + } + + public function testSetNullIdleTimeoutWhileOutputIsDisabled() + { + $process = $this->getProcess('sleep 3'); + $process->disableOutput(); + $process->setIdleTimeout(null); + } + + /** + * @dataProvider provideStartMethods + */ + public function testStartWithACallbackAndDisabledOutput($startMethod, $exception, $exceptionMessage) + { + $p = $this->getProcess(self::$phpBin.' -r "usleep(500000);"'); + $p->disableOutput(); + $this->setExpectedException($exception, $exceptionMessage); + $p->{$startMethod}(function () {}); + } + + public function provideStartMethods() + { + return array( + array('start', 'Symfony\Component\Process\Exception\LogicException', 'Output has been disabled, enable it to allow the use of a callback.'), + array('run', 'Symfony\Component\Process\Exception\LogicException', 'Output has been disabled, enable it to allow the use of a callback.'), + array('mustRun', 'Symfony\Component\Process\Exception\LogicException', 'Output has been disabled, enable it to allow the use of a callback.'), + ); + } + + /** + * @dataProvider provideOutputFetchingMethods + */ + public function testGetOutputWhileDisabled($fetchMethod) + { + $p = $this->getProcess(self::$phpBin.' -r "usleep(500000);"'); + $p->disableOutput(); + $p->start(); + $this->setExpectedException('Symfony\Component\Process\Exception\LogicException', 'Output has been disabled.'); + $p->{$fetchMethod}(); + } + + public function provideOutputFetchingMethods() + { + return array( + array('getOutput'), + array('getIncrementalOutput'), + array('getErrorOutput'), + array('getIncrementalErrorOutput'), + ); + } + + public function responsesCodeProvider() + { + return array( + //expected output / getter / code to execute + //array(1,'getExitCode','exit(1);'), + //array(true,'isSuccessful','exit();'), + array('output', 'getOutput', 'echo \'output\';'), + ); + } + + public function pipesCodeProvider() + { + $variations = array( + 'fwrite(STDOUT, $in = file_get_contents(\'php://stdin\')); fwrite(STDERR, $in);', + 'include \''.__DIR__.'/PipeStdinInStdoutStdErrStreamSelect.php\';', + ); + + if ('\\' === DIRECTORY_SEPARATOR) { + // Avoid XL buffers on Windows because of https://bugs.php.net/bug.php?id=65650 + $sizes = array(1, 2, 4, 8); + } else { + $sizes = array(1, 16, 64, 1024, 4096); + } + + $codes = array(); + foreach ($sizes as $size) { + foreach ($variations as $code) { + $codes[] = array($code, $size); + } + } + + return $codes; + } + + /** + * provides default method names for simple getter/setter. + */ + public function methodProvider() + { + $defaults = array( + array('CommandLine'), + array('Timeout'), + array('WorkingDirectory'), + array('Env'), + array('Stdin'), + array('Input'), + array('Options'), + ); + + return $defaults; + } + + /** + * @param string $commandline + * @param null|string $cwd + * @param null|array $env + * @param null|string $input + * @param int $timeout + * @param array $options + * + * @return Process + */ + abstract protected function getProcess($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = array()); +} + +class Stringifiable +{ + public function __toString() + { + return 'stringifiable'; + } +} + +class NonStringifiable +{ +} diff --git a/library/symfony/process/Tests/ExecutableFinderTest.php b/library/symfony/process/Tests/ExecutableFinderTest.php new file mode 100644 index 000000000..812429e88 --- /dev/null +++ b/library/symfony/process/Tests/ExecutableFinderTest.php @@ -0,0 +1,144 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use Symfony\Component\Process\ExecutableFinder; + +/** + * @author Chris Smith <chris@cs278.org> + */ +class ExecutableFinderTest extends \PHPUnit_Framework_TestCase +{ + private $path; + + protected function tearDown() + { + if ($this->path) { + // Restore path if it was changed. + putenv('PATH='.$this->path); + } + } + + private function setPath($path) + { + $this->path = getenv('PATH'); + putenv('PATH='.$path); + } + + /** + * @requires PHP 5.4 + */ + public function testFind() + { + if (ini_get('open_basedir')) { + $this->markTestSkipped('Cannot test when open_basedir is set'); + } + + $this->setPath(dirname(PHP_BINARY)); + + $finder = new ExecutableFinder(); + $result = $finder->find($this->getPhpBinaryName()); + + $this->assertSamePath(PHP_BINARY, $result); + } + + public function testFindWithDefault() + { + if (ini_get('open_basedir')) { + $this->markTestSkipped('Cannot test when open_basedir is set'); + } + + $expected = 'defaultValue'; + + $this->setPath(''); + + $finder = new ExecutableFinder(); + $result = $finder->find('foo', $expected); + + $this->assertEquals($expected, $result); + } + + /** + * @requires PHP 5.4 + */ + public function testFindWithExtraDirs() + { + if (ini_get('open_basedir')) { + $this->markTestSkipped('Cannot test when open_basedir is set'); + } + + $this->setPath(''); + + $extraDirs = array(dirname(PHP_BINARY)); + + $finder = new ExecutableFinder(); + $result = $finder->find($this->getPhpBinaryName(), null, $extraDirs); + + $this->assertSamePath(PHP_BINARY, $result); + } + + /** + * @requires PHP 5.4 + */ + public function testFindWithOpenBaseDir() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Cannot run test on windows'); + } + + if (ini_get('open_basedir')) { + $this->markTestSkipped('Cannot test when open_basedir is set'); + } + + $this->iniSet('open_basedir', dirname(PHP_BINARY).(!defined('HHVM_VERSION') ? PATH_SEPARATOR.'/' : '')); + + $finder = new ExecutableFinder(); + $result = $finder->find($this->getPhpBinaryName()); + + $this->assertSamePath(PHP_BINARY, $result); + } + + /** + * @requires PHP 5.4 + */ + public function testFindProcessInOpenBasedir() + { + if (ini_get('open_basedir')) { + $this->markTestSkipped('Cannot test when open_basedir is set'); + } + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Cannot run test on windows'); + } + + $this->setPath(''); + $this->iniSet('open_basedir', PHP_BINARY.(!defined('HHVM_VERSION') ? PATH_SEPARATOR.'/' : '')); + + $finder = new ExecutableFinder(); + $result = $finder->find($this->getPhpBinaryName(), false); + + $this->assertSamePath(PHP_BINARY, $result); + } + + private function assertSamePath($expected, $tested) + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->assertEquals(strtolower($expected), strtolower($tested)); + } else { + $this->assertEquals($expected, $tested); + } + } + + private function getPhpBinaryName() + { + return basename(PHP_BINARY, '\\' === DIRECTORY_SEPARATOR ? '.exe' : ''); + } +} diff --git a/library/symfony/process/Tests/NonStopableProcess.php b/library/symfony/process/Tests/NonStopableProcess.php new file mode 100644 index 000000000..54510c16a --- /dev/null +++ b/library/symfony/process/Tests/NonStopableProcess.php @@ -0,0 +1,45 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Runs a PHP script that can be stopped only with a SIGKILL (9) signal for 3 seconds. + * + * @args duration Run this script with a custom duration + * + * @example `php NonStopableProcess.php 42` will run the script for 42 seconds + */ +function handleSignal($signal) +{ + switch ($signal) { + case SIGTERM: + $name = 'SIGTERM'; + break; + case SIGINT: + $name = 'SIGINT'; + break; + default: + $name = $signal.' (unknown)'; + break; + } + + echo "received signal $name\n"; +} + +declare (ticks = 1); +pcntl_signal(SIGTERM, 'handleSignal'); +pcntl_signal(SIGINT, 'handleSignal'); + +$duration = isset($argv[1]) ? (int) $argv[1] : 3; +$start = microtime(true); + +while ($duration > (microtime(true) - $start)) { + usleep(1000); +} diff --git a/library/symfony/process/Tests/PhpExecutableFinderTest.php b/library/symfony/process/Tests/PhpExecutableFinderTest.php new file mode 100644 index 000000000..87d0efe9e --- /dev/null +++ b/library/symfony/process/Tests/PhpExecutableFinderTest.php @@ -0,0 +1,119 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use Symfony\Component\Process\PhpExecutableFinder; + +/** + * @author Robert Schönthal <seroscho@googlemail.com> + */ +class PhpExecutableFinderTest extends \PHPUnit_Framework_TestCase +{ + /** + * tests find() with the env var PHP_PATH. + */ + public function testFindWithPhpPath() + { + if (defined('PHP_BINARY')) { + $this->markTestSkipped('The PHP binary is easily available as of PHP 5.4'); + } + + $f = new PhpExecutableFinder(); + + $current = $f->find(); + + //not executable PHP_PATH + putenv('PHP_PATH=/not/executable/php'); + $this->assertFalse($f->find(), '::find() returns false for not executable PHP'); + $this->assertFalse($f->find(false), '::find() returns false for not executable PHP'); + + //executable PHP_PATH + putenv('PHP_PATH='.$current); + $this->assertEquals($f->find(), $current, '::find() returns the executable PHP'); + $this->assertEquals($f->find(false), $current, '::find() returns the executable PHP'); + } + + /** + * tests find() with the constant PHP_BINARY. + * + * @requires PHP 5.4 + */ + public function testFind() + { + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Should not be executed in HHVM context.'); + } + + $f = new PhpExecutableFinder(); + + $current = PHP_BINARY; + $args = 'phpdbg' === PHP_SAPI ? ' -qrr' : ''; + + $this->assertEquals($current.$args, $f->find(), '::find() returns the executable PHP'); + $this->assertEquals($current, $f->find(false), '::find() returns the executable PHP'); + } + + /** + * tests find() with the env var / constant PHP_BINARY with HHVM. + */ + public function testFindWithHHVM() + { + if (!defined('HHVM_VERSION')) { + $this->markTestSkipped('Should be executed in HHVM context.'); + } + + $f = new PhpExecutableFinder(); + + $current = getenv('PHP_BINARY') ?: PHP_BINARY; + + $this->assertEquals($current.' --php', $f->find(), '::find() returns the executable PHP'); + $this->assertEquals($current, $f->find(false), '::find() returns the executable PHP'); + } + + /** + * tests find() with the env var PHP_PATH. + */ + public function testFindArguments() + { + $f = new PhpExecutableFinder(); + + if (defined('HHVM_VERSION')) { + $this->assertEquals($f->findArguments(), array('--php'), '::findArguments() returns HHVM arguments'); + } elseif ('phpdbg' === PHP_SAPI) { + $this->assertEquals($f->findArguments(), array('-qrr'), '::findArguments() returns phpdbg arguments'); + } else { + $this->assertEquals($f->findArguments(), array(), '::findArguments() returns no arguments'); + } + } + + /** + * tests find() with default executable. + */ + public function testFindWithSuffix() + { + if (defined('PHP_BINARY')) { + $this->markTestSkipped('The PHP binary is easily available as of PHP 5.4'); + } + + putenv('PHP_PATH='); + putenv('PHP_PEAR_PHP_BIN='); + $f = new PhpExecutableFinder(); + + $current = $f->find(); + + //TODO maybe php executable is custom or even Windows + if ('\\' === DIRECTORY_SEPARATOR) { + $this->assertTrue(is_executable($current)); + $this->assertTrue((bool) preg_match('/'.addslashes(DIRECTORY_SEPARATOR).'php\.(exe|bat|cmd|com)$/i', $current), '::find() returns the executable PHP with suffixes'); + } + } +} diff --git a/library/symfony/process/Tests/PhpProcessTest.php b/library/symfony/process/Tests/PhpProcessTest.php new file mode 100644 index 000000000..2cf79aa1a --- /dev/null +++ b/library/symfony/process/Tests/PhpProcessTest.php @@ -0,0 +1,53 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use Symfony\Component\Process\PhpExecutableFinder; +use Symfony\Component\Process\PhpProcess; + +class PhpProcessTest extends \PHPUnit_Framework_TestCase +{ + public function testNonBlockingWorks() + { + $expected = 'hello world!'; + $process = new PhpProcess(<<<PHP +<?php echo '$expected'; +PHP + ); + $process->start(); + $process->wait(); + $this->assertEquals($expected, $process->getOutput()); + } + + public function testCommandLine() + { + if ('phpdbg' === PHP_SAPI) { + $this->markTestSkipped('phpdbg SAPI is not supported by this test.'); + } + + $process = new PhpProcess(<<<PHP +<?php echo 'foobar'; +PHP + ); + + $f = new PhpExecutableFinder(); + $commandLine = $f->find(); + + $this->assertSame($commandLine, $process->getCommandLine(), '::getCommandLine() returns the command line of PHP before start'); + + $process->start(); + $this->assertSame($commandLine, $process->getCommandLine(), '::getCommandLine() returns the command line of PHP after start'); + + $process->wait(); + $this->assertSame($commandLine, $process->getCommandLine(), '::getCommandLine() returns the command line of PHP after wait'); + } +} diff --git a/library/symfony/process/Tests/PipeStdinInStdoutStdErrStreamSelect.php b/library/symfony/process/Tests/PipeStdinInStdoutStdErrStreamSelect.php new file mode 100644 index 000000000..bbd7ddfeb --- /dev/null +++ b/library/symfony/process/Tests/PipeStdinInStdoutStdErrStreamSelect.php @@ -0,0 +1,72 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +define('ERR_SELECT_FAILED', 1); +define('ERR_TIMEOUT', 2); +define('ERR_READ_FAILED', 3); +define('ERR_WRITE_FAILED', 4); + +$read = array(STDIN); +$write = array(STDOUT, STDERR); + +stream_set_blocking(STDIN, 0); +stream_set_blocking(STDOUT, 0); +stream_set_blocking(STDERR, 0); + +$out = $err = ''; +while ($read || $write) { + $r = $read; + $w = $write; + $e = null; + $n = stream_select($r, $w, $e, 5); + + if (false === $n) { + die(ERR_SELECT_FAILED); + } elseif ($n < 1) { + die(ERR_TIMEOUT); + } + + if (in_array(STDOUT, $w) && strlen($out) > 0) { + $written = fwrite(STDOUT, (binary) $out, 32768); + if (false === $written) { + die(ERR_WRITE_FAILED); + } + $out = (binary) substr($out, $written); + } + if (null === $read && '' === $out) { + $write = array_diff($write, array(STDOUT)); + } + + if (in_array(STDERR, $w) && strlen($err) > 0) { + $written = fwrite(STDERR, (binary) $err, 32768); + if (false === $written) { + die(ERR_WRITE_FAILED); + } + $err = (binary) substr($err, $written); + } + if (null === $read && '' === $err) { + $write = array_diff($write, array(STDERR)); + } + + if ($r) { + $str = fread(STDIN, 32768); + if (false !== $str) { + $out .= $str; + $err .= $str; + } + if (false === $str || feof(STDIN)) { + $read = null; + if (!feof(STDIN)) { + die(ERR_READ_FAILED); + } + } + } +} diff --git a/library/symfony/process/Tests/ProcessBuilderTest.php b/library/symfony/process/Tests/ProcessBuilderTest.php new file mode 100644 index 000000000..1b5056d1b --- /dev/null +++ b/library/symfony/process/Tests/ProcessBuilderTest.php @@ -0,0 +1,225 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use Symfony\Component\Process\ProcessBuilder; + +class ProcessBuilderTest extends \PHPUnit_Framework_TestCase +{ + public function testInheritEnvironmentVars() + { + $_ENV['MY_VAR_1'] = 'foo'; + + $proc = ProcessBuilder::create() + ->add('foo') + ->getProcess(); + + unset($_ENV['MY_VAR_1']); + + $env = $proc->getEnv(); + $this->assertArrayHasKey('MY_VAR_1', $env); + $this->assertEquals('foo', $env['MY_VAR_1']); + } + + public function testAddEnvironmentVariables() + { + $pb = new ProcessBuilder(); + $env = array( + 'foo' => 'bar', + 'foo2' => 'bar2', + ); + $proc = $pb + ->add('command') + ->setEnv('foo', 'bar2') + ->addEnvironmentVariables($env) + ->inheritEnvironmentVariables(false) + ->getProcess() + ; + + $this->assertSame($env, $proc->getEnv()); + } + + public function testProcessShouldInheritAndOverrideEnvironmentVars() + { + $_ENV['MY_VAR_1'] = 'foo'; + + $proc = ProcessBuilder::create() + ->setEnv('MY_VAR_1', 'bar') + ->add('foo') + ->getProcess(); + + unset($_ENV['MY_VAR_1']); + + $env = $proc->getEnv(); + $this->assertArrayHasKey('MY_VAR_1', $env); + $this->assertEquals('bar', $env['MY_VAR_1']); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException + */ + public function testNegativeTimeoutFromSetter() + { + $pb = new ProcessBuilder(); + $pb->setTimeout(-1); + } + + public function testNullTimeout() + { + $pb = new ProcessBuilder(); + $pb->setTimeout(10); + $pb->setTimeout(null); + + $r = new \ReflectionObject($pb); + $p = $r->getProperty('timeout'); + $p->setAccessible(true); + + $this->assertNull($p->getValue($pb)); + } + + public function testShouldSetArguments() + { + $pb = new ProcessBuilder(array('initial')); + $pb->setArguments(array('second')); + + $proc = $pb->getProcess(); + + $this->assertContains('second', $proc->getCommandLine()); + } + + public function testPrefixIsPrependedToAllGeneratedProcess() + { + $pb = new ProcessBuilder(); + $pb->setPrefix('/usr/bin/php'); + + $proc = $pb->setArguments(array('-v'))->getProcess(); + if ('\\' === DIRECTORY_SEPARATOR) { + $this->assertEquals('"/usr/bin/php" "-v"', $proc->getCommandLine()); + } else { + $this->assertEquals("'/usr/bin/php' '-v'", $proc->getCommandLine()); + } + + $proc = $pb->setArguments(array('-i'))->getProcess(); + if ('\\' === DIRECTORY_SEPARATOR) { + $this->assertEquals('"/usr/bin/php" "-i"', $proc->getCommandLine()); + } else { + $this->assertEquals("'/usr/bin/php' '-i'", $proc->getCommandLine()); + } + } + + public function testArrayPrefixesArePrependedToAllGeneratedProcess() + { + $pb = new ProcessBuilder(); + $pb->setPrefix(array('/usr/bin/php', 'composer.phar')); + + $proc = $pb->setArguments(array('-v'))->getProcess(); + if ('\\' === DIRECTORY_SEPARATOR) { + $this->assertEquals('"/usr/bin/php" "composer.phar" "-v"', $proc->getCommandLine()); + } else { + $this->assertEquals("'/usr/bin/php' 'composer.phar' '-v'", $proc->getCommandLine()); + } + + $proc = $pb->setArguments(array('-i'))->getProcess(); + if ('\\' === DIRECTORY_SEPARATOR) { + $this->assertEquals('"/usr/bin/php" "composer.phar" "-i"', $proc->getCommandLine()); + } else { + $this->assertEquals("'/usr/bin/php' 'composer.phar' '-i'", $proc->getCommandLine()); + } + } + + public function testShouldEscapeArguments() + { + $pb = new ProcessBuilder(array('%path%', 'foo " bar', '%baz%baz')); + $proc = $pb->getProcess(); + + if ('\\' === DIRECTORY_SEPARATOR) { + $this->assertSame('^%"path"^% "foo \\" bar" "%baz%baz"', $proc->getCommandLine()); + } else { + $this->assertSame("'%path%' 'foo \" bar' '%baz%baz'", $proc->getCommandLine()); + } + } + + public function testShouldEscapeArgumentsAndPrefix() + { + $pb = new ProcessBuilder(array('arg')); + $pb->setPrefix('%prefix%'); + $proc = $pb->getProcess(); + + if ('\\' === DIRECTORY_SEPARATOR) { + $this->assertSame('^%"prefix"^% "arg"', $proc->getCommandLine()); + } else { + $this->assertSame("'%prefix%' 'arg'", $proc->getCommandLine()); + } + } + + /** + * @expectedException \Symfony\Component\Process\Exception\LogicException + */ + public function testShouldThrowALogicExceptionIfNoPrefixAndNoArgument() + { + ProcessBuilder::create()->getProcess(); + } + + public function testShouldNotThrowALogicExceptionIfNoArgument() + { + $process = ProcessBuilder::create() + ->setPrefix('/usr/bin/php') + ->getProcess(); + + if ('\\' === DIRECTORY_SEPARATOR) { + $this->assertEquals('"/usr/bin/php"', $process->getCommandLine()); + } else { + $this->assertEquals("'/usr/bin/php'", $process->getCommandLine()); + } + } + + public function testShouldNotThrowALogicExceptionIfNoPrefix() + { + $process = ProcessBuilder::create(array('/usr/bin/php')) + ->getProcess(); + + if ('\\' === DIRECTORY_SEPARATOR) { + $this->assertEquals('"/usr/bin/php"', $process->getCommandLine()); + } else { + $this->assertEquals("'/usr/bin/php'", $process->getCommandLine()); + } + } + + public function testShouldReturnProcessWithDisabledOutput() + { + $process = ProcessBuilder::create(array('/usr/bin/php')) + ->disableOutput() + ->getProcess(); + + $this->assertTrue($process->isOutputDisabled()); + } + + public function testShouldReturnProcessWithEnabledOutput() + { + $process = ProcessBuilder::create(array('/usr/bin/php')) + ->disableOutput() + ->enableOutput() + ->getProcess(); + + $this->assertFalse($process->isOutputDisabled()); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException + * @expectedExceptionMessage Symfony\Component\Process\ProcessBuilder::setInput only accepts strings or stream resources. + */ + public function testInvalidInput() + { + $builder = ProcessBuilder::create(); + $builder->setInput(array()); + } +} diff --git a/library/symfony/process/Tests/ProcessFailedExceptionTest.php b/library/symfony/process/Tests/ProcessFailedExceptionTest.php new file mode 100644 index 000000000..0d763a470 --- /dev/null +++ b/library/symfony/process/Tests/ProcessFailedExceptionTest.php @@ -0,0 +1,146 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use Symfony\Component\Process\Exception\ProcessFailedException; + +/** + * @author Sebastian Marek <proofek@gmail.com> + */ +class ProcessFailedExceptionTest extends \PHPUnit_Framework_TestCase +{ + /** + * tests ProcessFailedException throws exception if the process was successful. + */ + public function testProcessFailedExceptionThrowsException() + { + $process = $this->getMock( + 'Symfony\Component\Process\Process', + array('isSuccessful'), + array('php') + ); + $process->expects($this->once()) + ->method('isSuccessful') + ->will($this->returnValue(true)); + + $this->setExpectedException( + '\InvalidArgumentException', + 'Expected a failed process, but the given process was successful.' + ); + + new ProcessFailedException($process); + } + + /** + * tests ProcessFailedException uses information from process output + * to generate exception message. + */ + public function testProcessFailedExceptionPopulatesInformationFromProcessOutput() + { + $cmd = 'php'; + $exitCode = 1; + $exitText = 'General error'; + $output = 'Command output'; + $errorOutput = 'FATAL: Unexpected error'; + $workingDirectory = getcwd(); + + $process = $this->getMock( + 'Symfony\Component\Process\Process', + array('isSuccessful', 'getOutput', 'getErrorOutput', 'getExitCode', 'getExitCodeText', 'isOutputDisabled', 'getWorkingDirectory'), + array($cmd) + ); + $process->expects($this->once()) + ->method('isSuccessful') + ->will($this->returnValue(false)); + + $process->expects($this->once()) + ->method('getOutput') + ->will($this->returnValue($output)); + + $process->expects($this->once()) + ->method('getErrorOutput') + ->will($this->returnValue($errorOutput)); + + $process->expects($this->once()) + ->method('getExitCode') + ->will($this->returnValue($exitCode)); + + $process->expects($this->once()) + ->method('getExitCodeText') + ->will($this->returnValue($exitText)); + + $process->expects($this->once()) + ->method('isOutputDisabled') + ->will($this->returnValue(false)); + + $process->expects($this->once()) + ->method('getWorkingDirectory') + ->will($this->returnValue($workingDirectory)); + + $exception = new ProcessFailedException($process); + + $this->assertEquals( + "The command \"$cmd\" failed.\n\nExit Code: $exitCode($exitText)\n\nWorking directory: {$workingDirectory}\n\nOutput:\n================\n{$output}\n\nError Output:\n================\n{$errorOutput}", + $exception->getMessage() + ); + } + + /** + * Tests that ProcessFailedException does not extract information from + * process output if it was previously disabled. + */ + public function testDisabledOutputInFailedExceptionDoesNotPopulateOutput() + { + $cmd = 'php'; + $exitCode = 1; + $exitText = 'General error'; + $workingDirectory = getcwd(); + + $process = $this->getMock( + 'Symfony\Component\Process\Process', + array('isSuccessful', 'isOutputDisabled', 'getExitCode', 'getExitCodeText', 'getOutput', 'getErrorOutput', 'getWorkingDirectory'), + array($cmd) + ); + $process->expects($this->once()) + ->method('isSuccessful') + ->will($this->returnValue(false)); + + $process->expects($this->never()) + ->method('getOutput'); + + $process->expects($this->never()) + ->method('getErrorOutput'); + + $process->expects($this->once()) + ->method('getExitCode') + ->will($this->returnValue($exitCode)); + + $process->expects($this->once()) + ->method('getExitCodeText') + ->will($this->returnValue($exitText)); + + $process->expects($this->once()) + ->method('isOutputDisabled') + ->will($this->returnValue(true)); + + $process->expects($this->once()) + ->method('getWorkingDirectory') + ->will($this->returnValue($workingDirectory)); + + $exception = new ProcessFailedException($process); + + $this->assertEquals( + "The command \"$cmd\" failed.\n\nExit Code: $exitCode($exitText)\n\nWorking directory: {$workingDirectory}", + $exception->getMessage() + ); + } +} diff --git a/library/symfony/process/Tests/ProcessInSigchildEnvironment.php b/library/symfony/process/Tests/ProcessInSigchildEnvironment.php new file mode 100644 index 000000000..3977bcdcf --- /dev/null +++ b/library/symfony/process/Tests/ProcessInSigchildEnvironment.php @@ -0,0 +1,22 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use Symfony\Component\Process\Process; + +class ProcessInSigchildEnvironment extends Process +{ + protected function isSigchildEnabled() + { + return true; + } +} diff --git a/library/symfony/process/Tests/ProcessUtilsTest.php b/library/symfony/process/Tests/ProcessUtilsTest.php new file mode 100644 index 000000000..e6564cde5 --- /dev/null +++ b/library/symfony/process/Tests/ProcessUtilsTest.php @@ -0,0 +1,48 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use Symfony\Component\Process\ProcessUtils; + +class ProcessUtilsTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider dataArguments + */ + public function testEscapeArgument($result, $argument) + { + $this->assertSame($result, ProcessUtils::escapeArgument($argument)); + } + + public function dataArguments() + { + if ('\\' === DIRECTORY_SEPARATOR) { + return array( + array('"\"php\" \"-v\""', '"php" "-v"'), + array('"foo bar"', 'foo bar'), + array('^%"path"^%', '%path%'), + array('"<|>\\" \\"\'f"', '<|>" "\'f'), + array('""', ''), + array('"with\trailingbs\\\\"', 'with\trailingbs\\'), + ); + } + + return array( + array("'\"php\" \"-v\"'", '"php" "-v"'), + array("'foo bar'", 'foo bar'), + array("'%path%'", '%path%'), + array("'<|>\" \"'\\''f'", '<|>" "\'f'), + array("''", ''), + array("'with\\trailingbs\\'", 'with\trailingbs\\'), + ); + } +} diff --git a/library/symfony/process/Tests/SigchildDisabledProcessTest.php b/library/symfony/process/Tests/SigchildDisabledProcessTest.php new file mode 100644 index 000000000..fdae5ec25 --- /dev/null +++ b/library/symfony/process/Tests/SigchildDisabledProcessTest.php @@ -0,0 +1,263 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +class SigchildDisabledProcessTest extends AbstractProcessTest +{ + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testGetExitCode() + { + parent::testGetExitCode(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testGetExitCodeIsNullOnStart() + { + parent::testGetExitCodeIsNullOnStart(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testGetExitCodeIsNullOnWhenStartingAgain() + { + parent::testGetExitCodeIsNullOnWhenStartingAgain(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testExitCodeCommandFailed() + { + parent::testExitCodeCommandFailed(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testMustRun() + { + parent::testMustRun(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testSuccessfulMustRunHasCorrectExitCode() + { + parent::testSuccessfulMustRunHasCorrectExitCode(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + */ + public function testMustRunThrowsException() + { + parent::testMustRunThrowsException(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + */ + public function testProcessIsSignaledIfStopped() + { + parent::testProcessIsSignaledIfStopped(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved. + */ + public function testProcessWithTermSignal() + { + parent::testProcessWithTermSignal(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved. + */ + public function testProcessIsNotSignaled() + { + parent::testProcessIsNotSignaled(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved. + */ + public function testProcessWithoutTermSignal() + { + parent::testProcessWithoutTermSignal(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testCheckTimeoutOnStartedProcess() + { + parent::testCheckTimeoutOnStartedProcess(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. The process identifier can not be retrieved. + */ + public function testGetPid() + { + parent::testGetPid(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. The process identifier can not be retrieved. + */ + public function testGetPidIsNullBeforeStart() + { + parent::testGetPidIsNullBeforeStart(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. The process identifier can not be retrieved. + */ + public function testGetPidIsNullAfterRun() + { + parent::testGetPidIsNullAfterRun(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testExitCodeText() + { + $process = $this->getProcess('qdfsmfkqsdfmqmsd'); + $process->run(); + + $process->getExitCodeText(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testExitCodeTextIsNullWhenExitCodeIsNull() + { + parent::testExitCodeTextIsNullWhenExitCodeIsNull(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testIsSuccessful() + { + parent::testIsSuccessful(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testIsSuccessfulOnlyAfterTerminated() + { + parent::testIsSuccessfulOnlyAfterTerminated(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testIsNotSuccessful() + { + parent::testIsNotSuccessful(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testTTYCommandExitCode() + { + parent::testTTYCommandExitCode(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. The process can not be signaled. + */ + public function testSignal() + { + parent::testSignal(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved. + */ + public function testProcessWithoutTermSignalIsNotSignaled() + { + parent::testProcessWithoutTermSignalIsNotSignaled(); + } + + public function testStopWithTimeoutIsActuallyWorking() + { + $this->markTestSkipped('Stopping with signal is not supported in sigchild environment'); + } + + public function testProcessThrowsExceptionWhenExternallySignaled() + { + $this->markTestSkipped('Retrieving Pid is not supported in sigchild environment'); + } + + public function testExitCodeIsAvailableAfterSignal() + { + $this->markTestSkipped('Signal is not supported in sigchild environment'); + } + + public function testRunProcessWithTimeout() + { + $this->markTestSkipped('Signal (required for timeout) is not supported in sigchild environment'); + } + + public function provideStartMethods() + { + return array( + array('start', 'Symfony\Component\Process\Exception\LogicException', 'Output has been disabled, enable it to allow the use of a callback.'), + array('run', 'Symfony\Component\Process\Exception\LogicException', 'Output has been disabled, enable it to allow the use of a callback.'), + array('mustRun', 'Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.'), + ); + } + + /** + * {@inheritdoc} + */ + protected function getProcess($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = array()) + { + $process = new ProcessInSigchildEnvironment($commandline, $cwd, $env, $input, $timeout, $options); + $process->setEnhanceSigchildCompatibility(false); + + return $process; + } +} diff --git a/library/symfony/process/Tests/SigchildEnabledProcessTest.php b/library/symfony/process/Tests/SigchildEnabledProcessTest.php new file mode 100644 index 000000000..2668a9b4b --- /dev/null +++ b/library/symfony/process/Tests/SigchildEnabledProcessTest.php @@ -0,0 +1,148 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +class SigchildEnabledProcessTest extends AbstractProcessTest +{ + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved. + */ + public function testProcessIsSignaledIfStopped() + { + parent::testProcessIsSignaledIfStopped(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved. + */ + public function testProcessWithTermSignal() + { + parent::testProcessWithTermSignal(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved. + */ + public function testProcessIsNotSignaled() + { + parent::testProcessIsNotSignaled(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved. + */ + public function testProcessWithoutTermSignal() + { + parent::testProcessWithoutTermSignal(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. The process identifier can not be retrieved. + */ + public function testGetPid() + { + parent::testGetPid(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. The process identifier can not be retrieved. + */ + public function testGetPidIsNullBeforeStart() + { + parent::testGetPidIsNullBeforeStart(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. The process identifier can not be retrieved. + */ + public function testGetPidIsNullAfterRun() + { + parent::testGetPidIsNullAfterRun(); + } + + public function testExitCodeText() + { + $process = $this->getProcess('qdfsmfkqsdfmqmsd'); + $process->run(); + + $this->assertInternalType('string', $process->getExitCodeText()); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. The process can not be signaled. + */ + public function testSignal() + { + parent::testSignal(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved. + */ + public function testProcessWithoutTermSignalIsNotSignaled() + { + parent::testProcessWithoutTermSignalIsNotSignaled(); + } + + public function testProcessThrowsExceptionWhenExternallySignaled() + { + $this->markTestSkipped('Retrieving Pid is not supported in sigchild environment'); + } + + public function testExitCodeIsAvailableAfterSignal() + { + $this->markTestSkipped('Signal is not supported in sigchild environment'); + } + + public function testStartAfterATimeout() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Restarting a timed-out process on Windows is not supported in sigchild environment'); + } + parent::testStartAfterATimeout(); + } + + public function testStopWithTimeoutIsActuallyWorking() + { + $this->markTestSkipped('Stopping with signal is not supported in sigchild environment'); + } + + public function testRunProcessWithTimeout() + { + $this->markTestSkipped('Signal (required for timeout) is not supported in sigchild environment'); + } + + public function testCheckTimeoutOnStartedProcess() + { + $this->markTestSkipped('Signal (required for timeout) is not supported in sigchild environment'); + } + + /** + * {@inheritdoc} + */ + protected function getProcess($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = array()) + { + $process = new ProcessInSigchildEnvironment($commandline, $cwd, $env, $input, $timeout, $options); + $process->setEnhanceSigchildCompatibility(true); + + return $process; + } +} diff --git a/library/symfony/process/Tests/SignalListener.php b/library/symfony/process/Tests/SignalListener.php new file mode 100644 index 000000000..4206550f5 --- /dev/null +++ b/library/symfony/process/Tests/SignalListener.php @@ -0,0 +1,25 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +// required for signal handling +declare (ticks = 1); + +pcntl_signal(SIGUSR1, function () {echo 'Caught SIGUSR1'; exit;}); + +$n = 0; + +// ticks require activity to work - sleep(4); does not work +while ($n < 400) { + usleep(10000); + ++$n; +} + +return; diff --git a/library/symfony/process/Tests/SimpleProcessTest.php b/library/symfony/process/Tests/SimpleProcessTest.php new file mode 100644 index 000000000..78f20eb10 --- /dev/null +++ b/library/symfony/process/Tests/SimpleProcessTest.php @@ -0,0 +1,216 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use Symfony\Component\Process\Process; + +class SimpleProcessTest extends AbstractProcessTest +{ + private $enabledSigchild = false; + + protected function setUp() + { + ob_start(); + phpinfo(INFO_GENERAL); + + $this->enabledSigchild = false !== strpos(ob_get_clean(), '--enable-sigchild'); + } + + public function testGetExitCode() + { + $this->skipIfPHPSigchild(); // This test use exitcode that is not available in this case + parent::testGetExitCode(); + } + + public function testExitCodeCommandFailed() + { + $this->skipIfPHPSigchild(); // This test use exitcode that is not available in this case + parent::testExitCodeCommandFailed(); + } + + public function testProcessIsSignaledIfStopped() + { + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved'); + parent::testProcessIsSignaledIfStopped(); + } + + public function testProcessWithTermSignal() + { + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved'); + parent::testProcessWithTermSignal(); + } + + public function testProcessIsNotSignaled() + { + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved'); + parent::testProcessIsNotSignaled(); + } + + public function testProcessWithoutTermSignal() + { + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved'); + parent::testProcessWithoutTermSignal(); + } + + public function testExitCodeText() + { + $this->skipIfPHPSigchild(); // This test use exitcode that is not available in this case + parent::testExitCodeText(); + } + + public function testIsSuccessful() + { + $this->skipIfPHPSigchild(); // This test use PID that is not available in this case + parent::testIsSuccessful(); + } + + public function testIsNotSuccessful() + { + $this->skipIfPHPSigchild(); // This test use PID that is not available in this case + parent::testIsNotSuccessful(); + } + + public function testGetPid() + { + $this->skipIfPHPSigchild(); // This test use PID that is not available in this case + parent::testGetPid(); + } + + public function testGetPidIsNullBeforeStart() + { + $this->skipIfPHPSigchild(); // This test use PID that is not available in this case + parent::testGetPidIsNullBeforeStart(); + } + + public function testGetPidIsNullAfterRun() + { + $this->skipIfPHPSigchild(); // This test use PID that is not available in this case + parent::testGetPidIsNullAfterRun(); + } + + public function testSignal() + { + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. The process can not be signaled.'); + parent::testSignal(); + } + + public function testProcessWithoutTermSignalIsNotSignaled() + { + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved'); + parent::testProcessWithoutTermSignalIsNotSignaled(); + } + + public function testProcessThrowsExceptionWhenExternallySignaled() + { + $this->skipIfPHPSigchild(); // This test use PID that is not available in this case + parent::testProcessThrowsExceptionWhenExternallySignaled(); + } + + public function testExitCodeIsAvailableAfterSignal() + { + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. The process can not be signaled.'); + parent::testExitCodeIsAvailableAfterSignal(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\LogicException + * @expectedExceptionMessage Can not send signal on a non running process. + */ + public function testSignalProcessNotRunning() + { + parent::testSignalProcessNotRunning(); + } + + public function testSignalWithWrongIntSignal() + { + if ($this->enabledSigchild) { + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. The process can not be signaled.'); + } else { + $this->setExpectedException('Symfony\Component\Process\Exception\RuntimeException', 'Error while sending signal `-4`.'); + } + parent::testSignalWithWrongIntSignal(); + } + + public function testSignalWithWrongNonIntSignal() + { + if ($this->enabledSigchild) { + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. The process can not be signaled.'); + } else { + $this->setExpectedException('Symfony\Component\Process\Exception\RuntimeException', 'Error while sending signal `Céphalopodes`.'); + } + parent::testSignalWithWrongNonIntSignal(); + } + + public function testStopTerminatesProcessCleanly() + { + $process = $this->getProcess(self::$phpBin.' -r "echo \'foo\'; sleep(1); echo \'bar\';"'); + $process->run(function () use ($process) { + $process->stop(); + }); + $this->assertTrue(true, 'A call to stop() is not expected to cause wait() to throw a RuntimeException'); + } + + public function testKillSignalTerminatesProcessCleanly() + { + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. The process can not be signaled.'); + + $process = $this->getProcess(self::$phpBin.' -r "echo \'foo\'; sleep(1); echo \'bar\';"'); + $process->run(function () use ($process) { + if ($process->isRunning()) { + $process->signal(defined('SIGKILL') ? SIGKILL : 9); + } + }); + $this->assertTrue(true, 'A call to signal() is not expected to cause wait() to throw a RuntimeException'); + } + + public function testTermSignalTerminatesProcessCleanly() + { + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. The process can not be signaled.'); + + $process = $this->getProcess(self::$phpBin.' -r "echo \'foo\'; sleep(1); echo \'bar\';"'); + $process->run(function () use ($process) { + if ($process->isRunning()) { + $process->signal(defined('SIGTERM') ? SIGTERM : 15); + } + }); + $this->assertTrue(true, 'A call to signal() is not expected to cause wait() to throw a RuntimeException'); + } + + public function testStopWithTimeoutIsActuallyWorking() + { + $this->skipIfPHPSigchild(); + + parent::testStopWithTimeoutIsActuallyWorking(); + } + + /** + * {@inheritdoc} + */ + protected function getProcess($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = array()) + { + return new Process($commandline, $cwd, $env, $input, $timeout, $options); + } + + private function skipIfPHPSigchild() + { + if ($this->enabledSigchild) { + $this->markTestSkipped('Your PHP has been compiled with --enable-sigchild, this test can not be executed'); + } + } + + private function expectExceptionIfPHPSigchild($classname, $message) + { + if ($this->enabledSigchild) { + $this->setExpectedException($classname, $message); + } + } +} diff --git a/library/symfony/process/phpunit.xml.dist b/library/symfony/process/phpunit.xml.dist new file mode 100644 index 000000000..788500084 --- /dev/null +++ b/library/symfony/process/phpunit.xml.dist @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd" + backupGlobals="false" + colors="true" + bootstrap="vendor/autoload.php" +> + <php> + <ini name="error_reporting" value="-1" /> + </php> + + <testsuites> + <testsuite name="Symfony Process Component Test Suite"> + <directory>./Tests/</directory> + </testsuite> + </testsuites> + + <filter> + <whitelist> + <directory>./</directory> + <exclude> + <directory>./Tests</directory> + <directory>./vendor</directory> + </exclude> + </whitelist> + </filter> +</phpunit> diff --git a/view/tpl/admin_plugins.tpl b/view/tpl/admin_plugins.tpl index 4b91f9e46..e10cde81d 100755 --- a/view/tpl/admin_plugins.tpl +++ b/view/tpl/admin_plugins.tpl @@ -1,19 +1,55 @@ -<div class="generic-content-wrapper-styled" id='adminpage'> - <h1>{{$title}} - {{$page}}</h1> - - <ul id='pluginslist'> - {{foreach $plugins as $p}} - <li class='plugin {{$p.1}}'> - {{if ! $p.2.disabled}} - <a class='toggleplugin' href='{{$baseurl}}/admin/{{$function}}/{{$p.0}}?a=t&t={{$form_security_token}}' title="{{if $p.1==on}}Disable{{else}}Enable{{/if}}" ><i class='fa {{if $p.1==on}}fa-check-square-o{{else}}fa-square-o{{/if}} admin-icons'></i></a> - {{else}} - <i class='fa fa-stop admin-icons'></i> - {{/if}} - <a href='{{$baseurl}}/admin/{{$function}}/{{$p.0}}'><span class='name'>{{$p.2.name}}</span></a> - <span class="version">{{$p.2.version}}</span>{{if $p.2.disabled}} {{$disabled}}{{/if}} - {{if $p.2.experimental}} {{$experimental}} {{/if}}{{if $p.2.unsupported}} {{$unsupported}} {{/if}} +<div class="generic-content-wrapper"> + <div class="section-title-wrapper"> + <div class="pull-right"> + <button class="btn btn-success btn-xs" onclick="openClose('form');">{{$addrepo}}</button> + </div> + <h2 id="title">{{$title}} - {{$page}}</h2> + <div class="clear"></div> + </div> + <div id="form" class="section-content-tools-wrapper"{{if !$expandform}} style="display:none;"{{/if}}> + {{$form}} + </div> + <div class="clear"></div> + <div id="chat-rotator-wrapper" class="center-block"> + <div id="chat-rotator"></div> + </div> + <div class="clear"></div> + <div id="new-repo-info" class="section-content-wrapper"></div> + <div class="section-content-wrapper-np"> + {{foreach $plugins as $p}} + <div class="section-content-tools-wrapper" id="pluginslist"> + <div class="contact-info plugin {{$p.1}}"> + {{if ! $p.2.disabled}} + <a class='toggleplugin' href='{{$baseurl}}/admin/{{$function}}/{{$p.0}}?a=t&t={{$form_security_token}}' title="{{if $p.1==on}}Disable{{else}}Enable{{/if}}" ><i class='fa {{if $p.1==on}}fa-check-square-o{{else}}fa-square-o{{/if}} admin-icons'></i></a> + {{else}} + <i class='fa fa-stop admin-icons'></i> + {{/if}} + <a href='{{$baseurl}}/admin/{{$function}}/{{$p.0}}'><span class='name'>{{$p.2.name}}</span></a> - <span class="version">{{$p.2.version}}</span>{{if $p.2.disabled}} {{$disabled}}{{/if}} + {{if $p.2.experimental}} {{$experimental}} {{/if}}{{if $p.2.unsupported}} {{$unsupported}} {{/if}} - <div class='desc'>{{$p.2.description}}</div> - </li> - {{/foreach}} - </ul> + <div class='desc'>{{$p.2.description}}</div> + </div> + </div> + {{/foreach}} + + </div> </div> + +<script> + function adminPluginsAddRepo() { + var repoURL = $('#id_repoURL').val(); + $('#chat-rotator').spin('tiny'); + $.post( + "/admin/plugins/addrepo", {repoURL: repoURL}, + function(response) { + $('#chat-rotator').spin(false); + if (response.success) { + $('#new-repo-info').html('<h3>Repo Info</h3><p>The repo was cloned to<br>' + response.message + '</p>'); + } else { + window.console.log('Error adding repo :' + response['message']); + } + return false; + }, + 'json'); + } +</script>
\ No newline at end of file diff --git a/view/tpl/admin_plugins_addrepo.tpl b/view/tpl/admin_plugins_addrepo.tpl new file mode 100644 index 000000000..fe5fecc25 --- /dev/null +++ b/view/tpl/admin_plugins_addrepo.tpl @@ -0,0 +1,8 @@ +<form id="add-plugin-repo-form" action="{{$post}}" method="post" > + + <p class="descriptive-text">{{$desc}}</p> + {{include file="field_input.tpl" field=$repoURL}} + <div class="btn-group pull-right"> + <button id="add-plugin-repo-submit" class="btn btn-primary" type="submit" name="submit" onclick="adminPluginsAddRepo(); return false;">{{$submit}}</button> + </div> +</form> |