├── .github ├── funding.yml └── workflows │ ├── build.yml │ └── frontbot.yml ├── .gitignore ├── Makefile ├── code-checker.neon ├── composer.json ├── license.md ├── phpstan.neon ├── readme.md ├── src ├── CommandProcessor.php ├── Commit.php ├── CommitId.php ├── Git.php ├── GitRepository.php ├── Helpers.php ├── IRunner.php ├── RunnerResult.php ├── Runners │ ├── CliRunner.php │ ├── MemoryRunner.php │ └── OldGitRunner.php └── exceptions.php └── tests ├── .gitignore ├── GitPhp ├── CommandProcessor.phpt ├── CommitId.phpt ├── GitRepository.branches.phpt ├── GitRepository.commit.phpt ├── GitRepository.directory.phpt ├── GitRepository.execute.phpt ├── GitRepository.files.phpt ├── GitRepository.getBranches.phpt ├── GitRepository.getCommit.phpt ├── GitRepository.getCurrentBranchName.phpt ├── GitRepository.getLastCommitId.phpt ├── GitRepository.getLocalBranches.phpt ├── GitRepository.getRemoteBranches.phpt ├── GitRepository.getTags.phpt ├── GitRepository.remotes.phpt ├── GitRepository.tags.phpt ├── Helpers.extractRepositoryNameFromUrl.phpt ├── OldGitRunner.phpt ├── RunnerResult.phpt ├── bootstrap.php └── fixtures │ ├── file1.txt │ ├── file2.txt │ ├── file3.txt │ ├── file4.txt │ └── file5.txt └── libs └── AssertRunner.php /.github/funding.yml: -------------------------------------------------------------------------------- 1 | custom: ["https://donate.stripe.com/7sIcO2a9maTSg2A9AA", "https://www.janpecha.cz/donate/git-php/"] 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - 'v*' 9 | 10 | pull_request: 11 | 12 | jobs: 13 | tests: 14 | uses: janpecha/actions/.github/workflows/nette-tester-library.yml@master 15 | with: 16 | phpVersions: '["8.0", "8.1", "8.2", "8.3", "8.4"]' 17 | lowestDependencies: true 18 | 19 | coding-style: 20 | uses: janpecha/actions/.github/workflows/code-checker.yml@master 21 | 22 | static-analysis: 23 | uses: janpecha/actions/.github/workflows/phpstan.yml@master 24 | -------------------------------------------------------------------------------- /.github/workflows/frontbot.yml: -------------------------------------------------------------------------------- 1 | name: FrontBot 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 0 * * 4' # every thursday at 0:00 7 | 8 | jobs: 9 | frontbot: 10 | uses: janpecha/actions/.github/workflows/frontbot.yml@master 11 | with: 12 | committer: "Jan Pecha " 13 | secrets: 14 | FRONTBOT_TOKEN: ${{ secrets.FRONTBOT_TOKEN }} 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /tests/tmp/ 3 | /composer.lock 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | src_dir = src/ 2 | tester_bin = vendor/bin/tester 3 | tests_dir = tests/ 4 | coverage_name = $(tests_dir)coverage.html 5 | php_bin = php 6 | phpstan_bin = phpstan 7 | 8 | .PHONY: test coverage clean phpstan 9 | test: 10 | @$(tester_bin) -p $(php_bin) -C $(tests_dir) 11 | 12 | coverage: 13 | @$(tester_bin) -p $(php_bin) -C -d zend_extension=xdebug --coverage $(coverage_name) --coverage-src $(src_dir) $(tests_dir) 14 | 15 | clean: 16 | @rm -f $(coverage_name) 17 | 18 | phpstan: 19 | @$(phpstan_bin) 20 | -------------------------------------------------------------------------------- /code-checker.neon: -------------------------------------------------------------------------------- 1 | sets: 2 | - JP\CodeChecker\Sets\CzProjectMinimum 3 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "czproject/git-php", 3 | "type": "library", 4 | "description": "Library for work with Git repository in PHP.", 5 | "keywords": ["git"], 6 | "license": "BSD-3-Clause", 7 | "authors": [ 8 | { 9 | "name": "Jan Pecha", 10 | "email": "janpecha@email.cz" 11 | } 12 | ], 13 | "funding": [ 14 | {"type": "stripe", "url": "https://donate.stripe.com/7sIcO2a9maTSg2A9AA"}, 15 | {"type": "other", "url": "https://www.janpecha.cz/donate/git-php/"} 16 | ], 17 | "require": { 18 | "php": "8.0 - 8.4" 19 | }, 20 | "autoload": { 21 | "classmap": ["src/"] 22 | }, 23 | "require-dev": { 24 | "nette/tester": "^2.4" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | New BSD License 2 | --------------- 3 | 4 | Copyright © 2013 Jan Pecha (https://www.janpecha.cz/) All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, 7 | are permitted provided that the following conditions are met: 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | * Neither the name of Jan Pecha nor the names of its contributors may be used to 14 | endorse or promote products derived from this software without specific prior 15 | written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 24 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: max 3 | treatPhpDocTypesAsCertain: false 4 | 5 | paths: 6 | - src 7 | - tests 8 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Git-PHP 2 | ======= 3 | 4 | [![Build Status](https://github.com/czproject/git-php/workflows/Build/badge.svg)](https://github.com/czproject/git-php/actions) 5 | [![Downloads this Month](https://img.shields.io/packagist/dm/czproject/git-php.svg)](https://packagist.org/packages/czproject/git-php) 6 | [![Latest Stable Version](https://poser.pugx.org/czproject/git-php/v/stable)](https://github.com/czproject/git-php/releases) 7 | [![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://github.com/czproject/git-php/blob/master/license.md) 8 | 9 | 10 | Library for work with Git repository in PHP. 11 | 12 | Donate 13 | 14 | 15 | Installation 16 | ------------ 17 | 18 | [Download a latest package](https://github.com/czproject/git-php/releases) or use [Composer](http://getcomposer.org/): 19 | 20 | ``` 21 | composer require czproject/git-php 22 | ``` 23 | 24 | Library requires PHP 8.0 or later and `git` client (path to Git must be in system variable `PATH`). 25 | 26 | Git installers: 27 | 28 | * for Linux - https://git-scm.com/download/linux 29 | * for Windows - https://git-scm.com/download/win 30 | * for others - https://git-scm.com/downloads 31 | 32 | 33 | Usage 34 | ----- 35 | 36 | ``` php 37 | $git = new CzProject\GitPhp\Git; 38 | // create repo object 39 | $repo = $git->open('/path/to/repo'); 40 | 41 | // create a new file in repo 42 | $filename = $repo->getRepositoryPath() . '/readme.txt'; 43 | file_put_contents($filename, "Lorem ipsum 44 | dolor 45 | sit amet 46 | "); 47 | 48 | // commit 49 | $repo->addFile($filename); 50 | $repo->commit('init commit'); 51 | ``` 52 | 53 | 54 | Initialization of empty repository 55 | ---------------------------------- 56 | 57 | ``` php 58 | $repo = $git->init('/path/to/repo-directory'); 59 | ``` 60 | 61 | With parameters: 62 | 63 | ``` php 64 | $repo = $git->init('/path/to/repo-directory', [ 65 | '--bare', // creates bare repo 66 | ]); 67 | ``` 68 | 69 | 70 | Cloning of repository 71 | --------------------- 72 | 73 | ``` php 74 | // Cloning of repository into subdirectory 'git-php' in current working directory 75 | $repo = $git->cloneRepository('https://github.com/czproject/git-php.git'); 76 | 77 | // Cloning of repository into own directory 78 | $repo = $git->cloneRepository('https://github.com/czproject/git-php.git', '/path/to/my/subdir'); 79 | ``` 80 | 81 | 82 | Basic operations 83 | ---------------- 84 | 85 | ``` php 86 | $repo->hasChanges(); // returns boolean 87 | $repo->commit('commit message'); 88 | $repo->merge('branch-name'); 89 | $repo->checkout('master'); 90 | 91 | $repo->getRepositoryPath(); 92 | 93 | // adds files into commit 94 | $repo->addFile('file.txt'); 95 | $repo->addFile('file1.txt', 'file2.txt'); 96 | $repo->addFile(['file3.txt', 'file4.txt']); 97 | 98 | // renames files in repository 99 | $repo->renameFile('old.txt', 'new.txt'); 100 | $repo->renameFile([ 101 | 'old1.txt' => 'new1.txt', 102 | 'old2.txt' => 'new2.txt', 103 | ]); 104 | 105 | // removes files from repository 106 | $repo->removeFile('file.txt'); 107 | $repo->removeFile('file1.txt', 'file2.txt'); 108 | $repo->removeFile(['file3.txt', 'file4.txt']); 109 | 110 | // adds all changes in repository 111 | $repo->addAllChanges(); 112 | ``` 113 | 114 | 115 | 116 | Branches 117 | -------- 118 | 119 | ``` php 120 | // gets list of all repository branches (remotes & locals) 121 | $repo->getBranches(); 122 | 123 | // gets list of all local branches 124 | $repo->getLocalBranches(); 125 | 126 | // gets name of current branch 127 | $repo->getCurrentBranchName(); 128 | 129 | // creates new branch 130 | $repo->createBranch('new-branch'); 131 | 132 | // creates new branch and checkout 133 | $repo->createBranch('patch-1', TRUE); 134 | 135 | // removes branch 136 | $repo->removeBranch('branch-name'); 137 | ``` 138 | 139 | 140 | Tags 141 | ---- 142 | 143 | ``` php 144 | // gets list of all tags in repository 145 | $repo->getTags(); 146 | 147 | // creates new tag 148 | $repo->createTag('v1.0.0'); 149 | $repo->createTag('v1.0.0', $options); 150 | $repo->createTag('v1.0.0', [ 151 | '-m' => 'message', 152 | ]); 153 | 154 | // renames tag 155 | $repo->renameTag('old-tag-name', 'new-tag-name'); 156 | 157 | // removes tag 158 | $repo->removeTag('tag-name'); 159 | ``` 160 | 161 | 162 | History 163 | ------- 164 | 165 | ``` php 166 | // returns last commit ID on current branch 167 | $commitId = $repo->getLastCommitId(); 168 | $commitId->getId(); // or (string) $commitId 169 | 170 | // returns commit data 171 | $commit = $repo->getCommit('734713bc047d87bf7eac9674765ae793478c50d3'); 172 | $commit->getId(); // instance of CommitId 173 | $commit->getSubject(); 174 | $commit->getBody(); 175 | $commit->getAuthorName(); 176 | $commit->getAuthorEmail(); 177 | $commit->getAuthorDate(); 178 | $commit->getCommitterName(); 179 | $commit->getCommitterEmail(); 180 | $commit->getCommitterDate(); 181 | $commit->getDate(); 182 | 183 | // returns commit data of last commit on current branch 184 | $commit = $repo->getLastCommit(); 185 | ``` 186 | 187 | 188 | Remotes 189 | ------- 190 | 191 | ``` php 192 | // pulls changes from remote 193 | $repo->pull('remote-name', ['--options']); 194 | $repo->pull('origin'); 195 | 196 | // pushs changes to remote 197 | $repo->push('remote-name', ['--options']); 198 | $repo->push('origin'); 199 | $repo->push(['origin', 'master'], ['-u']); 200 | 201 | // fetchs changes from remote 202 | $repo->fetch('remote-name', ['--options']); 203 | $repo->fetch('origin'); 204 | $repo->fetch(['origin', 'master']); 205 | 206 | // adds remote repository 207 | $repo->addRemote('remote-name', 'repository-url', ['--options']); 208 | $repo->addRemote('origin', 'git@github.com:czproject/git-php.git'); 209 | 210 | // renames remote 211 | $repo->renameRemote('old-remote-name', 'new-remote-name'); 212 | $repo->renameRemote('origin', 'upstream'); 213 | 214 | // removes remote 215 | $repo->removeRemote('remote-name'); 216 | $repo->removeRemote('origin'); 217 | 218 | // changes remote URL 219 | $repo->setRemoteUrl('remote-name', 'new-repository-url'); 220 | $repo->setRemoteUrl('upstream', 'https://github.com/czproject/git-php.git'); 221 | ``` 222 | 223 | **Troubleshooting - How to provide username and password for commands** 224 | 225 | 1) use SSH instead of HTTPS - https://stackoverflow.com/a/8588786 226 | 2) store credentials to *Git Credential Storage* 227 | * http://www.tilcode.com/push-github-without-entering-username-password-windows-git-bash/ 228 | * https://help.github.com/articles/caching-your-github-password-in-git/ 229 | * https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage 230 | 3) insert user and password into remote URL - https://stackoverflow.com/a/16381160 231 | * `git remote add origin https://user:password@server/path/repo.git` 232 | 4) for `push()` you can use `--repo` argument - https://stackoverflow.com/a/12193555 233 | * `$git->push(NULL, ['--repo' => 'https://user:password@server/path/repo.git']);` 234 | 235 | 236 | Other commands 237 | -------------- 238 | 239 | For running other commands you can use `execute` method: 240 | 241 | ```php 242 | $output = $repo->execute('command'); 243 | $output = $repo->execute('command', 'with', 'parameters'); 244 | 245 | // example: 246 | $repo->execute('remote', 'set-branches', $originName, $branches); 247 | ``` 248 | 249 | 250 | Custom methods 251 | -------------- 252 | 253 | You can create custom methods. For example: 254 | 255 | ``` php 256 | class OwnGit extends \CzProject\GitPhp\Git 257 | { 258 | public function open($directory) 259 | { 260 | return new OwnGitRepository($directory, $this->runner); 261 | } 262 | } 263 | 264 | class OwnGitRepository extends \CzProject\GitPhp\GitRepository 265 | { 266 | public function setRemoteBranches($name, array $branches) 267 | { 268 | $this->run('remote', 'set-branches', $name, $branches); 269 | return $this; 270 | } 271 | } 272 | 273 | 274 | $git = new OwnGit; 275 | $repo = $git->open('/path/to/repo'); 276 | $repo->addRemote('origin', 'repository-url'); 277 | $repo->setRemoteBranches('origin', [ 278 | 'branch-1', 279 | 'branch-2', 280 | ]); 281 | ``` 282 | 283 | ------------------------------ 284 | 285 | License: [New BSD License](license.md) 286 |
Author: Jan Pecha, https://www.janpecha.cz/ 287 | -------------------------------------------------------------------------------- /src/CommandProcessor.php: -------------------------------------------------------------------------------- 1 | isWindows = FALSE; 25 | 26 | } elseif ($mode === self::MODE_WINDOWS) { 27 | $this->isWindows = TRUE; 28 | 29 | } elseif ($mode === self::MODE_DETECT) { 30 | $this->isWindows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; 31 | 32 | } else { 33 | throw new InvalidArgumentException("Invalid mode '$mode'."); 34 | } 35 | } 36 | 37 | 38 | /** 39 | * @param string $app 40 | * @param array $args 41 | * @param array|NULL $env 42 | * @return string 43 | */ 44 | public function process($app, array $args, ?array $env = NULL) 45 | { 46 | $cmd = []; 47 | 48 | foreach ($args as $arg) { 49 | if (is_array($arg)) { 50 | foreach ($arg as $key => $value) { 51 | $_c = ''; 52 | 53 | if (is_string($key)) { 54 | $_c = "$key "; 55 | } 56 | 57 | if (is_bool($value)) { 58 | $value = $value ? '1' : '0'; 59 | 60 | } elseif ($value instanceof CommitId) { 61 | $value = $value->toString(); 62 | 63 | } elseif ($value === NULL) { 64 | // ignored 65 | continue; 66 | 67 | } elseif (!is_scalar($value)) { 68 | throw new InvalidStateException('Unknow option value type ' . (is_object($value) ? get_class($value) : gettype($value)) . '.'); 69 | } 70 | 71 | $cmd[] = $_c . $this->escapeArgument((string) $value); 72 | } 73 | 74 | } elseif (is_scalar($arg) && !is_bool($arg)) { 75 | $cmd[] = $this->escapeArgument((string) $arg); 76 | 77 | } elseif ($arg === NULL) { 78 | // ignored 79 | 80 | } elseif ($arg instanceof CommitId) { 81 | $cmd[] = $arg->toString(); 82 | 83 | } else { 84 | throw new InvalidStateException('Unknow argument type ' . (is_object($arg) ? get_class($arg) : gettype($arg)) . '.'); 85 | } 86 | } 87 | 88 | $envPrefix = ''; 89 | 90 | if ($env !== NULL) { 91 | foreach ($env as $envVar => $envValue) { 92 | if ($this->isWindows) { 93 | $envPrefix .= 'set ' . $envVar . '=' . $envValue . ' && '; 94 | 95 | } else { 96 | $envPrefix .= $envVar . '=' . $envValue . ' '; 97 | } 98 | } 99 | } 100 | 101 | return $envPrefix . $app . ' ' . implode(' ', $cmd); 102 | } 103 | 104 | 105 | /** 106 | * @param string $value 107 | * @return string 108 | */ 109 | private function escapeArgument($value) 110 | { 111 | // inspired by Nette Tester 112 | if (preg_match('#^[a-z0-9._-]+\z#i', $value)) { 113 | return $value; 114 | } 115 | 116 | if ($this->isWindows) { 117 | return '"' . str_replace('"', '""', $value) . '"'; 118 | } 119 | 120 | return escapeshellarg($value); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Commit.php: -------------------------------------------------------------------------------- 1 | id = $id; 59 | $this->subject = $subject; 60 | $this->body = $body; 61 | $this->authorEmail = $authorEmail; 62 | $this->authorName = $authorName; 63 | $this->authorDate = $authorDate; 64 | $this->committerEmail = $committerEmail; 65 | $this->committerName = $committerName; 66 | $this->committerDate = $committerDate; 67 | } 68 | 69 | 70 | /** 71 | * @return CommitId 72 | */ 73 | public function getId() 74 | { 75 | return $this->id; 76 | } 77 | 78 | 79 | /** 80 | * @return string 81 | */ 82 | public function getSubject() 83 | { 84 | return $this->subject; 85 | } 86 | 87 | 88 | /** 89 | * @return string|NULL 90 | */ 91 | public function getBody() 92 | { 93 | return $this->body; 94 | } 95 | 96 | 97 | /** 98 | * @return string|NULL 99 | */ 100 | public function getAuthorName() 101 | { 102 | return $this->authorName; 103 | } 104 | 105 | 106 | /** 107 | * @return string 108 | */ 109 | public function getAuthorEmail() 110 | { 111 | return $this->authorEmail; 112 | } 113 | 114 | 115 | /** 116 | * @return \DateTimeImmutable 117 | */ 118 | public function getAuthorDate() 119 | { 120 | return $this->authorDate; 121 | } 122 | 123 | 124 | /** 125 | * @return string|NULL 126 | */ 127 | public function getCommitterName() 128 | { 129 | return $this->committerName; 130 | } 131 | 132 | 133 | /** 134 | * @return string 135 | */ 136 | public function getCommitterEmail() 137 | { 138 | return $this->committerEmail; 139 | } 140 | 141 | 142 | /** 143 | * @return \DateTimeImmutable 144 | */ 145 | public function getCommitterDate() 146 | { 147 | return $this->committerDate; 148 | } 149 | 150 | 151 | /** 152 | * Alias for getAuthorDate() 153 | * @return \DateTimeImmutable 154 | */ 155 | public function getDate() 156 | { 157 | return $this->authorDate; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/CommitId.php: -------------------------------------------------------------------------------- 1 | id = $id; 24 | } 25 | 26 | 27 | /** 28 | * @return string 29 | */ 30 | public function toString() 31 | { 32 | return $this->id; 33 | } 34 | 35 | 36 | /** 37 | * @return string 38 | */ 39 | public function __toString() 40 | { 41 | return $this->id; 42 | } 43 | 44 | 45 | /** 46 | * @param string $id 47 | * @return bool 48 | */ 49 | public static function isValid($id) 50 | { 51 | return is_string($id) && preg_match('/^[0-9a-f]{40}$/i', $id); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Git.php: -------------------------------------------------------------------------------- 1 | runner = $runner !== NULL ? $runner : new Runners\CliRunner; 17 | } 18 | 19 | 20 | /** 21 | * @param string $directory 22 | * @return GitRepository 23 | */ 24 | public function open($directory) 25 | { 26 | return new GitRepository($directory, $this->runner); 27 | } 28 | 29 | 30 | /** 31 | * Init repo in directory 32 | * @param string $directory 33 | * @param array|NULL $params 34 | * @return GitRepository 35 | * @throws GitException 36 | */ 37 | public function init($directory, ?array $params = NULL) 38 | { 39 | if (is_dir("$directory/.git")) { 40 | throw new GitException("Repo already exists in $directory."); 41 | } 42 | 43 | if (!is_dir($directory) && !@mkdir($directory, 0777, TRUE)) { // intentionally @; not atomic; from Nette FW 44 | throw new GitException("Unable to create directory '$directory'."); 45 | } 46 | 47 | try { 48 | $this->run($directory, [ 49 | 'init', 50 | $params, 51 | '--end-of-options', 52 | $directory 53 | ]); 54 | 55 | } catch (GitException $e) { 56 | throw new GitException("Git init failed (directory $directory).", $e->getCode(), $e); 57 | } 58 | 59 | return $this->open($directory); 60 | } 61 | 62 | 63 | /** 64 | * Clones GIT repository from $url into $directory 65 | * @param string $url 66 | * @param string|NULL $directory 67 | * @param array|NULL $params 68 | * @return GitRepository 69 | * @throws GitException 70 | */ 71 | public function cloneRepository($url, $directory = NULL, ?array $params = NULL) 72 | { 73 | if ($directory !== NULL && is_dir("$directory/.git")) { 74 | throw new GitException("Repo already exists in $directory."); 75 | } 76 | 77 | $cwd = $this->runner->getCwd(); 78 | 79 | if ($directory === NULL) { 80 | $directory = Helpers::extractRepositoryNameFromUrl($url); 81 | $directory = "$cwd/$directory"; 82 | 83 | } elseif(!Helpers::isAbsolute($directory)) { 84 | $directory = "$cwd/$directory"; 85 | } 86 | 87 | if ($params === NULL) { 88 | $params = '-q'; 89 | } 90 | 91 | try { 92 | $this->run($cwd, [ 93 | 'clone', 94 | $params, 95 | '--end-of-options', 96 | $url, 97 | $directory 98 | ]); 99 | 100 | } catch (GitException $e) { 101 | $stderr = ''; 102 | $result = $e->getRunnerResult(); 103 | 104 | if ($result !== NULL && $result->hasErrorOutput()) { 105 | $stderr = implode(PHP_EOL, $result->getErrorOutput()); 106 | } 107 | 108 | throw new GitException("Git clone failed (directory $directory)." . ($stderr !== '' ? ("\n$stderr") : '')); 109 | } 110 | 111 | return $this->open($directory); 112 | } 113 | 114 | 115 | /** 116 | * @param string $url 117 | * @param array|NULL $refs 118 | * @return bool 119 | */ 120 | public function isRemoteUrlReadable($url, ?array $refs = NULL) 121 | { 122 | $result = $this->runner->run($this->runner->getCwd(), [ 123 | 'ls-remote', 124 | '--heads', 125 | '--quiet', 126 | '--end-of-options', 127 | $url, 128 | $refs, 129 | ], [ 130 | 'GIT_TERMINAL_PROMPT' => 0, 131 | ]); 132 | 133 | return $result->isOk(); 134 | } 135 | 136 | 137 | /** 138 | * @param string $cwd 139 | * @param array $args 140 | * @param array $env 141 | * @return RunnerResult 142 | * @throws GitException 143 | */ 144 | private function run($cwd, array $args, ?array $env = NULL) 145 | { 146 | $result = $this->runner->run($cwd, $args, $env); 147 | 148 | if (!$result->isOk()) { 149 | throw new GitException("Command '{$result->getCommand()}' failed (exit-code {$result->getExitCode()}).", $result->getExitCode(), NULL, $result); 150 | } 151 | 152 | return $result; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/GitRepository.php: -------------------------------------------------------------------------------- 1 | repository = $path; 34 | $this->runner = $runner !== NULL ? $runner : new Runners\CliRunner; 35 | } 36 | 37 | 38 | /** 39 | * @return string 40 | */ 41 | public function getRepositoryPath() 42 | { 43 | return $this->repository; 44 | } 45 | 46 | 47 | /** 48 | * Creates a tag. 49 | * `git tag ` 50 | * @param string $name 51 | * @param array|NULL $options 52 | * @throws GitException 53 | * @return static 54 | */ 55 | public function createTag($name, $options = NULL) 56 | { 57 | $this->run('tag', $options, '--end-of-options', $name); 58 | return $this; 59 | } 60 | 61 | 62 | /** 63 | * Removes tag. 64 | * `git tag -d ` 65 | * @param string $name 66 | * @throws GitException 67 | * @return static 68 | */ 69 | public function removeTag($name) 70 | { 71 | $this->run('tag', [ 72 | '-d' => $name, 73 | ]); 74 | return $this; 75 | } 76 | 77 | 78 | /** 79 | * Renames tag. 80 | * `git tag ` 81 | * `git tag -d ` 82 | * @param string $oldName 83 | * @param string $newName 84 | * @throws GitException 85 | * @return static 86 | */ 87 | public function renameTag($oldName, $newName) 88 | { 89 | // http://stackoverflow.com/a/1873932 90 | // create new as alias to old (`git tag NEW OLD`) 91 | $this->run('tag', '--end-of-options', $newName, $oldName); 92 | // delete old (`git tag -d OLD`) 93 | $this->removeTag($oldName); 94 | return $this; 95 | } 96 | 97 | 98 | /** 99 | * Returns list of tags in repo. 100 | * @return string[]|NULL NULL => no tags 101 | * @throws GitException 102 | */ 103 | public function getTags() 104 | { 105 | return $this->extractFromCommand(['tag'], 'trim'); 106 | } 107 | 108 | 109 | /** 110 | * Merges branches. 111 | * `git merge ` 112 | * @param string $branch 113 | * @param array|NULL $options 114 | * @throws GitException 115 | * @return static 116 | */ 117 | public function merge($branch, $options = NULL) 118 | { 119 | $this->run('merge', $options, '--end-of-options', $branch); 120 | return $this; 121 | } 122 | 123 | 124 | /** 125 | * Creates new branch. 126 | * `git branch ` 127 | * (optionaly) `git checkout ` 128 | * @param string $name 129 | * @param bool $checkout 130 | * @throws GitException 131 | * @return static 132 | */ 133 | public function createBranch($name, $checkout = FALSE) 134 | { 135 | // git branch $name 136 | $this->run('branch', '--end-of-options', $name); 137 | 138 | if ($checkout) { 139 | $this->checkout($name); 140 | } 141 | 142 | return $this; 143 | } 144 | 145 | 146 | /** 147 | * Removes branch. 148 | * `git branch -d ` 149 | * @param string $name 150 | * @throws GitException 151 | * @return static 152 | */ 153 | public function removeBranch($name) 154 | { 155 | $this->run('branch', [ 156 | '-d' => $name, 157 | ]); 158 | return $this; 159 | } 160 | 161 | 162 | /** 163 | * Gets name of current branch 164 | * `git branch` + magic 165 | * @return string 166 | * @throws GitException 167 | */ 168 | public function getCurrentBranchName() 169 | { 170 | try { 171 | $branch = $this->extractFromCommand(['branch', '-a', '--no-color'], function($value) { 172 | if (isset($value[0]) && $value[0] === '*') { 173 | return trim(substr($value, 1)); 174 | } 175 | 176 | return FALSE; 177 | }); 178 | 179 | if (is_array($branch)) { 180 | return $branch[0]; 181 | } 182 | 183 | } catch (GitException $e) { 184 | // nothing 185 | } 186 | 187 | throw new GitException('Getting of current branch name failed.'); 188 | } 189 | 190 | 191 | /** 192 | * Returns list of all (local & remote) branches in repo. 193 | * @return string[]|NULL NULL => no branches 194 | * @throws GitException 195 | */ 196 | public function getBranches() 197 | { 198 | return $this->extractFromCommand(['branch', '-a', '--no-color'], function($value) { 199 | return trim(substr($value, 1)); 200 | }); 201 | } 202 | 203 | 204 | /** 205 | * Returns list of remote branches in repo. 206 | * @return string[]|NULL NULL => no branches 207 | * @throws GitException 208 | */ 209 | public function getRemoteBranches() 210 | { 211 | return $this->extractFromCommand(['branch', '-r', '--no-color'], function($value) { 212 | return trim(substr($value, 1)); 213 | }); 214 | } 215 | 216 | 217 | /** 218 | * Returns list of local branches in repo. 219 | * @return string[]|NULL NULL => no branches 220 | * @throws GitException 221 | */ 222 | public function getLocalBranches() 223 | { 224 | return $this->extractFromCommand(['branch', '--no-color'], function($value) { 225 | return trim(substr($value, 1)); 226 | }); 227 | } 228 | 229 | 230 | /** 231 | * Checkout branch. 232 | * `git checkout ` 233 | * @param string $name 234 | * @throws GitException 235 | * @return static 236 | */ 237 | public function checkout($name) 238 | { 239 | if (!is_string($name)) { 240 | throw new InvalidArgumentException('Branch name must be string.'); 241 | } 242 | 243 | if ($name === '') { 244 | throw new InvalidArgumentException('Branch name cannot be empty.'); 245 | } 246 | 247 | if ($name[0] === '-') { 248 | throw new InvalidArgumentException('Branch name cannot be option name.'); 249 | } 250 | 251 | $this->run('checkout', $name); 252 | return $this; 253 | } 254 | 255 | 256 | /** 257 | * Removes file(s). 258 | * `git rm ` 259 | * @param string|string[] $file 260 | * @throws GitException 261 | * @return static 262 | */ 263 | public function removeFile($file) 264 | { 265 | if (!is_array($file)) { 266 | $file = func_get_args(); 267 | } 268 | 269 | foreach ($file as $item) { 270 | $this->run('rm', '-r', '--end-of-options', $item); 271 | } 272 | 273 | return $this; 274 | } 275 | 276 | 277 | /** 278 | * Adds file(s). 279 | * `git add ` 280 | * @param string|string[] $file 281 | * @throws GitException 282 | * @return static 283 | */ 284 | public function addFile($file) 285 | { 286 | if (!is_array($file)) { 287 | $file = func_get_args(); 288 | } 289 | 290 | foreach ($file as $item) { 291 | assert(is_string($item)); 292 | 293 | // make sure the given item exists 294 | // this can be a file or an directory, git supports both 295 | $path = Helpers::isAbsolute($item) ? $item : ($this->getRepositoryPath() . DIRECTORY_SEPARATOR . $item); 296 | 297 | if (!file_exists($path)) { 298 | throw new GitException("The path at '$item' does not represent a valid file."); 299 | } 300 | 301 | $this->run('add', '--end-of-options', $item); 302 | } 303 | 304 | return $this; 305 | } 306 | 307 | 308 | /** 309 | * Adds all created, modified & removed files. 310 | * `git add --all` 311 | * @throws GitException 312 | * @return static 313 | */ 314 | public function addAllChanges() 315 | { 316 | $this->run('add', '--all'); 317 | return $this; 318 | } 319 | 320 | 321 | /** 322 | * Renames file(s). 323 | * `git mv ` 324 | * @param string|string[] $file from: array('from' => 'to', ...) || (from, to) 325 | * @param string|NULL $to 326 | * @throws GitException 327 | * @return static 328 | */ 329 | public function renameFile($file, $to = NULL) 330 | { 331 | if (!is_array($file)) { // rename(file, to); 332 | $file = [ 333 | $file => $to, 334 | ]; 335 | } 336 | 337 | foreach ($file as $from => $to) { 338 | $this->run('mv', '--end-of-options', $from, $to); 339 | } 340 | 341 | return $this; 342 | } 343 | 344 | 345 | /** 346 | * Commits changes 347 | * `git commit -m ` 348 | * @param string $message 349 | * @param array|NULL $options 350 | * @throws GitException 351 | * @return static 352 | */ 353 | public function commit($message, $options = NULL) 354 | { 355 | $this->run('commit', $options, [ 356 | '-m' => $message, 357 | ]); 358 | return $this; 359 | } 360 | 361 | 362 | /** 363 | * Returns last commit ID on current branch 364 | * `git log --pretty=format:"%H" -n 1` 365 | * @return CommitId 366 | * @throws GitException 367 | */ 368 | public function getLastCommitId() 369 | { 370 | $result = $this->run('log', '--pretty=format:%H', '-n', '1'); 371 | $lastLine = $result->getOutputLastLine(); 372 | return new CommitId((string) $lastLine); 373 | } 374 | 375 | 376 | /** 377 | * @return Commit 378 | */ 379 | public function getLastCommit() 380 | { 381 | return $this->getCommit($this->getLastCommitId()); 382 | } 383 | 384 | 385 | /** 386 | * @param string|CommitId $commitId 387 | * @return Commit 388 | */ 389 | public function getCommit($commitId) 390 | { 391 | if (!($commitId instanceof CommitId)) { 392 | $commitId = new CommitId($commitId); 393 | } 394 | 395 | // subject 396 | $result = $this->run('log', '-1', $commitId, '--format=%s'); 397 | $subject = rtrim($result->getOutputAsString()); 398 | 399 | // body 400 | $result = $this->run('log', '-1', $commitId, '--format=%b'); 401 | $body = rtrim($result->getOutputAsString()); 402 | 403 | // author email 404 | $result = $this->run('log', '-1', $commitId, '--format=%ae'); 405 | $authorEmail = rtrim($result->getOutputAsString()); 406 | 407 | // author name 408 | $result = $this->run('log', '-1', $commitId, '--format=%an'); 409 | $authorName = rtrim($result->getOutputAsString()); 410 | 411 | // author date 412 | $result = $this->run('log', '-1', $commitId, '--pretty=format:%ad', '--date=iso-strict'); 413 | $authorDate = \DateTimeImmutable::createFromFormat(\DateTime::ATOM, (string) $result->getOutputLastLine()); 414 | 415 | if (!($authorDate instanceof \DateTimeImmutable)) { 416 | throw new GitException('Failed fetching of commit author date.', 0, NULL, $result); 417 | } 418 | 419 | // committer email 420 | $result = $this->run('log', '-1', $commitId, '--format=%ce'); 421 | $committerEmail = rtrim($result->getOutputAsString()); 422 | 423 | // committer name 424 | $result = $this->run('log', '-1', $commitId, '--format=%cn'); 425 | $committerName = rtrim($result->getOutputAsString()); 426 | 427 | // committer date 428 | $result = $this->run('log', '-1', $commitId, '--pretty=format:%cd', '--date=iso-strict'); 429 | $committerDate = \DateTimeImmutable::createFromFormat(\DateTime::ATOM, (string) $result->getOutputLastLine()); 430 | 431 | if (!($committerDate instanceof \DateTimeImmutable)) { 432 | throw new GitException('Failed fetching of commit committer date.', 0, NULL, $result); 433 | } 434 | 435 | return new Commit( 436 | $commitId, 437 | $subject, 438 | $body !== '' ? $body : NULL, 439 | $authorEmail, 440 | $authorName !== '' ? $authorName : NULL, 441 | $authorDate, 442 | $committerEmail, 443 | $committerName !== '' ? $committerName : NULL, 444 | $committerDate 445 | ); 446 | } 447 | 448 | 449 | /** 450 | * Exists changes? 451 | * `git status` + magic 452 | * @return bool 453 | * @throws GitException 454 | */ 455 | public function hasChanges() 456 | { 457 | // Make sure the `git status` gets a refreshed look at the working tree. 458 | $this->run('update-index', '-q', '--refresh'); 459 | $result = $this->run('status', '--porcelain'); 460 | return $result->hasOutput(); 461 | } 462 | 463 | 464 | /** 465 | * Pull changes from a remote 466 | * @param string|string[]|NULL $remote 467 | * @param array|NULL $options 468 | * @return static 469 | * @throws GitException 470 | */ 471 | public function pull($remote = NULL, ?array $options = NULL) 472 | { 473 | $this->run('pull', $options, '--end-of-options', $remote); 474 | return $this; 475 | } 476 | 477 | 478 | /** 479 | * Push changes to a remote 480 | * @param string|string[]|NULL $remote 481 | * @param array|NULL $options 482 | * @return static 483 | * @throws GitException 484 | */ 485 | public function push($remote = NULL, ?array $options = NULL) 486 | { 487 | $this->run('push', $options, '--end-of-options', $remote); 488 | return $this; 489 | } 490 | 491 | 492 | /** 493 | * Run fetch command to get latest branches 494 | * @param string|string[]|NULL $remote 495 | * @param array|NULL $options 496 | * @return static 497 | * @throws GitException 498 | */ 499 | public function fetch($remote = NULL, ?array $options = NULL) 500 | { 501 | $this->run('fetch', $options, '--end-of-options', $remote); 502 | return $this; 503 | } 504 | 505 | 506 | /** 507 | * Adds new remote repository 508 | * @param string $name 509 | * @param string $url 510 | * @param array|NULL $options 511 | * @return static 512 | * @throws GitException 513 | */ 514 | public function addRemote($name, $url, ?array $options = NULL) 515 | { 516 | $this->run('remote', 'add', $options, '--end-of-options', $name, $url); 517 | return $this; 518 | } 519 | 520 | 521 | /** 522 | * Renames remote repository 523 | * @param string $oldName 524 | * @param string $newName 525 | * @return static 526 | * @throws GitException 527 | */ 528 | public function renameRemote($oldName, $newName) 529 | { 530 | $this->run('remote', 'rename', '--end-of-options', $oldName, $newName); 531 | return $this; 532 | } 533 | 534 | 535 | /** 536 | * Removes remote repository 537 | * @param string $name 538 | * @return static 539 | * @throws GitException 540 | */ 541 | public function removeRemote($name) 542 | { 543 | $this->run('remote', 'remove', '--end-of-options', $name); 544 | return $this; 545 | } 546 | 547 | 548 | /** 549 | * Changes remote repository URL 550 | * @param string $name 551 | * @param string $url 552 | * @param array|NULL $options 553 | * @return static 554 | * @throws GitException 555 | */ 556 | public function setRemoteUrl($name, $url, ?array $options = NULL) 557 | { 558 | $this->run('remote', 'set-url', $options, '--end-of-options', $name, $url); 559 | return $this; 560 | } 561 | 562 | 563 | /** 564 | * @param mixed ...$cmd 565 | * @return string[] returns output 566 | * @throws GitException 567 | */ 568 | public function execute(...$cmd) 569 | { 570 | $result = $this->run(...$cmd); 571 | return $result->getOutput(); 572 | } 573 | 574 | 575 | /** 576 | * Runs command and returns result. 577 | * @param mixed ...$args 578 | * @return RunnerResult 579 | * @throws GitException 580 | */ 581 | public function run(...$args) 582 | { 583 | $result = $this->runner->run($this->repository, $args); 584 | 585 | if (!$result->isOk()) { 586 | throw new GitException("Command '{$result->getCommand()}' failed (exit-code {$result->getExitCode()}).", $result->getExitCode(), NULL, $result); 587 | } 588 | 589 | return $result; 590 | } 591 | 592 | 593 | /** 594 | * @param array $args 595 | * @param (callable(string $value): (string|FALSE))|NULL $filter 596 | * @return string[]|NULL 597 | * @throws GitException 598 | */ 599 | protected function extractFromCommand(array $args, ?callable $filter = NULL) 600 | { 601 | $result = $this->run(...$args); 602 | $output = $result->getOutput(); 603 | 604 | if ($filter !== NULL) { 605 | $newArray = []; 606 | 607 | foreach ($output as $line) { 608 | $value = $filter($line); 609 | 610 | if ($value === FALSE) { 611 | continue; 612 | } 613 | 614 | $newArray[] = (string) $value; 615 | } 616 | 617 | $output = $newArray; 618 | } 619 | 620 | if (empty($output)) { 621 | return NULL; 622 | } 623 | 624 | return $output; 625 | } 626 | } 627 | -------------------------------------------------------------------------------- /src/Helpers.php: -------------------------------------------------------------------------------- 1 | repo 36 | // host.xz:foo/.git => foo 37 | $directory = rtrim($url, '/'); 38 | 39 | if (substr($directory, -5) === '/.git') { 40 | $directory = substr($directory, 0, -5); 41 | } 42 | 43 | $directory = basename($directory, '.git'); 44 | 45 | if (($pos = strrpos($directory, ':')) !== FALSE) { 46 | $directory = substr($directory, $pos + 1); 47 | } 48 | 49 | return $directory; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/IRunner.php: -------------------------------------------------------------------------------- 1 | $args 13 | * @param array|NULL $env 14 | * @return RunnerResult 15 | */ 16 | function run($cwd, array $args, ?array $env = NULL); 17 | 18 | 19 | /** 20 | * @return string 21 | */ 22 | function getCwd(); 23 | } 24 | -------------------------------------------------------------------------------- /src/RunnerResult.php: -------------------------------------------------------------------------------- 1 | command = (string) $command; 32 | $this->exitCode = (int) $exitCode; 33 | $this->output = $output; 34 | $this->errorOutput = $errorOutput; 35 | } 36 | 37 | 38 | /** 39 | * @return bool 40 | */ 41 | public function isOk() 42 | { 43 | return $this->exitCode === 0; 44 | } 45 | 46 | 47 | /** 48 | * @return string 49 | */ 50 | public function getCommand() 51 | { 52 | return $this->command; 53 | } 54 | 55 | 56 | /** 57 | * @return int 58 | */ 59 | public function getExitCode() 60 | { 61 | return $this->exitCode; 62 | } 63 | 64 | 65 | /** 66 | * @return string[] 67 | */ 68 | public function getOutput() 69 | { 70 | if (is_string($this->output)) { 71 | return $this->splitOutput($this->output); 72 | } 73 | 74 | return $this->output; 75 | } 76 | 77 | 78 | /** 79 | * @return string 80 | */ 81 | public function getOutputAsString() 82 | { 83 | if (is_string($this->output)) { 84 | return $this->output; 85 | } 86 | 87 | return implode("\n", $this->output); 88 | } 89 | 90 | 91 | /** 92 | * @return string|NULL 93 | */ 94 | public function getOutputLastLine() 95 | { 96 | $output = $this->getOutput(); 97 | $lastLine = end($output); 98 | return is_string($lastLine) ? $lastLine : NULL; 99 | } 100 | 101 | 102 | /** 103 | * @return bool 104 | */ 105 | public function hasOutput() 106 | { 107 | if (is_string($this->output)) { 108 | return trim($this->output) !== ''; 109 | } 110 | 111 | return !empty($this->output); 112 | } 113 | 114 | 115 | /** 116 | * @return string[] 117 | */ 118 | public function getErrorOutput() 119 | { 120 | if (is_string($this->errorOutput)) { 121 | return $this->splitOutput($this->errorOutput); 122 | } 123 | 124 | return $this->errorOutput; 125 | } 126 | 127 | 128 | /** 129 | * @return string 130 | */ 131 | public function getErrorOutputAsString() 132 | { 133 | if (is_string($this->errorOutput)) { 134 | return $this->errorOutput; 135 | } 136 | 137 | return implode("\n", $this->errorOutput); 138 | } 139 | 140 | 141 | /** 142 | * @return bool 143 | */ 144 | public function hasErrorOutput() 145 | { 146 | if (is_string($this->errorOutput)) { 147 | return trim($this->errorOutput) !== ''; 148 | } 149 | 150 | return !empty($this->errorOutput); 151 | } 152 | 153 | 154 | /** 155 | * @return string 156 | */ 157 | public function toText() 158 | { 159 | return '$ ' . $this->getCommand() . "\n\n" 160 | . "---- STDOUT: \n\n" 161 | . implode("\n", $this->getOutput()) . "\n\n" 162 | . "---- STDERR: \n\n" 163 | . implode("\n", $this->getErrorOutput()) . "\n\n" 164 | . '=> ' . $this->getExitCode() . "\n\n"; 165 | } 166 | 167 | 168 | /** 169 | * @param string $output 170 | * @return string[] 171 | */ 172 | private function splitOutput($output) 173 | { 174 | $output = str_replace(["\r\n", "\r"], "\n", $output); 175 | $output = rtrim($output, "\n"); 176 | 177 | if ($output === '') { 178 | return []; 179 | } 180 | 181 | return explode("\n", $output); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/Runners/CliRunner.php: -------------------------------------------------------------------------------- 1 | gitBinary = $gitBinary; 28 | $this->commandProcessor = new CommandProcessor; 29 | } 30 | 31 | 32 | /** 33 | * @return RunnerResult 34 | */ 35 | public function run($cwd, array $args, ?array $env = NULL) 36 | { 37 | if (!is_dir($cwd)) { 38 | throw new GitException("Directory '$cwd' not found"); 39 | } 40 | 41 | $descriptorspec = [ 42 | 0 => ['pipe', 'r'], // stdin 43 | 1 => ['pipe', 'w'], // stdout 44 | 2 => ['pipe', 'w'], // stderr 45 | ]; 46 | 47 | $pipes = []; 48 | $command = $this->commandProcessor->process($this->gitBinary, $args); 49 | $process = proc_open($command, $descriptorspec, $pipes, $cwd, $env, [ 50 | 'bypass_shell' => TRUE, 51 | ]); 52 | 53 | if (!$process) { 54 | throw new GitException("Executing of command '$command' failed (directory $cwd)."); 55 | } 56 | 57 | if (!(is_array($pipes) 58 | && isset($pipes[0], $pipes[1], $pipes[2]) 59 | && is_resource($pipes[0]) 60 | && is_resource($pipes[1]) 61 | && is_resource($pipes[2]) 62 | )) { 63 | throw new GitException("Invalid pipes for command '$command' failed (directory $cwd)."); 64 | } 65 | 66 | // Reset output and error 67 | stream_set_blocking($pipes[1], FALSE); 68 | stream_set_blocking($pipes[2], FALSE); 69 | $stdout = ''; 70 | $stderr = ''; 71 | 72 | while (TRUE) { 73 | // Read standard output 74 | $stdoutOutput = stream_get_contents($pipes[1]); 75 | 76 | if (is_string($stdoutOutput)) { 77 | $stdout .= $stdoutOutput; 78 | } 79 | 80 | // Read error output 81 | $stderrOutput = stream_get_contents($pipes[2]); 82 | 83 | if (is_string($stderrOutput)) { 84 | $stderr .= $stderrOutput; 85 | } 86 | 87 | // We are done 88 | if ((feof($pipes[1]) || $stdoutOutput === FALSE) && (feof($pipes[2]) || $stderrOutput === FALSE)) { 89 | break; 90 | } 91 | } 92 | 93 | $returnCode = proc_close($process); 94 | return new RunnerResult($command, $returnCode, $stdout, $stderr); 95 | } 96 | 97 | 98 | /** 99 | * @return string 100 | */ 101 | public function getCwd() 102 | { 103 | $cwd = getcwd(); 104 | 105 | if (!is_string($cwd)) { 106 | throw new \CzProject\GitPhp\InvalidStateException('Getting of CWD failed.'); 107 | } 108 | 109 | return $cwd; 110 | } 111 | 112 | 113 | /** 114 | * @param string $output 115 | * @return string[] 116 | */ 117 | protected function convertOutput($output) 118 | { 119 | $output = str_replace(["\r\n", "\r"], "\n", $output); 120 | $output = rtrim($output, "\n"); 121 | 122 | if ($output === '') { 123 | return []; 124 | } 125 | 126 | return explode("\n", $output); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Runners/MemoryRunner.php: -------------------------------------------------------------------------------- 1 | [command => RunnerResult] */ 22 | private $results = []; 23 | 24 | 25 | /** 26 | * @param string $cwd 27 | */ 28 | public function __construct($cwd) 29 | { 30 | $this->cwd = $cwd; 31 | $this->commandProcessor = new CommandProcessor; 32 | } 33 | 34 | 35 | /** 36 | * @param array $args 37 | * @param array $env 38 | * @param string|array $output 39 | * @param string|array $errorOutput 40 | * @param int $exitCode 41 | * @return self 42 | */ 43 | public function setResult(array $args, array $env, $output, $errorOutput = [], $exitCode = 0) 44 | { 45 | $cmd = $this->commandProcessor->process('git', $args, $env); 46 | $this->results[$cmd] = new RunnerResult($cmd, $exitCode, $output, $errorOutput); 47 | return $this; 48 | } 49 | 50 | 51 | /** 52 | * @return RunnerResult 53 | */ 54 | public function run($cwd, array $args, ?array $env = NULL) 55 | { 56 | $cmd = $this->commandProcessor->process('git', $args, $env); 57 | 58 | if (!isset($this->results[$cmd])) { 59 | throw new \CzProject\GitPhp\InvalidStateException("Missing result for command '$cmd'."); 60 | } 61 | 62 | return $this->results[$cmd]; 63 | } 64 | 65 | 66 | /** 67 | * @return string 68 | */ 69 | public function getCwd() 70 | { 71 | return $this->cwd; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Runners/OldGitRunner.php: -------------------------------------------------------------------------------- 1 | runner = $runner !== NULL ? $runner : new CliRunner; 19 | } 20 | 21 | 22 | public function run($cwd, array $args, ?array $env = NULL) 23 | { 24 | if (($key = array_search('--end-of-options', $args)) !== FALSE) { 25 | unset($args[$key]); 26 | } 27 | 28 | return $this->runner->run($cwd, $args, $env); 29 | } 30 | 31 | 32 | public function getCwd() 33 | { 34 | return $this->runner->getCwd(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/exceptions.php: -------------------------------------------------------------------------------- 1 | runnerResult = $runnerResult; 27 | } 28 | 29 | 30 | /** 31 | * @return RunnerResult|NULL 32 | */ 33 | public function getRunnerResult() 34 | { 35 | return $this->runnerResult; 36 | } 37 | } 38 | 39 | 40 | class InvalidArgumentException extends Exception 41 | { 42 | } 43 | 44 | 45 | class InvalidStateException extends Exception 46 | { 47 | } 48 | 49 | 50 | class StaticClassException extends Exception 51 | { 52 | } 53 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | **/output/ 2 | coverage.html 3 | -------------------------------------------------------------------------------- /tests/GitPhp/CommandProcessor.phpt: -------------------------------------------------------------------------------- 1 | TRUE, 19 | '--third' => '"go"', 20 | ], 21 | ]; 22 | 23 | $env = [ 24 | 'ENV_1' => 'value1', 25 | 'ENV_2' => 'value2', 26 | ]; 27 | 28 | $processor = new CommandProcessor(CommandProcessor::MODE_WINDOWS); 29 | Assert::same('set ENV_1=value1 && set ENV_2=value2 && git first --second 1 --third """go"""', $processor->process('git', $options, $env)); 30 | 31 | $processor = new CommandProcessor(CommandProcessor::MODE_NON_WINDOWS); 32 | Assert::same('ENV_1=value1 ENV_2=value2 git first --second 1 --third \'"go"\'', $processor->process('git', $options, $env)); 33 | }); 34 | 35 | 36 | test(function () { 37 | $options = [ 38 | 'first', 39 | [ 40 | '--second' => new CommitId('734713bc047d87bf7eac9674765ae793478c50d3'), 41 | '--one' => TRUE, 42 | '--two' => FALSE, 43 | '--three' => NULL, 44 | TRUE, 45 | FALSE, 46 | NULL, 47 | ], 48 | NULL, 49 | 'arg', 50 | new CommitId('734713bc047d87bf7eac9674765ae793478c50d3'), 51 | ]; 52 | 53 | $processor = new CommandProcessor(CommandProcessor::MODE_NON_WINDOWS); 54 | Assert::same(implode(' ', [ 55 | 'git', 56 | 'first', 57 | '--second 734713bc047d87bf7eac9674765ae793478c50d3', 58 | '--one 1', 59 | '--two 0', 60 | '1', 61 | '0', 62 | 'arg', 63 | '734713bc047d87bf7eac9674765ae793478c50d3', 64 | ]), $processor->process('git', $options)); 65 | }); 66 | 67 | 68 | test(function () { 69 | $processor = new CommandProcessor(CommandProcessor::MODE_NON_WINDOWS); 70 | 71 | Assert::exception(function () use ($processor) { 72 | $processor->process('git', [ 73 | (object) [], 74 | ]); 75 | }, InvalidStateException::class, 'Unknow argument type stdClass.'); 76 | 77 | Assert::exception(function () use ($processor) { 78 | $processor->process('git', [ 79 | TRUE, 80 | ]); 81 | }, InvalidStateException::class, 'Unknow argument type boolean.'); 82 | 83 | Assert::exception(function () use ($processor) { 84 | $processor->process('git', [ 85 | FALSE, 86 | ]); 87 | }, InvalidStateException::class, 'Unknow argument type boolean.'); 88 | 89 | Assert::exception(function () use ($processor) { 90 | $processor->process('git', [ 91 | [ 92 | (object) [], 93 | ], 94 | ]); 95 | }, InvalidStateException::class, 'Unknow option value type stdClass.'); 96 | }); 97 | 98 | 99 | test(function () { 100 | 101 | Assert::exception(function () { 102 | $processor = new CommandProcessor('INVALID'); 103 | 104 | }, InvalidArgumentException::class, "Invalid mode 'INVALID'."); 105 | }); 106 | -------------------------------------------------------------------------------- /tests/GitPhp/CommitId.phpt: -------------------------------------------------------------------------------- 1 | toString()); 14 | 15 | 16 | Assert::exception(function () { 17 | new CommitId('test'); 18 | 19 | }, InvalidArgumentException::class, "Invalid commit ID 'test'."); 20 | 21 | 22 | Assert::exception(function () { 23 | new CommitId([]); 24 | 25 | }, InvalidArgumentException::class, "Invalid commit ID, expected string, array given."); 26 | -------------------------------------------------------------------------------- /tests/GitPhp/GitRepository.branches.phpt: -------------------------------------------------------------------------------- 1 | assert(['branch', '--end-of-options', 'master']); 16 | $runner->assert(['branch', '--end-of-options', 'develop']); 17 | $runner->assert(['checkout', 'develop']); 18 | $runner->assert(['merge', '--end-of-options', 'feature-1']); 19 | $runner->assert(['branch', '-d', 'feature-1']); 20 | $runner->assert(['checkout', 'master']); 21 | 22 | $repo = $git->open(__DIR__); 23 | $repo->createBranch('master'); 24 | $repo->createBranch('develop', TRUE); 25 | $repo->merge('feature-1'); 26 | $repo->removeBranch('feature-1'); 27 | $repo->checkout('master'); 28 | -------------------------------------------------------------------------------- /tests/GitPhp/GitRepository.commit.phpt: -------------------------------------------------------------------------------- 1 | assert(['commit', '-m', 'Commit message']); 16 | 17 | $repo = $git->open(__DIR__); 18 | $repo->commit('Commit message'); 19 | -------------------------------------------------------------------------------- /tests/GitPhp/GitRepository.directory.phpt: -------------------------------------------------------------------------------- 1 | open(__DIR__); 16 | Assert::same(__DIR__, $repoA->getRepositoryPath()); 17 | 18 | $repoA = $git->open(__DIR__ . '/.git'); 19 | Assert::same(__DIR__, $repoA->getRepositoryPath()); 20 | 21 | $repoA = $git->open(__DIR__ . '/.git/'); 22 | Assert::same(__DIR__, $repoA->getRepositoryPath()); 23 | 24 | 25 | Assert::exception(function () use ($git) { 26 | $git->open(__DIR__ . '/unexists'); 27 | 28 | }, GitException::class, "Repository '" . __DIR__ . "/unexists' not found."); 29 | -------------------------------------------------------------------------------- /tests/GitPhp/GitRepository.execute.phpt: -------------------------------------------------------------------------------- 1 | open(__DIR__); 14 | 15 | $runner->setResult(['branch'], [], [ 16 | '* master', 17 | ]); 18 | Assert::same([ 19 | '* master', 20 | ], $repo->execute('branch')); 21 | 22 | 23 | $runner->setResult(['remote', '-v'], [], []); 24 | Assert::same([], $repo->execute(['remote', '-v'])); 25 | 26 | $runner->setResult(['remote', 'add', 'origin', 'https://github.com/czproject/git-php.git'], [], []); 27 | $repo->execute(['remote', 'add', 'origin', 'https://github.com/czproject/git-php.git']); 28 | 29 | $runner->setResult(['remote', '-v'], [], [ 30 | "origin\thttps://github.com/czproject/git-php.git (fetch)", 31 | "origin\thttps://github.com/czproject/git-php.git (push)", 32 | ]); 33 | Assert::same([ 34 | "origin\thttps://github.com/czproject/git-php.git (fetch)", 35 | "origin\thttps://github.com/czproject/git-php.git (push)", 36 | ], $repo->execute(['remote', '-v'])); 37 | 38 | 39 | $runner->setResult(['blabla'], [], [], [], 1); 40 | Assert::exception(function () use ($repo) { 41 | $repo->execute('blabla'); 42 | }, CzProject\GitPhp\GitException::class, "Command 'git blabla' failed (exit-code 1)."); 43 | -------------------------------------------------------------------------------- /tests/GitPhp/GitRepository.files.phpt: -------------------------------------------------------------------------------- 1 | open(__DIR__ . '/fixtures'); 15 | 16 | 17 | test(function () use ($repo, $runner) { 18 | $runner->resetAsserts(); 19 | $runner->assert(['add', '--end-of-options', 'file1.txt']); 20 | $runner->assert(['add', '--end-of-options', 'file2.txt']); 21 | $runner->assert(['add', '--end-of-options', 'file3.txt']); 22 | $runner->assert(['add', '--end-of-options', 'file4.txt']); 23 | $runner->assert(['add', '--end-of-options', 'file5.txt']); 24 | 25 | $repo->addFile('file1.txt'); 26 | $repo->addFile([ 27 | 'file2.txt', 28 | 'file3.txt', 29 | ]); 30 | $repo->addFile('file4.txt', 'file5.txt'); 31 | }); 32 | 33 | 34 | test(function () use ($repo) { 35 | Assert::exception(function () use ($repo) { 36 | $repo->addFile('not-found.txt'); 37 | }, GitException::class, "The path at 'not-found.txt' does not represent a valid file."); 38 | }); 39 | 40 | 41 | test(function () use ($repo, $runner) { 42 | $runner->resetAsserts(); 43 | $runner->assert(['rm', '-r', '--end-of-options', 'file1.txt']); 44 | $runner->assert(['rm', '-r', '--end-of-options', 'file2.txt']); 45 | $runner->assert(['rm', '-r', '--end-of-options', 'file3.txt']); 46 | $runner->assert(['rm', '-r', '--end-of-options', 'file4.txt']); 47 | $runner->assert(['rm', '-r', '--end-of-options', 'file5.txt']); 48 | 49 | $repo->removeFile('file1.txt'); 50 | $repo->removeFile([ 51 | 'file2.txt', 52 | 'file3.txt', 53 | ]); 54 | $repo->removeFile('file4.txt', 'file5.txt'); 55 | }); 56 | 57 | 58 | test(function () use ($repo, $runner) { 59 | $runner->resetAsserts(); 60 | $runner->assert(['mv', '--end-of-options', 'file1.txt', 'new1.txt']); 61 | $runner->assert(['mv', '--end-of-options', 'file2.txt', 'new2.txt']); 62 | $runner->assert(['mv', '--end-of-options', 'file3.txt', 'new3.txt']); 63 | 64 | $repo->renameFile('file1.txt', 'new1.txt'); 65 | $repo->renameFile([ 66 | 'file2.txt' => 'new2.txt', 67 | 'file3.txt' => 'new3.txt', 68 | ]); 69 | }); 70 | 71 | 72 | test(function () use ($repo, $runner) { 73 | $runner->resetAsserts(); 74 | $runner->assert(['add', '--all']); 75 | 76 | $repo->addAllChanges(); 77 | }); 78 | 79 | 80 | 81 | test(function () use ($repo, $runner) { 82 | $runner->resetAsserts(); 83 | $runner->assert(['update-index', '-q', '--refresh']); 84 | $runner->assert(['status', '--porcelain']); 85 | 86 | Assert::false($repo->hasChanges()); 87 | }); 88 | -------------------------------------------------------------------------------- /tests/GitPhp/GitRepository.getBranches.phpt: -------------------------------------------------------------------------------- 1 | open(__DIR__); 15 | 16 | $runner->setResult(['branch', '-a', '--no-color'], [], [ 17 | ' master', 18 | '* develop', 19 | ' remotes/origin/master', 20 | ]); 21 | Assert::same([ 22 | 'master', 23 | 'develop', 24 | 'remotes/origin/master', 25 | ], $repo->getBranches()); 26 | 27 | 28 | $runner->setResult(['branch', '-a', '--no-color'], [], []); 29 | Assert::null($repo->getBranches()); 30 | -------------------------------------------------------------------------------- /tests/GitPhp/GitRepository.getCommit.phpt: -------------------------------------------------------------------------------- 1 | assert( 17 | ['log', '--pretty=format:%H', '-n', '1'], 18 | [], 19 | ['734713bc047d87bf7eac9674765ae793478c50d3'] 20 | ); 21 | 22 | // commit subject 23 | $runner->assert( 24 | ['log', '-1', '734713bc047d87bf7eac9674765ae793478c50d3', '--format=%s'], 25 | [], 26 | ['init commit', '', ''] 27 | ); 28 | 29 | // commit body 30 | $runner->assert( 31 | ['log', '-1', '734713bc047d87bf7eac9674765ae793478c50d3', '--format=%b'], 32 | [], 33 | ['', '', ''] // 3 empty lines 34 | ); 35 | 36 | // author email 37 | $runner->assert( 38 | ['log', '-1', '734713bc047d87bf7eac9674765ae793478c50d3', '--format=%ae'], 39 | [], 40 | ['john@example.com'] 41 | ); 42 | 43 | // author name 44 | $runner->assert( 45 | ['log', '-1', '734713bc047d87bf7eac9674765ae793478c50d3', '--format=%an'], 46 | [], 47 | ['John Example'] 48 | ); 49 | 50 | // author date 51 | $runner->assert( 52 | ['log', '-1', '734713bc047d87bf7eac9674765ae793478c50d3', '--pretty=format:%ad', '--date=iso-strict'], 53 | [], 54 | ["2021-04-29T15:55:09+00:00"] 55 | ); 56 | 57 | // committer email 58 | $runner->assert( 59 | ['log', '-1', '734713bc047d87bf7eac9674765ae793478c50d3', '--format=%ce'], 60 | [], 61 | ["john@committer.com"] 62 | ); 63 | 64 | // committer name 65 | $runner->assert( 66 | ['log', '-1', '734713bc047d87bf7eac9674765ae793478c50d3', '--format=%cn'], 67 | [], 68 | ["John Committer"] 69 | ); 70 | 71 | // committter date 72 | $runner->assert( 73 | ['log', '-1', '734713bc047d87bf7eac9674765ae793478c50d3', '--pretty=format:%cd', '--date=iso-strict'], 74 | [], 75 | ['2021-04-29T17:55:09+02:00'] 76 | ); 77 | 78 | $timestamp = \DateTimeImmutable::createFromFormat(\DateTime::ATOM, '2021-04-29T16:55:09+01:00')->getTimestamp(); 79 | 80 | $repo = $git->open(__DIR__); 81 | $commit = $repo->getLastCommit(); 82 | Assert::same('734713bc047d87bf7eac9674765ae793478c50d3', $commit->getId()->toString()); 83 | Assert::same('init commit', $commit->getSubject()); 84 | Assert::null($commit->getBody()); 85 | Assert::same($timestamp, $commit->getDate()->getTimestamp()); 86 | 87 | Assert::same('john@example.com', $commit->getAuthorEmail()); 88 | Assert::same('John Example', $commit->getAuthorName()); 89 | Assert::same($timestamp, $commit->getAuthorDate()->getTimestamp()); 90 | 91 | Assert::same('john@committer.com', $commit->getCommitterEmail()); 92 | Assert::same('John Committer', $commit->getCommitterName()); 93 | Assert::same($timestamp, $commit->getCommitterDate()->getTimestamp()); 94 | }); 95 | 96 | 97 | test(function () { 98 | $runner = new AssertRunner(__DIR__); 99 | $git = new Git($runner); 100 | 101 | // commit subject 102 | $runner->assert( 103 | ['log', '-1', '734713bc047d87bf7eac9674765ae793478c50d3', '--format=%s'], 104 | [], 105 | ['init commit', '', ''] 106 | ); 107 | 108 | // commit body 109 | $runner->assert( 110 | ['log', '-1', '734713bc047d87bf7eac9674765ae793478c50d3', '--format=%b'], 111 | [], 112 | ['first line', 'second line', '', ''] // + 2 empty lines 113 | ); 114 | 115 | // author email 116 | $runner->assert( 117 | ['log', '-1', '734713bc047d87bf7eac9674765ae793478c50d3', '--format=%ae'], 118 | [], 119 | ['john@example.com'] 120 | ); 121 | 122 | // author name 123 | $runner->assert( 124 | ['log', '-1', '734713bc047d87bf7eac9674765ae793478c50d3', '--format=%an'], 125 | [], 126 | ['John Example'] 127 | ); 128 | 129 | // author date 130 | $runner->assert( 131 | ['log', '-1', '734713bc047d87bf7eac9674765ae793478c50d3', '--pretty=format:%ad', '--date=iso-strict'], 132 | [], 133 | ["2021-04-29T15:55:09+00:00"] 134 | ); 135 | 136 | // committer email 137 | $runner->assert( 138 | ['log', '-1', '734713bc047d87bf7eac9674765ae793478c50d3', '--format=%ce'], 139 | [], 140 | ["john@committer.com"] 141 | ); 142 | 143 | // committer name 144 | $runner->assert( 145 | ['log', '-1', '734713bc047d87bf7eac9674765ae793478c50d3', '--format=%cn'], 146 | [], 147 | ["John Committer"] 148 | ); 149 | 150 | // committter date 151 | $runner->assert( 152 | ['log', '-1', '734713bc047d87bf7eac9674765ae793478c50d3', '--pretty=format:%cd', '--date=iso-strict'], 153 | [], 154 | ['2021-04-29T17:55:09+02:00'] 155 | ); 156 | 157 | $timestamp = \DateTimeImmutable::createFromFormat(\DateTime::ATOM, '2021-04-29T16:55:09+01:00')->getTimestamp(); 158 | 159 | $repo = $git->open(__DIR__); 160 | $commit = $repo->getCommit('734713bc047d87bf7eac9674765ae793478c50d3'); 161 | Assert::same('734713bc047d87bf7eac9674765ae793478c50d3', $commit->getId()->toString()); 162 | Assert::same('init commit', $commit->getSubject()); 163 | Assert::same("first line\nsecond line", $commit->getBody()); 164 | Assert::same($timestamp, $commit->getDate()->getTimestamp()); 165 | 166 | Assert::same('john@example.com', $commit->getAuthorEmail()); 167 | Assert::same('John Example', $commit->getAuthorName()); 168 | Assert::same($timestamp, $commit->getAuthorDate()->getTimestamp()); 169 | 170 | Assert::same('john@committer.com', $commit->getCommitterEmail()); 171 | Assert::same('John Committer', $commit->getCommitterName()); 172 | Assert::same($timestamp, $commit->getCommitterDate()->getTimestamp()); 173 | }); 174 | -------------------------------------------------------------------------------- /tests/GitPhp/GitRepository.getCurrentBranchName.phpt: -------------------------------------------------------------------------------- 1 | open(__DIR__); 15 | 16 | $runner->setResult(['branch', '-a', '--no-color'], [], [ 17 | ' master', 18 | '* develop', 19 | ' remotes/origin/master', 20 | ]); 21 | Assert::same('develop', $repo->getCurrentBranchName()); 22 | 23 | 24 | $runner->setResult(['branch', '-a', '--no-color'], [], []); 25 | Assert::exception(function () use ($repo) { 26 | $repo->getCurrentBranchName(); 27 | }, GitException::class, 'Getting of current branch name failed.'); 28 | 29 | 30 | $runner->setResult(['branch', '-a', '--no-color'], [], [], [], 1); 31 | Assert::exception(function () use ($repo) { 32 | $repo->getCurrentBranchName(); 33 | }, GitException::class, 'Getting of current branch name failed.'); 34 | -------------------------------------------------------------------------------- /tests/GitPhp/GitRepository.getLastCommitId.phpt: -------------------------------------------------------------------------------- 1 | assert( 15 | ['log', '--pretty=format:%H', '-n', '1'], 16 | [], 17 | ['734713bc047d87bf7eac9674765ae793478c50d3'] 18 | ); 19 | 20 | $repo = $git->open(__DIR__); 21 | Assert::same('734713bc047d87bf7eac9674765ae793478c50d3', $repo->getLastCommitId()->toString()); 22 | -------------------------------------------------------------------------------- /tests/GitPhp/GitRepository.getLocalBranches.phpt: -------------------------------------------------------------------------------- 1 | open(__DIR__); 15 | 16 | $runner->setResult(['branch', '--no-color'], [], [ 17 | ' master', 18 | '* develop', 19 | ]); 20 | Assert::same([ 21 | 'master', 22 | 'develop', 23 | ], $repo->getLocalBranches()); 24 | 25 | 26 | $runner->setResult(['branch', '--no-color'], [], []); 27 | Assert::null($repo->getLocalBranches()); 28 | -------------------------------------------------------------------------------- /tests/GitPhp/GitRepository.getRemoteBranches.phpt: -------------------------------------------------------------------------------- 1 | open(__DIR__); 15 | 16 | $runner->setResult(['branch', '-r', '--no-color'], [], [ 17 | ' origin/master', 18 | '* origin/version-2' 19 | ]); 20 | Assert::same([ 21 | 'origin/master', 22 | 'origin/version-2', 23 | ], $repo->getRemoteBranches()); 24 | 25 | 26 | $runner->setResult(['branch', '-r', '--no-color'], [], []); 27 | Assert::null($repo->getRemoteBranches()); 28 | -------------------------------------------------------------------------------- /tests/GitPhp/GitRepository.getTags.phpt: -------------------------------------------------------------------------------- 1 | open(__DIR__); 14 | 15 | $runner->setResult(['tag'], [], [ 16 | ' v1.0.0 ', 17 | 'v1.0.1', 18 | 'v1.0.2', 19 | 'v2.0.0', 20 | 'v3.0.0', 21 | 'v3.1.0', 22 | ]); 23 | Assert::same([ 24 | 'v1.0.0', 25 | 'v1.0.1', 26 | 'v1.0.2', 27 | 'v2.0.0', 28 | 'v3.0.0', 29 | 'v3.1.0', 30 | ], $repo->getTags()); 31 | -------------------------------------------------------------------------------- /tests/GitPhp/GitRepository.remotes.phpt: -------------------------------------------------------------------------------- 1 | assert(['clone', '-q', '--end-of-options', 'git@github.com:czproject/git-php.git', __DIR__]); 16 | $runner->assert(['remote', 'add', '--end-of-options', 'origin2', 'git@github.com:czproject/git-php.git']); 17 | $runner->assert(['remote', 'add', '--end-of-options', 'remote', 'git@github.com:czproject/git-php.git']); 18 | $runner->assert(['remote', 'add', [ 19 | '--mirror=push', 20 | ], '--end-of-options', 'only-push', 'test-url']); 21 | $runner->assert(['remote', 'rename', '--end-of-options', 'remote', 'origin3']); 22 | $runner->assert(['remote', 'set-url', [ 23 | '--push', 24 | ], '--end-of-options', 'origin3', 'test-url']); 25 | $runner->assert(['remote', 'remove', '--end-of-options', 'origin2']); 26 | 27 | $repo = $git->cloneRepository('git@github.com:czproject/git-php.git', __DIR__); 28 | $repo->addRemote('origin2', 'git@github.com:czproject/git-php.git'); 29 | $repo->addRemote('remote', 'git@github.com:czproject/git-php.git'); 30 | $repo->addRemote('only-push', 'test-url', [ 31 | '--mirror=push', 32 | ]); 33 | $repo->renameRemote('remote', 'origin3'); 34 | $repo->setRemoteUrl('origin3', 'test-url', [ 35 | '--push', 36 | ]); 37 | $repo->removeRemote('origin2'); 38 | 39 | $runner->assert(['push', '--end-of-options', 'origin']); 40 | $runner->assert(['push', '--repo', 'https://user:password@example.com/MyUser/MyRepo.git', '--end-of-options']); 41 | $runner->assert(['push', '--end-of-options', 'origin', 'master']); 42 | $runner->assert(['fetch', '--end-of-options', 'origin']); 43 | $runner->assert(['fetch', '--end-of-options', 'origin', 'master']); 44 | $runner->assert(['pull', '--end-of-options', 'origin']); 45 | $runner->assert(['pull', '--end-of-options', 'origin', 'master']); 46 | $repo->push('origin'); 47 | $repo->push(NULL, ['--repo' => 'https://user:password@example.com/MyUser/MyRepo.git']); 48 | $repo->push(['origin', 'master']); 49 | $repo->fetch('origin'); 50 | $repo->fetch(['origin', 'master']); 51 | $repo->pull('origin'); 52 | $repo->pull(['origin', 'master']); 53 | -------------------------------------------------------------------------------- /tests/GitPhp/GitRepository.tags.phpt: -------------------------------------------------------------------------------- 1 | assert(['tag', '--end-of-options', 'v1.0.0']); 16 | $runner->assert(['tag', '--end-of-options', 'v2.0.0', 'v1.0.0']); 17 | $runner->assert(['tag', '-d', 'v1.0.0']); 18 | $runner->assert(['tag', '-d', 'v2.0.0']); 19 | 20 | $repo = $git->open(__DIR__); 21 | $repo->createTag('v1.0.0'); 22 | $repo->renameTag('v1.0.0', 'v2.0.0'); 23 | $repo->removeTag('v2.0.0'); 24 | -------------------------------------------------------------------------------- /tests/GitPhp/Helpers.extractRepositoryNameFromUrl.phpt: -------------------------------------------------------------------------------- 1 | assert(['branch', 'master']); 16 | $assertRunner->assert(['branch', 'develop']); 17 | $assertRunner->assert(['checkout', 'develop']); 18 | $assertRunner->assert(['merge', 'feature-1']); 19 | $assertRunner->assert(['branch', '-d', 'feature-1']); 20 | $assertRunner->assert(['checkout', 'master']); 21 | 22 | $repo = $git->open(__DIR__); 23 | $repo->createBranch('master'); 24 | $repo->createBranch('develop', TRUE); 25 | $repo->merge('feature-1'); 26 | $repo->removeBranch('feature-1'); 27 | $repo->checkout('master'); 28 | -------------------------------------------------------------------------------- /tests/GitPhp/RunnerResult.phpt: -------------------------------------------------------------------------------- 1 | hasOutput()); 14 | Assert::same(['foo', 'bar'], $result->getOutput()); 15 | Assert::same("foo\r\nbar\r\n", $result->getOutputAsString()); 16 | Assert::same('bar', $result->getOutputLastLine()); 17 | 18 | Assert::true($result->hasErrorOutput()); 19 | Assert::same(['error', 'error2'], $result->getErrorOutput()); 20 | Assert::same("error\r\nerror2\r\n", $result->getErrorOutputAsString()); 21 | }); 22 | 23 | 24 | test(function () { 25 | $result = new RunnerResult('cat-file', 0, "\r\n", "\r\n"); 26 | 27 | Assert::false($result->hasOutput()); 28 | Assert::same([], $result->getOutput()); 29 | Assert::same("\r\n", $result->getOutputAsString()); 30 | Assert::null($result->getOutputLastLine()); 31 | 32 | Assert::false($result->hasErrorOutput()); 33 | Assert::same([], $result->getErrorOutput()); 34 | Assert::same("\r\n", $result->getErrorOutputAsString()); 35 | }); 36 | 37 | 38 | test(function () { 39 | $result = new RunnerResult('cat-file', 0, '', ''); 40 | 41 | Assert::false($result->hasOutput()); 42 | Assert::same([], $result->getOutput()); 43 | Assert::same('', $result->getOutputAsString()); 44 | Assert::null($result->getOutputLastLine()); 45 | 46 | Assert::false($result->hasErrorOutput()); 47 | Assert::same([], $result->getErrorOutput()); 48 | Assert::same('', $result->getErrorOutputAsString()); 49 | }); 50 | 51 | 52 | test(function () { 53 | $result = new RunnerResult('cat-file', 0, ['foo', 'bar'], ['error', 'error2']); 54 | 55 | Assert::true($result->hasOutput()); 56 | Assert::same(['foo', 'bar'], $result->getOutput()); 57 | Assert::same("foo\nbar", $result->getOutputAsString()); 58 | Assert::same('bar', $result->getOutputLastLine()); 59 | 60 | Assert::true($result->hasErrorOutput()); 61 | Assert::same(['error', 'error2'], $result->getErrorOutput()); 62 | Assert::same("error\nerror2", $result->getErrorOutputAsString()); 63 | }); 64 | 65 | 66 | test(function () { 67 | $result = new RunnerResult('cat-file', 0, [], []); 68 | 69 | Assert::false($result->hasOutput()); 70 | Assert::same([], $result->getOutput()); 71 | Assert::same('', $result->getOutputAsString()); 72 | Assert::null($result->getOutputLastLine()); 73 | 74 | Assert::false($result->hasErrorOutput()); 75 | Assert::same([], $result->getErrorOutput()); 76 | Assert::same('', $result->getErrorOutputAsString()); 77 | }); 78 | -------------------------------------------------------------------------------- /tests/GitPhp/bootstrap.php: -------------------------------------------------------------------------------- 1 | getRunnerResult(); 20 | 21 | if ($result !== NULL) { 22 | echo $result->getCommand(), "\n"; 23 | echo 'EXIT CODE: ', $result->getExitCode(), "\n"; 24 | echo "--------------\n", 25 | $result->getOutputAsString(), "\n"; 26 | 27 | if ($result->hasErrorOutput()) { 28 | echo "--------------\n", 29 | implode("\n", $result->getErrorOutput()), "\n"; 30 | } 31 | } 32 | 33 | throw $e; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/GitPhp/fixtures/file1.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/czproject/git-php/adfba0751e6db4a1948580a2682a2890bf6d89d8/tests/GitPhp/fixtures/file1.txt -------------------------------------------------------------------------------- /tests/GitPhp/fixtures/file2.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/czproject/git-php/adfba0751e6db4a1948580a2682a2890bf6d89d8/tests/GitPhp/fixtures/file2.txt -------------------------------------------------------------------------------- /tests/GitPhp/fixtures/file3.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/czproject/git-php/adfba0751e6db4a1948580a2682a2890bf6d89d8/tests/GitPhp/fixtures/file3.txt -------------------------------------------------------------------------------- /tests/GitPhp/fixtures/file4.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/czproject/git-php/adfba0751e6db4a1948580a2682a2890bf6d89d8/tests/GitPhp/fixtures/file4.txt -------------------------------------------------------------------------------- /tests/GitPhp/fixtures/file5.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/czproject/git-php/adfba0751e6db4a1948580a2682a2890bf6d89d8/tests/GitPhp/fixtures/file5.txt -------------------------------------------------------------------------------- /tests/libs/AssertRunner.php: -------------------------------------------------------------------------------- 1 | cwd = $cwd; 31 | $this->commandProcessor = new CommandProcessor; 32 | } 33 | 34 | 35 | /** 36 | * @param mixed[] $expectedArgs 37 | * @param array $expectedEnv 38 | * @param string[] $resultOutput 39 | * @param string[] $resultErrorOutput 40 | * @param int $resultExitCode 41 | * @return self 42 | */ 43 | public function assert(array $expectedArgs, array $expectedEnv = [], array $resultOutput = [], array $resultErrorOutput = [], $resultExitCode = 0) 44 | { 45 | $cmd = $this->commandProcessor->process('git', $expectedArgs, $expectedEnv); 46 | $this->asserts[] = new RunnerResult($cmd, $resultExitCode, $resultOutput, $resultErrorOutput); 47 | return $this; 48 | } 49 | 50 | 51 | /** 52 | * @return self 53 | */ 54 | public function resetAsserts() 55 | { 56 | $this->asserts = []; 57 | return $this; 58 | } 59 | 60 | 61 | /** 62 | * @return RunnerResult 63 | */ 64 | public function run($cwd, array $args, ?array $env = NULL) 65 | { 66 | if (empty($this->asserts)) { 67 | throw new \CzProject\GitPhp\InvalidStateException('Missing asserts, use $runner->assert().'); 68 | } 69 | 70 | $cmd = $this->commandProcessor->process('git', $args, $env); 71 | $result = current($this->asserts); 72 | 73 | if (!($result instanceof RunnerResult)) { 74 | throw new \CzProject\GitPhp\InvalidStateException("Missing assert for command '$cmd'"); 75 | } 76 | 77 | \Tester\Assert::same($result->getCommand(), $cmd); 78 | next($this->asserts); 79 | return $result; 80 | } 81 | 82 | 83 | /** 84 | * @return string 85 | */ 86 | public function getCwd() 87 | { 88 | return $this->cwd; 89 | } 90 | } 91 | --------------------------------------------------------------------------------