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
14 changes: 14 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,16 @@
* text=auto
/tests/fixtures/*.bundle binary

# Left out of the archive Composer installs: only src/, the docs and the license ship.
/.gitattributes export-ignore
/.github export-ignore
/.gitignore export-ignore
/.php-cs-fixer.php export-ignore
/benchmark.php export-ignore
/castor.php export-ignore
/phpstan-baseline.neon export-ignore
/phpstan.neon export-ignore
/phpunit.xml.dist export-ignore
/test-git-versions.php export-ignore
/tests export-ignore
/tools export-ignore
13 changes: 8 additions & 5 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ We accept contributions via pull requests on GitHub. Please review these guideli

## Guidelines

* Please follow the [PSR-12 Coding Style Guide](https://www.php-fig.org/psr/psr-12/).
* Follow the coding standards of PHP-CS-Fixer, configured in `.php-cs-fixer.php` (the Symfony and PhpCsFixer rule sets): `castor cs` applies them.
* Ensure that the current tests pass, and if you've added something new, add the tests where relevant.
* Send a coherent commit history, making sure each commit in your pull request is meaningful.
* You may need to [rebase](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) to avoid merge conflicts.
Expand Down Expand Up @@ -62,10 +62,13 @@ locally, ready to receive new commits. `build` rebuilds `tests/fixtures/foobar.b
from that directory, restricted to the refs listed in `tests/fixtures/bundle-refs.txt` —
add your new branch or tag there first if you introduced one.

Then update the commit SHA constants in `AbstractTestCase` to match, and run
`tests/fixtures/verify-bundle.sh`. It checks the bundle's integrity, its ref list against
`bundle-refs.txt`, and its size, since GitHub renders any change to this binary file as an
opaque diff. If your change intentionally adds a ref or grows the file, update
Existing refs must not move, since the tests rely on the commits they point to: add your
commits on new branches or tags, and constants in `AbstractTestCase` for the ones your
tests use. Then add the new refs to `tests/fixtures/bundle-heads.txt` (the output of
`git bundle list-heads tests/fixtures/foobar.bundle`), and run
`tests/fixtures/verify-bundle.sh`. It checks the bundle's integrity, its refs against
`bundle-refs.txt` and `bundle-heads.txt`, and its size, since GitHub renders any change to
this binary file as an opaque diff. If your change intentionally adds a ref or grows the file, update
`tests/fixtures/bundle-refs.txt` or `MAX_SIZE_KB` in that script as part of the same pull
request, so the reason for the change is explicit and reviewable rather than a silent
binary diff.
Expand Down
13 changes: 13 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: monthly

- package-ecosystem: composer
directories:
- /tools/php-cs-fixer
- /tools/phpstan
schedule:
interval: monthly
110 changes: 110 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ on:
branches: [ main ]
pull_request:

permissions:
contents: read

jobs:
verify-fixture:
name: Verify Test Fixture Bundle
Expand Down Expand Up @@ -34,6 +37,16 @@ jobs:
- name: Run CS check
run: castor cs --dry-run

check-archive:
name: Check the Archive
runs-on: ubuntu-24.04
steps:
- name: Checkout Code
uses: actions/checkout@v7

- name: Ship only the code, the docs and the license
run: diff <(git archive HEAD | tar -t | cut -d/ -f1 | sort -u) <(printf '%s\n' CHANGELOG.md LICENSE README.md UPGRADE-2.0.md composer.json doc src | sort)

phpstan:
name: Static Analysis
runs-on: ubuntu-24.04
Expand Down Expand Up @@ -90,3 +103,100 @@ jobs:

- name: Execute PHPUnit
run: vendor/bin/phpunit

tests-git:
name: Test git ${{ matrix.git }}
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
# The oldest supported git, and the one of Ubuntu 20.04: the other jobs test the latest one.
include:
- git: '1.8.4'
sha256: '5e0efe7cd30e258bc3be9ae023790743974e76eb1b24ecac24de6fc2af2b2786'
- git: '2.25.1'
sha256: 'aaef0cd609a7730cfbae530d7c91214a941d7e26054d2314cc99794880156b33'

steps:
- name: Checkout Code
uses: actions/checkout@v7

- name: Cache git
id: cache-git
uses: actions/cache@v6
with:
path: ~/git
key: git-${{ matrix.git }}-ubuntu-24.04

- name: Build git
if: steps.cache-git.outputs.cache-hit != 'true'
run: |
curl -fsSL "https://github.com/git/git/archive/refs/tags/v${{ matrix.git }}.tar.gz" -o git.tar.gz
echo "${{ matrix.sha256 }} git.tar.gz" | sha256sum --check
tar xzf git.tar.gz
make -C "git-${{ matrix.git }}" prefix="$HOME/git" CFLAGS='-std=gnu17 -O2' NO_OPENSSL=1 NO_CURL=1 NO_EXPAT=1 NO_GETTEXT=1 NO_PERL=1 NO_PYTHON=1 NO_TCLTK=1 --jobs="$(nproc)" install

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'

- name: Setup Problem Matchers
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"

- name: Install Composer dependencies
run: composer update --prefer-dist --no-interaction

- name: Execute PHPUnit
run: GIT_COMMAND="$HOME/git/bin/git" vendor/bin/phpunit

tests-ffi:
name: Test PHP ${{ matrix.php }} (LibGit2Driver)
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
php: ['8.4', '8.5']
env:
# LibGit2Driver only supports libgit2 1.9, which Ubuntu 24.04 doesn't ship.
LIBGIT2_VERSION: '1.9.1'
LIBGIT2_SHA256: '14cab3014b2b7ad75970ff4548e83615f74d719afe00aa479b4a889c1e13fc00'
LIBGIT2_CMAKE_FLAGS: '-DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=OFF -DBUILD_CLI=OFF -DUSE_HTTPS=OFF -DUSE_SSH=OFF'

steps:
- name: Checkout Code
uses: actions/checkout@v7

- name: Cache libgit2
id: cache-libgit2
uses: actions/cache@v6
with:
path: ~/libgit2
key: libgit2-${{ env.LIBGIT2_VERSION }}-${{ env.LIBGIT2_CMAKE_FLAGS }}-ubuntu-24.04

- name: Build libgit2
if: steps.cache-libgit2.outputs.cache-hit != 'true'
run: |
curl -fsSL "https://github.com/libgit2/libgit2/archive/refs/tags/v${LIBGIT2_VERSION}.tar.gz" -o libgit2.tar.gz
echo "${LIBGIT2_SHA256} libgit2.tar.gz" | sha256sum --check
tar xzf libgit2.tar.gz
cmake -S "libgit2-${LIBGIT2_VERSION}" -B libgit2-build -DCMAKE_INSTALL_PREFIX="$HOME/libgit2" -DCMAKE_INSTALL_LIBDIR=lib $LIBGIT2_CMAKE_FLAGS
cmake --build libgit2-build --parallel
cmake --install libgit2-build

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: ffi

- name: Setup Problem Matchers
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"

- name: Install Composer dependencies
run: composer update --prefer-dist --no-interaction

- name: Execute PHPUnit
run: LD_LIBRARY_PATH="$HOME/libgit2/lib" vendor/bin/phpunit
env:
GITLIB_DRIVER: ffi
2 changes: 2 additions & 0 deletions .php-cs-fixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
__FILE__,
'castor.php',
])
->notPath('git-builds')
->notPath('var')
;

Expand All @@ -38,6 +39,7 @@
'@Symfony' => true,
'@Symfony:risky' => true,
'php_unit_internal_class' => false, // From @PhpCsFixer but we don't want it
'php_unit_strict' => true, // assertSame() instead of assertEquals()
'php_unit_test_class_requires_covers' => false, // From @PhpCsFixer but we don't want it
'phpdoc_add_missing_param_annotation' => false, // From @PhpCsFixer but we don't want it
'header_comment' => ['header' => $fileHeaderComment],
Expand Down
138 changes: 121 additions & 17 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,63 @@ 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).
Changes since [v1.6.0](https://github.com/gitonomy/gitlib/releases/tag/v1.6.0). To upgrade, see
[UPGRADE-2.0.md](UPGRADE-2.0.md): most of the API of 1.x that 2.0 replaces keeps working,
deprecated until 3.0.

### Breaking changes

- Run every git operation through a driver (`Gitonomy\Git\Driver`): the domain objects call a
`Driver\RepositoryDriverInterface`, typed (hashes, enums, value objects of `Driver\Data`),
instead of building processes and parsing their output. A failing operation always throws, a
missing revision or object a `ReferenceNotFoundException`, a timeout a
`ProcessTimedOutException`; dates are `\DateTimeImmutable`; names in diffs and trees are
UTF-8; references are read again after every write gitlib makes, and peel annotated tags;
the diff of a merge commit is against its first parent; blame lines, tree entries, diff
lines and logs are typed objects; the subject of a message is its first paragraph. See
[UPGRADE-2.0.md](UPGRADE-2.0.md) for every change.
([#253](https://github.com/gitonomy/gitlib/pull/253))
- Require PHP 8.4 or later, Symfony 6.4/7.4/8.1+: most classes are `final` (some `readonly`),
and every method has native types. ([#238](https://github.com/gitonomy/gitlib/pull/238))
- Remove `Gitonomy\Git\Util\StringHelper`, replaced by `symfony/string`, and its configurable
encoding: gitlib reads everything in UTF-8. ([#252](https://github.com/gitonomy/gitlib/pull/252))
- Require git 1.8.4 or later, the oldest version the test suite passes with: it simplifies the
log of a range filtered by path like the current versions, and `Log::count()` needs git
1.7.2. ([#253](https://github.com/gitonomy/gitlib/pull/253))
- `Diff::parse()` throws for a diff without its raw information, deprecated since 1.6.0
([#229](https://github.com/gitonomy/gitlib/pull/229)): parse the output of
`git diff --raw --patch`. ([#253](https://github.com/gitonomy/gitlib/pull/253))

### Features

- Add `WorkingCopy::merge()` to support `git merge`: accepts a `Commit`, `Reference` or
string revision, plus optional extra CLI args (e.g. `['--no-ff']`), mirroring the existing
`checkout()` method. ([#248](https://github.com/gitonomy/gitlib/pull/248), fixes #118)
- Add an experimental `Driver\Ffi\LibGit2Driver`, running libgit2 1.9 in-process through PHP's
FFI extension, as an alternative to the default `Driver\Cli\CliDriver`: pass it as the driver of
`Repository`. It avoids spawning a `git` process for every operation; see
["Choosing a driver"](doc/repository.md#choosing-a-driver) for its requirements, its
differences with `CliDriver`, and when it is faster. `benchmark.php` compares both drivers
on a repository. With php-fpm, `LibGit2Driver::preload()` keeps libgit2 loaded from
`opcache.preload`. ([#253](https://github.com/gitonomy/gitlib/pull/253))
- `Repository` takes the working directory and the driver as arguments: a `Driver\Cli\CliDriver`
by default, configured through its constructor (git binary, environment, timeout and logger,
which gets every command, clones included), or any `Driver\DriverInterface`, to replace it in
tests; see ["Using your own driver"](doc/repository.md#using-your-own-driver). The driver
finds the repository: linked worktrees, submodules and reftable repositories open, and a
directory that isn't a repository throws right away.
([#253](https://github.com/gitonomy/gitlib/pull/253))
- Add static methods to `Repository` creating repositories, `init()`, `initBare()`,
`cloneFrom()`, `cloneBareFrom()` and `mirrorFrom()`, and checking a repository URL,
`isValidRepository()` and `isValidRepositoryAndBranch()`; see
["Create and access git repositories"](doc/admin.md).
([#253](https://github.com/gitonomy/gitlib/pull/253))
- Add `Repository::getDiff($from, $to)`, `Repository::getMergeBase()` and `WorkingCopy::getDiff()`,
`WorkingCopy::checkoutNewBranch()`, `Branch::moveTo()` and `Branch::isMergedInto()`,
`Log::withOffset()` and `withLimit()`, `Blame::getHunks()` and the line numbers of
`Blame\Line`, `TreeEntry` and `Submodule`, `Diff\DiffLine` and `Diff\DiffLineType`,
`File::isModeChange()`, `FileChange::getAdditions()` and `getDeletions()`,
`PushReference::getFullname()` and `getRange()`, and the SSH and X.509 signatures of tags.
([#253](https://github.com/gitonomy/gitlib/pull/253))
- Add `WorkingCopy::merge()` to support `git merge`, fast-forwarding or not, with a
`FastForward` enum. ([#248](https://github.com/gitonomy/gitlib/pull/248), fixes #118)
- Add `Repository::runProcess()` to expose stderr from successful commands: `run()` only
ever returned stdout, even on success, but some git commands (e.g. `push`) write
meaningful output to stderr even when they succeed. `run()` now delegates to it and keeps
Expand All @@ -26,8 +76,69 @@ Changes since [v1.6.0](https://github.com/gitonomy/gitlib/releases/tag/v1.6.0).
still exists afterward.
([#244](https://github.com/gitonomy/gitlib/pull/244), fixes #14)

### Deprecations

All are removed in 3.0; see [UPGRADE-2.0.md](UPGRADE-2.0.md).
([#253](https://github.com/gitonomy/gitlib/pull/253))

- `Admin`, use the static methods of `Repository`, and `Repository::cloneTo()`.
- The array of options of `Repository` and `Admin` (`working_dir`, `logger`, `command`,
`environment_variables`, `inherit_environment_variables`, `process_timeout`, `debug`): pass
the working directory and a `CliDriver` instead; `inherit_environment_variables` has no
counterpart, pass the variables git needs instead. `Repository::setLogger()` and `getLogger()`.
- Passing `$reload` to `Repository::getReferences()`, `ReferenceBag::update()`,
`Branch::isMergedTo()`, and arguments to `Commit::getIncludingBranches()`.
- Calling `Repository::getDiff()` with one argument, `Log::getDiff()`, `Diff::setRepository()`,
`File::setRepository()`, `addChange()`, `isChangeMode()`, `isDelete()` and `getAnchor()`,
`FileChange::getCount()` and its `LINE_*` constants, reading diff lines as arrays.
- `Log::setOffset()` and `setLimit()`.
- A range of lines as a string in `Repository::getBlame()`, `Blame::getGroupedLines()` and
`Blame\Line::getLine()`.
- `CommitReference`, `Tree::getCommitReferenceEntries()`, reading tree entries as arrays.
- The branch to create of `WorkingCopy::checkout()`, the arguments of `git merge` of
`WorkingCopy::merge()`.
- `Commit::getShortMessage()` and `getFixedShortHash()`, `Repository::shell()`,
`PushReference::getReference()` and `getRevision()`.

### Bug fixes

- Fix `Commit::getDiff()` returning an empty diff for a root commit, and throwing for a merge
commit (`CliDriver`). ([#253](https://github.com/gitonomy/gitlib/pull/253))
- Fix `Repository::getDiff()`, `WorkingCopy::getDiffPending()` and `WorkingCopy::getDiffStaged()`
failing to parse the output of git before 2.16, which writes "..." after abbreviated hashes,
and throwing during a conflict. ([#253](https://github.com/gitonomy/gitlib/pull/253))
- Fix the names of files with spaces in diffs, split at the wrong place, the hashes of the files
following a type change, shifted, and hunks of one line, counted as empty.
([#253](https://github.com/gitonomy/gitlib/pull/253))
- Fix annotated tags never resolving to their commit (`ReferenceBag::resolveTags()`,
`Commit::resolveReferences()`), tags created or deleted missing from `getTags()`, and
`getHead()` throwing after a checkout of a new branch: references are read again after
every write gitlib makes. ([#253](https://github.com/gitonomy/gitlib/pull/253))
- Fix `createBranch()` and `createTag()` moving an existing reference instead of refusing it,
a branch named "remotes/x" taken for a remote one, and branches whose name ends with "HEAD"
missing. ([#253](https://github.com/gitonomy/gitlib/pull/253))
- Fix `isValidRepositoryAndBranch()` matching the end of the name of a branch ("release" for
"feature/release"), and `isValidRepository()` reading a relative path as the name of a remote
of PHP's working directory, or returning `false` when git can't run.
([#253](https://github.com/gitonomy/gitlib/pull/253))
- Fix `getTree()` reading the tree of a commit, and arguments holding a NUL byte throwing a
`ValueError`: they throw a `ReferenceNotFoundException` and an `InvalidArgumentException`.
([#253](https://github.com/gitonomy/gitlib/pull/253))
- Fix the log of a merge of an unsigned or SSH-signed tag throwing, or swallowing commits up to
the next PGP signature. ([#253](https://github.com/gitonomy/gitlib/pull/253))
- Fix `Commit::getBodyMessage()` losing the second line of a message without an empty line
after its subject, tags without tagger throwing, and SSH-signed tags not seen as signed.
([#253](https://github.com/gitonomy/gitlib/pull/253))
- Fix blame lines numbered from 1 for a range of lines. ([#253](https://github.com/gitonomy/gitlib/pull/253))
- Fix `Hooks` failing silently, in a repository without hooks directory, and writing the hooks
of a linked worktree where git doesn't run them. ([#253](https://github.com/gitonomy/gitlib/pull/253))
- Fix blame lines starting with a TAB character (`Blame\Line::getContent()`): code removing it
must stop doing so. ([#253](https://github.com/gitonomy/gitlib/pull/253))
- Fix reading a commit whose `encoding` header isn't UTF-8, which threw: its message, author and
committer are now re-encoded to UTF-8, like `git log` does. ([#253](https://github.com/gitonomy/gitlib/pull/253))
- Fix file names holding non-ASCII or special characters (tab, quote, backslash) in trees
(`Tree::getEntries()`), and special characters in diffs, which kept git's C-style quoting.
([#253](https://github.com/gitonomy/gitlib/pull/253))
- Fix relative paths not resolving against the repository in `Repository::run()`: the git
process spawned by `run()` never had its working directory set, so commands such as
`apply` resolved paths against the calling script's cwd instead of `--work-tree`, failing
Expand All @@ -43,9 +154,9 @@ Changes since [v1.6.0](https://github.com/gitonomy/gitlib/releases/tag/v1.6.0).
branch existed.
- `Tag::getBodyMessage()` dropped or truncated a line for annotated tags using the
standard `subject\n\nbody` convention.
- `Repository::run()` can return `null` on failure when `debug=false`; several call sites
(`Tag::isAnnotated()`, `PushReference::isForce()`, `Blob::getContent()`, ...) did not
handle that case and produced wrong data or crashed. All call sites now check for `null`.
- With the `debug` option set to `false`, several call sites (`Tag::isAnnotated()`,
`PushReference::isForce()`, `Blob::getContent()`, ...) produced wrong data or crashed
when git failed.
- `Repository::shell()` interpolated the repository path into the shell command without
escaping it; it is now passed through `escapeshellarg()`.
- `Hooks::set()` created hook scripts world-writable (`0777`); they are now created
Expand All @@ -54,16 +165,9 @@ Changes since [v1.6.0](https://github.com/gitonomy/gitlib/releases/tag/v1.6.0).

### Minor

- Replace `Gitonomy\Git\Util\StringHelper` with `symfony/string`'s `CodePointString`, which
covers the same UTF-8-aware `strlen`/`substr`/`strpos`/`strrpos` operations.
**Breaking:** `StringHelper` is removed, along with its configurable encoding
(`getEncoding()`/`setEncoding()`); the library now consistently targets `UTF-8`.
([#252](https://github.com/gitonomy/gitlib/pull/252))
- Modernize for PHP 8.4+, Symfony 6.4/7.4/8.1+ and PHPUnit 12: full property/parameter/return
type coverage, `readonly`/`final` where applicable, PHPUnit 12 attributes, and new
php-cs-fixer/PHPStan/Castor tooling.
**Breaking:** the minimum supported PHP version is now 8.4.
([#238](https://github.com/gitonomy/gitlib/pull/238))
- CI tests git 1.8.4 and 2.25.1, analyses the tests and the libgit2 driver with PHPStan,
checks the archive Composer installs, and the suite deletes what it creates, in any order.
([#253](https://github.com/gitonomy/gitlib/pull/253))
- Add a regression test for reading a blob's content in a tree that also contains a
submodule. ([#243](https://github.com/gitonomy/gitlib/pull/243))
- Replace the network-dependent test fixture (cloned from GitHub on every run) with a local,
Expand Down
Loading