libnoa 0.1.1

AI-native distributed version control system with per-agent workspace isolation, JSONL append-only logs, snapshot-based history, and full git protocol compatibility
Documentation
use anyhow::Result;
use std::sync::Arc;

use crate::repo::Repository;

pub async fn run_push(remote_name: &str) -> Result<()> {
    let root = Repository::find(std::path::Path::new("."))?;
    let repo = Repository::open(&root)?;

    let remote = repo
        .config
        .get_remote(remote_name)
        .ok_or_else(|| anyhow::anyhow!("remote '{}' not found", remote_name))?
        .clone();

    let db = Arc::clone(&repo.db);
    drop(repo);
    crate::git::export_noa_to_git(&root, db).await?;

    let output = std::process::Command::new("git")
        .args(["push", &remote.url])
        .current_dir(&root)
        .output()?;

    if output.status.success() {
        if crate::git::export::detect_lfs_available(&root) {
            crate::git::export::lfs_push_all(&root, &remote.url);
        }
        println!("Pushed to {} ({})", remote_name, remote.url);
    } else {
        let stderr = String::from_utf8_lossy(&output.stderr);
        anyhow::bail!("git push failed: {}", stderr);
    }

    Ok(())
}

pub async fn run_pull(remote_name: &str) -> Result<()> {
    let root = Repository::find(std::path::Path::new("."))?;
    let repo = Repository::open(&root)?;

    let remote = repo
        .config
        .get_remote(remote_name)
        .ok_or_else(|| anyhow::anyhow!("remote '{}' not found", remote_name))?
        .clone();

    let output = std::process::Command::new("git")
        .args(["pull", &remote.url])
        .current_dir(&root)
        .output()?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        anyhow::bail!("git pull failed: {}", stderr);
    }

    if crate::git::export::detect_lfs_available(&root)
        && crate::git::export::has_lfs_tracking(&root)
    {
        crate::git::export::lfs_pull(&root);
    }

    let db = Arc::clone(&repo.db);
    let head_ws = repo.read_head()?;
    drop(repo);
    crate::git::import::import_git_to_noa(&root, db.clone()).await?;

    let ref_store = crate::refs::RedbRefStore::new(db.clone())?;
    let head_ref = crate::refs::RefStore::get(&ref_store, "HEAD")
        .await
        .ok()
        .flatten();
    if let Some(snap_id) = head_ref {
        let ws_mgr = crate::workspace::WorkspaceManager::new(db)?;
        ws_mgr.update_head(&head_ws, &snap_id).await.ok();
    }

    println!("Pulled from {} and re-imported into noa", remote_name);

    Ok(())
}

pub async fn run_fetch(remote_name: &str) -> Result<()> {
    let root = Repository::find(std::path::Path::new("."))?;
    let repo = Repository::open(&root)?;

    let remote = repo
        .config
        .get_remote(remote_name)
        .ok_or_else(|| anyhow::anyhow!("remote '{}' not found", remote_name))?
        .clone();

    let backend = crate::git::GitBackend::new();
    let refs = crate::remote::RemoteBackend::list_refs(&backend, &remote.url).await?;

    if refs.is_empty() {
        println!("No remote refs found.");
        return Ok(());
    }

    println!("Remote refs from {}:", remote_name);
    for r in &refs {
        println!(
            "  {} -> {}",
            r.name,
            &r.commit_hash[..12.min(r.commit_hash.len())]
        );
    }

    Ok(())
}

pub async fn run_clone(url: &str, path: &str) -> Result<()> {
    let target = std::path::PathBuf::from(path);
    let canonical = if target.exists() {
        target.canonicalize().unwrap_or(target)
    } else {
        target
    };

    println!("Cloning {} into {} ...", url, canonical.display());

    crate::git::clone_git_to_noa(url, &canonical).await?;

    println!("Cloned and imported into noa: {}", canonical.display());
    println!(".git/ and .noa/ coexist — git manages source, noa manages agent data.");
    Ok(())
}

