├── .gitignore ├── .travis.yml ├── DependencyInjection ├── Configuration.php └── FeatureToggleExtension.php ├── Exception └── FeatureToggleNotFoundException.php ├── Feature ├── Feature.php ├── FeatureInterface.php └── FeatureManager.php ├── FeatureToggleBundle.php ├── README.md ├── Resources ├── config │ └── services.xml └── meta │ └── LICENSE ├── Templating └── Helper │ └── FeatureToggleHelper.php ├── Tests ├── DependencyInjection │ └── FeatureToggleExtensionTest.php ├── Templating │ └── Helper │ │ └── FeatureToggleHelperTest.php ├── autoload.php.dist └── bootstrap.php ├── Twig ├── FeatureToggleExtension.php ├── FeatureToggleNode.php └── FeatureToggleTokenParser.php ├── composer.json ├── phpunit.xml.dist └── vendor └── vendors.php /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE directories 2 | /nbproject/private/ 3 | /nbproject 4 | /.project 5 | /.buildpath 6 | /.settings 7 | 8 | phperrors.log 9 | phpunit.xml 10 | Tests/autoload.php 11 | 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | 7 | env: 8 | - SYMFONY_VERSION=v2.0.5 9 | - SYMFONY_VERSION=origin/master 10 | 11 | before_script: php vendor/vendors.php 12 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Benjamin Grandfond 15 | * @since 2011-09-20 16 | */ 17 | class Configuration implements ConfigurationInterface 18 | { 19 | public function getConfigTreeBuilder() 20 | { 21 | $treeBuilder = new TreeBuilder(); 22 | $rootNode = $treeBuilder->root('feature_toggle'); 23 | 24 | $rootNode 25 | ->children() 26 | ->arrayNode('features') 27 | ->canBeUnset() 28 | ->treatNullLike(array()) 29 | ->prototype('array') 30 | ->children() 31 | ->scalarNode('name')->defaultValue('')->end() 32 | ->booleanNode('enabled')->defaultValue(true)->end() 33 | ->end() 34 | ->end() 35 | ->end(); 36 | 37 | return $treeBuilder; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /DependencyInjection/FeatureToggleExtension.php: -------------------------------------------------------------------------------- 1 | 18 | * @author Benjamin Grandfond 19 | * @since 2011-09-20 20 | */ 21 | class FeatureToggleExtension extends Extension 22 | { 23 | public function load(array $configs, ContainerBuilder $container) 24 | { 25 | $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); 26 | $loader->load('services.xml'); 27 | 28 | $processor = new Processor(); 29 | $configuration = new Configuration(); 30 | $config = $processor->processConfiguration($configuration, $configs); 31 | 32 | foreach ($config['features'] as $feature) { 33 | $feature = new Feature($feature['name'], $feature['enabled']); 34 | $featureDefinition = new Definition( 35 | $container->getParameter('feature_toggle.feature.class'), 36 | array( 37 | 'name' => $feature->getName(), 38 | 'enabled' => $feature->isEnabled() 39 | ) 40 | ); 41 | $featureDefinition->addTag('feature_toggle.features'); 42 | 43 | $container->setDefinition('feature_toggle.features.'.$feature->getName(), $featureDefinition); 44 | } 45 | 46 | $manager = $container->getDefinition('feature_toggle.manager'); 47 | 48 | foreach ($container->findTaggedServiceIds('feature_toggle.features') as $id => $attributes) { 49 | $manager->addMethodCall('add', array(new Reference($id))); 50 | } 51 | 52 | $definition = new Definition('Emka\FeatureToggleBundle\Twig\FeatureToggleExtension', array($manager)); 53 | $definition->addTag('twig.extension'); 54 | $container->setDefinition('feature_toggle.twig.extension', $definition); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Exception/FeatureToggleNotFoundException.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | class Feature implements FeatureInterface 11 | { 12 | protected $name, 13 | $isEnabled; 14 | 15 | /** 16 | * @param string $name 17 | * @param string $isEnabled 18 | */ 19 | public function __construct($name, $isEnabled) 20 | { 21 | $this->name = $name; 22 | $this->isEnabled = $isEnabled; 23 | } 24 | 25 | /** 26 | * @return bool 27 | */ 28 | public function isEnabled() 29 | { 30 | return $this->isEnabled === true; 31 | } 32 | 33 | /** 34 | * @param $name 35 | */ 36 | public function setName($name) 37 | { 38 | $this->name = $name; 39 | } 40 | 41 | /** 42 | * @return string 43 | */ 44 | public function getName() 45 | { 46 | return $this->name; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Feature/FeatureInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | 11 | interface FeatureInterface 12 | { 13 | /** 14 | * @abstract 15 | * @return boolean 16 | */ 17 | function isEnabled(); 18 | } 19 | -------------------------------------------------------------------------------- /Feature/FeatureManager.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | class FeatureManager implements \ArrayAccess 11 | { 12 | protected $features; 13 | 14 | public function __construct(array $features = array()) 15 | { 16 | $this->features = $features; 17 | } 18 | 19 | /** 20 | * @param $feature 21 | */ 22 | public function add($feature) 23 | { 24 | $this->offsetSet($feature->getName(), $feature); 25 | } 26 | 27 | /** 28 | * @param $featureName 29 | * @return bool 30 | */ 31 | public function has($featureName) 32 | { 33 | return $this->offsetExists($featureName); 34 | } 35 | 36 | /** 37 | * @param $featureName 38 | * @return Feature 39 | */ 40 | public function get($featureName) 41 | { 42 | return $this->offsetGet($featureName); 43 | } 44 | 45 | /** 46 | * Whether a offset exists 47 | * 48 | * @link http://php.net/manual/en/arrayaccess.offsetexists.php 49 | * 50 | * @param mixed $offset 51 | * @return boolean Returns true on success or false on failure. 52 | */ 53 | public function offsetExists($offset) 54 | { 55 | return isset($this->features[$offset]); 56 | } 57 | 58 | /** 59 | * Offset to retrieve 60 | * 61 | * @link http://php.net/manual/en/arrayaccess.offsetget.php 62 | * 63 | * @param mixed $offset 64 | * @return mixed Can return all value types. 65 | */ 66 | public function offsetGet($offset) 67 | { 68 | return $this->offsetExists($offset) ? $this->features[$offset] : null; 69 | } 70 | 71 | /** 72 | * Offset to set 73 | * 74 | * @param mixed $offset 75 | * @param mixed $value 76 | * @return void 77 | */ 78 | public function offsetSet($offset, $value) 79 | { 80 | $this->features[$offset] = $value; 81 | } 82 | 83 | /** 84 | * Offset to unset 85 | * 86 | * @link http://php.net/manual/en/arrayaccess.offsetunset.php 87 | * 88 | * @param mixed $offset 89 | * @return void 90 | */ 91 | public function offsetUnset($offset) 92 | { 93 | unset($this->features[$offset]); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /FeatureToggleBundle.php: -------------------------------------------------------------------------------- 1 | 11 | * @since 2011-09-19 12 | */ 13 | class FeatureToggleBundle extends Bundle 14 | { 15 | public function build(ContainerBuilder $container) 16 | { 17 | parent::build($container); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | EmkaFeatureToggleBundle 2 | ======================= 3 | 4 | This bundle helps you easily configure your feature toggling in Symfony2 5 | by adding some simple tags to twig and extending it's configuration. 6 | 7 | It's under developement and even though it's functional, its behavior and configuration may (and will) change. 8 | 9 | Configuring your features 10 | ------------------------- 11 | 12 | Add the following lines to your config.yml: 13 | 14 | ``` 15 | feature_toggle: 16 | features: 17 | my_feature: 18 | name: my_feature # the name you use in your template 19 | enabled: true # false 20 | ``` 21 | 22 | Use feature toggling in your templates 23 | ------------------------------------- 24 | 25 | Once you've configured your features, you can surround a block of code in Twig template with a `feature` tag. 26 | 27 | ``` 28 | {% feature 'my_feature' %} 29 | ... add you code 30 | {% endfeature %} 31 | ``` 32 | 33 | Now setting `enabled: false` in config.yml will hide all part of code defined with same feature name. 34 | After each configuration change don't forget to clear your cache to update your templates. 35 | -------------------------------------------------------------------------------- /Resources/config/services.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Emka\FeatureToggleBundle\Feature\Feature 9 | Emka\FeatureToggleBundle\Feature\FeatureManager 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Resources/meta/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Marek Kalnik 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Templating/Helper/FeatureToggleHelper.php: -------------------------------------------------------------------------------- 1 | 11 | * @since 2011-11 12 | */ 13 | class FeatureToggleHelper extends Helper 14 | { 15 | 16 | protected $features; 17 | 18 | /** 19 | * @param Array $featuresConfig An array containing 'name' and 'enabled' parameters 20 | */ 21 | public function __construct($featuresConfig) 22 | { 23 | $features = array(); 24 | foreach ($featuresConfig as $feature) { 25 | $features[$feature['name']] = $feature['enabled']; 26 | } 27 | 28 | $this->features = $features; 29 | } 30 | 31 | /** 32 | * If feature is set to hidden: wraps it with "feature-toggle" css class 33 | * 34 | * @param String $featureName 35 | * @return String 36 | */ 37 | public function startToggle($featureName) 38 | { 39 | if ($this->isHidden($featureName)) { 40 | return '
'; 41 | } 42 | } 43 | 44 | /** 45 | * Closes the wrapping div, if features is hidden 46 | * 47 | * @return String 48 | */ 49 | public function endToggle($featureName) 50 | { 51 | if ($this->isHidden($featureName)) { 52 | return '
'; 53 | } 54 | } 55 | 56 | /** 57 | * Checks feature config to see if it's disabled 58 | * 59 | * @param String $featureName 60 | * @return Boolean 61 | */ 62 | protected function isHidden($featureName) 63 | { 64 | return isset($this->features[$featureName]) && !$this->features[$featureName]; 65 | } 66 | 67 | /** 68 | * Return helper's name for symfony internals 69 | * 70 | * @return String 71 | */ 72 | public function getName() 73 | { 74 | return 'feature_toggle'; 75 | } 76 | } -------------------------------------------------------------------------------- /Tests/DependencyInjection/FeatureToggleExtensionTest.php: -------------------------------------------------------------------------------- 1 | 20 | * @author Benjamin Grandfond 21 | * @since 2011-09-20 22 | */ 23 | class FeatureToggleExtensionTest extends \PHPUnit_Framework_TestCase 24 | { 25 | private $kernel; 26 | private $container; 27 | 28 | static public function assertSaneContainer(Container $container, $message = '') 29 | { 30 | $errors = array(); 31 | foreach ($container->getServiceIds() as $id) { 32 | try { 33 | $container->get($id); 34 | } catch (\Exception $e) { 35 | $errors[$id] = $e->getMessage(); 36 | } 37 | } 38 | 39 | self::assertEquals(array(), $errors, $message); 40 | } 41 | 42 | protected function setUp() 43 | { 44 | $this->kernel = $this->getMock('Symfony\\Component\\HttpKernel\\KernelInterface'); 45 | 46 | $this->container = new ContainerBuilder(); 47 | $this->container->addScope(new Scope('request')); 48 | $this->container->register('request', 'Symfony\\Component\\HttpFoundation\\Request')->setScope('request'); 49 | $this->container->register('templating.helper.assets', $this->getMockClass('Symfony\\Component\\Templating\\Helper\\AssetsHelper')); 50 | $this->container->register('templating.helper.router', $this->getMockClass('Symfony\\Bundle\\FrameworkBundle\\Templating\\Helper\\RouterHelper')) 51 | ->addArgument(new Definition($this->getMockClass('Symfony\\Component\\Routing\\RouterInterface'))); 52 | $this->container->register('twig', 'Twig_Environment'); 53 | $this->container->setParameter('kernel.bundles', array()); 54 | $this->container->setParameter('kernel.cache_dir', __DIR__); 55 | $this->container->setParameter('kernel.debug', false); 56 | $this->container->setParameter('kernel.root_dir', __DIR__); 57 | $this->container->setParameter('kernel.charset', 'UTF-8'); 58 | $this->container->set('kernel', $this->kernel); 59 | } 60 | 61 | public function testDefaultConfig() 62 | { 63 | $extension = new \Emka\FeatureToggleBundle\DependencyInjection\FeatureToggleExtension(); 64 | $extension->load(array(array()), $this->container); 65 | 66 | $this->assertFalse($this->container->has('feature_toggle.features')); 67 | } 68 | 69 | public function testConfig() 70 | { 71 | $extension = new \Emka\FeatureToggleBundle\DependencyInjection\FeatureToggleExtension(); 72 | $extension->load(array(array( 73 | 'features' => array( 74 | 'test_enabled' => array( 75 | 'name' => 'test_enabled', 76 | 'enabled' => true, 77 | ), 78 | 'test_disabled' => array( 79 | 'name' => 'test_disabled', 80 | 'enabled' => false, 81 | ) 82 | ) 83 | )), $this->container); 84 | 85 | $this->assertTrue($this->container->has('feature_toggle.features.test_enabled')); 86 | $this->assertTrue($this->container->has('feature_toggle.features.test_disabled')); 87 | 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Tests/Templating/Helper/FeatureToggleHelperTest.php: -------------------------------------------------------------------------------- 1 | 'test', 12 | 'enabled' => false, 13 | ))); 14 | 15 | $this->assertEquals( 16 | '
', 17 | $helper->startToggle('test') 18 | ); 19 | 20 | $this->assertEquals( 21 | '
', 22 | $helper->endToggle('test') 23 | ); 24 | } 25 | } -------------------------------------------------------------------------------- /Tests/autoload.php.dist: -------------------------------------------------------------------------------- 1 | registerNamespaces(array( 16 | 'Symfony' => array($vendorDir.'/symfony/src', $vendorDir.'/bundles'), 17 | )); 18 | $loader->registerPrefixes(array( 19 | 'Twig_Extensions_' => __DIR__.'/../vendor/twig-extensions/lib', 20 | 'Twig_' => __DIR__.'/../vendor/twig/lib', 21 | )); 22 | 23 | $loader->register(); 24 | 25 | spl_autoload_register(function($class) { 26 | if (0 === strpos($class, 'Emka\\FeatureToggleBundle\\')) { 27 | $path = __DIR__.'/../'.implode('/', array_slice(explode('\\', $class), 2)).'.php'; 28 | if (!stream_resolve_include_path($path)) { 29 | return false; 30 | } 31 | require_once $path; 32 | return true; 33 | } 34 | }); 35 | -------------------------------------------------------------------------------- /Tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 8 | * @author Benjamin Grandfond 9 | * @since 2011-09-17 10 | */ 11 | 12 | use Emka\FeatureToggleBundle\Twig\FeatureToggleTokenParser; 13 | use Emka\FeatureToggleBundle\Feature\FeatureManager; 14 | 15 | class FeatureToggleExtension extends \Twig_Extension 16 | { 17 | protected $manager; 18 | 19 | public function __construct(FeatureManager $manager) 20 | { 21 | $this->manager = $manager; 22 | } 23 | 24 | /** 25 | * @return FeatureManager 26 | */ 27 | public function getManager() 28 | { 29 | return $this->manager; 30 | } 31 | 32 | public function getTokenParsers() 33 | { 34 | return array(new FeatureToggleTokenParser($this->getManager())); 35 | } 36 | 37 | public function getFilters() 38 | { 39 | return array(); 40 | } 41 | 42 | public function getName() 43 | { 44 | return 'featuretoggle'; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Twig/FeatureToggleNode.php: -------------------------------------------------------------------------------- 1 | 8 | * @author Benjamin Grandfond 9 | * @since 2011-09-19 10 | */ 11 | 12 | class FeatureToggleNode extends \Twig_Node 13 | { 14 | protected $name; 15 | 16 | public function __construct($name, \Twig_NodeInterface $feature, $lineno, $tag = null) 17 | { 18 | parent::__construct(array('feature' => $feature), array('name' => $name), $lineno, $tag); 19 | } 20 | 21 | public function compile(\Twig_Compiler $compiler) 22 | { 23 | $globals = $compiler->getEnvironment()->getGlobals(); 24 | $enabled = (boolean)!isset($globals['_features']) 25 | || !isset($globals['_features'][$name]) 26 | || $globals['_features'][$name] == true; 27 | 28 | $compiler 29 | ->addDebugInfo($this) 30 | ->write(sprintf('if (!%b) {', $enabled)) 31 | ->indent() 32 | ->write('
') 33 | ->outdent() 34 | ->write('}') 35 | ->subcompile($this->getNode('feature')) 36 | ->write() 37 | ->indent() 38 | ->outdent(); 39 | 40 | // features not defined, current feature not defined or current feature set to true 41 | if () { 42 | $compiler 43 | ->write('echo ') 44 | ->subcompile($this->getNode('feature')) 45 | ->raw(";\n"); 46 | } else { 47 | // add div, subcompile, close div 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Twig/FeatureToggleTokenParser.php: -------------------------------------------------------------------------------- 1 | 11 | * @author Benjamin Grandfond 12 | * @since 2011-09-17 13 | * 14 | * {% feature name %} 15 | * code 16 | * {% endfeature %} 17 | * ---- EQUALS -------- 18 | * {% if feature_enabled(name) === true %} 19 | * code 20 | * {% endif %} 21 | */ 22 | class FeatureToggleTokenParser extends \Twig_TokenParser 23 | { 24 | protected $manager; 25 | 26 | /** 27 | * @param \Emka\FeatureToggleBundle\Feature\FeatureManager $manager 28 | */ 29 | public function __construct(FeatureManager $manager) 30 | { 31 | $this->manager = $manager; 32 | } 33 | 34 | public function parse(\Twig_Token $token) 35 | { 36 | $name = null; 37 | 38 | $stream = $this->parser->getStream(); 39 | while (!$stream->test(\Twig_Token::BLOCK_END_TYPE)) { 40 | if ($stream->test(\Twig_Token::STRING_TYPE)) { 41 | $name = $stream->next()->getValue(); 42 | 43 | if (!$this->manager->has($name)) { 44 | throw new FeatureToggleNotFoundException('The feature "%s" does not exist.', $name); 45 | } else { 46 | $feature = $this->manager->get($name); 47 | } 48 | } else { 49 | $token = $stream->getCurrent(); 50 | throw new \Twig_Error_Syntax(sprintf('Unexpected token "%s" of value %s".', \Twig_Token::typeToEnglish($token->getType(), $token->getLine()), $token->getValue()), $token->getLine()); 51 | } 52 | } 53 | 54 | $stream->expect(\Twig_Token::BLOCK_END_TYPE); 55 | 56 | // Store the body of the feature. 57 | $body = $this->parser->subparse(array($this, 'decideFeatureEnd'), true); 58 | 59 | $stream->expect(\Twig_Token::BLOCK_END_TYPE); 60 | 61 | if ($feature->isEnabled()) { 62 | return $body; 63 | } 64 | 65 | return; 66 | } 67 | 68 | /** 69 | * Test whether the feature is ended or not. 70 | * 71 | * @param \Twig_Token $token 72 | * @return bool 73 | */ 74 | public function decideFeatureEnd(\Twig_Token $token) 75 | { 76 | return $token->test($this->getEndTag()); 77 | } 78 | 79 | /** 80 | * Return the tag that marks the beginning of a feature. 81 | * 82 | * @return string 83 | */ 84 | public function getTag() 85 | { 86 | return 'feature'; 87 | } 88 | 89 | /** 90 | * Return the tag that marks the end of the feature. 91 | * 92 | * @return string 93 | */ 94 | public function getEndTag() 95 | { 96 | return 'end'.$this->getTag(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "emka/featuretogglebundle", 3 | "description": "Feature toggling", 4 | "keywords": ["feature toggling"], 5 | "type": "symfony-bundle", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Marek Kalnik", 10 | "email": "marekk@theodo.fr" 11 | }, 12 | { 13 | "name": "Benjamin Grandfond", 14 | "email": "benjaming@theodo.fr" 15 | } 16 | ], 17 | "require": { 18 | "twig/extensions": "dev-master" 19 | }, 20 | "autoload": { 21 | "psr-0": { "Emka\\FeatureToggleBundle": "" } 22 | }, 23 | "target-dir": "Emka/FeatureToggleBundle" 24 | } 25 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ./Tests 8 | 9 | 10 | 11 | 12 | 13 | ./ 14 | 15 | ./Resources 16 | ./Tests 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /vendor/vendors.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | set_time_limit(0); 13 | 14 | $vendorDir = __DIR__; 15 | $deps = array( 16 | array('symfony', 'http://github.com/symfony/symfony', isset($_SERVER['SYMFONY_VERSION']) ? $_SERVER['SYMFONY_VERSION'] : 'origin/master'), 17 | array('twig', 'https://github.com/fabpot/Twig', 'origin/master'), 18 | ); 19 | 20 | foreach ($deps as $dep) { 21 | list($name, $url, $rev) = $dep; 22 | 23 | echo "> Installing/Updating $name\n"; 24 | 25 | $installDir = $vendorDir.'/'.$name; 26 | if (!is_dir($installDir)) { 27 | system(sprintf('git clone -q %s %s', escapeshellarg($url), escapeshellarg($installDir))); 28 | } 29 | 30 | system(sprintf('cd %s && git fetch -q origin && git reset --hard %s', escapeshellarg($installDir), escapeshellarg($rev))); 31 | } 32 | --------------------------------------------------------------------------------