use std::path::Path;
use std::process::Command;
use super::{Repo, RepoError};
#[derive(Debug, Default, Clone)]
#[non_exhaustive]
pub struct CheckoutOptions {
pub force: bool,
}
impl CheckoutOptions {
#[must_use]
pub const fn forced() -> Self {
Self { force: true }
}
}
impl Repo {
pub async fn checkout(&self, revspec: &str, opts: CheckoutOptions) -> Result<(), RepoError> {
if !opts.force {
let status = self.status().await?;
if !status.unstaged.is_empty() || !status.staged.is_empty() {
let mut paths = Vec::new();
paths.extend(status.staged.iter().cloned());
paths.extend(status.unstaged.iter().cloned());
return Err(RepoError::DirtyWorkingTree { paths });
}
}
let workdir = self.path().to_path_buf();
let revspec = revspec.to_string();
tokio::task::spawn_blocking(move || run_checkout(&workdir, &revspec, opts.force))
.await
.map_err(|join| RepoError::CheckoutFailed {
revspec: String::new(),
cause: format!("spawn_blocking join: {join}"),
})?
}
}
fn run_checkout(workdir: &Path, revspec: &str, force: bool) -> Result<(), RepoError> {
let mut cmd = Command::new("git");
cmd.arg("checkout");
if force {
cmd.arg("--force");
}
cmd.arg(revspec).current_dir(workdir);
let out = cmd.output().map_err(|e| RepoError::CheckoutFailed {
revspec: revspec.to_string(),
cause: format!("spawn `git checkout`: {e}"),
})?;
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
if stderr.contains("did not match any")
|| stderr.contains("unknown revision")
|| stderr.contains("not a valid object name")
|| stderr.contains("pathspec")
{
return Err(RepoError::RevspecNotFound { revspec: revspec.to_string() });
}
return Err(RepoError::CheckoutFailed {
revspec: revspec.to_string(),
cause: format!("git checkout: {}", stderr.trim()),
});
}
Ok(())
}