├── .gitignore ├── CHANGELOG ├── LICENSE ├── README.markdown ├── composer.json ├── lib └── PHPGit │ ├── Command.php │ ├── Configuration.php │ └── Repository.php ├── prove.php └── test ├── PHPGit_RepoTest.php ├── PHPGit_RepoTestHelper.php └── vendor └── lime.php /.gitignore: -------------------------------------------------------------------------------- 1 | nbproject 2 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | v1.4.0 - 2011-11-30 2 | 3 | * Changed everything to PEAR coding standards to improve framework compatibility 4 | 5 | v1.3.0 - 2010-07-22 6 | 7 | * Add phpGitRepo::getTags to get the list of tags 8 | 9 | v1.2.1 - 2010-07-22 10 | 11 | * Fix commit messages 12 | 13 | v1.2 - 2010-07-22 14 | 15 | * Add phpGitRepo::getCommits to get an array of the more recent commits 16 | 17 | v1.1 - 2010-07-22 18 | 19 | * Add phpGitRepo::create to create a new Git repository on filesystem 20 | 21 | v1.0 - 2010-05-18 22 | 23 | * First stable release 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010 Thibault Duplessis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # PHP Git Repo 2 | 3 | Manage a Git repository with PHP. 4 | Provide an object oriented wrapper to run any Git command. 5 | 6 | ## Requirements 7 | 8 | - PHP >= 5.2 (PHP 5.3 works fine) 9 | - Git >= 1.5 10 | 11 | ## Instantiate a PHPGit_Repository 12 | 13 | $repo = new PHPGit_Repository('/path/to/the/git/repo'); 14 | 15 | It does NOT create a Git repo, but a PHP object to manipulate an existing Git repo. 16 | 17 | ## Create a Git repository 18 | 19 | If the Git repository does not exist yet on filesystem, PHPGit_Repository can create it for you. 20 | 21 | $repo = PHPGit_Repository::create('/path/to/the/git/repo'); 22 | 23 | It runs `git init` and returns a PHPGit_Repository object. 24 | 25 | ## Run git commands 26 | 27 | git commands can be run with the same syntax as in the CLI. Some examples: 28 | 29 | // change current branch to master 30 | $repo->git('checkout master'); 31 | 32 | // pull from a remote 33 | $repo->git('pull origin master'); 34 | 35 | // add a remote repo 36 | $repo->git('remote add origin git://github.com/ornicar/php-git-repo.git'); 37 | 38 | There is no limitation, you can run any git command. 39 | 40 | The git() method returns the output string: 41 | 42 | echo $repo->git('log --oneline'); 43 | 44 | e30b70b Move test repo to system tmp dir, introduce PHPGit_Command 45 | 01fabb1 Add test repo 46 | 12a95e6 Add base class with basic unit test 47 | 58e7769 Fix readme 48 | c14c9ec Initial commit 49 | 50 | The git() method throws a GitRuntimeException if the command is invalid: 51 | 52 | $repo->git('wtf'); // this git command does NOT exist: throw GitRuntimeException 53 | 54 | ## Get branches informations 55 | 56 | Some shortcut methods are provided to deal with branches in a convenient way. 57 | 58 | ### Get the branches list: 59 | 60 | $branches = $repo->getBranches(); 61 | // returns array('master', 'other_branch') 62 | 63 | ### Get the current branch: 64 | 65 | $branch = $repo->getCurrentBranch(); 66 | // returns 'master' 67 | 68 | ### Know if the repo has a given branch: 69 | 70 | $hasBranch = $repo->hasBranch('master'); 71 | // returns true 72 | 73 | ## Get tags informations 74 | 75 | ### Get the tags list: 76 | 77 | $tags = $repo->getTags(); 78 | // returns array('first_release', 'v2') 79 | 80 | ## Get commits informations 81 | 82 | You can get an array of the last commits on the current branch. 83 | 84 | $commits = $repo->getCommits(15); 85 | // returns an array of the 15 last commits 86 | 87 | Internally, this methods run `git log` with formatted output. The return value should look like: 88 | 89 | Array 90 | ( 91 | [0] => Array 92 | ( 93 | [id] => affb0e84a11b4180b0fa0e5d36bdac73584f0d71 94 | [tree] => 4b825dc642cb6eb9a060e54bf8d69288fbee4904 95 | [author] => Array 96 | ( 97 | [name] => ornicar 98 | [email] => myemail@gmail.com 99 | ) 100 | 101 | [authored_date] => 2010-09-22 19:17:35 +0200 102 | [commiter] => Array 103 | ( 104 | [name] => ornicar 105 | [email] => myemail@gmail.com 106 | ) 107 | 108 | [committed_date] => 2010-09-22 19:17:35 +0200 109 | [message] => My commit message 110 | ) 111 | 112 | [1] => Array 113 | ( 114 | ... 115 | 116 | The first commit is the more recent one. 117 | 118 | ## Debug mode 119 | 120 | `PHPGit_Repository` constructor second parameter lets you enable debug mode. 121 | When debug mode is on, commands and their output are displayed. 122 | 123 | $repo = new PHPGit_Repository('/path/to/the/git/repo', true); 124 | 125 | ## Configure 126 | 127 | `PHPGit_Repository` can be configured by passing an array of options to the constructor third parameter. 128 | 129 | ### Change git executable path 130 | 131 | You may need to provide the path to the git executable. 132 | 133 | $repo = new PHPGit_Repository('/path/to/the/git/repo', false, array('git_executable' => '/usr/bin/git')); 134 | 135 | On most Unix system, it's `/usr/bin/git`. On Windows, it may be `C:\Program Files\Git\bin`. 136 | 137 | ### Change the command class 138 | 139 | By default, `PHPGit_Repository` will use `PHPGit_Command` class to implement Git commands. 140 | By replacing this option, you can use your own command implementation: 141 | 142 | $repo = new PHPGit_Repository('/path/to/the/git/repo', false, array('command_class' => 'myGitCommand')); 143 | 144 | ## Run test suite 145 | 146 | All code is fully unit tested. To run tests on your server, from a CLI, run 147 | 148 | php /path/to/php-git-repo/prove.php 149 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ornicar/php-git-repo", 3 | "type": "library", 4 | "description": "An object oriented wrapper to run any Git command", 5 | "version": "1.3", 6 | "homepage": "https://github.com/ornicar/php-git-repo", 7 | "keywords": ["git"], 8 | "license": "MIT", 9 | "authors": [ 10 | { 11 | "name": "Thibault Duplessis", 12 | "email": "thibault.duplessis@gmail.com", 13 | "homepage": "http://ornicar.github.com" 14 | } 15 | ], 16 | "require": { 17 | "php": ">=5.2" 18 | }, 19 | "autoload": { 20 | "psr-0": { "PHPGit_": "lib/" } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/PHPGit/Command.php: -------------------------------------------------------------------------------- 1 | dir = $dir; 32 | $this->commandString = $commandString; 33 | $this->debug = $debug; 34 | } 35 | 36 | public function run() 37 | { 38 | $commandToRun = sprintf('cd %s && %s', escapeshellarg($this->dir), $this->commandString); 39 | 40 | if($this->debug) { 41 | print $commandToRun."\n"; 42 | } 43 | 44 | ob_start(); 45 | passthru($commandToRun, $returnVar); 46 | $output = ob_get_clean(); 47 | 48 | if($this->debug) { 49 | print $output."\n"; 50 | } 51 | 52 | if(0 !== $returnVar) { 53 | // Git 1.5.x returns 1 when running "git status" 54 | if(1 === $returnVar && 0 === strncmp($this->commandString, 'git status', 10)) { 55 | // it's ok 56 | } 57 | else { 58 | throw new GitRuntimeException(sprintf( 59 | 'Command %s failed with code %s: %s', 60 | $commandToRun, 61 | $returnVar, 62 | $output 63 | ), $returnVar); 64 | } 65 | } 66 | 67 | return trim($output); 68 | } 69 | } 70 | 71 | class GitRuntimeException extends RuntimeException {} 72 | -------------------------------------------------------------------------------- /lib/PHPGit/Configuration.php: -------------------------------------------------------------------------------- 1 | 9 | * @license MIT License 10 | * 11 | * Documentation: http://github.com/ornicar/php-git-repo/blob/master/README.markdown 12 | * Tickets: http://github.com/ornicar/php-git-repo/issues 13 | */ 14 | class PHPGit_Configuration 15 | { 16 | const USER_NAME = 'user.name'; 17 | const USER_EMAIL = 'user.email'; 18 | 19 | /** 20 | * Holds the actual configuration 21 | * @var array 22 | */ 23 | protected $configuration = array(); 24 | 25 | /** 26 | * Holds the Git repository instance. 27 | * @var PHPGit_Repository 28 | */ 29 | protected $repository; 30 | 31 | public function __construct(PHPGit_Repository $gitRepo) 32 | { 33 | $this->repository = $gitRepo; 34 | } 35 | 36 | /** 37 | * Get a config option 38 | * 39 | * @param string $configOption The config option to read 40 | * @param mixed $fallback Value will be returned, if $configOption is not set 41 | * 42 | * @return string 43 | */ 44 | public function get($configOption, $fallback = null) 45 | { 46 | if (isset($this->configuration[$configOption])) { 47 | $optionValue = $this->configuration[$configOption]; 48 | } else { 49 | if (array_key_exists($configOption, $this->configuration)) { 50 | $optionValue = $fallback; 51 | } 52 | 53 | try { 54 | $optionValue = $this->repository->git(sprintf('config --get ' . $configOption)); 55 | $this->configuration[$configOption] = $optionValue; 56 | } catch (GitRuntimeException $e) { 57 | $optionValue = $fallback; 58 | $this->configuration[$configOption] = null; 59 | } 60 | } 61 | 62 | return $optionValue; 63 | } 64 | 65 | /** 66 | * Set or change a *repository* config option 67 | * 68 | * @param string $configOption 69 | * @param mixed $configValue 70 | */ 71 | public function set($configOption, $configValue) 72 | { 73 | $this->repository->git(sprintf('config --local %s %s', $configOption, $configValue)); 74 | unset($this->configuration[$configOption]); 75 | } 76 | 77 | /** 78 | * Removes a option from local config 79 | * 80 | * @param string $configOption 81 | */ 82 | public function remove($configOption) 83 | { 84 | $this->repository->git(sprintf('config --local --unset %s', $configOption)); 85 | unset($this->configuration[$configOption]); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /lib/PHPGit/Repository.php: -------------------------------------------------------------------------------- 1 | 15 | * @license MIT License 16 | * 17 | * Documentation: http://github.com/ornicar/php-git-repo/blob/master/README.markdown 18 | * Tickets: http://github.com/ornicar/php-git-repo/issues 19 | */ 20 | class PHPGit_Repository 21 | { 22 | /** 23 | * @var string local repository directory 24 | */ 25 | protected $dir; 26 | 27 | protected $dateFormat = 'iso'; 28 | protected $logFormat = '"%H|%T|%an|%ae|%ad|%cn|%ce|%cd|%s"'; 29 | 30 | /** 31 | * @var boolean Whether to enable debug mode or not 32 | * When debug mode is on, commands and their output are displayed 33 | */ 34 | protected $debug; 35 | 36 | /** 37 | * @var array of options 38 | */ 39 | protected $options; 40 | 41 | protected static $defaultOptions = array( 42 | 'command_class' => 'PHPGit_Command', // class used to create a command 43 | 'git_executable' => '/usr/bin/git' // path of the executable on the server 44 | ); 45 | 46 | /** 47 | * Instanciate a new Git repository wrapper 48 | * 49 | * @param string $dir real filesystem path of the repository 50 | * @param boolean $debug 51 | * @param array $options 52 | */ 53 | public function __construct($dir, $debug = false, array $options = array()) 54 | { 55 | $this->dir = $dir; 56 | $this->debug = $debug; 57 | $this->options = array_merge(self::$defaultOptions, $options); 58 | 59 | $this->checkIsValidGitRepo(); 60 | } 61 | 62 | 63 | /** 64 | * Helper method to get a list of commits which exist in $sourceBranch that do not yet exist in $targetBranch. 65 | * 66 | * @param string $targetBranch 67 | * @param string $sourceBranch 68 | * @return array Formatted list of commits. 69 | */ 70 | public function getDifferenceBetweenBranches($targetBranch, $sourceBranch) 71 | { 72 | $output = $this->git(sprintf('log %s..%s --date=%s --format=format:%s', $targetBranch, $sourceBranch, $this->dateFormat, $this->logFormat)); 73 | return $this->parseLogsIntoArray($output); 74 | } 75 | 76 | /** 77 | * Create a new Git repository in filesystem, running "git init" 78 | * Returns the git repository wrapper 79 | * 80 | * @param string $dir real filesystem path of the repository 81 | * @param boolean $debug 82 | * @param array $options 83 | * @return PHPGit_Repository 84 | **/ 85 | public static function create($dir, $debug = false, array $options = array()) 86 | { 87 | $options = array_merge(self::$defaultOptions, $options); 88 | $commandString = $options['git_executable'].' init'; 89 | $command = new $options['command_class']($dir, $commandString, $debug); 90 | $command->run(); 91 | 92 | $repo = new self($dir, $debug, $options); 93 | 94 | return $repo; 95 | } 96 | 97 | /** 98 | * Clone a new Git repository in filesystem, running "git clone" 99 | * Returns the git repository wrapper 100 | * 101 | * @param string $url of the repository 102 | * @param string $dir real filesystem path of the repository 103 | * @param boolean $debug 104 | * @param array $options 105 | * @return PHPGit_Repository 106 | **/ 107 | public static function cloneUrl($url, $dir, $debug = false, array $options = array()) 108 | { 109 | $options = array_merge(self::$defaultOptions, $options); 110 | $commandString = $options['git_executable'].' clone '.escapeshellarg($url).' '.escapeshellarg($dir); 111 | $command = new $options['command_class'](getcwd(), $commandString, $debug); 112 | $command->run(); 113 | 114 | $repo = new self($dir, $debug, $options); 115 | 116 | return $repo; 117 | } 118 | 119 | /** 120 | * Get the configuration for current 121 | * @return PHPGit_Configuration 122 | */ 123 | public function getConfiguration() 124 | { 125 | return new PHPGit_Configuration($this); 126 | } 127 | 128 | /** 129 | * Get branches list 130 | * 131 | * @return array list of branches names 132 | */ 133 | public function getBranches($flags='') 134 | { 135 | return array_filter(preg_replace('/[\s\*]/', '', explode("\n", $this->git('branch '.$flags)))); 136 | } 137 | 138 | /** 139 | * Get current branch 140 | * 141 | * @return string the current branch name 142 | */ 143 | public function getCurrentBranch() 144 | { 145 | $output = $this->git('branch'); 146 | 147 | foreach(explode("\n", $this->git('branch')) as $branchLine) { 148 | if('*' === $branchLine{0}) { 149 | return substr($branchLine, 2); 150 | } 151 | } 152 | } 153 | 154 | /** 155 | * Tell if a branch exists 156 | * 157 | * @return boolean true if the branch exists, false otherwise 158 | */ 159 | public function hasBranch($branchName) 160 | { 161 | return in_array($branchName, $this->getBranches()); 162 | } 163 | 164 | /** 165 | * Get tags list 166 | * 167 | * @return array list of tag names 168 | */ 169 | public function getTags() 170 | { 171 | $output = $this->git('tag'); 172 | return $output ? array_filter(explode("\n", $output)) : array(); 173 | } 174 | 175 | /** 176 | * Return the result of `git log` formatted in a PHP array 177 | * 178 | * @return array list of commits and their properties 179 | **/ 180 | public function getCommits($nbCommits = 10) 181 | { 182 | $output = $this->git(sprintf('log -n %d --date=%s --format=format:%s', $nbCommits, $this->dateFormat, $this->logFormat)); 183 | return $this->parseLogsIntoArray($output); 184 | } 185 | 186 | /** 187 | * Convert a formatted log string into an array 188 | * @param string $logOutput The output from a `git log` command formated using $this->logFormat 189 | */ 190 | private function parseLogsIntoArray($logOutput) 191 | { 192 | $commits = array(); 193 | foreach(explode("\n", $logOutput) as $line) { 194 | $infos = explode('|', $line); 195 | $commits[] = array( 196 | 'id' => $infos[0], 197 | 'tree' => $infos[1], 198 | 'author' => array( 199 | 'name' => $infos[2], 200 | 'email' => $infos[3] 201 | ), 202 | 'authored_date' => $infos[4], 203 | 'commiter' => array( 204 | 'name' => $infos[5], 205 | 'email' => $infos[6] 206 | ), 207 | 'committed_date' => $infos[7], 208 | 'message' => $infos[8] 209 | ); 210 | } 211 | return $commits; 212 | } 213 | 214 | /** 215 | * Check if a directory is a valid Git repository 216 | */ 217 | public function checkIsValidGitRepo() 218 | { 219 | if(!file_exists($this->dir.'/.git/HEAD')) { 220 | throw new InvalidGitRepositoryDirectoryException($this->dir.' is not a valid Git repository'); 221 | } 222 | } 223 | 224 | /** 225 | * Run any git command, like "status" or "checkout -b mybranch origin/mybranch" 226 | * 227 | * @throws RuntimeException 228 | * @param string $commandString 229 | * @return string $output 230 | */ 231 | public function git($commandString) 232 | { 233 | // clean commands that begin with "git " 234 | $commandString = preg_replace('/^git\s/', '', $commandString); 235 | 236 | $commandString = $this->options['git_executable'].' '.$commandString; 237 | 238 | $command = new $this->options['command_class']($this->dir, $commandString, $this->debug); 239 | 240 | return $command->run(); 241 | } 242 | 243 | /** 244 | * Get the repository directory 245 | * 246 | * @return string the repository directory 247 | */ 248 | public function getDir() 249 | { 250 | return $this->dir; 251 | } 252 | } 253 | 254 | class InvalidGitRepositoryDirectoryException extends InvalidArgumentException 255 | { 256 | } 257 | -------------------------------------------------------------------------------- /prove.php: -------------------------------------------------------------------------------- 1 | base_dir = realpath(dirname(__FILE__).'/test'); 8 | $h->register(glob(dirname(__FILE__).'/test/*Test.php')); 9 | exit($h->run() ? 0 : 1); 10 | -------------------------------------------------------------------------------- /test/PHPGit_RepoTest.php: -------------------------------------------------------------------------------- 1 | is($repo->git("branch"), '', '$repo->git("branch") returns nothing'); 11 | 12 | $t->is_deeply($repo->getBranches(), array(), 'No branches'); 13 | 14 | $t->is($repo->getCurrentBranch(), null, 'No current branch'); 15 | 16 | $t->is($repo->hasBranch('master'), false, 'No master branch'); 17 | 18 | try { 19 | $repo->git('checkout master'); 20 | $t->fail('Can not checkout master'); 21 | } catch (RuntimeException $e) { 22 | $t->pass('Can not checkout master'); 23 | } 24 | 25 | $repo->git('remote add origin git://github.com/ornicar/php-git-repo.git'); 26 | 27 | $repo->git('pull origin master'); 28 | 29 | $t->is_deeply($repo->getBranches(), array('master'), 'One branch master'); 30 | 31 | $t->is($repo->hasBranch('master'), true, 'master branch exists'); 32 | 33 | $t->is($repo->getCurrentBranch(), 'master', 'Current branch: master'); 34 | 35 | $repo->git('checkout -b other_branch'); 36 | 37 | $t->is_deeply($repo->getBranches(), array('master', 'other_branch'), 'Two branches, master and other_branch'); 38 | 39 | $t->is($repo->getCurrentBranch(), 'other_branch', 'Current branch: other_branch'); 40 | 41 | $t->is($repo->hasBranch('other_branch'), true, 'other_branch branch exists'); 42 | 43 | $repo->git('checkout master'); 44 | 45 | $t->is($repo->getCurrentBranch(), 'master', 'Current branch: master'); 46 | 47 | $t->comment('repeat "git " in the command string'); 48 | $repo->git('git checkout other_branch'); 49 | 50 | $t->is($repo->getCurrentBranch(), 'other_branch', 'Current branch: other_branch'); 51 | 52 | try { 53 | $repo->git('wtf'); 54 | $t->fail('wtf is not a valid command'); 55 | } catch (RuntimeException $e) { 56 | $t->pass('wtf is not a valid command'); 57 | } 58 | 59 | $t->comment('Use a valid git binary: /usr/bin/git'); 60 | 61 | $repo = _createTmpGitRepo($t, array('git_executable' => '/usr/bin/git')); 62 | 63 | $t->comment('Use a invalid git binary: /usr/bin/git-foobar'); 64 | 65 | try { 66 | $repo = _createTmpGitRepo($t, array('git_executable' => '/usr/bin/git-foobar')); 67 | $repo->git('status'); 68 | $t->fail('/usr/bin/git-foobar is not a valid git binary'); 69 | } catch (RuntimeException $e) { 70 | $t->pass('/usr/bin/git-foobar is not a valid git binary'); 71 | } 72 | 73 | $repoDir = sys_get_temp_dir() . '/php-git-repo/' . uniqid(); 74 | mkdir($repoDir); 75 | try { 76 | $repo = PHPGit_Repository::create($repoDir); 77 | $t->pass('Create a new Git repository in filesystem'); 78 | } catch (InvalidArgumentException $e) { 79 | $t->fail($e->getMessage()); 80 | } 81 | 82 | $repo = _createTmpGitRepo($t); 83 | 84 | $config = $repo->getConfiguration(); 85 | 86 | $t->ok($config->get('core.editor', true)); 87 | $config->set('core.editor', 'nano'); 88 | $t->is($config->get('core.editor'), 'nano'); 89 | $t->is($config->get('core.editor'), 'nano'); 90 | $config->remove('core.editor'); 91 | $t->ok($config->get('core.editor', true)); 92 | 93 | 94 | file_put_contents($repo->getDir() . '/README', 'No, finally, do not read me.'); 95 | $repo->git('add README'); 96 | $repo->git('commit -m "Add README"'); 97 | unlink($repo->getDir() . '/README'); 98 | $repo->git('rm README'); 99 | $repo->git('commit -m "Remove README"'); 100 | 101 | $log = $repo->getCommits(7); 102 | $t->ok(is_array($log)); 103 | $t->is(count($log), 2); 104 | $commit = $log[0]; 105 | $t->ok(is_array($commit)); 106 | $t->is($commit['message'], 'Remove README'); 107 | 108 | $t->is($commit['author']['name'], $config->get(PHPGit_Configuration::USER_NAME)); 109 | $t->is($commit['commiter']['name'], $config->get(PHPGit_Configuration::USER_NAME)); 110 | $commit = $log[1]; 111 | $t->is($commit['message'], 'Add README'); 112 | 113 | $t->is_deeply($repo->getTags(), array(), 'No tags'); 114 | $repo->git('tag -am "tag 1" first_tag'); 115 | $repo->git('tag -am "tag 2" second_tag'); 116 | $t->is_deeply($repo->getTags(), array('first_tag', 'second_tag'), '2 tags'); 117 | 118 | // cloneUrl 119 | $repoDir = sys_get_temp_dir() . '/php-git-repo/' . uniqid(); 120 | try { 121 | $repo = PHPGit_Repository::cloneUrl('https://github.com/ornicar/php-git-repo.git', $repoDir); 122 | $t->pass('Create a new Git repository in filesystem'); 123 | $t->is($repo->getCurrentBranch(), 'master', 'Current branch: master'); 124 | 125 | } catch (InvalidArgumentException $e) { 126 | $t->fail($e->getMessage()); 127 | } 128 | -------------------------------------------------------------------------------- /test/PHPGit_RepoTestHelper.php: -------------------------------------------------------------------------------- 1 | ok(!is_dir($repoDir . '/.git'), $repoDir . ' is not a Git repo'); 13 | 14 | try { 15 | new PHPGit_Repository($repoDir, true, $options); 16 | $t->fail($repoDir . ' is not a valid git repository'); 17 | } catch (InvalidArgumentException $e) { 18 | $t->pass($repoDir . ' is not a valid git repository'); 19 | } 20 | 21 | $t->comment('Create Git repo'); 22 | exec('git init ' . escapeshellarg($repoDir)); 23 | $t->ok(is_dir($repoDir . '/.git'), $repoDir . ' is a Git repo'); 24 | 25 | $repo = new PHPGit_Repository($repoDir, true, $options); 26 | $t->isa_ok($repo, 'PHPGit_Repository', $repoDir . ' is a valid git repo'); 27 | 28 | $originalRepoDir = dirname(__FILE__) . '/repo'; 29 | foreach (array('README.markdown', 'index.php') as $file) { 30 | copy($originalRepoDir . '/' . $file, $repoDir . '/' . $file); 31 | } 32 | 33 | return $repo; 34 | } -------------------------------------------------------------------------------- /test/vendor/lime.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | /** 12 | * Unit test library. 13 | * 14 | * @package lime 15 | * @author Fabien Potencier 16 | * @version SVN: $Id$ 17 | */ 18 | class lime_test 19 | { 20 | const EPSILON = 0.0000000001; 21 | 22 | protected $test_nb = 0; 23 | protected $output = null; 24 | protected $results = array(); 25 | protected $options = array(); 26 | 27 | static protected $all_results = array(); 28 | 29 | public function __construct($plan = null, $options = array()) 30 | { 31 | // for BC 32 | if (!is_array($options)) 33 | { 34 | $options = array('output' => $options); 35 | } 36 | 37 | $this->options = array_merge(array( 38 | 'force_colors' => false, 39 | 'output' => null, 40 | 'verbose' => false, 41 | ), $options); 42 | 43 | $this->output = $this->options['output'] ? $this->options['output'] : new lime_output($this->options['force_colors']); 44 | 45 | $caller = $this->find_caller(debug_backtrace()); 46 | self::$all_results[] = array( 47 | 'file' => $caller[0], 48 | 'tests' => array(), 49 | 'stats' => array('plan' => $plan, 'total' => 0, 'failed' => array(), 'passed' => array(), 'skipped' => array()), 50 | ); 51 | 52 | $this->results = &self::$all_results[count(self::$all_results) - 1]; 53 | 54 | null !== $plan and $this->output->echoln(sprintf("1..%d", $plan)); 55 | } 56 | 57 | static public function reset() 58 | { 59 | self::$all_results = array(); 60 | } 61 | 62 | static public function to_array() 63 | { 64 | return self::$all_results; 65 | } 66 | 67 | static public function to_xml($results = null) 68 | { 69 | if (is_null($results)) 70 | { 71 | $results = self::$all_results; 72 | } 73 | 74 | $dom = new DOMDocument('1.0', 'UTF-8'); 75 | $dom->formatOutput = true; 76 | $dom->appendChild($testsuites = $dom->createElement('testsuites')); 77 | 78 | $errors = 0; 79 | $failures = 0; 80 | $errors = 0; 81 | $skipped = 0; 82 | $assertions = 0; 83 | 84 | foreach ($results as $result) 85 | { 86 | $testsuites->appendChild($testsuite = $dom->createElement('testsuite')); 87 | $testsuite->setAttribute('name', basename($result['file'], '.php')); 88 | $testsuite->setAttribute('file', $result['file']); 89 | $testsuite->setAttribute('failures', count($result['stats']['failed'])); 90 | $testsuite->setAttribute('errors', 0); 91 | $testsuite->setAttribute('skipped', count($result['stats']['skipped'])); 92 | $testsuite->setAttribute('tests', $result['stats']['plan']); 93 | $testsuite->setAttribute('assertions', $result['stats']['plan']); 94 | 95 | $failures += count($result['stats']['failed']); 96 | $skipped += count($result['stats']['skipped']); 97 | $assertions += $result['stats']['plan']; 98 | 99 | foreach ($result['tests'] as $test) 100 | { 101 | $testsuite->appendChild($testcase = $dom->createElement('testcase')); 102 | $testcase->setAttribute('name', $test['message']); 103 | $testcase->setAttribute('file', $test['file']); 104 | $testcase->setAttribute('line', $test['line']); 105 | $testcase->setAttribute('assertions', 1); 106 | if (!$test['status']) 107 | { 108 | $testcase->appendChild($failure = $dom->createElement('failure')); 109 | $failure->setAttribute('type', 'lime'); 110 | if ($test['error']) 111 | { 112 | $failure->appendChild($dom->createTextNode($test['error'])); 113 | } 114 | } 115 | } 116 | } 117 | 118 | $testsuites->setAttribute('failures', $failures); 119 | $testsuites->setAttribute('errors', $errors); 120 | $testsuites->setAttribute('tests', $assertions); 121 | $testsuites->setAttribute('assertions', $assertions); 122 | $testsuites->setAttribute('skipped', $skipped); 123 | 124 | return $dom->saveXml(); 125 | } 126 | 127 | public function __destruct() 128 | { 129 | $plan = $this->results['stats']['plan']; 130 | $passed = count($this->results['stats']['passed']); 131 | $failed = count($this->results['stats']['failed']); 132 | $total = $this->results['stats']['total']; 133 | is_null($plan) and $plan = $total and $this->output->echoln(sprintf("1..%d", $plan)); 134 | 135 | if ($total > $plan) 136 | { 137 | $this->output->red_bar(sprintf("# Looks like you planned %d tests but ran %d extra.", $plan, $total - $plan)); 138 | } 139 | elseif ($total < $plan) 140 | { 141 | $this->output->red_bar(sprintf("# Looks like you planned %d tests but only ran %d.", $plan, $total)); 142 | } 143 | 144 | if ($failed) 145 | { 146 | $this->output->red_bar(sprintf("# Looks like you failed %d tests of %d.", $failed, $passed + $failed)); 147 | } 148 | else if ($total == $plan) 149 | { 150 | $this->output->green_bar("# Looks like everything went fine."); 151 | } 152 | 153 | flush(); 154 | } 155 | 156 | /** 157 | * Tests a condition and passes if it is true 158 | * 159 | * @param mixed $exp condition to test 160 | * @param string $message display output message when the test passes 161 | * 162 | * @return boolean 163 | */ 164 | public function ok($exp, $message = '') 165 | { 166 | $this->update_stats(); 167 | 168 | if ($result = (boolean) $exp) 169 | { 170 | $this->results['stats']['passed'][] = $this->test_nb; 171 | } 172 | else 173 | { 174 | $this->results['stats']['failed'][] = $this->test_nb; 175 | } 176 | $this->results['tests'][$this->test_nb]['message'] = $message; 177 | $this->results['tests'][$this->test_nb]['status'] = $result; 178 | $this->output->echoln(sprintf("%s %d%s", $result ? 'ok' : 'not ok', $this->test_nb, $message = $message ? sprintf('%s %s', 0 === strpos($message, '#') ? '' : ' -', $message) : '')); 179 | 180 | if (!$result) 181 | { 182 | $this->output->diag(sprintf(' Failed test (%s at line %d)', str_replace(getcwd(), '.', $this->results['tests'][$this->test_nb]['file']), $this->results['tests'][$this->test_nb]['line'])); 183 | } 184 | 185 | return $result; 186 | } 187 | 188 | /** 189 | * Compares two values and passes if they are equal (==) 190 | * 191 | * @param mixed $exp1 left value 192 | * @param mixed $exp2 right value 193 | * @param string $message display output message when the test passes 194 | * 195 | * @return boolean 196 | */ 197 | public function is($exp1, $exp2, $message = '') 198 | { 199 | if (is_object($exp1) || is_object($exp2)) 200 | { 201 | $value = $exp1 === $exp2; 202 | } 203 | else if (is_float($exp1) && is_float($exp2)) 204 | { 205 | $value = abs($exp1 - $exp2) < self::EPSILON; 206 | } 207 | else 208 | { 209 | $value = $exp1 == $exp2; 210 | } 211 | 212 | if (!$result = $this->ok($value, $message)) 213 | { 214 | $this->set_last_test_errors(array(sprintf(" got: %s", var_export($exp1, true)), sprintf(" expected: %s", var_export($exp2, true)))); 215 | } 216 | 217 | return $result; 218 | } 219 | 220 | /** 221 | * Compares two values and passes if they are not equal 222 | * 223 | * @param mixed $exp1 left value 224 | * @param mixed $exp2 right value 225 | * @param string $message display output message when the test passes 226 | * 227 | * @return boolean 228 | */ 229 | public function isnt($exp1, $exp2, $message = '') 230 | { 231 | if (!$result = $this->ok($exp1 != $exp2, $message)) 232 | { 233 | $this->set_last_test_errors(array(sprintf(" %s", var_export($exp2, true)), ' ne', sprintf(" %s", var_export($exp2, true)))); 234 | } 235 | 236 | return $result; 237 | } 238 | 239 | /** 240 | * Tests a string against a regular expression 241 | * 242 | * @param string $exp value to test 243 | * @param string $regex the pattern to search for, as a string 244 | * @param string $message display output message when the test passes 245 | * 246 | * @return boolean 247 | */ 248 | public function like($exp, $regex, $message = '') 249 | { 250 | if (!$result = $this->ok(preg_match($regex, $exp), $message)) 251 | { 252 | $this->set_last_test_errors(array(sprintf(" '%s'", $exp), sprintf(" doesn't match '%s'", $regex))); 253 | } 254 | 255 | return $result; 256 | } 257 | 258 | /** 259 | * Checks that a string doesn't match a regular expression 260 | * 261 | * @param string $exp value to test 262 | * @param string $regex the pattern to search for, as a string 263 | * @param string $message display output message when the test passes 264 | * 265 | * @return boolean 266 | */ 267 | public function unlike($exp, $regex, $message = '') 268 | { 269 | if (!$result = $this->ok(!preg_match($regex, $exp), $message)) 270 | { 271 | $this->set_last_test_errors(array(sprintf(" '%s'", $exp), sprintf(" matches '%s'", $regex))); 272 | } 273 | 274 | return $result; 275 | } 276 | 277 | /** 278 | * Compares two arguments with an operator 279 | * 280 | * @param mixed $exp1 left value 281 | * @param string $op operator 282 | * @param mixed $exp2 right value 283 | * @param string $message display output message when the test passes 284 | * 285 | * @return boolean 286 | */ 287 | public function cmp_ok($exp1, $op, $exp2, $message = '') 288 | { 289 | eval(sprintf("\$result = \$exp1 $op \$exp2;")); 290 | if (!$this->ok($result, $message)) 291 | { 292 | $this->set_last_test_errors(array(sprintf(" %s", str_replace("\n", '', var_export($exp1, true))), sprintf(" %s", $op), sprintf(" %s", str_replace("\n", '', var_export($exp2, true))))); 293 | } 294 | 295 | return $result; 296 | } 297 | 298 | /** 299 | * Checks the availability of a method for an object or a class 300 | * 301 | * @param mixed $object an object instance or a class name 302 | * @param string|array $methods one or more method names 303 | * @param string $message display output message when the test passes 304 | * 305 | * @return boolean 306 | */ 307 | public function can_ok($object, $methods, $message = '') 308 | { 309 | $result = true; 310 | $failed_messages = array(); 311 | foreach ((array) $methods as $method) 312 | { 313 | if (!method_exists($object, $method)) 314 | { 315 | $failed_messages[] = sprintf(" method '%s' does not exist", $method); 316 | $result = false; 317 | } 318 | } 319 | 320 | !$this->ok($result, $message); 321 | 322 | !$result and $this->set_last_test_errors($failed_messages); 323 | 324 | return $result; 325 | } 326 | 327 | /** 328 | * Checks the type of an argument 329 | * 330 | * @param mixed $var variable instance 331 | * @param string $class class or type name 332 | * @param string $message display output message when the test passes 333 | * 334 | * @return boolean 335 | */ 336 | public function isa_ok($var, $class, $message = '') 337 | { 338 | $type = is_object($var) ? get_class($var) : gettype($var); 339 | if (!$result = $this->ok($type == $class, $message)) 340 | { 341 | $this->set_last_test_errors(array(sprintf(" variable isn't a '%s' it's a '%s'", $class, $type))); 342 | } 343 | 344 | return $result; 345 | } 346 | 347 | /** 348 | * Checks that two arrays have the same values 349 | * 350 | * @param mixed $exp1 first variable 351 | * @param mixed $exp2 second variable 352 | * @param string $message display output message when the test passes 353 | * 354 | * @return boolean 355 | */ 356 | public function is_deeply($exp1, $exp2, $message = '') 357 | { 358 | if (!$result = $this->ok($this->test_is_deeply($exp1, $exp2), $message)) 359 | { 360 | $this->set_last_test_errors(array(sprintf(" got: %s", str_replace("\n", '', var_export($exp1, true))), sprintf(" expected: %s", str_replace("\n", '', var_export($exp2, true))))); 361 | } 362 | 363 | return $result; 364 | } 365 | 366 | /** 367 | * Always passes--useful for testing exceptions 368 | * 369 | * @param string $message display output message 370 | * 371 | * @return true 372 | */ 373 | public function pass($message = '') 374 | { 375 | return $this->ok(true, $message); 376 | } 377 | 378 | /** 379 | * Always fails--useful for testing exceptions 380 | * 381 | * @param string $message display output message 382 | * 383 | * @return false 384 | */ 385 | public function fail($message = '') 386 | { 387 | return $this->ok(false, $message); 388 | } 389 | 390 | /** 391 | * Outputs a diag message but runs no test 392 | * 393 | * @param string $message display output message 394 | * 395 | * @return void 396 | */ 397 | public function diag($message) 398 | { 399 | $this->output->diag($message); 400 | } 401 | 402 | /** 403 | * Counts as $nb_tests tests--useful for conditional tests 404 | * 405 | * @param string $message display output message 406 | * @param integer $nb_tests number of tests to skip 407 | * 408 | * @return void 409 | */ 410 | public function skip($message = '', $nb_tests = 1) 411 | { 412 | for ($i = 0; $i < $nb_tests; $i++) 413 | { 414 | $this->pass(sprintf("# SKIP%s", $message ? ' '.$message : '')); 415 | $this->results['stats']['skipped'][] = $this->test_nb; 416 | array_pop($this->results['stats']['passed']); 417 | } 418 | } 419 | 420 | /** 421 | * Counts as a test--useful for tests yet to be written 422 | * 423 | * @param string $message display output message 424 | * 425 | * @return void 426 | */ 427 | public function todo($message = '') 428 | { 429 | $this->pass(sprintf("# TODO%s", $message ? ' '.$message : '')); 430 | $this->results['stats']['skipped'][] = $this->test_nb; 431 | array_pop($this->results['stats']['passed']); 432 | } 433 | 434 | /** 435 | * Validates that a file exists and that it is properly included 436 | * 437 | * @param string $file file path 438 | * @param string $message display output message when the test passes 439 | * 440 | * @return boolean 441 | */ 442 | public function include_ok($file, $message = '') 443 | { 444 | if (!$result = $this->ok((@include($file)) == 1, $message)) 445 | { 446 | $this->set_last_test_errors(array(sprintf(" Tried to include '%s'", $file))); 447 | } 448 | 449 | return $result; 450 | } 451 | 452 | private function test_is_deeply($var1, $var2) 453 | { 454 | if (gettype($var1) != gettype($var2)) 455 | { 456 | return false; 457 | } 458 | 459 | if (is_array($var1)) 460 | { 461 | ksort($var1); 462 | ksort($var2); 463 | 464 | $keys1 = array_keys($var1); 465 | $keys2 = array_keys($var2); 466 | if (array_diff($keys1, $keys2) || array_diff($keys2, $keys1)) 467 | { 468 | return false; 469 | } 470 | $is_equal = true; 471 | foreach ($var1 as $key => $value) 472 | { 473 | $is_equal = $this->test_is_deeply($var1[$key], $var2[$key]); 474 | if ($is_equal === false) 475 | { 476 | break; 477 | } 478 | } 479 | 480 | return $is_equal; 481 | } 482 | else 483 | { 484 | return $var1 === $var2; 485 | } 486 | } 487 | 488 | public function comment($message) 489 | { 490 | $this->output->comment($message); 491 | } 492 | 493 | public function info($message) 494 | { 495 | $this->output->info($message); 496 | } 497 | 498 | public function error($message) 499 | { 500 | $this->output->error($message); 501 | } 502 | 503 | protected function update_stats() 504 | { 505 | ++$this->test_nb; 506 | ++$this->results['stats']['total']; 507 | 508 | list($this->results['tests'][$this->test_nb]['file'], $this->results['tests'][$this->test_nb]['line']) = $this->find_caller(debug_backtrace()); 509 | } 510 | 511 | protected function set_last_test_errors(array $errors) 512 | { 513 | $this->output->diag($errors); 514 | 515 | $this->results['tests'][$this->test_nb]['error'] = implode("\n", $errors); 516 | } 517 | 518 | protected function find_caller($traces) 519 | { 520 | // find the first call to a method of an object that is an instance of lime_test 521 | $t = array_reverse($traces); 522 | foreach ($t as $trace) 523 | { 524 | if (isset($trace['object']) && $trace['object'] instanceof lime_test) 525 | { 526 | return array($trace['file'], $trace['line']); 527 | } 528 | } 529 | 530 | // return the first call 531 | $last = count($traces) - 1; 532 | return array($traces[$last]['file'], $traces[$last]['line']); 533 | } 534 | } 535 | 536 | class lime_output 537 | { 538 | public $colorizer = null; 539 | 540 | public function __construct($force_colors = false) 541 | { 542 | $this->colorizer = new lime_colorizer($force_colors); 543 | } 544 | 545 | public function diag() 546 | { 547 | $messages = func_get_args(); 548 | foreach ($messages as $message) 549 | { 550 | echo $this->colorizer->colorize('# '.join("\n# ", (array) $message), 'COMMENT')."\n"; 551 | } 552 | } 553 | 554 | public function comment($message) 555 | { 556 | echo $this->colorizer->colorize(sprintf('# %s', $message), 'COMMENT')."\n"; 557 | } 558 | 559 | public function info($message) 560 | { 561 | echo $this->colorizer->colorize(sprintf('> %s', $message), 'INFO_BAR')."\n"; 562 | } 563 | 564 | public function error($message) 565 | { 566 | echo $this->colorizer->colorize(sprintf(' %s ', $message), 'RED_BAR')."\n"; 567 | } 568 | 569 | public function echoln($message, $colorizer_parameter = null, $colorize = true) 570 | { 571 | if ($colorize) 572 | { 573 | $message = preg_replace('/(?:^|\.)((?:not ok|dubious) *\d*)\b/e', '$this->colorizer->colorize(\'$1\', \'ERROR\')', $message); 574 | $message = preg_replace('/(?:^|\.)(ok *\d*)\b/e', '$this->colorizer->colorize(\'$1\', \'INFO\')', $message); 575 | $message = preg_replace('/"(.+?)"/e', '$this->colorizer->colorize(\'$1\', \'PARAMETER\')', $message); 576 | $message = preg_replace('/(\->|\:\:)?([a-zA-Z0-9_]+?)\(\)/e', '$this->colorizer->colorize(\'$1$2()\', \'PARAMETER\')', $message); 577 | } 578 | 579 | echo ($colorizer_parameter ? $this->colorizer->colorize($message, $colorizer_parameter) : $message)."\n"; 580 | } 581 | 582 | public function green_bar($message) 583 | { 584 | echo $this->colorizer->colorize($message.str_repeat(' ', 71 - min(71, strlen($message))), 'GREEN_BAR')."\n"; 585 | } 586 | 587 | public function red_bar($message) 588 | { 589 | echo $this->colorizer->colorize($message.str_repeat(' ', 71 - min(71, strlen($message))), 'RED_BAR')."\n"; 590 | } 591 | } 592 | 593 | class lime_output_color extends lime_output 594 | { 595 | } 596 | 597 | class lime_colorizer 598 | { 599 | static public $styles = array(); 600 | 601 | protected $colors_supported = false; 602 | 603 | public function __construct($force_colors = false) 604 | { 605 | if ($force_colors) 606 | { 607 | $this->colors_supported = true; 608 | } 609 | else 610 | { 611 | // colors are supported on windows with ansicon or on tty consoles 612 | if (DIRECTORY_SEPARATOR == '\\') 613 | { 614 | $this->colors_supported = false !== getenv('ANSICON'); 615 | } 616 | else 617 | { 618 | $this->colors_supported = function_exists('posix_isatty') && @posix_isatty(STDOUT); 619 | } 620 | } 621 | } 622 | 623 | public static function style($name, $options = array()) 624 | { 625 | self::$styles[$name] = $options; 626 | } 627 | 628 | public function colorize($text = '', $parameters = array()) 629 | { 630 | 631 | if (!$this->colors_supported) 632 | { 633 | return $text; 634 | } 635 | 636 | static $options = array('bold' => 1, 'underscore' => 4, 'blink' => 5, 'reverse' => 7, 'conceal' => 8); 637 | static $foreground = array('black' => 30, 'red' => 31, 'green' => 32, 'yellow' => 33, 'blue' => 34, 'magenta' => 35, 'cyan' => 36, 'white' => 37); 638 | static $background = array('black' => 40, 'red' => 41, 'green' => 42, 'yellow' => 43, 'blue' => 44, 'magenta' => 45, 'cyan' => 46, 'white' => 47); 639 | 640 | !is_array($parameters) && isset(self::$styles[$parameters]) and $parameters = self::$styles[$parameters]; 641 | 642 | $codes = array(); 643 | isset($parameters['fg']) and $codes[] = $foreground[$parameters['fg']]; 644 | isset($parameters['bg']) and $codes[] = $background[$parameters['bg']]; 645 | foreach ($options as $option => $value) 646 | { 647 | isset($parameters[$option]) && $parameters[$option] and $codes[] = $value; 648 | } 649 | 650 | return "\033[".implode(';', $codes).'m'.$text."\033[0m"; 651 | } 652 | } 653 | 654 | lime_colorizer::style('ERROR', array('bg' => 'red', 'fg' => 'white', 'bold' => true)); 655 | lime_colorizer::style('INFO', array('fg' => 'green', 'bold' => true)); 656 | lime_colorizer::style('PARAMETER', array('fg' => 'cyan')); 657 | lime_colorizer::style('COMMENT', array('fg' => 'yellow')); 658 | 659 | lime_colorizer::style('GREEN_BAR', array('fg' => 'white', 'bg' => 'green', 'bold' => true)); 660 | lime_colorizer::style('RED_BAR', array('fg' => 'white', 'bg' => 'red', 'bold' => true)); 661 | lime_colorizer::style('INFO_BAR', array('fg' => 'cyan', 'bold' => true)); 662 | 663 | class lime_harness extends lime_registration 664 | { 665 | public $options = array(); 666 | public $php_cli = null; 667 | public $stats = array(); 668 | public $output = null; 669 | 670 | public function __construct($options = array()) 671 | { 672 | // for BC 673 | if (!is_array($options)) 674 | { 675 | $options = array('output' => $options); 676 | } 677 | 678 | $this->options = array_merge(array( 679 | 'php_cli' => null, 680 | 'force_colors' => false, 681 | 'output' => null, 682 | 'verbose' => false, 683 | ), $options); 684 | 685 | $this->php_cli = $this->find_php_cli($this->options['php_cli']); 686 | $this->output = $this->options['output'] ? $this->options['output'] : new lime_output($this->options['force_colors']); 687 | } 688 | 689 | protected function find_php_cli($php_cli = null) 690 | { 691 | if (is_null($php_cli)) 692 | { 693 | if (getenv('PHP_PATH')) 694 | { 695 | $php_cli = getenv('PHP_PATH'); 696 | 697 | if (!is_executable($php_cli)) 698 | { 699 | throw new Exception('The defined PHP_PATH environment variable is not a valid PHP executable.'); 700 | } 701 | } 702 | else 703 | { 704 | $php_cli = PHP_BINDIR.DIRECTORY_SEPARATOR.'php'; 705 | } 706 | } 707 | 708 | if (is_executable($php_cli)) 709 | { 710 | return $php_cli; 711 | } 712 | 713 | $path = getenv('PATH') ? getenv('PATH') : getenv('Path'); 714 | $exe_suffixes = DIRECTORY_SEPARATOR == '\\' ? (getenv('PATHEXT') ? explode(PATH_SEPARATOR, getenv('PATHEXT')) : array('.exe', '.bat', '.cmd', '.com')) : array(''); 715 | foreach (array('php5', 'php') as $php_cli) 716 | { 717 | foreach ($exe_suffixes as $suffix) 718 | { 719 | foreach (explode(PATH_SEPARATOR, $path) as $dir) 720 | { 721 | $file = $dir.DIRECTORY_SEPARATOR.$php_cli.$suffix; 722 | if (is_executable($file)) 723 | { 724 | return $file; 725 | } 726 | } 727 | } 728 | } 729 | 730 | throw new Exception("Unable to find PHP executable."); 731 | } 732 | 733 | public function to_array() 734 | { 735 | $results = array(); 736 | foreach ($this->stats['files'] as $file => $stat) 737 | { 738 | $results = array_merge($results, $stat['output']); 739 | } 740 | 741 | return $results; 742 | } 743 | 744 | public function to_xml() 745 | { 746 | return lime_test::to_xml($this->to_array()); 747 | } 748 | 749 | public function run() 750 | { 751 | if (!count($this->files)) 752 | { 753 | throw new Exception('You must register some test files before running them!'); 754 | } 755 | 756 | // sort the files to be able to predict the order 757 | sort($this->files); 758 | 759 | $this->stats = array( 760 | 'files' => array(), 761 | 'failed_files' => array(), 762 | 'failed_tests' => 0, 763 | 'total' => 0, 764 | ); 765 | 766 | foreach ($this->files as $file) 767 | { 768 | $this->stats['files'][$file] = array(); 769 | $stats = &$this->stats['files'][$file]; 770 | 771 | $relative_file = $this->get_relative_file($file); 772 | 773 | $test_file = tempnam(sys_get_temp_dir(), 'lime'); 774 | $result_file = tempnam(sys_get_temp_dir(), 'lime'); 775 | file_put_contents($test_file, <<&1', escapeshellarg($this->php_cli), escapeshellarg($test_file)), $return); 785 | ob_end_clean(); 786 | unlink($test_file); 787 | 788 | $output = file_get_contents($result_file); 789 | $stats['output'] = $output ? unserialize($output) : ''; 790 | if (!$stats['output']) 791 | { 792 | $stats['output'] = array(array('file' => $file, 'tests' => array(), 'stats' => array('plan' => 1, 'total' => 1, 'failed' => array(0), 'passed' => array(), 'skipped' => array()))); 793 | } 794 | unlink($result_file); 795 | 796 | $file_stats = &$stats['output'][0]['stats']; 797 | 798 | $delta = 0; 799 | if ($return > 0) 800 | { 801 | $stats['status'] = 'dubious'; 802 | $stats['status_code'] = $return; 803 | } 804 | else 805 | { 806 | $this->stats['total'] += $file_stats['total']; 807 | 808 | if (!$file_stats['plan']) 809 | { 810 | $file_stats['plan'] = $file_stats['total']; 811 | } 812 | 813 | $delta = $file_stats['plan'] - $file_stats['total']; 814 | if (0 != $delta) 815 | { 816 | $stats['status'] = 'dubious'; 817 | $stats['status_code'] = 255; 818 | } 819 | else 820 | { 821 | $stats['status'] = $file_stats['failed'] ? 'not ok' : 'ok'; 822 | $stats['status_code'] = 0; 823 | } 824 | } 825 | 826 | $this->output->echoln(sprintf('%s%s%s', substr($relative_file, -min(67, strlen($relative_file))), str_repeat('.', 70 - min(67, strlen($relative_file))), $stats['status'])); 827 | 828 | if (0 != $stats['status_code']) 829 | { 830 | $this->output->echoln(sprintf(' Test returned status %s', $stats['status_code'])); 831 | } 832 | 833 | if ('ok' != $stats['status']) 834 | { 835 | $this->stats['failed_files'][] = $file; 836 | } 837 | 838 | if ($delta > 0) 839 | { 840 | $this->output->echoln(sprintf(' Looks like you planned %d tests but only ran %d.', $file_stats['plan'], $file_stats['total'])); 841 | 842 | $this->stats['failed_tests'] += $delta; 843 | $this->stats['total'] += $delta; 844 | } 845 | else if ($delta < 0) 846 | { 847 | $this->output->echoln(sprintf(' Looks like you planned %s test but ran %s extra.', $file_stats['plan'], $file_stats['total'] - $file_stats['plan'])); 848 | } 849 | 850 | if (false !== $file_stats && $file_stats['failed']) 851 | { 852 | $this->stats['failed_tests'] += count($file_stats['failed']); 853 | 854 | $this->output->echoln(sprintf(" Failed tests: %s", implode(', ', $file_stats['failed']))); 855 | } 856 | } 857 | 858 | if (count($this->stats['failed_files'])) 859 | { 860 | $format = "%-30s %4s %5s %5s %s"; 861 | $this->output->echoln(sprintf($format, 'Failed Test', 'Stat', 'Total', 'Fail', 'List of Failed')); 862 | $this->output->echoln("------------------------------------------------------------------"); 863 | foreach ($this->stats['files'] as $file => $stat) 864 | { 865 | if (!in_array($file, $this->stats['failed_files'])) 866 | { 867 | continue; 868 | } 869 | $relative_file = $this->get_relative_file($file); 870 | 871 | if (isset($stat['output'][0])) 872 | { 873 | $this->output->echoln(sprintf($format, substr($relative_file, -min(30, strlen($relative_file))), $stat['status_code'], count($stat['output'][0]['stats']['failed']) + count($stat['output'][0]['stats']['passed']), count($stat['output'][0]['stats']['failed']), implode(' ', $stat['output'][0]['stats']['failed']))); 874 | } 875 | else 876 | { 877 | $this->output->echoln(sprintf($format, substr($relative_file, -min(30, strlen($relative_file))), $stat['status_code'], '', '', '')); 878 | } 879 | } 880 | 881 | $this->output->red_bar(sprintf('Failed %d/%d test scripts, %.2f%% okay. %d/%d subtests failed, %.2f%% okay.', 882 | $nb_failed_files = count($this->stats['failed_files']), 883 | $nb_files = count($this->files), 884 | ($nb_files - $nb_failed_files) * 100 / $nb_files, 885 | $nb_failed_tests = $this->stats['failed_tests'], 886 | $nb_tests = $this->stats['total'], 887 | $nb_tests > 0 ? ($nb_tests - $nb_failed_tests) * 100 / $nb_tests : 0 888 | )); 889 | 890 | if ($this->options['verbose']) 891 | { 892 | foreach ($this->to_array() as $testsuite) 893 | { 894 | $first = true; 895 | foreach ($testsuite['stats']['failed'] as $testcase) 896 | { 897 | if (!isset($testsuite['tests'][$testcase]['file'])) 898 | { 899 | continue; 900 | } 901 | 902 | if ($first) 903 | { 904 | $this->output->echoln(''); 905 | $this->output->error($this->get_relative_file($testsuite['file']).$this->extension); 906 | $first = false; 907 | } 908 | 909 | $this->output->comment(sprintf(' at %s line %s', $this->get_relative_file($testsuite['tests'][$testcase]['file']).$this->extension, $testsuite['tests'][$testcase]['line'])); 910 | $this->output->info(' '.$testsuite['tests'][$testcase]['message']); 911 | $this->output->echoln($testsuite['tests'][$testcase]['error'], null, false); 912 | } 913 | } 914 | } 915 | } 916 | else 917 | { 918 | $this->output->green_bar(' All tests successful.'); 919 | $this->output->green_bar(sprintf(' Files=%d, Tests=%d', count($this->files), $this->stats['total'])); 920 | } 921 | 922 | return $this->stats['failed_files'] ? false : true; 923 | } 924 | 925 | public function get_failed_files() 926 | { 927 | return isset($this->stats['failed_files']) ? $this->stats['failed_files'] : array(); 928 | } 929 | } 930 | 931 | class lime_coverage extends lime_registration 932 | { 933 | public $files = array(); 934 | public $extension = '.php'; 935 | public $base_dir = ''; 936 | public $harness = null; 937 | public $verbose = false; 938 | protected $coverage = array(); 939 | 940 | public function __construct($harness) 941 | { 942 | $this->harness = $harness; 943 | 944 | if (!function_exists('xdebug_start_code_coverage')) 945 | { 946 | throw new Exception('You must install and enable xdebug before using lime coverage.'); 947 | } 948 | 949 | if (!ini_get('xdebug.extended_info')) 950 | { 951 | throw new Exception('You must set xdebug.extended_info to 1 in your php.ini to use lime coverage.'); 952 | } 953 | } 954 | 955 | public function run() 956 | { 957 | if (!count($this->harness->files)) 958 | { 959 | throw new Exception('You must register some test files before running coverage!'); 960 | } 961 | 962 | if (!count($this->files)) 963 | { 964 | throw new Exception('You must register some files to cover!'); 965 | } 966 | 967 | $this->coverage = array(); 968 | 969 | $this->process($this->harness->files); 970 | 971 | $this->output($this->files); 972 | } 973 | 974 | public function process($files) 975 | { 976 | if (!is_array($files)) 977 | { 978 | $files = array($files); 979 | } 980 | 981 | $tmp_file = sys_get_temp_dir().DIRECTORY_SEPARATOR.'test.php'; 982 | foreach ($files as $file) 983 | { 984 | $tmp = <<'.serialize(xdebug_get_code_coverage()).''; 989 | EOF; 990 | file_put_contents($tmp_file, $tmp); 991 | ob_start(); 992 | // see http://trac.symfony-project.org/ticket/5437 for the explanation on the weird "cd" thing 993 | passthru(sprintf('cd & %s %s 2>&1', escapeshellarg($this->harness->php_cli), escapeshellarg($tmp_file)), $return); 994 | $retval = ob_get_clean(); 995 | 996 | if (0 != $return) // test exited without success 997 | { 998 | // something may have gone wrong, we should warn the user so they know 999 | // it's a bug in their code and not symfony's 1000 | 1001 | $this->harness->output->echoln(sprintf('Warning: %s returned status %d, results may be inaccurate', $file, $return), 'ERROR'); 1002 | } 1003 | 1004 | if (false === $cov = @unserialize(substr($retval, strpos($retval, '') + 9, strpos($retval, '') - 9))) 1005 | { 1006 | if (0 == $return) 1007 | { 1008 | // failed to serialize, but PHP said it should of worked. 1009 | // something is seriously wrong, so abort with exception 1010 | throw new Exception(sprintf('Unable to unserialize coverage for file "%s"', $file)); 1011 | } 1012 | else 1013 | { 1014 | // failed to serialize, but PHP warned us that this might have happened. 1015 | // so we should ignore and move on 1016 | continue; // continue foreach loop through $this->harness->files 1017 | } 1018 | } 1019 | 1020 | foreach ($cov as $file => $lines) 1021 | { 1022 | if (!isset($this->coverage[$file])) 1023 | { 1024 | $this->coverage[$file] = $lines; 1025 | continue; 1026 | } 1027 | 1028 | foreach ($lines as $line => $flag) 1029 | { 1030 | if ($flag == 1) 1031 | { 1032 | $this->coverage[$file][$line] = 1; 1033 | } 1034 | } 1035 | } 1036 | } 1037 | 1038 | if (file_exists($tmp_file)) 1039 | { 1040 | unlink($tmp_file); 1041 | } 1042 | } 1043 | 1044 | public function output($files) 1045 | { 1046 | ksort($this->coverage); 1047 | $total_php_lines = 0; 1048 | $total_covered_lines = 0; 1049 | foreach ($files as $file) 1050 | { 1051 | $file = realpath($file); 1052 | $is_covered = isset($this->coverage[$file]); 1053 | $cov = isset($this->coverage[$file]) ? $this->coverage[$file] : array(); 1054 | $covered_lines = array(); 1055 | $missing_lines = array(); 1056 | 1057 | foreach ($cov as $line => $flag) 1058 | { 1059 | switch ($flag) 1060 | { 1061 | case 1: 1062 | $covered_lines[] = $line; 1063 | break; 1064 | case -1: 1065 | $missing_lines[] = $line; 1066 | break; 1067 | } 1068 | } 1069 | 1070 | $total_lines = count($covered_lines) + count($missing_lines); 1071 | if (!$total_lines) 1072 | { 1073 | // probably means that the file is not covered at all! 1074 | $total_lines = count($this->get_php_lines(file_get_contents($file))); 1075 | } 1076 | 1077 | $output = $this->harness->output; 1078 | $percent = $total_lines ? count($covered_lines) * 100 / $total_lines : 0; 1079 | 1080 | $total_php_lines += $total_lines; 1081 | $total_covered_lines += count($covered_lines); 1082 | 1083 | $relative_file = $this->get_relative_file($file); 1084 | $output->echoln(sprintf("%-70s %3.0f%%", substr($relative_file, -min(70, strlen($relative_file))), $percent), $percent == 100 ? 'INFO' : ($percent > 90 ? 'PARAMETER' : ($percent < 20 ? 'ERROR' : ''))); 1085 | if ($this->verbose && $is_covered && $percent != 100) 1086 | { 1087 | $output->comment(sprintf("missing: %s", $this->format_range($missing_lines))); 1088 | } 1089 | } 1090 | 1091 | $output->echoln(sprintf("TOTAL COVERAGE: %3.0f%%", $total_php_lines ? $total_covered_lines * 100 / $total_php_lines : 0)); 1092 | } 1093 | 1094 | public static function get_php_lines($content) 1095 | { 1096 | if (is_readable($content)) 1097 | { 1098 | $content = file_get_contents($content); 1099 | } 1100 | 1101 | $tokens = token_get_all($content); 1102 | $php_lines = array(); 1103 | $current_line = 1; 1104 | $in_class = false; 1105 | $in_function = false; 1106 | $in_function_declaration = false; 1107 | $end_of_current_expr = true; 1108 | $open_braces = 0; 1109 | foreach ($tokens as $token) 1110 | { 1111 | if (is_string($token)) 1112 | { 1113 | switch ($token) 1114 | { 1115 | case '=': 1116 | if (false === $in_class || (false !== $in_function && !$in_function_declaration)) 1117 | { 1118 | $php_lines[$current_line] = true; 1119 | } 1120 | break; 1121 | case '{': 1122 | ++$open_braces; 1123 | $in_function_declaration = false; 1124 | break; 1125 | case ';': 1126 | $in_function_declaration = false; 1127 | $end_of_current_expr = true; 1128 | break; 1129 | case '}': 1130 | $end_of_current_expr = true; 1131 | --$open_braces; 1132 | if ($open_braces == $in_class) 1133 | { 1134 | $in_class = false; 1135 | } 1136 | if ($open_braces == $in_function) 1137 | { 1138 | $in_function = false; 1139 | } 1140 | break; 1141 | } 1142 | 1143 | continue; 1144 | } 1145 | 1146 | list($id, $text) = $token; 1147 | 1148 | switch ($id) 1149 | { 1150 | case T_CURLY_OPEN: 1151 | case T_DOLLAR_OPEN_CURLY_BRACES: 1152 | ++$open_braces; 1153 | break; 1154 | case T_WHITESPACE: 1155 | case T_OPEN_TAG: 1156 | case T_CLOSE_TAG: 1157 | $end_of_current_expr = true; 1158 | $current_line += count(explode("\n", $text)) - 1; 1159 | break; 1160 | case T_COMMENT: 1161 | case T_DOC_COMMENT: 1162 | $current_line += count(explode("\n", $text)) - 1; 1163 | break; 1164 | case T_CLASS: 1165 | $in_class = $open_braces; 1166 | break; 1167 | case T_FUNCTION: 1168 | $in_function = $open_braces; 1169 | $in_function_declaration = true; 1170 | break; 1171 | case T_AND_EQUAL: 1172 | case T_BREAK: 1173 | case T_CASE: 1174 | case T_CATCH: 1175 | case T_CLONE: 1176 | case T_CONCAT_EQUAL: 1177 | case T_CONTINUE: 1178 | case T_DEC: 1179 | case T_DECLARE: 1180 | case T_DEFAULT: 1181 | case T_DIV_EQUAL: 1182 | case T_DO: 1183 | case T_ECHO: 1184 | case T_ELSEIF: 1185 | case T_EMPTY: 1186 | case T_ENDDECLARE: 1187 | case T_ENDFOR: 1188 | case T_ENDFOREACH: 1189 | case T_ENDIF: 1190 | case T_ENDSWITCH: 1191 | case T_ENDWHILE: 1192 | case T_EVAL: 1193 | case T_EXIT: 1194 | case T_FOR: 1195 | case T_FOREACH: 1196 | case T_GLOBAL: 1197 | case T_IF: 1198 | case T_INC: 1199 | case T_INCLUDE: 1200 | case T_INCLUDE_ONCE: 1201 | case T_INSTANCEOF: 1202 | case T_ISSET: 1203 | case T_IS_EQUAL: 1204 | case T_IS_GREATER_OR_EQUAL: 1205 | case T_IS_IDENTICAL: 1206 | case T_IS_NOT_EQUAL: 1207 | case T_IS_NOT_IDENTICAL: 1208 | case T_IS_SMALLER_OR_EQUAL: 1209 | case T_LIST: 1210 | case T_LOGICAL_AND: 1211 | case T_LOGICAL_OR: 1212 | case T_LOGICAL_XOR: 1213 | case T_MINUS_EQUAL: 1214 | case T_MOD_EQUAL: 1215 | case T_MUL_EQUAL: 1216 | case T_NEW: 1217 | case T_OBJECT_OPERATOR: 1218 | case T_OR_EQUAL: 1219 | case T_PLUS_EQUAL: 1220 | case T_PRINT: 1221 | case T_REQUIRE: 1222 | case T_REQUIRE_ONCE: 1223 | case T_RETURN: 1224 | case T_SL: 1225 | case T_SL_EQUAL: 1226 | case T_SR: 1227 | case T_SR_EQUAL: 1228 | case T_SWITCH: 1229 | case T_THROW: 1230 | case T_TRY: 1231 | case T_UNSET: 1232 | case T_UNSET_CAST: 1233 | case T_USE: 1234 | case T_WHILE: 1235 | case T_XOR_EQUAL: 1236 | $php_lines[$current_line] = true; 1237 | $end_of_current_expr = false; 1238 | break; 1239 | default: 1240 | if (false === $end_of_current_expr) 1241 | { 1242 | $php_lines[$current_line] = true; 1243 | } 1244 | } 1245 | } 1246 | 1247 | return $php_lines; 1248 | } 1249 | 1250 | public function compute($content, $cov) 1251 | { 1252 | $php_lines = self::get_php_lines($content); 1253 | 1254 | // we remove from $cov non php lines 1255 | foreach (array_diff_key($cov, $php_lines) as $line => $tmp) 1256 | { 1257 | unset($cov[$line]); 1258 | } 1259 | 1260 | return array($cov, $php_lines); 1261 | } 1262 | 1263 | public function format_range($lines) 1264 | { 1265 | sort($lines); 1266 | $formatted = ''; 1267 | $first = -1; 1268 | $last = -1; 1269 | foreach ($lines as $line) 1270 | { 1271 | if ($last + 1 != $line) 1272 | { 1273 | if ($first != -1) 1274 | { 1275 | $formatted .= $first == $last ? "$first " : "[$first - $last] "; 1276 | } 1277 | $first = $line; 1278 | $last = $line; 1279 | } 1280 | else 1281 | { 1282 | $last = $line; 1283 | } 1284 | } 1285 | if ($first != -1) 1286 | { 1287 | $formatted .= $first == $last ? "$first " : "[$first - $last] "; 1288 | } 1289 | 1290 | return $formatted; 1291 | } 1292 | } 1293 | 1294 | class lime_registration 1295 | { 1296 | public $files = array(); 1297 | public $extension = '.php'; 1298 | public $base_dir = ''; 1299 | 1300 | public function register($files_or_directories) 1301 | { 1302 | foreach ((array) $files_or_directories as $f_or_d) 1303 | { 1304 | if (is_file($f_or_d)) 1305 | { 1306 | $this->files[] = realpath($f_or_d); 1307 | } 1308 | elseif (is_dir($f_or_d)) 1309 | { 1310 | $this->register_dir($f_or_d); 1311 | } 1312 | else 1313 | { 1314 | throw new Exception(sprintf('The file or directory "%s" does not exist.', $f_or_d)); 1315 | } 1316 | } 1317 | } 1318 | 1319 | public function register_glob($glob) 1320 | { 1321 | if ($dirs = glob($glob)) 1322 | { 1323 | foreach ($dirs as $file) 1324 | { 1325 | $this->files[] = realpath($file); 1326 | } 1327 | } 1328 | } 1329 | 1330 | public function register_dir($directory) 1331 | { 1332 | if (!is_dir($directory)) 1333 | { 1334 | throw new Exception(sprintf('The directory "%s" does not exist.', $directory)); 1335 | } 1336 | 1337 | $files = array(); 1338 | 1339 | $current_dir = opendir($directory); 1340 | while ($entry = readdir($current_dir)) 1341 | { 1342 | if ($entry == '.' || $entry == '..') continue; 1343 | 1344 | if (is_dir($entry)) 1345 | { 1346 | $this->register_dir($entry); 1347 | } 1348 | elseif (preg_match('#'.$this->extension.'$#', $entry)) 1349 | { 1350 | $files[] = realpath($directory.DIRECTORY_SEPARATOR.$entry); 1351 | } 1352 | } 1353 | 1354 | $this->files = array_merge($this->files, $files); 1355 | } 1356 | 1357 | protected function get_relative_file($file) 1358 | { 1359 | return str_replace(DIRECTORY_SEPARATOR, '/', str_replace(array(realpath($this->base_dir).DIRECTORY_SEPARATOR, $this->extension), '', $file)); 1360 | } 1361 | } 1362 | --------------------------------------------------------------------------------