use std::path::Path;
use anyhow::{Result, bail};
pub fn commit_amend(workdir: &Path, message: Option<&str>) -> Result<()> {
if let Some(msg) = message {
super::run_git(
workdir,
&[
"commit",
"--quiet",
"--allow-empty",
"--amend",
"--only",
"-m",
msg,
],
)
} else {
super::run_git_interactive(
workdir,
&["commit", "--quiet", "--allow-empty", "--amend", "--only"],
)
}
}
pub fn commit_amend_message_unverified(workdir: &Path, message: &str) -> Result<()> {
super::run_git(
workdir,
&[
"commit",
"--quiet",
"--allow-empty",
"--amend",
"--only",
"--no-verify",
"--cleanup=verbatim",
"-m",
message,
],
)
}
pub fn commit_amend_no_edit(workdir: &Path, opts: &[&str]) -> Result<()> {
let before = if opts.is_empty() {
None
} else {
Some((
super::rev_parse(workdir, "HEAD")?,
super::rev_parse(workdir, "HEAD^{tree}")?,
))
};
let mut args = vec!["commit"];
args.extend(opts);
args.extend(["--amend", "--no-edit", "--allow-empty"]);
super::run_git(workdir, &args)?;
if let Some((head, tree)) = before {
if super::rev_parse(workdir, "HEAD^{tree}")? == tree {
bail!(
"`git commit --amend` left the commit as it was, so nothing was amended\n\
Either an argument after `--` kept git from committing, or what was staged \
already matched the commit"
);
}
if super::rev_parse(workdir, "HEAD^").is_ok_and(|parent| parent == head) {
bail!("`git commit --amend` committed on top of the target instead of amending it");
}
}
Ok(())
}
fn deletion_already_staged(workdir: &Path, path: &str) -> Result<bool> {
if workdir.join(path).symlink_metadata().is_ok() {
return Ok(false);
}
let out = super::run_git_stdout(
workdir,
&[
"diff",
"--cached",
"--name-only",
"--diff-filter=D",
"--",
path,
],
)?;
Ok(!out.trim().is_empty())
}
pub fn stage_files(workdir: &Path, files: &[&str]) -> Result<()> {
stage_files_opts(workdir, files, &[])
}
pub fn stage_files_opts(workdir: &Path, files: &[&str], opts: &[&str]) -> Result<()> {
let mut to_add: Vec<&str> = Vec::with_capacity(files.len());
for file in files {
if !deletion_already_staged(workdir, file)? {
to_add.push(file);
}
}
if to_add.is_empty() {
return Ok(());
}
let mut args = vec!["add"];
args.extend(opts);
args.push("--");
args.extend(&to_add);
run_add(workdir, &args, opts)
}
pub fn stage_path(workdir: &Path, path: &str) -> Result<()> {
stage_files(workdir, &[path])
}
pub fn stage_from(workdir: &Path, source: &str, files: &[&str]) -> Result<()> {
if files.is_empty() {
return Ok(());
}
let literal: Vec<String> = files.iter().map(|f| format!(":(literal){f}")).collect();
let source = format!("--source={source}");
let mut args = vec!["restore", "--staged", source.as_str(), "--"];
args.extend(literal.iter().map(|f| f.as_str()));
super::run_git(workdir, &args)
}
pub fn commit(workdir: &Path, message: &str) -> Result<()> {
commit_captured(workdir, message, &[])
}
pub fn commit_captured(workdir: &Path, message: &str, opts: &[&str]) -> Result<()> {
let mut args = vec!["commit"];
args.extend(opts);
args.extend(["-m", message]);
super::run_git(workdir, &args)
}
pub fn commit_opts(workdir: &Path, message: Option<&str>, opts: &[&str]) -> Result<()> {
let mut args = vec!["commit"];
args.extend(opts);
if let Some(message) = message {
args.extend(["-m", message]);
}
if message.is_none() || runs_uncaptured(opts) {
super::run_git_interactive(workdir, &args)
} else {
super::run_git(workdir, &args)
}
}
fn runs_uncaptured(opts: &[&str]) -> bool {
!opts.is_empty() && !crate::core::agent_mode::enabled()
}
pub fn reset_mixed(workdir: &Path, target: &str) -> Result<()> {
super::run_git(workdir, &["reset", target])
}
pub fn reset_soft(workdir: &Path, target: &str) -> Result<()> {
super::run_git(workdir, &["reset", "--soft", target])
}
pub fn reset_hard(workdir: &Path, target: &str) -> Result<()> {
super::run_git(workdir, &["reset", "--hard", target])
}
pub fn stage_all(workdir: &Path) -> Result<()> {
stage_all_opts(workdir, &[])
}
pub fn stage_all_opts(workdir: &Path, opts: &[&str]) -> Result<()> {
let mut args = vec!["add"];
args.extend(opts);
args.push("-A");
run_add(workdir, &args, opts)
}
fn run_add(workdir: &Path, args: &[&str], opts: &[&str]) -> Result<()> {
if runs_uncaptured(opts) {
super::run_git_interactive(workdir, args)
} else {
super::run_git(workdir, args)
}
}
pub fn commit_with_editor(workdir: &Path) -> Result<()> {
commit_opts(workdir, None, &[])
}
#[cfg(test)]
#[path = "git_commit_test.rs"]
mod tests;