use crate::prompt::Error;
use crate::template::GitRunner;
use crate::workspace::CONFLICTED_REF_PREFIX;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
const CONTEXT_EXCLUDES: &[&str] = &[
":(exclude)goal.md",
":(exclude)soul.md",
":(exclude)messages",
":(exclude)summary",
":(exclude)skills",
];
pub(super) fn terminal_ref_of(body: &str) -> Option<String> {
let mut lines = body.lines();
if lines.next() != Some("---") {
return None;
}
for line in lines {
if line == "---" {
return None;
}
if let Some(value) = line.strip_prefix("terminal_ref:") {
let value = value.trim();
if !value.is_empty() {
return Some(value.to_string());
}
}
}
None
}
pub(super) fn apply(
worktree: &Path,
child_id: &str,
terminal_ref: &str,
git: &dyn GitRunner,
) -> Result<(), Error> {
let fork = git
.run_capture(worktree, &["merge-base", "HEAD", terminal_ref])
.map_err(|source| Error::Git {
op: "transfer merge-base",
source,
})?;
let patch = patch_path(child_id);
let patch_str = patch.to_string_lossy().into_owned();
let output_arg = format!("--output={patch_str}");
let mut diff_args = vec!["diff", &fork, terminal_ref, &output_arg, "--"];
diff_args.extend_from_slice(CONTEXT_EXCLUDES);
git.run(worktree, &diff_args).map_err(|source| Error::Git {
op: "transfer diff",
source,
})?;
let empty = std::fs::metadata(&patch)
.map(|m| m.len() == 0)
.unwrap_or(true);
if empty {
let _ = std::fs::remove_file(&patch);
return Ok(());
}
let outcome = match git.run(worktree, &["apply", "--index", &patch_str]) {
Ok(()) => commit_transfer(worktree, child_id, git),
Err(_) => decline(worktree, child_id, terminal_ref, git),
};
let _ = std::fs::remove_file(&patch);
outcome
}
fn patch_path(child_id: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
std::env::temp_dir().join(format!("lernie-transfer-{child_id}-{nanos}.patch"))
}
fn commit_transfer(worktree: &Path, child_id: &str, git: &dyn GitRunner) -> Result<(), Error> {
let msg = format!("work-product transfer [{child_id}]");
git.run(worktree, &["commit", "-m", msg.as_str()])
.map_err(|source| Error::Git {
op: "transfer commit",
source,
})
}
fn decline(
worktree: &Path,
child_id: &str,
terminal_ref: &str,
git: &dyn GitRunner,
) -> Result<(), Error> {
let conflicted_ref = format!("{CONFLICTED_REF_PREFIX}{child_id}");
git.run(
worktree,
&["update-ref", conflicted_ref.as_str(), terminal_ref],
)
.map_err(|source| Error::Git {
op: "transfer decline update-ref",
source,
})
}
#[cfg(test)]
mod tests;