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
pub mod export;
pub mod import;
mod translate;

use async_trait::async_trait;
use std::process::Command;

pub use export::{
    clone_git_to_noa, detect_lfs_available, export_noa_to_git, has_lfs_tracking, lfs_install,
    lfs_pull, lfs_push_all,
};
pub use import::{import_git_to_noa, is_lfs_pointer};
pub use translate::GitTranslator;

use crate::{
    error::{NoaError, Result},
    remote::{FetchResult, FetchSpec, PushResult, PushSpec, RemoteBackend, RemoteRef},
};

pub struct GitBackend;

impl Default for GitBackend {
    fn default() -> Self {
        Self::new()
    }
}

impl GitBackend {
    pub fn new() -> Self {
        GitBackend
    }
}

#[async_trait]
impl RemoteBackend for GitBackend {
    fn protocol(&self) -> &str {
        "git"
    }

    async fn push(&self, url: &str, _: &[PushSpec]) -> Result<PushResult> {
        let output = Command::new("git")
            .args(["push", url])
            .output()
            .map_err(|e| NoaError::Remote(format!("git push failed: {}", e)))?;

        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
        let stderr = String::from_utf8_lossy(&output.stderr).to_string();

        if output.status.success() {
            Ok(PushResult {
                ok: true,
                message: format!("{}\n{}", stdout, stderr).trim().to_string(),
            })
        } else {
            Ok(PushResult {
                ok: false,
                message: stderr,
            })
        }
    }

    async fn fetch(&self, url: &str, _: &[FetchSpec]) -> Result<FetchResult> {
        let output = Command::new("git")
            .args(["fetch", url])
            .output()
            .map_err(|e| NoaError::Remote(format!("git fetch failed: {}", e)))?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(NoaError::Remote(format!("git fetch failed: {}", stderr)));
        }

        self.list_refs(url).await.map(|refs| FetchResult { refs })
    }

    async fn list_refs(&self, url: &str) -> Result<Vec<RemoteRef>> {
        let output = Command::new("git")
            .args(["ls-remote", "--refs", url])
            .output()
            .map_err(|e| NoaError::Remote(format!("git ls-remote failed: {}", e)))?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(NoaError::Remote(format!(
                "git ls-remote failed: {}",
                stderr
            )));
        }

        let stdout = String::from_utf8_lossy(&output.stdout);
        let mut refs = Vec::new();
        for line in stdout.lines() {
            let parts: Vec<&str> = line.splitn(2, '\t').collect();
            if parts.len() == 2 {
                refs.push(RemoteRef {
                    commit_hash: parts[0].to_string(),
                    name: parts[1].to_string(),
                });
            }
        }
        Ok(refs)
    }
}