git-remote-igit 0.1.0

Git remote helper for the native igit:// InfiniGit transport
//! Git remote-helper gateway for local InfiniGit development.
//!
//! Git invokes this executable for `igit://<host>/<username>/<repo>` remotes.
//! It resolves the username through the directory canister, then hands the smart protocol to
//! Git's battle-tested upload-pack/receive-pack implementation. Production
//! deployments replace the local repository path with the ICP canister-backed
//! object gateway; the wire protocol seen by Git remains identical.

use std::env;
use std::io::{self, BufRead, Write};
use std::path::{Component, Path, PathBuf};
use std::process::{Command, Stdio};

fn fail(message: impl AsRef<str>) -> ! {
    eprintln!("infinigit: {}", message.as_ref());
    std::process::exit(1)
}

fn valid_atom(value: &str) -> bool {
    !value.is_empty()
        && value.len() <= 128
        && value
            .bytes()
            .all(|c| c.is_ascii_alphanumeric() || matches!(c, b'-' | b'_' | b'.'))
        && !value.starts_with('.')
        && !value.ends_with('.')
}

fn parse_remote(url: &str) -> Result<(Option<&str>, &str, &str), String> {
    let value = url
        .strip_prefix("igit://")
        .or_else(|| url.strip_prefix("igit::"))
        .ok_or_else(|| "remote must use igit://<host>/<username>/<repository>".to_string())?;
    let parts = value.split('/').collect::<Vec<_>>();
    let (host, namespace, repository) = match parts.as_slice() {
        [namespace, repository] => (None, *namespace, *repository),
        [host, namespace, repository] if valid_atom(host) => (Some(*host), *namespace, *repository),
        _ => return Err("remote must contain a host, username, and repository".into()),
    };
    if namespace == "2vxsx-fae" || !valid_atom(namespace) {
        return Err("a valid InfiniGit username is required".into());
    }
    if !valid_atom(repository) {
        return Err("invalid repository name".into());
    }
    Ok((host, namespace, repository))
}

fn git_config(key: &str) -> Option<String> {
    let output = Command::new("git")
        .args(["config", "--global", "--get", key])
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }
    let value = String::from_utf8(output.stdout).ok()?.trim().to_owned();
    (!value.is_empty()).then_some(value)
}

fn configured(env_key: &str, git_key: &str) -> Option<String> {
    env::var(env_key).ok().or_else(|| git_config(git_key))
}

fn validate_host(host: Option<&str>, configured_host: Option<&str>) -> Result<(), String> {
    match (host, configured_host) {
        (Some(host), Some(configured)) if host != configured => {
            Err(format!("unknown InfiniGit host: {host}"))
        }
        _ => Ok(()),
    }
}

fn candid_principal_field(response: &str, field: &str) -> Option<String> {
    let marker = format!("{field} = principal \"");
    response
        .split(&marker)
        .nth(1)
        .and_then(|rest| rest.split('"').next())
        .filter(|value| !value.is_empty())
        .map(str::to_owned)
}

fn candid_text_field(response: &str, field: &str) -> Option<String> {
    let marker = format!("{field} = \"");
    response
        .split(&marker)
        .nth(1)
        .and_then(|rest| rest.split('"').next())
        .filter(|value| valid_atom(value))
        .map(str::to_owned)
}

fn resolve_repository(
    namespace: &str,
    repository: &str,
    directory: &str,
) -> (String, String, String) {
    let icp = configured("INFINIGIT_ICP_BIN", "infinigit.icp-bin").unwrap_or_else(|| "icp".into());
    let argument = format!("(\"{namespace}\", \"{repository}\")");
    let mut command = Command::new(icp);
    command.args([
        "canister",
        "call",
        directory,
        "resolve_repository",
        &argument,
    ]);
    if let Some(identity) = configured("INFINIGIT_IDENTITY", "infinigit.identity") {
        command.args(["--identity", &identity]);
    }
    if let Some(network) = configured("INFINIGIT_NETWORK", "infinigit.network") {
        command.args(["--network", &network]);
        if let Some(root_key) = configured("INFINIGIT_ROOT_KEY", "infinigit.root-key") {
            command.args(["--root-key", &root_key]);
        }
    } else if let Some(project_root) =
        configured("INFINIGIT_PROJECT_ROOT", "infinigit.project-root")
    {
        command.args(["--project-root-override", &project_root]);
    }
    let output = command
        .output()
        .unwrap_or_else(|e| fail(format!("cannot query the InfiniGit directory: {e}")));
    if !output.status.success() {
        fail("repository not found")
    }
    let response =
        String::from_utf8(output.stdout).unwrap_or_else(|_| fail("invalid directory response"));
    if response.contains("err =") {
        fail("repository not found")
    }
    let owner = candid_principal_field(&response, "owner")
        .unwrap_or_else(|| fail("invalid directory owner"));
    let shard = candid_principal_field(&response, "shard")
        .unwrap_or_else(|| fail("invalid directory shard"));
    let storage_id = candid_text_field(&response, "storage_id")
        .unwrap_or_else(|| fail("invalid directory storage ID"));
    (owner, shard, storage_id)
}

