use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use std::process::Command;
pub(crate) struct CheckoutCheckpoint {
pub reference: String,
pub commit: String,
}
pub(crate) fn preserve_shared_checkout(
dir: &Path,
task_id: &str,
) -> Result<Option<CheckoutCheckpoint>> {
crate::sanitize::validate_task_id(task_id)?;
let probe = Command::new("git")
.arg("-C").arg(dir)
.env("LC_ALL", "C")
.args(["rev-parse", "--show-toplevel"])
.output().context("Failed to inspect shared checkout")?;
if !probe.status.success() {
let error = String::from_utf8_lossy(&probe.stderr);
if error.contains("not a git repository") {
return Ok(None);
}
anyhow::bail!("Failed to inspect shared checkout: {}", error.trim());
}
let root = PathBuf::from(String::from_utf8(probe.stdout)?.trim());
let scratch = ScratchIndex::new()?;
let index = scratch.0.join("index");
let head = git(&root, &index, &["rev-parse", "--verify", "HEAD"]);
let parent = match head {
Ok(head) => {
git(&root, &index, &["read-tree", &head])?;
Some(head)
}
Err(error) => {
let branch = git(&root, &index, &["symbolic-ref", "HEAD"])?;
let exists = Command::new("git").arg("-C").arg(&root)
.args(["show-ref", "--verify", "--quiet", &branch]).status()?;
anyhow::ensure!(exists.code() == Some(1), "Cannot read shared checkout HEAD: {error}");
git(&root, &index, &["read-tree", "--empty"])?;
None
}
};
let base_tree = git(&root, &index, &["write-tree"])?;
crate::worktree::stage_aid_files_with_index(
&root,
crate::worktree::AidStageMode::All,
&["target", "node_modules", "__pycache__"],
Some(&index),
)?;
let tree = git(&root, &index, &["write-tree"])?;
if tree == base_tree {
return Ok(None);
}
let message = format!(
"[aid] shared checkout recovery for {task_id}\n\nIncludes shared edits; not a task-owned merge artifact."
);
let mut args = vec!["commit-tree", tree.as_str(), "-m", message.as_str()];
if let Some(parent) = parent.as_deref() {
args.extend(["-p", parent]);
}
let commit = git(&root, &index, &args)?;
let reference = format!("refs/aid/recovery/{task_id}/{:032x}", rand::random::<u128>());
git(&root, &index, &["update-ref", &reference, &commit, ""])?;
Ok(Some(CheckoutCheckpoint { reference, commit }))
}
fn git(root: &Path, index: &Path, args: &[&str]) -> Result<String> {
let output = Command::new("git")
.arg("-C").arg(root)
.args(["-c", "user.name=aid recovery", "-c", "user.email=aid@localhost"])
.env("GIT_INDEX_FILE", index)
.env("GIT_OPTIONAL_LOCKS", "0")
.args(args).output().context("Failed to run checkpoint Git command")?;
anyhow::ensure!(output.status.success(), "git {}: {}", args[0], String::from_utf8_lossy(&output.stderr).trim());
Ok(String::from_utf8(output.stdout)?.trim().to_string())
}
struct ScratchIndex(PathBuf);
impl ScratchIndex {
fn new() -> Result<Self> {
use std::os::unix::fs::DirBuilderExt;
let path = std::env::temp_dir().join(format!("aid-recovery-{:032x}", rand::random::<u128>()));
std::fs::DirBuilder::new().mode(0o700).create(&path)?;
Ok(Self(path.canonicalize()?))
}
}
impl Drop for ScratchIndex {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[cfg(test)]
#[path = "checkpoint_tests.rs"]
mod tests;