Skip to content

feat: Allow for patch stage/unstage in Git diff viewer#585

Open
atreat wants to merge 20 commits intoDimillian:mainfrom
atreat:git-stage-patch
Open

feat: Allow for patch stage/unstage in Git diff viewer#585
atreat wants to merge 20 commits intoDimillian:mainfrom
atreat:git-stage-patch

Conversation

@atreat
Copy link
Copy Markdown

@atreat atreat commented Mar 26, 2026

Changes

  • Allows user to hover over hunks in git diff viewer and stage each individually.
  • Renders action button in gutter next to line numbers.
  • Will draw a vertical line on the left side of the hunk to highlight which changes are staged.
  • When hunk contains only a deletion, will render action button on right side of deletion hunk

Idea is to keep action buttons near the center of the scrollable region so see can skim through changes and quickly stage/unstage hunks.

Screenshots:

stage hunk:
stage-btn

unstage hunk:
unstage-btn

unstage deletion only:
unstage-deletion-btn

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dfee6d8a7e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +1275 to +1278
let (diff_args, reverse_apply): (&[&str], bool) = match source {
"unstaged" => (&["diff", "--no-color", "-U0", "--"], false),
"staged" => (&["diff", "--cached", "--no-color", "-U0", "--"], true),
_ => unreachable!(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use ignore-whitespace mode when resolving display hunk IDs

When git_diff_ignore_whitespace_changes is enabled, get_git_diffs_inner computes displayHunkId values from git diff -w -U0 hunks, but this branch rebuilds the source patch without -w. In hunks that mix whitespace-only edits with real edits, the hunk header tuple (old_start/old_count/new_start/new_count) can differ, so build_display_hunk_patch cannot find the ID and returns “Display hunk no longer matches…”, breaking hunk stage/unstage in that mode.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d17749363c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

if new_line < insertion_point {
break;
}
delta -= hunk.old_count as isize;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Correct index→HEAD remapping for staged deletion hunks

map_new_to_old_line_clamped handles staged deletion hunks (new_count == 0) by subtracting old_count from delta, but this mapper is used to convert unstaged hunk old-line numbers (index coordinates) back into display old-line numbers (HEAD coordinates). After a staged deletion above the target, the old-line number should move forward, not backward; with the current sign, later unstaged hunks are mapped to the wrong range and find_display_hunk_span can’t locate them, so some hunks in mixed staged/unstaged files never get actionable Stage controls.

Useful? React with 👍 / 👎.

Comment on lines +204 to +206
let mut args = vec!["diff"];
if cached {
args.push("--cached");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Generate unstaged source diffs for untracked files

source_diff_for_path always shells out to regular git diff ... -- <path>, which compares index vs working tree. git diff -h shows untracked path diffs require the separate --no-index <path> <path> mode; without that, new files produce an empty patch. Since status still marks them as unstaged, these files end up with missing unstagedDiff/displayHunks, leaving line-level stage permanently unavailable for brand-new files.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 627fbe5aa4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +1282 to +1286
let mut args = vec!["diff"];
if source == "staged" {
args.push("--cached");
}
args.push("--no-color");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use --no-index when applying untracked display hunks

get_git_diffs_inner now creates display hunks for untracked files using no-index diffs, but this path rebuilds the source patch with plain git diff for source == "unstaged". For WT_NEW files that command returns an empty patch, so clicking the new Stage hunk action always errors with “No changes available for the requested display hunk.” and the feature is unusable for brand-new files.

Useful? React with 👍 / 👎.

Comment on lines +553 to +556
} else if body_line.starts_with(' ') {
old_cursor += 1;
new_cursor += 1;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve no-newline markers in parsed patch hunks

This parser only tracks +, -, and context-prefixed lines and silently drops \ No newline at end of file hunk markers, so rebuilt patches from build_display_hunk_patch/build_selected_patch lose required metadata. In hunks touching files without trailing newlines, git apply --cached --unidiff-zero can no longer match the source hunk and stage/unstage fails.

Useful? React with 👍 / 👎.

}

if let Some(text) = body_line.strip_prefix('+') {
if !body_line.starts_with("+++") {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop filtering valid +++/--- content lines

The starts_with("+++")/starts_with("---") filters run inside hunk bodies, so real changed content that begins with those prefixes (for example an added line +++title, represented as ++++title in patch text) is discarded as if it were a file header. That undercounts hunk lines and can produce mismatched display-hunk IDs or incomplete patches when applying the action.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@xkonjin xkonjin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review focusing on bugs, security, and tests.

Overall: I reviewed the diff and did not spot obvious bugs or security risks. Please ensure tests cover new logic and edge cases.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f586b57c9e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +314 to +316
ParsedDisplayLineType::Add | ParsedDisplayLineType::Del => current.push((index, line)),
_ => {
if !current.is_empty() {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep no-newline markers inside display hunk segments

build_display_segments terminates a segment on every non-add/delete line, so ParsedDisplayLineType::Meta (\ No newline at end of file) splits what is logically one change hunk into separate pieces. For a replacement at EOF without trailing newline, the parsed display stream becomes del, meta, add, meta; find_display_hunk_span then cannot satisfy the source hunk's expected add+del counts and drops that display hunk entirely, so line-level Stage/Unstage controls disappear for those edits.

Useful? React with 👍 / 👎.

@atreat atreat force-pushed the git-stage-patch branch from f586b57 to 059b937 Compare April 1, 2026 15:14
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 059b937c4c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}

pub(super) fn parse_zero_context_patch(diff_patch: &str) -> Result<ParsedPatch, String> {
let lines: Vec<&str> = diff_patch.lines().collect();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve CRLF when parsing zero-context patches

parse_zero_context_patch tokenizes the patch with diff_patch.lines(), which normalizes \r\n to \n and drops the trailing \r from +/- hunk lines. The rebuilt patch therefore no longer matches CRLF files (for example it emits -b instead of -b\r), and git apply --cached --unidiff-zero fails with “patch does not apply”. This breaks the new hunk stage/unstage flow for repositories checked out with CRLF line endings (common on Windows).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 28d6fc0ce4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +1200 to +1201
let source_patch =
load_selection_source_patch(&repo_root, action_path.as_str(), &source, false).await?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor ignore-whitespace mode for line-selection staging

stage_git_selection_inner always loads its source patch with ignore_whitespace_changes set to false, even though diff payloads can be generated in whitespace-ignored mode. In repositories where git_diff_ignore_whitespace_changes is enabled, selected line coordinates from the displayed -w diff can diverge from this non--w source patch, causing build_selected_patch to reject valid selections with “Selected lines do not match the current diff.” This makes the line-selection stage/unstage path unreliable whenever whitespace-only edits coexist with substantive edits.

Useful? React with 👍 / 👎.

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.

2 participants