tape-git-remote 0.1.1

git remote helper that stores repositories on Tapedrive: git clone/pull/push over tape://
Documentation
use std::io::{BufRead, Lines, Write};

use anyhow::{Context, Result, anyhow, bail};

use crate::{git::Repository, store::Store};

const URL_SCHEME: &str = "tape://";

pub async fn run_from_env() -> Result<()> {
    let store = Store::open(&bucket_from_args()?)?;
    let repository = Repository::current()?;
    let stdin = std::io::stdin();
    let mut lines = stdin.lock().lines();
    let mut out = std::io::stdout();

    while let Some(line) = lines.next() {
        let line = line.context("read command")?;
        let command = line.trim_end();
        if command.is_empty() {
            break;
        }
        if command == "capabilities" {
            capabilities(&mut out)?;
        } else if command == "list" || command == "list for-push" {
            list(&store, &mut out).await?;
        } else if command.starts_with("option ") {
            writeln!(out, "unsupported")?;
            out.flush()?;
        } else if command.starts_with("fetch ") {
            drain_batch(&mut lines)?;
            crate::fetch::fetch(&store, &repository).await?;
            writeln!(out)?;
            out.flush()?;
        } else if let Some(first) = command.strip_prefix("push ") {
            let specs = collect_batch(&mut lines, first, "push ")?;
            crate::push::push(&store, &repository, &specs, &mut out).await?;
        } else {
            bail!("unsupported command from git: `{command}`");
        }
    }
    Ok(())
}

fn bucket_from_args() -> Result<String> {
    let url = std::env::args()
        .nth(2)
        .or_else(|| std::env::args().nth(1))
        .ok_or_else(|| anyhow!("usage: git-remote-tape <remote> {URL_SCHEME}<bucket>"))?;
    let bucket = url
        .strip_prefix(URL_SCHEME)
        .ok_or_else(|| anyhow!("`{url}` is not a {URL_SCHEME} url"))?
        .trim_end_matches('/');
    if bucket.is_empty() {
        bail!("tape URL is missing its address");
    }
    Ok(bucket.to_string())
}

fn capabilities(out: &mut impl Write) -> Result<()> {
    writeln!(out, "fetch")?;
    writeln!(out, "push")?;
    writeln!(out, "option")?;
    writeln!(out)?;
    out.flush()?;
    Ok(())
}

fn drain_batch(lines: &mut Lines<impl BufRead>) -> Result<()> {
    for line in lines {
        if line.context("read batch")?.trim_end().is_empty() {
            break;
        }
    }
    Ok(())
}

fn collect_batch(
    lines: &mut Lines<impl BufRead>,
    first: &str,
    prefix: &str,
) -> Result<Vec<String>> {
    let mut batch = vec![first.to_string()];
    for line in lines {
        let line = line.context("read batch")?;
        let line = line.trim_end();
        if line.is_empty() {
            break;
        }
        batch.push(line.strip_prefix(prefix).unwrap_or(line).to_string());
    }
    Ok(batch)
}

async fn list(store: &Store, out: &mut impl Write) -> Result<()> {
    if let Some((index, _)) = store.read_index().await? {
        for (name, object_id) in &index.refs {
            writeln!(out, "{object_id} {name}")?;
        }
        if let Some(head) = index.head.as_deref() {
            if index.refs.contains_key(head) {
                writeln!(out, "@{head} HEAD")?;
            }
        }
    }
    writeln!(out)?;
    out.flush()?;
    Ok(())
}