chadtmiller.com/knotless
Git hosted in your atproto account, on any stock PDS, fast enough for agents.
git clone knotless://chadtmiller.com/knotlessneeds the knotless git helper. set it up
//! git smart HTTP, read only, by `git upload-pack --stateless-rpc`.
use crate::{App, AppError, repo_name};
use anyhow::Context;
use axum::body::{Body, Bytes};
use axum::extract::{Path, Query, State};
use axum::http::{HeaderMap, StatusCode, header};
use axum::response::Response;
use knotless::git::Git;
use serde::Deserialize;
use std::io::Read;
use std::process::Stdio;
use std::sync::Arc;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
use tokio_util::io::ReaderStream;
#[derive(Deserialize)]
pub struct Service {
service: Option<String>,
}
fn protocol(headers: &HeaderMap) -> String {
headers
.get("git-protocol")
.and_then(|v| v.to_str().ok())
.unwrap_or_default()
.to_string()
}
fn command(git: &Git, protocol: &str, advertise: bool) -> Command {
let mut cmd = Command::new("git");
cmd.arg("upload-pack").arg("--stateless-rpc");
if advertise {
cmd.arg("--http-backend-info-refs");
}
cmd.arg(&git.dir)
.env("GIT_PROTOCOL", protocol)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.kill_on_drop(true);
cmd
}
fn pkt(line: &str) -> String {
format!("{:04x}{line}", line.len() + 4)
}
pub async fn info_refs(
State(app): State<Arc<App>>,
Path((authority, repo)): Path<(String, String)>,
Query(q): Query<Service>,
headers: HeaderMap,
) -> Result<Response, AppError> {
if q.service.as_deref() != Some("git-upload-pack") {
return Err(AppError(
StatusCode::FORBIDDEN,
"read only: push with knotless:// instead".into(),
));
}
let git = app.mirror(&authority, repo_name(&repo)).await?;
let protocol = protocol(&headers);
let out = command(&git, &protocol, true)
.output()
.await
.context("running git upload-pack")
.map_err(AppError::internal)?;
let mut body = Vec::new();
if !protocol.contains("version=2") {
body.extend(pkt("# service=git-upload-pack\n").as_bytes());
body.extend(b"0000");
}
body.extend(out.stdout);
Ok(Response::builder()
.header(
header::CONTENT_TYPE,
"application/x-git-upload-pack-advertisement",
)
.header(header::CACHE_CONTROL, "no-cache")
.body(Body::from(body))
.expect("static headers are valid"))
}
pub async fn upload_pack(
State(app): State<Arc<App>>,
Path((authority, repo)): Path<(String, String)>,
headers: HeaderMap,
body: Bytes,
) -> Result<Response, AppError> {
let input = if headers
.get(header::CONTENT_ENCODING)
.is_some_and(|v| v == "gzip")
{
let mut out = Vec::new();
flate2::read::GzDecoder::new(&body[..])
.read_to_end(&mut out)
.map_err(|e| AppError(StatusCode::BAD_REQUEST, format!("bad gzip body: {e}")))?;
out
} else {
body.to_vec()
};
let git = app.mirror(&authority, repo_name(&repo)).await?;
let mut child = command(&git, &protocol(&headers), false)
.spawn()
.context("running git upload-pack")
.map_err(AppError::internal)?;
let mut stdin = child.stdin.take().expect("stdin is piped");
tokio::spawn(async move {
let _ = stdin.write_all(&input).await;
});
let stdout = child.stdout.take().expect("stdout is piped");
// The child is reaped when the stream ends; kill_on_drop covers a
// client that hangs up first.
tokio::spawn(async move {
let _ = child.wait().await;
});
Ok(Response::builder()
.header(header::CONTENT_TYPE, "application/x-git-upload-pack-result")
.header(header::CACHE_CONTROL, "no-cache")
.body(Body::from_stream(ReaderStream::new(stdout)))
.expect("static headers are valid"))
}