Conversation
Path lookup used object types to decide whether to descend, allowing a symlink or gitlink pointing to a tree to expose paths below it. Directory entry modes must bound descent so lookup respects the recorded tree layout. Direct object access remains available for inspecting malformed entries; path validation does not replace structural validation. Assisted-by: Claude Opus 5 Assisted-by: GPT-6 Signed-off-by: Hidde Beydals <hidde@hhh.computer>
hiddeco
force-pushed
the
fix/tree-traversal-and-worktree-recovery
branch
from
September 10, 2026 13:21
0b22c86 to
b8668da
Compare
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical error-identity and reset-synchronization issues remain, along with a moderate performance regression.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Improves unreadable and malformed Git tree handling, reset recovery, reference preservation, and path traversal.
Changes:
- Propagates subtree and malformed-tree errors.
- Defers reference updates until reset processing succeeds.
- Adds recovery and regression tests.
- Documents filesystem safety boundaries.
File summaries
| File | Summary |
|---|---|
worktree.go |
Updates reset and recovery logic. Findings: critical SkipWorktree synchronization issue; moderate duplicate tree-diff work. |
worktree_test.go |
Adds reset, checkout, pull, and recovery tests. |
worktree_fs.go |
Documents filesystem safety boundaries. |
worktree_fs_test.go |
Tests malformed and missing subtree handling. |
utils/merkletrie/doubleiter.go |
Preserves wrapped traversal errors. |
utils/merkletrie/doubleiter_test.go |
Tests propagated diff errors. |
plumbing/object/tree.go |
Improves tree validation, traversal, and error handling. Finding: critical loss of wrapped missing-object identity. |
plumbing/object/tree_test.go |
Adds tree traversal and malformed-object tests. |
options.go |
Removes obsolete internal reset state. |
Review details
Suppressed comments (1)
worktree.go:409
- HardReset computes the full index↔target tree diff here, but
resetIndeximmediately computes the same diff again before applying it. Every hard reset and forced checkout therefore walks and loads the target tree twice, which is a substantial regression for large repositories. Reuse this precomputed change set when resetting the index (while applying the existing path filter) instead of rebuilding it.
trackedChanges, err = w.diffTreeWithStaging(t, true)
if err != nil {
return err
- Files reviewed: 9/9 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+792
to
+793
| if errors.Is(cause, io.EOF) { | ||
| return fmt.Errorf("entry %q has mode %s: %v", e.Name, e.Mode, cause) |
Comment on lines
+407
to
+409
| trackedChanges, err = w.diffTreeWithStaging(t, true) | ||
| if err != nil { | ||
| return err |
Unreadable subtrees were reported as EOF, allowing checkout and archive generation to succeed with incomplete results. Rebuilding an index from that enumeration could turn omitted paths into deletions. Callers need distinct errors for missing objects, wrong object types, and decode failures, with error identities preserved through tree diffs. EOF causes remain text-only so they cannot signal normal exhaustion. Missing promisor objects also fail because tree enumeration cannot fetch them lazily. Checkout and reset must finish processing before changing references, so a failed tree read does not leave HEAD at the unreadable target. This does not make index and worktree updates atomic. Assisted-by: Claude Opus 5 Assisted-by: GPT-6 Signed-off-by: Hidde Beydals <hidde@hhh.computer>
The path-validation comment incorrectly described CherryPick as a direct validator caller. It also conflated checks on tree names with checks on existing filesystem symlinks, obscuring which boundary provides protection. Assisted-by: Claude Opus 5 Assisted-by: GPT-6 Signed-off-by: Hidde Beydals <hidde@hhh.computer>
The underlying filesystem bypasses the worktree wrapper's path checks. Root containment alone does not protect an in-root .git directory, so callers need that distinction before writing names obtained from trees. Caller-supplied filesystems may provide different containment guarantees. Assisted-by: Claude Opus 5 Assisted-by: GPT-6 Signed-off-by: Hidde Beydals <hidde@hhh.computer>
Pull advanced the current branch before resetting the worktree. An unreadable subtree left HEAD at the fetched commit despite the failure, and retrying reported that the repository was already up to date. The reference update must follow a successful reset while retaining support for pulls into an unborn branch. Assisted-by: GPT-6 Signed-off-by: Hidde Beydals <hidde@hhh.computer>
Ascending from a directory entry named a/b removed only one path component, leaving a root-level sibling z reported as a/z. Malformed entry names can span multiple path components, so ascent must restore the parent path by tree depth. Assisted-by: GPT-6 Signed-off-by: Hidde Beydals <hidde@hhh.computer>
Hard reset and forced checkout could not recover from an unreadable old HEAD tree because they read it to identify tracked deletions. The old index provides those paths, including staged additions absent from the target. KeepReset still needs the old HEAD tree for its conflict checks. Recovery requires a readable target and index; it does not make updates atomic. Assisted-by: GPT-6 Signed-off-by: Hidde Beydals <hidde@hhh.computer>
hiddeco
force-pushed
the
fix/tree-traversal-and-worktree-recovery
branch
from
September 10, 2026 14:05
b8668da to
f6bd1ba
Compare
Resetting the index diffed the target tree against the index as the worktree sees it, which leaves out the entries flagged SkipWorktree. A path outside the sparse directories kept the hash it held before the reset, or stayed in the index after the target dropped it, and the reset still reported success. Committing afterwards would have restored the path's earlier content. The index records every path the target holds, so the diff that brings it to the target reports skipped entries too. Rewriting an entry carries the flag over, because the entries are the only record of the sparse state once a reset does not restate SparseDirs, and the flag still decides which paths reach the worktree. Assisted-by: Claude Opus 5 (1M context) Signed-off-by: Hidde Beydals <hidde@hhh.computer>
hiddeco
force-pushed
the
fix/tree-traversal-and-worktree-recovery
branch
from
September 10, 2026 14:05
f6bd1ba to
105fffb
Compare
hiddeco
commented
Sep 10, 2026
| // flagged SkipWorktree. Callers that bring the index to a tree need | ||
| // those entries; callers that compare the index with the worktree must | ||
| // leave it unset, because a skipped path is absent from disk by design. | ||
| IgnoreSkipWorktree bool |
Member
Author
There was a problem hiding this comment.
Not too keen on the naming here, and open to alternatives.
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.
Missing, corrupt, or malformed tree objects could cause several misleading or unrecoverable outcomes:
This change reports unreadable subtrees as errors and updates references only after reset processing succeeds. Hard reset and forced checkout identify tracked deletions from the pre-reset index, allowing recovery without reading the old HEAD tree.
KeepResetretains its old-tree conflict checks. Index and worktree updates are not atomic.It also fixes path handling for malformed trees: lookup descends only through directory entries, and walking a directory whose name contains slashes no longer corrupts subsequent sibling paths. Directory-mode descent follows Git’s find_tree_entry. Direct object access remains available for inspection.
Missing subtrees remain errors in partial clones because go-git cannot fetch them lazily. Documentation clarifies validation boundaries, error identities, iterator recovery, and the protections bypassed by direct use of
Worktree.Filesystem.Regression tests cover repeated failures, reference and index preservation, recovery after missing objects become available, and malformed-entry path handling