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