use super::Error;
use crate::template::GitRunner;
use crate::workspace::CONFLICTED_REF_PREFIX;
use std::collections::BTreeMap;
use std::path::Path;
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Replayed {
Landed,
Conflicted(Vec<String>),
}
pub(crate) struct Replay<'a> {
pub(crate) branch_id: &'a str,
pub(crate) point: &'a str,
pub(crate) base: &'a str,
pub(crate) mark_id: &'a str,
pub(crate) mark_at: &'a str,
}
pub(crate) fn run(
worktree: &Path,
replay: &Replay<'_>,
git: &dyn GitRunner,
) -> Result<Replayed, Error> {
let branch = crate::workspace::agent_ref(replay.branch_id);
let range = format!("{}..HEAD", replay.point);
let stops = git
.run_capture(worktree, &["rev-list", "--count", &range])
.map_err(|source| Error::Git {
op: "rebase-forward replay count",
source,
})?
.trim()
.parse::<u32>()
.unwrap_or(0);
let mut result = git.run(
worktree,
&[
"rebase",
"--empty=keep",
"--onto",
replay.base,
replay.point,
&branch,
],
);
let mut budget = stops;
while let Err(source) = result {
let unmerged = unmerged_stages(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(worktree, &["rebase", "--abort"]);
return Err(Error::Git {
op: "rebase-forward rebase",
source,
});
}
if !marked.is_empty() {
return decline(worktree, replay, marked, git);
}
budget -= 1;
let mut add = vec!["add", "--"];
add.extend(keep.iter().map(|s| s.as_str()));
git.run(worktree, &add).map_err(|source| Error::Git {
op: "rebase-forward live-branch-wins add",
source,
})?;
result = git.run(
worktree,
&["-c", "core.editor=true", "rebase", "--continue"],
);
}
Ok(Replayed::Landed)
}
fn unmerged_stages(
worktree: &Path,
git: &dyn GitRunner,
) -> Result<BTreeMap<String, (bool, bool, bool)>, Error> {
let out = git
.run_capture(worktree, &["ls-files", "-u"])
.map_err(|source| Error::Git {
op: "rebase-forward 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(
worktree: &Path,
replay: &Replay<'_>,
paths: Vec<String>,
git: &dyn GitRunner,
) -> Result<Replayed, Error> {
git.run(worktree, &["rebase", "--abort"])
.map_err(|source| Error::Git {
op: "rebase-forward abort",
source,
})?;
let conflicted_ref = format!("{CONFLICTED_REF_PREFIX}{}", replay.mark_id);
git.run(
worktree,
&["update-ref", conflicted_ref.as_str(), replay.mark_at],
)
.map_err(|source| Error::Git {
op: "rebase-forward decline update-ref",
source,
})?;
Ok(Replayed::Conflicted(paths))
}