Skip to content

Fix diff-loading bugs that prevented the git tree filter from activating - #64

Merged
kboom merged 6 commits into
mainfrom
kboom-fix-diff-loading-bugs
Jul 20, 2026
Merged

kboom merged 6 commits into
mainfrom
kboom-fix-diff-loading-bugs

Conversation

@kboom

@kboom kboom commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Summary

The git tree filter would silently fail to load the diff (the explorer list wouldn't budge, and the settings button never appeared) under several real-world conditions. This PR fixes four distinct root causes found via systematic debugging, each backed by a regression test.

All work follows TDD: every fix has a test that is RED before the change and GREEN after. Full GitTreeFilter.Core suite: 84/84 passing.

Bugs fixed

Bug #1 — Detached HEAD (735f689)
AssertValidReference rejected a detached HEAD and threw HeadNotFoundException, deactivating the plugin. Now tolerates detached HEAD via a Head?.Tip == null guard.

Bug #2 — Worktrees / submodules not detected (b0d68ef)
VS IGitExt.ActiveRepositories is empty for worktrees, so the plugin reported "Could not identify GIT repository". Added GitRepositoryDiscovery which falls back to LibGit2Sharp Repository.Discover.

Bug #3 — Modern git index checksum error (3a6cd86)
On large repos with a modern index (UNTR extension) the diff failed with "invalid data in index — calculated checksum does not match expected". Root cause: the old LibGit2Sharp 0.27.0-preview (libgit2 ~1.0) couldn't parse a modern index. Upgraded to LibGit2Sharp 0.31.0 (libgit2 1.8), which VS itself ships.

Bug #4 — Repo owned by another user (b35c7fd)
The 0.31 upgrade pulled in libgit2 1.8, which enforces the CVE-2022-24765 "dubious ownership" check. On enterprise machines a worktree/clone owned by a different account made new Repository(path) throw "repository path … is not owned by current user" inside SetUpAsync. That exception escaped the async service factory, leaving SGitFilterService null → "Cannot find an instance of the GitTreeFilter.IGitFilterService service" ("cannot open").

  • Root cause fix: GlobalSettings.SetOwnerValidation(false) in GitRepositoryFactory.Create — VS already opened the repo for the user and the plugin only reads it.
  • Defense in depth: wrapped the fallible body of SetUpAsync in try/catch so any future setup exception degrades the plugin to INACTIVE (with the reason logged to the output pane) instead of nulling the whole service.

Also added thread-safe Output-pane diagnostics across the activation flow (a246704) to make future issues diagnosable from the log.

Testing

  • New/updated regression tests: detached HEAD, worktree discovery, ModernIndexChangesetTest, RepositoryOwnershipTest — each RED before its fix, GREEN after.
  • Full GitTreeFilter.Core suite: 84/84.
  • End-to-end: rebuilt the Release VSIX and verified against a real enterprise worktree — the filter now activates and loads the diff.

kboom and others added 5 commits July 20, 2026 15:16
AssertValidReference rejected any repository whose HEAD friendly name was not in the branch list, so a detached HEAD (checked-out commit/tag, mid-rebase/bisect, or detached worktree) threw HeadNotFoundException. The UI caught it and rendered an empty, unchanging list.

Validate HEAD by checking it resolves to a real commit (Head.Tip) instead, which accepts detached HEAD while still rejecting an unborn/head-less repo. Adds detached-HEAD changeset tests over the Basic and Merge fixtures.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 09814c7d-b363-45cb-83eb-1832f7a8abcb
TryGetRootRepositoryPath relied solely on IGitExt.ActiveRepositories, which is empty for git worktrees and submodules (.git is a file). The plugin then deactivated with 'Could not identify GIT repository to use', so selecting the filter did nothing even though the solution was inside a Git repo.

Add filesystem-based GitRepositoryDiscovery (LibGit2Sharp Repository.Discover) and use it as a fallback from the solution directory when IGitExt reports no repository. Covers worktrees, submodules, and nested-subdirectory solutions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 09814c7d-b363-45cb-83eb-1832f7a8abcb
The worktree scenario reaches PluginState=RUNNING yet selecting the filter
does nothing and the settings button never appears. The existing logs used
Debug.WriteLine (invisible in the VS Output pane) and none covered the
filter/changeset path, so the failing boundary was unobservable.

- GitTreeFilterOutput: drop the UI-thread guard (OutputStringThreadSafe is
  thread-safe) and add Log() with timestamp + thread id so background-thread
  stages can trace. This makes filter creation and changeset building visible.
- Instrument GitFilterProvider.CreateFilter (service null / plugin state /
  reference object), GitFileFilter ctor + OnInitialized (settings-button
  trigger), GetIncludedItemsAsync, InvalidateAsync and CreateChangesetAsync.
- Add a catch-all in CreateChangesetAsync so a non-GitOperationException no
  longer bubbles out of GetIncludedItemsAsync (which would abort filter
  initialization and hide the settings button); it is logged and degrades to
  an empty list instead.
- Promote repo-discovery and reference-resolution logs in the service to the
  Output pane, and log IsFilterApplied/lifecycle transitions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 09814c7d-b363-45cb-83eb-1832f7a8abcb
Bug #3 root cause: LibGit2Sharp 0.27.0-preview (libgit2 ~1.0) cannot parse
the index written by modern git when the untracked-cache (UNTR) extension is
present at scale (repos with core.untrackedCache/feature.manyFiles). This threw
"invalid data in index - calculated checksum does not match expected" from
RetrieveStatus in SolutionRepository.Changeset, leaving the Solution Explorer
list empty ("won't budge").

Fix: upgrade LibGit2Sharp 0.27.0-preview-0182 -> 0.31.0 (libgit2 ~1.8), which
parses the modern index cleanly. No API breaks. Native git2 binaries flow into
the VSIX via the Core ProjectReference (lib/win32/*).

Adds a regression test that builds a temp repo with feature.manyFiles + 500
files (confirmed RED on 0.27, GREEN on 0.31). Full Core suite: 83 passing.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 09814c7d-b363-45cb-83eb-1832f7a8abcb
libgit2 1.8 (bundled with LibGit2Sharp 0.31, adopted to fix the modern
index-checksum bug) enforces the "dubious ownership" check from
CVE-2022-24765. On enterprise machines a git worktree/clone is frequently
owned by a different account than the one running Visual Studio, so
`new Repository(path)` threw "repository path '...' is not owned by current
user" inside GitTreeFilterService.SetUpAsync. That exception escaped the
async service factory, leaving SGitFilterService null and making the filter
provider fail with "Cannot find an instance of the GitTreeFilter.
IGitFilterService service" ("cannot open").

Root cause fix: call GlobalSettings.SetOwnerValidation(false) in
GitRepositoryFactory.Create before opening the repository. Visual Studio has
already opened the repo for the user and the plugin only reads it, so the
ownership gate adds no protection here.

Defense in depth: wrap the fallible body of SetUpAsync in try/catch so any
future setup exception degrades the plugin to INACTIVE (with the reason
logged to the output pane) instead of nulling the whole service.

TDD: RepositoryOwnershipTest.OpeningRepositoryDisablesLibGit2OwnerValidation
opens a repo through the public SolutionRepository API and asserts owner
validation is disabled. RED before the fix, GREEN after. Full Core suite
84/84. End-to-end probe against the real worktree with the Release-output
0.31 DLLs: open now succeeds and RetrieveStatus returns 1658 entries.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 09814c7d-b363-45cb-83eb-1832f7a8abcb
@kboom
kboom enabled auto-merge (squash) July 20, 2026 17:58
GitHub automatically fails runs that use the deprecated actions/cache v2.
Bump to v4 (current major, input-compatible) to keep CI working.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 09814c7d-b363-45cb-83eb-1832f7a8abcb
@kboom
kboom disabled auto-merge July 20, 2026 18:07
@kboom
kboom merged commit 9307fe8 into main Jul 20, 2026
1 check passed
@kboom
kboom deleted the kboom-fix-diff-loading-bugs branch July 20, 2026 18:07
kboom added a commit that referenced this pull request Jul 20, 2026
Promote the diff-loading fixes from `main` to `release/v1` to cut a
normal **v1.20.0** release.

Merging this PR triggers `ci-release.yml`, which tags `v1.20.0`, builds
the VSIX, and creates a **draft** GitHub Release with the VSIX attached.
Publishing that draft release then pushes it to the VS Marketplace via
`publish.yml`.

### Included
- #64 Fix diff-loading bugs that prevented the git tree filter from
activating (detached HEAD, worktree detection, modern-index checksum,
repo-owned-by-another-user; + `actions/cache` v4 bump).
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