Skip to content

Preserve GVFS locks when commands collide with aliases - #2139

Draft
Tyrie Vella (tyrielv) wants to merge 3 commits into
microsoft:masterfrom
tyrielv:tyrielv/alias-resolution
Draft

Tyrie Vella (tyrielv) wants to merge 3 commits into
microsoft:masterfrom
tyrielv:tyrielv/alias-resolution

Conversation

@tyrielv

@tyrielv Tyrie Vella (tyrielv) commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #2130. This branch is based on #2130. Until #2130 merges, this diff also shows its two commits.

Problem and Context

The command hook uses a static command inventory before it checks for aliases.
The inventory omits built-ins such as stash and cannot cover custom executables.
A same-name alias can therefore bypass the required lock even when Git runs the real command.
Nested Git processes from external commands inherit hook suppression, so they cannot supply the missing lock.

Ordinary git stash already takes the lock.
This repair addresses command-resolution collisions, not general stash reliability or the read-only lock exemption policy.

Changes

  • Distinguish built-in hooks from external attempts before command-name normalization.
  • Remove the static command inventory from alias detection.
  • Require a successful, nonempty alias-config result before considering an alias exemption.
  • Use the parent Git executable for config queries and explicitly suppress recursive command hooks.
  • Probe executable names on the inherited Windows PATH, which Git already extends with its exec-path.
  • Preserve locks for real executables, including mixed-case names and executable suffixes.
  • Preserve genuine aliases and the normal lock policy for their inner commands.
  • Report config and executable-lookup failures and retain the conservative lock without granting an alias exemption.
  • Release external-command locks by typed PID ownership during post-command, without another alias or executable lookup.
  • Add unit tests for command resolution, parent-process lookup, precise failure diagnostics, and lock-ownership responses.
  • Add functional tests for collisions, aliases, nested commands, command spellings, directories on PATH, and invalid PATH entries.
  • Add functional tests that keep the lock holder alive until each test observes its wait message or completes without a wait.
  • Add a functional test that calls both hooks with a live Git PID and deletes the external executable between calls. The test asserts release before the Git process exits.
  • Document how aliases interact with command locks.

Related: #2138 changes read-only lock exemptions.
This repair remains separate and does not depend on that policy change.

Known gaps and deliberate decisions

  • External-executable race — An executable can disappear after GVFS acquires its lock but before Git starts it. Git 2.55.0.vfs.0.10 can skip post-command on that failure path. If Git then expands a locking alias, the child can wait on its parent's lock. Cancel the command to end the wait. The Git-side repair remains separate.
  • If an alias-resolution probe fails, the hook reports the failure and keeps the conservative command lock. It does not grant an alias exemption.
  • Hook diagnostics include the failing operation and native error details. They do not create a separate durable log.

Use one bounded retry policy for git.exe and libgit2 config reads. Retry only transient file access failures and preserve existing missing-key behavior.

Retry the libgit2 repository open, which parses .git\config. Retry the git config alias lookup in the hooks. Let latency-sensitive hook callers use a smaller attempt budget.

Match a transient failure only on the reason after the last "': " in an error line. A path or key that contains "Permission denied" no longer matches.

Trace when a transient failure persists after all attempts. Do not trace config names that contain URL credentials.

Add Enlistment.CreateGitProcess(ITracer). Use it for the mount cache server, retry, status cache, and required-config readers, so retries reach the mount trace.

Assisted-by: GPT-5.6 Sol
Assisted-by: Claude Sonnet 5.5
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
RetryConfig started git.exe once for gvfs.max-retries and once for
gvfs.timeout-seconds. Read both settings in process with libgit2. Open a
transient repository without its object store when the working directory
exists. Otherwise, read the global, XDG, system, and ProgramData config,
as GitProcess.GetFromConfig does before clone creates the repository.
The reads use the GitConfigReadRetry policy.

libgit2 does not read GIT_CONFIG_PARAMETERS, GIT_CONFIG_COUNT,
GIT_CONFIG_GLOBAL, GIT_CONFIG_SYSTEM, GIT_CONFIG_NOSYSTEM, or GIT_CONFIG.
It also does not use GIT_DIR, which git.exe uses to select a repository
when no working directory exists. When the environment sets any of these
variables, read the settings with git.exe.

libgit2 evaluates only the gitdir, gitdir/i, and onbranch includeIf
conditions. It skips other conditions, such as hasconfig:remote.*.url,
without an error. When the config has such a condition, the libgit2 read
fails, and git.exe reads the setting.

When libgit2 cannot read a setting for another reason, for example
because it rejects the repository's ownership, read the setting with
git.exe and trace RetryConfig_ReadWithGit. After a transient file access
failure, do not read again with git.exe, because libgit2 already used the
retry budget.

Move integer setting parsing to GitConfigHelper, so both readers report
the same default, minimum, and parse errors.

When the LibGit2Repo constructor fails, it releases its libgit2
reference. Suppress finalization in that case, so the finalizer does not
release the reference a second time.

Assisted-by: Claude Opus 5.5
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Distinguish built-in hooks from external attempts before alias detection.
Keep required locks when a real command takes precedence over an alias.
Use the parent Git executable for config queries and suppress recursive hooks.
Resolve external names on the inherited Windows PATH without command enumeration.
Release external-command locks by typed ownership during post-command.
Do not repeat mutable command resolution when deciding whether to release.
Report alias-resolution failures and retain the conservative lock.

Cover built-in and external collisions, genuine aliases, command spellings,
config or executable-lookup failures, and lock-ownership responses.
Keep lock-holder lifetimes deterministic in the functional cases.
Exercise both hooks with a live Git PID and remove the external between calls.
Keep the existing lock exemption policy unchanged.

Assisted-by: GPT-6.1 Sol
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant