diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 4e611ce8..b691d7de 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -2,7 +2,7 @@ name: Tests
on:
push:
- branches: [ main ]
+ branches: [ main, 1.x ]
pull_request:
jobs:
@@ -12,11 +12,11 @@ jobs:
strategy:
fail-fast: false
matrix:
- php: ['8.1', '8.2', '8.3', '8.4']
+ php: ['8.1', '8.2', '8.3', '8.4', '8.5']
composer-flags: ['']
name: ['']
include:
- - php: '8.0'
+ - php: '8.1'
composer-flags: '--prefer-lowest'
name: '(prefer lowest dependencies)'
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..061e75e0
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,104 @@
+# Changelog
+
+All notable changes to this project are documented in this file.
+
+## [Unreleased]
+
+Changes since [v1.6.0](https://github.com/gitonomy/gitlib/releases/tag/v1.6.0).
+
+1.7 prepares the upgrade to 2.0: it has the API of 2.0 that can be added without breaking 1.x,
+and deprecates what 2.0 replaces or breaks. Code without deprecations with 1.7 runs with 2.0
+too, so a library can require `^1.7 || ^2.0` with a single code path. The exceptions, the
+changes of 2.0 1.7 can't bring, are listed in the `UPGRADE-2.0.md` of 2.0.
+
+1.7 breaks nothing of 1.6: the parameters keep their names, the arguments added to the methods
+of classes that aren't final are read with `func_get_arg()`, commented out in their signature
+(`@psalm-variadic` for Psalm), and gitlib keeps calling the deprecated methods that a subclass
+overrides. Deprecations are triggered through `symfony/deprecation-contracts`, a new
+dependency.
+
+1.7 requires PHP 8.1, for the `DiffLineType` enum: on PHP 8.0, Composer keeps 1.6.
+
+### Features
+
+- `Gitonomy\Git\Driver\Cli\CliDriver` configures git: the binary, the environment variables,
+ the timeout and the logger. It takes the arguments of the `CliDriver` of 2.0.
+- `new Repository($dir, $workingDir, $driver)`: the second argument may be the working
+ directory, the third one is a `CliDriver`.
+- `Repository::init()`, `initBare()`, `cloneFrom()`, `cloneBareFrom()`, `mirrorFrom()`,
+ `isValidRepository()` and `isValidRepositoryAndBranch()` take the URL before the path and a
+ `CliDriver` as last argument. Like in 2.0, `isValidRepositoryAndBranch()` checks the exact
+ name of the branch, and both read a relative path as a path, never as the name of a remote
+ of the working directory of PHP.
+- `Repository::getDiff($from, $to)`, with two arguments, compares two revisions, `Repository::getMergeBase()`
+ returns their best common ancestor, `WorkingCopy::getDiff()` compares the working tree with
+ a revision. Like in 2.0, they throw a `ReferenceNotFoundException` for an unknown revision,
+ as `WorkingCopy::checkoutNewBranch()`.
+- `Diff::parse()`, `Diff::fromArray()` and `File::fromArray()` take the repository to read the
+ blobs from, the constructor of `Diff\File` the changes and the repository.
+ `File::isModeChange()`, `File::isCopy()`, `FileChange::getAdditions()` and
+ `FileChange::getDeletions()`.
+- `FileChange::getDiffLines()` returns the lines of a hunk as `Diff\DiffLine` objects, with
+ `getType()`, a `Diff\DiffLineType`, and `getContent()`. The constructor of `FileChange`
+ takes `DiffLine` objects.
+- `Repository::getBlame()` and `Blame` take the first and the last lines to blame, as
+ integers.
+ `Blame::getHunks()` returns `Blame\Hunk` objects, numbered like the file;
+ `Blame\Line::getNumber()` and `getOriginalNumber()`.
+- `Log::withOffset()` and `withLimit()` return another log.
+- `Branch::isMergedInto()` and `Branch::moveTo()`. `ReferenceBag::createBranch()` and
+ `createTag()` take a `Revision` too.
+- `PushReference::getFullname()`, `getRange()`, `isCreation()` and `isDeletion()`.
+- Iterating a `Tree` gives its entries, `TreeEntry` objects with `getName()`, `getMode()` and
+ `getObject()`, by name; `Tree::getBlobs()`, `getTrees()` and `getSubmodules()` return those
+ of one type.
+- `Submodule`, with `getHash()`. The submodules of trees are still `CommitReference`
+ objects, which extend `Submodule`.
+- `WorkingCopy::checkoutNewBranch()`. `WorkingCopy::checkout()` takes any `Revision`.
+- `Repository::getSizeInBytes()`, which doesn't follow links.
+
+### Deprecations
+
+- An array of options as second argument of `Repository`, the `debug` option set to `false`,
+ `Repository::setLogger()` and `getLogger()`: use the `workingDir` and `driver` arguments.
+- `Admin` and `Repository::cloneTo()`: use the static methods of `Repository`.
+- `Repository::getDiff()` with one argument and `Log::getDiff()`: use
+ `Repository::getDiff($from, $to)` or `WorkingCopy::getDiff()`.
+- `Diff::setRepository()`, `File::setRepository()`, `File::addChange()`,
+ `File::isChangeMode()`, `File::isDelete()`, `File::getAnchor()` and `FileChange::getCount()`.
+- `FileChange::getLines()`, which returns the lines as arrays, the `FileChange::LINE_*`
+ constants, and passing the lines to the constructor of `FileChange` as arrays: use
+ `getDiffLines()`, `DiffLineType` and `DiffLine` objects.
+- A range of lines as a string in `Repository::getBlame()` and `Blame`,
+ `Blame::getGroupedLines()` and `Blame\Line::getLine()`.
+- `Log::setOffset()`, `setLimit()`, a negative offset or limit, an option of `git log` but
+ `--all` as revision, and `count()` of a log whose offset or limit leaves commits out: it
+ counts them all, where 2.0 counts the commits the log iterates. Use `countCommits()`.
+- `ReferenceBag::update()`, `Branch::isMergedTo()`, arguments of
+ `Commit::getIncludingBranches()`, creating a branch or a tag that exists already, deleting
+ a reference by another name than its full name under `refs/`.
+- Passing an argument to `Repository::getReferences()`, to read the references again: open
+ a new `Repository`.
+- `PushReference::getReference()`, `getRevision()`, `isCreate()` and `isDelete()`.
+- `CommitReference`. `Tree::getEntries()`, `getBlobEntries()`, `getTreeEntries()` and
+ `getCommitReferenceEntries()`, which return the entries as arrays: iterate the tree, or use
+ `getBlobs()`, `getTrees()` and `getSubmodules()`.
+- The second argument of `WorkingCopy::checkout()`: use `checkoutNewBranch()`. A local
+ `Branch` object, which `checkout()` detaches HEAD on, where 2.0 attaches HEAD to the branch:
+ pass its name, or its commit.
+- Passing a revision name to `Repository::getTree()` or `getBlob()` instead of a hash, and
+ passing data to the constructor of `Commit`, internal in 2.0.
+- `Repository::getSize()`, `Repository::shell()`, `Commit::getShortMessage()`,
+ `Commit::getFixedShortHash()` and `Util\StringHelper`.
+- The parsers that 2.0 moves to its `CliDriver`, all but `DiffParser` and `ParserBase`.
+- The classes `final` in 2.0 are `@final since gitonomy/gitlib 1.7`. The constructors
+ internal in 2.0 (`Commit`, `Tree`, `Blob`, `Revision`, `Reference`, `ReferenceBag`) say so.
+
+## [v1.6.0] - 2025-12-09
+
+_Earlier versions are not documented here. See the
+[GitHub releases](https://github.com/gitonomy/gitlib/releases) and
+[commit history](https://github.com/gitonomy/gitlib/commits/v1.6.0) for prior changes._
+
+[Unreleased]: https://github.com/gitonomy/gitlib/compare/v1.6.0...1.x
+[v1.6.0]: https://github.com/gitonomy/gitlib/releases/tag/v1.6.0
diff --git a/composer.json b/composer.json
index 6979e26f..4b9a2fca 100644
--- a/composer.json
+++ b/composer.json
@@ -35,16 +35,18 @@
}
},
"require": {
- "php": "^8.0",
+ "php": "^8.1",
"ext-pcre": "*",
+ "symfony/deprecation-contracts": "^2.5 || ^3",
"symfony/polyfill-mbstring": "^1.7",
"symfony/process": "^5.4 || ^6.0 || ^7.0 || ^8.0"
},
"require-dev": {
"ext-fileinfo": "*",
- "phpspec/prophecy-phpunit": "^2.0",
+ "phpspec/prophecy-phpunit": "^2.1",
"phpunit/phpunit": "^7.5.20 || ^8.5.20 || ^9.5.9",
- "psr/log": "^1.0"
+ "psr/log": "^1.0",
+ "symfony/phpunit-bridge": "^6.4 || ^7.0"
},
"config": {
"preferred-install": "dist",
diff --git a/doc/admin.md b/doc/admin.md
index d4776804..bd774e5a 100644
--- a/doc/admin.md
+++ b/doc/admin.md
@@ -1,24 +1,25 @@
Create and access git repositories
==================================
-gitlib provides methods to initialize new repositories.
+gitlib provides static methods of `Repository` to initialize and clone
+repositories. Each of them takes a `CliDriver` as last argument, to
+configure git (see [Repository](repository.md)).
+
+`Admin` and `Repository::cloneTo()`, which did it before 1.7, are
+deprecated.
Create a repository
-------------------
-To initialize a new repository, use method `Admin::init`.
-
```php
-// Initialize a bare repository
-$repository = Gitonomy\Git\Admin::init('/path/to/repository');
+use Gitonomy\Git\Repository;
-// Initialize a non-bare repository
-$repository = Gitonomy\Git\Admin::init('/path/to/repository', false);
-```
+// Initialize a repository with a working directory
+$repository = Repository::init('/path/to/repository');
-Default behavior is to create a bare repository. If you want to
-initialize a repository with a working copy,pass `false` as third
-argument of Repository constructor.
+// Initialize a bare repository
+$repository = Repository::initBare('/path/to/repository');
+```
Cloning repositories
--------------------
@@ -26,49 +27,48 @@ Cloning repositories
You can clone a repository from an URL by doing:
```php
-// Clone to a bare repository
-$repository = Gitonomy\Git\Admin::cloneTo('/tmp/gitlib', 'https://github.com/gitonomy/gitlib.git');
+// Clone to a repository with a working directory
+$repository = Repository::cloneFrom('https://github.com/gitonomy/gitlib.git', '/tmp/gitlib');
-// Clone to a non-bare repository
-$repository = Gitonomy\Git\Admin::cloneTo('/tmp/gitlib', 'https://github.com/gitonomy/gitlib.git', false);
+// Clone to a bare repository
+$repository = Repository::cloneBareFrom('https://github.com/gitonomy/gitlib.git', '/tmp/gitlib');
```
-Default behavior is to clone in a bare repository.
-
You can also clone a repository and point it to a specific branch. In a
-non-bare repository, this branch will be checked out:
+repository with a working directory, this branch is checked out:
```php
-// Clone to a bare repository
-$repository = Gitonomy\Git\Admin::cloneBranchTo('/tmp/gitlib', 'https://github.com/gitonomy/gitlib.git', 'a-branch');
+$repository = Repository::cloneFrom('https://github.com/gitonomy/gitlib.git', '/tmp/gitlib', 'a-branch');
-// Clone to a non-bare repository
-$repository = Gitonomy\Git\Admin::cloneBranchTo('/tmp/gitlib', 'https://github.com/gitonomy/gitlib.git', 'a-branch', false);
+$repository = Repository::cloneBareFrom('https://github.com/gitonomy/gitlib.git', '/tmp/gitlib', 'a-branch');
```
-Clone a Repository object
--------------------------
-
-If you already have a Repository instance and want to clone it, you can
-use this shortcut:
+To clone a `Repository` object, clone its git directory:
```php
-$new = $repository->cloneTo('/tmp/clone');
+$new = Repository::cloneBareFrom($repository->getGitDir(), '/tmp/clone');
```
Mirror a repository
-------------------
-If you want to mirror fully a repository and all references, use the
-`mirrorTo` method. This method takes only two arguments, where to mirror
-and what to mirror:
+To mirror a repository and all its references into a bare repository,
+use the `mirrorFrom` method:
+
+```php
+$mirror = Repository::mirrorFrom('https://github.com/gitonomy/gitlib.git', '/tmp/mirror');
+```
+
+Check a remote repository
+-------------------------
+
+To check that git can read a repository, without cloning it:
```php
-// Mirror to a bare repository
-$mirror = Gitonomy\Git\Admin::mirrorTo('/tmp/mirror', 'https://github.com/gitonomy/gitlib.git');
+Repository::isValidRepository('https://github.com/gitonomy/gitlib.git');
-// Mirror to a non-bare repository
-$mirror = Gitonomy\Git\Admin::mirrorTo('/tmp/mirror', 'https://github.com/gitonomy/gitlib.git', false);
+// And that it has the branch "main", with this exact name
+Repository::isValidRepositoryAndBranch('https://github.com/gitonomy/gitlib.git', 'main');
```
### References
diff --git a/doc/blame.md b/doc/blame.md
index 80816c9b..bba94cd8 100644
--- a/doc/blame.md
+++ b/doc/blame.md
@@ -26,6 +26,17 @@ If you want to access directly a line:
$line = $blame->getLine(32);
```
+To blame some lines only, pass the first and the last ones:
+
+```php
+$blame = $repository->getBlame('master', 'README.md', 10, 20);
+```
+
+`getLines()` and `getLine()` number these lines from 1, where gitlib 2.0
+numbers them like the file: use `Line::getNumber()`. A range of lines as
+a string (`'10,20'`, `'10,+11'`, a regular expression), passed to
+`git blame -L`, is deprecated.
+
The Line object
---------------
@@ -33,8 +44,10 @@ LineObject represents an item of the blame file. It is composed of those
informations:
```php
-$line->getCommit(); // returns a Commit
-$line->getContent(); // returns text
+$line->getCommit(); // returns a Commit
+$line->getContent(); // returns text
+$line->getNumber(); // the number of the line in the file
+$line->getOriginalNumber(); // its number in the file of the commit
// you can access author from commmit:
$author = $line->getCommit()->getAuthorName();
@@ -46,13 +59,18 @@ Group reading by commit
If you plan to display it, you'll probably need a version where lines
from same commit are grouped.
-To do so, use the *getGroupedLines* method that will return an array
-like this:
+To do so, use the *getHunks* method, which returns `Blame\Hunk` objects,
+the consecutive lines last changed by the same commit, by number in the
+file:
```php
-$blame = array(
- array(Commit, array(1 => Line, 2 => Line, 3 => Line)),
- array(Commit, array(4 => Line)),
- array(Commit, array(5 => Line, 6 => Line))
-)
+foreach ($blame->getHunks() as $hunk) {
+ echo $hunk->getCommit()->getHash().PHP_EOL;
+
+ foreach ($hunk->getLines() as $number => $line) {
+ echo $number.': '.$line->getContent().PHP_EOL;
+ }
+}
```
+
+`getGroupedLines()` and `Line::getLine()` are deprecated.
diff --git a/doc/branch.md b/doc/branch.md
index d6e0872c..188fc3a8 100644
--- a/doc/branch.md
+++ b/doc/branch.md
@@ -14,3 +14,17 @@ You can check is the branch is a local or remote one:
$branch->isLocal();
$branch->isRemote();
```
+
+To check that the history of a revision contains the branch:
+
+```php
+$branch->isMergedInto('master');
+```
+
+`isMergedTo()` is deprecated.
+
+To point the branch to another revision:
+
+```php
+$branch = $branch->moveTo('a8b7e4...');
+```
diff --git a/doc/commit.md b/doc/commit.md
index 08e9b1e3..08d546ac 100644
--- a/doc/commit.md
+++ b/doc/commit.md
@@ -31,7 +31,7 @@ For example, if you want to display all parents, starting from a commit:
```php
function displayLog(Gitonomy\Git\Commit $commit) {
- echo '- '.$commit->getShortMessage().PHP_EOL;
+ echo '- '.$commit->getSubjectMessage().PHP_EOL;
foreach ($commit->getParents() as $parent) {
displayLog($parent);
}
@@ -94,24 +94,20 @@ To access the message, you can use the *getMessage* method:
$commit->getMessage();
```
-For your convenience, this library provides a shortcut method to keep
-only the first line or first 50 characters if the first line is too
-long:
+To keep only the first line, or its first 50 characters if it is too
+long, use [symfony/string](https://symfony.com/doc/current/string.html):
```php
-$commit->getShortMessage();
-```
+use function Symfony\Component\String\u;
-You can customize it like this:
+u($commit->getSubjectMessage())->truncate(50, '...');
-```php
-$commit->getShortMessage(45, true, '.');
+// Without cutting the last word
+u($commit->getSubjectMessage())->truncate(45, '.', false);
```
-- The first parameter is the max length of the message.
-- The second parameter determine if the last word should be cut or
- preserved
-- The third parameter is the separator
+`getShortMessage()`, which did it, is deprecated, as `getFixedShortHash()`:
+use `substr($commit->getHash(), 0, 6)`.
There are also two other methods for your convenience:
@@ -165,7 +161,11 @@ Find every branches containing a commit
---------------------------------------
```php
-$branches = $commit->getIncludingBranches($includeLocalBranches, $includeRemoteBranches);
-$localBranches = $commit->getIncludingBranches(true, false);
-$remoteBranches = $commit->getIncludingBranches(false, true);
+use Gitonomy\Git\Reference\Branch;
+
+$branches = $commit->getIncludingBranches();
+$localBranches = array_filter($branches, static fn (Branch $branch): bool => $branch->isLocal());
+$remoteBranches = array_filter($branches, static fn (Branch $branch): bool => $branch->isRemote());
```
+
+Passing arguments to `getIncludingBranches()` is deprecated.
diff --git a/doc/diff.md b/doc/diff.md
index 2ce7870a..a243d5df 100644
--- a/doc/diff.md
+++ b/doc/diff.md
@@ -4,25 +4,41 @@ Computing diff
Even if git is a diff-less storage engine, it's possible to compute
them.
-To compute a diff in git, you need to specify a *revision*. This
-revision can be a commit (*2bc7a8*) or a range (*2bc7a8..ff4c21b*).
+To compute a diff, pass the two revisions to compare to the *getDiff*
+method of the repository:
+
+```php
+$diff = $repository->getDiff('master@{2 days ago}', 'master');
+```
For more informations about git revisions: *man gitrevisions*.
-When you have decided the revision you want and have your *Repository*
-object, you can call the *getDiff* method on the repository:
+To compare with the best common ancestor, like `git diff a...b`:
+
+```php
+$diff = $repository->getDiff($repository->getMergeBase('a', 'b'), 'b');
+```
+
+To compare the working tree, staged or not, with a revision:
```php
-$diff = $repository->getDiff('master@{2 days ago}..master');
+$diff = $repository->getWorkingCopy()->getDiff('master');
```
-You can also access it from a *Log* object:
+Calling `getDiff()` with one argument, a range or a list of revisions, is
+deprecated, as `Log::getDiff()`.
+
+To parse the output of `git diff --raw --patch` yourself, give the
+repository to read the blobs from:
```php
-$log = $repository->getLog('master@{2 days ago}..master');
-$diff = $log->getDiff();
+use Gitonomy\Git\Diff\Diff;
+
+$diff = Diff::parse($output, $repository);
```
+`Diff::setRepository()` and `File::setRepository()` are deprecated.
+
Iterating a diff
----------------
@@ -56,7 +72,7 @@ $file->isDeletion();
$file->isModification();
$file->isRename();
-$file->isChangeMode();
+$file->isModeChange();
$file->getAdditions(); // Number of added lines
$file->getDeletions(); // Number of deleted lines
@@ -78,27 +94,35 @@ The FileChange object
A *File* object is composed of many changes. For each of those changes,
a *FileChange* object is associated.
-To access changes from a file, use the *getChanges* method:
+To access changes from a file, use the *getChanges* method. The
+*getDiffLines* method of a change returns its lines, `DiffLine` objects:
```php
-$changes = $file->getChanges();
-foreach ($changes as $change) {
- foreach ($lines as $data) {
- list ($type, $line) = $data;
- if ($type === FileChange::LINE_CONTEXT) {
- echo ' '.$line.PHP_EOL;
- } elseif ($type === FileChange::LINE_ADD) {
- echo '+'.$line.PHP_EOL;
- } else {
- echo '-'.$line.PHP_EOL;
- }
+use Gitonomy\Git\Diff\DiffLineType;
+
+foreach ($file->getChanges() as $change) {
+ foreach ($change->getDiffLines() as $line) {
+ echo match ($line->getType()) {
+ DiffLineType::Addition => '+',
+ DiffLineType::Context => ' ',
+ DiffLineType::Deletion => '-',
+ }.$line->getContent().PHP_EOL;
}
}
```
+`DiffLine::getContent()` returns the line, without its prefix (`+`, `-`
+or a space) nor its line break.
+
+`FileChange::getLines()`, which returns the lines as arrays
+(`[$type, $content]`), the `FileChange::LINE_ADD`, `LINE_CONTEXT` and
+`LINE_REMOVE` constants, and passing the lines to the constructor of
+`FileChange` as arrays are deprecated: use `getDiffLines()`, `DiffLineType`
+and `DiffLine` objects.
+
To get line numbers, use the range methods:
```php
-echo sprintf('Previously from line %s to %s%s', $change->getOldRangeStart(), $change->getOldRangeEnd(), PHP_EOL);
-echo sprintf('Now from line %s to %s%s', $change->getNewRangeStart(), $change->getNewRangeEnd(), PHP_EOL);
+echo sprintf('Previously %s lines from line %s%s', $change->getRangeOldCount(), $change->getRangeOldStart(), PHP_EOL);
+echo sprintf('Now %s lines from line %s%s', $change->getRangeNewCount(), $change->getRangeNewStart(), PHP_EOL);
```
diff --git a/doc/log.md b/doc/log.md
index 4dc5a465..6ed4d67b 100644
--- a/doc/log.md
+++ b/doc/log.md
@@ -35,21 +35,24 @@ If you want to count overall commits, without offset or limit, use the
```php
echo sprintf('This log contains %s commits%s', $log->countCommits(), PHP_EOL);
-
-// Countable interface
-echo sprintf('This log contains %s commits%s', count($log), PHP_EOL);
```
+`count($log)` counts them too, which is deprecated when the offset or
+the limit of the log leaves commits out: gitlib 2.0 counts the commits
+the log iterates.
+
Offset and limit
----------------
-Use those methods:
+Use those methods, which return another log:
```php
-$log->setOffset(32);
-$log->setLimit(40);
+$page = $log->withOffset(32)->withLimit(40);
// or read it:
-$log->getOffset();
-$log->getLimit();
+$page->getOffset();
+$page->getLimit();
```
+
+`setOffset()` and `setLimit()`, which changed the log, are deprecated, as
+a negative offset or limit.
diff --git a/doc/references.md b/doc/references.md
index 64430929..6a9a63b2 100644
--- a/doc/references.md
+++ b/doc/references.md
@@ -11,6 +11,11 @@ this object from a *Repository*, use the *getReferences* method:
$references = $repository->getReferences();
```
+The bag reads the references once. To see the writes of
+`Repository::run()` or of other processes, open a new `Repository`:
+passing an argument to `getReferences()`, to read them again, is
+deprecated.
+
First, you can test existence of tags and branches like this:
```php
@@ -64,16 +69,24 @@ on ReferenceBag object:
```php
// create a branch
$references = $repository->getReferences();
-$branch = $references->createBranch('foobar', 'a8b7e4...'); // commit to reference
+$branch = $references->createBranch('foobar', 'a8b7e4...'); // commit to reference, or a Revision
// create a tag
$references = $repository->getReferences();
-$tag = $references->createTag('0.3', 'a8b7e4...'); // commit to reference
+$tag = $references->createTag('0.3', 'a8b7e4...'); // commit to reference, or a Revision
+
+// move a branch, to a revision or its name
+$branch = $branch->moveTo('master');
// delete a branch or a tag
$branch->delete();
```
+Creating a branch or a tag that exists already, which moves it, is
+deprecated, as `ReferenceBag::update()`: use `Branch::moveTo()`. Deleting
+a reference by another name than its full name, like `HEAD` or `master`,
+is deprecated.
+
Resolution from a commit
------------------------
diff --git a/doc/repository.md b/doc/repository.md
index 05b4a4e7..626a8af2 100644
--- a/doc/repository.md
+++ b/doc/repository.md
@@ -8,32 +8,47 @@ to the constructor:
$repository = new Repository('/path/to/repo');
```
-Repository options
-------------------
+When the git directory is kept apart from the working directory, pass the
+working directory as second argument:
-The constructor of Repository takes an additional parameter: `$options`.
-This parameter can be used used to tune behavior of library.
+```php
+$repository = new Repository('/path/to/repo.git', '/path/to/working-dir');
+```
-Available options are:
+Configure git
+-------------
-- **debug** (default: true): Enables exception when edge cases are met
-- **environment\_variables**: (default: none) An array of environment
- variables to be set in sub-process
-- **logger**: (default: none) Logger to use for reporting of execution
- (a `Psr\Log\LoggerInterface`)
-- **command**: (default: `git`) Specify command to execute to run git
-- **working\_dir**: If you are using multiple working directories,
- this option is for you
+The last argument of the constructor is a `Gitonomy\Git\Driver\Cli\CliDriver`,
+which configures how gitlib runs git:
-An example:
+- **command** (default: `git`): the git binary;
+- **environmentVariables** (default: none): environment variables set on
+ every git process;
+- **processTimeout** (default: 3600): in seconds, `null` for none;
+- **logger** (default: none): a `Psr\Log\LoggerInterface` receiving every
+ git command run.
```php
-$repository = new Repository('/path/to/repo', [
- 'debug' => true,
- 'logger' => new Monolog\Logger(),
-]);
+use Gitonomy\Git\Driver\Cli\CliDriver;
+use Gitonomy\Git\Repository;
+
+$repository = new Repository('/path/to/repo', driver: new CliDriver(
+ command: '/home/alice/bin/git',
+ environmentVariables: ['GIT_SSH_COMMAND' => 'ssh -i /path/to/key'],
+ processTimeout: 60,
+ logger: new Monolog\Logger('repository'),
+));
```
+`CliDriver` has the arguments of the one of gitlib 2.0, where it runs git
+itself: code using it works with both versions.
+
+An array of options as second argument is deprecated: its `working_dir`
+option is the second argument, and its `logger`, `command`,
+`environment_variables` and `process_timeout` options configure a
+`CliDriver`. Setting its `debug` option to `false` is deprecated: gitlib
+2.0 always throws when git fails.
+
Test if a repository is bare
----------------------------
@@ -47,21 +62,18 @@ $repository->isBare();
Compute size of a repository
----------------------------
-To know how much size a repository is using on your drive, you can use
-`getSize` method on a *Repository* object.
-
-> **warning**
->
-> This command was only tested with linux.
-
-The returned size is in kilobytes:
+To know how much size the git directory of a repository is using on your
+drive, you can use `getSizeInBytes` method on a *Repository* object. It
+doesn't follow the links.
```php
-$size = $repository->getSize();
+$size = $repository->getSizeInBytes();
-echo 'Your repository size is '.$size.'KB';
+echo 'Your repository size is '.$size.' bytes';
```
+`getSize()`, in kilobytes, is deprecated.
+
Access HEAD
-----------
@@ -78,27 +90,14 @@ if ($repository->isHeadDetached()) {
}
```
-Options for repository
-----------------------
-
-### Logger
+Logger
+------
If you are developing, you may appreciate to have a logger inside
-repository, telling you every executed command.
-
-You call method `setLogger` as an option on repository creation:
-
-```php
-$repository->setLogger(new Monolog\Logger('repository'));
-
-$repository->run('fetch', ['--all']);
-```
-
-You can also specify as an option on repository creation:
+repository, telling you every executed command:
```php
-$logger = new MonologLogger('repository');
-$repository = new Repository('/path/foo', ['logger' => $logger]);
+$repository = new Repository('/path/foo', driver: new CliDriver(logger: new Monolog\Logger('repository')));
$repository->run('fetch', ['--all']);
```
@@ -111,37 +110,4 @@ debug last command (fetch) return code: 0
debug last command (fetch) output: Fetching origin
```
-### Disable debug-mode
-
-Gitlib throws an exception when something seems wrong. If a `git` command exits
-with a non-zero code, then execution will be stopped, and a `RuntimeException`
-will be thrown. If you want to prevent this, set the `debug` option to` false`.
-This will make `Repository` log errors and return empty data instead of
-throwing exceptions.
-
-```php
-$repository = new Repository('/tmp/foo', ['debug' => false, 'logger' => $logger]);
-```
-
-> **note**
->
-> If you plan to disable debug, you should rely on the logger to keep a trace
-> of the failing cases.
-
-### Specify git command to use
-
-You can pass the option `command` to specify which command to use to run git
-calls. If you have a git binary located somewhere else, use this option to
-specify to gitlib path to your git binary:
-
-```php
-$repository = new Gitonomy\Git\Repository('/tmp/foo', ['command' => '/home/alice/bin/git']);
-```
-
-### Environment variables
-
-It is possible to send environment variables to the `git` commands.
-
-```php
-$repository = new Gitonomy\Git\Repository('/tmp/foo', ['environment_variables' => ['GIT_']])
-```
+`setLogger()` and `getLogger()` are deprecated.
diff --git a/doc/tree.md b/doc/tree.md
index 0c5c8ef7..d26bce65 100644
--- a/doc/tree.md
+++ b/doc/tree.md
@@ -13,23 +13,21 @@ $tree = $commit->getTree();
This tree is the entry point of all of your files.
-The main method for a tree is the *getEntries* method. This method will
-return an array, indexed by name. Each of those elements will be the
-entry mode and the entry object.
+Iterating a tree gives its entries, `TreeEntry` objects, by name. An
+entry has a name, a mode and an object: a `Blob`, a `Tree` or a
+`Submodule`.
Let's understand how it works with a concrete example:
```php
function displayTree(Tree $tree, $indent = 0)
{
- $indent = str_repeat(' ', $indent);
- foreach ($tree->getEntries() as $name => $data) {
- list($mode, $entry) = $data;
- if ($entry instanceof Tree) {
- echo $indent.$name.'/'.PHP_EOL;
- displayTree($tree, $indent + 1);
+ foreach ($tree as $name => $entry) {
+ if ($entry->getObject() instanceof Tree) {
+ echo str_repeat(' ', $indent).$name.'/'.PHP_EOL;
+ displayTree($entry->getObject(), $indent + 1);
} else {
- echo $indent.$name.PHP_EOL;
+ echo str_repeat(' ', $indent).$name.PHP_EOL;
}
}
}
@@ -39,6 +37,17 @@ displayTree($commit->getTree());
This method will recursively display all entries of a tree.
+`getBlobs()`, `getTrees()` and `getSubmodules()` return the entries of
+one type, by name. A submodule is a `Submodule`, with the hash of the
+commit it points to: check it with `instanceof Submodule`. The
+`CommitReference` class of its objects, which extends `Submodule`, is
+deprecated.
+
+`getEntries()`, `getBlobEntries()`, `getTreeEntries()` and
+`getCommitReferenceEntries()`, which return the entries as arrays
+(`[$mode, $object]`), are deprecated: iterate the tree, or use
+`getBlobs()`, `getTrees()` and `getSubmodules()`.
+
Resolve a path
--------------
diff --git a/doc/workingcopy.md b/doc/workingcopy.md
index 4ecea6c1..8ae81272 100644
--- a/doc/workingcopy.md
+++ b/doc/workingcopy.md
@@ -13,18 +13,21 @@ $wc = $repo->getWorkingCopy();
Checkout a revision
-------------------
-You can checkout any revision using *checkout* method. You can also pass
-a second argument, which will be passed as argument with `-b`:
+You can checkout any revision using *checkout* method, and create a
+branch with *checkoutNewBranch*:
```php
// git checkout master
$wc->checkout('master');
-// git checkout origin/master -b master
-$wc->checkout('origin/master', 'master');
+// git checkout -b master origin/master
+$wc->checkoutNewBranch('master', 'origin/master');
```
-You can also pass a *Reference* or a *Commit*.
+You can also pass a *Revision*, like a *Reference* or a *Commit*. Passing
+the branch to create as second argument of `checkout()` is deprecated, as
+a local *Branch* object, which detaches HEAD: pass its name to attach HEAD
+to it, like gitlib 2.0 does with the object.
Staged modifications
--------------------
@@ -45,3 +48,14 @@ You can get pending modifications on tracked files by calling method
```php
$diff = $wc->getDiffPending();
```
+
+All modifications
+-----------------
+
+You can get the modifications of the working tree, staged or not, since
+a revision, `HEAD` by default:
+
+```php
+$diff = $wc->getDiff();
+$diff = $wc->getDiff('master');
+```
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 77fe6238..afa6bbc8 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -21,6 +21,9 @@
./tests/Gitonomy/Git/Tests
+
+
+
./src/Gitonomy/Git
diff --git a/src/Gitonomy/Git/Admin.php b/src/Gitonomy/Git/Admin.php
index 7b32794c..9d561284 100644
--- a/src/Gitonomy/Git/Admin.php
+++ b/src/Gitonomy/Git/Admin.php
@@ -12,13 +12,18 @@
namespace Gitonomy\Git;
+use Gitonomy\Git\Driver\Cli\CliDriver;
use Gitonomy\Git\Exception\RuntimeException;
-use Symfony\Component\Process\Process;
+use Gitonomy\Git\Util\LegacyOverride;
/**
* Administration class for Git repositories.
*
* @author Alexandre Salomé
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use the static methods of Repository instead
+ *
+ * @final since gitonomy/gitlib 1.7
*/
class Admin
{
@@ -32,15 +37,18 @@ class Admin
* @throws RuntimeException Directory exists or not writable (only if debug=true)
*
* @return Repository
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use Repository::initBare() or Repository::init() instead
*/
public static function init($path, $bare = true, array $options = [])
{
- $process = static::getProcess('init', array_merge(['-q'], $bare ? ['--bare'] : [], [$path]), $options);
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "%s::%s()" instead.', __METHOD__, Repository::class, $bare ? 'initBare' : 'init');
- $process->run();
-
- if (!$process->isSuccessFul()) {
- throw new RuntimeException(sprintf("Error on repository initialization, command wasn't successful (%s). Error output:\n%s", $process->getCommandLine(), $process->getErrorOutput()));
+ $driver = CliDriver::fromOptions($options);
+ if ($bare) {
+ $driver->initBare($path);
+ } else {
+ $driver->init($path);
}
return new Repository($path, $options);
@@ -57,10 +65,15 @@ public static function init($path, $bare = true, array $options = [])
* @param array $options options for Repository creation
*
* @return bool true if url is valid
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use Repository::isValidRepository() instead
*/
public static function isValidRepository($url, array $options = [])
{
- $process = static::getProcess('ls-remote', [$url], $options);
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "%s::isValidRepository()" instead.', __METHOD__, Repository::class);
+
+ // Unlike Repository::isValidRepository(), in the working directory of PHP, where a relative path may name a remote.
+ $process = CliDriver::fromOptions($options)->createProcess('ls-remote', [$url]);
$process->run();
@@ -80,10 +93,16 @@ public static function isValidRepository($url, array $options = [])
* @param array $options options for Repository creation
*
* @return bool true if url is valid and branch exists
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use Repository::isValidRepositoryAndBranch() instead
*/
public static function isValidRepositoryAndBranch($url, $branchName, array $options = [])
{
- $process = static::getProcess('ls-remote', ['--heads', $url, $branchName], $options);
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "%s::isValidRepositoryAndBranch()" instead.', __METHOD__, Repository::class);
+
+ // Unlike Repository::isValidRepositoryAndBranch(), "release" matches "feature/release", and "rel" "release",
+ // in the working directory of PHP.
+ $process = CliDriver::fromOptions($options)->createProcess('ls-remote', ['--heads', $url, $branchName]);
$process->run();
$processOutput = $process->getOutput();
@@ -100,12 +119,16 @@ public static function isValidRepositoryAndBranch($url, $branchName, array $opti
* @param array $options options for Repository creation
*
* @return Repository
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use Repository::cloneBareFrom() or Repository::cloneFrom() instead
*/
public static function cloneTo($path, $url, $bare = true, array $options = [])
{
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "%s::%s()" instead, which takes the URL before the path.', __METHOD__, Repository::class, $bare ? 'cloneBareFrom' : 'cloneFrom');
+
$args = $bare ? ['--bare'] : [];
- return static::cloneRepository($path, $url, $args, $options);
+ return self::cloneWith($path, $url, $args, $options);
}
/**
@@ -118,15 +141,19 @@ public static function cloneTo($path, $url, $bare = true, array $options = [])
* @param array $options options for Repository creation
*
* @return Repository
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use Repository::cloneBareFrom() or Repository::cloneFrom() with their "branch" argument instead
*/
public static function cloneBranchTo($path, $url, $branch, $bare = true, $options = [])
{
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "%s::%s()" with its "branch" argument instead, which takes the URL before the path.', __METHOD__, Repository::class, $bare ? 'cloneBareFrom' : 'cloneFrom');
+
$args = ['--branch', $branch];
if ($bare) {
$args[] = '--bare';
}
- return static::cloneRepository($path, $url, $args, $options);
+ return self::cloneWith($path, $url, $args, $options);
}
/**
@@ -137,10 +164,14 @@ public static function cloneBranchTo($path, $url, $branch, $bare = true, $option
* @param array $options options for Repository creation
*
* @return Repository
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use Repository::mirrorFrom() instead
*/
public static function mirrorTo($path, $url, array $options = [])
{
- return static::cloneRepository($path, $url, ['--mirror'], $options);
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "%s::mirrorFrom()" instead, which takes the URL before the path.', __METHOD__, Repository::class);
+
+ return self::cloneWith($path, $url, ['--mirror'], $options);
}
/**
@@ -152,37 +183,27 @@ public static function mirrorTo($path, $url, array $options = [])
* @param array $options options for Repository creation
*
* @return Repository
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use Repository::cloneFrom(), Repository::cloneBareFrom() or Repository::mirrorFrom() instead
*/
public static function cloneRepository($path, $url, array $args = [], array $options = [])
{
- $process = static::getProcess('clone', array_merge(['-q'], $args, [$url, $path]), $options);
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%1$s()" method is deprecated, use "%2$s::cloneFrom()", "%2$s::cloneBareFrom()" or "%2$s::mirrorFrom()" instead, which take the URL before the path; they support no other argument of "git clone".', __METHOD__, Repository::class);
- $process->run();
-
- if (!$process->isSuccessFul()) {
- throw new RuntimeException(sprintf('Error while initializing repository: %s', $process->getErrorOutput()));
- }
+ CliDriver::fromOptions($options)->cloneRepository($url, $path, $args);
return new Repository($path, $options);
}
- /**
- * This internal method is used to create a process object.
- */
- private static function getProcess($command, array $args = [], array $options = [])
+ private static function cloneWith($path, $url, array $args, array $options)
{
- $is_windows = defined('PHP_WINDOWS_VERSION_BUILD');
- $options = array_merge([
- 'environment_variables' => $is_windows ? ['PATH' => getenv('PATH')] : [],
- 'command' => 'git',
- 'process_timeout' => 3600,
- ], $options);
-
- $process = new Process(array_merge([$options['command'], $command], $args));
- $process->setEnv($options['environment_variables']);
- $process->setTimeout($options['process_timeout']);
- $process->setIdleTimeout($options['process_timeout']);
-
- return $process;
+ // Like in 1.6, a subclass overriding cloneRepository() clones the repository.
+ if (LegacyOverride::isOverridden(static::class, self::class, 'cloneRepository')) {
+ return static::cloneRepository($path, $url, $args, $options);
+ }
+
+ CliDriver::fromOptions($options)->cloneRepository($url, $path, $args);
+
+ return new Repository($path, $options);
}
}
diff --git a/src/Gitonomy/Git/Blame.php b/src/Gitonomy/Git/Blame.php
index dec0773b..e9d3e407 100644
--- a/src/Gitonomy/Git/Blame.php
+++ b/src/Gitonomy/Git/Blame.php
@@ -12,12 +12,15 @@
namespace Gitonomy\Git;
+use Gitonomy\Git\Blame\Hunk;
use Gitonomy\Git\Blame\Line;
use Gitonomy\Git\Exception\InvalidArgumentException;
use Gitonomy\Git\Parser\BlameParser;
/**
* @author Alexandre Salomé
+ *
+ * @final since gitonomy/gitlib 1.7
*/
class Blame implements \Countable
{
@@ -47,14 +50,33 @@ class Blame implements \Countable
protected $lines;
/**
- * @param string $lineRange Argument to pass to git blame (-L).
- * Can be a line range (40,60 or 40,+21)
- * or a regexp ('/^function$/')
+ * The fourth argument is the first line to blame, and a fifth one the last
+ * line, like in 2.0: like "git blame -L 5,3", a last line before the first
+ * one blames the lines 3 to 5.
+ *
+ * A string as fourth argument, a range of lines passed to "git blame -L",
+ * like "40,60", "40,+21" or a regexp ("/^function$/"), is deprecated.
+ *
+ * @param int|string|null $lineRange The first line to blame, the first one of the file when null
+ * @param int|null $lastLine The last line to blame, the last one of the file when null
+ *
+ * @throws InvalidArgumentException An invalid range of lines
*/
- public function __construct(Repository $repository, Revision $revision, $file, $lineRange = null)
+ public function __construct(Repository $repository, Revision $revision, $file, $lineRange = null, ?int $lastLine = null)
{
+ if (is_string($lineRange)) {
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'Passing a range of lines as a string to "%s::getBlame()" or "%s()" is deprecated, pass the first and the last lines instead.', Repository::class, __METHOD__);
+ } elseif (null !== $lastLine) {
+ if ((null !== $lineRange && $lineRange < 1) || $lastLine < 1) {
+ throw new InvalidArgumentException(sprintf('Invalid range of lines "%s,%s": the lines are numbered from 1.', $lineRange, $lastLine));
+ }
+
+ $lineRange = ($lineRange ?? 1).','.$lastLine;
+ }
+
$this->repository = $repository;
$this->revision = $revision;
+ // A first line alone, "-L 5" like in 1.6, blames from this line to the end of the file.
$this->lineRange = $lineRange;
$this->file = $file;
}
@@ -77,13 +99,42 @@ public function getLine($number)
return $lines[$number];
}
+ /**
+ * @return list The consecutive lines last changed by the same commit
+ */
+ public function getHunks(): array
+ {
+ $hunks = [];
+ $commit = null;
+ $lines = [];
+ foreach ($this->getLines() as $line) {
+ if ($commit !== $line->getCommit() && [] !== $lines) {
+ $hunks[] = new Hunk($commit, $lines);
+ $lines = [];
+ }
+ $commit = $line->getCommit();
+ // By number in the file, like in gitlib 2.0, where getLines() numbers them so too.
+ $lines[$line->getNumber()] = $line;
+ }
+
+ if (null !== $commit) {
+ $hunks[] = new Hunk($commit, $lines);
+ }
+
+ return $hunks;
+ }
+
/**
* Returns lines grouped by commit.
*
* @return array a list of two-elements array (commit, lines)
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use getHunks() instead
*/
public function getGroupedLines()
{
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "getHunks()" instead.', __METHOD__);
+
$result = [];
$commit = null;
$current = [];
diff --git a/src/Gitonomy/Git/Blame/Hunk.php b/src/Gitonomy/Git/Blame/Hunk.php
new file mode 100644
index 00000000..7919cf25
--- /dev/null
+++ b/src/Gitonomy/Git/Blame/Hunk.php
@@ -0,0 +1,45 @@
+
+ * (c) Julien DIDIER
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace Gitonomy\Git\Blame;
+
+use Gitonomy\Git\Commit;
+
+/**
+ * Consecutive lines of a blame, last changed by the same commit.
+ *
+ * @author Grégoire Pineau
+ */
+final class Hunk
+{
+ /**
+ * @param array $lines By number
+ */
+ public function __construct(
+ private Commit $commit,
+ private array $lines,
+ ) {
+ }
+
+ public function getCommit(): Commit
+ {
+ return $this->commit;
+ }
+
+ /**
+ * @return array By number
+ */
+ public function getLines(): array
+ {
+ return $this->lines;
+ }
+}
diff --git a/src/Gitonomy/Git/Blame/Line.php b/src/Gitonomy/Git/Blame/Line.php
index 83a7a912..1b10a2f3 100644
--- a/src/Gitonomy/Git/Blame/Line.php
+++ b/src/Gitonomy/Git/Blame/Line.php
@@ -16,6 +16,8 @@
/**
* @author Alexandre Salomé
+ *
+ * @final since gitonomy/gitlib 1.7
*/
class Line
{
@@ -45,8 +47,29 @@ public function getContent()
return $this->content;
}
+ /**
+ * @return int The number of the line in the blamed file
+ */
+ public function getNumber(): int
+ {
+ return (int) $this->targetLine;
+ }
+
+ /**
+ * @return int The number of the line in the file of the commit that last changed it
+ */
+ public function getOriginalNumber(): int
+ {
+ return (int) $this->sourceLine;
+ }
+
+ /**
+ * @deprecated since gitonomy/gitlib 1.7, use getOriginalNumber() instead
+ */
public function getLine()
{
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "getOriginalNumber()" instead.', __METHOD__);
+
return $this->sourceLine;
}
diff --git a/src/Gitonomy/Git/Blob.php b/src/Gitonomy/Git/Blob.php
index dfe885fe..990773ff 100644
--- a/src/Gitonomy/Git/Blob.php
+++ b/src/Gitonomy/Git/Blob.php
@@ -16,6 +16,8 @@
* Representation of a Blob commit.
*
* @author Alexandre Salomé
+ *
+ * @final since gitonomy/gitlib 1.7
*/
class Blob
{
@@ -52,6 +54,8 @@ class Blob
/**
* @param Repository $repository Repository where the blob is located
* @param string $hash Hash of the blob
+ *
+ * In gitlib 2.0, the constructor is internal: get it from Repository::getBlob().
*/
public function __construct(Repository $repository, $hash)
{
diff --git a/src/Gitonomy/Git/Commit.php b/src/Gitonomy/Git/Commit.php
index 573d3b52..38859bf9 100644
--- a/src/Gitonomy/Git/Commit.php
+++ b/src/Gitonomy/Git/Commit.php
@@ -23,6 +23,8 @@
* Representation of a Git commit.
*
* @author Alexandre Salomé
+ *
+ * @final since gitonomy/gitlib 1.7
*/
class Commit extends Revision
{
@@ -38,9 +40,17 @@ class Commit extends Revision
*
* @param Repository $repository Repository of the commit
* @param string $hash Hash of the commit
+ *
+ * In gitlib 2.0, the constructor is internal, and takes a CommitData
+ * instead of an array: get it from Repository::getCommit(). Passing data
+ * is deprecated.
*/
public function __construct(Repository $repository, $hash, array $data = [])
{
+ if ([] !== $data) {
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'Passing data to "%s()" is deprecated, the constructor is internal in 2.0, and takes a "CommitData" instead of an array: get the commit from "%s::getCommit()".', __METHOD__, Repository::class);
+ }
+
if (!preg_match('/^[a-f0-9]{40}$/', $hash)) {
throw new ReferenceNotFoundException($hash);
}
@@ -50,6 +60,9 @@ public function __construct(Repository $repository, $hash, array $data = [])
$this->setData($data);
}
+ /**
+ * In gitlib 2.0, it is internal, and takes a CommitData instead of an array.
+ */
public function setData(array $data)
{
foreach ($data as $name => $value) {
@@ -64,10 +77,7 @@ public function getDiff()
{
$args = ['-r', '-p', '--raw', '-m', '-M', '--no-commit-id', '--full-index', $this->revision];
- $diff = Diff::parse($this->repository->run('diff-tree', $args));
- $diff->setRepository($this->repository);
-
- return $diff;
+ return Diff::parse($this->repository->run('diff-tree', $args), $this->repository);
}
/**
@@ -94,10 +104,14 @@ public function getShortHash()
* Returns a fixed-with short hash.
*
* @return string Short hash
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use substr($commit->getHash(), 0, $length) instead
*/
public function getFixedShortHash($length = 6)
{
- return StringHelper::substr($this->revision, 0, $length);
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "substr($commit->getHash(), 0, $length)" instead.', __METHOD__);
+
+ return substr($this->revision, 0, $length);
}
/**
@@ -149,7 +163,7 @@ public function getTree()
public function getLastModification($path = null)
{
if (0 === strpos($path, '/')) {
- $path = StringHelper::substr($path, 1);
+ $path = substr($path, 1);
}
if ($getWorkingDir = $this->repository->getWorkingDir()) {
@@ -171,17 +185,23 @@ public function getLastModification($path = null)
* @param string $separator
*
* @return string
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use symfony/string instead: u($commit->getSubjectMessage())->truncate($length, $separator, !$preserve)
*/
public function getShortMessage($length = 50, $preserve = false, $separator = '...')
{
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use symfony/string instead: "u($commit->getSubjectMessage())->truncate($length, $separator, !$preserve)".', __METHOD__);
+
$message = $this->getData('subjectMessage');
+ // The encoding StringHelper::setEncoding() set, without loading this deprecated class.
+ $encoding = class_exists(StringHelper::class, false) ? StringHelper::getEncoding() : 'utf-8';
- if (StringHelper::strlen($message) > $length) {
- if ($preserve && false !== ($breakpoint = StringHelper::strpos($message, ' ', $length))) {
+ if (mb_strlen($message, $encoding) > $length) {
+ if ($preserve && false !== ($breakpoint = mb_strpos($message, ' ', $length, $encoding))) {
$length = $breakpoint;
}
- return rtrim(StringHelper::substr($message, 0, $length)).$separator;
+ return rtrim(mb_substr($message, 0, $length, $encoding)).$separator;
}
return $message;
@@ -200,6 +220,8 @@ public function resolveReferences()
/**
* Find branch containing the commit.
*
+ * Passing arguments, to keep local or remote branches only, is deprecated.
+ *
* @param bool $local set true to try to locate a commit on local repository
* @param bool $remote set true to try to locate a commit on remote repository
*
@@ -207,6 +229,10 @@ public function resolveReferences()
*/
public function getIncludingBranches($local = true, $remote = true)
{
+ if (func_num_args() > 0) {
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'Passing arguments to "%s()" is deprecated, filter the branches it returns with "%s::isLocal()" or "isRemote()" instead.', __METHOD__, Branch::class);
+ }
+
$arguments = ['--contains', $this->revision];
if ($local && $remote) {
@@ -229,7 +255,7 @@ public function getIncludingBranches($local = true, $remote = true)
$branchesName = explode("\n", trim(str_replace('*', '', $result)));
$branchesName = array_filter($branchesName, function ($v) {
- return false === StringHelper::strpos($v, '->');
+ return false === strpos($v, '->');
});
$branchesName = array_map('trim', $branchesName);
@@ -239,7 +265,7 @@ public function getIncludingBranches($local = true, $remote = true)
foreach ($branchesName as $branchName) {
if (false === $local) {
$branches[] = $references->getRemoteBranch($branchName);
- } elseif (0 === StringHelper::strrpos($branchName, 'remotes/')) {
+ } elseif (0 === strrpos($branchName, 'remotes/')) {
$branches[] = $references->getRemoteBranch(str_replace('remotes/', '', $branchName));
} else {
$branches[] = $references->getBranch($branchName);
diff --git a/src/Gitonomy/Git/CommitReference.php b/src/Gitonomy/Git/CommitReference.php
index 7be3d89c..7afbebd4 100644
--- a/src/Gitonomy/Git/CommitReference.php
+++ b/src/Gitonomy/Git/CommitReference.php
@@ -12,7 +12,13 @@
namespace Gitonomy\Git;
-class CommitReference
+/**
+ * The submodules of trees, a Submodule: 2.0 returns Submodule objects, and
+ * makes this class its alias.
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use Submodule instead
+ */
+class CommitReference extends Submodule
{
/**
* @var string
@@ -23,4 +29,12 @@ public function __construct($hash)
{
$this->hash = $hash;
}
+
+ /**
+ * The hash of this class, which the objects 1.6 serialized have too, unlike the one of Submodule.
+ */
+ public function getHash(): string
+ {
+ return $this->hash;
+ }
}
diff --git a/src/Gitonomy/Git/Diff/Diff.php b/src/Gitonomy/Git/Diff/Diff.php
index 7737ee8f..87d3a521 100644
--- a/src/Gitonomy/Git/Diff/Diff.php
+++ b/src/Gitonomy/Git/Diff/Diff.php
@@ -14,11 +14,14 @@
use Gitonomy\Git\Parser\DiffParser;
use Gitonomy\Git\Repository;
+use Gitonomy\Git\Util\LegacyOverride;
/**
* Representation of a diff.
*
* @author Alexandre Salomé
+ *
+ * @final since gitonomy/gitlib 1.7
*/
class Diff
{
@@ -45,21 +48,38 @@ public function __construct(array $files, $rawDiff)
}
/**
+ * Parses the output of ``git diff --raw --patch``.
+ *
+ * @param Repository|null $repository The repository to read the blobs of the files from
+ *
* @return Diff
+ *
+ * @psalm-variadic The arguments this method gets with func_get_arg(), commented out in its signature
*/
- public static function parse($rawDiff)
+ public static function parse($rawDiff /* , ?Repository $repository = null */)
{
+ // A second argument, without breaking the classes overriding this method.
+ $repository = 1 < \func_num_args() ? func_get_arg(1) : null;
+
$parser = new DiffParser();
$parser->parse($rawDiff);
- return new self($parser->files, $rawDiff);
+ $diff = new self($parser->files, $rawDiff);
+ if (null !== $repository) {
+ $diff->attachRepository($repository);
+ }
+
+ return $diff;
}
+ /**
+ * @deprecated since gitonomy/gitlib 1.7, the diffs gitlib returns have their repository already
+ */
public function setRepository(Repository $repository)
{
- foreach ($this->files as $file) {
- $file->setRepository($repository);
- }
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, the diffs gitlib returns have their repository already.', __METHOD__);
+
+ $this->attachRepository($repository);
}
/**
@@ -103,20 +123,43 @@ function (File $file) {
/**
* Create a new instance of Diff from an array.
*
- * @param array $array The array
+ * @param array $array The array
+ * @param Repository|null $repository The repository to read the blobs of the files from
*
* @return Diff The new instance
+ *
+ * @psalm-variadic The arguments this method gets with func_get_arg(), commented out in its signature
*/
- public static function fromArray(array $array)
+ public static function fromArray(array $array /* , ?Repository $repository = null */)
{
+ // A second argument, without breaking the classes overriding this method.
+ $repository = 1 < \func_num_args() ? func_get_arg(1) : null;
+
return new static(
array_map(
- function ($array) {
- return File::fromArray($array);
+ function ($array) use ($repository) {
+ return File::fromArray($array, $repository);
},
$array['files']
),
$array['rawDiff']
);
}
+
+ private function attachRepository(Repository $repository): void
+ {
+ foreach ($this->files as $file) {
+ // Like in 1.6, a subclass overriding File::setRepository() gets the repository.
+ if (LegacyOverride::isOverridden($file, File::class, 'setRepository')) {
+ $file->setRepository($repository);
+
+ continue;
+ }
+
+ // Without the deprecation of File::setRepository().
+ \Closure::bind(static function () use ($file, $repository) {
+ $file->repository = $repository;
+ }, null, File::class)();
+ }
+ }
}
diff --git a/src/Gitonomy/Git/Diff/DiffLine.php b/src/Gitonomy/Git/Diff/DiffLine.php
new file mode 100644
index 00000000..cabd2429
--- /dev/null
+++ b/src/Gitonomy/Git/Diff/DiffLine.php
@@ -0,0 +1,40 @@
+
+ * (c) Julien DIDIER
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace Gitonomy\Git\Diff;
+
+/**
+ * A line of a hunk.
+ *
+ * @author Grégoire Pineau
+ */
+final class DiffLine
+{
+ public function __construct(
+ private readonly DiffLineType $type,
+ private readonly string $content,
+ ) {
+ }
+
+ public function getType(): DiffLineType
+ {
+ return $this->type;
+ }
+
+ /**
+ * @return string The line, without its "+", "-" or " " prefix, nor its line break
+ */
+ public function getContent(): string
+ {
+ return $this->content;
+ }
+}
diff --git a/src/Gitonomy/Git/Diff/DiffLineType.php b/src/Gitonomy/Git/Diff/DiffLineType.php
new file mode 100644
index 00000000..a00cc0f8
--- /dev/null
+++ b/src/Gitonomy/Git/Diff/DiffLineType.php
@@ -0,0 +1,26 @@
+
+ * (c) Julien DIDIER
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace Gitonomy\Git\Diff;
+
+/**
+ * The kind of a line of a hunk, its values those of the deprecated
+ * FileChange::LINE_* constants.
+ *
+ * @author Grégoire Pineau
+ */
+enum DiffLineType: int
+{
+ case Addition = 1;
+ case Context = 0;
+ case Deletion = -1;
+}
diff --git a/src/Gitonomy/Git/Diff/File.php b/src/Gitonomy/Git/Diff/File.php
index bfca155c..1b72d86f 100644
--- a/src/Gitonomy/Git/Diff/File.php
+++ b/src/Gitonomy/Git/Diff/File.php
@@ -13,9 +13,12 @@
namespace Gitonomy\Git\Diff;
use Gitonomy\Git\Repository;
+use Gitonomy\Git\Util\LegacyOverride;
/**
* @author Alexandre Salomé
+ *
+ * @final since gitonomy/gitlib 1.7
*/
class File
{
@@ -54,6 +57,11 @@ class File
*/
protected $isBinary;
+ /**
+ * @var bool
+ */
+ private $isCopy;
+
/**
* @var FileChange[] An array of FileChange objects
*/
@@ -66,8 +74,11 @@ class File
/**
* Instanciates a new File object.
+ *
+ * @param FileChange[] $changes
+ * @param Repository|null $repository The repository to read the blobs from
*/
- public function __construct($oldName, $newName, $oldMode, $newMode, $oldIndex, $newIndex, $isBinary)
+ public function __construct($oldName, $newName, $oldMode, $newMode, $oldIndex, $newIndex, $isBinary, $isCopy = false, array $changes = [], ?Repository $repository = null)
{
$this->oldName = $oldName;
$this->newName = $newName;
@@ -76,12 +87,18 @@ public function __construct($oldName, $newName, $oldMode, $newMode, $oldIndex, $
$this->oldIndex = $oldIndex;
$this->newIndex = $newIndex;
$this->isBinary = $isBinary;
-
- $this->changes = [];
+ $this->isCopy = $isCopy;
+ $this->changes = $changes;
+ $this->repository = $repository;
}
+ /**
+ * @deprecated since gitonomy/gitlib 1.7, give the changes to the constructor instead
+ */
public function addChange(FileChange $change)
{
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, give the changes to the constructor instead.', __METHOD__);
+
$this->changes[] = $change;
}
@@ -108,23 +125,46 @@ public function isModification()
/**
* Indicates if it's a rename.
*
- * A rename can only occurs if it's a modification (not a creation or a deletion).
+ * A rename can only occurs if it's a modification (not a creation or a deletion), and is not a copy.
*
* @return bool
*/
public function isRename()
{
- return $this->isModification() && $this->oldName !== $this->newName;
+ return $this->isModification() && $this->oldName !== $this->newName && !$this->isCopy;
+ }
+
+ /**
+ * Indicates if it's a copy.
+ *
+ * Unlike a rename, the source file of a copy still exists after the change.
+ * The diffs gitlib reads have no copies: git detects them with -C only.
+ */
+ public function isCopy(): bool
+ {
+ return $this->isCopy;
}
/**
* Indicates if the file mode has changed.
*/
- public function isChangeMode()
+ public function isModeChange(): bool
{
return $this->isModification() && $this->oldMode !== $this->newMode;
}
+ /**
+ * Indicates if the file mode has changed.
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use isModeChange() instead
+ */
+ public function isChangeMode()
+ {
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "isModeChange()" instead.', __METHOD__);
+
+ return $this->isModeChange();
+ }
+
/**
* Indicates if this diff file is a deletion.
*
@@ -139,10 +179,14 @@ public function isDeletion()
* Indicates if this diff file is a deletion.
*
* @return bool
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use isDeletion() instead
*/
public function isDelete()
{
- return null === $this->newName;
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "isDeletion()" instead.', __METHOD__);
+
+ return $this->isDeletion();
}
/**
@@ -152,7 +196,8 @@ public function getAdditions()
{
$result = 0;
foreach ($this->changes as $change) {
- $result += $change->getCount(FileChange::LINE_ADD);
+ // Like in 1.6, a subclass overriding FileChange::getCount() counts the lines.
+ $result += LegacyOverride::isOverridden($change, FileChange::class, 'getCount') ? $change->getCount(DiffLineType::Addition->value) : $change->getAdditions();
}
return $result;
@@ -165,7 +210,8 @@ public function getDeletions()
{
$result = 0;
foreach ($this->changes as $change) {
- $result += $change->getCount(FileChange::LINE_REMOVE);
+ // Like in 1.6, a subclass overriding FileChange::getCount() counts the lines.
+ $result += LegacyOverride::isOverridden($change, FileChange::class, 'getCount') ? $change->getCount(DiffLineType::Deletion->value) : $change->getDeletions();
}
return $result;
@@ -240,21 +286,31 @@ public function toArray()
}
/**
+ * @param Repository|null $repository The repository to read the blobs from
+ *
* @return File
+ *
+ * @psalm-variadic The arguments this method gets with func_get_arg(), commented out in its signature
*/
- public static function fromArray(array $array)
+ public static function fromArray(array $array /* , ?Repository $repository = null */)
{
- $file = new self($array['old_name'], $array['new_name'], $array['old_mode'], $array['new_mode'], $array['old_index'], $array['new_index'], $array['is_binary']);
+ // A second argument, without breaking the classes overriding this method.
+ $repository = 1 < \func_num_args() ? func_get_arg(1) : null;
- foreach ($array['changes'] as $change) {
- $file->addChange(FileChange::fromArray($change));
- }
+ $changes = array_map(function (array $change) {
+ return FileChange::fromArray($change);
+ }, $array['changes']);
- return $file;
+ return new self($array['old_name'], $array['new_name'], $array['old_mode'], $array['new_mode'], $array['old_index'], $array['new_index'], $array['is_binary'], $array['is_copy'] ?? false, $changes, $repository);
}
+ /**
+ * @deprecated since gitonomy/gitlib 1.7, a presentation concern: use substr($file->getNewIndex(), 0, 12) instead
+ */
public function getAnchor()
{
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "substr($file->getNewIndex(), 0, 12)" instead.', __METHOD__);
+
return substr($this->newIndex, 0, 12);
}
@@ -263,8 +319,13 @@ public function getRepository()
return $this->repository;
}
+ /**
+ * @deprecated since gitonomy/gitlib 1.7, the diffs gitlib returns have their repository already
+ */
public function setRepository(Repository $repository)
{
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, the diffs gitlib returns have their repository already.', __METHOD__);
+
$this->repository = $repository;
}
diff --git a/src/Gitonomy/Git/Diff/FileChange.php b/src/Gitonomy/Git/Diff/FileChange.php
index cca18dc6..2da0f01a 100644
--- a/src/Gitonomy/Git/Diff/FileChange.php
+++ b/src/Gitonomy/Git/Diff/FileChange.php
@@ -12,10 +12,26 @@
namespace Gitonomy\Git\Diff;
+use Gitonomy\Git\Exception\InvalidArgumentException;
+
+/**
+ * @final since gitonomy/gitlib 1.7
+ */
class FileChange
{
+ /**
+ * @deprecated since gitonomy/gitlib 1.7, use DiffLineType::Context instead
+ */
const LINE_CONTEXT = 0;
+
+ /**
+ * @deprecated since gitonomy/gitlib 1.7, use DiffLineType::Deletion instead
+ */
const LINE_REMOVE = -1;
+
+ /**
+ * @deprecated since gitonomy/gitlib 1.7, use DiffLineType::Addition instead
+ */
const LINE_ADD = 1;
protected $rangeOldStart;
@@ -25,36 +41,52 @@ class FileChange
protected $lines;
/**
- * @param int $rangeOldStart
- * @param int $rangeOldCount
- * @param int $rangeNewStart
- * @param int $rangeNewCount
- * @param array $lines
+ * @param int $rangeOldStart
+ * @param int $rangeOldCount
+ * @param int $rangeNewStart
+ * @param int $rangeNewCount
+ * @param list $lines The lines as arrays, [$type, $content], are deprecated
*
* @return void
*/
public function __construct($rangeOldStart, $rangeOldCount, $rangeNewStart, $rangeNewCount, $lines)
{
+ foreach ($lines as $line) {
+ if (!$line instanceof DiffLine) {
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'Passing the lines to "%s()" as arrays is deprecated, pass "%s" objects instead.', __METHOD__, DiffLine::class);
+
+ break;
+ }
+ }
+
$this->rangeOldStart = $rangeOldStart;
$this->rangeOldCount = $rangeOldCount;
$this->rangeNewStart = $rangeNewStart;
$this->rangeNewCount = $rangeNewCount;
- $this->lines = $lines;
+ // As arrays, like in 1.6: the subclasses may read them.
+ $this->lines = array_map(static fn ($line) => $line instanceof DiffLine ? [$line->getType()->value, $line->getContent()] : $line, $lines);
}
/**
* @return int
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use getAdditions() or getDeletions() instead
*/
public function getCount($type)
{
- $result = 0;
- foreach ($this->lines as $line) {
- if ($line[0] === $type) {
- $result++;
- }
- }
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "getAdditions()" or "getDeletions()" instead.', __METHOD__);
- return $result;
+ return $this->countLines($type);
+ }
+
+ public function getAdditions(): int
+ {
+ return $this->countLines(DiffLineType::Addition->value);
+ }
+
+ public function getDeletions(): int
+ {
+ return $this->countLines(DiffLineType::Deletion->value);
}
/**
@@ -90,10 +122,22 @@ public function getRangeNewCount()
}
/**
- * @return array
+ * @return list
+ */
+ public function getDiffLines(): array
+ {
+ return array_map(static fn (array $line): DiffLine => new DiffLine(self::lineType($line[0]), $line[1]), array_values($this->lines));
+ }
+
+ /**
+ * @return array The lines, as arrays: [$type, $content]
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use getDiffLines() instead
*/
public function getLines()
{
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "getDiffLines()" instead.', __METHOD__);
+
return $this->lines;
}
@@ -118,6 +162,30 @@ public function toArray()
*/
public static function fromArray(array $array)
{
- return new self($array['range_old_start'], $array['range_old_count'], $array['range_new_start'], $array['range_new_count'], $array['lines']);
+ $change = new self($array['range_old_start'], $array['range_old_count'], $array['range_new_start'], $array['range_new_count'], []);
+ // The lines as they are, like in 1.6.
+ $change->lines = $array['lines'];
+
+ return $change;
+ }
+
+ /**
+ * @throws InvalidArgumentException $type is none of the values of DiffLineType
+ */
+ private static function lineType(int $type): DiffLineType
+ {
+ return DiffLineType::tryFrom($type) ?? throw new InvalidArgumentException(sprintf('Unknown type of line %d: the types are 1 (an addition), 0 (a context line) and -1 (a deletion).', $type));
+ }
+
+ private function countLines($type): int
+ {
+ $result = 0;
+ foreach ($this->lines as $line) {
+ if ($line[0] === $type) {
+ $result++;
+ }
+ }
+
+ return $result;
}
}
diff --git a/src/Gitonomy/Git/Driver/Cli/CliDriver.php b/src/Gitonomy/Git/Driver/Cli/CliDriver.php
new file mode 100644
index 00000000..c999bd2e
--- /dev/null
+++ b/src/Gitonomy/Git/Driver/Cli/CliDriver.php
@@ -0,0 +1,238 @@
+
+ * (c) Julien DIDIER
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace Gitonomy\Git\Driver\Cli;
+
+use Gitonomy\Git\Exception\RuntimeException;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Process\Process;
+
+/**
+ * Configures how gitlib runs git: pass it as the "driver" argument of
+ * Repository, and of its static methods creating a repository.
+ *
+ * Its constructor is the one of the CliDriver of gitlib 2.0, to write code
+ * running with both versions.
+ *
+ * @author Grégoire Pineau
+ */
+final class CliDriver
+{
+ /**
+ * @param string $command The git binary
+ * @param array $environmentVariables Set on every git process, false removing an inherited one
+ * @param float|null $processTimeout In seconds, for the whole process and without output; null for none
+ * @param LoggerInterface|null $logger Receives every git command a repository runs, with its output
+ */
+ public function __construct(
+ private string $command = 'git',
+ private array $environmentVariables = [],
+ private ?float $processTimeout = 3600,
+ private ?LoggerInterface $logger = null,
+ ) {
+ }
+
+ /**
+ * @internal For the deprecated options of Admin and Repository::cloneTo(), to be removed in 2.0
+ */
+ public static function fromOptions(array $options): self
+ {
+ $options = array_merge([
+ 'environment_variables' => defined('PHP_WINDOWS_VERSION_BUILD') ? ['PATH' => getenv('PATH')] : [],
+ 'command' => 'git',
+ 'process_timeout' => 3600,
+ ], $options);
+
+ return new self($options['command'], $options['environment_variables'], $options['process_timeout']);
+ }
+
+ /**
+ * @internal For Repository, which takes options in 1.x
+ *
+ * @return array{command: string, environment_variables: array, process_timeout: float|null, logger: LoggerInterface|null}
+ */
+ public function getOptions(): array
+ {
+ return [
+ 'command' => $this->command,
+ 'environment_variables' => $this->environmentVariables,
+ 'process_timeout' => $this->processTimeout,
+ 'logger' => $this->logger,
+ ];
+ }
+
+ /**
+ * @internal For Repository::init()
+ *
+ * @throws RuntimeException
+ */
+ public function init(string $path): void
+ {
+ $this->initRepository(['-q', $path]);
+ }
+
+ /**
+ * @internal For Repository::initBare()
+ *
+ * @throws RuntimeException
+ */
+ public function initBare(string $path): void
+ {
+ $this->initRepository(['-q', '--bare', $path]);
+ }
+
+ /**
+ * @internal For Repository::cloneFrom()
+ *
+ * @throws RuntimeException
+ */
+ public function cloneFrom(string $url, string $path, ?string $branch): void
+ {
+ $this->cloneRepository($url, $path, null === $branch ? [] : ['--branch', $branch]);
+ }
+
+ /**
+ * @internal For Repository::cloneBareFrom()
+ *
+ * @throws RuntimeException
+ */
+ public function cloneBareFrom(string $url, string $path, ?string $branch): void
+ {
+ $this->cloneRepository($url, $path, null === $branch ? ['--bare'] : ['--branch', $branch, '--bare']);
+ }
+
+ /**
+ * @internal For Repository::mirrorFrom()
+ *
+ * @throws RuntimeException
+ */
+ public function mirrorFrom(string $url, string $path): void
+ {
+ $this->cloneRepository($url, $path, ['--mirror']);
+ }
+
+ /**
+ * @internal For Repository::isValidRepository()
+ */
+ public function isValidRepository(string $url): bool
+ {
+ $process = $this->createLsRemoteProcess([], $url, []);
+ $process->run();
+
+ return $process->isSuccessful();
+ }
+
+ /**
+ * @internal For Repository::isValidRepositoryAndBranch()
+ */
+ public function isValidRepositoryAndBranch(string $url, string $branch): bool
+ {
+ // git lists the branches whose name ends with the pattern: "release" matches "feature/release".
+ $process = $this->createLsRemoteProcess(['--heads'], $url, ['refs/heads/'.$branch]);
+ $process->run();
+ if (!$process->isSuccessful()) {
+ return false;
+ }
+
+ foreach (explode("\n", $process->getOutput()) as $line) {
+ if (str_ends_with($line, "\trefs/heads/".$branch)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @internal For the deprecated Admin, to be removed in 2.0
+ *
+ * @param string[] $args Arguments of ``git clone``
+ *
+ * @throws RuntimeException
+ */
+ public function cloneRepository(string $url, string $path, array $args): void
+ {
+ $process = $this->createProcess('clone', array_merge(['-q'], $args, [$url, $path]));
+ $process->run();
+
+ if (!$process->isSuccessful()) {
+ throw new RuntimeException(sprintf('Error while initializing repository: %s', $process->getErrorOutput()));
+ }
+ }
+
+ /**
+ * @internal For the deprecated Admin, to be removed in 2.0
+ *
+ * @param string[] $args
+ */
+ public function createProcess(string $command, array $args): Process
+ {
+ $process = new Process(array_merge([$this->command, $command], $args));
+ $process->setEnv($this->environmentVariables);
+ $process->setTimeout($this->processTimeout);
+ $process->setIdleTimeout($this->processTimeout);
+
+ return $process;
+ }
+
+ /**
+ * Like in 2.0, out of the working directory of PHP, which may be a
+ * repository, like the checkout of an application: ls-remote would read
+ * its configuration, and a relative path as the name of one of its remotes.
+ *
+ * @param string[] $options
+ * @param string[] $patterns
+ */
+ private function createLsRemoteProcess(array $options, string $url, array $patterns): Process
+ {
+ $process = $this->createProcess('ls-remote', array_merge($options, [self::absoluteUrl($url)], $patterns));
+ $process->setWorkingDirectory(sys_get_temp_dir());
+
+ return $process;
+ }
+
+ /**
+ * A relative path, relative to the working directory of PHP.
+ */
+ private static function absoluteUrl(string $url): string
+ {
+ if (str_starts_with($url, '/') || 1 === preg_match('#^[a-zA-Z]:[\\\\/]#', $url)) {
+ return $url;
+ }
+
+ // Like git: a URL, or an address like "host:path", has a colon before any slash.
+ $colon = strpos($url, ':');
+ $slash = strpos($url, '/');
+ if (false !== $colon && (false === $slash || $colon < $slash)) {
+ return $url;
+ }
+
+ $cwd = getcwd();
+
+ return false === $cwd ? $url : $cwd.'/'.$url;
+ }
+
+ /**
+ * @param string[] $args
+ *
+ * @throws RuntimeException
+ */
+ private function initRepository(array $args): void
+ {
+ $process = $this->createProcess('init', $args);
+ $process->run();
+
+ if (!$process->isSuccessful()) {
+ throw new RuntimeException(sprintf("Error on repository initialization, command wasn't successful (%s). Error output:\n%s", $process->getCommandLine(), $process->getErrorOutput()));
+ }
+ }
+}
diff --git a/src/Gitonomy/Git/Exception/LogicException.php b/src/Gitonomy/Git/Exception/LogicException.php
index 8d299890..3d5866fe 100644
--- a/src/Gitonomy/Git/Exception/LogicException.php
+++ b/src/Gitonomy/Git/Exception/LogicException.php
@@ -2,6 +2,9 @@
namespace Gitonomy\Git\Exception;
+/**
+ * @final since gitonomy/gitlib 1.7
+ */
class LogicException extends \LogicException implements GitExceptionInterface
{
}
diff --git a/src/Gitonomy/Git/Exception/ProcessException.php b/src/Gitonomy/Git/Exception/ProcessException.php
index 6d6b6912..c4e20028 100644
--- a/src/Gitonomy/Git/Exception/ProcessException.php
+++ b/src/Gitonomy/Git/Exception/ProcessException.php
@@ -4,6 +4,9 @@
use Symfony\Component\Process\Process;
+/**
+ * @final since gitonomy/gitlib 1.7
+ */
class ProcessException extends RuntimeException implements GitExceptionInterface
{
protected $process;
diff --git a/src/Gitonomy/Git/Exception/ReferenceNotFoundException.php b/src/Gitonomy/Git/Exception/ReferenceNotFoundException.php
index dd9ae98d..1de3908c 100644
--- a/src/Gitonomy/Git/Exception/ReferenceNotFoundException.php
+++ b/src/Gitonomy/Git/Exception/ReferenceNotFoundException.php
@@ -12,6 +12,9 @@
namespace Gitonomy\Git\Exception;
+/**
+ * @final since gitonomy/gitlib 1.7
+ */
class ReferenceNotFoundException extends \InvalidArgumentException implements GitExceptionInterface
{
public function __construct($reference)
diff --git a/src/Gitonomy/Git/Exception/UnexpectedValueException.php b/src/Gitonomy/Git/Exception/UnexpectedValueException.php
index 83b3bebb..00d26bf5 100644
--- a/src/Gitonomy/Git/Exception/UnexpectedValueException.php
+++ b/src/Gitonomy/Git/Exception/UnexpectedValueException.php
@@ -2,6 +2,9 @@
namespace Gitonomy\Git\Exception;
+/**
+ * @final since gitonomy/gitlib 1.7
+ */
class UnexpectedValueException extends \UnexpectedValueException implements GitExceptionInterface
{
}
diff --git a/src/Gitonomy/Git/Hooks.php b/src/Gitonomy/Git/Hooks.php
index dd0adbe8..d5fdd0a6 100644
--- a/src/Gitonomy/Git/Hooks.php
+++ b/src/Gitonomy/Git/Hooks.php
@@ -20,6 +20,8 @@
* Hooks collection, aggregated by repository.
*
* @author Alexandre Salomé
+ *
+ * @final since gitonomy/gitlib 1.7
*/
class Hooks
{
diff --git a/src/Gitonomy/Git/Log.php b/src/Gitonomy/Git/Log.php
index 47665359..fae16366 100644
--- a/src/Gitonomy/Git/Log.php
+++ b/src/Gitonomy/Git/Log.php
@@ -13,12 +13,16 @@
namespace Gitonomy\Git;
use Gitonomy\Git\Diff\Diff;
+use Gitonomy\Git\Exception\InvalidArgumentException;
use Gitonomy\Git\Exception\ProcessException;
use Gitonomy\Git\Exception\ReferenceNotFoundException;
+use Gitonomy\Git\Util\LegacyOverride;
use Gitonomy\Git\Util\StringHelper;
/**
* @author Alexandre Salomé
+ *
+ * @final since gitonomy/gitlib 1.7
*/
class Log implements \Countable, \IteratorAggregate
{
@@ -62,6 +66,17 @@ public function __construct(Repository $repository, $revisions = null, $paths =
$revisions = new RevisionList($repository, $revisions);
}
+ foreach (null === $revisions ? [] : $revisions->getAll() as $revision) {
+ if ($revision instanceof Commit || $revision instanceof Reference) {
+ continue;
+ }
+
+ $name = $revision->getRevision();
+ if (str_starts_with($name, '-') && '--all' !== $name) {
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'Passing "%s", an option of "git log", as revision to "%s()" is deprecated, it throws in 2.0: pass revisions, ranges, names to exclude or "--all".', $name, __METHOD__);
+ }
+ }
+
if (null === $paths) {
$paths = [];
} elseif (is_string($paths)) {
@@ -70,6 +85,14 @@ public function __construct(Repository $repository, $revisions = null, $paths =
throw new \InvalidArgumentException(sprintf('Expected a string or an array, got a "%s".', is_object($paths) ? get_class($paths) : gettype($paths)));
}
+ if (null !== $offset && $offset < 0) {
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'Passing a negative offset to "%s()" is deprecated, it throws in 2.0.', __METHOD__);
+ }
+
+ if (null !== $limit && $limit < 0) {
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'Passing a negative limit to "%s()" is deprecated, it throws in 2.0: pass null for no limit.', __METHOD__);
+ }
+
$this->repository = $repository;
$this->revisions = $revisions;
$this->paths = $paths;
@@ -79,10 +102,14 @@ public function __construct(Repository $repository, $revisions = null, $paths =
/**
* @return Diff
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use Repository::getDiff() instead: it ignored the paths of the log
*/
public function getDiff()
{
- return $this->repository->getDiff($this->revisions);
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "%s::getDiff()" instead.', __METHOD__, Repository::class);
+
+ return $this->repository->getLegacyDiff($this->revisions);
}
/**
@@ -109,11 +136,32 @@ public function getOffset()
return $this->offset;
}
+ /**
+ * @return self The log, starting from the commit $offset
+ *
+ * @throws InvalidArgumentException $offset is negative
+ */
+ public function withOffset(?int $offset): self
+ {
+ if (null !== $offset && $offset < 0) {
+ throw new InvalidArgumentException(sprintf('The offset of a log can\'t be negative, %d given.', $offset));
+ }
+
+ $log = clone $this;
+ $log->offset = $offset;
+
+ return $log;
+ }
+
/**
* @param int $offset
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use withOffset() instead
*/
public function setOffset($offset)
{
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "withOffset()" instead.', __METHOD__);
+
$this->offset = $offset;
return $this;
@@ -127,11 +175,32 @@ public function getLimit()
return $this->limit;
}
+ /**
+ * @return self The log, of $limit commits at most
+ *
+ * @throws InvalidArgumentException $limit is negative
+ */
+ public function withLimit(?int $limit): self
+ {
+ if (null !== $limit && $limit < 0) {
+ throw new InvalidArgumentException(sprintf('The limit of a log can\'t be negative, %d given.', $limit));
+ }
+
+ $log = clone $this;
+ $log->limit = $limit;
+
+ return $log;
+ }
+
/**
* @param int $limit
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use withLimit() instead
*/
public function setLimit($limit)
{
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "withLimit()" instead.', __METHOD__);
+
$this->limit = $limit;
return $this;
@@ -145,7 +214,12 @@ public function getSingleCommit()
$limit = $this->limit;
$this->limit = 1;
$commits = $this->getCommits();
- $this->setLimit($limit);
+ // Like in 1.6, a subclass overriding setLimit() sets the limit back.
+ if (LegacyOverride::isOverridden($this, self::class, 'setLimit')) {
+ $this->setLimit($limit);
+ } else {
+ $this->limit = $limit;
+ }
if (count($commits) === 0) {
throw new ReferenceNotFoundException('The log is empty');
@@ -159,7 +233,9 @@ public function getSingleCommit()
*/
public function getCommits()
{
- $args = ['--encoding='.StringHelper::getEncoding(), '--format=raw'];
+ // The encoding StringHelper::setEncoding() set, without loading this deprecated class.
+ $encoding = class_exists(StringHelper::class, false) ? StringHelper::getEncoding() : 'utf-8';
+ $args = ['--encoding='.$encoding, '--format=raw'];
if (null !== $this->offset) {
$args[] = '--skip='.((int) $this->offset);
@@ -204,12 +280,26 @@ public function getCommits()
}
/**
+ * Counting a log whose offset or limit leaves commits out is deprecated:
+ * this count includes them, where 2.0 counts the commits the log iterates.
+ *
* @see Countable
*/
#[\ReturnTypeWillChange]
public function count()
{
- return $this->countCommits();
+ $count = $this->countCommits();
+
+ $iterated = max(0, $count - (int) $this->offset);
+ if (null !== $this->limit) {
+ $iterated = min($iterated, (int) $this->limit);
+ }
+
+ if ($iterated !== $count) {
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'Counting a log whose offset or limit leaves commits out with "%s()" is deprecated, it counts only the commits the log iterates in 2.0: use "countCommits()" to count them all.', __METHOD__);
+ }
+
+ return $count;
}
/**
diff --git a/src/Gitonomy/Git/Parser/BlameParser.php b/src/Gitonomy/Git/Parser/BlameParser.php
index 8dd6c570..377cc6c0 100644
--- a/src/Gitonomy/Git/Parser/BlameParser.php
+++ b/src/Gitonomy/Git/Parser/BlameParser.php
@@ -15,6 +15,9 @@
use Gitonomy\Git\Blame\Line;
use Gitonomy\Git\Repository;
+/**
+ * @deprecated since gitonomy/gitlib 1.7, gitlib 2.0 moves it to the internal parsers of its CliDriver
+ */
class BlameParser extends ParserBase
{
public $lines;
diff --git a/src/Gitonomy/Git/Parser/CommitParser.php b/src/Gitonomy/Git/Parser/CommitParser.php
index 6fff1ea4..bfcb64ed 100644
--- a/src/Gitonomy/Git/Parser/CommitParser.php
+++ b/src/Gitonomy/Git/Parser/CommitParser.php
@@ -14,6 +14,9 @@
use Gitonomy\Git\Exception\RuntimeException;
+/**
+ * @deprecated since gitonomy/gitlib 1.7, gitlib 2.0 merges it into the internal LogParser of its CliDriver
+ */
class CommitParser extends ParserBase
{
public $tree;
diff --git a/src/Gitonomy/Git/Parser/DiffParser.php b/src/Gitonomy/Git/Parser/DiffParser.php
index b6386ee9..cd741015 100644
--- a/src/Gitonomy/Git/Parser/DiffParser.php
+++ b/src/Gitonomy/Git/Parser/DiffParser.php
@@ -12,9 +12,14 @@
namespace Gitonomy\Git\Parser;
+use Gitonomy\Git\Diff\DiffLine;
+use Gitonomy\Git\Diff\DiffLineType;
use Gitonomy\Git\Diff\File;
use Gitonomy\Git\Diff\FileChange;
+/**
+ * @final since gitonomy/gitlib 1.7
+ */
class DiffParser extends ParserBase
{
public $files;
@@ -117,9 +122,8 @@ protected function doParse()
$newIndex = $newIndex === null ? '' : $newIndex;
$oldIndex = preg_match('/^0+$/', $oldIndex) ? null : $oldIndex;
$newIndex = preg_match('/^0+$/', $newIndex) ? null : $newIndex;
- $file = new File($oldName, $newName, $oldMode, $newMode, $oldIndex, $newIndex, $isBinary);
-
// 5. Diff
+ $changes = [];
while ($this->expects('@@ ')) {
$vars = $this->consumeRegexp('/-(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))?/');
$rangeOldStart = (int) $vars[1];
@@ -134,11 +138,11 @@ protected function doParse()
$lines = [];
while (true) {
if ($this->expects(' ')) {
- $lines[] = [FileChange::LINE_CONTEXT, $this->consumeTo("\n")];
+ $lines[] = new DiffLine(DiffLineType::Context, $this->consumeTo("\n"));
} elseif ($this->expects('+')) {
- $lines[] = [FileChange::LINE_ADD, $this->consumeTo("\n")];
+ $lines[] = new DiffLine(DiffLineType::Addition, $this->consumeTo("\n"));
} elseif ($this->expects('-')) {
- $lines[] = [FileChange::LINE_REMOVE, $this->consumeTo("\n")];
+ $lines[] = new DiffLine(DiffLineType::Deletion, $this->consumeTo("\n"));
} elseif ($this->expects("\ No newline at end of file")) {
// Ignore this case...
} else {
@@ -148,12 +152,10 @@ protected function doParse()
$this->consumeNewLine();
}
- $change = new FileChange($rangeOldStart, $rangeOldCount, $rangeNewStart, $rangeNewCount, $lines);
-
- $file->addChange($change);
+ $changes[] = new FileChange($rangeOldStart, $rangeOldCount, $rangeNewStart, $rangeNewCount, $lines);
}
- $this->files[] = $file;
+ $this->files[] = new File($oldName, $newName, $oldMode, $newMode, $oldIndex, $newIndex, $isBinary, false, $changes);
}
}
}
diff --git a/src/Gitonomy/Git/Parser/LogParser.php b/src/Gitonomy/Git/Parser/LogParser.php
index 67b86687..c92df2f0 100644
--- a/src/Gitonomy/Git/Parser/LogParser.php
+++ b/src/Gitonomy/Git/Parser/LogParser.php
@@ -12,6 +12,9 @@
namespace Gitonomy\Git\Parser;
+/**
+ * @deprecated since gitonomy/gitlib 1.7, gitlib 2.0 moves it to the internal parsers of its CliDriver
+ */
class LogParser extends CommitParser
{
public $log = [];
diff --git a/src/Gitonomy/Git/Parser/ReferenceParser.php b/src/Gitonomy/Git/Parser/ReferenceParser.php
index 82e3a8b5..4091bade 100644
--- a/src/Gitonomy/Git/Parser/ReferenceParser.php
+++ b/src/Gitonomy/Git/Parser/ReferenceParser.php
@@ -12,6 +12,9 @@
namespace Gitonomy\Git\Parser;
+/**
+ * @deprecated since gitonomy/gitlib 1.7, gitlib 2.0 moves it to the internal parsers of its CliDriver
+ */
class ReferenceParser extends ParserBase
{
public $references;
diff --git a/src/Gitonomy/Git/Parser/TagParser.php b/src/Gitonomy/Git/Parser/TagParser.php
index 1a1c084c..bfe8b6bc 100644
--- a/src/Gitonomy/Git/Parser/TagParser.php
+++ b/src/Gitonomy/Git/Parser/TagParser.php
@@ -14,6 +14,9 @@
use Gitonomy\Git\Exception\RuntimeException;
+/**
+ * @deprecated since gitonomy/gitlib 1.7, gitlib 2.0 moves it to the internal parsers of its CliDriver
+ */
class TagParser extends ParserBase
{
public $object;
diff --git a/src/Gitonomy/Git/Parser/TreeParser.php b/src/Gitonomy/Git/Parser/TreeParser.php
index d01f4b28..2636a565 100644
--- a/src/Gitonomy/Git/Parser/TreeParser.php
+++ b/src/Gitonomy/Git/Parser/TreeParser.php
@@ -12,6 +12,9 @@
namespace Gitonomy\Git\Parser;
+/**
+ * @deprecated since gitonomy/gitlib 1.7, gitlib 2.0 moves it to the internal parsers of its CliDriver
+ */
class TreeParser extends ParserBase
{
public $entries = [];
diff --git a/src/Gitonomy/Git/PushReference.php b/src/Gitonomy/Git/PushReference.php
index dea10823..ce851642 100644
--- a/src/Gitonomy/Git/PushReference.php
+++ b/src/Gitonomy/Git/PushReference.php
@@ -13,12 +13,15 @@
namespace Gitonomy\Git;
use Gitonomy\Git\Exception\LogicException;
+use Gitonomy\Git\Util\LegacyOverride;
/**
* Push reference contains a commit interval. This object aggregates methods
* for this interval.
*
* @author Julien DIDIER
+ *
+ * @final since gitonomy/gitlib 1.7
*/
class PushReference
{
@@ -65,11 +68,23 @@ public function getRepository()
return $this->repository;
}
+ /**
+ * @return string The full name of the pushed reference
+ */
+ public function getFullname(): string
+ {
+ return $this->reference;
+ }
+
/**
* @return string
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use getFullname() instead
*/
public function getReference()
{
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "getFullname()" instead.', __METHOD__);
+
return $this->reference;
}
@@ -95,7 +110,8 @@ public function getAfter()
public function getLog($excludes = [])
{
return $this->repository->getLog(array_merge(
- [$this->getRevision()],
+ // Like in 1.6, a subclass overriding getRevision() gives the range.
+ [LegacyOverride::isOverridden($this, self::class, 'getRevision') ? $this->getRevision() : $this->getRange()],
array_map(function ($e) {
return '^'.$e;
}, $excludes)
@@ -103,35 +119,73 @@ public function getLog($excludes = [])
}
/**
- * @return string
+ * @return string The pushed commits: "before..after", or "after" for a creation
+ *
+ * @throws LogicException The push deletes the reference
*/
- public function getRevision()
+ public function getRange(): string
{
- if ($this->isDelete()) {
+ if ($this->deletes()) {
throw new LogicException('No revision for deletion');
}
- if ($this->isCreate()) {
+ if ($this->creates()) {
return $this->getAfter();
}
return $this->getBefore().'..'.$this->getAfter();
}
+ /**
+ * @return string
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use getRange() instead
+ */
+ public function getRevision()
+ {
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "getRange()" instead.', __METHOD__);
+
+ return $this->getRange();
+ }
+
+ /**
+ * @return bool Whether the push creates the reference, like File::isCreation() for a file
+ */
+ public function isCreation(): bool
+ {
+ return $this->isZero($this->before);
+ }
+
/**
* @return bool
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use isCreation() instead
*/
public function isCreate()
{
- return $this->isZero($this->before);
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "isCreation()" instead.', __METHOD__);
+
+ return $this->isCreation();
+ }
+
+ /**
+ * @return bool Whether the push deletes the reference, like File::isDeletion() for a file
+ */
+ public function isDeletion(): bool
+ {
+ return $this->isZero($this->after);
}
/**
* @return bool
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use isDeletion() instead
*/
public function isDelete()
{
- return $this->isZero($this->after);
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "isDeletion()" instead.', __METHOD__);
+
+ return $this->isDeletion();
}
/**
@@ -147,7 +201,7 @@ public function isForce()
*/
public function isFastForward()
{
- return !$this->isDelete() && !$this->isCreate() && !$this->isForce();
+ return !$this->deletes() && !$this->creates() && !$this->isForce();
}
/**
@@ -163,7 +217,7 @@ protected function isZero($reference)
*/
protected function getForce()
{
- if ($this->isDelete() || $this->isCreate()) {
+ if ($this->deletes() || $this->creates()) {
return false;
}
@@ -174,4 +228,20 @@ protected function getForce()
return $this->before !== trim($result);
}
+
+ /**
+ * Like in 1.6, a subclass overriding isCreate() tells whether the push creates the reference.
+ */
+ private function creates(): bool
+ {
+ return LegacyOverride::isOverridden($this, self::class, 'isCreate') ? $this->isCreate() : $this->isCreation();
+ }
+
+ /**
+ * Like in 1.6, a subclass overriding isDelete() tells whether the push deletes the reference.
+ */
+ private function deletes(): bool
+ {
+ return LegacyOverride::isOverridden($this, self::class, 'isDelete') ? $this->isDelete() : $this->isDeletion();
+ }
}
diff --git a/src/Gitonomy/Git/Reference.php b/src/Gitonomy/Git/Reference.php
index 96b8fa12..f5f430cb 100644
--- a/src/Gitonomy/Git/Reference.php
+++ b/src/Gitonomy/Git/Reference.php
@@ -25,6 +25,9 @@ abstract class Reference extends Revision
{
protected $commitHash;
+ /**
+ * In gitlib 2.0, the constructor is internal, and requires the commit hash: get it from Repository::getReferences().
+ */
public function __construct(Repository $repository, $revision, $commitHash = null)
{
$this->repository = $repository;
diff --git a/src/Gitonomy/Git/Reference/Branch.php b/src/Gitonomy/Git/Reference/Branch.php
index 0d27fb88..ab5799df 100644
--- a/src/Gitonomy/Git/Reference/Branch.php
+++ b/src/Gitonomy/Git/Reference/Branch.php
@@ -15,12 +15,14 @@
use Gitonomy\Git\Exception\ProcessException;
use Gitonomy\Git\Exception\RuntimeException;
use Gitonomy\Git\Reference;
-use Gitonomy\Git\Util\StringHelper;
+use Gitonomy\Git\Revision;
/**
* Representation of a branch reference.
*
* @author Alexandre Salomé
+ *
+ * @final since gitonomy/gitlib 1.7
*/
class Branch extends Reference
{
@@ -55,6 +57,18 @@ public function isLocal()
return $this->local;
}
+ /**
+ * Checks if the history of $revision contains this branch.
+ */
+ public function isMergedInto(Revision|string $revision): bool
+ {
+ // ReferenceBag::createBranch() keeps the name it got instead of a hash, like in 1.6.
+ $commit = $this->repository->getRevision($this->getCommitHash())->getCommit();
+ $mergeBase = $this->repository->getMergeBase($commit, $revision);
+
+ return null !== $mergeBase && $mergeBase->getHash() === $commit->getHash();
+ }
+
/**
* Check if this branch is merged to a destination branch
* Optionally, check only with remote branches.
@@ -63,9 +77,13 @@ public function isLocal()
* @param bool $compareOnlyWithRemote
*
* @return null|bool
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use isMergedInto() instead
*/
public function isMergedTo($destinationBranchName = 'master', $compareOnlyWithRemote = false)
{
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "%s::isMergedInto()" instead.', __METHOD__, self::class);
+
$arguments = ['-a'];
if ($compareOnlyWithRemote) {
@@ -91,13 +109,29 @@ public function isMergedTo($destinationBranchName = 'master', $compareOnlyWithRe
$output = explode("\n", trim(str_replace(['*', 'remotes/'], '', $result)));
$filtered_output = array_filter($output, static function ($v) {
- return false === StringHelper::strpos($v, '->');
+ return false === strpos($v, '->');
});
$trimmed_output = array_map('trim', $filtered_output);
return in_array($this->getName(), $trimmed_output, true);
}
+ /**
+ * Points the branch to $revision.
+ *
+ * @return self The branch, pointing to $revision
+ */
+ public function moveTo(Revision|string $revision): self
+ {
+ $hash = ($revision instanceof Revision ? $revision : $this->repository->getRevision($revision))->getCommit()->getHash();
+ $this->repository->run('update-ref', [$this->getFullname(), $hash]);
+
+ $references = $this->repository->getReferences();
+ $references->invalidate();
+
+ return $this->isLocal() ? $references->getBranch($this->getName()) : $references->getRemoteBranch($this->getName());
+ }
+
private function detectBranchType()
{
if (null === $this->local) {
diff --git a/src/Gitonomy/Git/Reference/Stash.php b/src/Gitonomy/Git/Reference/Stash.php
index 9f26802f..fb8ae331 100644
--- a/src/Gitonomy/Git/Reference/Stash.php
+++ b/src/Gitonomy/Git/Reference/Stash.php
@@ -16,6 +16,8 @@
/**
* @author Alexandre Salomé
+ *
+ * @final since gitonomy/gitlib 1.7
*/
class Stash extends Reference
{
diff --git a/src/Gitonomy/Git/Reference/Tag.php b/src/Gitonomy/Git/Reference/Tag.php
index 78c43b25..cae854ce 100644
--- a/src/Gitonomy/Git/Reference/Tag.php
+++ b/src/Gitonomy/Git/Reference/Tag.php
@@ -24,6 +24,8 @@
*
* @author Alexandre Salomé
* @author Bruce Wells
+ *
+ * @final since gitonomy/gitlib 1.7
*/
class Tag extends Reference
{
diff --git a/src/Gitonomy/Git/ReferenceBag.php b/src/Gitonomy/Git/ReferenceBag.php
index 71ef9ae4..2358a46b 100644
--- a/src/Gitonomy/Git/ReferenceBag.php
+++ b/src/Gitonomy/Git/ReferenceBag.php
@@ -17,12 +17,15 @@
use Gitonomy\Git\Reference\Branch;
use Gitonomy\Git\Reference\Stash;
use Gitonomy\Git\Reference\Tag;
+use Gitonomy\Git\Util\LegacyOverride;
/**
* Reference set associated to a repository.
*
* @author Alexandre Salomé
* @author Julien DIDIER
+ *
+ * @final since gitonomy/gitlib 1.7
*/
class ReferenceBag implements \Countable, \IteratorAggregate
{
@@ -65,6 +68,8 @@ class ReferenceBag implements \Countable, \IteratorAggregate
* Constructor.
*
* @param Repository $repository The repository
+ *
+ * In gitlib 2.0, the constructor is internal: get it from Repository::getReferences().
*/
public function __construct($repository)
{
@@ -104,44 +109,63 @@ public function has($fullname)
/**
* @return Reference
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use Branch::moveTo() instead
*/
public function update(Reference $reference)
{
- $fullname = $reference->getFullname();
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "%s::moveTo()" instead.', __METHOD__, Branch::class);
- $this->initialize();
- $this->repository->run('update-ref', [$fullname, $reference->getCommitHash()]);
-
- $this->references[$fullname] = $reference;
-
- return $reference;
+ return $this->updateReference($reference);
}
/**
- * @return Reference
+ * Creating a branch that exists already, which moves it, is deprecated.
+ *
+ * @param Revision|string $commitHash The hash of a commit, or a Revision
+ *
+ * @return Branch
*/
public function createBranch($name, $commitHash)
{
- $branch = new Branch($this->repository, 'refs/heads/'.$name, $commitHash);
+ if ($this->has('refs/heads/'.$name)) {
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'Creating the branch "%s", which exists already, is deprecated, it throws in 2.0: use "%s::moveTo()" instead.', $name, Branch::class);
+ }
- return $this->update($branch);
+ $branch = new Branch($this->repository, 'refs/heads/'.$name, self::commitHash($commitHash));
+
+ return $this->write($branch);
}
/**
- * @return Reference
+ * Creating a tag that exists already, which moves it, is deprecated.
+ *
+ * @param Revision|string $commitHash The hash of a commit, or a Revision
+ *
+ * @return Tag
*/
public function createTag($name, $commitHash)
{
- $tag = new Tag($this->repository, 'refs/tags/'.$name, $commitHash);
+ if ($this->has('refs/tags/'.$name)) {
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'Creating the tag "%s", which exists already, is deprecated, it throws in 2.0: delete it first.', $name);
+ }
- return $this->update($tag);
+ $tag = new Tag($this->repository, 'refs/tags/'.$name, self::commitHash($commitHash));
+
+ return $this->write($tag);
}
/**
+ * Deleting a reference by another name than its full name under "refs/", like "HEAD" or "master", is deprecated.
+ *
* @return void
*/
public function delete($fullname)
{
+ if (!str_starts_with($fullname, 'refs/')) {
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'Deleting "%s", which is not the full name of a reference under "refs/", is deprecated, it throws in 2.0.', $fullname);
+ }
+
$this->repository->run('update-ref', ['-d', $fullname]);
unset($this->references[$fullname]);
@@ -379,6 +403,19 @@ protected function initialize()
}
}
+ /**
+ * Reads the references again when next needed.
+ *
+ * @internal
+ */
+ public function invalidate(): void
+ {
+ $this->initialized = false;
+ $this->references = [];
+ $this->tags = [];
+ $this->branches = [];
+ }
+
/**
* @return int
*
@@ -402,4 +439,37 @@ public function getIterator()
return new \ArrayIterator($this->references);
}
+
+ /**
+ * @return Reference
+ */
+ private function updateReference(Reference $reference)
+ {
+ $fullname = $reference->getFullname();
+
+ $this->initialize();
+ $this->repository->run('update-ref', [$fullname, $reference->getCommitHash()]);
+
+ $this->references[$fullname] = $reference;
+
+ return $reference;
+ }
+
+ /**
+ * @return Reference
+ */
+ private function write(Reference $reference)
+ {
+ // Like in 1.6, a subclass overriding update() writes the reference.
+ return LegacyOverride::isOverridden($this, self::class, 'update') ? $this->update($reference) : $this->updateReference($reference);
+ }
+
+ /**
+ * @param Revision|string $commitHash
+ */
+ private static function commitHash($commitHash)
+ {
+ // A string as it is, like in 1.6: git resolves a name when writing the reference.
+ return $commitHash instanceof Revision ? $commitHash->getCommit()->getHash() : $commitHash;
+ }
}
diff --git a/src/Gitonomy/Git/Repository.php b/src/Gitonomy/Git/Repository.php
index 296496ae..fc78d09e 100644
--- a/src/Gitonomy/Git/Repository.php
+++ b/src/Gitonomy/Git/Repository.php
@@ -13,8 +13,10 @@
namespace Gitonomy\Git;
use Gitonomy\Git\Diff\Diff;
+use Gitonomy\Git\Driver\Cli\CliDriver;
use Gitonomy\Git\Exception\InvalidArgumentException;
use Gitonomy\Git\Exception\ProcessException;
+use Gitonomy\Git\Exception\ReferenceNotFoundException;
use Gitonomy\Git\Exception\RuntimeException;
use Psr\Log\LoggerInterface;
use Symfony\Component\Process\Process;
@@ -25,6 +27,8 @@
* Main entry point for browsing a Git repository.
*
* @author Alexandre Salomé
+ *
+ * @final since gitonomy/gitlib 1.7
*/
class Repository
{
@@ -101,7 +105,11 @@ class Repository
/**
* Constructs a new repository.
*
- * Available options are:
+ * The second argument is the working directory, when $dir is a git
+ * directory kept apart from it. An array of options there is deprecated:
+ * its "working_dir" option is this argument, and its "logger", "command",
+ * "environment_variables" and "process_timeout" options configure a
+ * CliDriver. Available options are:
*
* * working_dir : specify where working copy is located (option --work-tree)
*
@@ -111,13 +119,32 @@ class Repository
*
* * environment_variables : define environment variables for every ran process
*
- * @param string $dir path to git repository
- * @param array $options array of options values
+ * @param string $dir path to git repository
+ * @param string|array|null $options The working tree, or an array of options, deprecated
+ * @param CliDriver|null $driver Configures git, a CliDriver with its default arguments when null
*
* @throws InvalidArgumentException The folder does not exists
*/
- public function __construct($dir, $options = [])
+ public function __construct($dir, $options = [], ?CliDriver $driver = null)
{
+ if (!is_array($options)) {
+ $options = ['working_dir' => $options];
+ } elseif ([] !== $options) {
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'Passing an array of options to "%s()" is deprecated, pass the working directory as second argument, and configure the logger and git through the "driver" argument.', __METHOD__);
+
+ if (null !== $driver && [] !== array_intersect(array_keys($options), ['command', 'environment_variables', 'inherit_environment_variables', 'logger', 'process_timeout'])) {
+ throw new InvalidArgumentException('The driver argument can\'t be combined with the "logger" or git options: configure the driver instead.');
+ }
+
+ if (array_key_exists('debug', $options) && !$options['debug']) {
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'Setting the "debug" option of "%s()" to false is deprecated, a failing git operation always throws in 2.0: catch its exception instead.', __METHOD__);
+ }
+ }
+
+ if (null !== $driver) {
+ $options = array_merge($options, $driver->getOptions());
+ }
+
$options = array_merge([
'working_dir' => null,
'debug' => true,
@@ -152,6 +179,106 @@ public function __construct($dir, $options = [])
}
}
+ /**
+ * Initializes a repository with a working directory in $path.
+ *
+ * @param CliDriver|null $driver Configures git, a CliDriver with its default arguments when null
+ *
+ * @throws RuntimeException The directory can't be written, or is already a file
+ */
+ public static function init(string $path, ?CliDriver $driver = null): self
+ {
+ $driver = $driver ?? new CliDriver();
+ $driver->init($path);
+
+ return new self($path, null, $driver);
+ }
+
+ /**
+ * Initializes a bare repository in $path.
+ *
+ * @param CliDriver|null $driver Configures git, a CliDriver with its default arguments when null
+ *
+ * @throws RuntimeException The directory can't be written, or is already a file
+ */
+ public static function initBare(string $path, ?CliDriver $driver = null): self
+ {
+ $driver = $driver ?? new CliDriver();
+ $driver->initBare($path);
+
+ return new self($path, null, $driver);
+ }
+
+ /**
+ * Clones $url with a working directory in $path.
+ *
+ * @param string|null $branch The branch to check out, the default branch of $url when null
+ * @param CliDriver|null $driver Configures git, a CliDriver with its default arguments when null
+ *
+ * @throws RuntimeException
+ */
+ public static function cloneFrom(string $url, string $path, ?string $branch = null, ?CliDriver $driver = null): self
+ {
+ $driver = $driver ?? new CliDriver();
+ $driver->cloneFrom($url, $path, $branch);
+
+ return new self($path, null, $driver);
+ }
+
+ /**
+ * Clones the branches and the tags of $url into a bare repository in $path.
+ *
+ * @param string|null $branch The branch HEAD points to, the default branch of $url when null
+ * @param CliDriver|null $driver Configures git, a CliDriver with its default arguments when null
+ *
+ * @throws RuntimeException
+ */
+ public static function cloneBareFrom(string $url, string $path, ?string $branch = null, ?CliDriver $driver = null): self
+ {
+ $driver = $driver ?? new CliDriver();
+ $driver->cloneBareFrom($url, $path, $branch);
+
+ return new self($path, null, $driver);
+ }
+
+ /**
+ * Clones every reference of $url into a bare repository in $path, set up
+ * to mirror $url when fetching.
+ *
+ * @param CliDriver|null $driver Configures git, a CliDriver with its default arguments when null
+ *
+ * @throws RuntimeException
+ */
+ public static function mirrorFrom(string $url, string $path, ?CliDriver $driver = null): self
+ {
+ $driver = $driver ?? new CliDriver();
+ $driver->mirrorFrom($url, $path);
+
+ return new self($path, null, $driver);
+ }
+
+ /**
+ * Checks, without cloning it, that $url is a repository git can read:
+ * false when it doesn't exist, can't be reached, or refuses access.
+ *
+ * @param CliDriver|null $driver Configures git, a CliDriver with its default arguments when null
+ */
+ public static function isValidRepository(string $url, ?CliDriver $driver = null): bool
+ {
+ return ($driver ?? new CliDriver())->isValidRepository($url);
+ }
+
+ /**
+ * Checks, without cloning it, that $url is a repository git can read,
+ * and that it has the $branch branch.
+ *
+ * @param CliDriver|null $driver Configures git, a CliDriver with its default arguments when null
+ */
+ public static function isValidRepositoryAndBranch(string $url, string $branch, ?CliDriver $driver = null): bool
+ {
+ return ($driver ?? new CliDriver())->isValidRepositoryAndBranch($url, $branch);
+ }
+
/**
* Initializes directory attributes on repository:.
*
@@ -315,12 +442,16 @@ public function getWorkingCopy()
/**
* Returns the reference list associated to the repository.
*
- * @param bool $reload Reload references from the filesystem
+ * @param bool $reload Reload references from the filesystem, deprecated: open a new Repository instead
*
* @return ReferenceBag
*/
public function getReferences($reload = false)
{
+ if (\func_num_args() > 0) {
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'Passing an argument to "%s()" is deprecated, open a new Repository to read the references again.', __METHOD__);
+ }
+
if (null === $this->referenceBag || $reload) {
$this->referenceBag = new ReferenceBag($this);
}
@@ -347,12 +478,18 @@ public function getCommit($hash)
/**
* Instanciates a tree object or fetches one from the cache.
*
+ * Passing anything but a hash, like a revision name, is deprecated.
+ *
* @param string $hash A tree hash, with a length of 40
*
* @return Tree
*/
public function getTree($hash)
{
+ if (!preg_match('/^[0-9a-f]{4,40}$/', $hash)) {
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'Passing "%s", which is not a hash, to "%s()" is deprecated, it throws in 2.0: use "getRevision($name)->getCommit()->getTree()" instead.', $hash, __METHOD__);
+ }
+
if (!isset($this->objects[$hash])) {
$this->objects[$hash] = new Tree($this, $hash);
}
@@ -363,12 +500,18 @@ public function getTree($hash)
/**
* Instanciates a blob object or fetches one from the cache.
*
+ * Passing anything but a hash, like "HEAD:README.md", is deprecated.
+ *
* @param string $hash A blob hash, with a length of 40
*
* @return Blob
*/
public function getBlob($hash)
{
+ if (!preg_match('/^[0-9a-f]{4,40}$/', $hash)) {
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'Passing "%s", which is not a hash, to "%s()" is deprecated, it throws in 2.0: use "Tree::resolvePath()" instead.', $hash, __METHOD__);
+ }
+
if (!isset($this->objects[$hash])) {
$this->objects[$hash] = new Blob($this, $hash);
}
@@ -377,15 +520,29 @@ public function getBlob($hash)
}
/**
+ * Returns the commits that last changed the lines of $file.
+ *
+ * The third argument is the first line to blame, and a fourth one the last
+ * line, like in 2.0. A string as third argument, a range of lines like
+ * "git blame -L", is deprecated.
+ *
+ * @param int|string|null $lineRange The first line to blame, from 1, the first one of the file when null
+ * @param int|null $lastLine The last line to blame, the last one of the file when null
+ *
* @return Blame
+ *
+ * @psalm-variadic The arguments this method gets with func_get_arg(), commented out in its signature
*/
- public function getBlame($revision, $file, $lineRange = null)
+ public function getBlame($revision, $file, $lineRange = null /* , ?int $lastLine = null */)
{
+ // A fourth argument, without breaking the classes overriding this method.
+ $lastLine = 3 < \func_num_args() ? func_get_arg(3) : null;
+
if (is_string($revision)) {
$revision = $this->getRevision($revision);
}
- return new Blame($this, $revision, $file, $lineRange);
+ return new Blame($this, $revision, $file, $lineRange, $lastLine);
}
/**
@@ -408,29 +565,104 @@ public function getLog($revisions = null, $paths = null, $offset = null, $limit
}
/**
+ * Returns the best common ancestor of two commits, the one "git diff a...b" compares to.
+ *
+ * @return Commit|null Null when they have no common history
+ *
+ * @throws ReferenceNotFoundException A revision names no commit
+ */
+ public function getMergeBase(Revision|string $one, Revision|string $two): ?Commit
+ {
+ $process = $this->execute('merge-base', [$this->commitHash($one), $this->commitHash($two)]);
+
+ // git exits with 1, writing nothing, when the commits have no common ancestor.
+ if (1 === $process->getExitCode() && '' === $process->getErrorOutput()) {
+ return null;
+ }
+
+ if (!$process->isSuccessful()) {
+ // Returns with the "debug" option set to false.
+ $this->fail('merge-base', $process);
+
+ return null;
+ }
+
+ return $this->getCommit(trim($process->getOutput()));
+ }
+
+ /**
+ * Returns the changes from the commit of the first argument to the commit $to.
+ *
+ * Passing one argument is deprecated: an "a..b" or "a...b" range, two
+ * revisions in an array or a RevisionList, or one revision to compare
+ * with the working tree, like WorkingCopy::getDiff() does.
+ *
+ * @param RevisionList|Revision|string|array $revisions The revision to compare from, when a second argument is given
+ * @param Revision|string|null $to
+ *
* @return Diff
+ *
+ * @throws ReferenceNotFoundException A revision names no commit
+ *
+ * @psalm-variadic The arguments this method gets with func_get_arg(), commented out in its signature
*/
- public function getDiff($revisions)
+ public function getDiff($revisions /* , $to = null */)
+ {
+ // A second argument, without breaking the classes overriding this method.
+ $to = 1 < \func_num_args() ? func_get_arg(1) : null;
+
+ if (null === $to) {
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'Calling "%s()" with one argument is deprecated, pass the revisions to compare as first and second arguments, or use "%s::getDiff()" to compare a revision with the working tree.', __METHOD__, WorkingCopy::class);
+
+ return $this->getLegacyDiff($revisions);
+ }
+
+ if (!$revisions instanceof Revision && !is_string($revisions)) {
+ throw new InvalidArgumentException('The first argument of "Repository::getDiff()" must be a revision, when a second one is given.');
+ }
+
+ return $this->runDiff([$this->commitHash($revisions), $this->commitHash($to)]);
+ }
+
+ /**
+ * For the deprecated forms of getDiff() and Log::getDiff(), to be removed in 2.0.
+ *
+ * @param RevisionList|Revision|string|array $revisions
+ *
+ * @internal
+ */
+ public function getLegacyDiff($revisions): Diff
{
if (null !== $revisions && !$revisions instanceof RevisionList) {
$revisions = new RevisionList($this, $revisions);
}
- $args = array_merge(['-r', '-p', '--raw', '-m', '-M', '--no-commit-id', '--full-index'], $revisions->getAsTextArray());
-
- $diff = Diff::parse($this->run('diff', $args));
- $diff->setRepository($this);
+ return $this->runDiff($revisions->getAsTextArray());
+ }
- return $diff;
+ /**
+ * @return int The size of the files of the git directory, working tree excluded, in bytes
+ *
+ * @throws RuntimeException A file or a directory of the git directory can't be read
+ */
+ public function getSizeInBytes(): int
+ {
+ return self::directorySize($this->gitDir);
}
/**
* Returns the size of repository, in kilobytes.
*
+ * Unlike getSizeInBytes(), it counts the files the links point to.
+ *
* @return int A sum, in kilobytes
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use getSizeInBytes() instead
*/
public function getSize()
{
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "getSizeInBytes()" instead.', __METHOD__);
+
$totalBytes = 0;
$path = realpath($this->gitDir);
if ($path && file_exists($path)) {
@@ -446,9 +678,13 @@ public function getSize()
* Executes a shell command on the repository, using PHP pipes.
*
* @param string $command The command to execute
+ *
+ * @deprecated since gitonomy/gitlib 1.7, run "git shell" with Symfony Process instead
*/
public function shell($command, array $env = [])
{
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, run "git shell" with Symfony Process instead: it ignores the driver.', __METHOD__);
+
$argument = sprintf('%s \'%s\'', $command, $this->gitDir);
$prefix = '';
@@ -534,39 +770,15 @@ public function setDescription($description)
*/
public function run($command, $args = [])
{
- $process = $this->getProcess($command, $args);
-
- if ($this->logger) {
- $this->logger->info(sprintf('run command: %s "%s" ', $command, implode(' ', $args)));
- $before = microtime(true);
- }
-
- $process->run();
-
- $output = $process->getOutput();
-
- if ($this->logger && $this->debug) {
- $duration = microtime(true) - $before;
- $this->logger->debug(sprintf('last command (%s) duration: %sms', $command, sprintf('%.2f', $duration * 1000)));
- $this->logger->debug(sprintf('last command (%s) return code: %s', $command, $process->getExitCode()));
- $this->logger->debug(sprintf('last command (%s) output: %s', $command, $output));
- }
+ $process = $this->execute($command, $args);
if (!$process->isSuccessful()) {
- $error = sprintf("error while running %s\n output: \"%s\"", $command, $process->getErrorOutput());
-
- if ($this->logger) {
- $this->logger->error($error);
- }
-
- if ($this->debug) {
- throw new ProcessException($process);
- }
+ $this->fail($command, $process);
return;
}
- return $output;
+ return $process->getOutput();
}
/**
@@ -575,9 +787,13 @@ public function run($command, $args = [])
* @param LoggerInterface $logger A logger
*
* @return Repository The current repository
+ *
+ * @deprecated since gitonomy/gitlib 1.7, pass a CliDriver configured with the logger as driver argument instead
*/
public function setLogger(LoggerInterface $logger)
{
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, pass a "%s" configured with the logger as driver argument instead.', __METHOD__, CliDriver::class);
+
$this->logger = $logger;
return $this;
@@ -587,9 +803,13 @@ public function setLogger(LoggerInterface $logger)
* Returns repository logger.
*
* @return LoggerInterface the logger or null
+ *
+ * @deprecated since gitonomy/gitlib 1.7, the logger is an argument of CliDriver
*/
public function getLogger()
{
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, the logger is an argument of "%s".', __METHOD__, CliDriver::class);
+
return $this->logger;
}
@@ -600,10 +820,74 @@ public function getLogger()
* @param bool $bare flag indicating if repository is bare or has a working-copy
*
* @return Repository the newly created repository
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use Repository::cloneBareFrom() or Repository::cloneFrom() with getGitDir() as URL instead
*/
public function cloneTo($path, $bare = true, array $options = [])
{
- return Admin::cloneTo($path, $this->gitDir, $bare, $options);
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "%s::%s()" with "getGitDir()" as URL instead.', __METHOD__, self::class, $bare ? 'cloneBareFrom' : 'cloneFrom');
+
+ CliDriver::fromOptions($options)->cloneRepository($this->gitDir, $path, $bare ? ['--bare'] : []);
+
+ return new self($path, $options);
+ }
+
+ /**
+ * @param string[] $revisions
+ */
+ private function runDiff(array $revisions): Diff
+ {
+ $args = array_merge(['-r', '-p', '--raw', '-m', '-M', '--no-commit-id', '--full-index'], $revisions);
+
+ return Diff::parse($this->run('diff', $args), $this);
+ }
+
+ /**
+ * @throws ReferenceNotFoundException $revision names no commit
+ */
+ private function commitHash(Revision|string $revision): string
+ {
+ return ($revision instanceof Revision ? $revision : $this->getRevision($revision))->getCommit()->getHash();
+ }
+
+ /**
+ * Runs a git command, and logs it.
+ */
+ private function execute($command, $args = []): Process
+ {
+ $process = $this->getProcess($command, $args);
+
+ if ($this->logger) {
+ $this->logger->info(sprintf('run command: %s "%s" ', $command, implode(' ', $args)));
+ $before = microtime(true);
+ }
+
+ $process->run();
+
+ if ($this->logger && $this->debug) {
+ $duration = microtime(true) - $before;
+ $this->logger->debug(sprintf('last command (%s) duration: %sms', $command, sprintf('%.2f', $duration * 1000)));
+ $this->logger->debug(sprintf('last command (%s) return code: %s', $command, $process->getExitCode()));
+ $this->logger->debug(sprintf('last command (%s) output: %s', $command, $process->getOutput()));
+ }
+
+ return $process;
+ }
+
+ /**
+ * Logs the failure of a git command, and throws unless the "debug" option is false.
+ *
+ * @throws ProcessException
+ */
+ private function fail($command, Process $process): void
+ {
+ if ($this->logger) {
+ $this->logger->error(sprintf("error while running %s\n output: \"%s\"", $command, $process->getErrorOutput()));
+ }
+
+ if ($this->debug) {
+ throw new ProcessException($process);
+ }
}
/**
@@ -637,4 +921,49 @@ private function getProcess($command, $args = [])
return $process;
}
+
+ /**
+ * git may delete a file or a directory while it is counted, during a gc
+ * or a push: a lock, a temporary pack, the quarantine of a push... It no
+ * longer counts.
+ *
+ * @throws RuntimeException A file or a directory of $dir can't be read
+ */
+ private static function directorySize(string $dir): int
+ {
+ try {
+ $files = new \FilesystemIterator($dir, \FilesystemIterator::SKIP_DOTS);
+ } catch (\UnexpectedValueException $e) {
+ if (!file_exists($dir)) {
+ return 0;
+ }
+
+ throw new RuntimeException(sprintf('Unable to read the directory "%s".', $dir), 0, $e);
+ }
+
+ $size = 0;
+ /** @var \SplFileInfo $file */
+ foreach ($files as $file) {
+ // A link isn't followed: it may point to a file not deployed yet, like a hook.
+ if ($file->isLink()) {
+ continue;
+ }
+
+ if ($file->isDir()) {
+ $size += self::directorySize($file->getPathname());
+
+ continue;
+ }
+
+ try {
+ $size += $file->getSize();
+ } catch (\RuntimeException $e) {
+ if (file_exists($file->getPathname())) {
+ throw new RuntimeException(sprintf('Unable to read the size of "%s".', $file->getPathname()), 0, $e);
+ }
+ }
+ }
+
+ return $size;
+ }
}
diff --git a/src/Gitonomy/Git/Revision.php b/src/Gitonomy/Git/Revision.php
index 9e1921fb..146c1ad1 100644
--- a/src/Gitonomy/Git/Revision.php
+++ b/src/Gitonomy/Git/Revision.php
@@ -27,6 +27,9 @@ class Revision
*/
protected $revision;
+ /**
+ * In gitlib 2.0, the constructor is internal: get it from Repository::getRevision().
+ */
public function __construct(Repository $repository, $revision)
{
$this->repository = $repository;
diff --git a/src/Gitonomy/Git/RevisionList.php b/src/Gitonomy/Git/RevisionList.php
index 1c1a1dcd..471b1574 100644
--- a/src/Gitonomy/Git/RevisionList.php
+++ b/src/Gitonomy/Git/RevisionList.php
@@ -14,6 +14,8 @@
/**
* @author Alexandre Salomé
+ *
+ * @final since gitonomy/gitlib 1.7
*/
class RevisionList implements \IteratorAggregate, \Countable
{
diff --git a/src/Gitonomy/Git/Submodule.php b/src/Gitonomy/Git/Submodule.php
new file mode 100644
index 00000000..95326af6
--- /dev/null
+++ b/src/Gitonomy/Git/Submodule.php
@@ -0,0 +1,41 @@
+
+ * (c) Julien DIDIER
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace Gitonomy\Git;
+
+/**
+ * A submodule in a tree: the commit of another repository it points to.
+ *
+ * @author Grégoire Pineau
+ *
+ * @final
+ */
+class Submodule
+{
+ /**
+ * @var string
+ */
+ private $hash;
+
+ public function __construct(string $hash)
+ {
+ $this->hash = $hash;
+ }
+
+ /**
+ * @return string The commit of the submodule, in its own repository
+ */
+ public function getHash(): string
+ {
+ return $this->hash;
+ }
+}
diff --git a/src/Gitonomy/Git/Tree.php b/src/Gitonomy/Git/Tree.php
index 7830cfa4..87c2d867 100644
--- a/src/Gitonomy/Git/Tree.php
+++ b/src/Gitonomy/Git/Tree.php
@@ -17,8 +17,12 @@
/**
* @author Alexandre Salomé
+ *
+ * @final since gitonomy/gitlib 1.7
+ *
+ * @implements \IteratorAggregate
*/
-class Tree
+class Tree implements \IteratorAggregate
{
protected $repository;
protected $hash;
@@ -26,6 +30,9 @@ class Tree
protected $entries;
protected $entriesByType;
+ /**
+ * In gitlib 2.0, the constructor is internal: get it from Repository::getTree() or Commit::getTree().
+ */
public function __construct(Repository $repository, $hash)
{
$this->repository = $repository;
@@ -71,10 +78,54 @@ protected function initialize()
}
/**
- * @return array An associative array name => $object
+ * @return \ArrayIterator The entries, by name
+ */
+ public function getIterator(): \ArrayIterator
+ {
+ $this->initialize();
+
+ return new \ArrayIterator(self::treeEntries($this->entries));
+ }
+
+ /**
+ * @return array The blobs, by name
+ */
+ public function getBlobs(): array
+ {
+ $this->initialize();
+
+ return self::treeEntries($this->entriesByType['blob']);
+ }
+
+ /**
+ * @return array The submodules, by name
+ */
+ public function getSubmodules(): array
+ {
+ $this->initialize();
+
+ return self::treeEntries($this->entriesByType['commit']);
+ }
+
+ /**
+ * @return array The trees, by name
+ */
+ public function getTrees(): array
+ {
+ $this->initialize();
+
+ return self::treeEntries($this->entriesByType['tree']);
+ }
+
+ /**
+ * @return array An associative array name => [mode, object], a CommitReference being a Submodule
+ *
+ * @deprecated since gitonomy/gitlib 1.7, iterate the tree instead
*/
public function getEntries(): array
{
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, iterate the tree instead.', __METHOD__);
+
$this->initialize();
return $this->entries;
@@ -82,9 +133,13 @@ public function getEntries(): array
/**
* @return array An associative array of name => [mode, commit reference]
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use getSubmodules() instead
*/
public function getCommitReferenceEntries(): array
{
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "getSubmodules()" instead.', __METHOD__);
+
$this->initialize();
return $this->entriesByType['commit'];
@@ -92,9 +147,13 @@ public function getCommitReferenceEntries(): array
/**
* @return array An associative array of name => [mode, tree]
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use getTrees() instead
*/
public function getTreeEntries(): array
{
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "getTrees()" instead.', __METHOD__);
+
$this->initialize();
return $this->entriesByType['tree'];
@@ -102,9 +161,13 @@ public function getTreeEntries(): array
/**
* @return array An associative array of name => [mode, blob]
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use getBlobs() instead
*/
public function getBlobEntries(): array
{
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s()" method is deprecated, use "getBlobs()" instead.', __METHOD__);
+
$this->initialize();
return $this->entriesByType['blob'];
@@ -143,4 +206,19 @@ public function resolvePath($path)
return $element;
}
+
+ /**
+ * @param array $entries
+ *
+ * @return array
+ */
+ private static function treeEntries(array $entries): array
+ {
+ $treeEntries = [];
+ foreach ($entries as $name => [$mode, $object]) {
+ $treeEntries[$name] = new TreeEntry((string) $name, $mode, $object);
+ }
+
+ return $treeEntries;
+ }
}
diff --git a/src/Gitonomy/Git/TreeEntry.php b/src/Gitonomy/Git/TreeEntry.php
new file mode 100644
index 00000000..0c183a34
--- /dev/null
+++ b/src/Gitonomy/Git/TreeEntry.php
@@ -0,0 +1,46 @@
+
+ * (c) Julien DIDIER
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace Gitonomy\Git;
+
+/**
+ * An entry of a tree: a blob, a tree or a submodule, with its name and mode.
+ *
+ * @author Grégoire Pineau
+ */
+final class TreeEntry
+{
+ public function __construct(
+ private readonly string $name,
+ private readonly string $mode,
+ private readonly Blob|Tree|Submodule $object,
+ ) {
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * @return string The mode git stores, like "100644" for a file or "040000" for a tree
+ */
+ public function getMode(): string
+ {
+ return $this->mode;
+ }
+
+ public function getObject(): Blob|Tree|Submodule
+ {
+ return $this->object;
+ }
+}
diff --git a/src/Gitonomy/Git/Util/LegacyOverride.php b/src/Gitonomy/Git/Util/LegacyOverride.php
new file mode 100644
index 00000000..0eb406d2
--- /dev/null
+++ b/src/Gitonomy/Git/Util/LegacyOverride.php
@@ -0,0 +1,33 @@
+
+ * (c) Julien DIDIER
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace Gitonomy\Git\Util;
+
+/**
+ * Tells whether a subclass overrides a method that gitlib 1.x called itself,
+ * deprecated in 1.7: gitlib keeps calling the override, like before.
+ *
+ * @author Grégoire Pineau
+ *
+ * @internal To be removed in 2.0, with the deprecated methods
+ */
+final class LegacyOverride
+{
+ /**
+ * @param object|class-string $objectOrClass The instance, or the class of a static call
+ * @param class-string $class The class of gitlib declaring $method
+ */
+ public static function isOverridden(object|string $objectOrClass, string $class, string $method): bool
+ {
+ return $class !== (new \ReflectionMethod($objectOrClass, $method))->getDeclaringClass()->getName();
+ }
+}
diff --git a/src/Gitonomy/Git/Util/StringHelper.php b/src/Gitonomy/Git/Util/StringHelper.php
index d784b66b..5e652699 100644
--- a/src/Gitonomy/Git/Util/StringHelper.php
+++ b/src/Gitonomy/Git/Util/StringHelper.php
@@ -12,10 +12,14 @@
namespace Gitonomy\Git\Util;
+trigger_deprecation('gitonomy/gitlib', '1.7', 'The "%s" class is deprecated, use symfony/string instead: gitlib 2.0 reads the logs in UTF-8 only.', StringHelper::class);
+
/**
* Helper class to support language particularity.
*
* @author Alexandre Salomé
+ *
+ * @deprecated since gitonomy/gitlib 1.7, use symfony/string instead: gitlib 2.0 reads the logs in UTF-8 only
*/
class StringHelper
{
diff --git a/src/Gitonomy/Git/WorkingCopy.php b/src/Gitonomy/Git/WorkingCopy.php
index a94fbbb9..faf209ee 100644
--- a/src/Gitonomy/Git/WorkingCopy.php
+++ b/src/Gitonomy/Git/WorkingCopy.php
@@ -15,9 +15,13 @@
use Gitonomy\Git\Diff\Diff;
use Gitonomy\Git\Exception\InvalidArgumentException;
use Gitonomy\Git\Exception\LogicException;
+use Gitonomy\Git\Exception\ReferenceNotFoundException;
+use Gitonomy\Git\Reference\Branch;
/**
* @author Alexandre Salomé
+ *
+ * @final since gitonomy/gitlib 1.7
*/
class WorkingCopy
{
@@ -48,32 +52,49 @@ public function getUntrackedFiles()
return $lines;
}
- public function getDiffPending()
+ /**
+ * Returns the changes of the working tree, staged or not, since $revision.
+ *
+ * @throws ReferenceNotFoundException $revision names no commit
+ */
+ public function getDiff(Revision|string $revision = 'HEAD'): Diff
{
- $diff = Diff::parse($this->run('diff', ['-r', '-p', '--raw', '-m', '-M', '--full-index']));
- $diff->setRepository($this->repository);
+ $hash = ($revision instanceof Revision ? $revision : $this->repository->getRevision($revision))->getCommit()->getHash();
- return $diff;
+ return Diff::parse($this->run('diff', ['-r', '-p', '--raw', '-m', '-M', '--full-index', $hash]), $this->repository);
}
- public function getDiffStaged()
+ public function getDiffPending()
{
- $diff = Diff::parse($this->run('diff', ['-r', '-p', '--raw', '-m', '-M', '--full-index', '--staged']));
- $diff->setRepository($this->repository);
+ return Diff::parse($this->run('diff', ['-r', '-p', '--raw', '-m', '-M', '--full-index']), $this->repository);
+ }
- return $diff;
+ public function getDiffStaged()
+ {
+ return Diff::parse($this->run('diff', ['-r', '-p', '--raw', '-m', '-M', '--full-index', '--staged']), $this->repository);
}
/**
+ * Passing a branch to create as second argument is deprecated, as a local
+ * Branch object, which detaches HEAD, where 2.0 attaches HEAD to it.
+ *
* @return WorkingCopy
*/
public function checkout($revision, $branch = null)
{
+ if (null !== $branch) {
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'Passing a branch to create to "%s()" is deprecated, use "checkoutNewBranch()" instead.', __METHOD__);
+ } elseif ($revision instanceof Branch && $revision->isLocal()) {
+ trigger_deprecation('gitonomy/gitlib', '1.7', 'Passing a local "%s" to "%s()", which detaches HEAD, is deprecated, 2.0 attaches HEAD to the branch: pass its name, or its commit to detach HEAD.', Branch::class, __METHOD__);
+ }
+
$args = [];
if ($revision instanceof Commit) {
$args[] = $revision->getHash();
} elseif ($revision instanceof Reference) {
$args[] = $revision->getFullname();
+ } elseif ($revision instanceof Revision) {
+ $args[] = $revision->getRevision();
} elseif (is_string($revision)) {
$args[] = $revision;
} else {
@@ -89,6 +110,24 @@ public function checkout($revision, $branch = null)
return $this;
}
+ /**
+ * Creates the branch $branch at $startPoint, and checks it out, like
+ * `git checkout -b`.
+ *
+ * @return $this
+ *
+ * @throws ReferenceNotFoundException $startPoint names no commit
+ */
+ public function checkoutNewBranch(string $branch, Revision|string $startPoint = 'HEAD'): self
+ {
+ // Like in 2.0. git gets the name, which tells a remote branch, to track it.
+ ($startPoint instanceof Revision ? $startPoint : $this->repository->getRevision($startPoint))->getCommit();
+
+ $this->run('checkout', ['-b', $branch, $startPoint instanceof Revision ? $startPoint->getRevision() : $startPoint]);
+
+ return $this;
+ }
+
protected function run($command, array $args = [])
{
return $this->repository->run($command, $args);
diff --git a/tests/Gitonomy/Git/Tests/AbstractTest.php b/tests/Gitonomy/Git/Tests/AbstractTest.php
index 0b9a51b6..31379fd0 100644
--- a/tests/Gitonomy/Git/Tests/AbstractTest.php
+++ b/tests/Gitonomy/Git/Tests/AbstractTest.php
@@ -12,7 +12,7 @@
namespace Gitonomy\Git\Tests;
-use Gitonomy\Git\Admin;
+use Gitonomy\Git\Driver\Cli\CliDriver;
use Gitonomy\Git\Repository;
use PHPUnit\Framework\TestCase;
@@ -42,7 +42,7 @@ abstract class AbstractTest extends TestCase
public static function createEmptyRepository($bare = true)
{
$dir = self::createTempDir();
- $repository = Admin::init($dir, $bare, self::getOptions());
+ $repository = $bare ? Repository::initBare($dir, self::getDriver()) : Repository::init($dir, self::getDriver());
self::registerDeletion($repository);
return $repository;
@@ -78,10 +78,11 @@ public static function provideEmpty()
public static function createFoobarRepository($bare = true)
{
if (null === self::$localRepository) {
- self::$localRepository = Admin::cloneTo(self::createTempDir(), self::REPOSITORY_URL, $bare, self::getOptions());
+ self::$localRepository = Repository::cloneBareFrom(self::REPOSITORY_URL, self::createTempDir(), null, self::getDriver());
}
- $repository = self::$localRepository->cloneTo(self::createTempDir(), $bare, self::getOptions());
+ $url = self::$localRepository->getGitDir();
+ $repository = $bare ? Repository::cloneBareFrom($url, self::createTempDir(), null, self::getDriver()) : Repository::cloneFrom($url, self::createTempDir(), null, self::getDriver());
self::registerDeletion($repository);
return $repository;
@@ -100,7 +101,7 @@ public static function registerDeletion(Repository $repository)
}
/**
- * Created an empty directory and return path to it.
+ * Created an empty directory, deleted at the end of the tests, and return path to it.
*
* @return string a fullpath
*/
@@ -110,6 +111,10 @@ public static function createTempDir()
unlink($tmpDir);
mkdir($tmpDir);
+ register_shutdown_function(function () use ($tmpDir) {
+ self::deleteDir($tmpDir);
+ });
+
return $tmpDir;
}
@@ -120,6 +125,11 @@ public static function createTempDir()
*/
protected static function deleteDir($dir)
{
+ // Deleted already, by another shutdown function.
+ if (!file_exists($dir)) {
+ return;
+ }
+
$iterator = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS);
$iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::CHILD_FIRST);
foreach ($iterator as $file) {
@@ -148,4 +158,11 @@ protected static function getOptions()
'process_timeout' => 60,
];
}
+
+ protected static function getDriver(): CliDriver
+ {
+ $options = self::getOptions();
+
+ return new CliDriver($options['command'], $options['environment_variables'], $options['process_timeout']);
+ }
}
diff --git a/tests/Gitonomy/Git/Tests/AdminTest.php b/tests/Gitonomy/Git/Tests/AdminTest.php
index 0f7460fa..d0c1be34 100644
--- a/tests/Gitonomy/Git/Tests/AdminTest.php
+++ b/tests/Gitonomy/Git/Tests/AdminTest.php
@@ -16,9 +16,15 @@
use Gitonomy\Git\Exception\RuntimeException;
use Gitonomy\Git\Reference\Branch;
use Gitonomy\Git\Repository;
+use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
+/**
+ * @group legacy
+ */
class AdminTest extends AbstractTest
{
+ use ExpectDeprecationTrait;
+
private $tmpDir;
/**
@@ -39,6 +45,8 @@ public function tearDownTmpDir()
public function testBare()
{
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Admin::init()" method is deprecated, use "Gitonomy\Git\Repository::initBare()" instead.');
+
$repository = Admin::init($this->tmpDir, true, self::getOptions());
$objectDir = $this->tmpDir.'/objects';
@@ -52,6 +60,8 @@ public function testBare()
public function testNotBare()
{
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Admin::init()" method is deprecated, use "Gitonomy\Git\Repository::init()" instead.');
+
$repository = Admin::init($this->tmpDir, false, self::getOptions());
$objectDir = $this->tmpDir.'/.git/objects';
@@ -68,6 +78,8 @@ public function testNotBare()
*/
public function testClone($repository)
{
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Repository::cloneTo()" method is deprecated, use "Gitonomy\Git\Repository::'.($repository->isBare() ? 'cloneBareFrom' : 'cloneFrom').'()" with "getGitDir()" as URL instead.');
+
$newDir = self::createTempDir();
$new = $repository->cloneTo($newDir, $repository->isBare(), self::getOptions());
self::registerDeletion($new);
@@ -88,6 +100,8 @@ public function testClone($repository)
public function testCloneBranchBare()
{
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Admin::cloneBranchTo()" method is deprecated, use "Gitonomy\Git\Repository::cloneBareFrom()" with its "branch" argument instead, which takes the URL before the path.');
+
//we can't use AbstractText::createFoobarRepository()
//because it does not clone other branches than "master"
//so we test it directly against the remote repository
@@ -103,6 +117,8 @@ public function testCloneBranchBare()
public function testCloneBranchNotBare()
{
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Admin::cloneBranchTo()" method is deprecated, use "Gitonomy\Git\Repository::cloneFrom()" with its "branch" argument instead, which takes the URL before the path.');
+
//we can't use AbstractText::createFoobarRepository()
//because it does not clone other branches than "master"
//so we test it directly against remote repository
@@ -121,6 +137,8 @@ public function testCloneBranchNotBare()
*/
public function testMirror($repository)
{
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Admin::mirrorTo()" method is deprecated, use "Gitonomy\Git\Repository::mirrorFrom()" instead, which takes the URL before the path.');
+
$newDir = self::createTempDir();
$new = Admin::mirrorTo($newDir, $repository->getGitDir(), self::getOptions());
self::registerDeletion($new);
@@ -143,12 +161,16 @@ public function testMirror($repository)
*/
public function testCheckValidRepository($repository)
{
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Admin::isValidRepository()" method is deprecated, use "Gitonomy\Git\Repository::isValidRepository()" instead.');
+
$url = $repository->getGitDir();
$this->assertTrue(Admin::isValidRepository($url));
}
public function testCheckInvalidRepository()
{
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Admin::isValidRepository()" method is deprecated, use "Gitonomy\Git\Repository::isValidRepository()" instead.');
+
$url = $this->tmpDir.'/invalid.git';
mkdir($url);
@@ -160,6 +182,8 @@ public function testCheckInvalidRepository()
*/
public function testCheckValidRepositoryAndBranch($repository)
{
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Admin::isValidRepositoryAndBranch()" method is deprecated, use "Gitonomy\Git\Repository::isValidRepositoryAndBranch()" instead.');
+
$url = $repository->getGitDir();
$this->assertTrue(Admin::isValidRepositoryAndBranch($url, 'master'));
}
@@ -169,12 +193,16 @@ public function testCheckValidRepositoryAndBranch($repository)
*/
public function testCheckInvalidRepositoryAndBranch($repository)
{
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Admin::isValidRepositoryAndBranch()" method is deprecated, use "Gitonomy\Git\Repository::isValidRepositoryAndBranch()" instead.');
+
$url = $repository->getGitDir();
$this->assertFalse(Admin::isValidRepositoryAndBranch($url, 'invalid-branch-name'));
}
public function testExistingFile()
{
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Admin::init()" method is deprecated, use "Gitonomy\Git\Repository::initBare()" instead.');
+
$this->expectException(RuntimeException::class);
$file = $this->tmpDir.'/test';
@@ -185,6 +213,8 @@ public function testExistingFile()
public function testCloneRepository()
{
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Admin::cloneRepository()" method is deprecated, use "Gitonomy\Git\Repository::cloneFrom()", "Gitonomy\Git\Repository::cloneBareFrom()" or "Gitonomy\Git\Repository::mirrorFrom()" instead, which take the URL before the path; they support no other argument of "git clone".');
+
$newDir = self::createTempDir();
$args = [];
diff --git a/tests/Gitonomy/Git/Tests/BackwardCompatibilityTest.php b/tests/Gitonomy/Git/Tests/BackwardCompatibilityTest.php
new file mode 100644
index 00000000..6c4434bd
--- /dev/null
+++ b/tests/Gitonomy/Git/Tests/BackwardCompatibilityTest.php
@@ -0,0 +1,259 @@
+
+ * (c) Julien DIDIER
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace Gitonomy\Git\Tests;
+
+use Gitonomy\Git\Admin;
+use Gitonomy\Git\Blame;
+use Gitonomy\Git\Diff\Diff;
+use Gitonomy\Git\Diff\File;
+use Gitonomy\Git\Diff\FileChange;
+use Gitonomy\Git\Log;
+use Gitonomy\Git\PushReference;
+use Gitonomy\Git\Reference;
+use Gitonomy\Git\ReferenceBag;
+use Gitonomy\Git\Repository;
+
+/**
+ * 1.7 keeps working what worked with 1.6: the named arguments, the classes
+ * extending the ones of gitlib, and the methods they override.
+ */
+class BackwardCompatibilityTest extends AbstractTest
+{
+ public function testEmptyOptions()
+ {
+ $source = self::createFoobarRepository(false);
+
+ $repository = new Repository($source->getWorkingDir(), []);
+
+ $this->assertSame($source->getWorkingDir(), $repository->getWorkingDir());
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testNamedArguments()
+ {
+ $source = self::createFoobarRepository(false);
+ $repository = new Repository($source->getGitDir(), options: ['working_dir' => $source->getWorkingDir()]);
+
+ $this->assertSame($source->getWorkingDir(), $repository->getWorkingDir());
+ $this->assertCount(1, $repository->getDiff(revisions: self::BEFORE_LONGFILE_COMMIT.'..'.self::LONGFILE_COMMIT)->getFiles());
+ $this->assertCount(3, $repository->getBlame(self::LONGFILE_COMMIT, 'README.md', lineRange: 5));
+ $this->assertCount(3, new Blame($repository, $repository->getRevision(self::LONGFILE_COMMIT), 'README.md', lineRange: 5));
+
+ $references = $repository->getReferences();
+ $references->createBranch('foobar', commitHash: self::INITIAL_COMMIT);
+ $references->createTag('foobar', commitHash: self::INITIAL_COMMIT);
+ $this->assertCount(2, $references->resolve(hash: self::INITIAL_COMMIT));
+ $this->assertCount(1, $references->resolveBranches(hash: self::INITIAL_COMMIT));
+ $this->assertCount(1, $references->resolveTags(hash: self::INITIAL_COMMIT));
+
+ $this->assertSame('refs/heads/foobar', (new PushReference($repository, reference: 'refs/heads/foobar', before: self::INITIAL_COMMIT, after: self::LONGFILE_COMMIT))->getFullname());
+
+ $data = $repository->getCommit(self::LONGFILE_COMMIT)->getDiff()->toArray();
+ $this->assertCount(1, Diff::fromArray(array: $data)->getFiles());
+ $this->assertSame('long.php', File::fromArray(array: $data['files'][0])->getName());
+ $this->assertCount(1, Diff::parse(rawDiff: $data['rawDiff'])->getFiles());
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testSubclassesOverridingMethodsWithTheSignatureOf16()
+ {
+ $repository = self::createFoobarRepository();
+ $legacy = new LegacyRepository($repository->getGitDir());
+
+ $this->assertCount(1, $legacy->getDiff(self::BEFORE_LONGFILE_COMMIT.'..'.self::LONGFILE_COMMIT)->getFiles());
+ $this->assertCount(3, $legacy->getBlame(self::LONGFILE_COMMIT, 'README.md', 5));
+
+ $data = $repository->getCommit(self::LONGFILE_COMMIT)->getDiff()->toArray();
+ $this->assertCount(1, LegacyDiff::parse($data['rawDiff'])->getFiles());
+ $this->assertCount(1, LegacyDiff::fromArray($data)->getFiles());
+ $this->assertSame('long.php', LegacyFile::fromArray($data['files'][0])->getName());
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testOverriddenAdminCloneRepository()
+ {
+ LegacyAdmin::$args = [];
+
+ $repository = LegacyAdmin::cloneTo(self::createTempDir(), self::createFoobarRepository()->getGitDir(), false);
+
+ $this->assertSame([[]], LegacyAdmin::$args);
+ $this->assertFalse($repository->isBare());
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testOverriddenReferenceBagUpdate()
+ {
+ $references = new LegacyReferenceBag(self::createFoobarRepository());
+
+ $references->createBranch('foobar', self::INITIAL_COMMIT);
+ $references->createTag('foobar', self::INITIAL_COMMIT);
+
+ $this->assertSame(['refs/heads/foobar', 'refs/tags/foobar'], $references->updated);
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testOverriddenLogSetLimit()
+ {
+ $log = new LegacyLog(self::createFoobarRepository(), self::LONGFILE_COMMIT, null, null, 5);
+
+ $this->assertSame(self::LONGFILE_COMMIT, $log->getSingleCommit()->getHash());
+ $this->assertSame([5], $log->limits);
+ $this->assertSame(5, $log->getLimit());
+ }
+
+ public function testOverriddenPushReferenceMethods()
+ {
+ $reference = new LegacyPushReference(self::createFoobarRepository(), 'refs/heads/foobar', self::BEFORE_LONGFILE_COMMIT, self::LONGFILE_COMMIT);
+
+ // The overrides say it creates the reference: neither forced, nor fast-forward.
+ $this->assertFalse($reference->isForce());
+ $this->assertFalse($reference->isFastForward());
+ $this->assertSame(self::LONGFILE_COMMIT, $reference->getRange());
+ // getRevision() gives the commits of the log, from the first one.
+ $this->assertCount(8, $reference->getLog()->getCommits());
+ }
+
+ public function testOverriddenFileChangeGetCount()
+ {
+ $file = new File('a.txt', 'a.txt', '100644', '100644', 'abc', 'def', false, false, [new LegacyFileChange(1, 1, 1, 1, [])]);
+
+ $this->assertSame(42, $file->getAdditions());
+ $this->assertSame(42, $file->getDeletions());
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testOverriddenFileSetRepository()
+ {
+ $repository = self::createFoobarRepository();
+ $file = new LegacyFile('a.txt', 'a.txt', '100644', '100644', 'abc', 'def', false);
+
+ (new Diff([$file], ''))->setRepository($repository);
+
+ $this->assertSame($repository, $file->repositorySet);
+ }
+}
+
+class LegacyRepository extends Repository
+{
+ public function getDiff($revisions)
+ {
+ return parent::getDiff($revisions);
+ }
+
+ public function getBlame($revision, $file, $lineRange = null)
+ {
+ return parent::getBlame($revision, $file, $lineRange);
+ }
+}
+
+class LegacyDiff extends Diff
+{
+ public static function parse($rawDiff)
+ {
+ return parent::parse($rawDiff);
+ }
+
+ public static function fromArray(array $array)
+ {
+ return parent::fromArray($array);
+ }
+}
+
+class LegacyFile extends File
+{
+ public $repositorySet;
+
+ public static function fromArray(array $array)
+ {
+ return parent::fromArray($array);
+ }
+
+ public function setRepository(Repository $repository)
+ {
+ $this->repositorySet = $repository;
+ }
+}
+
+class LegacyFileChange extends FileChange
+{
+ public function getCount($type)
+ {
+ return 42;
+ }
+}
+
+class LegacyAdmin extends Admin
+{
+ public static $args = [];
+
+ public static function cloneRepository($path, $url, array $args = [], array $options = [])
+ {
+ self::$args[] = $args;
+
+ return parent::cloneRepository($path, $url, $args, $options);
+ }
+}
+
+class LegacyReferenceBag extends ReferenceBag
+{
+ public $updated = [];
+
+ public function update(Reference $reference)
+ {
+ $this->updated[] = $reference->getFullname();
+
+ return parent::update($reference);
+ }
+}
+
+class LegacyLog extends Log
+{
+ public $limits = [];
+
+ public function setLimit($limit)
+ {
+ $this->limits[] = $limit;
+
+ return parent::setLimit($limit);
+ }
+}
+
+class LegacyPushReference extends PushReference
+{
+ public function isCreate()
+ {
+ return true;
+ }
+
+ public function isDelete()
+ {
+ return false;
+ }
+
+ public function getRevision()
+ {
+ return $this->getAfter();
+ }
+}
diff --git a/tests/Gitonomy/Git/Tests/BlameTest.php b/tests/Gitonomy/Git/Tests/BlameTest.php
index 6d92547d..d4735e8d 100644
--- a/tests/Gitonomy/Git/Tests/BlameTest.php
+++ b/tests/Gitonomy/Git/Tests/BlameTest.php
@@ -12,8 +12,16 @@
namespace Gitonomy\Git\Tests;
+use Gitonomy\Git\Blame\Line;
+use Gitonomy\Git\Exception\InvalidArgumentException;
+use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
+
class BlameTest extends AbstractTest
{
+ use ExpectDeprecationTrait;
+
+ const README_COMMIT = 'cfba9c69819a96df0635e2e7ddc86da20f954db6';
+
/**
* @dataProvider provideFoobar
*/
@@ -33,8 +41,102 @@ public function testBlame($repository)
/**
* @dataProvider provideFoobar
*/
+ public function testHunks($repository)
+ {
+ $hunks = $repository->getBlame(self::LONGFILE_COMMIT, 'README.md')->getHunks();
+
+ $this->assertCount(3, $hunks);
+ $this->assertSame(self::INITIAL_COMMIT, $hunks[0]->getCommit()->getHash());
+ $this->assertSame([1, 2, 3], array_keys($hunks[0]->getLines()));
+ $this->assertSame(self::README_COMMIT, $hunks[1]->getCommit()->getHash());
+ $this->assertSame([4, 5, 6], array_keys($hunks[1]->getLines()));
+ $this->assertSame([7], array_keys($hunks[2]->getLines()));
+
+ $line = $hunks[2]->getLines()[7];
+ $this->assertSame(7, $line->getNumber());
+ // The line 5 of the file in the initial commit.
+ $this->assertSame(5, $line->getOriginalNumber());
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ */
+ public function testRangeOfLines($repository)
+ {
+ $blame = $repository->getBlame(self::LONGFILE_COMMIT, 'README.md', 4, 6);
+
+ $this->assertCount(3, $blame);
+ $this->assertSame([4, 5, 6], array_map(function (Line $line) {
+ return $line->getNumber();
+ }, array_values($blame->getLines())));
+
+ $hunks = $blame->getHunks();
+ $this->assertCount(1, $hunks);
+ // Numbered like the file, unlike getLines(), which numbers them from 1.
+ $this->assertSame([4, 5, 6], array_keys($hunks[0]->getLines()));
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ */
+ public function testRangeOfLinesWithoutLastLine($repository)
+ {
+ $this->assertCount(3, $repository->getBlame(self::LONGFILE_COMMIT, 'README.md', 5));
+ $this->assertCount(2, $repository->getBlame(self::LONGFILE_COMMIT, 'README.md', null, 2));
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ */
+ public function testRangeOfLinesInReverse($repository)
+ {
+ $this->assertCount(3, $repository->getBlame(self::LONGFILE_COMMIT, 'README.md', 6, 4));
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ */
+ public function testRangeOfLinesFromZero($repository)
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Invalid range of lines "0,3": the lines are numbered from 1.');
+
+ $repository->getBlame(self::LONGFILE_COMMIT, 'README.md', 0, 3);
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ *
+ * @group legacy
+ */
+ public function testRangeOfLinesAsAString($repository)
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: Passing a range of lines as a string to "Gitonomy\Git\Repository::getBlame()" or "Gitonomy\Git\Blame::__construct()" is deprecated, pass the first and the last lines instead.');
+
+ $this->assertCount(3, $repository->getBlame(self::LONGFILE_COMMIT, 'README.md', '4,+3'));
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ *
+ * @group legacy
+ */
+ public function testLineGetLine($repository)
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Blame\Line::getLine()" method is deprecated, use "getOriginalNumber()" instead.');
+
+ $this->assertSame('5', $repository->getBlame(self::LONGFILE_COMMIT, 'README.md')->getLine(7)->getLine());
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ *
+ * @group legacy
+ */
public function testGroupedBlame($repository)
{
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Blame::getGroupedLines()" method is deprecated, use "getHunks()" instead.');
+
$blame = $repository->getBlame(self::LONGFILE_COMMIT, 'README.md')->getGroupedLines();
$this->assertCount(3, $blame);
diff --git a/tests/Gitonomy/Git/Tests/BlobTest.php b/tests/Gitonomy/Git/Tests/BlobTest.php
index 431a0a46..5cb51a74 100644
--- a/tests/Gitonomy/Git/Tests/BlobTest.php
+++ b/tests/Gitonomy/Git/Tests/BlobTest.php
@@ -49,7 +49,7 @@ public function testNotExisting($repository)
{
$this->expectException(RuntimeException::class);
- $blob = $repository->getBlob('foobar');
+ $blob = $repository->getBlob('deadbeefdeadbeefdeadbeefdeadbeefdeadbeef');
$blob->getContent();
}
diff --git a/tests/Gitonomy/Git/Tests/CommitTest.php b/tests/Gitonomy/Git/Tests/CommitTest.php
index 7d094648..ba74c7ac 100644
--- a/tests/Gitonomy/Git/Tests/CommitTest.php
+++ b/tests/Gitonomy/Git/Tests/CommitTest.php
@@ -16,11 +16,16 @@
use Gitonomy\Git\Diff\Diff;
use Gitonomy\Git\Exception\InvalidArgumentException;
use Gitonomy\Git\Exception\ReferenceNotFoundException;
+use Gitonomy\Git\Reference\Branch;
use Gitonomy\Git\Repository;
use Gitonomy\Git\Tree;
+use Gitonomy\Git\Util\StringHelper;
+use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
class CommitTest extends AbstractTest
{
+ use ExpectDeprecationTrait;
+
/**
* @dataProvider provideFoobar
*/
@@ -229,9 +234,13 @@ public function testGetSignedMessage($repository)
/**
* @dataProvider provideFoobar
+ *
+ * @group legacy
*/
public function testGetShortMessage($repository)
{
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Commit::getShortMessage()" method is deprecated, use symfony/string instead: "u($commit->getSubjectMessage())->truncate($length, $separator, !$preserve)".');
+
// tests with a multi-line message
$commit = $repository->getCommit(self::LONGMESSAGE_COMMIT);
@@ -264,9 +273,12 @@ public function testGetBodyMessage($repository)
/**
* @dataProvider provideFoobar
+ *
+ * @group legacy
*/
public function testGetIncludingBranchesException($repository)
{
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: Passing arguments to "Gitonomy\Git\Commit::getIncludingBranches()" is deprecated, filter the branches it returns with "Gitonomy\Git\Reference\Branch::isLocal()" or "isRemote()" instead.');
$this->expectException(InvalidArgumentException::class);
$commit = $repository->getCommit(self::INITIAL_COMMIT);
@@ -281,6 +293,24 @@ public function testGetIncludingBranches($repository)
{
$commit = $repository->getCommit(self::INITIAL_COMMIT);
+ $branches = $commit->getIncludingBranches();
+ $this->assertCount(count($repository->getReferences()->getBranches()), $branches);
+ $this->assertCount(count($repository->getReferences()->getLocalBranches()), array_filter($branches, function (Branch $branch) {
+ return $branch->isLocal();
+ }));
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ *
+ * @group legacy
+ */
+ public function testGetIncludingBranchesWithArguments($repository)
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: Passing arguments to "Gitonomy\Git\Commit::getIncludingBranches()" is deprecated, filter the branches it returns with "Gitonomy\Git\Reference\Branch::isLocal()" or "isRemote()" instead.');
+
+ $commit = $repository->getCommit(self::INITIAL_COMMIT);
+
$branches = $commit->getIncludingBranches(true, false);
$this->assertCount(count($repository->getReferences()->getLocalBranches()), $branches);
@@ -323,4 +353,42 @@ public function testEncoding($repository)
$this->assertEquals('contribute to AUTHORS file', $commit->getSubjectMessage());
}
+
+ /**
+ * @dataProvider provideFoobar
+ *
+ * @group legacy
+ */
+ public function testGetFixedShortHash($repository)
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Commit::getFixedShortHash()" method is deprecated, use "substr($commit->getHash(), 0, $length)" instead.');
+
+ $this->assertSame('74acd0', $repository->getCommit(self::INITIAL_COMMIT)->getFixedShortHash());
+ }
+
+ /**
+ * @runInSeparateProcess
+ *
+ * @group legacy
+ */
+ public function testStringHelperIsDeprecated()
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Util\StringHelper" class is deprecated, use symfony/string instead: gitlib 2.0 reads the logs in UTF-8 only.');
+
+ $this->assertSame(3, StringHelper::strlen('été'));
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ *
+ * @group legacy
+ */
+ public function testConstructorWithData($repository)
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: Passing data to "Gitonomy\\Git\\Commit::__construct()" is deprecated, the constructor is internal in 2.0, and takes a "CommitData" instead of an array: get the commit from "Gitonomy\\Git\\Repository::getCommit()".');
+
+ $commit = new Commit($repository, self::INITIAL_COMMIT, ['subjectMessage' => 'Hello']);
+
+ $this->assertSame('Hello', $commit->getSubjectMessage());
+ }
}
diff --git a/tests/Gitonomy/Git/Tests/DiffTest.php b/tests/Gitonomy/Git/Tests/DiffTest.php
index 9703bdde..2b9aac7b 100644
--- a/tests/Gitonomy/Git/Tests/DiffTest.php
+++ b/tests/Gitonomy/Git/Tests/DiffTest.php
@@ -13,11 +13,19 @@
namespace Gitonomy\Git\Tests;
use Gitonomy\Git\Diff\Diff;
+use Gitonomy\Git\Diff\DiffLine;
+use Gitonomy\Git\Diff\DiffLineType;
use Gitonomy\Git\Diff\File;
+use Gitonomy\Git\Diff\FileChange;
+use Gitonomy\Git\Exception\InvalidArgumentException;
+use Gitonomy\Git\Exception\ReferenceNotFoundException;
use Gitonomy\Git\Repository;
+use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
class DiffTest extends AbstractTest
{
+ use ExpectDeprecationTrait;
+
const DELETE_COMMIT = '519d5693c72c925cd59205d9f11e9fa1d550028b';
const CREATE_COMMIT = 'e6fa3c792facc06faa049a6938c84c411954deb5';
const RENAME_COMMIT = '6640e0ef31518054847a1876328e26ee64083e0a';
@@ -109,7 +117,7 @@ public function testGetFiles_Rename($repository)
$this->assertTrue($files[0]->isRename());
$this->assertFalse($files[0]->isDeletion());
$this->assertFalse($files[0]->isCreation());
- $this->assertFalse($files[0]->isChangeMode());
+ $this->assertFalse($files[0]->isModeChange());
}
/**
@@ -122,7 +130,7 @@ public function testGetFiles_Changemode($repository)
$this->assertCount(1, $files, '1 files modified');
$this->assertTrue($files[0]->isModification());
- $this->assertTrue($files[0]->isChangeMode());
+ $this->assertTrue($files[0]->isModeChange());
$this->assertFalse($files[0]->isDeletion());
$this->assertFalse($files[0]->isCreation());
$this->assertFalse($files[0]->isRename());
@@ -153,14 +161,13 @@ public function testWorksWithUmlauts($repository)
$this->assertSame('file_with_umlauts_\303\244\303\266\303\274', $files[0]->getNewName());
}
+ /**
+ * @group legacy
+ */
public function testDeleteFileWithoutRaw()
{
- $deprecationCalled = false;
- $self = $this;
- set_error_handler(function (int $errno, string $errstr) use ($self, &$deprecationCalled): void {
- $deprecationCalled = true;
- $self->assertSame('Using Diff::parse without raw information is deprecated. See https://github.com/gitonomy/gitlib/issues/227.', $errstr);
- }, E_USER_DEPRECATED);
+ // Not silenced, like in 1.6.
+ $this->expectDeprecation('Unsilenced deprecation: Using Diff::parse without raw information is deprecated. See https://github.com/gitonomy/gitlib/issues/227.');
$diff = Diff::parse(<<<'DIFF'
diff --git a/test b/test
@@ -170,24 +177,20 @@ public function testDeleteFileWithoutRaw()
DIFF);
$firstFile = $diff->getFiles()[0];
- restore_exception_handler();
-
- $this->assertTrue($deprecationCalled);
$this->assertFalse($firstFile->isCreation());
$this->assertTrue($firstFile->isDeletion());
- $this->assertFalse($firstFile->isChangeMode());
+ $this->assertFalse($firstFile->isModeChange());
$this->assertSame('e69de29bb2d1d6434b8b29ae775ad8c2e48c5391', $firstFile->getOldIndex());
$this->assertNull($firstFile->getNewIndex());
}
+ /**
+ * @group legacy
+ */
public function testModeChangeFileWithoutRaw()
{
- $deprecationCalled = false;
- $self = $this;
- set_error_handler(function (int $errno, string $errstr) use ($self, &$deprecationCalled): void {
- $deprecationCalled = true;
- $self->assertSame('Using Diff::parse without raw information is deprecated. See https://github.com/gitonomy/gitlib/issues/227.', $errstr);
- }, E_USER_DEPRECATED);
+ // Not silenced, like in 1.6.
+ $this->expectDeprecation('Unsilenced deprecation: Using Diff::parse without raw information is deprecated. See https://github.com/gitonomy/gitlib/issues/227.');
$diff = Diff::parse(<<<'DIFF'
diff --git a/a.out b/a.out
@@ -197,23 +200,15 @@ public function testModeChangeFileWithoutRaw()
DIFF);
$firstFile = $diff->getFiles()[0];
- restore_exception_handler();
-
- $this->assertTrue($deprecationCalled);
$this->assertFalse($firstFile->isCreation());
$this->assertFalse($firstFile->isDeletion());
- $this->assertTrue($firstFile->isChangeMode());
+ $this->assertTrue($firstFile->isModeChange());
$this->assertSame('', $firstFile->getOldIndex());
$this->assertSame('', $firstFile->getNewIndex());
}
public function testModeChangeFileWithRaw()
{
- $deprecationCalled = false;
- set_error_handler(function (int $errno, string $errstr) use (&$deprecationCalled): void {
- $deprecationCalled = true;
- }, E_USER_DEPRECATED);
-
$diff = Diff::parse(<<<'DIFF'
:100644 100755 d1af4b23d0cc9313e5b2d3ef2fb9696c94afaa81 d1af4b23d0cc9313e5b2d3ef2fb9696c94afaa81 M testfile
:100644 100755 d1af4b23d0cc9313e5b2d3ef2fb9696c94afaa82 d1af4b23d0cc9313e5b2d3ef2fb9696c94afaa82 M testfile2
@@ -230,19 +225,15 @@ public function testModeChangeFileWithRaw()
$firstFile = $files[0];
$secondFile = $files[1];
- restore_exception_handler();
-
- $this->assertFalse($deprecationCalled);
-
$this->assertFalse($firstFile->isCreation());
$this->assertFalse($firstFile->isDeletion());
- $this->assertTrue($firstFile->isChangeMode());
+ $this->assertTrue($firstFile->isModeChange());
$this->assertSame('d1af4b23d0cc9313e5b2d3ef2fb9696c94afaa81', $firstFile->getOldIndex());
$this->assertSame('d1af4b23d0cc9313e5b2d3ef2fb9696c94afaa81', $firstFile->getNewIndex());
$this->assertFalse($secondFile->isCreation());
$this->assertFalse($secondFile->isDeletion());
- $this->assertTrue($secondFile->isChangeMode());
+ $this->assertTrue($secondFile->isModeChange());
$this->assertSame('d1af4b23d0cc9313e5b2d3ef2fb9696c94afaa82', $secondFile->getOldIndex());
$this->assertSame('d1af4b23d0cc9313e5b2d3ef2fb9696c94afaa82', $secondFile->getNewIndex());
}
@@ -250,8 +241,7 @@ public function testModeChangeFileWithRaw()
public function testThrowErrorOnBlobGetWithoutIndex()
{
$repository = $this->getMockBuilder(Repository::class)->disableOriginalConstructor()->getMock();
- $file = new File('oldName', 'newName', '100755', '100644', '', '', false);
- $file->setRepository($repository);
+ $file = new File('oldName', 'newName', '100755', '100644', '', '', false, false, [], $repository);
try {
$file->getOldBlob();
@@ -267,13 +257,19 @@ public function testThrowErrorOnBlobGetWithoutIndex()
$this->assertFalse($file->isCreation());
$this->assertFalse($file->isDeletion());
- $this->assertTrue($file->isChangeMode());
+ $this->assertTrue($file->isModeChange());
$this->assertSame('', $file->getOldIndex());
$this->assertSame('', $file->getNewIndex());
}
+ /**
+ * @group legacy
+ */
public function testEmptyNewFile()
{
+ // Not silenced, like in 1.6.
+ $this->expectDeprecation('Unsilenced deprecation: Using Diff::parse without raw information is deprecated. See https://github.com/gitonomy/gitlib/issues/227.');
+
$diff = Diff::parse("diff --git a/test b/test\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391\n");
$firstFile = $diff->getFiles()[0];
@@ -283,8 +279,14 @@ public function testEmptyNewFile()
$this->assertNull($firstFile->getOldName());
}
+ /**
+ * @group legacy
+ */
public function testEmptyOldFile()
{
+ // Not silenced, like in 1.6.
+ $this->expectDeprecation('Unsilenced deprecation: Using Diff::parse without raw information is deprecated. See https://github.com/gitonomy/gitlib/issues/227.');
+
$diff = Diff::parse("diff --git a/test b/test\ndeleted file mode 100644\nindex e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000\n");
$firstFile = $diff->getFiles()[0];
@@ -293,4 +295,226 @@ public function testEmptyOldFile()
$this->assertNull($firstFile->getNewName());
$this->assertSame('test', $firstFile->getOldName());
}
+
+ /**
+ * @dataProvider provideFoobar
+ */
+ public function testParseWithTheRepository($repository)
+ {
+ $raw = $repository->run('diff-tree', ['-r', '-p', '--raw', '-m', '-M', '--no-commit-id', '--full-index', self::CREATE_COMMIT]);
+
+ $file = Diff::parse($raw, $repository)->getFiles()[0];
+
+ $this->assertSame($repository, $file->getRepository());
+ $this->assertSame("getNewBlob()->getContent());
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ */
+ public function testFromArrayWithTheRepository($repository)
+ {
+ $data = $repository->getCommit(self::CREATE_COMMIT)->getDiff()->toArray();
+
+ $diff = Diff::fromArray($data, $repository);
+
+ $this->verifyCreateCommitDiff($diff);
+ $this->assertSame($repository, $diff->getFiles()[0]->getRepository());
+ $this->assertSame($repository, File::fromArray($data['files'][0], $repository)->getRepository());
+ }
+
+ public function testConstructorTakesTheChangesAndTheRepository()
+ {
+ $repository = $this->getMockBuilder(Repository::class)->disableOriginalConstructor()->getMock();
+ $change = new FileChange(1, 1, 1, 2, [new DiffLine(DiffLineType::Context, 'a'), new DiffLine(DiffLineType::Addition, 'b')]);
+
+ $file = new File('a.txt', 'a.txt', '100644', '100644', 'abc', 'def', false, false, [$change], $repository);
+
+ $this->assertSame([$change], $file->getChanges());
+ $this->assertSame(1, $file->getAdditions());
+ $this->assertSame(0, $file->getDeletions());
+ $this->assertSame($repository, $file->getRepository());
+ $this->assertFalse($file->isCopy());
+ }
+
+ public function testCopy()
+ {
+ $file = new File('a.txt', 'b.txt', '100644', '100644', 'abc', 'abc', false, true);
+
+ $this->assertTrue($file->isCopy());
+ $this->assertFalse($file->isRename());
+ // Like in 1.6, toArray() has no "is_copy" key, which 2.0 adds: fromArray() reads it.
+ $this->assertArrayNotHasKey('is_copy', $file->toArray());
+ $this->assertTrue(File::fromArray(['is_copy' => true] + $file->toArray())->isCopy());
+ }
+
+ public function testFileChangeCountsTheLines()
+ {
+ $change = new FileChange(1, 2, 1, 2, [new DiffLine(DiffLineType::Deletion, 'a'), new DiffLine(DiffLineType::Addition, 'b'), new DiffLine(DiffLineType::Addition, 'c'), new DiffLine(DiffLineType::Context, 'd')]);
+
+ $this->assertSame(2, $change->getAdditions());
+ $this->assertSame(1, $change->getDeletions());
+ }
+
+ public function testFileChangeTakesDiffLines()
+ {
+ $lines = [new DiffLine(DiffLineType::Deletion, 'a'), new DiffLine(DiffLineType::Addition, 'b'), new DiffLine(DiffLineType::Context, 'c')];
+
+ $change = new FileChange(1, 2, 1, 2, $lines);
+
+ $this->assertEquals($lines, $change->getDiffLines());
+ $this->assertSame([[-1, 'a'], [1, 'b'], [0, 'c']], $change->toArray()['lines']);
+ $this->assertEquals($lines, FileChange::fromArray($change->toArray())->getDiffLines());
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ */
+ public function testGetDiffLines($repository)
+ {
+ $change = $repository->getCommit(self::LONGFILE_COMMIT)->getDiff()->getFiles()[0]->getChanges()[0];
+
+ $lines = $change->getDiffLines();
+
+ $this->assertCount($change->getRangeNewCount(), $lines);
+ $this->assertSame(DiffLineType::Addition, $lines[0]->getType());
+ $this->assertSame(array_map(static fn (DiffLine $line): array => [$line->getType()->value, $line->getContent()], $lines), $change->toArray()['lines']);
+ }
+
+ public function testGetDiffLinesThrowsForAnUnknownType()
+ {
+ $change = FileChange::fromArray(['range_old_start' => 1, 'range_old_count' => 1, 'range_new_start' => 1, 'range_new_count' => 1, 'lines' => [[5, 'a']]]);
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Unknown type of line 5: the types are 1 (an addition), 0 (a context line) and -1 (a deletion).');
+
+ $change->getDiffLines();
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testFileChangeLinesAsArrays()
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: Passing the lines to "Gitonomy\Git\Diff\FileChange::__construct()" as arrays is deprecated, pass "Gitonomy\Git\Diff\DiffLine" objects instead.');
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Diff\FileChange::getLines()" method is deprecated, use "getDiffLines()" instead.');
+
+ $change = new FileChange(1, 1, 1, 2, [[FileChange::LINE_CONTEXT, 'a'], [FileChange::LINE_ADD, 'b']]);
+
+ $this->assertSame([[0, 'a'], [1, 'b']], $change->getLines());
+ $this->assertEquals([new DiffLine(DiffLineType::Context, 'a'), new DiffLine(DiffLineType::Addition, 'b')], $change->getDiffLines());
+ $this->assertSame(1, $change->getAdditions());
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ */
+ public function testGetDiffBetweenTwoRevisions($repository)
+ {
+ $diff = $repository->getDiff(self::BEFORE_LONGFILE_COMMIT, $repository->getCommit(self::LONGFILE_COMMIT));
+
+ $this->assertSame($repository->getCommit(self::LONGFILE_COMMIT)->getDiff()->toArray()['files'], $diff->toArray()['files']);
+ $this->assertSame($repository, $diff->getFiles()[0]->getRepository());
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ */
+ public function testGetDiffRefusesAListOfRevisionsWithTo($repository)
+ {
+ $this->expectException(InvalidArgumentException::class);
+
+ $repository->getDiff([self::BEFORE_LONGFILE_COMMIT], self::LONGFILE_COMMIT);
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ *
+ * @group legacy
+ */
+ public function testGetDiffWithOneArgument($repository)
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: Calling "Gitonomy\Git\Repository::getDiff()" with one argument is deprecated, pass the revisions to compare as first and second arguments, or use "Gitonomy\Git\WorkingCopy::getDiff()" to compare a revision with the working tree.');
+
+ $diff = $repository->getDiff(self::BEFORE_LONGFILE_COMMIT.'..'.self::LONGFILE_COMMIT);
+
+ $this->assertSame($repository->getDiff(self::BEFORE_LONGFILE_COMMIT, self::LONGFILE_COMMIT)->toArray(), $diff->toArray());
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ *
+ * @group legacy
+ */
+ public function testLogGetDiff($repository)
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Log::getDiff()" method is deprecated, use "Gitonomy\Git\Repository::getDiff()" instead.');
+
+ $diff = $repository->getLog(self::BEFORE_LONGFILE_COMMIT.'..'.self::LONGFILE_COMMIT)->getDiff();
+
+ $this->assertSame($repository->getDiff(self::BEFORE_LONGFILE_COMMIT, self::LONGFILE_COMMIT)->toArray(), $diff->toArray());
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testSetRepository()
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Diff\Diff::setRepository()" method is deprecated, the diffs gitlib returns have their repository already.');
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Diff\File::setRepository()" method is deprecated, the diffs gitlib returns have their repository already.');
+
+ $repository = $this->getMockBuilder(Repository::class)->disableOriginalConstructor()->getMock();
+ $diff = new Diff([new File('a.txt', 'a.txt', '100644', '100644', 'abc', 'def', false)], '');
+ $diff->setRepository($repository);
+
+ $this->assertSame($repository, $diff->getFiles()[0]->getRepository());
+
+ $file = new File('a.txt', 'a.txt', '100644', '100644', 'abc', 'def', false);
+ $file->setRepository($repository);
+
+ $this->assertSame($repository, $file->getRepository());
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testAddChange()
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Diff\File::addChange()" method is deprecated, give the changes to the constructor instead.');
+
+ $file = new File('a.txt', 'a.txt', '100644', '100644', 'abc', 'def', false);
+ $change = new FileChange(1, 1, 1, 1, [new DiffLine(DiffLineType::Addition, 'a')]);
+ $file->addChange($change);
+
+ $this->assertSame([$change], $file->getChanges());
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testDeprecatedNames()
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Diff\File::isChangeMode()" method is deprecated, use "isModeChange()" instead.');
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Diff\File::isDelete()" method is deprecated, use "isDeletion()" instead.');
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Diff\File::getAnchor()" method is deprecated, use "substr($file->getNewIndex(), 0, 12)" instead.');
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Diff\FileChange::getCount()" method is deprecated, use "getAdditions()" or "getDeletions()" instead.');
+
+ $file = new File('a.txt', null, '100755', null, 'abcdef0123456789', null, false);
+
+ $this->assertFalse($file->isChangeMode());
+ $this->assertTrue($file->isDelete());
+ $this->assertSame('', $file->getAnchor());
+ $this->assertSame('0123456789ab', (new File('a.txt', 'a.txt', '100644', '100644', 'abc', '0123456789abcdef', false))->getAnchor());
+ $this->assertSame(1, (new FileChange(1, 1, 1, 1, [new DiffLine(DiffLineType::Addition, 'a')]))->getCount(DiffLineType::Addition->value));
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ */
+ public function testGetDiffThrowsForAnUnknownRevision($repository)
+ {
+ $this->expectException(ReferenceNotFoundException::class);
+
+ $repository->getDiff(self::LONGFILE_COMMIT, 'unknown');
+ }
}
diff --git a/tests/Gitonomy/Git/Tests/LogTest.php b/tests/Gitonomy/Git/Tests/LogTest.php
index a02184dd..38ac22cf 100644
--- a/tests/Gitonomy/Git/Tests/LogTest.php
+++ b/tests/Gitonomy/Git/Tests/LogTest.php
@@ -12,10 +12,84 @@
namespace Gitonomy\Git\Tests;
+use Gitonomy\Git\Exception\InvalidArgumentException;
use Gitonomy\Git\Parser\LogParser;
+use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
class LogTest extends AbstractTest
{
+ use ExpectDeprecationTrait;
+
+ /**
+ * @dataProvider provideFoobar
+ */
+ public function testWithOffsetAndLimit($repository)
+ {
+ $log = $repository->getLog(self::LONGFILE_COMMIT);
+
+ $page = $log->withOffset(1)->withLimit(1);
+
+ $this->assertSame([self::BEFORE_LONGFILE_COMMIT], array_map(function ($commit) {
+ return $commit->getHash();
+ }, $page->getCommits()));
+ $this->assertSame(1, $page->getOffset());
+ $this->assertSame(1, $page->getLimit());
+ $this->assertNull($log->getOffset());
+ $this->assertNull($log->getLimit());
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ */
+ public function testWithNegativeOffset($repository)
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('The offset of a log can\'t be negative, -1 given.');
+
+ $repository->getLog()->withOffset(-1);
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ */
+ public function testWithNegativeLimit($repository)
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('The limit of a log can\'t be negative, -1 given.');
+
+ $repository->getLog()->withLimit(-1);
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ *
+ * @group legacy
+ */
+ public function testSetOffsetAndLimit($repository)
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Log::setOffset()" method is deprecated, use "withOffset()" instead.');
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Log::setLimit()" method is deprecated, use "withLimit()" instead.');
+
+ $log = $repository->getLog(self::LONGFILE_COMMIT);
+ $log->setOffset(1)->setLimit(1);
+
+ $this->assertSame(self::BEFORE_LONGFILE_COMMIT, $log->getSingleCommit()->getHash());
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ *
+ * @group legacy
+ */
+ public function testNegativeOffsetAndLimit($repository)
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: Passing a negative offset to "Gitonomy\Git\Log::__construct()" is deprecated, it throws in 2.0.');
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: Passing a negative limit to "Gitonomy\Git\Log::__construct()" is deprecated, it throws in 2.0: pass null for no limit.');
+
+ $log = $repository->getLog(self::LONGFILE_COMMIT, null, -1, -1);
+
+ $this->assertCount(8, $log->getCommits());
+ }
/**
* @dataProvider provideFoobar
*/
@@ -133,4 +207,38 @@ public function testParsesCommitsWithAndWithoutGitButlerHeaders(): void
$this->assertEquals("Commit with GitButler headers\n", $log[1]['message']);
$this->assertEquals("Another commit without GitButler headers\n", $log[2]['message']);
}
+
+ /**
+ * @dataProvider provideFoobar
+ *
+ * @group legacy
+ */
+ public function testCountWithALimit($repository)
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: Counting a log whose offset or limit leaves commits out with "Gitonomy\\Git\\Log::count()" is deprecated, it counts only the commits the log iterates in 2.0: use "countCommits()" to count them all.');
+
+ $log = $repository->getLog(self::LONGFILE_COMMIT)->withLimit(2);
+
+ $this->assertCount(8, $log);
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ */
+ public function testCountWithALimitLeavingNoCommitOut($repository)
+ {
+ $this->assertCount(8, $repository->getLog(self::LONGFILE_COMMIT)->withOffset(0)->withLimit(10));
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ *
+ * @group legacy
+ */
+ public function testOptionAsRevision($repository)
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: Passing "--no-merges", an option of "git log", as revision to "Gitonomy\\Git\\Log::__construct()" is deprecated, it throws in 2.0: pass revisions, ranges, names to exclude or "--all".');
+
+ $this->assertCount(1, $repository->getLog(['--no-merges', self::MERGE_COMMIT])->withLimit(1)->getCommits());
+ }
}
diff --git a/tests/Gitonomy/Git/Tests/PushReferenceTest.php b/tests/Gitonomy/Git/Tests/PushReferenceTest.php
index 4490a3f9..8c638313 100644
--- a/tests/Gitonomy/Git/Tests/PushReferenceTest.php
+++ b/tests/Gitonomy/Git/Tests/PushReferenceTest.php
@@ -12,10 +12,14 @@
namespace Gitonomy\Git\Tests;
+use Gitonomy\Git\Exception\LogicException;
use Gitonomy\Git\PushReference;
+use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
class PushReferenceTest extends AbstractTest
{
+ use ExpectDeprecationTrait;
+
const CREATE = 1;
const DELETE = 2;
const FORCE = 4;
@@ -38,8 +42,8 @@ public function provideIsers()
public function testIsers($reference, $before, $after, $mask)
{
$reference = new PushReference(self::createFoobarRepository(), $reference, $before, $after);
- $this->assertEquals($mask & self::CREATE, $reference->isCreate(), 'Create value is correct.');
- $this->assertEquals($mask & self::DELETE, $reference->isDelete(), 'Delete value is correct.');
+ $this->assertEquals($mask & self::CREATE, $reference->isCreation(), 'Create value is correct.');
+ $this->assertEquals($mask & self::DELETE, $reference->isDeletion(), 'Delete value is correct.');
$this->assertEquals($mask & self::FORCE, $reference->isForce(), 'Force value is correct.');
$this->assertEquals($mask & self::FAST_FORWARD, $reference->isFastForward(), 'FastForward value is correct.');
}
@@ -53,7 +57,7 @@ public function testLog($repository)
$log = $ref->getLog()->getCommits();
$this->assertCount(7, $log, '7 commits in log');
- $this->assertEquals('add a long file', $log[0]->getShortMessage(), 'First commit is correct');
+ $this->assertEquals('add a long file', $log[0]->getSubjectMessage(), 'First commit is correct');
}
/**
@@ -66,7 +70,7 @@ public function testSignedLog($repository)
$ref = new PushReference($repository, 'foo', self::INITIAL_COMMIT, self::SIGNED_COMMIT);
$log = $ref->getLog()->getCommits();
$this->assertCount(16, $log, '16 commits in log');
- $this->assertEquals('signed commit', $log[0]->getShortMessage(), 'Last commit is correct');
+ $this->assertEquals('signed commit', $log[0]->getSubjectMessage(), 'Last commit is correct');
}
/**
@@ -78,6 +82,45 @@ public function testLogWithExclude($repository)
$log = $ref->getLog([self::INITIAL_COMMIT])->getCommits();
$this->assertCount(7, $log, '7 commits in log');
- $this->assertEquals('add a long file', $log[0]->getShortMessage(), 'First commit is correct');
+ $this->assertEquals('add a long file', $log[0]->getSubjectMessage(), 'First commit is correct');
+ }
+
+ public function testFullnameAndRange()
+ {
+ $repository = self::createFoobarRepository();
+
+ $reference = new PushReference($repository, 'refs/heads/foo', self::BEFORE_LONGFILE_COMMIT, self::LONGFILE_COMMIT);
+ $this->assertSame('refs/heads/foo', $reference->getFullname());
+ $this->assertSame(self::BEFORE_LONGFILE_COMMIT.'..'.self::LONGFILE_COMMIT, $reference->getRange());
+
+ $creation = new PushReference($repository, 'refs/heads/foo', PushReference::ZERO, self::LONGFILE_COMMIT);
+ $this->assertSame(self::LONGFILE_COMMIT, $creation->getRange());
+ }
+
+ public function testRangeOfADeletion()
+ {
+ $reference = new PushReference(self::createFoobarRepository(), 'refs/heads/foo', self::LONGFILE_COMMIT, PushReference::ZERO);
+
+ $this->expectException(LogicException::class);
+
+ $reference->getRange();
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testDeprecatedNames()
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\PushReference::getReference()" method is deprecated, use "getFullname()" instead.');
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\PushReference::getRevision()" method is deprecated, use "getRange()" instead.');
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\PushReference::isCreate()" method is deprecated, use "isCreation()" instead.');
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\PushReference::isDelete()" method is deprecated, use "isDeletion()" instead.');
+
+ $reference = new PushReference(self::createFoobarRepository(), 'refs/heads/foo', PushReference::ZERO, self::LONGFILE_COMMIT);
+
+ $this->assertSame('refs/heads/foo', $reference->getReference());
+ $this->assertSame(self::LONGFILE_COMMIT, $reference->getRevision());
+ $this->assertTrue($reference->isCreate());
+ $this->assertFalse($reference->isDelete());
}
}
diff --git a/tests/Gitonomy/Git/Tests/ReferenceTest.php b/tests/Gitonomy/Git/Tests/ReferenceTest.php
index 0f975577..158b518c 100644
--- a/tests/Gitonomy/Git/Tests/ReferenceTest.php
+++ b/tests/Gitonomy/Git/Tests/ReferenceTest.php
@@ -12,12 +12,17 @@
namespace Gitonomy\Git\Tests;
+use Gitonomy\Git\Exception\ProcessException;
use Gitonomy\Git\Exception\ReferenceNotFoundException;
use Gitonomy\Git\Reference\Branch;
use Gitonomy\Git\Reference\Tag;
+use Gitonomy\Git\Repository;
+use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
class ReferenceTest extends AbstractTest
{
+ use ExpectDeprecationTrait;
+
/**
* @dataProvider provideEmpty
*/
@@ -212,9 +217,13 @@ public function testCreateAndDeleteBranch($repository)
/**
* @dataProvider provideFoobar
+ *
+ * @group legacy
*/
public function testIsBranchMergedToMaster()
{
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Reference\Branch::isMergedTo()" method is deprecated, use "Gitonomy\Git\Reference\Branch::isMergedInto()" instead.');
+
$repository = self::createFoobarRepository(false);
$repository->run('config', ['--local', 'user.name', '"Unit Test"']);
@@ -237,4 +246,124 @@ public function testIsBranchMergedToMaster()
$this->assertFalse($branch->isMergedTo('master'));
}
+
+ public function testIsMergedInto()
+ {
+ $repository = self::createFoobarRepository();
+ $references = $repository->getReferences();
+
+ $this->assertTrue($references->getBranch('master')->isMergedInto('master'));
+ $this->assertFalse($references->getBranch('new-feature')->isMergedInto('master'));
+ $this->assertTrue($references->createBranch('initial', self::INITIAL_COMMIT)->isMergedInto($references->getBranch('new-feature')));
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ */
+ public function testMoveTo($repository)
+ {
+ $references = $repository->getReferences();
+ $branch = $references->createBranch('foobar', self::INITIAL_COMMIT);
+
+ $moved = $branch->moveTo(self::LONGFILE_COMMIT);
+
+ $this->assertSame(self::LONGFILE_COMMIT, $moved->getCommitHash());
+ // The bag read before sees the move, like in 2.0.
+ $this->assertSame(self::LONGFILE_COMMIT, $references->getBranch('foobar')->getCommitHash());
+ $this->assertContains($moved, $references->getLocalBranches());
+ $this->assertSame(self::LONGFILE_COMMIT."\n", $repository->run('rev-parse', ['refs/heads/foobar']));
+ // The branch read before keeps its commit.
+ $this->assertSame(self::INITIAL_COMMIT, $branch->getCommitHash());
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ */
+ public function testMoveToAnAnnotatedTag($repository)
+ {
+ $branch = $repository->getReferences()->createBranch('foobar', self::INITIAL_COMMIT);
+
+ $moved = $branch->moveTo($repository->getReferences()->getTag('annotated'));
+
+ // The commit of the tag, not the tag object.
+ $this->assertSame($repository->getRevision('annotated^{commit}')->getCommit()->getHash(), $moved->getCommitHash());
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ */
+ public function testCreateBranchFromARevision($repository)
+ {
+ $branch = $repository->getReferences()->createBranch('foobar', $repository->getRevision('master'));
+
+ $this->assertSame($repository->getReferences()->getBranch('master')->getCommitHash(), $branch->getCommitHash());
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ *
+ * @group legacy
+ */
+ public function testCreateExistingReferences($repository)
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: Creating the branch "master", which exists already, is deprecated, it throws in 2.0: use "Gitonomy\Git\Reference\Branch::moveTo()" instead.');
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: Creating the tag "0.1", which exists already, is deprecated, it throws in 2.0: delete it first.');
+
+ $references = $repository->getReferences();
+ $references->createBranch('master', self::INITIAL_COMMIT);
+ $references->createTag('0.1', self::INITIAL_COMMIT);
+
+ $this->assertSame(self::INITIAL_COMMIT."\n", $repository->run('rev-parse', ['refs/heads/master']));
+ $this->assertSame(self::INITIAL_COMMIT."\n", $repository->run('rev-parse', ['refs/tags/0.1']));
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ *
+ * @group legacy
+ */
+ public function testDeleteOutsideRefs($repository)
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: Deleting "HEAD", which is not the full name of a reference under "refs/", is deprecated, it throws in 2.0.');
+
+ $repository->getReferences()->delete('HEAD');
+
+ // git deletes the branch HEAD points to.
+ $this->assertFalse((new Repository($repository->getGitDir(), null, self::getDriver()))->getReferences()->hasBranch('master'));
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ *
+ * @group legacy
+ */
+ public function testUpdate($repository)
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\ReferenceBag::update()" method is deprecated, use "Gitonomy\Git\Reference\Branch::moveTo()" instead.');
+
+ $repository->getReferences()->update(new Branch($repository, 'refs/heads/foobar', self::INITIAL_COMMIT));
+
+ $this->assertSame(self::INITIAL_COMMIT."\n", $repository->run('rev-parse', ['refs/heads/foobar']));
+ }
+
+ public function testCreateBranchFromAName()
+ {
+ $repository = self::createFoobarRepository();
+ $references = $repository->getReferences();
+ $master = $references->getBranch('master')->getCommitHash();
+
+ $branch = $references->createBranch('foobar', 'master');
+
+ // The name, like in 1.6, where 2.0 resolves it.
+ $this->assertSame('master', $branch->getCommitHash());
+ $this->assertSame($master."\n", $repository->run('rev-parse', ['refs/heads/foobar']));
+ $this->assertTrue($branch->isMergedInto('master'));
+ }
+
+ public function testCreateBranchFromAnUnknownName()
+ {
+ $this->expectException(ProcessException::class);
+
+ self::createFoobarRepository()->getReferences()->createBranch('foobar', 'unknown');
+ }
}
diff --git a/tests/Gitonomy/Git/Tests/RepositoryTest.php b/tests/Gitonomy/Git/Tests/RepositoryTest.php
index fc5c7deb..39285451 100644
--- a/tests/Gitonomy/Git/Tests/RepositoryTest.php
+++ b/tests/Gitonomy/Git/Tests/RepositoryTest.php
@@ -13,14 +13,239 @@
namespace Gitonomy\Git\Tests;
use Gitonomy\Git\Blob;
+use Gitonomy\Git\Driver\Cli\CliDriver;
+use Gitonomy\Git\Exception\InvalidArgumentException;
+use Gitonomy\Git\Exception\ProcessException;
+use Gitonomy\Git\Exception\ReferenceNotFoundException;
use Gitonomy\Git\Exception\RuntimeException;
+use Gitonomy\Git\Reference\Branch;
+use Gitonomy\Git\Repository;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
+use Psr\Log\LoggerInterface;
+use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
class RepositoryTest extends AbstractTest
{
+ use ExpectDeprecationTrait;
use ProphecyTrait;
+ public function testInit()
+ {
+ $dir = self::createTempDir();
+ $repository = Repository::init($dir, self::getDriver());
+ self::registerDeletion($repository);
+
+ $this->assertFalse($repository->isBare());
+ $this->assertSame($dir.'/.git', $repository->getGitDir());
+ $this->assertSame($dir, $repository->getWorkingDir());
+ }
+
+ public function testInitBare()
+ {
+ $dir = self::createTempDir();
+ $repository = Repository::initBare($dir, self::getDriver());
+ self::registerDeletion($repository);
+
+ $this->assertTrue($repository->isBare());
+ $this->assertSame($dir, $repository->getGitDir());
+ $this->assertDirectoryExists($dir.'/objects');
+ }
+
+ public function testInitThrowsForAFile()
+ {
+ $file = self::createTempDir().'/file';
+ touch($file);
+
+ $this->expectException(RuntimeException::class);
+
+ Repository::initBare($file, self::getDriver());
+ }
+
+ public function testCloneFrom()
+ {
+ $dir = self::createTempDir();
+ $repository = Repository::cloneFrom(self::createFoobarRepository()->getGitDir(), $dir, null, self::getDriver());
+ self::registerDeletion($repository);
+
+ $this->assertFalse($repository->isBare());
+ $this->assertSame($dir, $repository->getWorkingDir());
+ $this->assertSame('master', $repository->getHead()->getName());
+ $this->assertTrue($repository->getReferences()->hasRemoteBranch('origin/new-feature'));
+ }
+
+ public function testCloneFromABranch()
+ {
+ $repository = Repository::cloneFrom(self::createFoobarRepository()->getGitDir(), self::createTempDir(), 'new-feature', self::getDriver());
+ self::registerDeletion($repository);
+
+ $this->assertSame('new-feature', $repository->getHead()->getName());
+ }
+
+ public function testCloneBareFrom()
+ {
+ $dir = self::createTempDir();
+ $repository = Repository::cloneBareFrom(self::createFoobarRepository()->getGitDir(), $dir, null, self::getDriver());
+ self::registerDeletion($repository);
+
+ $this->assertTrue($repository->isBare());
+ $this->assertSame($dir, $repository->getGitDir());
+ $this->assertSame('master', $repository->getHead()->getName());
+ $this->assertTrue($repository->getReferences()->hasBranch('new-feature'));
+ }
+
+ public function testCloneBareFromABranch()
+ {
+ $repository = Repository::cloneBareFrom(self::createFoobarRepository()->getGitDir(), self::createTempDir(), 'new-feature', self::getDriver());
+ self::registerDeletion($repository);
+
+ $head = $repository->getHead();
+ $this->assertInstanceOf(Branch::class, $head);
+ $this->assertSame('new-feature', $head->getName());
+ }
+
+ public function testCloneFromThrowsForAMissingRepository()
+ {
+ $this->expectException(RuntimeException::class);
+
+ Repository::cloneFrom(self::createTempDir().'/missing', self::createTempDir(), null, self::getDriver());
+ }
+
+ public function testMirrorFrom()
+ {
+ $source = self::createFoobarRepository(false);
+ $dir = self::createTempDir();
+ $repository = Repository::mirrorFrom($source->getGitDir(), $dir, self::getDriver());
+ self::registerDeletion($repository);
+
+ $this->assertTrue($repository->isBare());
+ $this->assertSame($dir, $repository->getGitDir());
+ // A mirror keeps the remote branches of its source, which a bare clone leaves out.
+ $this->assertTrue($repository->getReferences()->has('refs/remotes/origin/new-feature'));
+ }
+
+ public function testIsValidRepository()
+ {
+ $this->assertTrue(Repository::isValidRepository(self::createFoobarRepository()->getGitDir(), self::getDriver()));
+ $this->assertFalse(Repository::isValidRepository(self::createTempDir(), self::getDriver()));
+ }
+
+ public function testIsValidRepositoryAndBranch()
+ {
+ $url = self::createFoobarRepository()->getGitDir();
+
+ $this->assertTrue(Repository::isValidRepositoryAndBranch($url, 'new-feature', self::getDriver()));
+ // The exact name, where Admin::isValidRepositoryAndBranch() matches "new-feature" too.
+ $this->assertFalse(Repository::isValidRepositoryAndBranch($url, 'feature', self::getDriver()));
+ $this->assertFalse(Repository::isValidRepositoryAndBranch($url, 'unknown', self::getDriver()));
+ $this->assertFalse(Repository::isValidRepositoryAndBranch(self::createTempDir(), 'master', self::getDriver()));
+ }
+
+ public function testWorkingDirArgument()
+ {
+ $source = self::createFoobarRepository(false);
+
+ $repository = new Repository($source->getGitDir(), $source->getWorkingDir(), self::getDriver());
+
+ $this->assertFalse($repository->isBare());
+ $this->assertSame($source->getGitDir(), $repository->getGitDir());
+ $this->assertSame($source->getWorkingDir(), $repository->getWorkingDir());
+ }
+
+ public function testDriverConfiguresGit()
+ {
+ $driver = new CliDriver('git', ['GIT_CONFIG_PARAMETERS' => "'gitlib.test=yes'"]);
+ $repository = new Repository(self::createFoobarRepository()->getGitDir(), null, $driver);
+
+ $this->assertSame("yes\n", $repository->run('config', ['gitlib.test']));
+ }
+
+ public function testDriverCommand()
+ {
+ $repository = new Repository(self::createFoobarRepository()->getGitDir(), null, new CliDriver('not-git'));
+
+ $this->expectException(ProcessException::class);
+
+ $repository->run('status');
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testOptionsAreDeprecated()
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: Passing an array of options to "Gitonomy\Git\Repository::__construct()" is deprecated, pass the working directory as second argument, and configure the logger and git through the "driver" argument.');
+
+ $source = self::createFoobarRepository(false);
+ $repository = new Repository($source->getGitDir(), ['working_dir' => $source->getWorkingDir()] + self::getOptions());
+
+ $this->assertSame($source->getWorkingDir(), $repository->getWorkingDir());
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testDebugOptionSetToFalseIsDeprecated()
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: Setting the "debug" option of "Gitonomy\Git\Repository::__construct()" to false is deprecated, a failing git operation always throws in 2.0: catch its exception instead.');
+
+ $repository = new Repository(self::createFoobarRepository()->getGitDir(), ['debug' => false] + self::getOptions());
+
+ $this->assertNull($repository->run('not-a-command'));
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testGitOptionsCannotBeCombinedWithTheDriver()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('The driver argument can\'t be combined with the "logger" or git options: configure the driver instead.');
+
+ new Repository(self::createFoobarRepository()->getGitDir(), ['process_timeout' => 60], self::getDriver());
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testOtherOptionsCombineWithTheDriver()
+ {
+ $source = self::createFoobarRepository(false);
+ $driver = new CliDriver('git', ['GIT_CONFIG_PARAMETERS' => "'gitlib.test=yes'"]);
+
+ $repository = new Repository($source->getGitDir(), ['working_dir' => $source->getWorkingDir()], $driver);
+
+ $this->assertSame($source->getWorkingDir(), $repository->getWorkingDir());
+ $this->assertSame("yes\n", $repository->run('config', ['gitlib.test']));
+ }
+
+ /**
+ * @dataProvider provideFoobar
+ */
+ public function testGetMergeBase($repository)
+ {
+ $mergeBase = $repository->getMergeBase('master', $repository->getRevision(self::INITIAL_COMMIT));
+
+ $this->assertSame(self::INITIAL_COMMIT, $mergeBase->getHash());
+ }
+
+ public function testGetMergeBaseOfUnrelatedHistories()
+ {
+ $repository = self::createFoobarRepository();
+ $driver = new CliDriver('git', ['GIT_AUTHOR_NAME' => 'Unit Test', 'GIT_AUTHOR_EMAIL' => 'unit@example.com', 'GIT_COMMITTER_NAME' => 'Unit Test', 'GIT_COMMITTER_EMAIL' => 'unit@example.com']);
+ // A root commit of the empty tree, which every repository has.
+ $unrelated = trim((new Repository($repository->getGitDir(), null, $driver))->run('commit-tree', ['4b825dc642cb6eb9a060e54bf8d69288fbee4904', '-m', 'Unrelated']));
+
+ $this->assertNull($repository->getMergeBase('master', $unrelated));
+ }
+
+ public function testGetMergeBaseThrowsForAnUnknownRevision()
+ {
+ $this->expectException(ReferenceNotFoundException::class);
+
+ self::createFoobarRepository()->getMergeBase('master', 'unknown');
+ }
+
/**
* @dataProvider provideFoobar
*/
@@ -39,12 +264,30 @@ public function testGetBlobWithExistingWorks($repository)
/**
* @dataProvider provideFoobar
+ *
+ * @group legacy
*/
public function testGetSize($repository)
{
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Repository::getSize()" method is deprecated, use "getSizeInBytes()" instead.');
+
$size = $repository->getSize();
$this->assertGreaterThanOrEqual(57, $size, 'Repository is at least 57KB');
$this->assertLessThan(84, $size, 'Repository is less than 84KB');
+ $this->assertSame((int) round($repository->getSizeInBytes() / 1000), $size);
+ }
+
+ public function testGetSizeInBytesDoesNotFollowLinks()
+ {
+ $repository = self::createEmptyRepository();
+ $size = $repository->getSizeInBytes();
+
+ $this->assertGreaterThan(0, $size);
+
+ symlink($repository->getGitDir().'/missing', $repository->getGitDir().'/hooks/pre-commit');
+ symlink($repository->getGitDir().'/config', $repository->getGitDir().'/hooks/post-commit');
+
+ $this->assertSame($size, $repository->getSizeInBytes());
}
public function testIsBare()
@@ -78,10 +321,10 @@ public function testLoggerOk($repository)
->info('run command: remote "" ')
->shouldBeCalledTimes(1);
$loggerProphecy
- ->debug(Argument::type('string')) // duration, return code and output
- ->shouldBeCalledTimes(3);
+ ->debug(Argument::type('string')) // creation of the repository, duration, return code and output
+ ->shouldBeCalledTimes(4);
- $repository->setLogger($loggerProphecy->reveal());
+ $repository = new Repository($repository->getGitDir(), $repository->getWorkingDir(), new CliDriver('git', [], 60, $loggerProphecy->reveal()));
$repository->run('remote');
}
@@ -102,14 +345,93 @@ public function testLoggerNOk($repository)
->info(Argument::type('string'))
->shouldBeCalledTimes(1);
$loggerProphecy
- ->debug(Argument::type('string')) // duration, return code and output
- ->shouldBeCalledTimes(3);
+ ->debug(Argument::type('string')) // creation of the repository, duration, return code and output
+ ->shouldBeCalledTimes(4);
$loggerProphecy
->error(Argument::type('string'))
->shouldBeCalledTimes(1);
- $repository->setLogger($loggerProphecy->reveal());
+ $repository = new Repository($repository->getGitDir(), $repository->getWorkingDir(), new CliDriver('git', [], 60, $loggerProphecy->reveal()));
$repository->run('not-work');
}
+
+ /**
+ * @group legacy
+ */
+ public function testSetLoggerIsDeprecated()
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Repository::setLogger()" method is deprecated, pass a "Gitonomy\Git\Driver\Cli\CliDriver" configured with the logger as driver argument instead.');
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Repository::getLogger()" method is deprecated, the logger is an argument of "Gitonomy\Git\Driver\Cli\CliDriver".');
+
+ $repository = self::createFoobarRepository();
+ $logger = $this->prophesize(LoggerInterface::class)->reveal();
+ $repository->setLogger($logger);
+
+ $this->assertSame($logger, $repository->getLogger());
+ }
+
+ public function testGetMergeBaseOfUnrelatedHistoriesLogsNoError()
+ {
+ $repository = self::createFoobarRepository();
+ $driver = new CliDriver('git', ['GIT_AUTHOR_NAME' => 'Unit Test', 'GIT_AUTHOR_EMAIL' => 'unit@example.com', 'GIT_COMMITTER_NAME' => 'Unit Test', 'GIT_COMMITTER_EMAIL' => 'unit@example.com']);
+ $unrelated = trim((new Repository($repository->getGitDir(), null, $driver))->run('commit-tree', ['4b825dc642cb6eb9a060e54bf8d69288fbee4904', '-m', 'Unrelated']));
+
+ $loggerProphecy = $this->prophesize(LoggerInterface::class);
+ $loggerProphecy->error(Argument::any())->shouldNotBeCalled();
+ $loggerProphecy->info(Argument::any())->shouldBeCalled();
+ $loggerProphecy->debug(Argument::any())->shouldBeCalled();
+
+ $repository = new Repository($repository->getGitDir(), null, new CliDriver('git', [], 60, $loggerProphecy->reveal()));
+
+ $this->assertNull($repository->getMergeBase('master', $unrelated));
+ }
+
+ public function testIsValidRepositoryReadsARelativePathAsAPath()
+ {
+ $repository = self::createFoobarRepository(false);
+ $cwd = getcwd();
+
+ try {
+ // A repository, whose remote "origin" git would read in its working directory.
+ chdir($repository->getWorkingDir());
+ $this->assertFalse(Repository::isValidRepository('origin', self::getDriver()));
+ $this->assertFalse(Repository::isValidRepositoryAndBranch('origin', 'master', self::getDriver()));
+
+ chdir(dirname($repository->getWorkingDir()));
+ $this->assertTrue(Repository::isValidRepository(basename($repository->getWorkingDir()), self::getDriver()));
+ } finally {
+ chdir($cwd);
+ }
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testGetObjectsByName()
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: Passing "master^{tree}", which is not a hash, to "Gitonomy\\Git\\Repository::getTree()" is deprecated, it throws in 2.0: use "getRevision($name)->getCommit()->getTree()" instead.');
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: Passing "master:README.md", which is not a hash, to "Gitonomy\\Git\\Repository::getBlob()" is deprecated, it throws in 2.0: use "Tree::resolvePath()" instead.');
+
+ $repository = self::createFoobarRepository();
+
+ $this->assertArrayHasKey('README.md', iterator_to_array($repository->getTree('master^{tree}')));
+ $this->assertStringContainsString('Foo Bar project', $repository->getBlob('master:README.md')->getContent());
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testGetReferencesReadsThemAgain()
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: Passing an argument to "Gitonomy\\Git\\Repository::getReferences()" is deprecated, open a new Repository to read the references again.');
+
+ $repository = self::createFoobarRepository();
+ $this->assertFalse($repository->getReferences()->hasBranch('foobar'));
+
+ $repository->run('branch', ['foobar', self::INITIAL_COMMIT]);
+
+ $this->assertFalse($repository->getReferences()->hasBranch('foobar'));
+ $this->assertTrue($repository->getReferences(true)->hasBranch('foobar'));
+ }
}
diff --git a/tests/Gitonomy/Git/Tests/TreeTest.php b/tests/Gitonomy/Git/Tests/TreeTest.php
index b7ba7d32..d45c20e5 100644
--- a/tests/Gitonomy/Git/Tests/TreeTest.php
+++ b/tests/Gitonomy/Git/Tests/TreeTest.php
@@ -14,63 +14,76 @@
use Gitonomy\Git\Blob;
use Gitonomy\Git\CommitReference;
+use Gitonomy\Git\Submodule;
+use Gitonomy\Git\Tree;
+use Gitonomy\Git\TreeEntry;
+use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
class TreeTest extends AbstractTest
{
+ use ExpectDeprecationTrait;
+
const PATH_RESOLVING_COMMIT = 'cc06ac171d884282202dff88c1ded499a1f89420';
/**
* @dataProvider provideFooBar
*/
- public function testGetEntries($repository)
+ public function testIterate($repository)
{
$tree = $repository->getCommit(self::LONGFILE_COMMIT)->getTree();
- $entries = $tree->getEntries();
-
- $this->assertNotEmpty($entries['long.php'], 'long.php is present');
- $this->assertTrue($entries['long.php'][1] instanceof Blob, 'long.php is a Blob');
+ $entries = iterator_to_array($tree);
- $this->assertNotEmpty($entries['README.md'], 'README.md is present');
- $this->assertTrue($entries['README.md'][1] instanceof Blob, 'README.md is a Blob');
+ $this->assertInstanceOf(TreeEntry::class, $entries['long.php']);
+ $this->assertSame('long.php', $entries['long.php']->getName());
+ $this->assertSame('100644', $entries['long.php']->getMode());
+ $this->assertInstanceOf(Blob::class, $entries['long.php']->getObject());
+ $this->assertInstanceOf(Blob::class, $entries['README.md']->getObject());
}
/**
* @dataProvider provideFooBar
*/
- public function testGetCommitReferenceEntries($repository)
+ public function testGetSubmodules($repository)
{
$tree = $repository->getCommit(self::NO_MESSAGE_COMMIT)->getTree();
- $commits = $tree->getCommitReferenceEntries();
+ $submodules = $tree->getSubmodules();
- $this->assertNotEmpty($commits['barbaz'], 'barbaz is present');
- $this->assertTrue($commits['barbaz'][1] instanceof CommitReference, 'barbaz is a Commit');
+ $this->assertSame(['barbaz'], array_keys($submodules));
+ $this->assertSame('barbaz', $submodules['barbaz']->getName());
+ $this->assertSame('160000', $submodules['barbaz']->getMode());
+ $this->assertInstanceOf(Submodule::class, $submodules['barbaz']->getObject());
+ // A CommitReference, like in 1.6, which 2.0 makes an alias of Submodule.
+ $this->assertSame(CommitReference::class, get_class($submodules['barbaz']->getObject()));
+ $this->assertSame('5a519c47166f333e32091e166eb55f729106c964', $submodules['barbaz']->getObject()->getHash());
}
/**
* @dataProvider provideFooBar
*/
- public function testGetTreeEntries($repository)
+ public function testGetTrees($repository)
{
- $tree = $repository->getCommit(self::NO_MESSAGE_COMMIT)->getTree();
+ $this->assertSame([], $repository->getCommit(self::NO_MESSAGE_COMMIT)->getTree()->getTrees());
- $trees = $tree->getTreeEntries();
+ $trees = $repository->getCommit(self::PATH_RESOLVING_COMMIT)->getTree()->getTrees();
- $this->assertEmpty($trees);
+ $this->assertSame('040000', $trees['test']->getMode());
+ $this->assertInstanceOf(Tree::class, $trees['test']->getObject());
}
/**
* @dataProvider provideFooBar
*/
- public function testGetBlobEntries($repository)
+ public function testGetBlobs($repository)
{
$tree = $repository->getCommit(self::NO_MESSAGE_COMMIT)->getTree();
- $blobs = $tree->getBlobEntries();
+ $blobs = $tree->getBlobs();
- $this->assertNotEmpty($blobs['README.md'], 'README.md is present');
- $this->assertTrue($blobs['README.md'][1] instanceof Blob, 'README.md is a blob');
+ $this->assertSame('README.md', $blobs['README.md']->getName());
+ $this->assertInstanceOf(Blob::class, $blobs['README.md']->getObject());
+ $this->assertArrayNotHasKey('barbaz', $blobs);
}
/**
@@ -82,8 +95,73 @@ public function testResolvePath($repository)
$path = 'test/a/b/c';
$resolved = $tree->resolvePath($path);
- $entries = $resolved->getEntries();
- $this->assertNotEmpty($entries['d'], 'Successfully resolved source folder');
+ $this->assertArrayHasKey('d', iterator_to_array($resolved), 'Successfully resolved source folder');
+ }
+
+ /**
+ * @dataProvider provideFooBar
+ *
+ * @group legacy
+ */
+ public function testGetEntries($repository)
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Tree::getEntries()" method is deprecated, iterate the tree instead.');
+
+ $tree = $repository->getCommit(self::LONGFILE_COMMIT)->getTree();
+
+ $entries = $tree->getEntries();
+
+ $this->assertSame('100644', $entries['long.php'][0]);
+ $this->assertInstanceOf(Blob::class, $entries['long.php'][1]);
+ $this->assertInstanceOf(Blob::class, $entries['README.md'][1]);
+ }
+
+ /**
+ * @dataProvider provideFooBar
+ *
+ * @group legacy
+ */
+ public function testGetCommitReferenceEntries($repository)
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Tree::getCommitReferenceEntries()" method is deprecated, use "getSubmodules()" instead.');
+
+ $tree = $repository->getCommit(self::NO_MESSAGE_COMMIT)->getTree();
+
+ $commits = $tree->getCommitReferenceEntries();
+
+ $this->assertSame('160000', $commits['barbaz'][0]);
+ $this->assertInstanceOf(CommitReference::class, $commits['barbaz'][1]);
+ }
+
+ /**
+ * @dataProvider provideFooBar
+ *
+ * @group legacy
+ */
+ public function testGetTreeEntries($repository)
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Tree::getTreeEntries()" method is deprecated, use "getTrees()" instead.');
+
+ $trees = $repository->getCommit(self::PATH_RESOLVING_COMMIT)->getTree()->getTreeEntries();
+
+ $this->assertSame('040000', $trees['test'][0]);
+ $this->assertInstanceOf(Tree::class, $trees['test'][1]);
+ }
+
+ /**
+ * @dataProvider provideFooBar
+ *
+ * @group legacy
+ */
+ public function testGetBlobEntries($repository)
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: The "Gitonomy\Git\Tree::getBlobEntries()" method is deprecated, use "getBlobs()" instead.');
+
+ $tree = $repository->getCommit(self::NO_MESSAGE_COMMIT)->getTree();
+
+ $blobs = $tree->getBlobEntries();
+
+ $this->assertInstanceOf(Blob::class, $blobs['README.md'][1]);
}
}
diff --git a/tests/Gitonomy/Git/Tests/WorkingCopyTest.php b/tests/Gitonomy/Git/Tests/WorkingCopyTest.php
index dfa048f9..cdb1a4d3 100644
--- a/tests/Gitonomy/Git/Tests/WorkingCopyTest.php
+++ b/tests/Gitonomy/Git/Tests/WorkingCopyTest.php
@@ -12,25 +12,65 @@
namespace Gitonomy\Git\Tests;
-use Gitonomy\Git\Admin;
+use Gitonomy\Git\Repository;
use Gitonomy\Git\Exception\LogicException;
+use Gitonomy\Git\Exception\ReferenceNotFoundException;
use Gitonomy\Git\Exception\RuntimeException;
use Gitonomy\Git\Reference\Branch;
+use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
class WorkingCopyTest extends AbstractTest
{
+ use ExpectDeprecationTrait;
+
public function testNoWorkingCopyInBare()
{
$this->expectException(LogicException::class);
$path = self::createTempDir();
- $repo = Admin::init($path, true, self::getOptions());
+ $repo = Repository::initBare($path, self::getDriver());
$repo->getWorkingCopy();
}
+ public function testCheckoutNewBranch()
+ {
+ $repository = self::createFoobarRepository(false);
+ $wc = $repository->getWorkingCopy();
+ $wc->checkoutNewBranch('new-feature', 'origin/new-feature');
+
+ $head = $repository->getHead();
+ $this->assertInstanceOf(Branch::class, $head, 'HEAD is a branch');
+ $this->assertEquals('new-feature', $head->getName(), 'HEAD is branch new-feature');
+ $this->assertSame($repository->getReferences()->getRemoteBranch('origin/new-feature')->getCommitHash(), $head->getCommitHash());
+ }
+
+ public function testCheckoutNewBranchFromHead()
+ {
+ $repository = self::createFoobarRepository(false);
+ $repository->getWorkingCopy()->checkoutNewBranch('foobar');
+
+ $head = $repository->getHead();
+ $this->assertSame('foobar', $head->getName());
+ $this->assertSame($repository->getReferences()->getBranch('master')->getCommitHash(), $head->getCommitHash());
+ }
+
+ public function testCheckoutARevision()
+ {
+ $repository = self::createFoobarRepository(false);
+ $repository->getWorkingCopy()->checkout($repository->getRevision('0.1'));
+
+ $this->assertTrue($repository->isHeadDetached());
+ $this->assertSame($repository->getRevision('0.1')->getCommit()->getHash(), $repository->getHeadCommit()->getHash());
+ }
+
+ /**
+ * @group legacy
+ */
public function testCheckout()
{
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: Passing a branch to create to "Gitonomy\Git\WorkingCopy::checkout()" is deprecated, use "checkoutNewBranch()" instead.');
+
$repository = self::createFoobarRepository(false);
$wc = $repository->getWorkingCopy();
$wc->checkout('origin/new-feature', 'new-feature');
@@ -40,6 +80,27 @@ public function testCheckout()
$this->assertEquals('new-feature', $head->getName(), 'HEAD is branch new-feature');
}
+ public function testDiff()
+ {
+ $repository = self::createFoobarRepository(false);
+ $wc = $repository->getWorkingCopy();
+
+ $this->assertCount(0, $wc->getDiff()->getFiles());
+
+ file_put_contents($repository->getWorkingDir().'/staged.txt', 'staged');
+ $repository->run('add', ['staged.txt']);
+ file_put_contents($repository->getWorkingDir().'/README.md', 'pending');
+
+ $diff = $wc->getDiff();
+ $this->assertSame(['README.md', 'staged.txt'], array_map(function ($file) {
+ return $file->getName();
+ }, $diff->getFiles()));
+ $this->assertSame($repository, $diff->getFiles()[0]->getRepository());
+
+ // Against another revision, the changes of the commits since.
+ $this->assertGreaterThan(2, count($wc->getDiff($repository->getRevision(self::LONGFILE_COMMIT))->getFiles()));
+ }
+
public function testDiffStaged()
{
$repository = self::createFoobarRepository(false);
@@ -110,4 +171,36 @@ public function testGetUntracked()
$this->assertContains('untracked.txt', $wc->getUntrackedFiles());
}
+
+ public function testDiffThrowsForAnUnknownRevision()
+ {
+ $this->expectException(ReferenceNotFoundException::class);
+
+ self::createFoobarRepository(false)->getWorkingCopy()->getDiff('unknown');
+ }
+
+ public function testCheckoutNewBranchThrowsForAnUnknownStartPoint()
+ {
+ $repository = self::createFoobarRepository(false);
+
+ try {
+ $repository->getWorkingCopy()->checkoutNewBranch('foobar', 'unknown');
+ $this->fail('An unknown start point throws.');
+ } catch (ReferenceNotFoundException $e) {
+ $this->assertFalse((new Repository($repository->getGitDir(), null, self::getDriver()))->getReferences()->hasBranch('foobar'));
+ }
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testCheckoutALocalBranchObject()
+ {
+ $this->expectDeprecation('Since gitonomy/gitlib 1.7: Passing a local "Gitonomy\\Git\\Reference\\Branch" to "Gitonomy\\Git\\WorkingCopy::checkout()", which detaches HEAD, is deprecated, 2.0 attaches HEAD to the branch: pass its name, or its commit to detach HEAD.');
+
+ $repository = self::createFoobarRepository(false);
+ $repository->getWorkingCopy()->checkout($repository->getReferences()->getBranch('master'));
+
+ $this->assertTrue($repository->isHeadDetached());
+ }
}