use anyhow::{bail, Context, Result};
use colored::Colorize;
use console::Term;
use dialoguer::{theme::ColorfulTheme, Select};
use std::path::Path;
use std::process::Command;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StagingAction {
All,
Patch,
Continue,
Abort,
}
#[derive(Debug, Clone, Copy)]
pub enum ContinueLabel {
EmptyBranch,
AmendMessageOnly,
}
impl ContinueLabel {
fn as_str(self) -> &'static str {
match self {
Self::EmptyBranch => "Empty branch (no changes)",
Self::AmendMessageOnly => "Just edit the commit message",
}
}
}
pub fn prompt_action(
workdir: &Path,
continue_label: ContinueLabel,
non_tty_hint: &str,
) -> Result<StagingAction> {
if !Term::stderr().is_term() {
bail!("No files staged. {}", non_tty_hint);
}
let change_count = count_uncommitted_changes(workdir);
let stage_all_label = if change_count > 0 {
format!("Stage all changes ({} files modified)", change_count)
} else {
"Stage all changes".to_string()
};
let options = [
stage_all_label.as_str(),
"Select changes to commit (--patch)",
continue_label.as_str(),
"Abort",
];
let choice = Select::with_theme(&ColorfulTheme::default())
.with_prompt("No files staged. What would you like to do?")
.items(options.as_slice())
.default(0)
.interact()?;
Ok(match choice {
0 => StagingAction::All,
1 => StagingAction::Patch,
2 => StagingAction::Continue,
_ => StagingAction::Abort,
})
}
pub fn stage_all(workdir: &Path) -> Result<()> {
let status = Command::new("git")
.args(["add", "-A"])
.current_dir(workdir)
.status()
.context("Failed to run git add -A")?;
if !status.success() {
bail!("Failed to stage changes");
}
Ok(())
}
pub fn stage_patch(workdir: &Path) -> Result<()> {
let status = Command::new("git")
.args(["add", "--patch"])
.current_dir(workdir)
.status()
.context("Failed to run git add --patch")?;
if !status.success() {
bail!("git add --patch exited with an error");
}
Ok(())
}
pub fn is_staging_area_empty(workdir: &Path) -> Result<bool> {
let status = Command::new("git")
.args(["diff", "--cached", "--quiet"])
.current_dir(workdir)
.status()
.context("Failed to check staged changes")?;
Ok(status.success())
}
pub fn count_uncommitted_changes(workdir: &Path) -> usize {
Command::new("git")
.args(["status", "--porcelain"])
.current_dir(workdir)
.output()
.map(|o| {
String::from_utf8_lossy(&o.stdout)
.lines()
.filter(|l| !l.is_empty())
.count()
})
.unwrap_or(0)
}
pub fn has_uncommitted_changes(workdir: &Path) -> bool {
Command::new("git")
.args(["status", "--porcelain"])
.current_dir(workdir)
.output()
.map(|o| !o.stdout.is_empty())
.unwrap_or(false)
}
pub fn print_patch_empty_notice() {
println!(
"{}",
"No hunks staged. Aborted — re-run and pick hunks, or stage via `git add` first.".dimmed()
);
}