fn repository_path(root: &Path, principal: &str, repository: &str) -> Result<PathBuf, String> {
    if root
        .components()
        .any(|part| matches!(part, Component::ParentDir))
    {
        return Err("INFINIGIT_DATA_DIR cannot contain '..'".into());
    }
    Ok(root.join(principal).join(format!("{repository}.git")))
}

fn pack_bridge() -> PathBuf {
    env::var_os("INFINIGIT_PACK_BIN")
        .map(PathBuf::from)
        .or_else(|| git_config("infinigit.pack-bin").map(PathBuf::from))
        .unwrap_or_else(|| {
            env::current_exe()
                .unwrap_or_else(|e| fail(e.to_string()))
                .with_file_name("infinigit-pack")
        })
}

fn run_pack(
    action: &str,
    canister: &str,
    owner: &str,
    repository: &str,
    repo: &Path,
    project_root: Option<&Path>,
) -> bool {
    let mut command = Command::new(pack_bridge());
    command
        .args([action, canister, owner, repository])
        .arg(repo);
    if let Some(root) = project_root {
        command
            .current_dir(root)
            .env("INFINIGIT_PROJECT_ROOT", root);
    }
    if let Some(icp) = configured("INFINIGIT_ICP_BIN", "infinigit.icp-bin") {
        command.env("INFINIGIT_ICP_BIN", icp);
    }
    if let Some(network) = configured("INFINIGIT_NETWORK", "infinigit.network") {
        command.env("INFINIGIT_NETWORK", network);
    }
    if let Some(root_key) = configured("INFINIGIT_ROOT_KEY", "infinigit.root-key") {
        command.env("INFINIGIT_ROOT_KEY", root_key);
    }
    if let Some(identity) = configured("INFINIGIT_IDENTITY", "infinigit.identity") {
        command.env("INFINIGIT_IDENTITY", identity);
    }
    command
        .status()
        .unwrap_or_else(|e| fail(format!("cannot start pack bridge: {e}")))
        .success()
}

fn sync_from_canister(
    canister: &str,
    owner: &str,
    repository: &str,
    repo: &Path,
    project_root: Option<&Path>,
) {
    if !run_pack(
        "materialize",
        canister,
        owner,
        repository,
        repo,
        project_root,
    ) {
        fail("repository not found")
    }
}

