use super::super::Error;
use super::LandOutcome;
use crate::template::GitRunner;
use crate::workspace::CONFLICTED_REF_PREFIX;
use std::collections::BTreeMap;
use std::path::Path;
pub(super) fn run(
parent_worktree: &Path,
parent_id: &str,
compactor_id: &str,
compactor_ref: &str,
point: &str,
base: &str,
git: &dyn GitRunner,
) -> Result<LandOutcome, Error> {
let branch = crate::workspace::agent_ref(parent_id);
let range = format!("{point}..HEAD");
let stops = git
.run_capture(parent_worktree, &["rev-list", "--count", &range])
.map_err(|source| Error::Git {
op: "compaction land replay count",
source,
})?
.trim()
.parse::<u32>()
.unwrap_or(0);
let mut result = git.run(
parent_worktree,
&["rebase", "--empty=keep", "--onto", base, point, &branch],
);
let mut budget = stops;
while let Err(source) = result {
let unmerged = unmerged_stages(parent_worktree, git)?;
let keep: Vec<&String> = unmerged
.iter()
.filter(|(_, s)| **s == (true, false, true))
.map(|(path, _)| path)
.collect();
let marked: Vec<String> = unmerged
.keys()
.filter(|path| !keep.contains(path))
.map(String::clone)
.collect();
if unmerged.is_empty() || budget == 0 {
let _ = git.run(parent_worktree, &["rebase", "--abort"]);
return Err(Error::Git {
op: "compaction land rebase",
source,
});
}
if !marked.is_empty() {
return decline(parent_worktree, compactor_id, compactor_ref, marked, git);
}
budget -= 1;
let mut add = vec!["add", "--"];
add.extend(keep.iter().map(|s| s.as_str()));
git.run(parent_worktree, &add)
.map_err(|source| Error::Git {
op: "compaction land live-branch-wins add",
source,
})?;
result = git.run(
parent_worktree,
&["-c", "core.editor=true", "rebase", "--continue"],
);
}
Ok(LandOutcome::Landed)
}
fn unmerged_stages(
parent_worktree: &Path,
git: &dyn GitRunner,
) -> Result<BTreeMap<String, (bool, bool, bool)>, Error> {
let out = git
.run_capture(parent_worktree, &["ls-files", "-u"])
.map_err(|source| Error::Git {
op: "compaction land unmerged",
source,
})?;
let mut stages: BTreeMap<String, (bool, bool, bool)> = BTreeMap::new();
for line in out.lines() {
let Some((meta, path)) = line.split_once('\t') else {
continue;
};
let entry = stages.entry(path.to_string()).or_default();
match meta.rsplit(' ').next() {
Some("1") => entry.0 = true,
Some("2") => entry.1 = true,
Some("3") => entry.2 = true,
_ => {}
}
}
Ok(stages)
}
fn decline(
parent_worktree: &Path,
compactor_id: &str,
compactor_ref: &str,
paths: Vec<String>,
git: &dyn GitRunner,
) -> Result<LandOutcome, Error> {
git.run(parent_worktree, &["rebase", "--abort"])
.map_err(|source| Error::Git {
op: "compaction land abort",
source,
})?;
let conflicted_ref = format!("{CONFLICTED_REF_PREFIX}{compactor_id}");
git.run(
parent_worktree,
&["update-ref", conflicted_ref.as_str(), compactor_ref],
)
.map_err(|source| Error::Git {
op: "compaction land decline update-ref",
source,
})?;
Ok(LandOutcome::Conflicted(paths))
}