Merge 03fe6a9826 into sapling-pr-archive-bolinfest

This commit is contained in:
Michael Bolin
2025-06-25 16:38:39 -07:00
committed by GitHub
5 changed files with 145 additions and 0 deletions

View File

@@ -250,6 +250,33 @@ impl<'a> App<'a> {
SlashCommand::Quit => {
break;
}
SlashCommand::Diff => {
use crate::get_git_diff::get_git_diff;
let (is_repo, diff_text) = match get_git_diff() {
Ok(v) => v,
Err(e) => {
let msg = format!("Failed to compute diff: {e}");
match &mut self.app_state {
AppState::Chat { widget } => {
widget.add_background_event(msg);
}
_ => {}
}
continue;
}
};
let text = if is_repo {
diff_text
} else {
"`/diff` — _not inside a git repository_".to_string()
};
if let AppState::Chat { widget } = &mut self.app_state {
widget.add_background_event(text);
}
}
},
}
}

View File

@@ -387,6 +387,14 @@ impl ChatWidget<'_> {
self.app_event_tx.send(AppEvent::Redraw);
}
/// Inject a background event into the conversation history. This is used
/// for displaying informational messages that originate from the UI
/// itself (e.g. the `/diff` command) rather than from the backend agent.
pub(crate) fn add_background_event(&mut self, message: String) {
self.conversation_history.add_background_event(message);
self.request_redraw();
}
pub(crate) fn handle_scroll_delta(&mut self, scroll_delta: i32) {
// If the user is trying to scroll exactly one line, we let them, but
// otherwise we assume they are trying to scroll in larger increments.

View File

@@ -0,0 +1,106 @@
//! Utility to compute the current Git diff for the working directory.
//!
//! The implementation mirrors the behaviour of the TypeScript version in
//! `codex-cli`: it returns the diff for tracked changes as well as any
//! untracked files. When the current directory is not inside a Git
//! repository, the function returns `Ok((false, String::new()))`.
use std::io;
use std::path::Path;
use std::process::{Command, Stdio};
/// Return value of [`get_git_diff`].
///
/// * `bool` Whether the current working directory is inside a Git repo.
/// * `String` The concatenated diff (may be empty).
pub(crate) fn get_git_diff() -> io::Result<(bool, String)> {
// First check if we are inside a Git repository.
if !inside_git_repo()? {
return Ok((false, String::new()));
}
// 1. Diff for tracked files.
let tracked_diff = run_git_capture_diff(&["diff", "--color"])?;
// 2. Determine untracked files.
let untracked_output = run_git_capture_stdout(&["ls-files", "--others", "--exclude-standard"])?;
let mut untracked_diff = String::new();
let null_device: &Path = if cfg!(windows) { Path::new("NUL") } else { Path::new("/dev/null") };
for file in untracked_output.split('\n').map(str::trim).filter(|s| !s.is_empty()) {
// Use `git diff --no-index` to generate a diff against the null device.
let args = [
"diff",
"--color",
"--no-index",
"--",
null_device.to_str().unwrap_or("/dev/null"),
file,
];
match run_git_capture_diff(&args) {
Ok(diff) => untracked_diff.push_str(&diff),
// If the file disappeared between ls-files and diff we ignore the error.
Err(err) if err.kind() == io::ErrorKind::NotFound => {},
Err(err) => return Err(err),
}
}
Ok((true, format!("{}{}", tracked_diff, untracked_diff)))
}
/// Helper that executes `git` with the given `args` and returns `stdout` as a
/// UTF-8 string. Any non-zero exit status is considered an *error*.
fn run_git_capture_stdout(args: &[&str]) -> io::Result<String> {
let output = Command::new("git")
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::null())
.output()?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
} else {
Err(io::Error::new(
io::ErrorKind::Other,
format!("git {:?} failed with status {}", args, output.status),
))
}
}
/// Like [`run_git_capture_stdout`] but treats exit status 1 as success and
/// returns stdout. Git returns 1 for diffs when differences are present.
fn run_git_capture_diff(args: &[&str]) -> io::Result<String> {
let output = Command::new("git")
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::null())
.output()?;
if output.status.success() || output.status.code() == Some(1) {
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
} else {
Err(io::Error::new(
io::ErrorKind::Other,
format!("git {:?} failed with status {}", args, output.status),
))
}
}
/// Determine if the current directory is inside a Git repository.
fn inside_git_repo() -> io::Result<bool> {
let status = Command::new("git")
.args(["rev-parse", "--is-inside-work-tree"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
match status {
Ok(s) if s.success() => Ok(true),
Ok(_) => Ok(false),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false), // git not installed
Err(e) => Err(e),
}
}

View File

@@ -31,6 +31,7 @@ mod conversation_history_widget;
mod exec_command;
mod git_warning_screen;
mod history_cell;
mod get_git_diff;
mod log_layer;
mod login_screen;
mod markdown;

View File

@@ -15,6 +15,8 @@ pub enum SlashCommand {
New,
ToggleMouseMode,
Quit,
/// Show git diff of the working directory.
Diff,
}
impl SlashCommand {
@@ -26,6 +28,7 @@ impl SlashCommand {
"Toggle mouse mode (enable for scrolling, disable for text selection)"
}
SlashCommand::Quit => "Exit the application.",
SlashCommand::Diff => "Show git diff of the working directory (including untracked files)",
}
}