Git - re-read the index base when staging, reverting or unstaging ranges#326397
Open
soreavis wants to merge 1 commit into
Open
Git - re-read the index base when staging, reverting or unstaging ranges#326397soreavis wants to merge 1 commit into
soreavis wants to merge 1 commit into
Conversation
Staging, reverting and unstaging selected line ranges computed the result against a document opened for the git index/HEAD content (openTextDocument on a git: URI). That content is served fresh by the git file system provider, but the workbench caches the document and its invalidation after a repository change is debounced, so a stage -> commit -> stage sequence with nothing in between reads the pre-commit base and stages an exact revert of the just-committed change. Read the base directly through the file system provider (workspace.fs.readFile), which always reads fresh, instead of opening a document for it. applyLineChanges now takes the two sides as strings so the freshly read base can be applied without a TextDocument. Fixes microsoft#64027
Contributor
📬 CODENOTIFYThe following users are being notified based on files changed in this PR: @lszomoruMatched files:
|
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes stale Git index/HEAD reads during range staging, reverting, and unstaging.
Changes:
- Reads Git base content directly from the filesystem provider.
- Refactors line-change application to operate on strings.
- Adds unit and end-to-end regression coverage.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
extensions/git/src/staging.ts |
Adds string-based line indexing and change application. |
extensions/git/src/commands.ts |
Uses fresh base content for range operations. |
extensions/git/src/test/staging.test.ts |
Tests line application and EOL edge cases. |
extensions/git/src/test/smoke.test.ts |
Covers the stale-index regression. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #64027
The bug
Stage a selected range, commit, then stage a second range with nothing in between, and the second stage also stages an exact revert of the just-committed region (it shows up in
git diff --cachedand gets committed). Reliably reproducible; @joaomoreno diagnosed the mechanism back in 2018 ("VS Code fails to refresh the index version of the file in time for that second stage").Root cause
_stageChanges/_revertChanges/the two unstage paths build the staged content by applying the selected line changes onto the index/HEAD base, which they obtain viaopenTextDocument(toGitUri(uri, '~' | 'HEAD')). The git file system provider serves that content fresh (readFilerunsgit showevery time), but the workbench caches theTextDocument, and the provider's invalidation event is@debounce(1100). So within ~1.1s of a commit the cached base is stale — the second stage applies the new hunk onto the pre-commit base and reverts the first commit.workspace.fs.readFileon the samegit:URI bypasses the document cache and is always fresh.The fix
Read the base directly with
workspace.fs.readFile+workspace.decode({ encoding: modifiedDocument.encoding })(the inverse of theworkspace.encode({ encoding })already used on the stage write path), instead of opening a document for it.applyLineChangesnow takes the two sides as strings, backed by a smallLineIndexhelper that provides thelineCount/line-length/getTextbehaviour it relied on — so the freshly read base can be applied without aTextDocument. Both parameters became strings because the unstage sites pass the git side as themodifiedargument.I kept
LineIndexrather than reaching foropenTextDocument({ content })deliberately: an untitled document per stage would land inworkspace.textDocumentsand fireonDidOpenTextDocumentto every extension on a frequently-run command, and slicing the string preserves the base bytes/EOL exactly (an untitled doc infers and can normalize EOL).LineIndexis verified byte-identical to the previousTextDocument-based algorithm.Scope
This changes the four
applyLineChanges-based range/hunk commands (stage/revert/unstage selected ranges and the gutter stage/revert/unstage-change actions). The diff-editor toolbar'sgit.diff.stageHunk/stageSelectionpath stages a string precomputed by the diff editor and never went through this base read, so it is unaffected.Tests
extensions/git/src/test/smoke.test.ts— an end-to-end regression: stage a region, commit, stage a second region, and assert the index contains only the second region. Verified it fails on the current code (stages1\n…— line 1 reverted) and passes with the fix.extensions/git/src/test/staging.test.ts— unit coverage for theapplyLineChangesstring refactor: mid-file, end-of-file insert/delete (Can't stage empty removed last line #59670), CRLF, and multiple selected changes.The end-to-end test covers
git.stageSelectedRanges(the~/index base), which is the reliably reproducible case since the working-tree quick diff always keeps that document open. The revert and unstage sites take the identical fresh read against theHEADbase, whose staleness is impractical to drive in an end-to-end test, so they lean on theapplyLineChangesbyte-parity units plus the shared read path.