├── 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 |
--------------------------------------------------------------------------------