diff --git a/.gitattributes b/.gitattributes
index cbf8b342..e39bddbb 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -13,3 +13,4 @@
/test-git-versions.php export-ignore
/tests export-ignore
/tools export-ignore
+/zensical.toml export-ignore
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 27976684..ca8ca8b2 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -11,3 +11,11 @@ updates:
- /tools/phpstan
schedule:
interval: monthly
+
+ - package-ecosystem: pip
+ directory: /tools/zensical
+ schedule:
+ interval: monthly
+ groups:
+ zensical:
+ patterns: ['*']
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 00000000..14513081
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,76 @@
+name: Documentation
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ paths:
+ - .github/workflows/docs.yml
+ - CHANGELOG.md
+ - README.md
+ - UPGRADE-2.0.md
+ - doc/**
+ - tools/zensical/**
+ - zensical.toml
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ # Let a deployment finish: the next one waits.
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+jobs:
+ build:
+ name: Build the Documentation
+ runs-on: ubuntu-24.04
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v7
+ with:
+ # The site is published: keep the token out of the workspace.
+ persist-credentials: false
+
+ - name: Setup Python
+ uses: actions/setup-python@v7
+ with:
+ python-version: '3.14'
+ cache: pip
+ cache-dependency-path: tools/zensical/requirements.txt
+
+ - name: Install Zensical
+ run: pip install -r tools/zensical/requirements.txt
+
+ - name: Write the pages
+ run: python tools/zensical/pages.py
+
+ # Strict, as configured in zensical.toml: a broken link or anchor fails.
+ - name: Build the site
+ run: zensical build --clean
+
+ - name: Upload the site
+ if: github.ref == 'refs/heads/main'
+ uses: actions/upload-pages-artifact@v5
+ with:
+ path: site
+
+ deploy:
+ name: Deploy to GitHub Pages
+ if: github.ref == 'refs/heads/main'
+ needs: build
+ runs-on: ubuntu-24.04
+ permissions:
+ pages: write
+ id-token: write
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - name: Setup Pages
+ uses: actions/configure-pages@v6
+
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v5
diff --git a/.gitignore b/.gitignore
index 0b49effe..3bac1c17 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
/.phpunit.cache
/composer.lock
/phpunit.xml
+/site/
/var/
/vendor
/git-builds/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a6b03ec2..e4427a87 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,242 +10,242 @@ 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))
-- Return the dates of commits and tags at the UTC offset of their author, committer or tagger,
- like `git log` shows them, instead of in UTC: the instant doesn't change, but `format()`
- returns their local time.
-- Require PHP 8.4 or later, Symfony 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))
-- The constructors of `Commit`, `Tree`, `Blob`, `Revision`, the references and `ReferenceBag`
- are internal: get these objects from `Repository`, which returns the same commit, tree or
- blob while the application uses it, and reads the references again after its writes.
- Extending `Revision` or `Reference` isn't supported anymore.
-- `ProcessException` has a message of one line, with the error of git, the first line of its
- error output starting with `fatal:` or `error:`, after the warnings:
- `The git command "clone" failed (exit 128): fatal: ...`. The command line, the rest of the
- error output and the output are no longer in the message: `getErrorOutput()` and
- `getOutput()` return the outputs of git.
-- When git can't start, a missing binary for instance, `CliDriver` throws a `DriverException`
- instead of a `ProcessException`, with a message of one line, without the command line:
- `The git command "clone" could not start: ...`.
-- `WorkingCopy::checkout()` throws a `ReferenceNotFoundException` for a name that no revision has,
- with both drivers: the name of a remote branch alone, like `feature` for `origin/feature`,
- created the local branch. Use `checkoutNewBranch()`.
+- 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))
+- Return the dates of commits and tags at the UTC offset of their author, committer or tagger,
+ like `git log` shows them, instead of in UTC: the instant doesn't change, but `format()`
+ returns their local time.
+- Require PHP 8.4 or later, Symfony 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))
+- The constructors of `Commit`, `Tree`, `Blob`, `Revision`, the references and `ReferenceBag`
+ are internal: get these objects from `Repository`, which returns the same commit, tree or
+ blob while the application uses it, and reads the references again after its writes.
+ Extending `Revision` or `Reference` isn't supported anymore.
+- `ProcessException` has a message of one line, with the error of git, the first line of its
+ error output starting with `fatal:` or `error:`, after the warnings:
+ `The git command "clone" failed (exit 128): fatal: ...`. The command line, the rest of the
+ error output and the output are no longer in the message: `getErrorOutput()` and
+ `getOutput()` return the outputs of git.
+- When git can't start, a missing binary for instance, `CliDriver` throws a `DriverException`
+ instead of a `ProcessException`, with a message of one line, without the command line:
+ `The git command "clone" could not start: ...`.
+- `WorkingCopy::checkout()` throws a `ReferenceNotFoundException` for a name that no revision has,
+ with both drivers: the name of a remote branch alone, like `feature` for `origin/feature`,
+ created the local branch. Use `checkoutNewBranch()`.
### Features
-- 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`, from a clone of gitlib,
- 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()`,
- `Repository::getSizeInBytes()` 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
- its exact original behavior.
- ([#247](https://github.com/gitonomy/gitlib/pull/247), fixes #205)
-- Support git's mnemonic diff prefixes (`diff.mnemonicPrefix`) in the diff parser: enabling
- this option replaces the default `a/`/`b/` prefixes with context-specific ones
- (`c/`/`i/`/`o/`/`w/`), which used to raise a `RuntimeException` in tools like GrumPHP that
- enable it. ([#245](https://github.com/gitonomy/gitlib/pull/245), fixes #114)
-- Support `"copy from"`/`"copy to"` in the diff parser: diffs produced with copy detection
- enabled (e.g. `diff.renames = copies`) used to fail to parse. `File` gains a new `isCopy()`
- flag, and `isRename()` no longer reports `true` for copies, since a copy's source file
- still exists afterward.
- ([#244](https://github.com/gitonomy/gitlib/pull/244), fixes #14)
+- 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`, from a clone of gitlib,
+ 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()`,
+ `Repository::getSizeInBytes()` 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
+ its exact original behavior.
+ ([#247](https://github.com/gitonomy/gitlib/pull/247), fixes #205)
+- Support git's mnemonic diff prefixes (`diff.mnemonicPrefix`) in the diff parser: enabling
+ this option replaces the default `a/`/`b/` prefixes with context-specific ones
+ (`c/`/`i/`/`o/`/`w/`), which used to raise a `RuntimeException` in tools like GrumPHP that
+ enable it. ([#245](https://github.com/gitonomy/gitlib/pull/245), fixes #114)
+- Support `"copy from"`/`"copy to"` in the diff parser: diffs produced with copy detection
+ enabled (e.g. `diff.renames = copies`) used to fail to parse. `File` gains a new `isCopy()`
+ flag, and `isRename()` no longer reports `true` for copies, since a copy's source file
+ 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()`.
-- `Repository::getSize()`, in kilobytes: use `getSizeInBytes()`.
-- `Exception\UnexpectedValueException`, which gitlib no longer throws.
+- `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()`.
+- `Repository::getSize()`, in kilobytes: use `getSizeInBytes()`.
+- `Exception\UnexpectedValueException`, which gitlib no longer throws.
### Security
-- Refuse a revision, a reference, a hash or a URL starting with a dash, which git read as an
- option: `getRevision('--output=/path')`, `WorkingCopy::checkout()` or `merge()` could write
- a file, and `Repository::isValidRepository('--upload-pack=...')` run a command. A path may
- start with a dash: `init()` and the clones end the options of git with `--`.
-- Refuse an `ext::` URL, like `ext::sh -c ...`, whose command git runs, by default before git
- 2.12.
-- Refuse an ssh URL whose host or user starts with a dash, even encoded (`%2D`), like
- `ssh://-oProxyCommand=.../repository`: git before 2.14.1 passes it to ssh, which reads it as an
- option and runs the command (CVE-2017-1000117). A `git://` URL whose host starts with a dash
- is refused too: git passes it to the command of `core.gitProxy`.
-- Hide the credentials of URLs (`https://user:token@host`), a password holding a `#` or a `?`
- included, in the messages of exceptions, in their traces (`#[\SensitiveParameter]`), and in
- the logs. The exceptions of Symfony Process, which hold the command line, no longer leak, not
- even as the previous exception.
-- Refuse a hook name that isn't the one of a file of the hooks directory:
- `Hooks::set('../config', ...)` and `remove('../config')` wrote and deleted the
- configuration of the repository.
-- Fix `Hooks` writing through a symbolic link to a missing file, out of the hooks directory:
- `has()` returned `false` for it, and `set()` followed it.
-- Ignore the replacements of objects (`git replace`, `refs/replace/`) with `CliDriver`, like
- `LibGit2Driver`: whoever could push such a reference to a repository changed the message of a
- commit, or the content of a blob, gitlib read for a hash.
-- Fix `WorkingCopy::checkout()` of a path, like `checkout('README.md')`, discarding the changes
- of the file: a path is no revision, and throws a `ReferenceNotFoundException`.
-- Fix moving or deleting a symbolic reference, which changed the branch it points to instead:
- `ReferenceBag::delete('refs/remotes/origin/HEAD')` deleted `origin/main`.
-- Fix `ReferenceBag::delete('HEAD')` deleting `HEAD`, which left no repository: a name outside
- `refs/` throws an `InvalidArgumentException`.
+- Refuse a revision, a reference, a hash or a URL starting with a dash, which git read as an
+ option: `getRevision('--output=/path')`, `WorkingCopy::checkout()` or `merge()` could write
+ a file, and `Repository::isValidRepository('--upload-pack=...')` run a command. A path may
+ start with a dash: `init()` and the clones end the options of git with `--`.
+- Refuse an `ext::` URL, like `ext::sh -c ...`, whose command git runs, by default before git
+ 2.12.
+- Refuse an ssh URL whose host or user starts with a dash, even encoded (`%2D`), like
+ `ssh://-oProxyCommand=.../repository`: git before 2.14.1 passes it to ssh, which reads it as an
+ option and runs the command (CVE-2017-1000117). A `git://` URL whose host starts with a dash
+ is refused too: git passes it to the command of `core.gitProxy`.
+- Hide the credentials of URLs (`https://user:token@host`), a password holding a `#` or a `?`
+ included, in the messages of exceptions, in their traces (`#[\SensitiveParameter]`), and in
+ the logs. The exceptions of Symfony Process, which hold the command line, no longer leak, not
+ even as the previous exception.
+- Refuse a hook name that isn't the one of a file of the hooks directory:
+ `Hooks::set('../config', ...)` and `remove('../config')` wrote and deleted the
+ configuration of the repository.
+- Fix `Hooks` writing through a symbolic link to a missing file, out of the hooks directory:
+ `has()` returned `false` for it, and `set()` followed it.
+- Ignore the replacements of objects (`git replace`, `refs/replace/`) with `CliDriver`, like
+ `LibGit2Driver`: whoever could push such a reference to a repository changed the message of a
+ commit, or the content of a blob, gitlib read for a hash.
+- Fix `WorkingCopy::checkout()` of a path, like `checkout('README.md')`, discarding the changes
+ of the file: a path is no revision, and throws a `ReferenceNotFoundException`.
+- Fix moving or deleting a symbolic reference, which changed the branch it points to instead:
+ `ReferenceBag::delete('refs/remotes/origin/HEAD')` deleted `origin/main`.
+- Fix `ReferenceBag::delete('HEAD')` deleting `HEAD`, which left no repository: a name outside
+ `refs/` throws an `InvalidArgumentException`.
### Bug fixes
-- Fix the configuration of the user changing what `CliDriver` reads: `color.ui=always`,
- `log.showSignature`, `diff.noprefix`, `diff.external`, `diff.submodule`, textconv filters,
- `column.ui`... made reads throw or return wrong data, and `diff.algorithm`, `diff.context`,
- `diff.orderFile`, `log.follow` or `branch.sort` changed diffs, logs and the branches of a
- commit. gitlib sets them back to git's defaults; see
- ["Git configuration"](doc/repository.md#git-configuration).
-- Fix `Commit::getShortHash()`, `Commit::getDiff()` and `Repository::getDiff()` failing when a
- file of the working tree has the name of the revision they read, which git found ambiguous.
-- Fix logs of thousands of revisions exceeding the limit of the command line: `CliDriver` passes
- them to git on its standard input.
-- 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 `Repository::getSize()` throwing for a link to a missing file in the git directory, like
- a hook not deployed yet: it no longer follows links. A file git deletes meanwhile, like a lock
- during a `gc` or a push, no longer makes it throw either.
-- 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
- when run from outside the repository directory.
- ([#249](https://github.com/gitonomy/gitlib/pull/249), fixes #67)
-- Fix diff parsing for dirty submodules without an index line: a submodule whose working
- tree changed but still points at the same commit is reported by git without an `index `
- line, which made the parser choke on the next hunk.
- ([#246](https://github.com/gitonomy/gitlib/pull/246), fixes #165)
-- Fix bugs and hardening issues found by a code audit:
- ([#239](https://github.com/gitonomy/gitlib/pull/239))
- - `ReferenceBag::getFirstBranch()` could return a non-branch reference, or throw, when no
- branch existed.
- - `Tag::getBodyMessage()` dropped or truncated a line for annotated tags using the
- standard `subject\n\nbody` convention.
- - 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
- `0700`.
- - Fixed the `isSuccessFul()` casing typo in `Admin`.
+- Fix the configuration of the user changing what `CliDriver` reads: `color.ui=always`,
+ `log.showSignature`, `diff.noprefix`, `diff.external`, `diff.submodule`, textconv filters,
+ `column.ui`... made reads throw or return wrong data, and `diff.algorithm`, `diff.context`,
+ `diff.orderFile`, `log.follow` or `branch.sort` changed diffs, logs and the branches of a
+ commit. gitlib sets them back to git's defaults; see
+ ["Git configuration"](doc/repository.md#git-configuration).
+- Fix `Commit::getShortHash()`, `Commit::getDiff()` and `Repository::getDiff()` failing when a
+ file of the working tree has the name of the revision they read, which git found ambiguous.
+- Fix logs of thousands of revisions exceeding the limit of the command line: `CliDriver` passes
+ them to git on its standard input.
+- 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 `Repository::getSize()` throwing for a link to a missing file in the git directory, like
+ a hook not deployed yet: it no longer follows links. A file git deletes meanwhile, like a lock
+ during a `gc` or a push, no longer makes it throw either.
+- 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
+ when run from outside the repository directory.
+ ([#249](https://github.com/gitonomy/gitlib/pull/249), fixes #67)
+- Fix diff parsing for dirty submodules without an index line: a submodule whose working
+ tree changed but still points at the same commit is reported by git without an `index `
+ line, which made the parser choke on the next hunk.
+ ([#246](https://github.com/gitonomy/gitlib/pull/246), fixes #165)
+- Fix bugs and hardening issues found by a code audit:
+ ([#239](https://github.com/gitonomy/gitlib/pull/239))
+ - `ReferenceBag::getFirstBranch()` could return a non-branch reference, or throw, when no
+ branch existed.
+ - `Tag::getBodyMessage()` dropped or truncated a line for annotated tags using the
+ standard `subject\n\nbody` convention.
+ - 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
+ `0700`.
+ - Fixed the `isSuccessFul()` casing typo in `Admin`.
### Minor
-- CI tests git 1.8.4 and 2.25.1, analyses the tests and the libgit2 driver with PHPStan,
- without baseline, checks the archive Composer installs, pins the third-party actions by
- commit, and the suite deletes what it creates, in any order.
- ([#253](https://github.com/gitonomy/gitlib/pull/253))
-- The tests run with a git configuration of their own, whatever the developer's
- (`tests/fixtures/home`), which sets what gitlib sets back to git's defaults, and without the
- variables of git of their environment: run from a hook, they wrote the repository of the hook
- (`GIT_DIR`, `GIT_INDEX_FILE`...).
-- 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,
- audited git bundle, plus a Console tool to regenerate it.
- ([#241](https://github.com/gitonomy/gitlib/pull/241),
- [#242](https://github.com/gitonomy/gitlib/pull/242))
-- CI: bump `actions/checkout` to v7 to fix a Node 20 deprecation warning.
- ([#240](https://github.com/gitonomy/gitlib/pull/240))
+- CI tests git 1.8.4 and 2.25.1, analyses the tests and the libgit2 driver with PHPStan,
+ without baseline, checks the archive Composer installs, pins the third-party actions by
+ commit, and the suite deletes what it creates, in any order.
+ ([#253](https://github.com/gitonomy/gitlib/pull/253))
+- The tests run with a git configuration of their own, whatever the developer's
+ (`tests/fixtures/home`), which sets what gitlib sets back to git's defaults, and without the
+ variables of git of their environment: run from a hook, they wrote the repository of the hook
+ (`GIT_DIR`, `GIT_INDEX_FILE`...).
+- 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,
+ audited git bundle, plus a Console tool to regenerate it.
+ ([#241](https://github.com/gitonomy/gitlib/pull/241),
+ [#242](https://github.com/gitonomy/gitlib/pull/242))
+- CI: bump `actions/checkout` to v7 to fix a Node 20 deprecation warning.
+ ([#240](https://github.com/gitonomy/gitlib/pull/240))
## [v1.6.0] - 2025-12-09
diff --git a/UPGRADE-2.0.md b/UPGRADE-2.0.md
index ddd2c018..e44b082c 100644
--- a/UPGRADE-2.0.md
+++ b/UPGRADE-2.0.md
@@ -15,570 +15,571 @@ changes, listed below too, break code that relies on them.
Requirements
------------
- * PHP 8.4 or later is required.
- * git 1.8.4 or later is required by `CliDriver`, the default driver: before,
- git simplifies the log of a range filtered by path differently, and it
- can't count commits (`Log::count()`) before 1.7.2.
- * `symfony/process` must be `^6.4`, `^7.4` or `^8.1`.
- * `symfony/string` `^7.4.6` or `^8.0.6`, `symfony/deprecation-contracts`
- `^2.5` or `^3`, and `psr/log` `^1.1`, `^2.0` or `^3.0` are new
- dependencies.
- * Repositories using SHA-256 hashes are refused, with an
- `UnsupportedOperationException`, by both drivers.
+* PHP 8.4 or later is required.
+* git 1.8.4 or later is required by `CliDriver`, the default driver: before,
+ git simplifies the log of a range filtered by path differently, and it
+ can't count commits (`Log::count()`) before 1.7.2.
+* `symfony/process` must be `^6.4`, `^7.4` or `^8.1`.
+* `symfony/string` `^7.4.6` or `^8.0.6`, `symfony/deprecation-contracts`
+ `^2.5` or `^3`, and `psr/log` `^1.1`, `^2.0` or `^3.0` are new
+ dependencies.
+* Repositories using SHA-256 hashes are refused, with an
+ `UnsupportedOperationException`, by both drivers.
Opening a repository
--------------------
- * The second argument of `Repository` is the working directory, when the
- git directory is 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 `Gitonomy\Git\Driver\Cli\CliDriver`, which `Repository` takes as its last
- argument. They can't be combined with this argument.
- * The `inherit_environment_variables` option has no counterpart in
- `CliDriver`: git inherits the environment variables of PHP anyway, and
- `$_SERVER` holds, under php-fpm, the headers and the credentials of the
- request. Pass the variables git needs to the `environmentVariables`
- argument instead.
-
- Before:
-
- ```php
- $repository = new Repository($path, [
- 'command' => '/usr/bin/git',
- 'logger' => $logger,
- 'process_timeout' => 60,
- ]);
- ```
-
- After:
-
- ```php
- use Gitonomy\Git\Driver\Cli\CliDriver;
-
- $repository = new Repository($path, driver: new CliDriver(command: '/usr/bin/git', processTimeout: 60, logger: $logger));
- ```
-
- * The logger is an argument of `CliDriver`, which logs every git command it
- runs, cloning and initializing included: `Repository::setLogger()` and
- `Repository::getLogger()` are deprecated. `LibGit2Driver` runs no command,
- and logs nothing.
- * An unknown option throws an `InvalidArgumentException`, instead of being
- ignored.
- * The `debug` option is deprecated, and setting it to `false` throws an
- `InvalidArgumentException`: see [Errors](#errors).
- * `$path` may be a linked worktree or a submodule, whose `.git` is a file.
- * A directory that isn't a repository throws an `InvalidArgumentException`
- right away, instead of a `ProcessException` at the first operation.
+* The second argument of `Repository` is the working directory, when the
+ git directory is 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 `Gitonomy\Git\Driver\Cli\CliDriver`, which `Repository` takes as its last
+ argument. They can't be combined with this argument.
+* The `inherit_environment_variables` option has no counterpart in
+ `CliDriver`: git inherits the environment variables of PHP anyway, and
+ `$_SERVER` holds, under php-fpm, the headers and the credentials of the
+ request. Pass the variables git needs to the `environmentVariables`
+ argument instead.
+
+ Before:
+
+ ```php
+ $repository = new Repository($path, [
+ 'command' => '/usr/bin/git',
+ 'logger' => $logger,
+ 'process_timeout' => 60,
+ ]);
+ ```
+
+ After:
+
+ ```php
+ use Gitonomy\Git\Driver\Cli\CliDriver;
+
+ $repository = new Repository($path, driver: new CliDriver(command: '/usr/bin/git', processTimeout: 60, logger: $logger));
+ ```
+
+* The logger is an argument of `CliDriver`, which logs every git command it
+ runs, cloning and initializing included: `Repository::setLogger()` and
+ `Repository::getLogger()` are deprecated. `LibGit2Driver` runs no command,
+ and logs nothing.
+* An unknown option throws an `InvalidArgumentException`, instead of being
+ ignored.
+* The `debug` option is deprecated, and setting it to `false` throws an
+ `InvalidArgumentException`: see [Errors](#errors).
+* `$path` may be a linked worktree or a submodule, whose `.git` is a file.
+* A directory that isn't a repository throws an `InvalidArgumentException`
+ right away, instead of a `ProcessException` at the first operation.
Creating repositories
---------------------
- * `Admin` is deprecated: use the static methods of `Repository`. They take
- the URL before the path, have a method for each kind of repository
- instead of the `$bare` argument, whose default was a bare repository, and
- take the driver as last argument:
-
- | 1.x | 2.0 |
- |-----------------------------------------------------|--------------------------------------------------------------|
- | `Admin::init($path)` | `Repository::initBare($path)` |
- | `Admin::init($path, false)` | `Repository::init($path)` |
- | `Admin::cloneTo($path, $url)` | `Repository::cloneBareFrom($url, $path)` |
- | `Admin::cloneTo($path, $url, false)` | `Repository::cloneFrom($url, $path)` |
- | `Admin::cloneBranchTo($path, $url, $branch)` | `Repository::cloneBareFrom($url, $path, $branch)` |
- | `Admin::cloneBranchTo($path, $url, $branch, false)` | `Repository::cloneFrom($url, $path, $branch)` |
- | `Admin::mirrorTo($path, $url)` | `Repository::mirrorFrom($url, $path)` |
- | `Admin::isValidRepository($url)` | `Repository::isValidRepository($url)` |
- | `Admin::isValidRepositoryAndBranch($url, $branch)` | `Repository::isValidRepositoryAndBranch($url, $branch)` |
- | `$repository->cloneTo($path)` | `Repository::cloneBareFrom($repository->getGitDir(), $path)` |
- | `$repository->cloneTo($path, false)` | `Repository::cloneFrom($repository->getGitDir(), $path)` |
-
- Until 3.0, `Admin` keeps working like in 1.x, with `CliDriver` and the
- options of 1.x.
- * `isValidRepositoryAndBranch()` checks the exact name of the branch:
- `release` no longer matches `feature/release`, nor `rel*` `release`.
- * `isValidRepository()` and `isValidRepositoryAndBranch()` read a relative
- path as a path, never as the name of a remote of PHP's working directory,
- and throw a `DriverException` when git can't start, instead of returning
- `false`.
-
- Before:
-
- ```php
- $repository = Admin::cloneTo($path, $url, false, ['process_timeout' => 60]);
- ```
-
- After:
-
- ```php
- use Gitonomy\Git\Driver\Cli\CliDriver;
-
- $repository = Repository::cloneFrom($url, $path, driver: new CliDriver(processTimeout: 60));
- ```
-
- * `Repository::cloneTo()` cloned with the driver of the repository: pass it
- to `cloneFrom()` or `cloneBareFrom()` as well.
- * `Admin::cloneRepository()` is deprecated, with no replacement for the
- arguments of `git clone` other than `--bare`, `--branch` and `--mirror`:
- run `git clone` yourself, then open the repository with
- `new Repository($path)`.
+* `Admin` is deprecated: use the static methods of `Repository`. They take
+ the URL before the path, have a method for each kind of repository
+ instead of the `$bare` argument, whose default was a bare repository, and
+ take the driver as last argument:
+
+ | 1.x | 2.0 |
+ |-----------------------------------------------------|--------------------------------------------------------------|
+ | `Admin::init($path)` | `Repository::initBare($path)` |
+ | `Admin::init($path, false)` | `Repository::init($path)` |
+ | `Admin::cloneTo($path, $url)` | `Repository::cloneBareFrom($url, $path)` |
+ | `Admin::cloneTo($path, $url, false)` | `Repository::cloneFrom($url, $path)` |
+ | `Admin::cloneBranchTo($path, $url, $branch)` | `Repository::cloneBareFrom($url, $path, $branch)` |
+ | `Admin::cloneBranchTo($path, $url, $branch, false)` | `Repository::cloneFrom($url, $path, $branch)` |
+ | `Admin::mirrorTo($path, $url)` | `Repository::mirrorFrom($url, $path)` |
+ | `Admin::isValidRepository($url)` | `Repository::isValidRepository($url)` |
+ | `Admin::isValidRepositoryAndBranch($url, $branch)` | `Repository::isValidRepositoryAndBranch($url, $branch)` |
+ | `$repository->cloneTo($path)` | `Repository::cloneBareFrom($repository->getGitDir(), $path)` |
+ | `$repository->cloneTo($path, false)` | `Repository::cloneFrom($repository->getGitDir(), $path)` |
+
+ Until 3.0, `Admin` keeps working like in 1.x, with `CliDriver` and the
+ options of 1.x.
+
+* `isValidRepositoryAndBranch()` checks the exact name of the branch:
+ `release` no longer matches `feature/release`, nor `rel*` `release`.
+* `isValidRepository()` and `isValidRepositoryAndBranch()` read a relative
+ path as a path, never as the name of a remote of PHP's working directory,
+ and throw a `DriverException` when git can't start, instead of returning
+ `false`.
+
+ Before:
+
+ ```php
+ $repository = Admin::cloneTo($path, $url, false, ['process_timeout' => 60]);
+ ```
+
+ After:
+
+ ```php
+ use Gitonomy\Git\Driver\Cli\CliDriver;
+
+ $repository = Repository::cloneFrom($url, $path, driver: new CliDriver(processTimeout: 60));
+ ```
+
+* `Repository::cloneTo()` cloned with the driver of the repository: pass it
+ to `cloneFrom()` or `cloneBareFrom()` as well.
+* `Admin::cloneRepository()` is deprecated, with no replacement for the
+ arguments of `git clone` other than `--bare`, `--branch` and `--mirror`:
+ run `git clone` yourself, then open the repository with
+ `new Repository($path)`.
References
----------
- * `Repository::getReferences()` reads the references again after every
- write gitlib makes (creating or deleting a reference, a checkout, a merge,
- `run()`...): passing `true` to read them again is deprecated. Open a new
- `Repository` to see the writes of other processes. A `Reference` object
- keeps the commit it pointed to when it was read.
- * `Reference::getCommitHash()` returns the commit an annotated tag points
- to, instead of the hash of the tag object: `ReferenceBag::resolve()`,
- `resolveTags()` and `Commit::resolveReferences()` find annotated tags.
- * `ReferenceBag::createBranch()` and `createTag()` throw an
- `InvalidArgumentException` when the reference exists, instead of moving
- it, and take a `Revision` or a revision name as second argument, instead
- of a commit hash only.
- * `ReferenceBag::update()` is deprecated: use `Branch::moveTo()`.
-
- Before:
-
- ```php
- $repository->getReferences()->update(new Branch($repository, 'refs/heads/main', $hash));
- ```
-
- After:
-
- ```php
- $repository->getReferences()->getBranch('main')->moveTo($hash);
- ```
-
- * `Branch::isMergedTo()` is deprecated: use `isMergedInto()`, which takes
- the destination as a `Revision` or a revision name, without a default
- (`'master'` before) nor `$compareOnlyWithRemote`.
- * Passing arguments to `Commit::getIncludingBranches()` is deprecated: it
- returns the local and remote branches, which `Branch::isLocal()` and
- `isRemote()` filter.
-
- Before:
-
- ```php
- $branches = $commit->getIncludingBranches(true, false);
- ```
-
- After:
-
- ```php
- $branches = array_filter($commit->getIncludingBranches(), static fn (Branch $branch): bool => $branch->isLocal());
- ```
-
- * `ReferenceBag::getFirstBranch()` returns `null` instead of `false` when
- there is no branch, and only branches.
- * `Repository::getHead()` returns a `Reference\Branch` or a `Commit`.
- `HEAD` pointing to another reference, like a tag, which git allows,
- throws a `ReferenceNotFoundException`, where 1.x returned the reference:
- `getHeadCommit()` still returns its commit.
- * The getters of `Reference\Tag` (`getTaggerName()`, `getTaggerEmail()`,
- `getTaggerDate()`, `getMessage()`, `getSubjectMessage()`,
- `getBodyMessage()`, `getGpgSignature()`) return `null` instead of `false`
- for a lightweight tag. The tagger is `null` for an annotated tag without
- one too, where gitlib threw. `getGpgSignature()` returns `null` for an
- unsigned tag instead of throwing, and finds X.509 and SSH signatures too.
- * A branch whose name ends with `HEAD` is listed: only the `HEAD` of a
- remote is left out.
- * Moving or deleting a symbolic reference, like `refs/remotes/origin/HEAD`,
- changes it, instead of the branch it points to.
- * `ReferenceBag::delete()` throws an `InvalidArgumentException` for a name
- outside `refs/`: `HEAD`, which `CliDriver` deleted, leaving no
- repository, or a short name, like `master`, which did nothing.
+* `Repository::getReferences()` reads the references again after every
+ write gitlib makes (creating or deleting a reference, a checkout, a merge,
+ `run()`...): passing `true` to read them again is deprecated. Open a new
+ `Repository` to see the writes of other processes. A `Reference` object
+ keeps the commit it pointed to when it was read.
+* `Reference::getCommitHash()` returns the commit an annotated tag points
+ to, instead of the hash of the tag object: `ReferenceBag::resolve()`,
+ `resolveTags()` and `Commit::resolveReferences()` find annotated tags.
+* `ReferenceBag::createBranch()` and `createTag()` throw an
+ `InvalidArgumentException` when the reference exists, instead of moving
+ it, and take a `Revision` or a revision name as second argument, instead
+ of a commit hash only.
+* `ReferenceBag::update()` is deprecated: use `Branch::moveTo()`.
+
+ Before:
+
+ ```php
+ $repository->getReferences()->update(new Branch($repository, 'refs/heads/main', $hash));
+ ```
+
+ After:
+
+ ```php
+ $repository->getReferences()->getBranch('main')->moveTo($hash);
+ ```
+
+* `Branch::isMergedTo()` is deprecated: use `isMergedInto()`, which takes
+ the destination as a `Revision` or a revision name, without a default
+ (`'master'` before) nor `$compareOnlyWithRemote`.
+* Passing arguments to `Commit::getIncludingBranches()` is deprecated: it
+ returns the local and remote branches, which `Branch::isLocal()` and
+ `isRemote()` filter.
+
+ Before:
+
+ ```php
+ $branches = $commit->getIncludingBranches(true, false);
+ ```
+
+ After:
+
+ ```php
+ $branches = array_filter($commit->getIncludingBranches(), static fn (Branch $branch): bool => $branch->isLocal());
+ ```
+
+* `ReferenceBag::getFirstBranch()` returns `null` instead of `false` when
+ there is no branch, and only branches.
+* `Repository::getHead()` returns a `Reference\Branch` or a `Commit`.
+ `HEAD` pointing to another reference, like a tag, which git allows,
+ throws a `ReferenceNotFoundException`, where 1.x returned the reference:
+ `getHeadCommit()` still returns its commit.
+* The getters of `Reference\Tag` (`getTaggerName()`, `getTaggerEmail()`,
+ `getTaggerDate()`, `getMessage()`, `getSubjectMessage()`,
+ `getBodyMessage()`, `getGpgSignature()`) return `null` instead of `false`
+ for a lightweight tag. The tagger is `null` for an annotated tag without
+ one too, where gitlib threw. `getGpgSignature()` returns `null` for an
+ unsigned tag instead of throwing, and finds X.509 and SSH signatures too.
+* A branch whose name ends with `HEAD` is listed: only the `HEAD` of a
+ remote is left out.
+* Moving or deleting a symbolic reference, like `refs/remotes/origin/HEAD`,
+ changes it, instead of the branch it points to.
+* `ReferenceBag::delete()` throws an `InvalidArgumentException` for a name
+ outside `refs/`: `HEAD`, which `CliDriver` deleted, leaving no
+ repository, or a short name, like `master`, which did nothing.
History
-------
- * `Log::setOffset()` and `setLimit()` are deprecated: `withOffset()` and
- `withLimit()` return another `Log`.
- * `count($log)` returns the number of commits the log iterates, after its
- offset and up to its limit; `countCommits()` still counts them all.
- * The revisions of a log are names, ranges (`a..b`, `a...b`), names to
- exclude (`^a`) or `--all`: any other option of `git log` throws an
- `InvalidArgumentException`, instead of being passed to git. An unknown
- revision throws a `ReferenceNotFoundException` naming it.
- * `Log::getDiff()` is deprecated: it ignored the paths of the log. Use
- `Repository::getDiff()`.
+* `Log::setOffset()` and `setLimit()` are deprecated: `withOffset()` and
+ `withLimit()` return another `Log`.
+* `count($log)` returns the number of commits the log iterates, after its
+ offset and up to its limit; `countCommits()` still counts them all.
+* The revisions of a log are names, ranges (`a..b`, `a...b`), names to
+ exclude (`^a`) or `--all`: any other option of `git log` throws an
+ `InvalidArgumentException`, instead of being passed to git. An unknown
+ revision throws a `ReferenceNotFoundException` naming it.
+* `Log::getDiff()` is deprecated: it ignored the paths of the log. Use
+ `Repository::getDiff()`.
Diffs
-----
- * `Repository::getDiff()` takes the commits to compare: `$from` and `$to`.
- Calling it with one argument is deprecated.
-
- | 1.x | 2.0 |
- |------------------------------------|------------------------------------------------------------------|
- | `$repository->getDiff('a..b')` | `$repository->getDiff('a', 'b')` |
- | `$repository->getDiff(['a', 'b'])` | `$repository->getDiff('a', 'b')` |
- | `$repository->getDiff('a...b')` | `$repository->getDiff($repository->getMergeBase('a', 'b'), 'b')` |
- | `$repository->getDiff('a')` | `$repository->getWorkingCopy()->getDiff('a')` |
-
- * The diff of a merge commit, `Commit::getDiff()`, is against its first
- parent, like GitHub shows it, with both drivers.
- * `Diff`, `Diff\File` and `Diff\FileChange` get their repository and their
- changes when constructed: `Diff::setRepository()`,
- `File::setRepository()` and `File::addChange()` are deprecated.
- `Diff::parse()` and `fromArray()` take the repository to read blobs from.
- * The lines of a hunk, `FileChange::getLines()`, are `Diff\DiffLine`
- objects, with `getType()`, a `Diff\DiffLineType`, and `getContent()`:
- reading them as arrays is deprecated, as the `FileChange::LINE_*`
- constants and `FileChange::getCount()`, replaced by `getAdditions()` and
- `getDeletions()`.
-
- Before:
-
- ```php
- foreach ($change->getLines() as [$type, $content]) {
- if (FileChange::LINE_ADD === $type) {
- // ...
- }
- }
- ```
-
- After:
-
- ```php
- use Gitonomy\Git\Diff\DiffLineType;
-
- foreach ($change->getLines() as $line) {
- if (DiffLineType::Addition === $line->getType()) {
- // ... $line->getContent()
- }
- }
- ```
-
- * `File::isChangeMode()`, `isDelete()` and `getAnchor()` are deprecated:
- use `isModeChange()`, `isDeletion()`, and
- `substr($file->getNewIndex(), 0, 12)`.
- * `File::getOldIndex()` and `getNewIndex()` return `null` for a missing
- hash, never an empty string; `getName()` returns a `string`.
- * `File::getOldBlob()` and `getNewBlob()` throw a `LogicException` without
- repository, and a `RuntimeException` without hash.
- * `Diff::parse()` throws an `InvalidArgumentException` for a diff without
- its raw information, deprecated since 1.6.0: parse the output of
- `git diff --raw --patch`, with `-z` or not. It reads the names of the
- files from the raw information: names with spaces are no longer split at
- the wrong place.
- * During a merge conflict, `WorkingCopy::getDiffPending()` and
- `getDiffStaged()` leave the unmerged files out, where they threw, and
- `WorkingCopy::getDiff()` compares them, conflict markers included, to the
- revision, like `git diff HEAD`.
+* `Repository::getDiff()` takes the commits to compare: `$from` and `$to`.
+ Calling it with one argument is deprecated.
+
+ | 1.x | 2.0 |
+ |------------------------------------|------------------------------------------------------------------|
+ | `$repository->getDiff('a..b')` | `$repository->getDiff('a', 'b')` |
+ | `$repository->getDiff(['a', 'b'])` | `$repository->getDiff('a', 'b')` |
+ | `$repository->getDiff('a...b')` | `$repository->getDiff($repository->getMergeBase('a', 'b'), 'b')` |
+ | `$repository->getDiff('a')` | `$repository->getWorkingCopy()->getDiff('a')` |
+
+* The diff of a merge commit, `Commit::getDiff()`, is against its first
+ parent, like GitHub shows it, with both drivers.
+* `Diff`, `Diff\File` and `Diff\FileChange` get their repository and their
+ changes when constructed: `Diff::setRepository()`,
+ `File::setRepository()` and `File::addChange()` are deprecated.
+ `Diff::parse()` and `fromArray()` take the repository to read blobs from.
+* The lines of a hunk, `FileChange::getLines()`, are `Diff\DiffLine`
+ objects, with `getType()`, a `Diff\DiffLineType`, and `getContent()`:
+ reading them as arrays is deprecated, as the `FileChange::LINE_*`
+ constants and `FileChange::getCount()`, replaced by `getAdditions()` and
+ `getDeletions()`.
+
+ Before:
+
+ ```php
+ foreach ($change->getLines() as [$type, $content]) {
+ if (FileChange::LINE_ADD === $type) {
+ // ...
+ }
+ }
+ ```
+
+ After:
+
+ ```php
+ use Gitonomy\Git\Diff\DiffLineType;
+
+ foreach ($change->getLines() as $line) {
+ if (DiffLineType::Addition === $line->getType()) {
+ // ... $line->getContent()
+ }
+ }
+ ```
+
+* `File::isChangeMode()`, `isDelete()` and `getAnchor()` are deprecated:
+ use `isModeChange()`, `isDeletion()`, and
+ `substr($file->getNewIndex(), 0, 12)`.
+* `File::getOldIndex()` and `getNewIndex()` return `null` for a missing
+ hash, never an empty string; `getName()` returns a `string`.
+* `File::getOldBlob()` and `getNewBlob()` throw a `LogicException` without
+ repository, and a `RuntimeException` without hash.
+* `Diff::parse()` throws an `InvalidArgumentException` for a diff without
+ its raw information, deprecated since 1.6.0: parse the output of
+ `git diff --raw --patch`, with `-z` or not. It reads the names of the
+ files from the raw information: names with spaces are no longer split at
+ the wrong place.
+* During a merge conflict, `WorkingCopy::getDiffPending()` and
+ `getDiffStaged()` leave the unmerged files out, where they threw, and
+ `WorkingCopy::getDiff()` compares them, conflict markers included, to the
+ revision, like `git diff HEAD`.
Blame
-----
- * `Repository::getBlame()` takes the first and the last lines to blame:
- a range as a string (`'3,5'`, `'3,+3'`) is deprecated, and a regular
- expression is no longer supported.
- * `Blame::getLines()` and `getLine()` number the lines like the file, from
- the first blamed line, instead of from 1.
- * `Blame\Line::getNumber()` and `getOriginalNumber()` return the number of
- the line in the file and in the commit that changed it, as integers:
- `Line::getLine()` is deprecated.
- * `Blame::getGroupedLines()` is deprecated: `getHunks()` returns
- `Blame\Hunk` objects.
+* `Repository::getBlame()` takes the first and the last lines to blame:
+ a range as a string (`'3,5'`, `'3,+3'`) is deprecated, and a regular
+ expression is no longer supported.
+* `Blame::getLines()` and `getLine()` number the lines like the file, from
+ the first blamed line, instead of from 1.
+* `Blame\Line::getNumber()` and `getOriginalNumber()` return the number of
+ the line in the file and in the commit that changed it, as integers:
+ `Line::getLine()` is deprecated.
+* `Blame::getGroupedLines()` is deprecated: `getHunks()` returns
+ `Blame\Hunk` objects.
Trees
-----
- * `Tree::getEntries()`, `getTreeEntries()`, `getBlobEntries()` and
- `getSubmoduleEntries()` return `TreeEntry` objects, with `getName()`,
- `getMode()` and `getObject()`: reading them as arrays
- (`[$mode, $object]`) is deprecated.
- * `CommitReference` is deprecated: it is `Submodule`. So is
- `Tree::getCommitReferenceEntries()`: use `getSubmoduleEntries()`.
- * `Tree::resolvePath()` throws an `InvalidArgumentException` explaining
- why a path doesn't resolve, a path through a submodule included, which
- threw an `UnexpectedValueException`, and resolves `/` to the tree.
+* `Tree::getEntries()`, `getTreeEntries()`, `getBlobEntries()` and
+ `getSubmoduleEntries()` return `TreeEntry` objects, with `getName()`,
+ `getMode()` and `getObject()`: reading them as arrays
+ (`[$mode, $object]`) is deprecated.
+* `CommitReference` is deprecated: it is `Submodule`. So is
+ `Tree::getCommitReferenceEntries()`: use `getSubmoduleEntries()`.
+* `Tree::resolvePath()` throws an `InvalidArgumentException` explaining
+ why a path doesn't resolve, a path through a submodule included, which
+ threw an `UnexpectedValueException`, and resolves `/` to the tree.
Working copy
------------
- * `WorkingCopy::checkout()` takes a `Revision` or a revision name: a local
- `Branch` object attaches `HEAD` to the branch, instead of detaching it.
- Its second argument, a branch to create, is deprecated: use
- `checkoutNewBranch($branch, $startPoint)`.
- * `WorkingCopy::checkout()` throws a `ReferenceNotFoundException` for a
- name that no revision has, with both drivers: the name of a remote
- branch alone, like `feature` for `origin/feature`, created the local
- branch, and a path, like `README.md`, discarded its changes.
+* `WorkingCopy::checkout()` takes a `Revision` or a revision name: a local
+ `Branch` object attaches `HEAD` to the branch, instead of detaching it.
+ Its second argument, a branch to create, is deprecated: use
+ `checkoutNewBranch($branch, $startPoint)`.
+* `WorkingCopy::checkout()` throws a `ReferenceNotFoundException` for a
+ name that no revision has, with both drivers: the name of a remote
+ branch alone, like `feature` for `origin/feature`, created the local
+ branch, and a path, like `README.md`, discarded its changes.
- Before:
+ Before:
- ```php
- $workingCopy->checkout('feature');
- ```
+ ```php
+ $workingCopy->checkout('feature');
+ ```
- After:
+ After:
- ```php
- $workingCopy->checkoutNewBranch('feature', 'origin/feature');
- ```
+ ```php
+ $workingCopy->checkoutNewBranch('feature', 'origin/feature');
+ ```
- * `WorkingCopy::merge()` takes a `FastForward` instead of arguments of
- `git merge`, deprecated.
+* `WorkingCopy::merge()` takes a `FastForward` instead of arguments of
+ `git merge`, deprecated.
- Before:
+ Before:
- ```php
- $workingCopy->merge('feature', ['--no-ff']);
- ```
+ ```php
+ $workingCopy->merge('feature', ['--no-ff']);
+ ```
- After:
+ After:
- ```php
- use Gitonomy\Git\FastForward;
+ ```php
+ use Gitonomy\Git\FastForward;
- $workingCopy->merge('feature', FastForward::Never);
- ```
+ $workingCopy->merge('feature', FastForward::Never);
+ ```
Commits, trees and blobs
------------------------
- * `Repository::getCommit()` takes a full hash. `getTree()` and `getBlob()`
- take a hash, abbreviated or not, instead of any revision: use
- `getRevision($name)->getCommit()` for a name.
- * Reading an object of another type throws a `ReferenceNotFoundException`
- ("The object ... is not a tree."): `getTree()` read the tree of a commit,
- and `getBlob()` threw a `ProcessException`.
- * The subject of a commit or a tag is its first paragraph, on one line, and
- its body what follows, like `git log --format=%s` and `%b`: a message
- without an empty line after its first line has no body, where its second
- line was lost.
- * `Commit::getShortMessage()` and `getFixedShortHash()` are deprecated: use
- `symfony/string` (`u($commit->getSubjectMessage())->truncate(50, '...')`)
- and `substr()`.
- * `Blob::getMimetype()` returns the mimetype alone (`text/plain`, instead
- of `text/plain; charset=us-ascii`), and throws instead of returning
- `false`.
- * `Repository` keeps the objects it returns only while your application
- uses them: memory no longer grows with every commit read.
+* `Repository::getCommit()` takes a full hash. `getTree()` and `getBlob()`
+ take a hash, abbreviated or not, instead of any revision: use
+ `getRevision($name)->getCommit()` for a name.
+* Reading an object of another type throws a `ReferenceNotFoundException`
+ ("The object ... is not a tree."): `getTree()` read the tree of a commit,
+ and `getBlob()` threw a `ProcessException`.
+* The subject of a commit or a tag is its first paragraph, on one line, and
+ its body what follows, like `git log --format=%s` and `%b`: a message
+ without an empty line after its first line has no body, where its second
+ line was lost.
+* `Commit::getShortMessage()` and `getFixedShortHash()` are deprecated: use
+ `symfony/string` (`u($commit->getSubjectMessage())->truncate(50, '...')`)
+ and `substr()`.
+* `Blob::getMimetype()` returns the mimetype alone (`text/plain`, instead
+ of `text/plain; charset=us-ascii`), and throws instead of returning
+ `false`.
+* `Repository` keeps the objects it returns only while your application
+ uses them: memory no longer grows with every commit read.
Errors
------
- * A failing git operation always throws, a
- `Gitonomy\Git\Exception\ProcessException` with `CliDriver`. Nothing
- returns `null` or empty data instead anymore: `Repository::run()` returns
- a `string`, and `Repository::getHead()` and `Repository::getHeadCommit()`
- never return `null`.
-
- Before:
-
- ```php
- $repository = new Repository($path, ['debug' => false]);
-
- if (null === $output = $repository->run('fetch', ['--all'])) {
- // handle the failure
- }
- ```
-
- After:
-
- ```php
- $repository = new Repository($path);
-
- try {
- $output = $repository->run('fetch', ['--all']);
- } catch (ProcessException $e) {
- // handle the failure
- }
- ```
-
- * Failures are no longer logged at the `error` level: the exception reports
- them, and the logger gets their error output at the `debug` level. Alerts
- based on `error` logs must rely on exceptions instead.
- * A missing revision, reference or object throws a
- `ReferenceNotFoundException`, with both drivers: it extends
- `Gitonomy\Git\Exception\InvalidArgumentException`, still an
- `\InvalidArgumentException`, and takes a whole message instead of a name.
- * A timeout throws a `Gitonomy\Git\Exception\ProcessTimedOutException`,
- and the other failures of Symfony Process a `DriverException`: no
- exception of Symfony or of PHP leaks anymore, not even as the previous
- exception, as they hold the command line.
- * `ProcessException` extends the new
- `Gitonomy\Git\Exception\DriverException`, itself a `RuntimeException`:
- catching `RuntimeException` or `GitExceptionInterface` still works.
- * The message of a `ProcessException` is one line, with the error of git:
- the first line of its error output starting with `fatal:` or `error:`,
- after the warnings, or its first line: `The git command "clone" failed
- (exit 128): fatal: ...`, or `The git command "diff" failed (exit 1).`
- when git writes no error. The command line, the rest of the error output
- and the output are no longer in the message: `getErrorOutput()` and
- `getOutput()` return the outputs of git. Its constructor takes the git
- command as second argument, optional.
- * When git can't start, a missing binary for instance, `CliDriver` throws
- a `DriverException` instead of a `ProcessException`, with a message of
- one line, without the command line: `The git command "clone" could not
- start: ...`.
- * These methods throw instead of returning a fallback value:
- * `Commit::getIncludingBranches()` threw nothing and returned `[]` when git
- failed;
- * `ReferenceBag` (`Repository::getReferences()`) only returns an empty bag
- for a repository without references, and throws for any other failure.
- * `Gitonomy\Git\Exception\UnexpectedValueException` is deprecated: gitlib
- no longer throws it. Catch `InvalidArgumentException` for
- `Tree::resolvePath()`.
- * `Commit::getShortHash()` throws a `ReferenceNotFoundException` for an
- unknown commit, instead of a `ProcessException`.
- * `Hooks` throws a `RuntimeException` when a hook exists already, doesn't
- exist, or can't be written, instead of a `LogicException` or a silent
- failure, and creates the hooks directory.
- * The `Admin` methods throw a `ProcessException`, still a
- `RuntimeException`, with the first line of git's error output in its
- message, or a `DriverException` when git can't start.
- * An argument holding a NUL byte throws an `InvalidArgumentException`,
- where PHP threw a `ValueError`: git and libgit2 read their arguments as C
- strings, which end at the first NUL byte.
- * A revision, a reference, a hash or a URL starting with a dash throws an
- `InvalidArgumentException`, with both drivers: git read it as an option,
- `getRevision('--output=/path')` writing a file, or
- `isValidRepository('--upload-pack=...')` running a command. A path may
- start with a dash.
- * An `ext::` URL throws an `InvalidArgumentException`, with both drivers:
- git runs its command, by default before git 2.12.
- * An ssh URL whose host or user starts with a dash, even encoded (`%2D`),
- like `ssh://-oProxyCommand=.../repository` or
- `ssh://-oProxyCommand=...@host/path`, or a `git://` URL whose host does,
- throws an `InvalidArgumentException`, with both drivers: git before 2.14.1
- passes it to ssh, or to the command of `core.gitProxy`, which reads it as
- an option.
- * `Hooks` throws an `InvalidArgumentException` for a name that isn't the one
- of a file of the hooks directory, like `../config` or `.hidden`.
- * The messages of exceptions, their traces (`#[\SensitiveParameter]`) and
- the logs hide the credentials of URLs
- (`https://***@example.com/repository.git`). `getErrorOutput()` and
- `getOutput()` of `ProcessException` don't.
- * Every message is a sentence ending with a dot.
+* A failing git operation always throws, a
+ `Gitonomy\Git\Exception\ProcessException` with `CliDriver`. Nothing
+ returns `null` or empty data instead anymore: `Repository::run()` returns
+ a `string`, and `Repository::getHead()` and `Repository::getHeadCommit()`
+ never return `null`.
+
+ Before:
+
+ ```php
+ $repository = new Repository($path, ['debug' => false]);
+
+ if (null === $output = $repository->run('fetch', ['--all'])) {
+ // handle the failure
+ }
+ ```
+
+ After:
+
+ ```php
+ $repository = new Repository($path);
+
+ try {
+ $output = $repository->run('fetch', ['--all']);
+ } catch (ProcessException $e) {
+ // handle the failure
+ }
+ ```
+
+* Failures are no longer logged at the `error` level: the exception reports
+ them, and the logger gets their error output at the `debug` level. Alerts
+ based on `error` logs must rely on exceptions instead.
+* A missing revision, reference or object throws a
+ `ReferenceNotFoundException`, with both drivers: it extends
+ `Gitonomy\Git\Exception\InvalidArgumentException`, still an
+ `\InvalidArgumentException`, and takes a whole message instead of a name.
+* A timeout throws a `Gitonomy\Git\Exception\ProcessTimedOutException`,
+ and the other failures of Symfony Process a `DriverException`: no
+ exception of Symfony or of PHP leaks anymore, not even as the previous
+ exception, as they hold the command line.
+* `ProcessException` extends the new
+ `Gitonomy\Git\Exception\DriverException`, itself a `RuntimeException`:
+ catching `RuntimeException` or `GitExceptionInterface` still works.
+* The message of a `ProcessException` is one line, with the error of git:
+ the first line of its error output starting with `fatal:` or `error:`,
+ after the warnings, or its first line: `The git command "clone" failed
+ (exit 128): fatal: ...`, or `The git command "diff" failed (exit 1).`
+ when git writes no error. The command line, the rest of the error output
+ and the output are no longer in the message: `getErrorOutput()` and
+ `getOutput()` return the outputs of git. Its constructor takes the git
+ command as second argument, optional.
+* When git can't start, a missing binary for instance, `CliDriver` throws
+ a `DriverException` instead of a `ProcessException`, with a message of
+ one line, without the command line: `The git command "clone" could not
+ start: ...`.
+* These methods throw instead of returning a fallback value:
+ * `Commit::getIncludingBranches()` threw nothing and returned `[]` when git
+ failed;
+ * `ReferenceBag` (`Repository::getReferences()`) only returns an empty bag
+ for a repository without references, and throws for any other failure.
+* `Gitonomy\Git\Exception\UnexpectedValueException` is deprecated: gitlib
+ no longer throws it. Catch `InvalidArgumentException` for
+ `Tree::resolvePath()`.
+* `Commit::getShortHash()` throws a `ReferenceNotFoundException` for an
+ unknown commit, instead of a `ProcessException`.
+* `Hooks` throws a `RuntimeException` when a hook exists already, doesn't
+ exist, or can't be written, instead of a `LogicException` or a silent
+ failure, and creates the hooks directory.
+* The `Admin` methods throw a `ProcessException`, still a
+ `RuntimeException`, with the first line of git's error output in its
+ message, or a `DriverException` when git can't start.
+* An argument holding a NUL byte throws an `InvalidArgumentException`,
+ where PHP threw a `ValueError`: git and libgit2 read their arguments as C
+ strings, which end at the first NUL byte.
+* A revision, a reference, a hash or a URL starting with a dash throws an
+ `InvalidArgumentException`, with both drivers: git read it as an option,
+ `getRevision('--output=/path')` writing a file, or
+ `isValidRepository('--upload-pack=...')` running a command. A path may
+ start with a dash.
+* An `ext::` URL throws an `InvalidArgumentException`, with both drivers:
+ git runs its command, by default before git 2.12.
+* An ssh URL whose host or user starts with a dash, even encoded (`%2D`),
+ like `ssh://-oProxyCommand=.../repository` or
+ `ssh://-oProxyCommand=...@host/path`, or a `git://` URL whose host does,
+ throws an `InvalidArgumentException`, with both drivers: git before 2.14.1
+ passes it to ssh, or to the command of `core.gitProxy`, which reads it as
+ an option.
+* `Hooks` throws an `InvalidArgumentException` for a name that isn't the one
+ of a file of the hooks directory, like `../config` or `.hidden`.
+* The messages of exceptions, their traces (`#[\SensitiveParameter]`) and
+ the logs hide the credentials of URLs
+ (`https://***@example.com/repository.git`). `getErrorOutput()` and
+ `getOutput()` of `ProcessException` don't.
+* Every message is a sentence ending with a dot.
Git configuration
-----------------
- * `CliDriver` sets back to git's defaults the configuration of the user that
- changes what git prints, what a read returns, or the upstream of a new
- branch: colors, columns, `branch.autoSetupMerge`,
- `branch.autoSetupRebase`, `branch.sort`, `log.follow`,
- `log.showSignature`, and for diffs `diff.algorithm`, `diff.context`,
- `diff.external`, `diff.orderFile`, `diff.submodule`, the prefixes, the
- textconv filters... Before, they made reads throw or return other data: a
- log of a path following its renames with `log.follow`, for instance. See
- ["Git configuration"](doc/repository.md#git-configuration).
- * `CliDriver` ignores the replacements of objects (`git replace`,
- `refs/replace/`), like `LibGit2Driver`: `run()` and `runProcess()` too.
- Run git yourself to read them.
+* `CliDriver` sets back to git's defaults the configuration of the user that
+ changes what git prints, what a read returns, or the upstream of a new
+ branch: colors, columns, `branch.autoSetupMerge`,
+ `branch.autoSetupRebase`, `branch.sort`, `log.follow`,
+ `log.showSignature`, and for diffs `diff.algorithm`, `diff.context`,
+ `diff.external`, `diff.orderFile`, `diff.submodule`, the prefixes, the
+ textconv filters... Before, they made reads throw or return other data: a
+ log of a path following its renames with `log.follow`, for instance. See
+ ["Git configuration"](doc/repository.md#git-configuration).
+* `CliDriver` ignores the replacements of objects (`git replace`,
+ `refs/replace/`), like `LibGit2Driver`: `run()` and `runProcess()` too.
+ Run git yourself to read them.
Dates
-----
- * `Commit::getAuthorDate()`, `Commit::getCommitterDate()` and
- `Reference\Tag::getTaggerDate()` return a `\DateTimeImmutable` instead of
- a `\DateTime`. Code that modifies the returned date must use the returned
- value of `modify()`, `setTimezone()`...
- * These dates are at the UTC offset of their author, committer or tagger,
- like `git log` shows them (`2016-08-17T12:00:00+02:00`), instead of in
- UTC. The instant doesn't change: comparisons and `getTimestamp()` return
- the same results, but `format()` returns the local time of the author,
- committer or tagger. To keep UTC, convert the date:
+* `Commit::getAuthorDate()`, `Commit::getCommitterDate()` and
+ `Reference\Tag::getTaggerDate()` return a `\DateTimeImmutable` instead of
+ a `\DateTime`. Code that modifies the returned date must use the returned
+ value of `modify()`, `setTimezone()`...
+* These dates are at the UTC offset of their author, committer or tagger,
+ like `git log` shows them (`2016-08-17T12:00:00+02:00`), instead of in
+ UTC. The instant doesn't change: comparisons and `getTimestamp()` return
+ the same results, but `format()` returns the local time of the author,
+ committer or tagger. To keep UTC, convert the date:
- Before:
+ Before:
- ```php
- $date = $commit->getAuthorDate()->format('Y-m-d H:i');
- ```
+ ```php
+ $date = $commit->getAuthorDate()->format('Y-m-d H:i');
+ ```
- After:
+ After:
- ```php
- $date = $commit->getAuthorDate()->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i');
- ```
+ ```php
+ $date = $commit->getAuthorDate()->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i');
+ ```
Extending gitlib
----------------
- * These classes are `final`: `Admin`, `Blame`, `Blob`, `Commit`,
- `Diff\File`, `Log`, `Parser\DiffParser`, `PushReference`,
- `Reference\Branch`, `Reference\Stash`, `Reference\Tag`, `ReferenceBag`,
- `Repository`, `Tree`, and the exceptions `LogicException`,
- `ProcessException`, `ReferenceNotFoundException` and
- `UnexpectedValueException`.
- * These classes are `final readonly`: `Blame\Line`, `Diff\Diff`,
- `Diff\FileChange`, `Hooks`, `RevisionList`, `WorkingCopy`.
- * Every method has native parameter and return types. A class extending
- `Parser\ParserBase`, which remains extendable, must declare the same
- types, and must not redeclare its properties.
- * The constructors of `Commit`, `Tree`, `Blob`, `Revision`, `Reference`,
- `Reference\Tag` and `ReferenceBag` are internal: get these objects from
- `Repository` (`getCommit()`, `getTree()`, `getBlob()`, `getRevision()`,
- `getReferences()`), which returns the same commit, tree or blob while your
- application uses it, and reads the references again after its writes.
- The constructor of `Reference` requires the commit hash. Extending
- `Revision` or `Reference` isn't supported anymore, and won't be possible
- in 3.0: `Reference` declares `getName()`, abstract.
- * To change how gitlib runs git, for example in tests, implement
- `Gitonomy\Git\Driver\DriverInterface` and pass it as the driver of
- `Repository`, instead of extending `Repository`: see
- ["Using your own driver"](doc/repository.md#using-your-own-driver).
- * `Commit::__construct()` takes a `Gitonomy\Git\Driver\Data\CommitData`
- instead of an array, and `Commit::setData()` is internal.
- * `Gitonomy\Git\Util\StringHelper` is removed, with its configurable
- encoding: use `symfony/string`. Logs are always read in UTF-8.
- * The parsers of the git output moved to `Gitonomy\Git\Driver\Cli\Parser`
- and are `final`, except `Parser\DiffParser` and `Parser\ParserBase`.
- `CommitParser` is merged into `LogParser`, and their results are objects
- of `Gitonomy\Git\Driver\Data`: `LogParser::$commits` (instead of `$log`),
- `TagParser::$data`, `ReferenceParser::$references`, `TreeParser::$entries`
- and `BlameParser::$lines`. `TreeParser` parses `git ls-tree -z`, and
- `BlameParser` no longer takes a `Repository`. `ParserBase::parse()`
- takes a `string`.
- * Objects serialized with 1.x (a `Commit` or a `Diff` in a cache...) can't
- be unserialized: clear such caches. `Diff::toArray()` and
- `Diff::fromArray()` remain compatible.
+* These classes are `final`: `Admin`, `Blame`, `Blob`, `Commit`,
+ `Diff\File`, `Log`, `Parser\DiffParser`, `PushReference`,
+ `Reference\Branch`, `Reference\Stash`, `Reference\Tag`, `ReferenceBag`,
+ `Repository`, `Tree`, and the exceptions `LogicException`,
+ `ProcessException`, `ReferenceNotFoundException` and
+ `UnexpectedValueException`.
+* These classes are `final readonly`: `Blame\Line`, `Diff\Diff`,
+ `Diff\FileChange`, `Hooks`, `RevisionList`, `WorkingCopy`.
+* Every method has native parameter and return types. A class extending
+ `Parser\ParserBase`, which remains extendable, must declare the same
+ types, and must not redeclare its properties.
+* The constructors of `Commit`, `Tree`, `Blob`, `Revision`, `Reference`,
+ `Reference\Tag` and `ReferenceBag` are internal: get these objects from
+ `Repository` (`getCommit()`, `getTree()`, `getBlob()`, `getRevision()`,
+ `getReferences()`), which returns the same commit, tree or blob while your
+ application uses it, and reads the references again after its writes.
+ The constructor of `Reference` requires the commit hash. Extending
+ `Revision` or `Reference` isn't supported anymore, and won't be possible
+ in 3.0: `Reference` declares `getName()`, abstract.
+* To change how gitlib runs git, for example in tests, implement
+ `Gitonomy\Git\Driver\DriverInterface` and pass it as the driver of
+ `Repository`, instead of extending `Repository`: see
+ ["Using your own driver"](doc/repository.md#using-your-own-driver).
+* `Commit::__construct()` takes a `Gitonomy\Git\Driver\Data\CommitData`
+ instead of an array, and `Commit::setData()` is internal.
+* `Gitonomy\Git\Util\StringHelper` is removed, with its configurable
+ encoding: use `symfony/string`. Logs are always read in UTF-8.
+* The parsers of the git output moved to `Gitonomy\Git\Driver\Cli\Parser`
+ and are `final`, except `Parser\DiffParser` and `Parser\ParserBase`.
+ `CommitParser` is merged into `LogParser`, and their results are objects
+ of `Gitonomy\Git\Driver\Data`: `LogParser::$commits` (instead of `$log`),
+ `TagParser::$data`, `ReferenceParser::$references`, `TreeParser::$entries`
+ and `BlameParser::$lines`. `TreeParser` parses `git ls-tree -z`, and
+ `BlameParser` no longer takes a `Repository`. `ParserBase::parse()`
+ takes a `string`.
+* Objects serialized with 1.x (a `Commit` or a `Diff` in a cache...) can't
+ be unserialized: clear such caches. `Diff::toArray()` and
+ `Diff::fromArray()` remain compatible.
Data returned
-------------
- * File names in diffs (`Diff\File`) and tree entries (`Tree`) are returned
- in UTF-8, instead of git's quoted form (`"caf\303\251.txt"`) for names
- with non-ASCII or special characters.
- * `Blame\Line::getContent()` no longer starts with a TAB.
- * `Commit::getDiff()` lists the files of a root commit, instead of
- returning an empty diff.
- * A commit read through `Repository::getCommit()` has its message as
- `git log` prints it, like a commit of a `Log` already had: without blank
- lines around it, nor whitespace at the end of its lines. It also comes
- re-encoded to UTF-8 when the commit has an `encoding` header, instead of
- throwing.
- * `Reference\Tag::isSigned()` returns `false` for a lightweight tag.
- * `PushReference::isForce()` returns `true` for histories without a common
- ancestor, where the constructor threw. `PushReference` runs git when
- `isForce()` needs it, not when constructed. `getReference()` and
- `getRevision()` are deprecated: use `getFullname()` and `getRange()`.
- * `Blob::getContent()` only reads blobs, where `git cat-file -p` also
- printed trees and commits.
- * `WorkingCopy::getUntrackedFiles()` and `RevisionList::getAll()` return
- lists, without gaps in their keys.
- * `Diff\File::toArray()` has a new `is_copy` key.
+* File names in diffs (`Diff\File`) and tree entries (`Tree`) are returned
+ in UTF-8, instead of git's quoted form (`"caf\303\251.txt"`) for names
+ with non-ASCII or special characters.
+* `Blame\Line::getContent()` no longer starts with a TAB.
+* `Commit::getDiff()` lists the files of a root commit, instead of
+ returning an empty diff.
+* A commit read through `Repository::getCommit()` has its message as
+ `git log` prints it, like a commit of a `Log` already had: without blank
+ lines around it, nor whitespace at the end of its lines. It also comes
+ re-encoded to UTF-8 when the commit has an `encoding` header, instead of
+ throwing.
+* `Reference\Tag::isSigned()` returns `false` for a lightweight tag.
+* `PushReference::isForce()` returns `true` for histories without a common
+ ancestor, where the constructor threw. `PushReference` runs git when
+ `isForce()` needs it, not when constructed. `getReference()` and
+ `getRevision()` are deprecated: use `getFullname()` and `getRange()`.
+* `Blob::getContent()` only reads blobs, where `git cat-file -p` also
+ printed trees and commits.
+* `WorkingCopy::getUntrackedFiles()` and `RevisionList::getAll()` return
+ lists, without gaps in their keys.
+* `Diff\File::toArray()` has a new `is_copy` key.
Other changes
-------------
- * Relative paths, in the arguments of `Repository::run()` and in the paths
- of a log, are relative to the repository instead of the current directory
- of PHP.
- * `Hooks::set()` makes hooks executable by their owner only (`0700`,
- instead of `0777`): change their permissions if another user runs them.
- * `Repository::shell()` is deprecated: it ignored the driver. Run
- `git shell` with Symfony Process instead.
- * `Hooks::has()` returns `true` for a symbolic link to a missing file:
- `get()` throws a `RuntimeException`, as it can't read it, `set()` and
- `setSymlink()` refuse it, as a hook that exists already, and `remove()`
- removes the link.
- * `Repository::getSize()` is deprecated: `getSizeInBytes()` returns bytes,
- instead of kilobytes. Neither follows links: a link in the git directory,
- to a hook not deployed yet for instance, counts for nothing, where
- `getSize()` counted the file it points to, and threw for a missing one.
+* Relative paths, in the arguments of `Repository::run()` and in the paths
+ of a log, are relative to the repository instead of the current directory
+ of PHP.
+* `Hooks::set()` makes hooks executable by their owner only (`0700`,
+ instead of `0777`): change their permissions if another user runs them.
+* `Repository::shell()` is deprecated: it ignored the driver. Run
+ `git shell` with Symfony Process instead.
+* `Hooks::has()` returns `true` for a symbolic link to a missing file:
+ `get()` throws a `RuntimeException`, as it can't read it, `set()` and
+ `setSymlink()` refuse it, as a hook that exists already, and `remove()`
+ removes the link.
+* `Repository::getSize()` is deprecated: `getSizeInBytes()` returns bytes,
+ instead of kilobytes. Neither follows links: a link in the git directory,
+ to a hook not deployed yet for instance, counts for nothing, where
+ `getSize()` counted the file it points to, and threw for a missing one.
diff --git a/tools/zensical/assets/favicon.svg b/tools/zensical/assets/favicon.svg
new file mode 100644
index 00000000..cfdd625d
--- /dev/null
+++ b/tools/zensical/assets/favicon.svg
@@ -0,0 +1 @@
+
diff --git a/tools/zensical/pages.py b/tools/zensical/pages.py
new file mode 100644
index 00000000..b0af78d9
--- /dev/null
+++ b/tools/zensical/pages.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python3
+"""Write the pages of the documentation site, as GitHub shows them, to the docs_dir of zensical.toml.
+
+The site is doc/, with README.md, UPGRADE-2.0.md and CHANGELOG.md, which live at the root of the
+repository. Zensical reads its pages from a single directory, which can't be the root, and runs
+no hooks: this script copies them there, with the assets of the site. The links between the
+pages are relative paths of the repository, like "doc/blob.md" or "../UPGRADE-2.0.md#errors":
+they are rewritten to the pages of the site.
+
+Usage, from the root of the repository: python3 tools/zensical/pages.py && zensical build
+"""
+
+import posixpath
+import re
+import shutil
+import sys
+from pathlib import Path
+
+REPOSITORY_DIR = Path(__file__).resolve().parents[2]
+# The docs_dir of zensical.toml: /var/ is ignored by git.
+OUTPUT_DIR = REPOSITORY_DIR / 'var/docs'
+ASSETS_DIR = Path(__file__).resolve().parent / 'assets'
+
+# The files of the root of the repository published as pages, and their page on the site.
+ROOT_PAGES = {
+ 'CHANGELOG.md': 'changelog.md',
+ 'README.md': 'index.md',
+ 'UPGRADE-2.0.md': 'upgrade-2.0.md',
+}
+
+# A relative link to a Markdown file: "](doc/blob.md)", "](../UPGRADE-2.0.md#errors)"...
+MARKDOWN_LINK = re.compile(r'\]\((?P(?![a-z]+:)[^)\s#]+\.md)(?P#[^)\s]*)?\)')
+
+
+def main() -> None:
+ if OUTPUT_DIR.exists():
+ shutil.rmtree(OUTPUT_DIR)
+ shutil.copytree(REPOSITORY_DIR / 'doc', OUTPUT_DIR)
+ shutil.copytree(ASSETS_DIR, OUTPUT_DIR / 'assets')
+
+ pages = dict(ROOT_PAGES)
+ for file in sorted((REPOSITORY_DIR / 'doc').rglob('*.md')):
+ path = file.relative_to(REPOSITORY_DIR).as_posix()
+ pages[path] = path.removeprefix('doc/')
+
+ for path, site_path in pages.items():
+ markdown = (REPOSITORY_DIR / path).read_text(encoding='utf-8')
+ (OUTPUT_DIR / site_path).write_text(rewrite_links(markdown, path, site_path), encoding='utf-8')
+
+ print(f'{len(pages)} pages written to {OUTPUT_DIR.relative_to(REPOSITORY_DIR)}/.', file=sys.stderr)
+
+
+def rewrite_links(markdown: str, path: str, site_path: str) -> str:
+ """Rewrite the links of the file path of the repository, published as site_path."""
+
+ def rewrite(match: re.Match[str]) -> str:
+ target = posixpath.normpath(posixpath.join(posixpath.dirname(path), match['path']))
+ if target in ROOT_PAGES:
+ target_site_path = ROOT_PAGES[target]
+ elif target.startswith('doc/'):
+ target_site_path = target.removeprefix('doc/')
+ else:
+ # Not a page: the strict build reports the link.
+ return match[0]
+
+ link = posixpath.relpath(target_site_path, posixpath.dirname(site_path) or '.')
+
+ return '](' + link + (match['fragment'] or '') + ')'
+
+ return MARKDOWN_LINK.sub(rewrite, markdown)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/zensical/requirements.txt b/tools/zensical/requirements.txt
new file mode 100644
index 00000000..ce2e03aa
--- /dev/null
+++ b/tools/zensical/requirements.txt
@@ -0,0 +1,13 @@
+# The documentation site (zensical.toml), with Python 3.14: `pip install zensical` in a new
+# virtualenv, then `pip freeze`. Dependabot updates them.
+click==8.5.0
+deepmerge==3.0.1
+jinja2==3.1.6
+markdown==3.11.1
+markupsafe==3.0.4
+pathspec==1.1.1
+pygments==2.21.0
+pymdown-extensions==12.2
+pyyaml==6.0.3
+tomli==2.5.0
+zensical==0.0.70
diff --git a/zensical.toml b/zensical.toml
new file mode 100644
index 00000000..654552d5
--- /dev/null
+++ b/zensical.toml
@@ -0,0 +1,95 @@
+# The documentation site, published on GitHub Pages by .github/workflows/docs.yml. Its pages are
+# doc/, README.md, UPGRADE-2.0.md and CHANGELOG.md, which tools/zensical/pages.py copies to
+# docs_dir first. With the Python packages of tools/zensical/requirements.txt:
+#
+# python3 tools/zensical/pages.py && zensical serve
+
+[project]
+site_name = "gitlib"
+site_url = "https://gitonomy.github.io/gitlib/"
+site_description = "Access git repositories from PHP, through the git binary or libgit2."
+copyright = "gitlib is released under the MIT license."
+repo_url = "https://github.com/gitonomy/gitlib"
+repo_name = "gitonomy/gitlib"
+docs_dir = "var/docs"
+site_dir = "site"
+strict = true
+
+nav = [
+ { "Getting started" = "index.md" },
+ { "Repository and drivers" = [
+ { "Opening a repository" = "repository.md" },
+ { "Creating repositories" = "admin.md" },
+ ] },
+ { "Objects" = [
+ { "Revisions" = "revision.md" },
+ { "Commits" = "commit.md" },
+ { "Log" = "log.md" },
+ { "Trees" = "tree.md" },
+ { "Blobs" = "blob.md" },
+ { "Diffs" = "diff.md" },
+ { "Blame" = "blame.md" },
+ { "Tags and branches" = "references.md" },
+ { "Branches" = "branch.md" },
+ ] },
+ { "Working copy" = "workingcopy.md" },
+ { "Hooks" = "hooks.md" },
+ { "Upgrade and changelog" = [
+ { "Upgrading to 2.0" = "upgrade-2.0.md" },
+ { "Changelog" = "changelog.md" },
+ ] },
+]
+
+[project.theme]
+language = "en"
+favicon = "assets/favicon.svg"
+# The fonts of the system: no request to Google Fonts.
+font = false
+features = [
+ "content.code.copy",
+ "navigation.footer",
+ "navigation.instant",
+ "navigation.path",
+ "navigation.sections",
+ "navigation.top",
+ "navigation.tracking",
+ "search.highlight",
+ "toc.follow",
+]
+
+[project.theme.icon]
+logo = "lucide/git-branch"
+repo = "fontawesome/brands/github"
+
+[[project.theme.palette]]
+media = "(prefers-color-scheme)"
+toggle.icon = "lucide/sun-moon"
+toggle.name = "Switch to light mode"
+
+[[project.theme.palette]]
+media = "(prefers-color-scheme: light)"
+scheme = "default"
+primary = "teal"
+accent = "teal"
+toggle.icon = "lucide/sun"
+toggle.name = "Switch to dark mode"
+
+[[project.theme.palette]]
+media = "(prefers-color-scheme: dark)"
+scheme = "slate"
+primary = "teal"
+accent = "teal"
+toggle.icon = "lucide/moon"
+toggle.name = "Switch to system preference"
+
+[project.markdown_extensions]
+admonition = {}
+attr_list = {}
+toc.permalink = true
+# The examples start without "