Skip to content

Fix what the last review before 2.0 found - #263

Merged
lyrixx merged 22 commits into
mainfrom
before-2.0
Oct 14, 2026
Merged

lyrixx merged 22 commits into
mainfrom
before-2.0

Conversation

@lyrixx

@lyrixx lyrixx commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

A last review of main before tagging 2.0: four checks in parallel (security and the CLI driver, LibGit2Driver, the API and the doc, the tests and the packaging) went over both previous audits, item by item, with both drivers and git 1.8.4. Every item of them is fixed or decided. This PR fixes what the review found, one commit each, with its tests and its doc.

Security

  1. Ignore core.fsmonitor, and say gitlib trusts the repository it opens. The configuration of a repository may name a command in core.fsmonitor, which git ran to read the working tree, and even to diff a commit. CliDriver sets it back to false. The doc said safe.directory applies: it doesn't, as CliDriver passes --git-dir. Its hooks and filters still run, like with git: only open a repository whose .git you trust.
  2. Look for a bundle in a local path only. LibGit2Driver checked whether a clone URL is a bundle with is_file(), which hands a URL to the stream wrapper of its scheme: for ftp://user:password@host/..., PHP connected to the host and sent the credentials.
  3. Refuse fd:: URLs. git talks through the file descriptors it inherits from PHP, like the socket of a database, and waited on it until it timed out, an hour by default.
  4. Say in the security policy that 1.x is no longer maintained, as it gets no fix for what 2.0 fixes.
  5. Require the versions of symfony/process without advisories: ^6.4.33 || ^7.4.5 || ^8.1 (CVE-2024-51736, CVE-2026-24739, on Windows).

API, before it is frozen

  1. Remove the arguments of git merge of WorkingCopy::merge(): Add WorkingCopy::merge() to support git merge #248 never was in a release, and 2.0 would have shipped their deprecation, and a FastForward|array argument for all of 2.x.
  2. Refuse a negative offset or limit of a log, which gave a wrong count, and different commits with each driver.
  3. Throw gitlib's exceptions: Hooks::remove() of a missing hook throws an InvalidArgumentException, like get(), and getSizeInBytes() no longer lets PHP's UnexpectedValueException out.
  4. Name the kinds of push like the ones of a file: PushReference::isCreation() and isDeletion(), like Diff\File; isCreate() and isDelete() are deprecated.
  5. Say the interfaces of the drivers may get methods in 2.x: the promise of backward compatibility covers the code calling them, not the drivers implementing them. The CLI parsers are internal.

Bugs

  1. LibGit2Driver reads @ as HEAD, followed by ~, ^ or : too: getRevision('@~1'), getLog('@~2..@'), checkout('@') threw.
  2. LibGit2Driver walks from HEAD too in a log of every reference: with a detached HEAD on a commit no reference has, commits were missing, without error.
  3. Don't throw for a branch another process created: getIncludingBranches() and getHead() threw after a write of another process, and getIncludingBranches() for every commit of a regular refs/remotes/origin/HEAD, as a mirror leaves it.
  4. Read the blame of a commit under two paths with CliDriver, after a merge whose sides kept a file under two names.
  5. Read malformed signatures like libgit2: one commit with an author like Name<email>, or without date, made the whole log throw with CliDriver.

Doc

  1. Complete the UPGRADE and the doc: an unknown revision throws a ReferenceNotFoundException, which a catch (RuntimeException) doesn't catch; getRawDiff(); the renamed parameters, for named arguments; ParserBase; the imports of the examples; the example of getHead(); the blame of LibGit2Driver; a password in a git:// URL.
  2. Fix the CHANGELOG: links to Security fixes, and align both drivers with git #254, and wrong entries. The header stays "Unreleased", until the release date is known.
  3. Link the CHANGELOG entries of this pull request to it.

Added after the first review

  1. Run ls-remote out of the working directory of PHP, and refuse an empty URL: git read the configuration of the repository PHP runs in, like its rewrites of URLs, and an empty URL named it.
  2. Remove the fields of TagData nothing reads: the object, its type and the name of the tag.
  3. Reword the exception messages the review found unclear: a revision starting with a dash without the ^{commit} gitlib adds, core.gitProxy for a git:// URL, "1 second", a timeout while git starts, a single unknown option, the path of getLastModification(), and the CONFLICT line of a merge.
  4. Test every deprecation: eleven deprecated methods, and the class CommitReference, ran in no test.

Checked locally

  • At each commit: both drivers pass the suite, and PHPStan and php-cs-fixer report nothing.
  • At the last one: 683 tests, also with git 1.8.4, 2.17.1 and 2.25.1, and the site builds in strict mode.