pub async fn run_clone_svn(url: &str, path: &str) -> Result<()> {
    let target = std::path::PathBuf::from(path);
    std::fs::create_dir_all(&target)?;

    let svn_url = if url.ends_with("/trunk") || url.contains("/trunk") {
        url.to_string()
    } else {
        format!("{}/trunk", url.trim_end_matches('/'))
    };

    println!(
        "Exporting from SVN {} into {} ...",
        svn_url,
        target.display()
    );

    let export_output = std::process::Command::new("svn")
        .args(["export", "--force", &svn_url, &target.to_string_lossy()])
        .output()?;

    if !export_output.status.success() {
        let stderr = String::from_utf8_lossy(&export_output.stderr);
        anyhow::bail!("svn export failed: {}", stderr);
    }

    std::process::Command::new("git")
        .args(["init"])
        .current_dir(&target)
        .status()?;

    std::process::Command::new("git")
        .args(["add", "-A"])
        .current_dir(&target)
        .status()?;

    let svn_rev_output = std::process::Command::new("svn")
        .args(["info", "--show-item", "revision", &svn_url])
        .output()
        .ok();

    let rev_info = svn_rev_output
        .and_then(|o| String::from_utf8(o.stdout).ok())
        .map(|s| s.trim().to_string())
        .unwrap_or_else(|| "?".to_string());

    let commit_msg = format!("imported from SVN {}@r{}", svn_url, rev_info);

    std::process::Command::new("git")
        .args(["commit", "-m", &commit_msg])
        .current_dir(&target)
        .env("GIT_AUTHOR_NAME", "noa-svn-bridge")
        .env("GIT_AUTHOR_EMAIL", "noa@noa.local")
        .env("GIT_COMMITTER_NAME", "noa-svn-bridge")
        .env("GIT_COMMITTER_EMAIL", "noa@noa.local")
        .status()?;

    let db_path = target.join(".noa").join("noa.redb");
    std::fs::create_dir_all(target.join(".noa").join("agent-logs"))?;

    let db = std::sync::Arc::new(
        redb::Database::builder()
            .create(&db_path)
            .map_err(|e| anyhow::anyhow!("db create failed: {}", e))?,
    );

    {
        let txn = db
            .begin_write()
            .map_err(|e| anyhow::anyhow!("db write failed: {}", e))?;
        {
            let _ = txn.open_table(redb::TableDefinition::<&[u8], &[u8]>::new("blobs"));
            let _ = txn.open_table(redb::TableDefinition::<&[u8], &[u8]>::new("trees"));
            let _ = txn.open_table(redb::TableDefinition::<&str, &[u8]>::new("snapshots"));
            let _ = txn.open_table(redb::TableDefinition::<&str, &[u8]>::new("workspaces"));
            let _ = txn.open_table(redb::TableDefinition::<&str, &[u8]>::new("refs"));
        }
        txn.commit()
            .map_err(|e| anyhow::anyhow!("db commit failed: {}", e))?;
    }

    let config = crate::config::RepoConfig {
        name: "default".to_string(),
        remotes: vec![crate::config::RemoteConfig {
            name: "svn-origin".to_string(),
            url: url.to_string(),
            protocol: "svn".to_string(),
        }],
        noa_remote: None,
    };
    config.save_to_dir(&target.join(".noa"))?;
    std::fs::write(target.join(".noa").join("HEAD"), "default\n")?;

    crate::git::import::import_git_to_noa(&target, std::sync::Arc::clone(&db)).await?;

    let ref_store = crate::refs::RedbRefStore::new(std::sync::Arc::clone(&db))?;
    let head_ref = crate::refs::RefStore::get(&ref_store, "HEAD")
        .await
        .ok()
        .flatten();
    let head_snap_id =
        head_ref.unwrap_or_else(|| crate::snapshot::SnapshotId("noa_empty".to_string()));

    let ws_mgr = crate::workspace::WorkspaceManager::new(std::sync::Arc::clone(&db))?;
    let now = chrono::Utc::now().timestamp_micros() as u64;
    ws_mgr
        .create(&crate::workspace::Workspace {
            name: "default".to_string(),
            head: head_snap_id.clone(),
            base: head_snap_id.clone(),
            agent_id: None,
            last_seq: 0,
            created_at: now,
            updated_at: now,
        })
        .await
        .ok();

    let gitignore_path = target.join(".gitignore");
    if !gitignore_path.exists() {
        std::fs::write(&gitignore_path, "# Added by noa\n.noa/\n")?;
    } else {
        let content = std::fs::read_to_string(&gitignore_path)?;
        if !content
            .lines()
            .any(|l| l.trim() == ".noa/" || l.trim() == ".noa")
        {
            std::fs::write(&gitignore_path, format!("{}\n.noa/\n", content.trim_end()))?;
        }
    }

    println!(
        "SVN repository exported and imported into noa: {}",
        target.display()
    );
    println!("Note: This is a one-time import. Use 'svn export' + 'noa snapshot create' for incremental sync.");
    Ok(())
}