use anyhow::Result;
use crate::{
log::AgentLog,
merge::{extract_conflicts, ConflictResolution},
object::{EntryKind, ObjectStore, TreeEntries, TreeEntry, TreeId},
repo::Repository,
snapshot::{content_addressed_snapshot_id_with_ts, SnapshotStore},
};
fn resolve_nested_entry<'a, O: ObjectStore + 'a>(
obj_store: &'a O,
entries: &'a mut [TreeEntry],
source: &'a TreeEntries,
path_parts: &'a [&'a str],
filter: &'a str,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<bool>> + 'a>> {
Box::pin(async move {
if path_parts.is_empty() {
return Ok(false);
}
let first = path_parts[0];
let rest = &path_parts[1..];
let is_leaf = rest.is_empty();
for target_entry in entries.iter_mut() {
if target_entry.name != first {
continue;
}
if is_leaf {
if target_entry.kind != EntryKind::Blob {
anyhow::bail!("'{}' is not a file", filter);
}
let source_id = source
.0
.iter()
.find(|e| e.name == first)
.map(|e| e.id.clone())
.ok_or_else(|| anyhow::anyhow!("path '{}' not found in source tree", filter))?;
target_entry.id = source_id;
return Ok(true);
}
if target_entry.kind != EntryKind::Tree {
anyhow::bail!("'{}' is not a directory", first);
}
let source_sub = obj_store
.get_tree(&TreeId(
source
.0
.iter()
.find(|e| e.name == first)
.map(|e| e.id.clone())
.unwrap_or_default(),
))
.await?;
let mut target_sub = obj_store.get_tree(&TreeId(target_entry.id.clone())).await?;
let found =
resolve_nested_entry(obj_store, &mut target_sub.0, &source_sub, rest, filter)
.await?;
if found {
let new_sub_id = obj_store.put_tree(&target_sub).await?;
target_entry.id = new_sub_id.0;
return Ok(true);
}
return Ok(false);
}
anyhow::bail!("path component '{}' not found in merge tree", first);
})
}
pub async fn run_resolve(
repo: &Repository,
strategy: &str,
path_filter: Option<&str>,
) -> Result<()> {
let resolution = match strategy {
"ours" => ConflictResolution::Ours,
"theirs" => ConflictResolution::Theirs,
_ => anyhow::bail!("unknown strategy '{strategy}', expected 'ours' or 'theirs'"),
};
let current = repo.read_head()?;
let log = repo.agent_log(¤t)?;
let entries = log.read_all().await?;
let merge_entries: Vec<_> = entries
.iter()
.filter(|e| e.op == crate::log::OpType::Merge)
.collect();
if merge_entries.is_empty() {
println!("No pending conflicts found.");
return Ok(());
}
let latest_merge = merge_entries.last().ok_or_else(|| {
anyhow::anyhow!("internal error: merge_entries unexpectedly empty after non-empty check")
})?;
let snap_store = repo.snapshot_store()?;
let obj_store = repo.object_store()?;
let ws_mgr = repo.workspace_manager()?;
let current_ws = ws_mgr
.get(¤t)
.await?
.ok_or_else(|| anyhow::anyhow!("workspace '{current}' not found"))?;
let merge_snap_id = latest_merge
.snapshot_id
.as_deref()
.ok_or_else(|| anyhow::anyhow!("merge entry has no snapshot id"))?;
let merge_snap = snap_store
.get(&crate::snapshot::SnapshotId(merge_snap_id.to_string()))
.await?;
let merge_tree = obj_store
.get_tree(&crate::object::TreeId(merge_snap.tree_hash.clone()))
.await?;
let mut resolved_entries = merge_tree.0.clone();
if let Some(filter) = path_filter {
let filter_parts: Vec<&str> = filter.split('/').collect();
if filter_parts.len() > 1 {
let source_tree = if merge_snap.parents.is_empty() {
crate::object::TreeEntries(vec![])
} else {
let pid = match resolution {
ConflictResolution::Ours => merge_snap.parents.first(),
ConflictResolution::Theirs => merge_snap.parents.get(1),
};
match pid {
Some(pid) => {
let source_snap = snap_store.get(pid).await?;
obj_store
.get_tree(&crate::object::TreeId(source_snap.tree_hash.clone()))
.await?
}
None => crate::object::TreeEntries(vec![]),
}
};
resolve_nested_entry(
&obj_store,
&mut resolved_entries,
&source_tree,
&filter_parts,
filter,
)
.await?;
} else {
let parent_snap_id = merge_snap.parents.first();
let parent_tree = if let Some(pid) = parent_snap_id {
let parent_snap = snap_store.get(pid).await?;
obj_store
.get_tree(&crate::object::TreeId(parent_snap.tree_hash.clone()))
.await?
} else {
crate::object::TreeEntries(vec![])
};
let theirs_snap_id = merge_snap.parents.get(1);
let theirs_tree = if let Some(tid) = theirs_snap_id {
let their_snap = snap_store.get(tid).await?;
obj_store
.get_tree(&crate::object::TreeId(their_snap.tree_hash.clone()))
.await?
} else {
parent_tree.clone()
};
let result = crate::merge::three_way_merge(&parent_tree, &merge_tree, &theirs_tree)?;
let merge_conflicts = extract_conflicts(&result.output);
let target_conflict = merge_conflicts
.iter()
.find(|c| c.path == filter)
.ok_or_else(|| anyhow::anyhow!("no conflict found for path '{filter}'"))?;
let resolved_blob_id = match resolution {
ConflictResolution::Ours => target_conflict.ours_id.as_deref(),
ConflictResolution::Theirs => target_conflict.theirs_id.as_deref(),
}
.ok_or_else(|| anyhow::anyhow!("no blob id available for resolution"))?;
for entry in &mut resolved_entries {
if entry.name == filter {
entry.id = resolved_blob_id.to_string();
}
}
}
} else if merge_snap.parents.len() >= 2 {
let base_snap = snap_store.get(¤t_ws.base).await?;
let base_tree = obj_store
.get_tree(&crate::object::TreeId(base_snap.tree_hash.clone()))
.await?;
let theirs_snap = snap_store
.get(
merge_snap
.parents
.get(1)
.ok_or_else(|| anyhow::anyhow!("merge snapshot has fewer than 2 parents"))?,
)
.await?;
let theirs_tree = obj_store
.get_tree(&crate::object::TreeId(theirs_snap.tree_hash.clone()))
.await?;
let result = crate::merge::three_way_merge(&base_tree, &merge_tree, &theirs_tree)?;
resolved_entries = result.into_tree_entries(&resolution).0;
}
let resolved_tree = crate::object::TreeEntries(resolved_entries);
let new_tree_id = obj_store.put_tree(&resolved_tree).await?;
let ours_id = latest_merge
.resolved_conflict_ours_id
.as_deref()
.unwrap_or("none");
let theirs_id = latest_merge
.resolved_conflict_theirs_id
.as_deref()
.unwrap_or("none");
let now = crate::now_micros();
let resolved_path = path_filter
.map(std::string::ToString::to_string)
.or_else(|| latest_merge.path.clone())
.unwrap_or_default();
let author = "noa-resolve".to_string();
let message = format!("resolve conflicts with strategy '{strategy}'");
let new_snap_id = content_addressed_snapshot_id_with_ts(
&new_tree_id.0,
std::slice::from_ref(&merge_snap.id),
¤t,
&author,
&message,
now,
);
let resolved_snapshot = crate::snapshot::Snapshot {
id: new_snap_id.clone(),
tree_hash: new_tree_id.0,
parents: vec![merge_snap.id.clone()],
workspace: current.clone(),
author,
timestamp: now,
message,
};
snap_store.store(&resolved_snapshot).await?;
ws_mgr.update_head(¤t, &resolved_snapshot.id).await?;
log.append(&crate::log::LogEntry {
seq: 0,
op: crate::log::OpType::Resolve,
path: Some(resolved_path.clone()),
blob_id: None,
from_path: None,
resolved_conflict_ours_id: Some(ours_id.to_string()),
resolved_conflict_theirs_id: Some(theirs_id.to_string()),
snapshot_id: Some(new_snap_id.0),
ts: now,
message: Some(format!("resolve {resolved_path} with {strategy}")),
})
.await?;
println!(
"Resolved conflict on '{resolved_path}' with strategy '{strategy}' (ours={ours_id}, theirs={theirs_id})"
);
println!("Created resolution snapshot: {}", resolved_snapshot.id);
println!("Run 'noa snapshot create' to commit additional changes.");
Ok(())
}