lyrixx added 22 commits October 14, 2026 18:40
The configuration of a repository may name a command in core.fsmonitor,
which git runs to read the working tree, and even to diff a commit:
getUntrackedFiles(), the diffs of the working copy and Commit::getDiff()
ran it. CliDriver sets core.fsmonitor back to false, on every command.

The doc said safe.directory applies: it doesn't, as CliDriver runs git
with --git-dir, which git trusts, whoever owns the repository. Its hooks
and the filters of its .gitattributes still run, like with git: the doc
says to only open a repository whose .git directory is trusted.
LibGit2Driver checks whether the URL of a clone is a bundle with
is_file(), which hands a URL to the stream wrapper of its scheme: for
"ftp://user:password@host/...", PHP connected to the host and sent the
credentials, before libgit2 saw the URL. With "ssh://" or "git://", PHP
warned that no wrapper exists, which error handlers turn into an
exception whose trace held the URL.

Only a local path, "file://" included, is looked at, and the URL is a
#[\SensitiveParameter].
An "fd::7" URL makes git talk through file descriptors it inherits from
PHP: an open file, the socket of a database... isValidRepository() of
such a URL, which an application may get from its users, made git wait
on a socket of the application until it timed out, an hour by default.

Both drivers refuse it with an InvalidArgumentException, like an "ext::"
URL.
The policy promised 12 months of support to the previous major release,
but 1.x has no branch, and gets no fix for the security issues 2.0
fixes. It now says only 2.x gets security fixes, and keeps the promise
for the next major releases.
…ased

WorkingCopy::merge() came with #248, after 1.6.0: no release has its
array of arguments of git merge ([], ["--no-ff"] or ["--ff-only"]),
which 2.0 deprecated in favor of FastForward. 2.0 would have shipped a
deprecation of its first day, and a FastForward|array argument for all
of 2.x.

merge() takes a FastForward only. The deprecation, its section of
UPGRADE-2.0.md and its line in the CHANGELOG go.
A negative offset or limit gave wrong data, different with each driver:
withLimit(-1) counted -1 commits, and iterated the whole history with
CliDriver, but one commit with LibGit2Driver. withOffset(-5) counted
more commits than it iterated.

Log, Repository::getLog() and Revision::getLog() throw an
InvalidArgumentException for one. A limit of 0 gives no commit, with
both drivers.
Hooks threw an InvalidArgumentException for a hook that doesn't exist in
get(), but a RuntimeException in remove(), where the UPGRADE announced a
RuntimeException for both. remove() throws an InvalidArgumentException
too, which is still the LogicException 1.x threw.

getSizeInBytes() let PHP's UnexpectedValueException out for a directory
it can't read, or which git deletes while it counts, like the quarantine
of a push. It walks the git directory itself: a directory git deleted
counts for nothing, and one it can't read throws a RuntimeException of
gitlib, like a file.
git reads "@" as HEAD since 1.8.5, alone or followed by "~", "^" or
":". libgit2 only reads it alone, and Revision::getCommit() always adds
"^{commit}": getRevision('@'), getLog('@~2..@'), getDiff('@~1', '@'),
checkout('@') or merge('@') threw a ReferenceNotFoundException with
LibGit2Driver, but not with CliDriver.

LibGit2Driver reads it as HEAD before libgit2 parses the revision, and
checkout('@') leaves HEAD attached, like checkout('HEAD') and git since
2.45.
A log without revisions is the one of every reference and HEAD, like
"git log --all". LibGit2Driver only walked from the references: with a
detached HEAD on a commit no reference has, like after the checkout of
a hash, the log lacked commits, and count() was wrong, without error.
Commit::getIncludingBranches() and Repository::getHead() read the
branches from git, then looked them up in the references gitlib read
before: a branch another process created or checked out since then made
them throw a ReferenceNotFoundException, which a long-running worker
meets. getIncludingBranches() also threw for every commit of a regular
refs/remotes/origin/HEAD, which a mirror leaves, and which the
references leave out.

Both read the references again for a branch they don't know.
getIncludingBranches() then leaves out a branch still unknown: the
default branch of a remote, or one deleted meanwhile.
After a merge whose sides kept a file under two names, git blames a
commit under both, and "git blame -p" repeats its "previous" and
"filename" lines on each of its groups of lines: the parser only read
them on the first one, and threw ('Expected "\t", got "filename b"').
LibGit2Driver read it.
Old commits, imported from other tools, have authors and committers git
wouldn't write anymore: "Name<email>", no date, an offset of 5 digits,
no name nor email. git and libgit2 read them, but CliDriver required
"Name <email> timestamp +hhmm" exactly, and one such commit made the
whole log throw.

