Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Tests

on:
push:
branches: [ main ]
branches: [ main, 1.x ]
pull_request:

jobs:
Expand All @@ -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)'

Expand Down
104 changes: 104 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
8 changes: 5 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
70 changes: 35 additions & 35 deletions doc/admin.md
Original file line number Diff line number Diff line change
@@ -1,74 +1,74 @@
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
--------------------

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
Expand Down
36 changes: 27 additions & 9 deletions doc/blame.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,28 @@ 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
---------------

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();
Expand All @@ -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.
14 changes: 14 additions & 0 deletions doc/branch.md
Original file line number Diff line number Diff line change
Expand Up @@ -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...');
```
32 changes: 16 additions & 16 deletions doc/commit.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -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.
Loading
Loading