├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── ROADMAP.md ├── SECURITY.md ├── composer.json ├── docker-compose.yml ├── ecs.php ├── examples └── show_file_contents.php ├── graphics ├── gitelephant_600.png └── gitelephant_high.png ├── phpstan.neon ├── phpunit.xml ├── rector.yaml └── src └── GitElephant ├── Command ├── BaseCommand.php ├── BranchCommand.php ├── Caller │ ├── AbstractCaller.php │ ├── Caller.php │ ├── CallerInterface.php │ └── CallerSSH2.php ├── CatFileCommand.php ├── CloneCommand.php ├── DiffCommand.php ├── DiffTreeCommand.php ├── FetchCommand.php ├── LogCommand.php ├── LogRangeCommand.php ├── LsTreeCommand.php ├── MainCommand.php ├── MergeCommand.php ├── MvCommand.php ├── PullCommand.php ├── PushCommand.php ├── Remote │ ├── AddSubCommand.php │ └── ShowSubCommand.php ├── RemoteCommand.php ├── ResetCommand.php ├── RevListCommand.php ├── RevParseCommand.php ├── ShowCommand.php ├── StashCommand.php ├── SubCommandCommand.php ├── SubmoduleCommand.php └── TagCommand.php ├── Exception ├── InvalidBranchNameException.php └── InvalidRepositoryPathException.php ├── Objects ├── Author.php ├── Branch.php ├── Commit.php ├── Commit │ └── Message.php ├── Diff │ ├── Diff.php │ ├── DiffChunk.php │ ├── DiffChunkLine.php │ ├── DiffChunkLineAdded.php │ ├── DiffChunkLineChanged.php │ ├── DiffChunkLineDeleted.php │ ├── DiffChunkLineUnchanged.php │ └── DiffObject.php ├── Log.php ├── LogRange.php ├── NodeObject.php ├── Remote.php ├── Tag.php ├── Tree.php ├── TreeObject.php └── TreeishInterface.php ├── Repository.php ├── Sequence ├── AbstractCollection.php ├── AbstractSequence.php ├── CollectionInterface.php ├── Sequence.php ├── SequenceInterface.php └── SortableInterface.php ├── Status ├── Status.php ├── StatusFile.php ├── StatusIndex.php └── StatusWorkingTree.php └── Utilities.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.9.19 2 | 3 | * Renamed some classes, GitAuthor becomes Author, TreeBranch becomes Branch, TreeTag becomes Tag and TreeObject becomes Object -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.2-alpine 2 | 3 | RUN apk update \ 4 | && apk add git zlib-dev \ 5 | && git config --global user.email "test@gitelephant.org" \ 6 | && git config --global user.name "GitElephant tests" 7 | 8 | RUN php -r "readfile('https://getcomposer.org/installer');" > composer-setup.php \ 9 | && php composer-setup.php \ 10 | && php -r "unlink('composer-setup.php');" \ 11 | && mv composer.phar /usr/local/bin/composer \ 12 | && docker-php-ext-install zip 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | todo 2 | ---- 3 | 4 | 0.8.* 5 | 6 | * add interface for caller DONE 7 | * commits count DONE 8 | 9 | 0.9.* 10 | * isolate objects like grit, clean constructor of Commit, Log, Tag, Tree, Diff by accepting the repository as mandatory argument DONE 11 | * find a way to populate object props from the sha inside the objects DONE 12 | * inject the caller and the command to the objects to populate props DONE 13 | * use sha (default to HEAD) whenever it's possible inside constructors DONE 14 | * remove the dependency-injection and config dependency DONE 15 | * rewrite the tree implementation to not use recursion on every request DONE 16 | * git pull 17 | 18 | 1.0.0 19 | * remotes DONE 20 | * better status handling with --porcelain DONE 21 | * named exceptions DONE 22 | * unstage DONE 23 | 24 | next 25 | * git blame 26 | * blobs management 27 | * submodules management 28 | * signed tags 29 | * SSH to execute command on remote server 30 | 31 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | To be up to date with security issues, use the latest available version. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | To report a vulnerability, please contact us at security@bernhard-webstudio.ch. 10 | 11 | We try to fix security errors as soon as possible and publish a new version with the appropriate corrections. 12 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypresslab/gitelephant", 3 | "description": "An abstraction layer for git written in PHP", 4 | "scripts": { 5 | "tests": "./vendor/bin/phpunit", 6 | "static-tests": "./vendor/bin/phpstan analyse -c phpstan.neon", 7 | "check-cs": "./vendor/bin/ecs check", 8 | "fix-cs": "./vendor/bin/ecs check --fix" 9 | }, 10 | "keywords": [ 11 | "git" 12 | ], 13 | "homepage": "http://gitelephant.cypresslab.net/", 14 | "license": "LGPL-3.0+", 15 | "authors": [ 16 | { 17 | "name": "Matteo Giachino", 18 | "email": "matteog@gmail.com" 19 | } 20 | ], 21 | "require": { 22 | "php": ">=7.2.0", 23 | "symfony/process": ">=3.4", 24 | "symfony/filesystem": ">=3.4", 25 | "symfony/finder": ">=3.4", 26 | "phpoption/phpoption": "1.*" 27 | }, 28 | "require-dev": { 29 | "php": ">=7.2.0", 30 | "phpunit/phpunit": "~8.0|~9.0", 31 | "mockery/mockery": "~1.1", 32 | "rector/rector": "*", 33 | "symplify/easy-coding-standard": "*", 34 | "phpstan/phpstan": "*", 35 | "phpstan/phpstan-phpunit": "*" 36 | }, 37 | "minimum-stability": "stable", 38 | "autoload": { 39 | "psr-0": { 40 | "GitElephant": "src/" 41 | } 42 | }, 43 | "autoload-dev": { 44 | "psr-0": { 45 | "GitElephant": "tests/" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | git: 2 | build: . 3 | volumes: 4 | - .:/code 5 | working_dir: /code 6 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | paths([__DIR__ . '/src', __DIR__ . '/tests']); 11 | 12 | // A. full sets 13 | $configurator->sets([ 14 | SetList::CLEAN_CODE, 15 | SetList::PSR_12 16 | ]); 17 | 18 | // B. standalone rule 19 | $configurator->ruleWithConfiguration(ArraySyntaxFixer::class, [ 20 | 'syntax' => 'short', 21 | ]); 22 | 23 | $configurator->skip(['Unused variable $deleted.' => ['src/GitElephant/Objects/Diff/DiffChunk.php'], 'Unused variable $new.' => ['src/GitElephant/Objects/Diff/DiffChunk.php']]); 24 | }; 25 | -------------------------------------------------------------------------------- /examples/show_file_contents.php: -------------------------------------------------------------------------------- 1 | getTree('HEAD', 'src/GitElephant/Repository.php'); 12 | 13 | $master = new Branch($repo, 'master'); // pick a branch 14 | $commit = Commit::pick($repo, '83e26d0f'); // pick a single commit 15 | $v1 = Tag::pick($repo, 'v0.1.0'); 16 | 17 | echo $repo->outputRawContent($binaryFile->getBlob(), $master); 18 | echo $repo->outputRawContent($binaryFile->getBlob(), $commit); 19 | echo $repo->outputRawContent($binaryFile->getBlob(), $v1); 20 | -------------------------------------------------------------------------------- /graphics/gitelephant_600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteosister/GitElephant/4e546eee4c9ad1e0226054f5756c4ef5217e2929/graphics/gitelephant_600.png -------------------------------------------------------------------------------- /graphics/gitelephant_high.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteosister/GitElephant/4e546eee4c9ad1e0226054f5756c4ef5217e2929/graphics/gitelephant_high.png -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 6 3 | paths: 4 | - src 5 | # - tests 6 | 7 | # things we disable for the moment, but one day... 8 | inferPrivatePropertyTypeFromConstructor: true 9 | ignoreErrors: 10 | - 11 | message: '#Unsafe usage of new static\(\).#' 12 | path: %currentWorkingDirectory% 13 | - 14 | message: '#Parameter \#1 \$command of class Symfony\\Component\\Process\\Process constructor expects array, string given.#' 15 | path: src/GitElephant/Command/Caller/Caller.php 16 | - 17 | identifier: missingType.iterableValue 18 | - 19 | identifier: missingType.generics 20 | 21 | includes: 22 | - vendor/phpstan/phpstan-phpunit/extension.neon 23 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | src/GitElephant/ 6 | 7 | 8 | 9 | 10 | tests/GitElephant/ 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /rector.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | # autoload_paths: 3 | # - 'vendor/autoload.php' 4 | paths: 5 | - 'src' 6 | - 'tests' 7 | php_version_features: '7.2' 8 | sets: 9 | - 'code-quality' 10 | - 'symfony-code-quality' 11 | - 'php71' 12 | - 'php72' 13 | - 'php73' 14 | - 'phpunit90' 15 | 16 | -------------------------------------------------------------------------------- /src/GitElephant/Command/BranchCommand.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | class BranchCommand extends BaseCommand 31 | { 32 | public const BRANCH_COMMAND = 'branch'; 33 | 34 | /** 35 | * constructor 36 | * 37 | * @param \GitElephant\Repository $repo The repository object this command 38 | * will interact with 39 | */ 40 | public function __construct(?Repository $repo = null) 41 | { 42 | parent::__construct($repo); 43 | } 44 | 45 | /** 46 | * Locate branches that contain a reference 47 | * 48 | * @param string $reference reference 49 | * 50 | * @throws \RuntimeException 51 | * @return string the command 52 | */ 53 | public function contains(string $reference): string 54 | { 55 | $this->clearAll(); 56 | $this->addCommandName(self::BRANCH_COMMAND); 57 | $this->addCommandArgument('--contains'); 58 | $this->addCommandSubject($reference); 59 | 60 | return $this->getCommand(); 61 | } 62 | 63 | /** 64 | * Create a new branch 65 | * 66 | * @param string $name The new branch name 67 | * @param string|null $startPoint the new branch start point. 68 | * 69 | * @throws \RuntimeException 70 | * @return string the command 71 | */ 72 | public function create(string $name, ?string $startPoint = null): string 73 | { 74 | $this->clearAll(); 75 | $this->addCommandName(self::BRANCH_COMMAND); 76 | $this->addCommandSubject($name); 77 | if (null !== $startPoint) { 78 | $this->addCommandSubject2($startPoint); 79 | } 80 | 81 | return $this->getCommand(); 82 | } 83 | 84 | /** 85 | * Lists branches 86 | * 87 | * @param bool $all lists all remotes 88 | * @param bool $simple list only branch names 89 | * 90 | * @throws \RuntimeException 91 | * @return string the command 92 | */ 93 | public function listBranches(bool $all = false, bool $simple = false): string 94 | { 95 | $this->clearAll(); 96 | $this->addCommandName(self::BRANCH_COMMAND); 97 | if (!$simple) { 98 | $this->addCommandArgument('-v'); 99 | } 100 | $this->addCommandArgument('--no-color'); 101 | $this->addCommandArgument('--no-abbrev'); 102 | if ($all) { 103 | $this->addCommandArgument('-a'); 104 | } 105 | 106 | return $this->getCommand(); 107 | } 108 | 109 | /** 110 | * Lists branches 111 | * 112 | * @deprecated This method uses an unconventional name but is being left in 113 | * place to remain compatible with existing code relying on it. 114 | * New code should be written to use listBranches(). 115 | * 116 | * @param bool $all lists all remotes 117 | * @param bool $simple list only branch names 118 | * 119 | * @throws \RuntimeException 120 | * @return string the command 121 | */ 122 | public function lists($all = false, bool $simple = false): string 123 | { 124 | return $this->listBranches($all, $simple); 125 | } 126 | 127 | /** 128 | * get info about a single branch 129 | * 130 | * @param string $name The branch name 131 | * @param bool $all lists all remotes 132 | * @param bool $simple list only branch names 133 | * @param bool $verbose verbose, show also the upstream branch 134 | * 135 | * @throws \RuntimeException 136 | * @return string 137 | */ 138 | public function singleInfo(string $name, bool $all = false, bool $simple = false, bool $verbose = false): string 139 | { 140 | $this->clearAll(); 141 | $this->addCommandName(self::BRANCH_COMMAND); 142 | if (!$simple) { 143 | $this->addCommandArgument('-v'); 144 | } 145 | $this->addCommandArgument('--list'); 146 | $this->addCommandArgument('--no-color'); 147 | $this->addCommandArgument('--no-abbrev'); 148 | if ($all) { 149 | $this->addCommandArgument('-a'); 150 | } 151 | if ($verbose) { 152 | $this->addCommandArgument('-vv'); 153 | } 154 | $this->addCommandSubject($name); 155 | 156 | return $this->getCommand(); 157 | } 158 | 159 | /** 160 | * Delete a branch by its name 161 | * 162 | * @param string $name The branch to delete 163 | * @param bool $force Force the delete 164 | * 165 | * @throws \RuntimeException 166 | * @return string the command 167 | */ 168 | public function delete(string $name, bool $force = false): string 169 | { 170 | $arg = $force ? '-D' : '-d'; 171 | $this->clearAll(); 172 | $this->addCommandName(self::BRANCH_COMMAND); 173 | $this->addCommandArgument($arg); 174 | $this->addCommandSubject($name); 175 | 176 | return $this->getCommand(); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/GitElephant/Command/Caller/AbstractCaller.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | abstract class AbstractCaller implements CallerInterface 29 | { 30 | /** 31 | * Git binary path 32 | * 33 | * @var string|null 34 | */ 35 | protected $binaryPath; 36 | 37 | /** 38 | * Git binary version 39 | * 40 | * @var string|null 41 | */ 42 | protected $binaryVersion; 43 | 44 | /** 45 | * the output lines of the command 46 | * 47 | * @var array 48 | */ 49 | protected $outputLines = []; 50 | 51 | /** 52 | * raw output of the command 53 | * 54 | * @var string 55 | */ 56 | protected $rawOutput; 57 | 58 | /** 59 | * @inheritdoc 60 | */ 61 | public function getBinaryPath(): string 62 | { 63 | return $this->binaryPath; 64 | } 65 | 66 | /** 67 | * path setter 68 | * 69 | * @param string $path the path to the system git binary 70 | */ 71 | public function setBinaryPath(string $path): self 72 | { 73 | $this->binaryPath = $path; 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * @inheritdoc 80 | */ 81 | public function getBinaryVersion(): string 82 | { 83 | if (is_null($this->binaryVersion)) { 84 | $this->execute('--version'); 85 | $version = $this->getOutput(); 86 | if (!preg_match('/^git version [0-9\.]+/', $version)) { 87 | throw new \RuntimeException('Could not parse git version. Unexpected format "' . $version . '".'); 88 | } 89 | $this->binaryVersion = preg_replace('/^git version ([0-9\.]+)/', '$1', $version); 90 | } 91 | 92 | return $this->binaryVersion; 93 | } 94 | 95 | /** 96 | * returns the output of the last executed command 97 | * 98 | * @return string 99 | */ 100 | public function getOutput(): string 101 | { 102 | return implode("\n", $this->outputLines); 103 | } 104 | 105 | /** 106 | * returns the output of the last executed command as an array of lines 107 | * 108 | * @param bool $stripBlankLines remove the blank lines 109 | * 110 | * @return array 111 | */ 112 | public function getOutputLines(bool $stripBlankLines = false): array 113 | { 114 | if ($stripBlankLines) { 115 | $output = []; 116 | foreach ($this->outputLines as $line) { 117 | if ('' !== $line) { 118 | $output[] = $line; 119 | } 120 | } 121 | 122 | return $output; 123 | } 124 | 125 | return $this->outputLines; 126 | } 127 | 128 | /** 129 | * Get RawOutput 130 | * 131 | * @return string 132 | */ 133 | public function getRawOutput(): string 134 | { 135 | return $this->rawOutput; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/GitElephant/Command/Caller/Caller.php: -------------------------------------------------------------------------------- 1 | 30 | * @author Tim Bernhard 31 | */ 32 | class Caller extends AbstractCaller 33 | { 34 | /** 35 | * the repository path 36 | * 37 | * @var string 38 | */ 39 | private $repositoryPath; 40 | 41 | /** 42 | * Class constructor 43 | * 44 | * @param string|null $gitPath the physical path to the git binary 45 | * @param string $repositoryPath the physical base path for the repository 46 | */ 47 | public function __construct($gitPath, $repositoryPath) 48 | { 49 | if (is_null($gitPath)) { 50 | // unix only! 51 | $gitPath = exec('which git'); 52 | } 53 | $this->setBinaryPath($gitPath); 54 | if (!is_dir($repositoryPath)) { 55 | throw new InvalidRepositoryPathException($repositoryPath); 56 | } 57 | $this->repositoryPath = $repositoryPath; 58 | } 59 | 60 | /** 61 | * Executes a command 62 | * 63 | * @param string $cmd the command to execute 64 | * @param bool $git if the command is git or a generic command 65 | * @param string $cwd the directory where the command must be executed 66 | * @param array $acceptedExitCodes exit codes accepted to consider the command execution successful 67 | * 68 | * @throws \RuntimeException 69 | * @throws \Symfony\Component\Process\Exception\InvalidArgumentException 70 | * @throws \Symfony\Component\Process\Exception\ProcessTimedOutException 71 | * @throws \Symfony\Component\Process\Exception\RuntimeException 72 | * @throws \Symfony\Component\Process\Exception\LogicException 73 | * @return Caller 74 | */ 75 | public function execute( 76 | string $cmd, 77 | bool $git = true, 78 | ?string $cwd = null, 79 | array $acceptedExitCodes = [0] 80 | ): CallerInterface { 81 | if ($git) { 82 | $cmd = $this->getBinaryPath() . ' ' . $cmd; 83 | } 84 | 85 | if (stripos(PHP_OS, 'WIN') !== 0) { 86 | // We rely on the C locale in all output we parse. 87 | $cmd = 'LC_ALL=C ' . $cmd; 88 | } 89 | 90 | if (is_null($cwd) || !is_dir($cwd)) { 91 | $cwd = $this->repositoryPath; 92 | } 93 | 94 | if (method_exists(Process::class, 'fromShellCommandline')) { 95 | $process = Process::fromShellCommandline($cmd, $cwd); 96 | } else { 97 | // compatibility fix required for symfony/process versions prior to v4.2. 98 | $process = new Process($cmd, $cwd); 99 | } 100 | 101 | $process->setTimeout(15000); 102 | $process->run(); 103 | if (!in_array($process->getExitCode(), $acceptedExitCodes)) { 104 | $text = 'Exit code: ' . $process->getExitCode(); 105 | $text .= ' while executing: "' . $cmd; 106 | $text .= '" with reason: ' . $process->getErrorOutput(); 107 | $text .= "\n" . $process->getOutput(); 108 | throw new \RuntimeException($text); 109 | } 110 | 111 | $this->rawOutput = $process->getOutput(); 112 | // rtrim values 113 | $values = array_map('rtrim', explode(PHP_EOL, $process->getOutput())); 114 | $this->outputLines = $values; 115 | 116 | return $this; 117 | } 118 | 119 | /** 120 | * returns the output of the last executed command 121 | * 122 | * @return string 123 | */ 124 | public function getOutput(): string 125 | { 126 | return implode("\n", $this->outputLines); 127 | } 128 | 129 | /** 130 | * returns the output of the last executed command as an array of lines 131 | * 132 | * @param bool $stripBlankLines remove the blank lines 133 | * 134 | * @return array 135 | */ 136 | public function getOutputLines(bool $stripBlankLines = false): array 137 | { 138 | if ($stripBlankLines) { 139 | $output = []; 140 | foreach ($this->outputLines as $line) { 141 | if ('' !== $line) { 142 | $output[] = $line; 143 | } 144 | } 145 | 146 | return $output; 147 | } 148 | 149 | return $this->outputLines; 150 | } 151 | 152 | /** 153 | * Get RawOutput 154 | * 155 | * @return string 156 | */ 157 | public function getRawOutput(): string 158 | { 159 | return $this->rawOutput; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/GitElephant/Command/Caller/CallerInterface.php: -------------------------------------------------------------------------------- 1 | 49 | */ 50 | public function getOutputLines(bool $stripBlankLines = false): array; 51 | 52 | /** 53 | * Returns the output of the last executed command. 54 | * May be adjusted, such as trimmed. 55 | * 56 | * @return string 57 | */ 58 | public function getOutput(): string; 59 | 60 | /** 61 | * Returns the output of the last executed command. 62 | * May not be adjusted, not trimmed or anything, really raw. 63 | * 64 | * @return string 65 | */ 66 | public function getRawOutput(): string; 67 | 68 | /** 69 | * Get the binary path 70 | * 71 | * @return string 72 | */ 73 | public function getBinaryPath(): string; 74 | 75 | /** 76 | * Get the binary version 77 | * 78 | * @return string 79 | */ 80 | public function getBinaryVersion(): string; 81 | } 82 | -------------------------------------------------------------------------------- /src/GitElephant/Command/Caller/CallerSSH2.php: -------------------------------------------------------------------------------- 1 | 27 | * @author Tim Bernhard 28 | */ 29 | class CallerSSH2 extends AbstractCaller 30 | { 31 | /** 32 | * @var resource 33 | */ 34 | private $resource; 35 | 36 | /** 37 | * @param resource $resource 38 | * @param string $gitPath path of the git executable on the remote host 39 | * 40 | * @internal param string $host remote host 41 | * @internal param int $port remote port 42 | */ 43 | public function __construct($resource, $gitPath = '/usr/bin/git') 44 | { 45 | $this->resource = $resource; 46 | $this->binaryPath = $gitPath; 47 | } 48 | 49 | /** 50 | * execute a command 51 | * 52 | * @param string $cmd the command 53 | * @param bool $git prepend git to the command 54 | * @param null|string $cwd directory where the command should be executed 55 | * 56 | * @return CallerInterface 57 | */ 58 | public function execute( 59 | $cmd, 60 | $git = true, 61 | $cwd = null 62 | ): \GitElephant\Command\Caller\CallerInterface { 63 | if ($git) { 64 | $cmd = $this->getBinaryPath() . ' ' . $cmd; 65 | } 66 | $stream = ssh2_exec($this->resource, $cmd); 67 | stream_set_blocking($stream, true); 68 | $data = stream_get_contents($stream); 69 | fclose($stream); 70 | 71 | $this->rawOutput = $data === false ? '' : $data; 72 | // rtrim values 73 | $values = array_map('rtrim', explode(PHP_EOL, $this->rawOutput)); 74 | $this->outputLines = $values; 75 | 76 | return $this; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/GitElephant/Command/CatFileCommand.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | class CatFileCommand extends BaseCommand 33 | { 34 | public const GIT_CAT_FILE = 'cat-file'; 35 | 36 | /** 37 | * constructor 38 | * 39 | * @param \GitElephant\Repository $repo The repository object this command 40 | * will interact with 41 | */ 42 | public function __construct(?Repository $repo = null) 43 | { 44 | parent::__construct($repo); 45 | } 46 | 47 | /** 48 | * command to show content of a Object at a given Treeish point 49 | * 50 | * @param \GitElephant\Objects\NodeObject $object a Object instance 51 | * @param \GitElephant\Objects\TreeishInterface|string $treeish an object with TreeishInterface interface 52 | * 53 | * @throws \RuntimeException 54 | * @return string 55 | */ 56 | public function content(NodeObject $object, $treeish): string 57 | { 58 | $this->clearAll(); 59 | $sha = $treeish instanceof TreeishInterface ? $treeish->getSha() : $treeish; 60 | $this->addCommandName(static::GIT_CAT_FILE); 61 | // pretty format 62 | $this->addCommandArgument('-p'); 63 | $this->addCommandSubject($sha . ':' . $object->getFullPath()); 64 | 65 | return $this->getCommand(); 66 | } 67 | 68 | /** 69 | * output an object content given it's sha 70 | * 71 | * @param string $sha 72 | * 73 | * @throws \RuntimeException 74 | * @return string 75 | */ 76 | public function contentBySha($sha): string 77 | { 78 | $this->clearAll(); 79 | $this->addCommandName(static::GIT_CAT_FILE); 80 | $this->addCommandArgument('-p'); 81 | $this->addCommandSubject($sha); 82 | 83 | return $this->getCommand(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/GitElephant/Command/CloneCommand.php: -------------------------------------------------------------------------------- 1 | 29 | * @author Kirk Madera 30 | */ 31 | class CloneCommand extends BaseCommand 32 | { 33 | public const GIT_CLONE_COMMAND = 'clone'; 34 | 35 | /** 36 | * constructor 37 | * 38 | * @param \GitElephant\Repository $repo The repository object this command 39 | * will interact with 40 | */ 41 | public function __construct(?Repository $repo = null) 42 | { 43 | parent::__construct($repo); 44 | } 45 | 46 | /** 47 | * Command to clone a repository 48 | * 49 | * @param string $url repository url 50 | * @param string $to where to clone the repo 51 | * @param string|null $repoReference Repo reference to clone. Required if performing a shallow clone. 52 | * @param int|null $depth Depth of commits to clone 53 | * @param bool $recursive Whether to recursively clone submodules. 54 | * 55 | * @throws \RuntimeException 56 | * @return string command 57 | */ 58 | public function cloneUrl( 59 | string $url, 60 | ?string $to = null, 61 | ?string $repoReference = null, 62 | ?int $depth = null, 63 | bool $recursive = false 64 | ): string { 65 | // get binary version before reset 66 | $version = $this->getBinaryVersion(); 67 | 68 | $this->clearAll(); 69 | $this->addCommandName(static::GIT_CLONE_COMMAND); 70 | $this->addCommandSubject($url); 71 | if (null !== $to) { 72 | $this->addCommandSubject2($to); 73 | } 74 | 75 | if (null !== $repoReference) { 76 | // git documentation says the --branch was added in 2.0.0, but it exists undocumented at least back to 1.8.3.1 77 | if (version_compare($version, '1.8.3.1', '<')) { 78 | throw new \RuntimeException( 79 | 'Please upgrade to git v1.8.3.1 or newer to support cloning a specific branch. You have ' . $version . '.' 80 | ); 81 | } 82 | $this->addCommandArgument('--branch=' . $repoReference); 83 | } 84 | 85 | if (null !== $depth) { 86 | $this->addCommandArgument('--depth=' . $depth); 87 | // shallow-submodules is a nice to have feature. Just ignoring if git version not high enough 88 | // It would be nice if this had a logger injected for us to log notices 89 | if (version_compare($version, '2.9.0', '>=') && $recursive && 1 == $depth) { 90 | $this->addCommandArgument('--shallow-submodules'); 91 | } 92 | } 93 | 94 | if ($recursive) { 95 | $this->addCommandArgument('--recursive'); 96 | } 97 | 98 | return $this->getCommand(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/GitElephant/Command/DiffCommand.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | class DiffCommand extends BaseCommand 32 | { 33 | public const DIFF_COMMAND = 'diff'; 34 | 35 | /** 36 | * constructor 37 | * 38 | * @param \GitElephant\Repository $repo The repository object this command 39 | * will interact with 40 | */ 41 | public function __construct(?Repository $repo = null) 42 | { 43 | parent::__construct($repo); 44 | } 45 | 46 | /** 47 | * build a diff command 48 | * 49 | * @param TreeishInterface $of the reference to diff 50 | * @param TreeishInterface|null $with the source reference to diff with $of, if not specified is the current HEAD 51 | * @param string|null $path the path to diff, if not specified the full repository 52 | * 53 | * @throws \RuntimeException 54 | * @return string 55 | */ 56 | public function diff($of, $with = null, $path = null): string 57 | { 58 | $this->clearAll(); 59 | $this->addCommandName(self::DIFF_COMMAND); 60 | // Instead of the first handful of characters, show the full pre- and post-image blob object names on the 61 | // "index" line when generating patch format output 62 | $this->addCommandArgument('--full-index'); 63 | $this->addCommandArgument('--no-color'); 64 | // Disallow external diff drivers 65 | $this->addCommandArgument('--no-ext-diff'); 66 | // Detect renames 67 | $this->addCommandArgument('-M'); 68 | $this->addCommandArgument('--dst-prefix=DST/'); 69 | $this->addCommandArgument('--src-prefix=SRC/'); 70 | 71 | $subject = ''; 72 | 73 | if (is_null($with)) { 74 | $subject .= $of . '^..' . $of; 75 | } else { 76 | $subject .= $with . '..' . $of; 77 | } 78 | 79 | if (!is_null($path)) { 80 | if (!is_string($path)) { 81 | /** @var Object $path */ 82 | $path = $path->getPath(); 83 | } 84 | $this->addPath($path); 85 | } 86 | 87 | $this->addCommandSubject($subject); 88 | 89 | return $this->getCommand(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/GitElephant/Command/DiffTreeCommand.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | class DiffTreeCommand extends BaseCommand 34 | { 35 | public const DIFF_TREE_COMMAND = 'diff-tree'; 36 | 37 | /** 38 | * constructor 39 | * 40 | * @param \GitElephant\Repository $repo The repository object this command 41 | * will interact with 42 | */ 43 | public function __construct(?Repository $repo = null) 44 | { 45 | parent::__construct($repo); 46 | } 47 | 48 | /** 49 | * get a diff of a root commit with the empty repository 50 | * 51 | * @param \GitElephant\Objects\Commit $commit the root commit object 52 | * 53 | * @throws \RuntimeException 54 | * @throws \InvalidArgumentException 55 | * @return string 56 | */ 57 | public function rootDiff(Commit $commit): string 58 | { 59 | if (!$commit->isRoot()) { 60 | throw new \InvalidArgumentException('rootDiff method accepts only root commits'); 61 | } 62 | $this->clearAll(); 63 | $this->addCommandName(static::DIFF_TREE_COMMAND); 64 | $this->addCommandArgument('--cc'); 65 | $this->addCommandArgument('--root'); 66 | $this->addCommandArgument('--dst-prefix=DST/'); 67 | $this->addCommandArgument('--src-prefix=SRC/'); 68 | $this->addCommandSubject($commit); 69 | 70 | return $this->getCommand(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/GitElephant/Command/FetchCommand.php: -------------------------------------------------------------------------------- 1 | getName(); 58 | } 59 | if ($branch instanceof Branch) { 60 | $branch = $branch->getName(); 61 | } 62 | 63 | $normalizedOptions = $this->normalizeOptions($options, $this->fetchCmdSwitchOptions()); 64 | 65 | $this->clearAll(); 66 | $this->addCommandName(self::GIT_FETCH_COMMAND); 67 | 68 | foreach ($normalizedOptions as $value) { 69 | $this->addCommandArgument($value); 70 | } 71 | 72 | if (!is_null($remote)) { 73 | $this->addCommandSubject($remote); 74 | } 75 | if (!is_null($branch)) { 76 | $this->addCommandSubject2($branch); 77 | } 78 | 79 | return $this->getCommand(); 80 | } 81 | 82 | /** 83 | * Valid options for remote command that do not require an associated value 84 | * 85 | * @return array Associative array mapping all non-value options and their respective normalized option 86 | */ 87 | public function fetchCmdSwitchOptions(): array 88 | { 89 | return [ 90 | self::GIT_FETCH_OPTION_TAGS => self::GIT_FETCH_OPTION_TAGS, 91 | ]; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/GitElephant/Command/LogCommand.php: -------------------------------------------------------------------------------- 1 | 32 | * @author Dhaval Patel 33 | */ 34 | class LogCommand extends BaseCommand 35 | { 36 | public const GIT_LOG = 'log'; 37 | 38 | /** 39 | * constructor 40 | * 41 | * @param \GitElephant\Repository $repo The repository object this command 42 | * will interact with 43 | */ 44 | public function __construct(?Repository $repo = null) 45 | { 46 | parent::__construct($repo); 47 | } 48 | 49 | /** 50 | * Build an object log command 51 | * 52 | * @param \GitElephant\Objects\NodeObject $obj the Object to get the log for 53 | * @param \GitElephant\Objects\Branch|string|null $branch the branch to consider 54 | * @param int|null $limit limit to n entries 55 | * @param int|null $offset skip n entries 56 | * 57 | * @throws \RuntimeException 58 | * @return string 59 | */ 60 | public function showObjectLog(NodeObject $obj, $branch = null, ?int $limit = null, ?int $offset = null): string 61 | { 62 | $subject = null; 63 | if (null !== $branch) { 64 | if ($branch instanceof Branch) { 65 | $subject .= $branch->getName(); 66 | } else { 67 | $subject .= (string) $branch; 68 | } 69 | } 70 | 71 | return $this->showLog($subject, $obj->getFullPath(), $limit, $offset); 72 | } 73 | 74 | /** 75 | * Build a generic log command 76 | * 77 | * @param \GitElephant\Objects\TreeishInterface|string $ref the reference to build the log for 78 | * @param string|null $path the physical path to the tree relative to the 79 | * repository root 80 | * @param int|null $limit limit to n entries 81 | * @param int|null $offset skip n entries 82 | * @param bool $firstParent skip commits brought in to branch by a merge 83 | * 84 | * @throws \RuntimeException 85 | * @return string 86 | */ 87 | public function showLog($ref, $path = null, $limit = null, ?int $offset = null, bool $firstParent = false): string 88 | { 89 | $this->clearAll(); 90 | 91 | $this->addCommandName(self::GIT_LOG); 92 | $this->addCommandArgument('-s'); 93 | $this->addCommandArgument('--pretty=raw'); 94 | $this->addCommandArgument('--no-color'); 95 | 96 | if (null !== $limit) { 97 | $limit = (int) $limit; 98 | $this->addCommandArgument('--max-count=' . $limit); 99 | } 100 | 101 | if (null !== $offset) { 102 | $offset = (int) $offset; 103 | $this->addCommandArgument('--skip=' . $offset); 104 | } 105 | 106 | if ($firstParent) { 107 | $this->addCommandArgument('--first-parent'); 108 | } 109 | 110 | if ($ref instanceof TreeishInterface) { 111 | $ref = $ref->getSha(); 112 | } 113 | 114 | if (null !== $path && !empty($path)) { 115 | $this->addPath($path); 116 | } 117 | 118 | $this->addCommandSubject($ref); 119 | 120 | return $this->getCommand(); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/GitElephant/Command/LogRangeCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @package GitElephant\Command 12 | * 13 | * Just for fun... 14 | */ 15 | 16 | namespace GitElephant\Command; 17 | 18 | use GitElephant\Objects\TreeishInterface; 19 | use GitElephant\Repository; 20 | 21 | /** 22 | * Log Range command generator 23 | * 24 | * @author Matteo Giachino 25 | * @author John Cartwright 26 | * @author Dhaval Patel 27 | */ 28 | class LogRangeCommand extends BaseCommand 29 | { 30 | public const GIT_LOG = 'log'; 31 | 32 | /** 33 | * constructor 34 | * 35 | * @param \GitElephant\Repository $repo The repository object this command 36 | * will interact with 37 | */ 38 | public function __construct(?Repository $repo = null) 39 | { 40 | parent::__construct($repo); 41 | } 42 | 43 | /** 44 | * Build a generic log command 45 | * 46 | * @param \GitElephant\Objects\TreeishInterface|string $refStart the reference range start to build the log for 47 | * @param \GitElephant\Objects\TreeishInterface|string $refEnd the reference range end to build the log for 48 | * @param string|null $path the physical path to the tree relative 49 | * to the repository root 50 | * @param int|null $limit limit to n entries 51 | * @param int|null $offset skip n entries 52 | * @param boolean|false $firstParent skip commits brought in to branch by a merge 53 | * 54 | * @throws \RuntimeException 55 | * @return string 56 | */ 57 | public function showLog( 58 | $refStart, 59 | $refEnd, 60 | $path = null, 61 | $limit = null, 62 | $offset = null, 63 | bool $firstParent = false 64 | ): string { 65 | $this->clearAll(); 66 | 67 | $this->addCommandName(self::GIT_LOG); 68 | $this->addCommandArgument('-s'); 69 | $this->addCommandArgument('--pretty=raw'); 70 | $this->addCommandArgument('--no-color'); 71 | 72 | if (null !== $limit) { 73 | $limit = (int) $limit; 74 | $this->addCommandArgument('--max-count=' . $limit); 75 | } 76 | 77 | if (null !== $offset) { 78 | $offset = (int) $offset; 79 | $this->addCommandArgument('--skip=' . $offset); 80 | } 81 | 82 | if ($firstParent) { 83 | $this->addCommandArgument('--first-parent'); 84 | } 85 | 86 | if ($refStart instanceof TreeishInterface) { 87 | $refStart = $refStart->getSha(); 88 | } 89 | 90 | if ($refEnd instanceof TreeishInterface) { 91 | $refEnd = $refEnd->getSha(); 92 | } 93 | 94 | if (null !== $path && !empty($path)) { 95 | $this->addPath($path); 96 | } 97 | 98 | $this->addCommandSubject($refStart . '..' . $refEnd); 99 | 100 | return $this->getCommand(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/GitElephant/Command/LsTreeCommand.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | class LsTreeCommand extends BaseCommand 34 | { 35 | public const LS_TREE_COMMAND = 'ls-tree'; 36 | 37 | /** 38 | * constructor 39 | * 40 | * @param \GitElephant\Repository $repo The repository object this command 41 | * will interact with 42 | */ 43 | public function __construct(?Repository $repo = null) 44 | { 45 | parent::__construct($repo); 46 | } 47 | 48 | /** 49 | * build a ls-tree command 50 | * 51 | * @param string|Branch $ref The reference to build the tree from 52 | * 53 | * @throws \RuntimeException 54 | * @return string 55 | */ 56 | public function fullTree($ref = 'HEAD'): string 57 | { 58 | $what = $ref; 59 | if ($ref instanceof TreeishInterface) { 60 | $what = $ref->getSha(); 61 | } 62 | $this->clearAll(); 63 | $this->addCommandName(self::LS_TREE_COMMAND); 64 | // recurse 65 | $this->addCommandArgument('-r'); 66 | // show trees 67 | $this->addCommandArgument('-t'); 68 | $this->addCommandArgument('-l'); 69 | $this->addCommandSubject($what); 70 | 71 | return $this->getCommand(); 72 | } 73 | 74 | /** 75 | * tree of a given path 76 | * 77 | * @param string|TreeishInterface $ref reference 78 | * @param string|NodeObject $path path 79 | * 80 | * @throws \RuntimeException 81 | * @return string 82 | */ 83 | public function tree($ref = 'HEAD', $path = null): string 84 | { 85 | if ($path instanceof NodeObject) { 86 | $subjectPath = $path->getFullPath() . ($path->isTree() ? '/' : ''); 87 | } else { 88 | $subjectPath = $path; 89 | } 90 | 91 | $what = $ref; 92 | if ($ref instanceof TreeishInterface) { 93 | $what = $ref->getSha(); 94 | } 95 | $subject = $what; 96 | 97 | $this->clearAll(); 98 | 99 | $this->addCommandName(self::LS_TREE_COMMAND); 100 | $this->addCommandArgument('-l'); 101 | $this->addCommandSubject($subject); 102 | $this->addPath($subjectPath); 103 | 104 | return $this->getCommand(); 105 | } 106 | 107 | /** 108 | * build ls-tree command that list all 109 | * 110 | * @param null|string $ref the reference to build the tree from 111 | * 112 | * @throws \RuntimeException 113 | * @return string 114 | */ 115 | public function listAll($ref = null): string 116 | { 117 | if (is_null($ref)) { 118 | $ref = 'HEAD'; 119 | } 120 | $this->clearAll(); 121 | $this->addCommandName(self::LS_TREE_COMMAND); 122 | $this->addCommandSubject($ref); 123 | 124 | return $this->getCommand(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/GitElephant/Command/MainCommand.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | class MainCommand extends BaseCommand 34 | { 35 | public const GIT_INIT = 'init'; 36 | public const GIT_STATUS = 'status'; 37 | public const GIT_ADD = 'add'; 38 | public const GIT_COMMIT = 'commit'; 39 | public const GIT_CHECKOUT = 'checkout'; 40 | public const GIT_MOVE = 'mv'; 41 | public const GIT_REMOVE = 'rm'; 42 | public const GIT_RESET = 'reset'; 43 | 44 | /** 45 | * constructor 46 | * 47 | * @param \GitElephant\Repository $repo The repository object this command 48 | * will interact with 49 | */ 50 | public function __construct(?Repository $repo = null) 51 | { 52 | parent::__construct($repo); 53 | } 54 | 55 | /** 56 | * Init the repository 57 | * 58 | * @param bool $bare 59 | * 60 | * @throws \RuntimeException 61 | * @return string 62 | */ 63 | public function init($bare = false, ?string $initialBranchName = null): string 64 | { 65 | $this->clearAll(); 66 | if ($bare) { 67 | $this->addCommandArgument('--bare'); 68 | } 69 | if ($initialBranchName) { 70 | $this->addCommandArgument('--initial-branch=' . $initialBranchName); 71 | } 72 | $this->addCommandName(self::GIT_INIT); 73 | 74 | return $this->getCommand(); 75 | } 76 | 77 | /** 78 | * Get the repository status 79 | * 80 | * @param bool $porcelain 81 | * 82 | * @throws \RuntimeException 83 | * @return string 84 | */ 85 | public function status($porcelain = false): string 86 | { 87 | $this->clearAll(); 88 | $this->addCommandName(self::GIT_STATUS); 89 | if ($porcelain) { 90 | $this->addCommandArgument('--porcelain'); 91 | } else { 92 | $this->addConfigs(['color.status' => 'false']); 93 | } 94 | 95 | return $this->getCommand(); 96 | } 97 | 98 | /** 99 | * Add a node to the stage 100 | * 101 | * @param string $what what should be added to the repository 102 | * 103 | * @throws \RuntimeException 104 | * @return string 105 | */ 106 | public function add($what = '.'): string 107 | { 108 | $this->clearAll(); 109 | $this->addCommandName(self::GIT_ADD); 110 | $this->addCommandArgument('--all'); 111 | $this->addCommandSubject($what); 112 | 113 | return $this->getCommand(); 114 | } 115 | 116 | /** 117 | * Remove a node from the stage and put in the working tree 118 | * 119 | * @param string $what what should be removed from the stage 120 | * 121 | * @throws \RuntimeException 122 | * @return string 123 | */ 124 | public function unstage($what): string 125 | { 126 | $this->clearAll(); 127 | $this->addCommandName(self::GIT_RESET); 128 | $this->addCommandArgument('HEAD'); 129 | $this->addPath($what); 130 | 131 | return $this->getCommand(); 132 | } 133 | 134 | /** 135 | * Commit 136 | * 137 | * @param string|null $message the commit message 138 | * @param bool $stageAll commit all changes 139 | * @param string|Author $author override the author for this commit 140 | * @param bool $allowEmpty whether to add param `--allow-empty` to commit command 141 | * @param \DateTimeInterface|null $date 142 | * 143 | * @throws \RuntimeException 144 | * @throws \InvalidArgumentException 145 | * @return string 146 | */ 147 | public function commit( 148 | ?string $message, 149 | bool $stageAll = false, 150 | $author = null, 151 | bool $allowEmpty = false, 152 | ?\DateTimeInterface $date = null 153 | ): string { 154 | $this->clearAll(); 155 | 156 | if (trim($message) === '' || is_null($message)) { 157 | throw new \InvalidArgumentException(sprintf('You can\'t commit without message')); 158 | } 159 | $this->addCommandName(self::GIT_COMMIT); 160 | 161 | if ($stageAll) { 162 | $this->addCommandArgument('-a'); 163 | } 164 | 165 | if ($author !== null) { 166 | $this->addCommandArgument('--author'); 167 | $this->addCommandArgument($author); 168 | } 169 | 170 | if ($allowEmpty) { 171 | $this->addCommandArgument('--allow-empty'); 172 | } 173 | 174 | if (null !== $date) { 175 | $this->addCommandArgument('--date'); 176 | $this->addCommandArgument($date->format(\DateTimeInterface::RFC822)); 177 | } 178 | 179 | $this->addCommandArgument('-m'); 180 | $this->addCommandSubject($message); 181 | 182 | return $this->getCommand(); 183 | } 184 | 185 | /** 186 | * Checkout a treeish reference 187 | * 188 | * @param string|Branch|TreeishInterface $ref the reference to checkout 189 | * 190 | * @throws \RuntimeException 191 | * @return string 192 | */ 193 | public function checkout($ref): string 194 | { 195 | $this->clearAll(); 196 | 197 | $what = $ref; 198 | if ($ref instanceof Branch) { 199 | $what = $ref->getName(); 200 | } elseif ($ref instanceof TreeishInterface) { 201 | $what = $ref->getSha(); 202 | } 203 | 204 | $this->addCommandName(self::GIT_CHECKOUT); 205 | $this->addCommandArgument('-q'); 206 | $this->addCommandSubject($what); 207 | 208 | return $this->getCommand(); 209 | } 210 | 211 | /** 212 | * Move a file/directory 213 | * 214 | * @param string|Object $from source path 215 | * @param string|Object $to destination path 216 | * 217 | * @throws \RuntimeException 218 | * @throws \InvalidArgumentException 219 | * @return string 220 | */ 221 | public function move($from, $to): string 222 | { 223 | $this->clearAll(); 224 | 225 | $from = trim($from); 226 | if (!$this->validatePath($from)) { 227 | throw new \InvalidArgumentException('Invalid source path'); 228 | } 229 | 230 | $to = trim($to); 231 | if (!$this->validatePath($to)) { 232 | throw new \InvalidArgumentException('Invalid destination path'); 233 | } 234 | 235 | $this->addCommandName(self::GIT_MOVE); 236 | $this->addCommandSubject($from); 237 | $this->addCommandSubject2($to); 238 | 239 | return $this->getCommand(); 240 | } 241 | 242 | /** 243 | * Remove a file/directory 244 | * 245 | * @param string|Object $path the path to remove 246 | * @param bool $recursive recurse 247 | * @param bool $force force 248 | * 249 | * @throws \RuntimeException 250 | * @throws \InvalidArgumentException 251 | * @return string 252 | */ 253 | public function remove($path, $recursive, $force): string 254 | { 255 | $this->clearAll(); 256 | 257 | $path = trim($path); 258 | if (!$this->validatePath($path)) { 259 | throw new \InvalidArgumentException('Invalid path'); 260 | } 261 | 262 | $this->addCommandName(self::GIT_REMOVE); 263 | 264 | if ($recursive) { 265 | $this->addCommandArgument('-r'); 266 | } 267 | 268 | if ($force) { 269 | $this->addCommandArgument('-f'); 270 | } 271 | 272 | $this->addPath($path); 273 | 274 | return $this->getCommand(); 275 | } 276 | 277 | /** 278 | * Validates a path 279 | * 280 | * @param string $path path 281 | * 282 | * @return bool 283 | */ 284 | protected function validatePath($path): bool 285 | { 286 | if (empty($path)) { 287 | return false; 288 | } 289 | 290 | // we are always operating from root directory 291 | // so forbid relative paths 292 | if (false !== strpos($path, '..')) { 293 | return false; 294 | } 295 | 296 | return true; 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /src/GitElephant/Command/MergeCommand.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | class MergeCommand extends BaseCommand 32 | { 33 | public const MERGE_COMMAND = 'merge'; 34 | public const MERGE_OPTION_FF_ONLY = '--ff-only'; 35 | public const MERGE_OPTION_NO_FF = '--no-ff'; 36 | 37 | /** 38 | * constructor 39 | * 40 | * @param \GitElephant\Repository $repo The repository object this command 41 | * will interact with 42 | */ 43 | public function __construct(?Repository $repo = null) 44 | { 45 | parent::__construct($repo); 46 | } 47 | 48 | /** 49 | * Generate a merge command 50 | * 51 | * @param \GitElephant\Objects\Branch $with the branch to merge 52 | * @param string $message a message for the merge commit, if merge is 3-way 53 | * @param array $options option flags for git merge 54 | * 55 | * @throws \RuntimeException 56 | * @return string 57 | */ 58 | public function merge(Branch $with, $message = '', array $options = []): string 59 | { 60 | if (in_array(self::MERGE_OPTION_FF_ONLY, $options) && in_array(self::MERGE_OPTION_NO_FF, $options)) { 61 | throw new \Symfony\Component\Process\Exception\InvalidArgumentException( 62 | "Invalid options: cannot use flags --ff-only and --no-ff together." 63 | ); 64 | } 65 | 66 | $normalizedOptions = $this->normalizeOptions($options, $this->mergeCmdSwitchOptions()); 67 | 68 | $this->clearAll(); 69 | $this->addCommandName(static::MERGE_COMMAND); 70 | 71 | foreach ($normalizedOptions as $value) { 72 | $this->addCommandArgument($value); 73 | } 74 | 75 | if (!empty($message)) { 76 | $this->addCommandArgument('-m'); 77 | $this->addCommandArgument($message); 78 | } 79 | 80 | $this->addCommandSubject($with->getFullRef()); 81 | 82 | return $this->getCommand(); 83 | } 84 | 85 | /** 86 | * Valid options for remote command that do not require an associated value 87 | * 88 | * @return array Associative array mapping all non-value options and their respective normalized option 89 | */ 90 | public function mergeCmdSwitchOptions(): array 91 | { 92 | return [ 93 | self::MERGE_OPTION_FF_ONLY => self::MERGE_OPTION_FF_ONLY, 94 | self::MERGE_OPTION_NO_FF => self::MERGE_OPTION_NO_FF, 95 | ]; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/GitElephant/Command/MvCommand.php: -------------------------------------------------------------------------------- 1 | isBlob()) { 58 | throw new \InvalidArgumentException("The given object is not a blob, it couldn't be renamed"); 59 | } 60 | $sourceName = $source->getFullPath(); 61 | } else { 62 | $sourceName = $source; 63 | } 64 | $this->clearAll(); 65 | $this->addCommandName(self::MV_COMMAND); 66 | // Skip move or rename actions which would lead to an error condition 67 | $this->addCommandArgument('-k'); 68 | $this->addCommandSubject($sourceName); 69 | $this->addCommandSubject2($target); 70 | 71 | return $this->getCommand(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/GitElephant/Command/PullCommand.php: -------------------------------------------------------------------------------- 1 | getName(); 57 | } 58 | if ($branch instanceof Branch) { 59 | $branch = $branch->getName(); 60 | } 61 | $this->clearAll(); 62 | $this->addCommandName(self::GIT_PULL_COMMAND); 63 | if ($rebase) { 64 | $this->addCommandArgument('--rebase'); 65 | } 66 | if (!is_null($remote)) { 67 | $this->addCommandSubject($remote); 68 | } 69 | if (!is_null($branch)) { 70 | $this->addCommandSubject2($branch); 71 | } 72 | 73 | return $this->getCommand(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/GitElephant/Command/PushCommand.php: -------------------------------------------------------------------------------- 1 | clearAll(); 55 | 56 | if ($remote instanceof Remote) { 57 | $remote = $remote->getName(); 58 | } 59 | if ($branch instanceof Branch) { 60 | $branch = $branch->getName(); 61 | } 62 | 63 | $this->addCommandName(self::GIT_PUSH_COMMAND); 64 | $this->addCommandSubject($remote); 65 | $this->addCommandSubject2($branch); 66 | 67 | if (!is_null($args)) { 68 | $this->addCommandArgument($args); 69 | } 70 | 71 | return $this->getCommand(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/GitElephant/Command/Remote/AddSubCommand.php: -------------------------------------------------------------------------------- 1 | 33 | */ 34 | 35 | class AddSubCommand extends SubCommandCommand 36 | { 37 | public const GIT_REMOTE_ADD = 'add'; 38 | public const GIT_REMOTE_ADD_OPTION_FETCH = '-f'; 39 | public const GIT_REMOTE_ADD_OPTION_TAGS = '--tags'; 40 | public const GIT_REMOTE_ADD_OPTION_NOTAGS = '--no-tags'; 41 | public const GIT_REMOTE_ADD_OPTION_MIRROR = '--mirror'; 42 | public const GIT_REMOTE_ADD_OPTION_SETHEAD = '-m'; 43 | public const GIT_REMOTE_ADD_OPTION_TRACK = '-t'; 44 | 45 | /** 46 | * constructor 47 | * 48 | * @param \GitElephant\Repository $repo The repository object this command 49 | * will interact with 50 | */ 51 | public function __construct(?Repository $repo = null) 52 | { 53 | parent::__construct($repo); 54 | } 55 | 56 | /** 57 | * Valid options for remote command that require an associated value 58 | * 59 | * @return array Array of all value-required options 60 | */ 61 | public function addCmdValueOptions(): array 62 | { 63 | return [ 64 | self::GIT_REMOTE_ADD_OPTION_TRACK => self::GIT_REMOTE_ADD_OPTION_TRACK, 65 | self::GIT_REMOTE_ADD_OPTION_MIRROR => self::GIT_REMOTE_ADD_OPTION_MIRROR, 66 | self::GIT_REMOTE_ADD_OPTION_SETHEAD => self::GIT_REMOTE_ADD_OPTION_SETHEAD, 67 | ]; 68 | } 69 | 70 | /** 71 | * switch only options for the add subcommand 72 | * 73 | * @return array 74 | */ 75 | public function addCmdSwitchOptions(): array 76 | { 77 | return [ 78 | self::GIT_REMOTE_ADD_OPTION_TAGS => self::GIT_REMOTE_ADD_OPTION_TAGS, 79 | self::GIT_REMOTE_ADD_OPTION_NOTAGS => self::GIT_REMOTE_ADD_OPTION_NOTAGS, 80 | self::GIT_REMOTE_ADD_OPTION_FETCH => self::GIT_REMOTE_ADD_OPTION_FETCH, 81 | ]; 82 | } 83 | 84 | /** 85 | * build add sub command 86 | * 87 | * @param string $name remote name 88 | * @param string $url URL of remote 89 | * @param array $options options for the add subcommand 90 | * 91 | * @return AddSubCommand 92 | */ 93 | public function prepare($name, $url, $options = []): self 94 | { 95 | $options = $this->normalizeOptions( 96 | $options, 97 | $this->addCmdSwitchOptions(), 98 | $this->addCmdValueOptions() 99 | ); 100 | 101 | $this->addCommandName(self::GIT_REMOTE_ADD); 102 | $this->addCommandSubject($name); 103 | $this->addCommandSubject($url); 104 | foreach ($options as $option) { 105 | $this->addCommandArgument($option); 106 | } 107 | 108 | return $this; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/GitElephant/Command/Remote/ShowSubCommand.php: -------------------------------------------------------------------------------- 1 | 33 | */ 34 | 35 | class ShowSubCommand extends SubCommandCommand 36 | { 37 | public const GIT_REMOTE_SHOW = 'show'; 38 | 39 | /** 40 | * constructor 41 | * 42 | * @param \GitElephant\Repository $repo The repository object this command 43 | * will interact with 44 | */ 45 | public function __construct(?Repository $repo = null) 46 | { 47 | parent::__construct($repo); 48 | } 49 | 50 | /** 51 | * build show sub command 52 | * 53 | * NOTE: for technical reasons $name is optional, however under normal 54 | * implementation it SHOULD be passed! 55 | * 56 | * @param string $name 57 | * @param bool $queryRemotes Fetch new information from remotes 58 | * 59 | * @return ShowSubCommand 60 | */ 61 | public function prepare($name = null, $queryRemotes = true): self 62 | { 63 | $this->addCommandName(self::GIT_REMOTE_SHOW); 64 | /** 65 | * only add subject if relevant, 66 | * otherwise on repositories without a remote defined (ie, fresh 67 | * init'd or mock) will likely trigger warning/error condition 68 | */ 69 | if ($name) { 70 | $this->addCommandSubject($name); 71 | } 72 | 73 | if (!$queryRemotes) { 74 | $this->addCommandArgument('-n'); 75 | } 76 | 77 | return $this; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/GitElephant/Command/RemoteCommand.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | class RemoteCommand extends BaseCommand 36 | { 37 | public const GIT_REMOTE = 'remote'; 38 | public const GIT_REMOTE_OPTION_VERBOSE = '--verbose'; 39 | public const GIT_REMOTE_OPTION_VERBOSE_SHORT = '-v'; 40 | 41 | /** 42 | * constructor 43 | * 44 | * @param \GitElephant\Repository $repo The repository object this command 45 | * will interact with 46 | */ 47 | public function __construct(?Repository $repo = null) 48 | { 49 | parent::__construct($repo); 50 | } 51 | 52 | /** 53 | * Build the remote command 54 | * 55 | * NOTE: git-remote is most useful when using its subcommands, therefore 56 | * in practice you will likely pass a SubCommandCommand object. This 57 | * class provide "convenience" methods that do this for you. 58 | * 59 | * @param \GitElephant\Command\SubCommandCommand $subcommand A subcommand object 60 | * @param array $options Options for the main git-remote command 61 | * 62 | * @throws \RuntimeException 63 | * @return string Command string to pass to caller 64 | */ 65 | public function remote(?SubCommandCommand $subcommand = null, array $options = []): string 66 | { 67 | $normalizedOptions = $this->normalizeOptions($options, $this->remoteCmdSwitchOptions()); 68 | 69 | $this->clearAll(); 70 | 71 | $this->addCommandName(self::GIT_REMOTE); 72 | 73 | foreach ($normalizedOptions as $value) { 74 | $this->addCommandArgument($value); 75 | } 76 | if ($subcommand !== null) { 77 | $this->addCommandSubject($subcommand); 78 | } 79 | 80 | return $this->getCommand(); 81 | } 82 | 83 | /** 84 | * Valid options for remote command that do not require an associated value 85 | * 86 | * @return array Associative array mapping all non-value options and their respective normalized option 87 | */ 88 | public function remoteCmdSwitchOptions(): array 89 | { 90 | return [ 91 | self::GIT_REMOTE_OPTION_VERBOSE => self::GIT_REMOTE_OPTION_VERBOSE, 92 | self::GIT_REMOTE_OPTION_VERBOSE_SHORT => self::GIT_REMOTE_OPTION_VERBOSE, 93 | ]; 94 | } 95 | 96 | /** 97 | * git-remote --verbose command 98 | * 99 | * @throws \RuntimeException 100 | * @return string 101 | */ 102 | public function verbose(): string 103 | { 104 | return $this->remote(null, [self::GIT_REMOTE_OPTION_VERBOSE]); 105 | } 106 | 107 | /** 108 | * git-remote show [name] command 109 | * 110 | * NOTE: for technical reasons $name is optional, however under normal 111 | * implementation it SHOULD be passed! 112 | * 113 | * @param string $name 114 | * @param bool $queryRemotes 115 | * 116 | * @throws \RuntimeException 117 | * @return string 118 | */ 119 | public function show($name = null, bool $queryRemotes = true): string 120 | { 121 | $subcmd = new ShowSubCommand(); 122 | $subcmd->prepare($name, $queryRemotes); 123 | 124 | return $this->remote($subcmd); 125 | } 126 | 127 | /** 128 | * git-remote add [options] 129 | * 130 | * @param string $name remote name 131 | * @param string $url URL of remote 132 | * @param array $options options for the add subcommand 133 | * 134 | * @throws \RuntimeException 135 | * @return string 136 | */ 137 | public function add($name, $url, $options = []): string 138 | { 139 | $subcmd = new AddSubCommand(); 140 | $subcmd->prepare($name, $url, $options); 141 | 142 | return $this->remote($subcmd); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/GitElephant/Command/ResetCommand.php: -------------------------------------------------------------------------------- 1 | clearAll(); 45 | $this->addCommandName(self::GIT_RESET_COMMAND); 46 | // if there are options add them. 47 | foreach ($options as $option) { 48 | $this->addCommandArgument($option); 49 | } 50 | 51 | if ($arg != null) { 52 | $this->addCommandSubject2($arg); 53 | } 54 | 55 | return $this->getCommand(); 56 | } 57 | 58 | /** 59 | * @param Repository $repository 60 | * @return ResetCommand 61 | */ 62 | public static function getInstance(?Repository $repository = null): \GitElephant\Command\ResetCommand 63 | { 64 | return new self($repository); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/GitElephant/Command/RevListCommand.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | class RevListCommand extends BaseCommand 33 | { 34 | public const GIT_REVLIST = 'rev-list'; 35 | 36 | /** 37 | * constructor 38 | * 39 | * @param \GitElephant\Repository $repo The repository object this command 40 | * will interact with 41 | */ 42 | public function __construct(?Repository $repo = null) 43 | { 44 | parent::__construct($repo); 45 | } 46 | 47 | /** 48 | * get tag commit command via rev-list 49 | * 50 | * @param \GitElephant\Objects\Tag $tag a tag instance 51 | * 52 | * @throws \RuntimeException 53 | * @return string 54 | */ 55 | public function getTagCommit(Tag $tag): string 56 | { 57 | $this->clearAll(); 58 | $this->addCommandName(static::GIT_REVLIST); 59 | // only the last commit 60 | $this->addCommandArgument('-n1'); 61 | $this->addCommandSubject($tag->getFullRef()); 62 | 63 | return $this->getCommand(); 64 | } 65 | 66 | /** 67 | * get the commits path to the passed commit. Useful to count commits in a repo 68 | * 69 | * @param \GitElephant\Objects\Commit $commit commit instance 70 | * @param int $max max count 71 | * 72 | * @throws \RuntimeException 73 | * @return string 74 | */ 75 | public function commitPath(Commit $commit, $max = 1000): string 76 | { 77 | $this->clearAll(); 78 | $this->addCommandName(static::GIT_REVLIST); 79 | $this->addCommandArgument(sprintf('--max-count=%s', $max)); 80 | $this->addCommandSubject($commit->getSha()); 81 | 82 | return $this->getCommand(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/GitElephant/Command/RevParseCommand.php: -------------------------------------------------------------------------------- 1 | clearAll(); 92 | $this->addCommandName(self::GIT_REV_PARSE_COMMAND); 93 | // if there are options add them. 94 | foreach ($options as $option) { 95 | $this->addCommandArgument($option); 96 | } 97 | 98 | if (!is_null($arg)) { 99 | $this->addCommandSubject2($arg); 100 | } 101 | 102 | return $this->getCommand(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/GitElephant/Command/ShowCommand.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | class ShowCommand extends BaseCommand 31 | { 32 | public const GIT_SHOW = 'show'; 33 | 34 | /** 35 | * constructor 36 | * 37 | * @param \GitElephant\Repository $repo The repository object this command 38 | * will interact with 39 | */ 40 | public function __construct(?Repository $repo = null) 41 | { 42 | parent::__construct($repo); 43 | } 44 | 45 | /** 46 | * build the show command 47 | * 48 | * @param string|\GitElephant\Objects\Commit $ref the reference for the show command 49 | * 50 | * @throws \RuntimeException 51 | * @return string 52 | */ 53 | public function showCommit($ref): string 54 | { 55 | $this->clearAll(); 56 | 57 | $this->addCommandName(self::GIT_SHOW); 58 | $this->addCommandArgument('-s'); 59 | $this->addCommandArgument('--pretty=raw'); 60 | $this->addCommandArgument('--no-color'); 61 | $this->addCommandSubject($ref); 62 | 63 | return $this->getCommand(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/GitElephant/Command/StashCommand.php: -------------------------------------------------------------------------------- 1 | 29 | * @author Kirk Madera 30 | */ 31 | class StashCommand extends BaseCommand 32 | { 33 | public const STASH_COMMAND = 'stash'; 34 | 35 | /** 36 | * constructor 37 | * 38 | * @param \GitElephant\Repository $repo The repository object this command 39 | * will interact with 40 | */ 41 | public function __construct(?Repository $repo = null) 42 | { 43 | parent::__construct($repo); 44 | } 45 | 46 | /** 47 | * Save your local modifications to a new stash, and run git reset --hard to revert them. 48 | * 49 | * @param string|null $message 50 | * @param boolean $includeUntracked 51 | * @param boolean $keepIndex 52 | * 53 | * @return string 54 | */ 55 | public function save($message = null, $includeUntracked = false, $keepIndex = false): string 56 | { 57 | $this->clearAll(); 58 | 59 | $this->addCommandName(self::STASH_COMMAND . ' save'); 60 | 61 | if (!is_null($message)) { 62 | $this->addCommandSubject($message); 63 | } 64 | 65 | if ($includeUntracked) { 66 | $this->addCommandArgument('--include-untracked'); 67 | } 68 | 69 | if ($keepIndex) { 70 | $this->addCommandArgument('--keep-index'); 71 | } 72 | 73 | return $this->getCommand(); 74 | } 75 | 76 | /** 77 | * Shows stash list 78 | * 79 | * @param array|null $options 80 | * 81 | * @return string 82 | */ 83 | public function listStashes(?array $options = null): string 84 | { 85 | $this->clearAll(); 86 | 87 | $this->addCommandName(self::STASH_COMMAND . ' list'); 88 | 89 | if (null !== $options) { 90 | $this->addCommandSubject($options); 91 | } 92 | 93 | return $this->getCommand(); 94 | } 95 | 96 | /** 97 | * Shows details for a specific stash 98 | * 99 | * @param string|int $stash 100 | * 101 | * @return string 102 | */ 103 | public function show($stash): string 104 | { 105 | $stash = $this->normalizeStashName($stash); 106 | $this->clearAll(); 107 | $this->addCommandName(self::STASH_COMMAND . ' show'); 108 | $this->addCommandSubject($stash); 109 | 110 | return $this->getCommand(); 111 | } 112 | 113 | /** 114 | * Drops a stash 115 | * 116 | * @param string $stash 117 | * 118 | * @return string 119 | */ 120 | public function drop($stash): string 121 | { 122 | $stash = $this->normalizeStashName($stash); 123 | $this->clearAll(); 124 | $this->addCommandName(self::STASH_COMMAND . ' drop'); 125 | $this->addCommandSubject($stash); 126 | 127 | return $this->getCommand(); 128 | } 129 | 130 | /** 131 | * Applies a stash 132 | * 133 | * @param string $stash 134 | * @param boolean $index 135 | * 136 | * @return string 137 | */ 138 | public function apply($stash, $index = false): string 139 | { 140 | $stash = $this->normalizeStashName($stash); 141 | $this->clearAll(); 142 | $this->addCommandName(self::STASH_COMMAND . ' apply'); 143 | $this->addCommandSubject($stash); 144 | if ($index) { 145 | $this->addCommandArgument('--index'); 146 | } 147 | 148 | return $this->getCommand(); 149 | } 150 | 151 | /** 152 | * Applies a stash, then removes it from the stash 153 | * 154 | * @param string $stash 155 | * @param boolean $index 156 | * 157 | * @return string 158 | */ 159 | public function pop($stash, $index = false): string 160 | { 161 | $stash = $this->normalizeStashName($stash); 162 | $this->clearAll(); 163 | $this->addCommandName(self::STASH_COMMAND . ' pop'); 164 | $this->addCommandSubject($stash); 165 | if ($index) { 166 | $this->addCommandArgument('--index'); 167 | } 168 | 169 | return $this->getCommand(); 170 | } 171 | 172 | /** 173 | * Creates and checks out a new branch named starting from the commit at which the was originally created 174 | * 175 | * @param string $branch 176 | * @param string $stash 177 | * 178 | * @return string 179 | */ 180 | public function branch($branch, $stash): string 181 | { 182 | $stash = $this->normalizeStashName($stash); 183 | $this->clearAll(); 184 | $this->addCommandName(self::STASH_COMMAND . ' branch'); 185 | $this->addCommandSubject($branch); 186 | $this->addCommandSubject2($stash); 187 | 188 | return $this->getCommand(); 189 | } 190 | 191 | /** 192 | * Remove all the stashed states. 193 | */ 194 | public function clear(): string 195 | { 196 | $this->clearAll(); 197 | $this->addCommandName(self::STASH_COMMAND . ' clear'); 198 | 199 | return $this->getCommand(); 200 | } 201 | 202 | /** 203 | * Create a stash (which is a regular commit object) and return its object name, without storing it anywhere in the 204 | * ref namespace. 205 | */ 206 | public function create(): string 207 | { 208 | $this->clearAll(); 209 | $this->addCommandName(self::STASH_COMMAND . ' create'); 210 | 211 | return $this->getCommand(); 212 | } 213 | 214 | /** 215 | * @param int|string $stash 216 | * 217 | * @return string 218 | */ 219 | private function normalizeStashName($stash): string 220 | { 221 | if (0 !== strpos($stash, 'stash@{')) { 222 | $stash = 'stash@{' . $stash . '}'; 223 | } 224 | 225 | return $stash; 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/GitElephant/Command/SubCommandCommand.php: -------------------------------------------------------------------------------- 1 | 33 | */ 34 | class SubCommandCommand extends BaseCommand 35 | { 36 | /** 37 | * Subjects to a subcommand name 38 | * 39 | * @var array 40 | */ 41 | private $orderedSubjects = []; 42 | 43 | /** 44 | * constructor 45 | * 46 | * @param \GitElephant\Repository $repo The repository object this command 47 | * will interact with 48 | */ 49 | public function __construct(?Repository $repo = null) 50 | { 51 | parent::__construct($repo); 52 | } 53 | 54 | /** 55 | * Clear all previous variables 56 | */ 57 | public function clearAll(): void 58 | { 59 | parent::clearAll(); 60 | $this->orderedSubjects = []; 61 | } 62 | 63 | /** 64 | * Add a subject to this subcommand 65 | * 66 | * @param SubCommandCommand|array|string $subject 67 | * @return void 68 | */ 69 | protected function addCommandSubject($subject): void 70 | { 71 | $this->orderedSubjects[] = $subject; 72 | } 73 | 74 | protected function getCommandSubjects(): array 75 | { 76 | return $this->orderedSubjects; 77 | } 78 | 79 | protected function extractArguments(array $args): string 80 | { 81 | $orderArgs = []; 82 | foreach ($args as $arg) { 83 | if (is_array($arg)) { 84 | foreach ($arg as $value) { 85 | if (!is_null($value)) { 86 | $orderArgs[] = escapeshellarg($value); 87 | } 88 | } 89 | } else { 90 | $orderArgs[] = escapeshellarg($arg); 91 | } 92 | } 93 | 94 | return implode(' ', $orderArgs); 95 | } 96 | 97 | /** 98 | * Get the sub command 99 | * 100 | * @return string 101 | * @throws \RuntimeException 102 | */ 103 | public function getCommand(): string 104 | { 105 | $command = $this->getCommandName(); 106 | 107 | $command .= ' '; 108 | $args = $this->getCommandArguments(); 109 | if (count($args) > 0) { 110 | $command .= $this->extractArguments($args); 111 | $command .= ' '; 112 | } 113 | $subjects = $this->getCommandSubjects(); 114 | if (!empty($subjects)) { 115 | $command .= implode(' ', array_map('escapeshellarg', $subjects)); 116 | } 117 | $command = preg_replace('/\\s{2,}/', ' ', $command); 118 | 119 | return trim($command); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/GitElephant/Command/SubmoduleCommand.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | class SubmoduleCommand extends BaseCommand 31 | { 32 | public const SUBMODULE_COMMAND = 'submodule'; 33 | public const SUBMODULE_ADD_COMMAND = 'add'; 34 | public const SUBMODULE_INIT_COMMAND = 'init'; 35 | public const SUBMODULE_UPDATE_COMMAND = 'update'; 36 | public const SUBMODULE_OPTION_FORCE = '--force'; 37 | public const SUBMODULE_OPTION_INIT = '--init'; 38 | public const SUBMODULE_OPTION_RECURSIVE = '--recursive'; 39 | 40 | /** 41 | * constructor 42 | * 43 | * @param \GitElephant\Repository $repo The repository object this command 44 | * will interact with 45 | */ 46 | public function __construct(?Repository $repo = null) 47 | { 48 | parent::__construct($repo); 49 | } 50 | 51 | /** 52 | * add a submodule 53 | * 54 | * @param string $gitUrl git url of the submodule 55 | * @param string $path path to register the submodule to 56 | * 57 | * @throws \RuntimeException 58 | * @return string 59 | */ 60 | public function add($gitUrl, $path = null): string 61 | { 62 | $this->clearAll(); 63 | $this->addCommandName(sprintf('%s %s', self::SUBMODULE_COMMAND, self::SUBMODULE_ADD_COMMAND)); 64 | $this->addCommandArgument($gitUrl); 65 | if (null !== $path) { 66 | $this->addCommandSubject($path); 67 | } 68 | 69 | return $this->getCommand(); 70 | } 71 | 72 | /** 73 | * initialize a repository's submodules 74 | * 75 | * @param string $path init only submodules at the specified path 76 | * 77 | * @return string 78 | */ 79 | public function init($path = null): string 80 | { 81 | $this->clearAll(); 82 | $this->addCommandName(sprintf('%s %s', self::SUBMODULE_COMMAND, self::SUBMODULE_INIT_COMMAND)); 83 | if (null !== $path) { 84 | $this->addPath($path); 85 | } 86 | 87 | return $this->getCommand(); 88 | } 89 | 90 | /** 91 | * Lists submodules 92 | * 93 | * @throws \RuntimeException 94 | * @return string the command 95 | */ 96 | public function listSubmodules(): string 97 | { 98 | $this->clearAll(); 99 | $this->addCommandName(self::SUBMODULE_COMMAND); 100 | 101 | return $this->getCommand(); 102 | } 103 | 104 | /** 105 | * Lists submodules 106 | * 107 | * @deprecated This method uses an unconventional name but is being left in 108 | * place to remain compatible with existing code relying on it. 109 | * New code should be written to use listSubmodules(). 110 | * 111 | * @throws \RuntimeException 112 | * @return string the command 113 | */ 114 | public function lists(): string 115 | { 116 | return $this->listSubmodules(); 117 | } 118 | 119 | /** 120 | * update a repository's submodules 121 | * 122 | * @param bool $recursive update recursively 123 | * @param bool $init init before update 124 | * @param bool $force force the checkout as part of update 125 | * @param string $path update only a specific submodule path 126 | * 127 | * @return string 128 | */ 129 | public function update( 130 | bool $recursive = false, 131 | bool $init = false, 132 | bool $force = false, 133 | ?string $path = null 134 | ): string { 135 | $this->clearAll(); 136 | $this->addCommandName(sprintf('%s %s', self::SUBMODULE_COMMAND, self::SUBMODULE_UPDATE_COMMAND)); 137 | if ($recursive) { 138 | $this->addCommandArgument(self::SUBMODULE_OPTION_RECURSIVE); 139 | } 140 | if ($init) { 141 | $this->addCommandArgument(self::SUBMODULE_OPTION_INIT); 142 | } 143 | if ($force) { 144 | $this->addCommandArgument(self::SUBMODULE_OPTION_FORCE); 145 | } 146 | if ($path !== null) { 147 | $this->addPath($path); 148 | } 149 | 150 | return $this->getCommand(); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/GitElephant/Command/TagCommand.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | class TagCommand extends BaseCommand 32 | { 33 | public const TAG_COMMAND = 'tag'; 34 | 35 | /** 36 | * constructor 37 | * 38 | * @param \GitElephant\Repository $repo The repository object this command 39 | * will interact with 40 | */ 41 | public function __construct(?Repository $repo = null) 42 | { 43 | parent::__construct($repo); 44 | } 45 | 46 | /** 47 | * Create a new tag 48 | * 49 | * @param string $name The new tag name 50 | * @param string|null $startPoint the new tag start point. 51 | * @param string|null $message the tag message 52 | * 53 | * @throws \RuntimeException 54 | * @return string the command 55 | */ 56 | public function create(string $name, $startPoint = null, $message = null): string 57 | { 58 | $this->clearAll(); 59 | $this->addCommandName(self::TAG_COMMAND); 60 | 61 | if (null !== $message) { 62 | $this->addCommandArgument('-m'); 63 | $this->addCommandArgument($message); 64 | } 65 | if (null !== $startPoint) { 66 | $this->addCommandArgument($name); 67 | $this->addCommandSubject($startPoint); 68 | } else { 69 | $this->addCommandSubject($name); 70 | } 71 | 72 | return $this->getCommand(); 73 | } 74 | 75 | /** 76 | * Lists tags 77 | * 78 | * @throws \RuntimeException 79 | * @return string the command 80 | */ 81 | public function listTags(): string 82 | { 83 | $this->clearAll(); 84 | $this->addCommandName(self::TAG_COMMAND); 85 | 86 | return $this->getCommand(); 87 | } 88 | 89 | /** 90 | * Lists tags 91 | * 92 | * @deprecated This method uses an unconventional name but is being left in 93 | * place to remain compatible with existing code relying on it. 94 | * New code should be written to use listTags(). 95 | * 96 | * @throws \RuntimeException 97 | * @return string the command 98 | */ 99 | public function lists(): string 100 | { 101 | return $this->listTags(); 102 | } 103 | 104 | /** 105 | * Delete a tag 106 | * 107 | * @param string|Tag $tag The name of tag, or the Tag instance to delete 108 | * 109 | * @throws \RuntimeException 110 | * @return string the command 111 | */ 112 | public function delete($tag): string 113 | { 114 | $this->clearAll(); 115 | $this->addCommandName(self::TAG_COMMAND); 116 | 117 | $name = $tag; 118 | if ($tag instanceof Tag) { 119 | $name = $tag->getName(); 120 | } 121 | 122 | $this->addCommandArgument('-d'); 123 | $this->addCommandSubject($name); 124 | 125 | return $this->getCommand(); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/GitElephant/Exception/InvalidBranchNameException.php: -------------------------------------------------------------------------------- 1 | messageTpl, $message), $code, $previous); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/GitElephant/Objects/Author.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | class Author 29 | { 30 | /** 31 | * Author name 32 | * 33 | * @var string 34 | */ 35 | private $name; 36 | 37 | /** 38 | * Author email 39 | * 40 | * @var string 41 | */ 42 | private $email; 43 | 44 | /** 45 | * return author as RFC 822 representation ( Foo Bar name . ' <' . $this->email . '>'; 52 | } 53 | 54 | /** 55 | * email setter 56 | * 57 | * @param string $email the email 58 | */ 59 | public function setEmail(string $email): void 60 | { 61 | $this->email = $email; 62 | } 63 | 64 | /** 65 | * email getter 66 | * 67 | * @return string|null 68 | */ 69 | public function getEmail(): ?string 70 | { 71 | return $this->email; 72 | } 73 | 74 | /** 75 | * name setter 76 | * 77 | * @param string $name the author name 78 | */ 79 | public function setName(string $name): void 80 | { 81 | $this->name = $name; 82 | } 83 | 84 | /** 85 | * name getter 86 | * 87 | * @return string|null 88 | */ 89 | public function getName(): ?string 90 | { 91 | return $this->name; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/GitElephant/Objects/Commit/Message.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | class Message 29 | { 30 | /** 31 | * the message 32 | * 33 | * @var array|string 34 | */ 35 | private $message; 36 | 37 | /** 38 | * Class constructor 39 | * 40 | * @param array|string $message Message lines 41 | */ 42 | public function __construct($message) 43 | { 44 | if (is_array($message)) { 45 | $this->message = $message; 46 | } else { 47 | $this->message = []; 48 | $this->message = (string)$message; 49 | } 50 | } 51 | 52 | /** 53 | * Short message equals first message line 54 | * 55 | * @return string|null 56 | */ 57 | public function getShortMessage(): ?string 58 | { 59 | return $this->toString(); 60 | } 61 | 62 | /** 63 | * Full commit message 64 | * 65 | * @return string|null 66 | */ 67 | public function getFullMessage(): ?string 68 | { 69 | return $this->toString(true); 70 | } 71 | 72 | /** 73 | * Return message string 74 | * 75 | * @param bool $full get the full message 76 | * 77 | * @return string|null 78 | */ 79 | public function toString(bool $full = false): ?string 80 | { 81 | if (empty($this->message)) { 82 | return null; 83 | } 84 | 85 | if ($full) { 86 | return implode(PHP_EOL, $this->message); 87 | } else { 88 | return $this->message[0]; 89 | } 90 | } 91 | 92 | /** 93 | * String representation equals short message 94 | * 95 | * @return string 96 | */ 97 | public function __toString(): string 98 | { 99 | $thisString = $this->toString(); 100 | return $thisString === null ? "" : $thisString; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/GitElephant/Objects/Diff/Diff.php: -------------------------------------------------------------------------------- 1 | 33 | */ 34 | class Diff implements \ArrayAccess, \Countable, \Iterator 35 | { 36 | /** 37 | * @var \GitElephant\Repository 38 | */ 39 | private $repository; 40 | 41 | /** 42 | * the cursor position 43 | * 44 | * @var int 45 | */ 46 | private $position; 47 | 48 | /** 49 | * DiffObject instances 50 | * 51 | * @var array 52 | */ 53 | private $diffObjects = []; 54 | 55 | /** 56 | * static generator to generate a Diff object 57 | * 58 | * @param \GitElephant\Repository $repository repository 59 | * @param null|string|\GitElephant\Objects\Commit $commit1 first commit 60 | * @param null|string|\GitElephant\Objects\Commit $commit2 second commit 61 | * @param null|string $path path to consider 62 | * 63 | * @throws \RuntimeException 64 | * @throws \InvalidArgumentException 65 | * @throws \Symfony\Component\Process\Exception\RuntimeException 66 | * @return Diff 67 | */ 68 | public static function create( 69 | Repository $repository, 70 | $commit1 = null, 71 | $commit2 = null, 72 | ?string $path = null 73 | ): \GitElephant\Objects\Diff\Diff { 74 | $commit = new self($repository); 75 | $commit->createFromCommand($commit1, $commit2, $path); 76 | 77 | return $commit; 78 | } 79 | 80 | /** 81 | * Class constructor 82 | * bare Diff object 83 | * 84 | * @param \GitElephant\Repository $repository repository instance 85 | * @param array $diffObjects array of diff objects 86 | */ 87 | public function __construct(Repository $repository, array $diffObjects = []) 88 | { 89 | $this->position = 0; 90 | $this->repository = $repository; 91 | $this->diffObjects = $diffObjects; 92 | } 93 | 94 | /** 95 | * get the commit properties from command 96 | * 97 | * @param string|null$commit1 commit 1 98 | * @param string|null$commit2 commit 2 99 | * @param string|null$path path 100 | * 101 | * @throws \RuntimeException 102 | * @throws \Symfony\Component\Process\Exception\InvalidArgumentException 103 | * @throws \Symfony\Component\Process\Exception\LogicException 104 | * @throws \InvalidArgumentException 105 | * @throws \Symfony\Component\Process\Exception\RuntimeException 106 | * @see ShowCommand::commitInfo 107 | */ 108 | public function createFromCommand($commit1 = null, $commit2 = null, $path = null): void 109 | { 110 | if (null === $commit1) { 111 | $commit1 = $this->getRepository()->getCommit(); 112 | } 113 | 114 | if (is_string($commit1)) { 115 | $commit1 = $this->getRepository()->getCommit($commit1); 116 | } 117 | 118 | if ($commit2 === null) { 119 | if ($commit1->isRoot()) { 120 | $command = DiffTreeCommand::getInstance($this->repository)->rootDiff($commit1); 121 | } else { 122 | $command = DiffCommand::getInstance($this->repository)->diff($commit1); 123 | } 124 | } else { 125 | if (is_string($commit2)) { 126 | $commit2 = $this->getRepository()->getCommit($commit2); 127 | } 128 | $command = DiffCommand::getInstance($this->repository)->diff($commit1, $commit2, $path); 129 | } 130 | 131 | $outputLines = $this->getCaller()->execute($command)->getOutputLines(); 132 | $this->parseOutputLines($outputLines); 133 | } 134 | 135 | /** 136 | * parse the output of a git command showing a commit 137 | * 138 | * @param array $outputLines output lines 139 | * 140 | * @throws \InvalidArgumentException 141 | */ 142 | private function parseOutputLines(array $outputLines): void 143 | { 144 | $this->diffObjects = []; 145 | $splitArray = Utilities::pregSplitArray($outputLines, '/^diff --git SRC\/(.*) DST\/(.*)$/'); 146 | 147 | foreach ($splitArray as $diffObjectLines) { 148 | $this->diffObjects[] = new DiffObject($diffObjectLines); 149 | } 150 | } 151 | 152 | /** 153 | * @return \GitElephant\Command\Caller\CallerInterface 154 | */ 155 | private function getCaller(): CallerInterface 156 | { 157 | return $this->getRepository()->getCaller(); 158 | } 159 | 160 | /** 161 | * Repository setter 162 | * 163 | * @param \GitElephant\Repository $repository the repository variable 164 | */ 165 | public function setRepository(Repository $repository): void 166 | { 167 | $this->repository = $repository; 168 | } 169 | 170 | /** 171 | * Repository getter 172 | * 173 | * @return \GitElephant\Repository 174 | */ 175 | public function getRepository(): \GitElephant\Repository 176 | { 177 | return $this->repository; 178 | } 179 | 180 | /** 181 | * ArrayAccess interface 182 | * 183 | * @param int $offset offset 184 | * 185 | * @return bool 186 | */ 187 | public function offsetExists($offset): bool 188 | { 189 | return isset($this->diffObjects[$offset]); 190 | } 191 | 192 | /** 193 | * ArrayAccess interface 194 | * 195 | * @param int $offset offset 196 | * 197 | * @return null|mixed 198 | */ 199 | public function offsetGet($offset): mixed 200 | { 201 | return isset($this->diffObjects[$offset]) ? $this->diffObjects[$offset] : null; 202 | } 203 | 204 | /** 205 | * ArrayAccess interface 206 | * 207 | * @param int|null $offset offset 208 | * @param mixed $value value 209 | */ 210 | public function offsetSet($offset, $value): void 211 | { 212 | if (is_null($offset)) { 213 | $this->diffObjects[] = $value; 214 | } else { 215 | $this->diffObjects[$offset] = $value; 216 | } 217 | } 218 | 219 | /** 220 | * ArrayAccess interface 221 | * 222 | * @param int $offset offset 223 | */ 224 | public function offsetUnset($offset): void 225 | { 226 | unset($this->diffObjects[$offset]); 227 | } 228 | 229 | /** 230 | * Countable interface 231 | * 232 | * @return int 233 | */ 234 | public function count(): int 235 | { 236 | return count($this->diffObjects); 237 | } 238 | 239 | /** 240 | * Iterator interface 241 | * 242 | * @return mixed 243 | */ 244 | public function current(): mixed 245 | { 246 | return $this->diffObjects[$this->position]; 247 | } 248 | 249 | /** 250 | * Iterator interface 251 | */ 252 | public function next(): void 253 | { 254 | ++$this->position; 255 | } 256 | 257 | /** 258 | * Iterator interface 259 | * 260 | * @return int 261 | */ 262 | public function key(): int 263 | { 264 | return $this->position; 265 | } 266 | 267 | /** 268 | * Iterator interface 269 | * 270 | * @return bool 271 | */ 272 | public function valid(): bool 273 | { 274 | return isset($this->diffObjects[$this->position]); 275 | } 276 | 277 | /** 278 | * Iterator interface 279 | */ 280 | public function rewind(): void 281 | { 282 | $this->position = 0; 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/GitElephant/Objects/Diff/DiffChunk.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | class DiffChunk implements \ArrayAccess, \Countable, \Iterator 29 | { 30 | /** 31 | * the cursor position 32 | * 33 | * @var int 34 | */ 35 | private $position; 36 | 37 | /** 38 | * diff start line from original file 39 | * 40 | * @var int 41 | */ 42 | private $originStartLine; 43 | 44 | /** 45 | * diff end line from original file 46 | * 47 | * @var int 48 | */ 49 | private $originEndLine; 50 | 51 | /** 52 | * diff start line from destination file 53 | * 54 | * @var int 55 | */ 56 | private $destStartLine; 57 | 58 | /** 59 | * diff end line from destination file 60 | * 61 | * @var int 62 | */ 63 | private $destEndLine; 64 | 65 | /** 66 | * hunk header line 67 | * 68 | * @var string 69 | */ 70 | private $headerLine; 71 | 72 | /** 73 | * array of lines 74 | * 75 | * @var array 76 | */ 77 | private $lines = []; 78 | 79 | /** 80 | * Class constructor 81 | * 82 | * @param array $lines output lines from git binary 83 | * 84 | * @throws \Exception 85 | */ 86 | public function __construct(array $lines) 87 | { 88 | $this->position = 0; 89 | 90 | $this->getLinesNumbers($lines[0]); 91 | $this->parseLines(array_slice($lines, 1)); 92 | } 93 | 94 | /** 95 | * Parse lines 96 | * 97 | * @param array $lines output lines 98 | * 99 | * @throws \Exception 100 | */ 101 | private function parseLines(array $lines): void 102 | { 103 | $originUnchanged = $this->originStartLine; 104 | $destUnchanged = $this->destStartLine; 105 | 106 | $deleted = $this->originStartLine; 107 | $new = $this->destStartLine; 108 | foreach ($lines as $line) { 109 | if (preg_match('/^\+(.*)/', $line)) { 110 | $this->lines[] = new DiffChunkLineAdded($new++, preg_replace('/\+(.*)/', ' $1', $line)); 111 | $destUnchanged++; 112 | } elseif (preg_match('/^-(.*)/', $line)) { 113 | $this->lines[] = new DiffChunkLineDeleted($deleted++, preg_replace('/-(.*)/', ' $1', $line)); 114 | $originUnchanged++; 115 | } elseif (preg_match('/^ (.*)/', $line) || $line == '') { 116 | $this->lines[] = new DiffChunkLineUnchanged($originUnchanged++, $destUnchanged++, $line); 117 | $deleted++; 118 | $new++; 119 | } elseif (!preg_match('/\\ No newline at end of file/', $line)) { 120 | throw new \Exception(sprintf('GitElephant was unable to parse the line %s', $line)); 121 | } 122 | } 123 | } 124 | 125 | /** 126 | * Get line numbers 127 | * 128 | * @param string $line a single line 129 | */ 130 | private function getLinesNumbers(string $line): void 131 | { 132 | $matches = []; 133 | preg_match('/@@ -(.*) \+(.*) @@?(.*)/', $line, $matches); 134 | if (!strpos($matches[1], ',')) { 135 | // one line 136 | $this->originStartLine = (int) $matches[1]; 137 | $this->originEndLine = (int) $matches[1]; 138 | } else { 139 | $this->originStartLine = (int) explode(',', $matches[1])[0]; 140 | $this->originEndLine = (int) explode(',', $matches[1])[1]; 141 | } 142 | 143 | if (!strpos($matches[2], ',')) { 144 | // one line 145 | $this->destStartLine = (int) $matches[2]; 146 | $this->destEndLine = (int) $matches[2]; 147 | } else { 148 | $this->destStartLine = (int) explode(',', $matches[2])[0]; 149 | $this->destEndLine = (int) explode(',', $matches[2])[1]; 150 | } 151 | } 152 | 153 | /** 154 | * destStartLine getter 155 | * 156 | * @return int 157 | */ 158 | public function getDestStartLine(): int 159 | { 160 | return $this->destStartLine; 161 | } 162 | 163 | /** 164 | * destEndLine getter 165 | * 166 | * @return int 167 | */ 168 | public function getDestEndLine(): int 169 | { 170 | return $this->destEndLine; 171 | } 172 | 173 | /** 174 | * originStartLine getter 175 | * 176 | * @return int 177 | */ 178 | public function getOriginStartLine(): int 179 | { 180 | return $this->originStartLine; 181 | } 182 | 183 | /** 184 | * originEndLine getter 185 | * 186 | * @return int 187 | */ 188 | public function getOriginEndLine(): int 189 | { 190 | return $this->originEndLine; 191 | } 192 | 193 | /** 194 | * Get hunk header line 195 | * 196 | * @return string 197 | */ 198 | public function getHeaderLine(): string 199 | { 200 | if (null === $this->headerLine) { 201 | $line = '@@'; 202 | $line .= ' -' . $this->getOriginStartLine() . ',' . $this->getOriginEndLine(); 203 | $line .= ' +' . $this->getDestStartLine() . ',' . $this->getDestEndLine(); 204 | $line .= ' @@'; 205 | 206 | $this->headerLine = $line; 207 | } 208 | 209 | return $this->headerLine; 210 | } 211 | 212 | /** 213 | * Get Lines 214 | * 215 | * @return array 216 | */ 217 | public function getLines(): array 218 | { 219 | return $this->lines; 220 | } 221 | 222 | /** 223 | * ArrayAccess interface 224 | * 225 | * @param int $offset offset 226 | * 227 | * @return bool 228 | */ 229 | public function offsetExists($offset): bool 230 | { 231 | return isset($this->lines[$offset]); 232 | } 233 | 234 | /** 235 | * ArrayAccess interface 236 | * 237 | * @param int $offset offset 238 | * 239 | * @return DiffChunkLine|null 240 | */ 241 | public function offsetGet($offset): ?DiffChunkLine 242 | { 243 | return isset($this->lines[$offset]) ? $this->lines[$offset] : null; 244 | } 245 | 246 | /** 247 | * ArrayAccess interface 248 | * 249 | * @param int|null $offset offset 250 | * @param mixed $value value 251 | */ 252 | public function offsetSet($offset, $value): void 253 | { 254 | if (is_null($offset)) { 255 | $this->lines[] = $value; 256 | } else { 257 | $this->lines[$offset] = $value; 258 | } 259 | } 260 | 261 | /** 262 | * ArrayAccess interface 263 | * 264 | * @param int $offset offset 265 | */ 266 | public function offsetUnset($offset): void 267 | { 268 | unset($this->lines[$offset]); 269 | } 270 | 271 | /** 272 | * Countable interface 273 | * 274 | * @return int 275 | */ 276 | public function count(): int 277 | { 278 | return count($this->lines); 279 | } 280 | 281 | /** 282 | * Iterator interface 283 | * 284 | * @return DiffChunkLine|null 285 | */ 286 | public function current(): ?DiffChunkLine 287 | { 288 | return $this->lines[$this->position]; 289 | } 290 | 291 | /** 292 | * Iterator interface 293 | */ 294 | public function next(): void 295 | { 296 | ++$this->position; 297 | } 298 | 299 | /** 300 | * Iterator interface 301 | * 302 | * @return int 303 | */ 304 | public function key(): int 305 | { 306 | return $this->position; 307 | } 308 | 309 | /** 310 | * Iterator interface 311 | * 312 | * @return bool 313 | */ 314 | public function valid(): bool 315 | { 316 | return isset($this->lines[$this->position]); 317 | } 318 | 319 | /** 320 | * Iterator interface 321 | */ 322 | public function rewind(): void 323 | { 324 | $this->position = 0; 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /src/GitElephant/Objects/Diff/DiffChunkLine.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | abstract class DiffChunkLine 29 | { 30 | public const UNCHANGED = "unchanged"; 31 | public const ADDED = "added"; 32 | public const DELETED = "deleted"; 33 | 34 | /** 35 | * line type 36 | * 37 | * @var string 38 | */ 39 | protected $type; 40 | 41 | /** 42 | * line content 43 | * 44 | * @var string 45 | */ 46 | protected $content; 47 | 48 | /** 49 | * toString magic method 50 | * 51 | * @return string the line content 52 | */ 53 | public function __toString(): string 54 | { 55 | return $this->getContent(); 56 | } 57 | 58 | /** 59 | * type setter 60 | * 61 | * @param string $type line type 62 | */ 63 | public function setType(string $type): void 64 | { 65 | $this->type = $type; 66 | } 67 | 68 | /** 69 | * type getter 70 | * 71 | * @return string 72 | */ 73 | public function getType(): string 74 | { 75 | return $this->type; 76 | } 77 | 78 | /** 79 | * content setter 80 | * 81 | * @param string $content line content 82 | */ 83 | public function setContent(string $content): void 84 | { 85 | $this->content = $content; 86 | } 87 | 88 | /** 89 | * content getter 90 | * 91 | * @return string 92 | */ 93 | public function getContent(): string 94 | { 95 | return $this->content; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/GitElephant/Objects/Diff/DiffChunkLineAdded.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | class DiffChunkLineAdded extends DiffChunkLineChanged 29 | { 30 | /** 31 | * Class constructor 32 | * 33 | * @param int $number line number 34 | * @param string $content the content 35 | */ 36 | public function __construct(int $number, string $content) 37 | { 38 | $this->setNumber($number); 39 | $this->setContent($content); 40 | $this->setType(self::ADDED); 41 | } 42 | 43 | /** 44 | * Get destination line number 45 | * 46 | * @return int 47 | */ 48 | public function getOriginNumber(): ?int 49 | { 50 | return null; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/GitElephant/Objects/Diff/DiffChunkLineChanged.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | abstract class DiffChunkLineChanged extends DiffChunkLine 29 | { 30 | /** 31 | * Line number 32 | * 33 | * @var int 34 | */ 35 | protected $number; 36 | 37 | /** 38 | * Set line number 39 | * 40 | * @param int $number line number 41 | */ 42 | public function setNumber(int $number): void 43 | { 44 | $this->number = $number; 45 | } 46 | 47 | /** 48 | * Get line number 49 | * 50 | * @return int 51 | */ 52 | public function getNumber(): ?int 53 | { 54 | return $this->number; 55 | } 56 | 57 | /** 58 | * Get origin line number 59 | * 60 | * @return int 61 | */ 62 | public function getOriginNumber(): ?int 63 | { 64 | return $this->getNumber(); 65 | } 66 | 67 | /** 68 | * Get destination line number 69 | * 70 | * @return int 71 | */ 72 | public function getDestNumber(): ?int 73 | { 74 | return $this->getNumber(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/GitElephant/Objects/Diff/DiffChunkLineDeleted.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | class DiffChunkLineDeleted extends DiffChunkLineChanged 29 | { 30 | /** 31 | * Class constructor 32 | * 33 | * @param int $number line number 34 | * @param string $content line content 35 | */ 36 | public function __construct(int $number, string $content) 37 | { 38 | $this->setNumber($number); 39 | $this->setContent($content); 40 | $this->setType(self::DELETED); 41 | } 42 | 43 | /** 44 | * Get destination line number 45 | * 46 | * @return int 47 | */ 48 | public function getDestNumber(): ?int 49 | { 50 | return null; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/GitElephant/Objects/Diff/DiffChunkLineUnchanged.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | class DiffChunkLineUnchanged extends DiffChunkLine 29 | { 30 | /** 31 | * Origin line number 32 | * 33 | * @var int 34 | */ 35 | protected $originNumber; 36 | 37 | /** 38 | * Destination line number 39 | * 40 | * @var int 41 | */ 42 | protected $destNumber; 43 | 44 | /** 45 | * Class constructor 46 | * 47 | * @param int $originNumber original line number 48 | * @param int $destinationNumber destination line number 49 | * @param string $content line content 50 | * 51 | * @internal param int $number line number 52 | */ 53 | public function __construct(int $originNumber, int $destinationNumber, string $content) 54 | { 55 | $this->setOriginNumber($originNumber); 56 | $this->setDestNumber($destinationNumber); 57 | $this->setContent($content); 58 | $this->setType(self::UNCHANGED); 59 | } 60 | 61 | /** 62 | * Set origin line number 63 | * 64 | * @param int $number line number 65 | */ 66 | public function setOriginNumber(int $number): void 67 | { 68 | $this->originNumber = $number; 69 | } 70 | 71 | /** 72 | * Get origin line number 73 | * 74 | * @return int 75 | */ 76 | public function getOriginNumber(): ?int 77 | { 78 | return $this->originNumber; 79 | } 80 | 81 | /** 82 | * Set destination line number 83 | * 84 | * @param int $number line number 85 | */ 86 | public function setDestNumber(int $number): void 87 | { 88 | $this->destNumber = $number; 89 | } 90 | 91 | /** 92 | * Get destination line number 93 | * 94 | * @return int 95 | */ 96 | public function getDestNumber(): ?int 97 | { 98 | return $this->destNumber; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/GitElephant/Objects/Log.php: -------------------------------------------------------------------------------- 1 | 31 | * @author Dhaval Patel 32 | */ 33 | class Log implements \ArrayAccess, \Countable, \Iterator 34 | { 35 | /** 36 | * @var \GitElephant\Repository 37 | */ 38 | private $repository; 39 | 40 | /** 41 | * the commits related to this log 42 | * 43 | * @var array 44 | */ 45 | private $commits = []; 46 | 47 | /** 48 | * the cursor position 49 | * 50 | * @var int 51 | */ 52 | private $position = 0; 53 | 54 | /** 55 | * static method to generate standalone log 56 | * 57 | * @param \GitElephant\Repository $repository repo 58 | * @param array $outputLines output lines from command.log 59 | * 60 | * @return \GitElephant\Objects\Log 61 | */ 62 | public static function createFromOutputLines(Repository $repository, array $outputLines): \GitElephant\Objects\Log 63 | { 64 | $log = new self($repository); 65 | $log->parseOutputLines($outputLines); 66 | 67 | return $log; 68 | } 69 | 70 | /** 71 | * Class constructor 72 | * 73 | * @param Repository $repository 74 | * @param string $ref 75 | * @param string|null $path 76 | * @param int $limit 77 | * @param int|null $offset 78 | * @param bool $firstParent 79 | */ 80 | public function __construct( 81 | Repository $repository, 82 | $ref = 'HEAD', 83 | $path = null, 84 | int $limit = 15, 85 | ?int $offset = null, 86 | bool $firstParent = false 87 | ) { 88 | $this->repository = $repository; 89 | $this->createFromCommand($ref, $path, $limit, $offset, $firstParent); 90 | } 91 | 92 | /** 93 | * get the commit properties from command 94 | * 95 | * @param string $ref treeish reference 96 | * @param string $path path 97 | * @param int $limit limit 98 | * @param int $offset offset 99 | * @param boolean $firstParent first parent 100 | * 101 | * @throws \RuntimeException 102 | * @throws \Symfony\Component\Process\Exception\LogicException 103 | * @throws \Symfony\Component\Process\Exception\InvalidArgumentException 104 | * @throws \Symfony\Component\Process\Exception\RuntimeException 105 | * @see ShowCommand::commitInfo 106 | */ 107 | private function createFromCommand( 108 | $ref, 109 | $path = null, 110 | ?int $limit = null, 111 | ?int $offset = null, 112 | bool $firstParent = false 113 | ): void { 114 | $command = LogCommand::getInstance($this->getRepository()) 115 | ->showLog($ref, $path, $limit, $offset, $firstParent); 116 | 117 | $outputLines = $this->getRepository() 118 | ->getCaller() 119 | ->execute($command) 120 | ->getOutputLines(true); 121 | 122 | $this->parseOutputLines($outputLines); 123 | } 124 | 125 | private function parseOutputLines(array $outputLines): void 126 | { 127 | $this->commits = []; 128 | $commits = Utilities::pregSplitFlatArray($outputLines, '/^commit (\w+)$/'); 129 | 130 | foreach ($commits as $commitOutputLines) { 131 | $this->commits[] = Commit::createFromOutputLines($this->getRepository(), $commitOutputLines); 132 | } 133 | } 134 | 135 | /** 136 | * Get array representation 137 | * 138 | * @return array 139 | */ 140 | public function toArray(): array 141 | { 142 | return $this->commits; 143 | } 144 | 145 | /** 146 | * Get the first commit 147 | * 148 | * @return Commit|null 149 | */ 150 | public function first(): ?\GitElephant\Objects\Commit 151 | { 152 | return $this->offsetGet(0); 153 | } 154 | 155 | /** 156 | * Get the last commit 157 | * 158 | * @return Commit|null 159 | */ 160 | public function last(): ?\GitElephant\Objects\Commit 161 | { 162 | return $this->offsetGet($this->count() - 1); 163 | } 164 | 165 | /** 166 | * Get commit at index 167 | * 168 | * @param int $index the commit index 169 | * 170 | * @return Commit|null 171 | */ 172 | public function index(int $index): ?\GitElephant\Objects\Commit 173 | { 174 | return $this->offsetGet($index); 175 | } 176 | 177 | /** 178 | * ArrayAccess interface 179 | * 180 | * @param int $offset offset 181 | * 182 | * @return bool 183 | */ 184 | public function offsetExists($offset): bool 185 | { 186 | return isset($this->commits[$offset]); 187 | } 188 | 189 | /** 190 | * ArrayAccess interface 191 | * 192 | * @param int $offset offset 193 | * 194 | * @return Commit|null 195 | */ 196 | public function offsetGet($offset): ?\GitElephant\Objects\Commit 197 | { 198 | return isset($this->commits[$offset]) ? $this->commits[$offset] : null; 199 | } 200 | 201 | /** 202 | * ArrayAccess interface 203 | * 204 | * @param int $offset offset 205 | * @param mixed $value value 206 | * 207 | * @return void 208 | * @throws \RuntimeException 209 | */ 210 | public function offsetSet($offset, $value): void 211 | { 212 | throw new \RuntimeException('Can\'t set elements on logs'); 213 | } 214 | 215 | /** 216 | * ArrayAccess interface 217 | * 218 | * @param int $offset offset 219 | * 220 | * @return void 221 | * @throws \RuntimeException 222 | */ 223 | public function offsetUnset($offset): void 224 | { 225 | throw new \RuntimeException('Can\'t unset elements on logs'); 226 | } 227 | 228 | /** 229 | * Countable interface 230 | * 231 | * @return int 232 | */ 233 | public function count(): int 234 | { 235 | return count($this->commits); 236 | } 237 | 238 | /** 239 | * Iterator interface 240 | * 241 | * @return Commit|null 242 | */ 243 | public function current(): ?\GitElephant\Objects\Commit 244 | { 245 | return $this->offsetGet($this->position); 246 | } 247 | 248 | /** 249 | * Iterator interface 250 | */ 251 | public function next(): void 252 | { 253 | ++$this->position; 254 | } 255 | 256 | /** 257 | * Iterator interface 258 | * 259 | * @return int 260 | */ 261 | public function key(): int 262 | { 263 | return $this->position; 264 | } 265 | 266 | /** 267 | * Iterator interface 268 | * 269 | * @return bool 270 | */ 271 | public function valid(): bool 272 | { 273 | return $this->offsetExists($this->position); 274 | } 275 | 276 | /** 277 | * Iterator interface 278 | */ 279 | public function rewind(): void 280 | { 281 | $this->position = 0; 282 | } 283 | 284 | /** 285 | * Repository setter 286 | * 287 | * @param \GitElephant\Repository $repository the repository variable 288 | */ 289 | public function setRepository(Repository $repository): void 290 | { 291 | $this->repository = $repository; 292 | } 293 | 294 | /** 295 | * Repository getter 296 | * 297 | * @return \GitElephant\Repository 298 | */ 299 | public function getRepository(): \GitElephant\Repository 300 | { 301 | return $this->repository; 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/GitElephant/Objects/LogRange.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | * 11 | * @package GitElephant\Objects 12 | * 13 | * Just for fun... 14 | */ 15 | 16 | namespace GitElephant\Objects; 17 | 18 | use GitElephant\Command\LogRangeCommand; 19 | use GitElephant\Repository; 20 | 21 | /** 22 | * Git range log abstraction object 23 | * 24 | * @author Matteo Giachino 25 | * @author John Cartwright 26 | * @author Dhaval Patel 27 | */ 28 | class LogRange implements \ArrayAccess, \Countable, \Iterator 29 | { 30 | /** 31 | * @var \GitElephant\Repository 32 | */ 33 | private $repository; 34 | 35 | /** 36 | * the commits related to this log 37 | * 38 | * @var array 39 | */ 40 | private $rangeCommits = []; 41 | 42 | /** 43 | * the cursor position 44 | * 45 | * @var int 46 | */ 47 | private $position = 0; 48 | 49 | /** 50 | * Class constructor 51 | * 52 | * @param \GitElephant\Repository $repository repo 53 | * @param string $refStart starting reference (excluded from the range) 54 | * @param string $refEnd ending reference 55 | * @param string|null $path path 56 | * @param int $limit limit 57 | * @param int $offset offset 58 | * @param boolean $firstParent first parent 59 | * 60 | * @throws \RuntimeException 61 | * @throws \Symfony\Component\Process\Exception\RuntimeException 62 | */ 63 | public function __construct( 64 | Repository $repository, 65 | $refStart, 66 | $refEnd, 67 | $path = null, 68 | int $limit = 15, 69 | int $offset = 0, 70 | bool $firstParent = false 71 | ) { 72 | $this->repository = $repository; 73 | $this->createFromCommand($refStart, $refEnd, $path, $limit, $offset, $firstParent); 74 | } 75 | 76 | /** 77 | * get the commit properties from command 78 | * 79 | * @param string $refStart treeish reference 80 | * @param string $refEnd treeish reference 81 | * @param string $path path 82 | * @param int $limit limit 83 | * @param int $offset offset 84 | * @param boolean $firstParent first parent 85 | * 86 | * @throws \RuntimeException 87 | * @throws \Symfony\Component\Process\Exception\LogicException 88 | * @throws \Symfony\Component\Process\Exception\InvalidArgumentException 89 | * @throws \Symfony\Component\Process\Exception\RuntimeException 90 | * @see ShowCommand::commitInfo 91 | */ 92 | private function createFromCommand( 93 | $refStart, 94 | $refEnd, 95 | $path = null, 96 | int $limit = 15, 97 | int $offset = 0, 98 | bool $firstParent = false 99 | ): void { 100 | $command = LogRangeCommand::getInstance($this->getRepository())->showLog( 101 | $refStart, 102 | $refEnd, 103 | $path, 104 | $limit, 105 | $offset, 106 | $firstParent 107 | ); 108 | 109 | $outputLines = $this->getRepository() 110 | ->getCaller() 111 | ->execute($command, true, $this->getRepository()->getPath()) 112 | ->getOutputLines(true); 113 | 114 | $this->parseOutputLines($outputLines); 115 | } 116 | 117 | private function parseOutputLines(array $outputLines): void 118 | { 119 | $commitLines = null; 120 | $this->rangeCommits = []; 121 | foreach ($outputLines as $line) { 122 | if (preg_match('/^commit (\w+)$/', $line) > 0) { 123 | if (null !== $commitLines) { 124 | $this->rangeCommits[] = Commit::createFromOutputLines($this->getRepository(), $commitLines); 125 | } 126 | $commitLines = []; 127 | } 128 | $commitLines[] = $line; 129 | } 130 | 131 | if (is_array($commitLines) && count($commitLines) !== 0) { 132 | $this->rangeCommits[] = Commit::createFromOutputLines($this->getRepository(), $commitLines); 133 | } 134 | } 135 | 136 | /** 137 | * Get array representation 138 | * 139 | * @return array 140 | */ 141 | public function toArray(): array 142 | { 143 | return $this->rangeCommits; 144 | } 145 | 146 | /** 147 | * Get the first commit 148 | * 149 | * @return Commit|null 150 | */ 151 | public function first(): ?\GitElephant\Objects\Commit 152 | { 153 | return $this->offsetGet(0); 154 | } 155 | 156 | /** 157 | * Get the last commit 158 | * 159 | * @return Commit|null 160 | */ 161 | public function last(): ?\GitElephant\Objects\Commit 162 | { 163 | return $this->offsetGet($this->count() - 1); 164 | } 165 | 166 | /** 167 | * Get commit at index 168 | * 169 | * @param int $index the commit index 170 | * 171 | * @return Commit|null 172 | */ 173 | public function index(int $index): ?\GitElephant\Objects\Commit 174 | { 175 | return $this->offsetGet($index); 176 | } 177 | 178 | /** 179 | * ArrayAccess interface 180 | * 181 | * @param int $offset offset 182 | * 183 | * @return bool 184 | */ 185 | public function offsetExists($offset): bool 186 | { 187 | return isset($this->rangeCommits[$offset]); 188 | } 189 | 190 | /** 191 | * ArrayAccess interface 192 | * 193 | * @param int $offset offset 194 | * 195 | * @return Commit|null 196 | */ 197 | public function offsetGet($offset): ?\GitElephant\Objects\Commit 198 | { 199 | return isset($this->rangeCommits[$offset]) ? $this->rangeCommits[$offset] : null; 200 | } 201 | 202 | /** 203 | * ArrayAccess interface 204 | * 205 | * @param int $offset offset 206 | * @param mixed $value value 207 | * 208 | * @return void 209 | * @throws \RuntimeException 210 | */ 211 | public function offsetSet($offset, $value): void 212 | { 213 | throw new \RuntimeException('Can\'t set elements on logs'); 214 | } 215 | 216 | /** 217 | * ArrayAccess interface 218 | * 219 | * @param int $offset offset 220 | * 221 | * @return void 222 | * @throws \RuntimeException 223 | */ 224 | public function offsetUnset($offset): void 225 | { 226 | throw new \RuntimeException('Can\'t unset elements on logs'); 227 | } 228 | 229 | /** 230 | * Countable interface 231 | * 232 | * @return int 233 | */ 234 | public function count(): int 235 | { 236 | return count($this->rangeCommits); 237 | } 238 | 239 | /** 240 | * Iterator interface 241 | * 242 | * @return Commit|null 243 | */ 244 | public function current(): ?\GitElephant\Objects\Commit 245 | { 246 | return $this->offsetGet($this->position); 247 | } 248 | 249 | /** 250 | * Iterator interface 251 | */ 252 | public function next(): void 253 | { 254 | ++$this->position; 255 | } 256 | 257 | /** 258 | * Iterator interface 259 | * 260 | * @return int 261 | */ 262 | public function key(): int 263 | { 264 | return $this->position; 265 | } 266 | 267 | /** 268 | * Iterator interface 269 | * 270 | * @return bool 271 | */ 272 | public function valid(): bool 273 | { 274 | return $this->offsetExists($this->position); 275 | } 276 | 277 | /** 278 | * Iterator interface 279 | */ 280 | public function rewind(): void 281 | { 282 | $this->position = 0; 283 | } 284 | 285 | /** 286 | * Repository setter 287 | * 288 | * @param \GitElephant\Repository $repository the repository variable 289 | */ 290 | public function setRepository(Repository $repository): void 291 | { 292 | $this->repository = $repository; 293 | } 294 | 295 | /** 296 | * Repository getter 297 | * 298 | * @return \GitElephant\Repository 299 | */ 300 | public function getRepository(): \GitElephant\Repository 301 | { 302 | return $this->repository; 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /src/GitElephant/Objects/Tag.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | class Tag extends NodeObject 34 | { 35 | /** 36 | * tag name 37 | * 38 | * @var string 39 | */ 40 | private $name; 41 | 42 | /** 43 | * full reference 44 | * 45 | * @var string 46 | */ 47 | private $fullRef; 48 | 49 | /** 50 | * sha 51 | * 52 | * @var string 53 | */ 54 | private $sha; 55 | 56 | /** 57 | * Creates a new tag on the repository and returns it 58 | * 59 | * @param \GitElephant\Repository $repository repository instance 60 | * @param string $name branch name 61 | * @param string $startPoint branch to start from 62 | * @param string $message tag message 63 | * 64 | * @throws \RuntimeException 65 | * @return \GitElephant\Objects\Tag 66 | */ 67 | public static function create( 68 | Repository $repository, 69 | string $name, 70 | $startPoint = null, 71 | ?string $message = null 72 | ): ?\GitElephant\Objects\Tag { 73 | $repository 74 | ->getCaller() 75 | ->execute(TagCommand::getInstance($repository)->create($name, $startPoint, $message)); 76 | 77 | return $repository->getTag($name); 78 | } 79 | 80 | /** 81 | * static generator to generate a single commit from output of command.show service 82 | * 83 | * @param \GitElephant\Repository $repository repository 84 | * @param array $outputLines output lines 85 | * @param string $name name 86 | * 87 | * @throws \RuntimeException 88 | * @throws \InvalidArgumentException 89 | * @throws \Symfony\Component\Process\Exception\RuntimeException 90 | * @return Tag 91 | */ 92 | public static function createFromOutputLines( 93 | Repository $repository, 94 | array $outputLines, 95 | string $name 96 | ): \GitElephant\Objects\Tag { 97 | $tag = new self($repository, $name); 98 | $tag->parseOutputLines($outputLines); 99 | 100 | return $tag; 101 | } 102 | 103 | /** 104 | * Class constructor 105 | * 106 | * @param \GitElephant\Repository $repository repository instance 107 | * @param string $name name 108 | * 109 | * @throws \RuntimeException 110 | * @throws \InvalidArgumentException 111 | * @internal param string $line a single tag line from the git binary 112 | */ 113 | public function __construct(Repository $repository, string $name) 114 | { 115 | $this->repository = $repository; 116 | $this->name = $name; 117 | $this->fullRef = 'refs/tags/' . $this->name; 118 | $this->createFromCommand(); 119 | } 120 | 121 | /** 122 | * factory method 123 | * 124 | * @param \GitElephant\Repository $repository repository instance 125 | * @param string $name name 126 | * 127 | * @return \GitElephant\Objects\Tag 128 | */ 129 | public static function pick(Repository $repository, string $name): \GitElephant\Objects\Tag 130 | { 131 | return new self($repository, $name); 132 | } 133 | 134 | /** 135 | * deletes the tag 136 | */ 137 | public function delete(): void 138 | { 139 | $this->repository 140 | ->getCaller() 141 | ->execute(TagCommand::getInstance($this->getRepository())->delete($this)); 142 | } 143 | 144 | /** 145 | * get the commit properties from command 146 | * 147 | * @see ShowCommand::commitInfo 148 | */ 149 | private function createFromCommand(): void 150 | { 151 | $command = TagCommand::getInstance($this->getRepository())->listTags(); 152 | $outputLines = $this->getCaller()->execute($command, true, $this->getRepository()->getPath())->getOutputLines(); 153 | $this->parseOutputLines($outputLines); 154 | } 155 | 156 | /** 157 | * parse the output of a git command showing a commit 158 | * 159 | * @param array $outputLines output lines 160 | * 161 | * @throws \RuntimeException 162 | * @throws \Symfony\Component\Process\Exception\InvalidArgumentException 163 | * @throws \Symfony\Component\Process\Exception\LogicException 164 | * @throws \InvalidArgumentException 165 | * @throws \Symfony\Component\Process\Exception\RuntimeException 166 | * @return void 167 | */ 168 | private function parseOutputLines(array $outputLines) 169 | { 170 | $found = false; 171 | foreach ($outputLines as $tagString) { 172 | if ($tagString != '' and $this->name === trim($tagString)) { 173 | $lines = $this->getCaller() 174 | ->execute(RevListCommand::getInstance($this->getRepository())->getTagCommit($this)) 175 | ->getOutputLines(); 176 | $this->setSha($lines[0]); 177 | $found = true; 178 | break; 179 | } 180 | } 181 | 182 | if (!$found) { 183 | throw new \InvalidArgumentException(sprintf('the tag %s doesn\'t exists', $this->name)); 184 | } 185 | } 186 | 187 | /** 188 | * toString magic method 189 | * 190 | * @return string the sha 191 | */ 192 | public function __toString(): string 193 | { 194 | return $this->getSha(); 195 | } 196 | 197 | /** 198 | * @return CallerInterface 199 | */ 200 | private function getCaller(): CallerInterface 201 | { 202 | return $this->getRepository()->getCaller(); 203 | } 204 | 205 | /** 206 | * name getter 207 | * 208 | * @return string 209 | */ 210 | public function getName(): string 211 | { 212 | return $this->name; 213 | } 214 | 215 | /** 216 | * fullRef getter 217 | * 218 | * @return string 219 | */ 220 | public function getFullRef(): string 221 | { 222 | return $this->fullRef; 223 | } 224 | 225 | /** 226 | * sha setter 227 | * 228 | * @param string $sha sha 229 | */ 230 | public function setSha(string $sha): void 231 | { 232 | $this->sha = $sha; 233 | } 234 | 235 | /** 236 | * sha getter 237 | * 238 | * @return string 239 | */ 240 | public function getSha(): string 241 | { 242 | return $this->sha; 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/GitElephant/Objects/TreeObject.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | interface TreeishInterface 29 | { 30 | /** 31 | * get the unique sha for the treeish object 32 | * 33 | * @abstract 34 | */ 35 | public function getSha(): ?string; 36 | 37 | /** 38 | * toString magic method, should return the sha of the treeish 39 | * 40 | * @abstract 41 | */ 42 | public function __toString(): string; 43 | } 44 | -------------------------------------------------------------------------------- /src/GitElephant/Sequence/AbstractCollection.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | namespace GitElephant\Sequence; 20 | 21 | use PhpOption\LazyOption; 22 | use PhpOption\None; 23 | use PhpOption\Some; 24 | 25 | abstract class AbstractCollection implements \IteratorAggregate 26 | { 27 | /** 28 | * @param mixed $searchedElem 29 | * 30 | * @return bool 31 | */ 32 | public function contains($searchedElem): bool 33 | { 34 | foreach ($this as $elem) { 35 | if ($elem === $searchedElem) { 36 | return true; 37 | } 38 | } 39 | 40 | return false; 41 | } 42 | 43 | /** 44 | * @param callable $callable 45 | * 46 | * @return \PhpOption\LazyOption 47 | */ 48 | public function find(callable $callable): LazyOption 49 | { 50 | $self = $this; 51 | 52 | return new LazyOption(function () use ($callable, $self) { 53 | foreach ($self as $elem) { 54 | if ($callable($elem) === true) { 55 | return new Some($elem); 56 | } 57 | } 58 | 59 | return None::create(); 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/GitElephant/Sequence/CollectionInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | namespace GitElephant\Sequence; 20 | 21 | /** 22 | * Basic interface which adds some behaviors, and a few methods common to all 23 | * collections. 24 | * 25 | * @author Johannes M. Schmitt 26 | */ 27 | interface CollectionInterface extends \Traversable, \Countable 28 | { 29 | /** 30 | * Returns whether this collection contains the passed element. 31 | * 32 | * @param mixed $elem 33 | * 34 | * @return boolean 35 | */ 36 | public function contains($elem): bool; 37 | 38 | /** 39 | * Returns whether the collection is empty. 40 | * 41 | * @return boolean 42 | */ 43 | public function isEmpty(): bool; 44 | 45 | /** 46 | * Returns a filtered collection of the same type. 47 | * 48 | * Removes all elements for which the provided callable returns false. 49 | * 50 | * @param callable $callable receives an element of the collection and must 51 | * return true (= keep) or false (= remove). 52 | * 53 | * @return CollectionInterface 54 | */ 55 | public function filter(callable $callable): CollectionInterface; 56 | 57 | /** 58 | * Returns a filtered collection of the same type. 59 | * 60 | * Removes all elements for which the provided callable returns true. 61 | * 62 | * @param callable $callable receives an element of the collection and must 63 | * return true (= remove) or false (= keep). 64 | * 65 | * @return CollectionInterface 66 | */ 67 | public function filterNot(callable $callable): CollectionInterface; 68 | 69 | /** 70 | * Applies the callable to an initial value and each element, going left to 71 | * right. 72 | * 73 | * @param mixed $initialValue 74 | * @param callable $callable receives the current value (the first time this 75 | * equals $initialValue) and the element 76 | * 77 | * @return mixed the last value returned by $callable, or $initialValue if 78 | * collection is empty. 79 | */ 80 | public function foldLeft($initialValue, callable $callable); 81 | 82 | /** 83 | * Applies the callable to each element, and an initial value, going right to 84 | * left. 85 | * 86 | * @param mixed $initialValue 87 | * @param callable $callable receives the element, and the current value (the 88 | * first time this equals $initialValue). 89 | * 90 | * @return mixed the last value returned by $callable, or $initialValue if 91 | * collection is empty. 92 | */ 93 | public function foldRight($initialValue, callable $callable); 94 | } 95 | -------------------------------------------------------------------------------- /src/GitElephant/Sequence/Sequence.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | namespace GitElephant\Sequence; 20 | 21 | /** 22 | * Unsorted sequence implementation. 23 | * 24 | * Characteristics: 25 | * 26 | * - Keys: consequentially numbered, without gaps 27 | * - Values: anything, duplicates allowed 28 | * - Ordering: same as input unless when explicitly sorted 29 | * 30 | * @author Johannes M. Schmitt 31 | */ 32 | class Sequence extends AbstractSequence implements SortableInterface 33 | { 34 | public function sortWith(callable $callable): void 35 | { 36 | usort($this->elements, $callable); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/GitElephant/Sequence/SequenceInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | namespace GitElephant\Sequence; 20 | 21 | use PhpOption\Option; 22 | 23 | /** 24 | * Interface for mutable sequences. 25 | * 26 | * Equality of elements in the sequence is established via a shallow comparison 27 | * (===). 28 | * 29 | * @author Johannes M. Schmitt 30 | */ 31 | interface SequenceInterface extends CollectionInterface 32 | { 33 | /** 34 | * Returns the first element in the collection if available. 35 | * 36 | * @return Option 37 | */ 38 | public function first(): Option; 39 | 40 | /** 41 | * Returns the last element in the collection if available. 42 | * 43 | * @return Option 44 | */ 45 | public function last(): Option; 46 | 47 | /** 48 | * Returns all elements in this sequence. 49 | * 50 | * @return array 51 | */ 52 | public function all(): array; 53 | 54 | /** 55 | * Returns a new Sequence with all elements in reverse order. 56 | * 57 | * @return SequenceInterface 58 | */ 59 | public function reverse(): SequenceInterface; 60 | 61 | /** 62 | * Adds the elements of another sequence to this sequence. 63 | * 64 | * @param SequenceInterface $seq 65 | * 66 | * @return SequenceInterface 67 | */ 68 | public function addSequence(SequenceInterface $seq): SequenceInterface; 69 | 70 | /** 71 | * Returns the index of the passed element. 72 | * 73 | * @param mixed $elem 74 | * 75 | * @return integer the index (0-based), or -1 if not found 76 | */ 77 | public function indexOf($elem): int; 78 | 79 | /** 80 | * Returns the last index of the passed element. 81 | * 82 | * @param mixed $elem 83 | * 84 | * @return integer the index (0-based), or -1 if not found 85 | */ 86 | public function lastIndexOf($elem): int; 87 | 88 | /** 89 | * Returns whether the given index is defined in the sequence. 90 | * 91 | * @param integer $index (0-based) 92 | * 93 | * @return boolean 94 | */ 95 | public function isDefinedAt(int $index): bool; 96 | 97 | /** 98 | * Returns the first index where the given callable returns true. 99 | * 100 | * @param callable $callable receives the element as first argument, and 101 | * returns true, or false 102 | * 103 | * @return integer the index (0-based), or -1 if the callable returns false 104 | * for all elements 105 | */ 106 | public function indexWhere(callable $callable): int; 107 | 108 | /** 109 | * Returns the last index where the given callable returns true. 110 | * 111 | * @param callable $callable receives the element as first argument, and 112 | * returns true, or false 113 | * 114 | * @return integer the index (0-based), or -1 if the callable returns false 115 | * for all elements 116 | */ 117 | public function lastIndexWhere(callable $callable): int; 118 | 119 | /** 120 | * Returns all indices of this collection. 121 | * 122 | * @return integer[] 123 | */ 124 | public function indices(): array; 125 | 126 | /** 127 | * Returns the element at the given index. 128 | * 129 | * @param integer $index (0-based) 130 | * 131 | * @return mixed 132 | */ 133 | public function get(int $index); 134 | 135 | /** 136 | * Adds an element to the sequence. 137 | * 138 | * @param mixed $elem 139 | * 140 | * @return void 141 | */ 142 | public function add($elem): void; 143 | 144 | /** 145 | * Removes the element at the given index, and returns it. 146 | * 147 | * @param integer $index 148 | * 149 | * @return mixed 150 | */ 151 | public function remove(int $index); 152 | 153 | /** 154 | * Adds all elements to the sequence. 155 | * 156 | * @param array $elements 157 | * 158 | * @return void 159 | */ 160 | public function addAll(array $elements): void; 161 | 162 | /** 163 | * Updates the value at the given index. 164 | * 165 | * @param integer $index 166 | * @param mixed $value 167 | * 168 | * @return void 169 | */ 170 | public function update(int $index, $value): void; 171 | 172 | /** 173 | * Returns a new sequence by omitting the given number of elements from the 174 | * beginning. 175 | * 176 | * If the passed number is greater than the available number of elements, all 177 | * will be removed. 178 | * 179 | * @param integer $number 180 | * 181 | * @return SequenceInterface 182 | */ 183 | public function drop(int $number): SequenceInterface; 184 | 185 | /** 186 | * Returns a new sequence by omitting the given number of elements from the 187 | * end. 188 | * 189 | * If the passed number is greater than the available number of elements, all 190 | * will be removed. 191 | * 192 | * @param integer $number 193 | * 194 | * @return SequenceInterface 195 | */ 196 | public function dropRight(int $number): SequenceInterface; 197 | 198 | /** 199 | * Returns a new sequence by omitting elements from the beginning for as long 200 | * as the callable returns true. 201 | * 202 | * @param callable $callable Receives the element to drop as first argument, 203 | * and returns true (drop), or false (stop). 204 | * 205 | * @return SequenceInterface 206 | */ 207 | public function dropWhile(callable $callable): SequenceInterface; 208 | 209 | /** 210 | * Creates a new collection by taking the given number of elements from the 211 | * beginning of the current collection. 212 | * 213 | * If the passed number is greater than the available number of elements, 214 | * then all elements will be returned as a new collection. 215 | * 216 | * @param integer $number 217 | * 218 | * @return CollectionInterface 219 | */ 220 | public function take(int $number): CollectionInterface; 221 | 222 | /** 223 | * Creates a new collection by taking elements from the current collection 224 | * for as long as the callable returns true. 225 | * 226 | * @param callable $callable 227 | * 228 | * @return CollectionInterface 229 | */ 230 | public function takeWhile(callable $callable): CollectionInterface; 231 | 232 | /** 233 | * Creates a new collection by applying the passed callable to all elements 234 | * of the current collection. 235 | * 236 | * @param callable $callable 237 | * 238 | * @return CollectionInterface 239 | */ 240 | public function map(callable $callable): CollectionInterface; 241 | } 242 | -------------------------------------------------------------------------------- /src/GitElephant/Sequence/SortableInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | namespace GitElephant\Sequence; 20 | 21 | /** 22 | * Interface for sortable collections. 23 | * 24 | * @author Johannes M. Schmitt 25 | */ 26 | interface SortableInterface 27 | { 28 | public function sortWith(callable $callable): void; 29 | } 30 | -------------------------------------------------------------------------------- /src/GitElephant/Status/Status.php: -------------------------------------------------------------------------------- 1 | 41 | */ 42 | protected $files; 43 | 44 | /** 45 | * Private constructor in order to follow the singleton pattern 46 | * 47 | * @param Repository $repository 48 | * 49 | * @throws \RuntimeException 50 | * @throws \Symfony\Component\Process\Exception\RuntimeException 51 | */ 52 | private function __construct(Repository $repository) 53 | { 54 | $this->files = []; 55 | $this->repository = $repository; 56 | $this->createFromCommand(); 57 | } 58 | 59 | /** 60 | * @param Repository $repository 61 | * 62 | * @return \GitElephant\Status\Status 63 | */ 64 | public static function get(Repository $repository) 65 | { 66 | return new static($repository); 67 | } 68 | 69 | /** 70 | * create from git command 71 | */ 72 | private function createFromCommand(): void 73 | { 74 | $command = MainCommand::getInstance($this->repository)->status(true); 75 | $lines = $this->repository->getCaller()->execute($command)->getOutputLines(true); 76 | $this->parseOutputLines($lines); 77 | } 78 | 79 | /** 80 | * all files 81 | * 82 | * @return Sequence 83 | */ 84 | public function all(): \GitElephant\Sequence\Sequence 85 | { 86 | return new Sequence($this->files); 87 | } 88 | 89 | /** 90 | * untracked files 91 | * 92 | * @return Sequence 93 | */ 94 | public function untracked(): \GitElephant\Sequence\Sequence 95 | { 96 | return $this->filterByType(StatusFile::UNTRACKED); 97 | } 98 | 99 | /** 100 | * modified files 101 | * 102 | * @return Sequence 103 | */ 104 | public function modified(): \GitElephant\Sequence\Sequence 105 | { 106 | return $this->filterByType(StatusFile::MODIFIED); 107 | } 108 | 109 | /** 110 | * added files 111 | * 112 | * @return Sequence 113 | */ 114 | public function added(): \GitElephant\Sequence\Sequence 115 | { 116 | return $this->filterByType(StatusFile::ADDED); 117 | } 118 | 119 | /** 120 | * deleted files 121 | * 122 | * @return Sequence 123 | */ 124 | public function deleted(): \GitElephant\Sequence\Sequence 125 | { 126 | return $this->filterByType(StatusFile::DELETED); 127 | } 128 | 129 | /** 130 | * renamed files 131 | * 132 | * @return Sequence 133 | */ 134 | public function renamed(): \GitElephant\Sequence\Sequence 135 | { 136 | return $this->filterByType(StatusFile::RENAMED); 137 | } 138 | 139 | /** 140 | * copied files 141 | * 142 | * @return Sequence 143 | */ 144 | public function copied(): \GitElephant\Sequence\Sequence 145 | { 146 | return $this->filterByType(StatusFile::COPIED); 147 | } 148 | 149 | /** 150 | * create objects from command output 151 | * https://www.kernel.org/pub/software/scm/git/docs/git-status.html in the output section 152 | * 153 | * 154 | * @param array $lines 155 | */ 156 | private function parseOutputLines(array $lines): void 157 | { 158 | foreach ($lines as $line) { 159 | $matches = $this->splitStatusLine($line); 160 | if ($matches) { 161 | $x = isset($matches[1]) ? $matches[1] : null; 162 | $y = isset($matches[2]) ? $matches[2] : null; 163 | $file = isset($matches[3]) ? $matches[3] : null; 164 | $renamedFile = isset($matches[5]) ? $matches[5] : null; 165 | $this->files[] = StatusFile::create($x, $y, $file, $renamedFile); 166 | } 167 | } 168 | } 169 | 170 | /** 171 | * @param string $line 172 | * 173 | * @return array|null 174 | */ 175 | protected function splitStatusLine(string $line) 176 | { 177 | preg_match('/^([MADRCU\? ])?([MADRCU\? ])?\ "?([^"]+?)"?( -> "?([^"]+?)"?)?$/', $line, $matches); 178 | return $matches; 179 | } 180 | 181 | /** 182 | * filter files status in working tree and in index status 183 | * 184 | * @param string $type 185 | * 186 | * @return Sequence 187 | */ 188 | protected function filterByType(string $type): \GitElephant\Sequence\Sequence 189 | { 190 | if (!$this->files) { 191 | return new Sequence(); 192 | } 193 | 194 | return new Sequence( 195 | array_filter( 196 | $this->files, 197 | function (StatusFile $statusFile) use ($type) { 198 | return $type === $statusFile->getWorkingTreeStatus() || $type === $statusFile->getIndexStatus(); 199 | } 200 | ) 201 | ); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/GitElephant/Status/StatusFile.php: -------------------------------------------------------------------------------- 1 | x = ' ' === $x ? null : $x; 79 | $this->y = ' ' === $y ? null : $y; 80 | $this->name = $name; 81 | $this->renamed = $renamed; 82 | } 83 | 84 | /** 85 | * @param string $x X section of the status --porcelain output 86 | * @param string $y Y section of the status --porcelain output 87 | * @param string $name file name 88 | * @param string $renamed new file name (if renamed) 89 | * 90 | * @return StatusFile 91 | */ 92 | public static function create( 93 | string $x, 94 | string $y, 95 | string $name, 96 | ?string $renamed = null 97 | ): \GitElephant\Status\StatusFile { 98 | return new self($x, $y, $name, $renamed); 99 | } 100 | 101 | /** 102 | * @return bool 103 | */ 104 | public function isRenamed(): bool 105 | { 106 | return $this->renamed !== null; 107 | } 108 | 109 | /** 110 | * Get the file name 111 | * 112 | * @return string 113 | */ 114 | public function getName(): string 115 | { 116 | return $this->name; 117 | } 118 | 119 | /** 120 | * Get the renamed 121 | * 122 | * @return string|null 123 | */ 124 | public function getRenamed(): ?string 125 | { 126 | return $this->renamed; 127 | } 128 | 129 | /** 130 | * Get the status of the index 131 | * 132 | * @return string 133 | */ 134 | public function getIndexStatus(): ?string 135 | { 136 | return $this->x; 137 | } 138 | 139 | /** 140 | * Get the status of the working tree 141 | * 142 | * @return string|null 143 | */ 144 | public function getWorkingTreeStatus(): ?string 145 | { 146 | return $this->y; 147 | } 148 | 149 | /** 150 | * description of the status 151 | * 152 | * @return void 153 | */ 154 | public function calculateDescription(): void 155 | { 156 | $status = $this->x . $this->y; 157 | $matching = [ 158 | '/ [MD]/' => 'not updated', 159 | '/M[MD]/' => 'updated in index', 160 | '/A[MD]/' => 'added to index', 161 | '/D[M]/' => 'deleted from index', 162 | '/R[MD]/' => 'renamed in index', 163 | '/C[MD]/' => 'copied in index', 164 | '/[MARC] /' => 'index and work tree matches', 165 | '/[MARC]M/' => 'work tree changed since index', 166 | '/[MARC]D/' => 'deleted in work tree', 167 | '/DD/' => 'unmerged, both deleted', 168 | '/AU/' => 'unmerged, added by us', 169 | '/UD/' => 'unmerged, deleted by them', 170 | '/UA/' => 'unmerged, added by them', 171 | '/DU/' => 'unmerged, deleted by us', 172 | '/AA/' => 'unmerged, both added', 173 | '/UU/' => 'unmerged, both modified', 174 | '/\?\?/' => 'untracked', 175 | '/!!/' => 'ignored', 176 | ]; 177 | $out = []; 178 | foreach ($matching as $pattern => $label) { 179 | if (preg_match($pattern, $status)) { 180 | $out[] = $label; 181 | } 182 | } 183 | 184 | $this->description = implode(', ', $out); 185 | } 186 | 187 | /** 188 | * Set Description 189 | * 190 | * @param string $description the description variable 191 | */ 192 | public function setDescription(string $description): void 193 | { 194 | $this->description = $description; 195 | } 196 | 197 | /** 198 | * Get Description. 199 | * Note that in certain environments, git might 200 | * format the output differently, leading to the description 201 | * being an empty string. Use setDescription(string) to set it yourself. 202 | * 203 | * @see #calulcateDescription() 204 | * @see #setDescription($description) 205 | * @return string 206 | */ 207 | public function getDescription(): string 208 | { 209 | if ($this->description === null) { 210 | $this->calculateDescription(); 211 | } 212 | 213 | return $this->description; 214 | } 215 | 216 | /** 217 | * Set Type 218 | * 219 | * @param string $type the type variable 220 | */ 221 | public function setType(string $type): void 222 | { 223 | $this->type = $type; 224 | } 225 | 226 | /** 227 | * Get the Type of status/change. 228 | * Please note that this type might not be set by default. 229 | * 230 | * @return string 231 | */ 232 | public function getType(): ?string 233 | { 234 | return $this->type; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/GitElephant/Status/StatusIndex.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | public function untracked(): \GitElephant\Sequence\Sequence 36 | { 37 | return new Sequence(); 38 | } 39 | 40 | /** 41 | * all files with modified status in the index 42 | * 43 | * @return Sequence 44 | */ 45 | public function all(): \GitElephant\Sequence\Sequence 46 | { 47 | return new Sequence( 48 | array_filter( 49 | $this->files, 50 | function (StatusFile $statusFile) { 51 | return $statusFile->getIndexStatus() && '?' !== $statusFile->getIndexStatus(); 52 | } 53 | ) 54 | ); 55 | } 56 | 57 | /** 58 | * filter files by index status 59 | * 60 | * @param string $type 61 | * 62 | * @return Sequence 63 | */ 64 | protected function filterByType(string $type): \GitElephant\Sequence\Sequence 65 | { 66 | if (!$this->files) { 67 | return new Sequence(); 68 | } 69 | 70 | return new Sequence( 71 | array_filter( 72 | $this->files, 73 | function (StatusFile $statusFile) use ($type) { 74 | return $type === $statusFile->getIndexStatus(); 75 | } 76 | ) 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/GitElephant/Status/StatusWorkingTree.php: -------------------------------------------------------------------------------- 1 | files, 42 | function (StatusFile $statusFile) { 43 | $status = $statusFile->getWorkingTreeStatus(); 44 | return $status !== null && $status != ""; 45 | } 46 | ) 47 | ); 48 | } 49 | 50 | /** 51 | * filter files by working tree status 52 | * 53 | * @param string $type 54 | * 55 | * @return Sequence 56 | */ 57 | protected function filterByType(string $type): \GitElephant\Sequence\Sequence 58 | { 59 | if (!$this->files) { 60 | return new Sequence(); 61 | } 62 | 63 | return new Sequence( 64 | array_filter( 65 | $this->files, 66 | function (StatusFile $statusFile) use ($type) { 67 | return $type === $statusFile->getWorkingTreeStatus(); 68 | } 69 | ) 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/GitElephant/Utilities.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | class Utilities 29 | { 30 | /** 31 | * explode an array by lines that match a regular expression 32 | * 33 | * @param string[] $list a flat array 34 | * @param string $pattern a regular expression 35 | * 36 | * @return array> 37 | */ 38 | public static function pregSplitArray(array $list, string $pattern): array 39 | { 40 | $slices = []; 41 | $index = -1; 42 | foreach ($list as $value) { 43 | if (preg_match($pattern, $value) === 1) { 44 | ++$index; 45 | } 46 | 47 | if ($index !== -1) { 48 | $slices[$index][] = $value; 49 | } 50 | } 51 | 52 | return $slices; 53 | } 54 | 55 | /** 56 | * @param string[] $list a flat array 57 | * @param string $pattern a regular expression 58 | * 59 | * @return array> 60 | */ 61 | public static function pregSplitFlatArray(array $list, string $pattern): array 62 | { 63 | $slices = []; 64 | $index = -1; 65 | foreach ($list as $value) { 66 | if (preg_match($pattern, $value) === 1) { 67 | ++$index; 68 | } 69 | 70 | $slices[$index + 1][] = $value; 71 | } 72 | 73 | return $slices; 74 | } 75 | } 76 | --------------------------------------------------------------------------------