ParserBase::parseSignature() reads them like libgit2, which
LibGit2Driver reads them with: the email between the last "<" and ">",
the name before it, and the date and offset optional, the epoch in UTC
without them. Tags read their tagger with it too.
PushReference::isCreate() and isDelete() named what Diff\File names
isCreation() and isDeletion(), whose isDelete() 2.0 deprecates for
this: one concept, two names. PushReference gets isCreation() and
isDeletion(), and isCreate() and isDelete() are deprecated. The
argument of its constructor holding the full name of the reference is
named $fullname, like getFullname().
gitlib required symfony/process ^6.4 || ^7.4 || ^8.1, but the versions
before 6.4.33 and 7.4.5 escape the arguments wrongly on Windows
(CVE-2024-51736, CVE-2026-24739). Recent Composer versions refuse to
install them, so the job of the lowest dependencies tested 6.4.33, not
the 6.4.0 gitlib declared. It requires ^6.4.33 || ^7.4.5 || ^8.1.
The doc invites to implement DriverInterface and
RepositoryDriverInterface, in tests for instance, without saying whether
2.x may add methods to them: any new method would have broken every
implementation. Both interfaces say gitlib may add methods in a minor
version: the promise of backward compatibility covers the code calling
them, not the drivers implementing them.

The parsers of Driver\Cli\Parser, which only CliDriver uses, are
internal.
- UPGRADE-2.0.md says an unknown revision throws a
  ReferenceNotFoundException, which a catch of RuntimeException doesn't
  catch, where getDiff(), getBlame() or checkout() threw a
  ProcessException; that getRawDiff() returns the output of
  "git diff --raw -z --patch", or nothing with LibGit2Driver; which
  parameters are renamed, for named arguments; and how ParserBase
  changes. Its examples import the classes they use.
- doc/diff.md says what getRawDiff() returns.
- The example of getHead() in doc/repository.md checks the class of
  HEAD, which PHPStan understands.
- The differences of LibGit2Driver list the blame, which may attribute
  some lines to another commit, and ignores blame.ignoreRevsFile.
- The doc says a password in a git:// URL, which takes no credentials,
  isn't hidden: git quotes such a URL without its scheme.
The entries of the work of #254 had no link, or the one of #253, and
some were wrong for 2.0:

- reftable repositories open with CliDriver only;
- run() doesn't keep its "exact original behavior", but still returns
  the standard output;
- Repository::cloneTo() read like a replacement of Admin, and $reload
  like an argument of ReferenceBag::update() and Branch::isMergedTo(),
  which are deprecated themselves;
- the fix of #239 for the "debug" option set to false is gone: 2.0
  refuses that option.

The header stays "Unreleased", until the release date is known.
The entries of #263 had no link: they get one, after the one of #254 when they complete its work.
…y URL

isValidRepository() and isValidRepositoryAndBranch() ran ls-remote in
the working directory of PHP, which may be a repository, like the
checkout of an application: git read its configuration, its rewrites of
URLs for instance, and an empty URL named it, which was then valid with
CliDriver, not with LibGit2Driver. git 1.8.4 can't even run in a linked
worktree, which made them return false.

ls-remote runs in the temporary directory, and both drivers refuse an
empty URL with an InvalidArgumentException.
TagData held the object a tag points to, its type and the name of the
tag, which nothing reads: the reference gives the object already. A
driver implemented outside gitlib had to fill them anyway. TagData
keeps the tagger, the message and the signature.
- A revision starting with a dash names the revision, without the
  "^{commit}" gitlib adds: Revision::getCommit() checks it first.
- A git:// URL whose host starts with a dash names the command of
  core.gitProxy, not ssh only.
- getIncludingBranches(false, false) says what to do, without "a
  least".
- A timeout of one second is "1 second", and a timeout git reaches
  while it starts is a ProcessTimedOutException, instead of a
  DriverException holding the command line.
- A single unknown option of Repository is "Unknown option".
- getLastModification() of a path no commit changes names the path.
- The ProcessException of a merge in conflict has the first CONFLICT
  line git writes on its output, instead of nothing.
Eleven deprecated methods ran in no test: File::addChange(),
isChangeMode(), isDelete() and getAnchor(), FileChange::getCount(),
Diff::setRepository(), PushReference::getReference() and getRevision(),
Commit::getFixedShortHash(), Log::setOffset() and Repository::shell().
Nothing checked the message of the deprecation of CommitReference
either. Nothing fixed their behavior until 3.0: each gets a legacy test,
CommitReference in a process of its own, as Submodule declares its alias
once loaded, and shell() through a command of ~/git-shell-commands.
@lyrixx
lyrixx merged commit 7b32f0b into main Oct 14, 2026
13 checks passed
@lyrixx
lyrixx deleted the before-2.0 branch October 14, 2026 17:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant