-
Notifications
You must be signed in to change notification settings - Fork 132
fix: parse filenames w.o. prefix, quoted unicode. Improve parse errors #439
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR enhances diff and status parsing to correctly handle filenames without a/b prefixes and quoted/unicode paths, and improves parse error diagnostics. Key updates include a new FilePath type with quoting support, a revamped parser error model with richer context, and updates across ops and UI to use the new path formatting. Tests were added for non-ASCII filenames, quoted escapes, and ambiguous cases.
- Add FilePath with fmt() to unescape quoted paths (using smashquote) and switch DiffHeader to use FilePath.
- Overhaul parser errors to accumulate ThinParseError with contextual formatting and logging; support headers without prefixes and robust newline-or-EOF handling.
- Update ops, UI rendering, and status parsing to use Strings/Cow for paths; add tests for non-ASCII and quoted filenames.
Reviewed Changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| src/tests/snapshots/gitu__tests__non_ascii_filename.snap | Adds snapshot to validate UI with non-ASCII filenames. |
| src/tests/mod.rs | Adds test to exercise non-ASCII filename handling. |
| src/screen/status.rs | Switches untracked paths to String-based flow and PathBuf conversion. |
| src/ops/unstage.rs | Uses FilePath::fmt() for path resolution in unstage. |
| src/ops/stage.rs | Uses FilePath::fmt() for path resolution in stage. |
| src/ops/show.rs | Uses FilePath::fmt() for editor paths. |
| src/ops/discard.rs | Uses FilePath::fmt() for paths; adjusts discard behavior for Added/Renamed/Other. |
| src/items.rs | Formats displayed filenames using FilePath::fmt(). |
| src/highlight.rs | Uses FilePath::fmt() for syntax highlight path discrimination. |
| src/gitu_diff.rs | Major parser refactor: FilePath, improved errors, support for quoted/unprefixed paths. |
| src/git/status.rs | Changes StatusFile path types to String. |
| src/git/parse/status/mod.rs | Parses status paths as Strings and unescapes quoted paths (for path; not yet for new_path). |
| Cargo.toml | Adds tinyvec and smashquote dependencies. |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| ItemData::Delta { diff, file_i } => { | ||
| let file_path = &diff.file_diffs[*file_i].header.new_file; | ||
| let path: &str = &file_path.fmt(&diff.text); | ||
| editor(Path::new(path), None) | ||
| } |
Copilot
AI
Oct 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This takes a reference to a temporary Cow returned by fmt(), resulting in a dangling reference. Bind the Cow (or an owned String) to a local variable so it lives through the call.
| } => { | ||
| let file_path = &diff.file_diffs[*file_i].header.new_file; | ||
| let path: &str = &file_path.fmt(&diff.text); | ||
| editor( | ||
| Path::new(path), | ||
| Some(diff.file_line_of_first_diff(*file_i, *hunk_i) as u32), | ||
| ) | ||
| } |
Copilot
AI
Oct 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same temporary reference issue as above; path borrows from a temporary Cow. Store the Cow or an owned String in a local variable before passing a reference to Path::new.
| let old_mask = diff.mask_old_hunk(file_index, hunk_index); | ||
| let old_file_range = file_diff.header.old_file.clone(); | ||
| let old_path = &diff.text[old_file_range]; | ||
| let old_path = &old_file_range.fmt(&diff.text); |
Copilot
AI
Oct 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both lines take references to temporary Cow values returned by fmt(), which will drop immediately, leaving dangling references. Assign the Cow to a local variable and pass a reference to its contents.
| let new_mask = diff.mask_new_hunk(file_index, hunk_index); | ||
| let new_file_range = file_diff.header.new_file.clone(); | ||
| let new_path = &diff.text[new_file_range]; | ||
| let new_path = &new_file_range.fmt(&diff.text); |
Copilot
AI
Oct 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both lines take references to temporary Cow values returned by fmt(), which will drop immediately, leaving dangling references. Assign the Cow to a local variable and pass a reference to its contents.
| // TODO Need to handle invalid unicode | ||
| let line_start = self.input[..cursor].rfind('\n').unwrap_or(0); | ||
| let line_end = self.input[line_start..] | ||
| .find('\n') |
Copilot
AI
Oct 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
line_end is computed as an offset within the slice self.input[line_start..] but then used as an absolute index, which can cause slicing panics. Convert the relative offset to an absolute index.
| .find('\n') | |
| .find('\n') | |
| .map(|offset| line_start + offset) |
| let file_path = &diff.file_diffs[*file_i].header.new_file; | ||
| let path: &str = &file_path.fmt(&diff.text); | ||
| editor(Path::new(path), None) |
Copilot
AI
Oct 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggested fix: bind the Cow to a local variable to avoid borrowing a temporary. For example: let path = file_path.fmt(&diff.text); editor(Path::new(path.as_ref()), None).
| let file_path = &diff.file_diffs[*file_i].header.new_file; | ||
| let path: &str = &file_path.fmt(&diff.text); | ||
| editor( | ||
| Path::new(path), | ||
| Some(diff.file_line_of_first_diff(*file_i, *hunk_i) as u32), | ||
| ) |
Copilot
AI
Oct 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggested fix: let path = file_path.fmt(&diff.text); editor(Path::new(path.as_ref()), Some(...)). This ensures the referent outlives the call.
| let old_mask = diff.mask_old_hunk(file_index, hunk_index); | ||
| let old_file_range = file_diff.header.old_file.clone(); | ||
| let old_path = &diff.text[old_file_range]; | ||
| let old_path = &old_file_range.fmt(&diff.text); |
Copilot
AI
Oct 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggested fix: store the Cow in locals so their lifetimes cover the call: let old = old_file_range.fmt(&diff.text); let newp = new_file_range.fmt(&diff.text); then pass old.as_ref(), newp.as_ref() to iter_syntax_highlights.
| let new_mask = diff.mask_new_hunk(file_index, hunk_index); | ||
| let new_file_range = file_diff.header.new_file.clone(); | ||
| let new_path = &diff.text[new_file_range]; | ||
| let new_path = &new_file_range.fmt(&diff.text); |
Copilot
AI
Oct 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggested fix: store the Cow in locals so their lifetimes cover the call: let old = old_file_range.fmt(&diff.text); let newp = new_file_range.fmt(&diff.text); then pass old.as_ref(), newp.as_ref() to iter_syntax_highlights.
21610ed to
ba69ec4
Compare
No description provided.