From 0a966db494666a6664cdccf432ed18e752026bc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Wed, 14 Oct 2026 14:56:56 +0200 Subject: [PATCH 1/2] Indent the lists of UPGRADE-2.0.md and CHANGELOG.md like doc/ GitHub nests the blocks of a list item at the column of its text, 2 spaces after "- " and 3 after " * ". Python-Markdown, which builds the documentation site, nests them at 4 spaces per level: it rendered the nested lists and the code blocks of these files out of their items. Their items are written like the pages of doc/ write them, "- text", with a blank line between a nested block and the next item of a loose list. Only whitespace changes: GitHub renders both files the same. --- CHANGELOG.md | 436 +++++++++++----------- UPGRADE-2.0.md | 987 +++++++++++++++++++++++++------------------------ 2 files changed, 712 insertions(+), 711 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6b03ec..e4427a8 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 ddd2c01..e44b082 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. From 27ba5748ae677648bd7a1ac5dc369c9e7b28f810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Wed, 14 Oct 2026 14:56:56 +0200 Subject: [PATCH 2/2] Publish the documentation on GitHub Pages doc/, README.md, UPGRADE-2.0.md and CHANGELOG.md become a site, built by Zensical with its theme: a light and a dark mode, a search, the fonts of the system, and no request to Google Fonts. Zensical reads the pages from a single directory, which can't be the root of the repository, and runs no hooks: tools/zensical/pages.py copies them to var/docs first, and rewrites the links between the files of the root and doc/, which stay relative paths of the repository, as GitHub shows them. The Documentation workflow builds the site, strictly, on the pull requests changing it, and deploys it to GitHub Pages from main. Dependabot updates its Python dependencies. zensical.toml stays out of the archive Composer installs. --- .gitattributes | 1 + .github/dependabot.yml | 8 +++ .github/workflows/docs.yml | 76 +++++++++++++++++++++++++ .gitignore | 1 + tools/zensical/assets/favicon.svg | 1 + tools/zensical/pages.py | 74 ++++++++++++++++++++++++ tools/zensical/requirements.txt | 13 +++++ zensical.toml | 95 +++++++++++++++++++++++++++++++ 8 files changed, 269 insertions(+) create mode 100644 .github/workflows/docs.yml create mode 100644 tools/zensical/assets/favicon.svg create mode 100644 tools/zensical/pages.py create mode 100644 tools/zensical/requirements.txt create mode 100644 zensical.toml diff --git a/.gitattributes b/.gitattributes index cbf8b34..e39bddb 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 2797668..ca8ca8b 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 0000000..1451308 --- /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 0b49eff..3bac1c1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /.phpunit.cache /composer.lock /phpunit.xml +/site/ /var/ /vendor /git-builds/ diff --git a/tools/zensical/assets/favicon.svg b/tools/zensical/assets/favicon.svg new file mode 100644 index 0000000..cfdd625 --- /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 0000000..b0af78d --- /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 0000000..ce2e03a --- /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 0000000..654552d --- /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 "