use std::path::Path;
use std::process::Command;
pub fn commit(workspace: &Path, message: &str) -> Result<String, String> {
run_commit(workspace, &["commit", "-m", message])
}
pub fn amend(workspace: &Path, message: &str) -> Result<String, String> {
run_commit(workspace, &["commit", "--amend", "-m", message])
}
pub fn show_head(workspace: &Path) -> Result<String, String> {
let out = Command::new("git")
.args(["show", "HEAD", "--no-color", "--stat", "-p"])
.current_dir(workspace)
.output()
.map_err(|e| e.to_string())?;
if out.status.success() {
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
} else {
Err(String::from_utf8_lossy(&out.stderr)
.lines()
.find(|l| !l.trim().is_empty())
.unwrap_or("git show failed")
.trim()
.to_string())
}
}
pub fn cherry_pick(workspace: &Path, hash: &str) -> Result<String, String> {
run_commit(workspace, &["cherry-pick", hash])
}
pub fn revert(workspace: &Path, hash: &str) -> Result<String, String> {
run_commit(workspace, &["revert", "--no-edit", hash])
}
pub fn head_message(workspace: &Path) -> String {
Command::new("git")
.args(["log", "-1", "--pretty=%B", "HEAD"])
.current_dir(workspace)
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_default()
}
pub fn rev_parse(workspace: &Path, rev: &str) -> Option<String> {
let out = Command::new("git")
.args(["rev-parse", "--verify", "--quiet", rev])
.current_dir(workspace)
.output()
.ok()?;
if !out.status.success() {
return None;
}
let h = String::from_utf8_lossy(&out.stdout).trim().to_string();
(!h.is_empty()).then_some(h)
}
pub fn reset_soft(workspace: &Path, rev: &str) -> Result<(), String> {
let out = Command::new("git")
.args(["reset", "--soft", rev])
.current_dir(workspace)
.output()
.map_err(|e| e.to_string())?;
if out.status.success() {
Ok(())
} else {
Err(String::from_utf8_lossy(&out.stderr)
.lines()
.map(str::trim)
.find(|l| !l.is_empty())
.unwrap_or("git reset failed")
.to_string())
}
}
fn run_commit(workspace: &Path, args: &[&str]) -> Result<String, String> {
let out = Command::new("git")
.args(args)
.current_dir(workspace)
.output()
.map_err(|e| e.to_string())?;
let stdout = String::from_utf8_lossy(&out.stdout);
if out.status.success() {
return Ok(stdout
.lines()
.find(|l| !l.trim().is_empty())
.unwrap_or("committed")
.trim()
.to_string());
}
let stderr = String::from_utf8_lossy(&out.stderr);
let pick = |s: &str| {
s.lines()
.map(str::trim)
.find(|l| !l.is_empty())
.map(str::to_string)
};
Err(pick(&stderr)
.or_else(|| pick(&stdout))
.unwrap_or_else(|| "git commit failed".to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use std::process::Command;
fn init_repo() -> tempfile::TempDir {
let d = tempfile::tempdir().unwrap();
for args in [
&["init", "-q"][..],
&["config", "user.email", "t@example.com"][..],
&["config", "user.name", "Test"][..],
&["config", "commit.gpgsign", "false"][..],
] {
let _ = Command::new("git")
.args(args)
.current_dir(d.path())
.output();
}
d
}
fn git_ok(d: &Path, args: &[&str]) {
let out = Command::new("git")
.args(args)
.current_dir(d)
.output()
.expect("git");
assert!(out.status.success(), "git {args:?} failed: {out:?}");
}
#[test]
fn amend_rewrites_head_message() {
let d = init_repo();
std::fs::write(d.path().join("a.txt"), "alpha").unwrap();
git_ok(d.path(), &["add", "."]);
commit(d.path(), "first commit").unwrap();
assert_eq!(head_message(d.path()), "first commit");
amend(d.path(), "rewritten subject").unwrap();
assert_eq!(head_message(d.path()), "rewritten subject");
let s = show_head(d.path()).unwrap();
assert!(s.contains("a.txt"));
}
}