Repository navigation
fix(git): keep single-file operations out of .git and symlinked directories - #683
cursor[bot] wants to merge 1 commit into
Conversation
[Medium impact] The changes affect file and hunk commit and discard actions in the app. The PR should not merge until directory symlink aliases into Findings
SummaryAdds checks against repository-root paths, git metadata names, and directory symlinks that leave the repository. File restores now match wildcard characters literally.
Reviews (2) · Last reviewed commit: "fix(git): keep single-file operations ou..." · Reviewed by Greptile |
…tories repo_relative_path only normalized paths lexically, so two escapes remained after the absolute and `..` checks: - A path through a symlinked directory (`linked/victim.txt`, with `linked` pointing outside the repository) passed every check, and restore_file, restore_hunk and commit_file then read, wrote or deleted outside the work tree. The deepest existing ancestor of the target must now resolve inside the work tree. The final component is not resolved, so tracked symlinks such as `result` keep working. - `.git/HEAD` (or `.GIT/HEAD` on a case-insensitive volume) was a valid repository-relative path, so discarding it deleted repository metadata. Any `.git` component is now rejected, as is a path naming the work tree root. restore_file also matched its path as a pathspec, so discarding a tracked file named `*.nix` restored every `.nix` file. The checkout now matches the path literally. The symlinked-directory check follows the first version of #683.
c96908a to
2abc42a
Compare
| anyhow::ensure!( | ||
| resolved.starts_with(workdir), | ||
| "path `{}` resolves outside repository `{}`", | ||
| target.display(), | ||
| workdir.display() | ||
| ); |
There was a problem hiding this comment.
Symlinks bypass the metadata check
With linked -> .git, restore_file(dir, "linked/HEAD") passes the new checks: the supplied path contains no .git component, and the resolved parent passes starts_with(workdir). The untracked-file branch then deletes .git/HEAD, breaking the repository. Reject resolved parents inside git metadata as well as literal .git components.
How this was verified: discard_single_file forwards the filename to restore_file, whose parent check accepts the alias before remove_file deletes the metadata file.
🎨 Storybook previewUpdated for 1bf79fe
|
📋 PR Overview
🔬 Coverage
|
…tories repo_relative_path only normalized paths lexically, so two escapes remained after the absolute and `..` checks: - A path through a symlinked directory (`linked/victim.txt`, with `linked` pointing outside the repository) passed every check, and restore_file, restore_hunk and commit_file then read, wrote or deleted outside the work tree. The deepest existing ancestor of the target must now resolve inside the work tree. The final component is not resolved, so tracked symlinks such as `result` keep working. - `.git/HEAD` (or `.GIT/HEAD` on a case-insensitive volume) was a valid repository-relative path, so discarding it deleted repository metadata. Any `.git` component is now rejected, as is a path naming the work tree root. restore_file also matched its path as a pathspec, so discarding a tracked file named `*.nix` restored every `.nix` file. The checkout now matches the path literally. The symlinked-directory check follows the first version of #683.
2abc42a to
1bf79fe
Compare
|








Summary
Builds on #679, which moved single-file git operations onto
repo_relative_pathand closed the absolute-path and..escapes. Three gaps remained:linked-out/victim.txt, withlinked-outa symlink to a directory outside the reporestore_filedeletes the outside file andrestore_hunkrewrites it.git/HEADor.GIT/HEADrestore_filedeletes repository metadata*.nix.nixfileresult -> /nix/store/...(leaf symlink)Every caller of
repo_relative_path(commit_files,commit_hunk,restore_hunk,restore_file) gets the checks. It sits on top of #679, now merged. It replaces the first version of this PR, which targeteddevelop; its symlinked-directory check is kept.#no-linear
Test Plan
git::exectests: metadata and root rejection,restore_filerefusing.git/HEAD, the symlinked-directory escape throughrestore_file,restore_hunkandcommit_file, removing an untracked leaf symlink without touching its target, and literal glob matching. Four fail without the fix; the leaf-symlink test guards against over-rejection and passes either way. All 47git::exectests pass with the fix.cargo clippy -p nixmac --all-targets -- -D warningsflags nothing in the changed code (its only finding is an existinglet_and_returninevolve/nixmac_ignore.rs, untouched here).Docs
Merge Danger
Door: two-way.
Blast Radius: single-file git operations (commit or discard a file or hunk). Paths under
.git, the work tree root, and paths that pass through a symlinked directory resolving outside the repository now return an error. Symlinked directories that stay inside the repository and leaf symlinks are unaffected.buildwill fail at notarization until the Apple Developer agreement is renewed, as on every PR right now.🤖 Generated with Claude Code