libnoa 0.3.2

AI-native distributed version control
Documentation
use anyhow::Result;

use crate::{
    log::AgentLog,
    merge::{extract_conflicts, ConflictResolution},
    object::ObjectStore,
    repo::Repository,
    snapshot::{content_addressed_snapshot_id_with_ts, SnapshotStore},
};

pub async fn run_create(repo: &Repository, name: &str, agent: Option<&str>) -> Result<()> {
    let ws_mgr = repo.workspace_manager()?;

    let base_snapshot = match ws_mgr.get(&repo.read_head()?).await? {
        Some(ws) => ws.head.clone(),
        None => crate::snapshot::empty_snapshot_id(),
    };

    let now = crate::now_micros();
    let ws = crate::workspace::Workspace {
        name: name.to_string(),
        head: base_snapshot.clone(),
        base: base_snapshot.clone(),
        agent_id: agent.map(std::string::ToString::to_string),
        last_seq: 0,
        created_at: now,
        updated_at: now,
    };
    ws_mgr.create(&ws).await?;

    let log = repo.agent_log(name)?;
    log.append(&crate::log::LogEntry {
        seq: 1,
        op: crate::log::OpType::Snapshot,
        path: None,
        blob_id: None,
        from_path: None,
        resolved_conflict_ours_id: None,
        resolved_conflict_theirs_id: None,
        snapshot_id: Some(base_snapshot.0.clone()),
        ts: now,
        message: Some(format!("workspace {name} created")),
    })
    .await?;

    println!("Created workspace '{name}' (base: {base_snapshot})");
    Ok(())
}

pub async fn run_switch(repo: &Repository, name: &str) -> Result<()> {
    let ws_mgr = repo.workspace_manager()?;
    ws_mgr
        .get(name)
        .await?
        .ok_or_else(|| anyhow::anyhow!("workspace '{name}' not found"))?;

    let prev = repo.read_head()?;
    repo.write_orig_head(&prev)?;
    repo.write_head(name)?;

    println!("Switched to workspace '{name}'");
    Ok(())
}

pub async fn run_list(repo: &Repository) -> Result<()> {
    let ws_mgr = repo.workspace_manager()?;
    let list = ws_mgr.list().await?;
    let current = repo.read_head()?;

    if list.is_empty() {
        println!("No workspaces found.");
        return Ok(());
    }

    for ws in &list {
        let marker = if ws.name == current { "*" } else { " " };
        println!(
            "{} {:<20} head: {} base: {}",
            marker, ws.name, ws.head, ws.base
        );
    }
    Ok(())
}

pub async fn run_delete(repo: &Repository, name: &str) -> Result<()> {
    let current = repo.read_head()?;
    if name == current {
        anyhow::bail!("cannot delete the active workspace '{name}'");
    }

    let ws_mgr = repo.workspace_manager()?;
    let existed = ws_mgr.delete(name).await?;
    if !existed {
        anyhow::bail!("workspace '{name}' not found");
    }

    println!("Deleted workspace '{name}'");
    Ok(())
}

pub async fn run_merge(repo: &Repository, from: &str, strategy: &str) -> Result<()> {
    let resolution = match strategy {
        "ours" => ConflictResolution::Ours,
        "theirs" => ConflictResolution::Theirs,
        _ => anyhow::bail!("unknown strategy '{strategy}', expected 'ours' or 'theirs'"),
    };

    let ws_mgr = repo.workspace_manager()?;
    let current = repo.read_head()?;

    let from_ws = ws_mgr
        .get(from)
        .await?
        .ok_or_else(|| anyhow::anyhow!("workspace '{from}' not found"))?;
    let cur_ws = ws_mgr
        .get(&current)
        .await?
        .ok_or_else(|| anyhow::anyhow!("workspace '{current}' not found"))?;

    let snap_store = repo.snapshot_store()?;
    let obj_store = repo.object_store()?;

    let empty_tree = crate::object::TreeEntries(vec![]);

    let base_tree = if cur_ws.base.is_empty() {
        empty_tree.clone()
    } else {
        let base_snap = snap_store.get(&cur_ws.base).await?;
        obj_store
            .get_tree(&crate::object::TreeId(base_snap.tree_hash))
            .await?
    };
    let ours_tree = if cur_ws.head.is_empty() {
        empty_tree.clone()
    } else {
        let ours_snap = snap_store.get(&cur_ws.head).await?;
        obj_store
            .get_tree(&crate::object::TreeId(ours_snap.tree_hash))
            .await?
    };
    let theirs_tree = if from_ws.head.is_empty() {
        empty_tree
    } else {
        let their_snap = snap_store.get(&from_ws.head).await?;
        obj_store
            .get_tree(&crate::object::TreeId(their_snap.tree_hash))
            .await?
    };

    let result = crate::merge::merge_trees_recursive(
        base_tree,
        ours_tree,
        theirs_tree,
        obj_store.clone(),
        &resolution,
    )
    .await?;

    let conflicts = extract_conflicts(&result.output);
    if !conflicts.is_empty() {
        println!("Conflicts detected:");
        for c in &conflicts {
            println!("  CONFLICT: {}", c.path);
        }
        println!(
            "{} conflict(s) found. Resolving with --strategy={}.",
            conflicts.len(),
            strategy
        );
    }

    let resolved_tree = result.into_tree_entries(&resolution);

    let new_tree_id = obj_store.put_tree(&resolved_tree).await?;

    let author = "noa".to_string();
    let message = format!("merge {from} into {current}");
    let now = crate::now_micros();
    let merge_snapshot = crate::snapshot::Snapshot {
        id: content_addressed_snapshot_id_with_ts(
            &new_tree_id.0,
            &[cur_ws.head.clone(), from_ws.head.clone()],
            &current,
            &author,
            &message,
            now,
        ),
        tree_hash: new_tree_id.0,
        parents: vec![cur_ws.head.clone(), from_ws.head.clone()],
        workspace: current.clone(),
        author,
        timestamp: now,
        message,
    };

    snap_store.store(&merge_snapshot).await?;

    let log = repo.agent_log(&current)?;
    let now = crate::now_micros();
    let new_seq = log
        .append(&crate::log::LogEntry {
            seq: 0,
            op: crate::log::OpType::Merge,
            path: None,
            blob_id: None,
            from_path: None,
            resolved_conflict_ours_id: if conflicts.is_empty() {
                None
            } else {
                Some(
                    conflicts
                        .iter()
                        .filter_map(|c| c.ours_id.as_deref())
                        .collect::<Vec<_>>()
                        .join(","),
                )
            },
            resolved_conflict_theirs_id: if conflicts.is_empty() {
                None
            } else {
                Some(
                    conflicts
                        .iter()
                        .filter_map(|c| c.theirs_id.as_deref())
                        .collect::<Vec<_>>()
                        .join(","),
                )
            },
            snapshot_id: Some(merge_snapshot.id.0.clone()),
            ts: now,
            message: Some(format!("merge {from} into {current}")),
        })
        .await?;

    ws_mgr
        .update_head_seq_and_base(&current, &merge_snapshot.id, new_seq, &from_ws.head)
        .await?;

    if conflicts.is_empty() {
        println!("Merged {} into {} -> {}", from, current, merge_snapshot.id);
    } else {
        println!(
            "Merged {} into {} -> {} ({} conflict(s) auto-resolved with {})",
            from,
            current,
            merge_snapshot.id,
            conflicts.len(),
            strategy
        );
    }
    Ok(())
}