fn sync_to_canister(
    canister: &str,
    owner: &str,
    repository: &str,
    repo: &Path,
    project_root: Option<&Path>,
) {
    if !run_pack("upload", canister, owner, repository, repo, project_root) {
        fail("canister rejected push")
    }
}

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() != 3 {
        fail("usage: git-remote-igit <remote-name> <igit-url>");
    }
    let (host, namespace, repository) = parse_remote(&args[2]).unwrap_or_else(|e| fail(e));
    let configured_host = git_config("infinigit.host");
    validate_host(host, configured_host.as_deref()).unwrap_or_else(|e| fail(e));
    let direct_data = env::var_os("INFINIGIT_DATA_DIR").is_some();
    let directory = env::var("INFINIGIT_DIRECTORY_CANISTER_ID")
        .ok()
        .or_else(|| {
            (!direct_data)
                .then(|| git_config("infinigit.directory-canister"))
                .flatten()
        });
    let direct_canister = env::var("INFINIGIT_CANISTER").ok();
    let (owner, canister, storage_id) = if let Some(directory) = directory.as_deref() {
        let (owner, shard, storage_id) = resolve_repository(namespace, repository, directory);
        (owner, Some(shard), storage_id)
    } else {
        (namespace.to_owned(), direct_canister, repository.to_owned())
    };
    let caller = env::var("INFINIGIT_PRINCIPAL").ok();
    let project_root =
        configured("INFINIGIT_PROJECT_ROOT", "infinigit.project-root").map(PathBuf::from);
    let root = env::var_os("INFINIGIT_DATA_DIR")
        .map(PathBuf::from)
        .or_else(|| git_config("infinigit.data-dir").map(PathBuf::from))
        .unwrap_or_else(|| PathBuf::from(".infinigit/repositories"));
    let repo = repository_path(&root, namespace, repository).unwrap_or_else(|e| fail(e));
    if let Some(canister) = canister.as_deref() {
        eprintln!(
            "infinigit: synchronizing {} via canister {}",
            repo.display(),
            canister
        );
        sync_from_canister(
            canister,
            &owner,
            &storage_id,
            &repo,
            project_root.as_deref(),
        );
    }
    if !repo.join("HEAD").is_file() {
        fail(format!("repository does not exist: {}", repo.display()));
    }
    let public = Command::new("git")
        .args([
            "--git-dir",
            repo.to_str().unwrap(),
            "config",
            "--bool",
            "infinigit.public",
        ])
        .output()
        .map(|output| {
            output.status.success() && String::from_utf8_lossy(&output.stdout).trim() == "true"
        })
        .unwrap_or(false);
    let owner_authenticated = caller.as_deref() == Some(owner.as_str());

    let stdin = io::stdin();
    let mut lines = stdin.lock().lines();
    while let Some(Ok(line)) = lines.next() {
        match line.as_str() {
            "capabilities" => {
                println!("connect");
                println!();
            }
            "connect git-upload-pack" | "connect git-receive-pack" => {
                let reading = line == "connect git-upload-pack";
                if canister.is_none() && !(owner_authenticated || (reading && public)) {
                    fail("repository not found");
                }
                println!();
                io::stdout().flush().unwrap_or_else(|e| fail(e.to_string()));
                let service = line.strip_prefix("connect ").unwrap();
                let status = Command::new(service)
                    .arg(&repo)
                    .stdin(Stdio::inherit())
                    .stdout(Stdio::inherit())
                    .stderr(Stdio::inherit())
                    .status()
                    .unwrap_or_else(|e| fail(format!("cannot start {service}: {e}")));
                if status.success() && !reading {
                    if let Some(canister) = canister.as_deref() {
                        sync_to_canister(
                            canister,
                            &owner,
                            &storage_id,
                            &repo,
                            project_root.as_deref(),
                        );
                    }
                }
                std::process::exit(status.code().unwrap_or(1));
            }
            "" => return,
            other => fail(format!("unsupported remote-helper command: {other}")),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_supported_urls_and_rejects_anonymous_identity() {
        assert_eq!(
            parse_remote("igit://aaaaa-aa/demo").unwrap(),
            (None, "aaaaa-aa", "demo")
        );
        assert_eq!(
            parse_remote("igit://localhost/alice/demo").unwrap(),
            (Some("localhost"), "alice", "demo")
        );
        assert!(parse_remote("https://aaaaa-aa/demo").is_err());
        assert!(parse_remote("igit://2vxsx-fae/demo").is_err());
        assert!(parse_remote("igit://aaaaa-aa/../escape").is_err());
        assert!(parse_remote("infinigit://aaaaa-aa/demo").is_err());
    }

    #[test]
    fn parses_directory_principals_and_rejects_missing_fields() {
        let response = "owner = principal \"aaaaa-aa\"; shard = principal \"rrkah-fqaaa-aaaaa-aaaaq-cai\"; storage_id = \"igit-r-7\"";
        assert_eq!(
            candid_principal_field(response, "owner").as_deref(),
            Some("aaaaa-aa")
        );
        assert_eq!(
            candid_principal_field(response, "shard").as_deref(),
            Some("rrkah-fqaaa-aaaaa-aaaaq-cai")
        );
        assert_eq!(candid_principal_field(response, "missing"), None);
        assert_eq!(
            candid_text_field(response, "storage_id").as_deref(),
            Some("igit-r-7")
        );
        assert_eq!(
            candid_text_field("storage_id = \"bad/value\"", "storage_id"),
            None
        );
    }

    #[test]
    fn host_validation_accepts_configured_and_legacy_urls_but_rejects_other_installations() {
        assert!(validate_host(Some("localhost"), Some("localhost")).is_ok());
        assert!(validate_host(None, Some("localhost")).is_ok());
        assert!(validate_host(Some("git.example"), None).is_ok());
        assert_eq!(
            validate_host(Some("other.example"), Some("git.example")),
            Err("unknown InfiniGit host: other.example".into())
        );
    }
}