use std::fmt;
use std::path::Path;
use xshell::Shell;
#[derive(Debug, Clone)]
enum Ref {
Branch(String),
Commit(String),
}
impl fmt::Display for Ref {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Branch(name) => write!(f, "{}", name),
Self::Commit(sha) => write!(f, "{}", sha),
}
}
}
impl Ref {
fn current(sh: &Shell) -> Result<Self, Box<dyn std::error::Error>> {
if let Ok(branch) = rbmt_cmd!(sh, "git symbolic-ref -q --short HEAD").read() {
return Ok(Self::Branch(branch.trim().to_string()));
}
let sha = rbmt_cmd!(sh, "git rev-parse HEAD").read()?;
Ok(Self::Commit(sha.trim().to_string()))
}
}
pub struct GitSwitchGuard<'a> {
sh: &'a Shell,
original_ref: Ref,
}
impl<'a> GitSwitchGuard<'a> {
pub fn new(sh: &'a Shell, git_ref: &str) -> Result<Self, Box<dyn std::error::Error>> {
let original_ref = Ref::current(sh)?;
rbmt_eprintln!("Switching from {} to {}", original_ref, git_ref);
rbmt_cmd!(sh, "git switch --detach").arg(git_ref).run()?;
Ok(Self { sh, original_ref })
}
}
impl Drop for GitSwitchGuard<'_> {
fn drop(&mut self) {
rbmt_eprintln!("Returning to original ref {}", self.original_ref);
let git_switch = match &self.original_ref {
Ref::Branch(name) => {
rbmt_cmd!(self.sh, "git switch").arg(name)
}
Ref::Commit(sha) => {
rbmt_cmd!(self.sh, "git switch --detach").arg(sha)
}
};
git_switch.run().expect("Failed to switch back to previous ref");
}
}
pub fn current_commit_id(sh: &Shell) -> Option<String> {
sh.cmd("git").args(["rev-parse", "HEAD"]).quiet().read().ok().map(|s| s.trim().to_owned())
}
pub fn has_changes_since(
sh: &Shell,
baseline: &str,
path: &Path,
) -> Result<bool, Box<dyn std::error::Error>> {
let output = rbmt_cmd!(sh, "git diff --name-only {baseline} -- {path}").read()?;
Ok(!output.trim().is_empty())
}
pub fn list_commits(sh: &Shell, base: &str) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let range_base = rbmt_cmd!(sh, "git merge-base HEAD {base}").read()?;
let range_base = range_base.trim();
let output = rbmt_cmd!(sh, "git log --reverse --format=%H {range_base}..HEAD").read()?;
let commits = output.lines().map(|s| s.trim().to_owned()).filter(|s| !s.is_empty()).collect();
Ok(commits)
}
pub fn for_each_commit<F>(
sh: &Shell,
lockfile: crate::lock::LockFile,
baseline: Option<&str>,
mut on_commit: F,
) -> Result<(), Box<dyn std::error::Error>>
where
F: FnMut(&Shell) -> Result<(), Box<dyn std::error::Error>>,
{
if let Some(baseline) = baseline {
let commits = list_commits(sh, baseline)?;
if commits.is_empty() {
rbmt_eprintln!("No commits found between '{}' and HEAD.", baseline);
return Ok(());
}
for sha in commits {
rbmt_eprintln!("Running on commit {}...", &sha[..12]);
let _git_guard = GitSwitchGuard::new(sh, &sha)?;
let _lockfile_guard = lockfile.activate(sh)?;
on_commit(sh)?;
}
} else {
let _lockfile_guard = lockfile.activate(sh)?;
on_commit(sh)?;
}
Ok(())
}