use crate::commands::intent;
use crate::core::merge::{find_merge_base, is_ancestor, merge_trees, MergeStrategy};
use crate::core::{
find_repo_root, get_current_branch, read_head, write_ref, Commit, Object, ObjectHash,
};
use crate::errors::LitError;
use crate::response::ConvergeResponse;
use crate::storage::ObjectStore;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConvergeStrategy {
Auto,
Rebase,
Squash,
Accumulate,
}
impl std::str::FromStr for ConvergeStrategy {
type Err = LitError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"auto" => Ok(Self::Auto),
"rebase" => Ok(Self::Rebase),
"squash" => Ok(Self::Squash),
"accumulate" => Ok(Self::Accumulate),
_ => Err(LitError::general(format!(
"Unknown converge strategy: '{}' (expected auto, rebase, squash, accumulate)",
s
))),
}
}
}
pub fn execute(
intent_id: String,
strategy: Option<String>,
verify: bool,
dry_run: bool,
target_branch: Option<String>,
) -> Result<ConvergeResponse, LitError> {
let repo_root = find_repo_root()?;
let store = ObjectStore::new(&repo_root);
let intent_data = intent::load_intent(&repo_root, &intent_id)?;
if intent_data.status != intent::IntentStatus::Active {
return Err(LitError::general(format!(
"Intent {} is {} — only active intents can be converged",
intent_id, intent_data.status
)));
}
if intent_data.commits.is_empty() {
return Err(LitError::general(format!(
"Intent {} has no commits to converge",
intent_id
)));
}
let strat = match strategy {
Some(ref s) => s.parse::<ConvergeStrategy>()?,
None => ConvergeStrategy::Auto,
};
if verify {
for hash in &intent_data.commits {
let obj_hash = ObjectHash::from_hex(hash.clone());
match store.read(&obj_hash) {
Ok(Object::Commit(_)) => {}
Ok(_) => {
return Err(LitError::general(format!(
"Verify failed: {} is not a commit object",
hash
)));
}
Err(e) => {
return Err(LitError::general(format!(
"Verify failed: cannot read commit {}: {}",
hash, e
)));
}
}
}
}
if strat == ConvergeStrategy::Accumulate && !intent_data.children.is_empty() {
for child_id in &intent_data.children {
let child = intent::load_intent(&repo_root, child_id)?;
if child.status != intent::IntentStatus::Converged {
return Err(LitError::general(format!(
"Child intent {} is {} — all children must be converged before accumulate",
child_id, child.status
)));
}
}
}
let head_hash_str = read_head(&repo_root)?;
let head_hash = ObjectHash::from_hex(head_hash_str);
let current_branch = target_branch
.unwrap_or_else(|| get_current_branch(&repo_root).unwrap_or_else(|_| "main".to_string()));
let tip_hash_str = intent_data.commits.last().unwrap().clone();
let tip_hash = ObjectHash::from_hex(tip_hash_str.clone());
if dry_run {
let can_ff = is_ancestor(&store, &head_hash, &tip_hash).unwrap_or(false);
return Ok(ConvergeResponse {
converged: false,
strategy: format!("{:?}", strat).to_lowercase(),
intent_id: intent_id.clone(),
intent_title: intent_data.title.clone(),
commit_hash: None,
commits_converged: intent_data.commits.len(),
fast_forward: can_ff,
message: format!(
"Dry-run: would converge {} commit(s) from '{}' via {} (ff={})",
intent_data.commits.len(),
intent_data.title,
format!("{:?}", strat).to_lowercase(),
can_ff,
),
details: Some(serde_json::json!({
"intent": intent_data,
"dry_run": true,
"fast_forward_possible": can_ff,
})),
});
}
let (merge_hash, was_ff) = match strat {
ConvergeStrategy::Auto | ConvergeStrategy::Rebase => {
if is_ancestor(&store, &head_hash, &tip_hash)? {
write_ref(
&repo_root,
&format!("heads/{}", current_branch),
tip_hash.as_str(),
)?;
(tip_hash_str.clone(), true)
} else {
let merge_base = find_merge_base(&store, &head_hash, &tip_hash)?;
let ours_tree = get_commit_tree(&store, &head_hash)?;
let theirs_tree = get_commit_tree(&store, &tip_hash)?;
let base_tree = match &merge_base {
Some(bh) => Some(get_commit_tree(&store, bh)?),
None => None,
};
let result = merge_trees(
&store,
base_tree.as_ref(),
&ours_tree,
&theirs_tree,
MergeStrategy::Recursive,
)?;
if result.has_conflicts {
return Err(LitError::general(format!(
"Converge of '{}' has merge conflicts — resolve manually then retry",
intent_data.title
)));
}
let tree_hash = result
.tree
.ok_or_else(|| LitError::general("Merge produced no tree"))?;
let author = intent_data.agent.clone();
let commit = Commit::new(
tree_hash,
vec![head_hash.clone(), tip_hash.clone()],
author,
format!("Converge intent '{}' ({})", intent_data.title, intent_id),
);
let obj = Object::Commit(commit);
let hash = store.write(&obj)?;
write_ref(
&repo_root,
&format!("heads/{}", current_branch),
hash.as_str(),
)?;
(hash.to_string(), false)
}
}
ConvergeStrategy::Squash => {
let ours_tree = get_commit_tree(&store, &head_hash)?;
let theirs_tree = get_commit_tree(&store, &tip_hash)?;
let merge_base = find_merge_base(&store, &head_hash, &tip_hash)?;
let base_tree = match &merge_base {
Some(bh) => Some(get_commit_tree(&store, bh)?),
None => None,
};
let result = merge_trees(
&store,
base_tree.as_ref(),
&ours_tree,
&theirs_tree,
MergeStrategy::Recursive,
)?;
if result.has_conflicts {
return Err(LitError::general(format!(
"Squash converge of '{}' has conflicts — resolve manually",
intent_data.title
)));
}
let tree_hash = result
.tree
.ok_or_else(|| LitError::general("Squash produced no tree"))?;
let message = format!(
"Squash converge: {} ({} commit(s))\n\nIntent: {}\nAgent: {}",
intent_data.title,
intent_data.commits.len(),
intent_id,
intent_data.agent,
);
let commit = Commit::new(
tree_hash,
vec![head_hash.clone()],
intent_data.agent.clone(),
message,
);
let obj = Object::Commit(commit);
let hash = store.write(&obj)?;
write_ref(
&repo_root,
&format!("heads/{}", current_branch),
hash.as_str(),
)?;
(hash.to_string(), false)
}
ConvergeStrategy::Accumulate => {
if intent_data.children.is_empty() {
intent::mark_converged(&repo_root, &intent_id)?;
return Ok(ConvergeResponse {
converged: true,
strategy: "accumulate".into(),
intent_id: intent_id.clone(),
intent_title: intent_data.title.clone(),
commit_hash: None,
commits_converged: intent_data.commits.len(),
fast_forward: false,
message: format!(
"Intent '{}' queued for parent convergence",
intent_data.title
),
details: None,
});
}
if is_ancestor(&store, &head_hash, &tip_hash)? {
write_ref(
&repo_root,
&format!("heads/{}", current_branch),
tip_hash.as_str(),
)?;
(tip_hash_str.clone(), true)
} else {
let merge_base = find_merge_base(&store, &head_hash, &tip_hash)?;
let ours_tree = get_commit_tree(&store, &head_hash)?;
let theirs_tree = get_commit_tree(&store, &tip_hash)?;
let base_tree = match &merge_base {
Some(bh) => Some(get_commit_tree(&store, bh)?),
None => None,
};
let result = merge_trees(
&store,
base_tree.as_ref(),
&ours_tree,
&theirs_tree,
MergeStrategy::Recursive,
)?;
if result.has_conflicts {
return Err(LitError::general(format!(
"Accumulate converge of '{}' has conflicts",
intent_data.title
)));
}
let tree_hash = result
.tree
.ok_or_else(|| LitError::general("Accumulate merge produced no tree"))?;
let commit = Commit::new(
tree_hash,
vec![head_hash, tip_hash],
intent_data.agent.clone(),
format!(
"Accumulate converge: {} ({} child intents)",
intent_data.title,
intent_data.children.len()
),
);
let obj = Object::Commit(commit);
let hash = store.write(&obj)?;
write_ref(
&repo_root,
&format!("heads/{}", current_branch),
hash.as_str(),
)?;
(hash.to_string(), false)
}
}
};
let converged_intent = intent::mark_converged(&repo_root, &intent_id)?;
Ok(ConvergeResponse {
converged: true,
strategy: format!("{:?}", strat).to_lowercase(),
intent_id: intent_id.clone(),
intent_title: converged_intent.title.clone(),
commit_hash: Some(merge_hash),
commits_converged: converged_intent.commits.len(),
fast_forward: was_ff,
message: format!(
"Converged intent '{}' — {} commit(s) via {} strategy",
converged_intent.title,
converged_intent.commits.len(),
format!("{:?}", strat).to_lowercase(),
),
details: Some(serde_json::json!({
"intent": converged_intent,
})),
})
}
fn get_commit_tree(
store: &ObjectStore,
commit_hash: &ObjectHash,
) -> Result<crate::core::Tree, LitError> {
let commit = match store.read(commit_hash)? {
Object::Commit(c) => c,
_ => {
return Err(LitError::general(format!(
"Expected commit: {}",
commit_hash
)))
}
};
match store.read(&commit.tree)? {
Object::Tree(t) => Ok(t),
_ => Err(LitError::general(format!("Expected tree: {}", commit.tree))),
}
}