#![allow(dead_code)]
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use alkcall::core::Identity;
use alkgit::{
serve_duplex, serve_stateless_advertisement, serve_stateless_post, AccessAction,
AuthorizedRepo, BodyReader, GitPackGen, GitPackIngest, GitRefs, GitRegistry, Limits,
RepoRecord, Service, SubstrateError, Visibility,
};
#[cfg(feature = "sqlite")]
use alkgit_substrate::PackServing;
use alkgit_substrate::{AssemblyOptions, MaintenanceOpts, SubstrateAssembly};
use bytes::Bytes;
use futures::io::AsyncWrite as FuturesAsyncWrite;
use futures::stream;
use parking_lot::Mutex;
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::Semaphore;
use tokio::task::JoinHandle;
pub(crate) const DOOR_CONN_TIMEOUT: Duration = Duration::from_secs(120);
const RELAY_CHUNK_BYTES: usize = 16384;
pub const GIT_RUN_TIMEOUT: Duration = Duration::from_secs(90);
pub const DEFAULT_REPO_ID: &str = "owner/real-git";
pub fn writer_identity(id: &str) -> Identity {
Identity {
id: id.to_owned(),
scopes: Vec::new(),
resources: Default::default(),
}
}
pub fn public_granted_record(repo_id: &str, identity: &Identity) -> RepoRecord {
RepoRecord {
repo_id: repo_id.to_owned(),
visibility: Visibility::Public,
grants: BTreeMap::from([(
identity.id.clone(),
vec![AccessAction::Read, AccessAction::Write],
)]),
}
}
pub fn unique_root(tag: &str) -> PathBuf {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or_default();
let dir = std::env::temp_dir().join(format!(
"alkgit-substrate-real-git-{tag}-{}-{n}-{nanos}",
std::process::id()
));
std::fs::create_dir_all(&dir).expect("the harness root creates");
dir
}
pub struct Harness {
pub assembly: SubstrateAssembly,
pub record: RepoRecord,
pub identity: Identity,
pub root: PathBuf,
}
impl Drop for Harness {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.root);
}
}
impl Harness {
pub async fn mem(tag: &str, repo_id: &str) -> Harness {
Harness::mem_tuned(tag, repo_id, Default::default()).await
}
pub async fn mem_tuned(tag: &str, repo_id: &str, maintenance: MaintenanceOpts) -> Harness {
let root = unique_root(tag);
let options = AssemblyOptions {
maintenance,
..AssemblyOptions::new(root.join("work"), root.join("large"))
};
let assembly = SubstrateAssembly::mem(
Arc::new(alkstore_mem::MemStore::new()),
Arc::new(alkblobs::Store::mem_only()),
options,
)
.await
.unwrap_or_else(|error| panic!("the {tag} mem assembly composes: {error:?}"));
Harness {
assembly,
record: public_granted_record(repo_id, &writer_identity("u-writer")),
identity: writer_identity("u-writer"),
root,
}
.with_registered_repo()
.await
}
#[cfg(feature = "sqlite")]
pub async fn sqlite(tag: &str, repo_id: &str, pack_serving: PackServing) -> Harness {
let root = unique_root(tag);
let options = AssemblyOptions {
pack_serving,
..AssemblyOptions::new(root.join("work"), root.join("large"))
};
let assembly = SubstrateAssembly::sqlite(root.join("assembly.db"), options)
.await
.unwrap_or_else(|error| panic!("the {tag} sqlite assembly composes: {error:?}"));
Harness {
assembly,
record: public_granted_record(repo_id, &writer_identity("u-writer")),
identity: writer_identity("u-writer"),
root,
}
.with_registered_repo()
.await
}
async fn with_registered_repo(self) -> Harness {
self.assembly
.registry_store()
.put_repo(&self.record)
.await
.unwrap_or_else(|error| panic!("the harness's repo registers: {error:?}"));
self
}
pub fn git_url(&self, addr: std::net::SocketAddr) -> String {
format!("git://{addr}/{}", self.record.repo_id)
}
pub fn work_root(&self) -> PathBuf {
self.root.join("work")
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn http_url(&self, addr: std::net::SocketAddr) -> String {
format!("http://{addr}/{}", self.record.repo_id)
}
}
#[derive(Clone, Default)]
pub struct StreamCapture {
pub to_server: Arc<Mutex<Vec<u8>>>,
pub to_client: Arc<Mutex<Vec<u8>>>,
}
impl StreamCapture {
pub fn new() -> StreamCapture {
StreamCapture::default()
}
pub fn to_server(&self) -> Vec<u8> {
self.to_server.lock().clone()
}
pub fn to_client(&self) -> Vec<u8> {
self.to_client.lock().clone()
}
}
#[derive(Clone, Default)]
pub struct GateRendezvous {
pub held: Arc<tokio::sync::Notify>,
release_notify: Arc<tokio::sync::Notify>,
released: Arc<AtomicBool>,
}
impl GateRendezvous {
pub fn new() -> GateRendezvous {
GateRendezvous::default()
}
pub fn release(&self) {
self.released.store(true, AtomicOrdering::SeqCst);
self.release_notify.notify_one();
}
async fn wait_release(&self) {
while !self.released.load(AtomicOrdering::SeqCst) {
self.release_notify.notified().await;
}
}
pub fn is_released(&self) -> bool {
self.released.load(AtomicOrdering::SeqCst)
}
}
#[derive(Clone, Default)]
pub struct DoorOptions {
pub capture: Option<StreamCapture>,
pub gate: Option<GateRendezvous>,
}
pub struct DoorHandle {
pub addr: std::net::SocketAddr,
pub accept: JoinHandle<()>,
}
pub async fn spawn_daemon_door(harness: &Harness, options: DoorOptions) -> DoorHandle {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.unwrap_or_else(|error| panic!("the duplex door binds: {error}"));
let addr = listener.local_addr().expect("the door's local addr");
let registry: Arc<dyn GitRegistry> = harness.assembly.registry().clone();
let refs: Arc<dyn GitRefs> = harness.assembly.refs().clone();
let packgen: Arc<dyn GitPackGen> = harness.assembly.packgen().clone();
let ingest: Arc<dyn GitPackIngest> = harness.assembly.ingest().clone();
let identity = harness.identity.clone();
let limits = Limits::default();
let accept = tokio::spawn(async move {
loop {
let Ok((conn, _peer)) = listener.accept().await else {
break;
};
let registry = Arc::clone(®istry);
let refs = Arc::clone(&refs);
let packgen = Arc::clone(&packgen);
let ingest = Arc::clone(&ingest);
let identity = identity.clone();
let options = options.clone();
tokio::spawn(serve_daemon_conn(
conn, identity, registry, refs, packgen, ingest, limits, options,
));
}
});
DoorHandle { addr, accept }
}
#[allow(clippy::too_many_arguments)]
async fn serve_daemon_conn(
conn: TcpStream,
identity: Identity,
registry: Arc<dyn GitRegistry>,
refs: Arc<dyn GitRefs>,
packgen: Arc<dyn GitPackGen>,
ingest: Arc<dyn GitPackIngest>,
limits: Limits,
options: DoorOptions,
) {
let session = serve_daemon_conn_inner(
conn, identity, registry, refs, packgen, ingest, limits, options,
);
match tokio::time::timeout(DOOR_CONN_TIMEOUT, session).await {
Ok(Ok(())) => {}
Ok(Err(error)) => eprintln!("the duplex door session ended: {error:?}"),
Err(_elapsed) => eprintln!("the duplex door session exceeded the hygiene timeout"),
}
}
#[allow(clippy::too_many_arguments)]
async fn serve_daemon_conn_inner(
mut conn: TcpStream,
identity: Identity,
registry: Arc<dyn GitRegistry>,
refs: Arc<dyn GitRefs>,
packgen: Arc<dyn GitPackGen>,
ingest: Arc<dyn GitPackIngest>,
limits: Limits,
options: DoorOptions,
) -> Result<(), SubstrateError> {
let Some(frame) = read_daemon_frame(&mut conn).await else {
return Ok(());
};
let Some((service, repo_id)) = parse_daemon_frame(&frame) else {
return Ok(());
};
let action = if service.is_write() {
AccessAction::Write
} else {
AccessAction::Read
};
let Ok(record) = registry.resolve(&repo_id).await else {
return Ok(());
};
let Some(marker) = AuthorizedRepo::authorize(record, Some(&identity), action) else {
return Ok(());
};
let (pair_client, pair_server) = duplex(1 << 20);
let pipeline = Arc::new(Semaphore::new(limits.max_concurrent_blocking_tasks));
let (mut client_read, mut client_write) = tokio::io::split(conn);
let (mut server_read, mut server_write) = tokio::io::split(pair_client);
let up = relay_up(
&mut client_read,
&mut server_write,
options.capture.clone(),
options.gate.clone(),
);
let down = telemetry_copy(
&mut server_read,
&mut client_write,
options
.capture
.as_ref()
.map(|capture| capture.to_client.clone()),
);
let serve = serve_duplex(
&marker,
pair_server,
limits,
service,
refs,
packgen,
ingest,
false,
pipeline,
);
let (up, down, serve) = tokio::join!(up, down, serve);
down?;
up?;
serve
}
async fn read_daemon_frame(conn: &mut TcpStream) -> Option<Vec<u8>> {
let mut header = [0u8; 4];
tokio::time::timeout(DOOR_CONN_TIMEOUT, conn.read_exact(&mut header))
.await
.ok()?
.ok()?;
let length_text = std::str::from_utf8(&header).ok()?;
if !length_text.bytes().all(|byte| byte.is_ascii_hexdigit()) {
return None;
}
let len = usize::from_str_radix(length_text, 16).ok()?;
if !(4..=(1 << 16)).contains(&len) {
return None;
}
let mut payload = vec![0u8; len - 4];
tokio::time::timeout(DOOR_CONN_TIMEOUT, conn.read_exact(&mut payload))
.await
.ok()?
.ok()?;
Some(payload)
}
pub fn parse_daemon_frame(frame: &[u8]) -> Option<(Service, String)> {
let text = String::from_utf8(frame.to_vec()).ok()?;
let mut segments = text.split('\0');
let (service_text, repo_path) = segments.next()?.split_once(' ')?;
let service = service_text.parse::<Service>().ok()?;
let repo_id = repo_path.strip_prefix('/').unwrap_or(repo_path);
if repo_id.is_empty() {
return None;
}
let version_2 = segments.any(|segment| segment == "version=2");
if service == Service::UploadPack && !version_2 {
return None;
}
Some((service, repo_id.to_owned()))
}
async fn relay_up(
src: &mut (impl tokio::io::AsyncRead + Unpin),
dst: &mut (impl tokio::io::AsyncWrite + Unpin),
capture: Option<StreamCapture>,
gate: Option<GateRendezvous>,
) -> std::io::Result<()> {
let to_server = capture.as_ref().map(|capture| capture.to_server.clone());
let push_capture = |bytes: &[u8]| {
if let Some(to_server) = &to_server {
to_server.lock().extend_from_slice(bytes);
}
};
if let Some(gate) = &gate {
let mut staged = Vec::new();
let mut held = false;
while !held {
let mut header = [0u8; 4];
if src.read_exact(&mut header).await.is_err() {
return Ok(());
}
let Ok(len) =
usize::from_str_radix(std::str::from_utf8(&header).expect("hex header"), 16)
else {
return Ok(());
};
let mut payload = vec![0u8; len.saturating_sub(4)];
if len >= 4 && src.read_exact(&mut payload).await.is_err() {
return Ok(());
}
if len < 4 {
staged.extend_from_slice(&header);
held = true;
} else {
let chunk = [&header[..], &payload].concat();
push_capture(&chunk);
dst.write_all(&chunk).await?;
}
}
gate.held.notify_one();
gate.wait_release().await;
push_capture(&staged);
dst.write_all(&staged).await?;
}
let mut chunk = vec![0u8; RELAY_CHUNK_BYTES];
loop {
match src.read(&mut chunk).await {
Ok(0) => return Ok(()),
Ok(n) => {
push_capture(&chunk[..n]);
dst.write_all(&chunk[..n]).await?;
}
Err(error) => return Err(error),
}
}
}
async fn telemetry_copy(
src: &mut (impl tokio::io::AsyncRead + Unpin),
dst: &mut (impl tokio::io::AsyncWrite + Unpin),
capture: Option<Arc<Mutex<Vec<u8>>>>,
) -> std::io::Result<()> {
let mut chunk = vec![0u8; RELAY_CHUNK_BYTES];
loop {
match src.read(&mut chunk).await {
Ok(0) => return Ok(()),
Ok(n) => {
if let Some(capture) = &capture {
capture.lock().extend_from_slice(&chunk[..n]);
}
dst.write_all(&chunk[..n]).await?;
}
Err(error) => return Err(error),
}
}
}
pub async fn spawn_http_door(harness: &Harness) -> DoorHandle {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.unwrap_or_else(|error| panic!("the http door binds: {error}"));
let addr = listener.local_addr().expect("the door's local addr");
let registry: Arc<dyn GitRegistry> = harness.assembly.registry().clone();
let refs: Arc<dyn GitRefs> = harness.assembly.refs().clone();
let packgen: Arc<dyn GitPackGen> = harness.assembly.packgen().clone();
let ingest: Arc<dyn GitPackIngest> = harness.assembly.ingest().clone();
let limits = Limits::default();
let accept = tokio::spawn(async move {
loop {
let Ok((conn, _peer)) = listener.accept().await else {
break;
};
let registry = Arc::clone(®istry);
let refs = Arc::clone(&refs);
let packgen = Arc::clone(&packgen);
let ingest = Arc::clone(&ingest);
tokio::spawn(serve_http_conn(
conn, registry, refs, packgen, ingest, limits,
));
}
});
DoorHandle { addr, accept }
}
#[allow(clippy::too_many_arguments)]
async fn serve_http_conn(
mut conn: TcpStream,
registry: Arc<dyn GitRegistry>,
refs: Arc<dyn GitRefs>,
packgen: Arc<dyn GitPackGen>,
ingest: Arc<dyn GitPackIngest>,
limits: Limits,
) {
let session = serve_http_conn_inner(&mut conn, registry, refs, packgen, ingest, limits);
tokio::pin!(session);
match tokio::time::timeout(DOOR_CONN_TIMEOUT, &mut session).await {
Ok(Ok(())) | Ok(Err(())) | Err(_) => {}
}
}
async fn serve_http_conn_inner(
conn: &mut TcpStream,
registry: Arc<dyn GitRegistry>,
refs: Arc<dyn GitRefs>,
packgen: Arc<dyn GitPackGen>,
ingest: Arc<dyn GitPackIngest>,
limits: Limits,
) -> Result<(), ()> {
let mut buffer: Vec<u8> = Vec::new();
loop {
let Some(mut request) = read_http_request(conn, &mut buffer).await else {
return Ok(());
};
let Some(repo_id) = request.path.as_deref().and_then(repo_id_from_path) else {
return Ok(());
};
let Ok(record) = registry.resolve(&repo_id).await else {
return Ok(());
};
let Some(marker) = AuthorizedRepo::authorize(record, None, AccessAction::Read) else {
return Ok(());
};
match serve_http_request(
conn,
&mut request,
&marker,
&refs,
&packgen,
&ingest,
limits,
)
.await
{
Some(true) => {}
_ => return Ok(()),
}
}
}
async fn serve_http_request(
conn: &mut TcpStream,
request: &mut HttpRequest,
marker: &AuthorizedRepo,
refs: &Arc<dyn GitRefs>,
packgen: &Arc<dyn GitPackGen>,
ingest: &Arc<dyn GitPackIngest>,
limits: Limits,
) -> Option<bool> {
let path = request.path.clone()?;
if request.method == "GET" {
if !path.ends_with("/info/refs")
|| request.query.as_deref() != Some("service=git-upload-pack")
{
return None;
}
let sink = Sink::default();
serve_stateless_advertisement(
marker,
sink.clone(),
Service::UploadPack,
Arc::clone(refs),
&limits,
false,
)
.await
.ok()?;
respond(
conn,
"application/x-git-upload-pack-advertisement",
&sink.body(),
)
.await;
Some(true)
} else if request.method == "POST" {
if !path.ends_with("/git-upload-pack") {
return None;
}
let body = std::mem::take(&mut request.body);
let reader = BodyReader::new(stream::iter(vec![Ok::<Bytes, std::convert::Infallible>(
Bytes::from(body),
)]));
let sink = Sink::default();
serve_stateless_post(
marker,
reader,
sink.clone(),
limits,
Service::UploadPack,
Arc::clone(refs),
Arc::clone(packgen),
Arc::clone(ingest),
false,
Arc::new(Semaphore::new(limits.max_concurrent_blocking_tasks)),
)
.await
.ok()?;
respond(conn, "application/x-git-upload-pack-result", &sink.body()).await;
Some(true)
} else {
None
}
}
async fn respond(conn: &mut TcpStream, content_type: &str, body: &[u8]) {
conn.write_all(
format!(
"HTTP/1.1 200 OK\r\nContent-Type: {content_type}\r\nCache-Control: no-cache\r\nContent-Length: {}\r\n\r\n",
body.len()
)
.as_bytes(),
)
.await
.expect("the door writes the head");
conn.write_all(body)
.await
.expect("the door writes the body");
conn.flush().await.expect("the door flushes the body");
}
pub struct HttpRequest {
pub method: String,
pub path: Option<String>,
pub query: Option<String>,
pub body: Vec<u8>,
pub content_length: Option<u64>,
pub authorization: Option<String>,
pub range: Option<String>,
}
pub(crate) async fn read_http_request(
conn: &mut TcpStream,
buffer: &mut Vec<u8>,
) -> Option<HttpRequest> {
let mut chunk = [0u8; 4096];
let head_end = loop {
if let Some(position) = buffer.windows(4).position(|window| window == b"\r\n\r\n") {
break position;
}
let read = conn.read(&mut chunk).await.ok()?;
if read == 0 {
return None;
}
buffer.extend_from_slice(&chunk[..read]);
if buffer.len() > 64 * 1024 {
return None;
}
};
let head_text = String::from_utf8(buffer[..head_end].to_vec()).ok()?;
let mut lines = head_text.split("\r\n");
let request_line = lines.next()?;
let mut parts = request_line.split(' ');
let method = parts.next()?.to_owned();
let path = parts.next()?;
let (path, query) = match path.split_once('?') {
Some((path, query)) => (path.to_owned(), Some(query.to_owned())),
None => (path.to_owned(), None),
};
let mut content_length: Option<u64> = None;
let mut expects_continue = false;
let mut authorization: Option<String> = None;
let mut range: Option<String> = None;
for header in lines {
let Some((name, value)) = header.split_once(':') else {
continue;
};
if name.trim().eq_ignore_ascii_case("content-length") {
content_length = value.trim().parse().ok();
} else if name.trim().eq_ignore_ascii_case("expect")
&& value.trim().eq_ignore_ascii_case("100-continue")
{
expects_continue = true;
} else if name.trim().eq_ignore_ascii_case("authorization") {
authorization = Some(value.trim().to_owned());
} else if name.trim().eq_ignore_ascii_case("range") {
range = Some(value.trim().to_owned());
}
}
let body_start = head_end + 4;
let body_len = content_length.unwrap_or(0) as usize;
if expects_continue && body_len > 0 {
conn.write_all(b"HTTP/1.1 100 Continue\r\n\r\n")
.await
.expect("the interim continue writes");
}
while buffer.len() < body_start + body_len {
let read = conn.read(&mut chunk).await.ok()?;
if read == 0 {
return None;
}
buffer.extend_from_slice(&chunk[..read]);
}
let body = buffer[body_start..body_start + body_len].to_vec();
buffer.drain(..body_start + body_len);
Some(HttpRequest {
method,
path: Some(path),
query,
body,
content_length,
authorization,
range,
})
}
fn repo_id_from_path(path: &str) -> Option<String> {
let trimmed = path.strip_prefix('/')?;
let rest = trimmed
.strip_suffix("/info/refs")
.or_else(|| trimmed.strip_suffix("/git-upload-pack"))?;
if rest.is_empty() {
return None;
}
Some(rest.to_owned())
}
#[derive(Clone, Default)]
pub struct Sink(Arc<Mutex<Vec<u8>>>);
impl Sink {
fn body(&self) -> Vec<u8> {
self.0.lock().clone()
}
}
impl FuturesAsyncWrite for Sink {
fn poll_write(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<std::io::Result<usize>> {
self.0.lock().extend_from_slice(buf);
std::task::Poll::Ready(Ok(buf.len()))
}
fn poll_flush(
self: std::pin::Pin<&mut Self>,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
std::task::Poll::Ready(Ok(()))
}
fn poll_close(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<std::io::Result<()>> {
self.poll_flush(cx)
}
}
fn git_env() -> Vec<(String, String)> {
vec![
("GIT_CONFIG_NOSYSTEM".to_owned(), "1".to_owned()),
("GIT_CONFIG_GLOBAL".to_owned(), "/dev/null".to_owned()),
("GIT_TERMINAL_PROMPT".to_owned(), "0".to_owned()),
]
}
pub fn git_command() -> Option<tokio::process::Command> {
git_on_path()?;
let mut command = tokio::process::Command::new("git");
command
.env_clear()
.envs(git_env())
.env("PATH", std::env::var("PATH").unwrap_or_default());
Some(command)
}
fn git_on_path() -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
for dir in std::env::split_paths(&path) {
let candidate = dir.join("git");
if !candidate.is_file() {
continue;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let executable = std::fs::metadata(&candidate)
.map(|meta| meta.permissions().mode() & 0o111 != 0)
.unwrap_or(false);
if !executable {
continue;
}
}
return Some(candidate);
}
None
}
pub struct GitRun {
pub code: i32,
pub stdout: String,
pub stderr: String,
}
impl GitRun {
pub fn expect_ok(self) -> GitRun {
assert_eq!(
self.code, 0,
"the git invocation failed: {}{}",
self.stdout, self.stderr
);
self
}
pub fn ok(&self) -> bool {
self.code == 0
}
pub fn stderr(&self) -> &str {
&self.stderr
}
}
pub async fn git_run(cwd: Option<&Path>, args: &[&str]) -> Option<GitRun> {
if git_command().is_none() {
eprintln!("skipping row: no `git` binary on PATH");
return None;
}
let mut command = git_command().expect("the git binary the row resolved");
if let Some(cwd) = cwd {
command.current_dir(cwd);
}
command.args(args).stdin(Stdio::null());
let output = match tokio::time::timeout(GIT_RUN_TIMEOUT, command.output()).await {
Ok(output) => output.expect("the git invocation spawns"),
Err(_elapsed) => panic!("the git invocation exceeded {GIT_RUN_TIMEOUT:?}: {args:?}"),
};
Some(GitRun {
code: output.status.code().unwrap_or(1),
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
})
}
pub async fn git(cwd: Option<&Path>, args: &[&str]) -> Option<GitRun> {
git_run(cwd, args).await.map(GitRun::expect_ok)
}
pub async fn git_with(
cwd: Option<&Path>,
args: &[&str],
extra_env: &[(&str, &str)],
) -> Option<GitRun> {
if git_command().is_none() {
eprintln!("skipping row: no `git` binary on PATH");
return None;
}
let mut command = git_command().expect("the git binary the row resolved");
if let Some(cwd) = cwd {
command.current_dir(cwd);
}
for (name, value) in extra_env {
command.env(name, value);
}
command.args(args).stdin(Stdio::null());
let output = match tokio::time::timeout(GIT_RUN_TIMEOUT, command.output()).await {
Ok(output) => output.expect("the git invocation spawns"),
Err(_elapsed) => panic!("the git invocation exceeded {GIT_RUN_TIMEOUT:?}: {args:?}"),
};
let run = GitRun {
code: output.status.code().unwrap_or(1),
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
};
Some(run.expect_ok())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Pkt {
Line(Vec<u8>),
Control,
}
pub fn pkt_stream(body: &[u8]) -> Vec<Pkt> {
let mut lines = Vec::new();
let mut cursor = body;
while cursor.len() >= 4 {
let len = usize::from_str_radix(std::str::from_utf8(&cursor[..4]).expect("hex header"), 16)
.expect("hex length");
if len < 4 {
lines.push(Pkt::Control);
cursor = &cursor[4..];
continue;
}
if cursor.len() < len {
lines.push(Pkt::Line(cursor[4..].to_vec()));
break;
}
lines.push(Pkt::Line(cursor[4..len].to_vec()));
cursor = &cursor[len..];
}
lines
}
pub fn sideband_report_lines(to_client: &[u8]) -> Vec<String> {
let mut payload = Vec::new();
for pkt in pkt_stream(to_client) {
if let Pkt::Line(line) = &pkt {
let (band, rest) = line.split_first().expect("band chunk nonempty");
if *band == 1 {
payload.extend_from_slice(rest);
}
}
}
let mut inner = pkt_stream(&payload);
let last = inner.pop().expect("the report's framing is non-empty");
assert!(
matches!(last, Pkt::Control),
"the report's inner flush terminates it"
);
inner
.into_iter()
.map(|pkt| match pkt {
Pkt::Line(line) => String::from_utf8_lossy(&line)
.trim_end_matches('\n')
.to_owned(),
Pkt::Control => String::new(),
})
.collect()
}
pub struct SentPack {
pub object_count: u32,
pub entry_count: usize,
pub full_oids: Vec<gix_hash::ObjectId>,
pub deltas: Vec<(gix_hash::ObjectId, DeltaKind)>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DeltaKind {
InPack,
ThinBase,
}
pub fn extract_and_walk_client_pack(to_server: &[u8]) -> Option<SentPack> {
let magic = to_server.windows(4).position(|window| window == b"PACK")?;
parse_client_pack(&to_server[magic..])
}
pub fn parse_client_pack(pack: &[u8]) -> Option<SentPack> {
let header: [u8; 12] = pack[..12].try_into().expect("the pack header present");
let (version, object_count) =
gix_pack::data::header::decode(&header).expect("the captured pack's header decodes");
assert_eq!(version, gix_pack::data::Version::V2, "the client's pack");
let entries = gix_pack::data::input::BytesToEntriesIter::new_from_header(
std::io::Cursor::new(pack),
gix_pack::data::input::Mode::Verify,
gix_pack::data::input::EntryDataMode::Keep,
gix_hash::Kind::Sha1,
)
.expect("the captured pack's stream starts");
let mut sent = SentPack {
object_count,
entry_count: 0,
full_oids: Vec::new(),
deltas: Vec::new(),
};
let mut full_oids_by_offset: BTreeMap<u64, gix_hash::ObjectId> = BTreeMap::new();
let mut pending_ofs_bases: Vec<(u64, usize)> = Vec::new();
for entry in entries {
let entry = entry.ok()?;
sent.entry_count += 1;
match &entry.header {
gix_pack::data::entry::Header::Blob
| gix_pack::data::entry::Header::Tree
| gix_pack::data::entry::Header::Commit
| gix_pack::data::entry::Header::Tag => {
let kind = match entry.header {
gix_pack::data::entry::Header::Blob => gix_object::Kind::Blob,
gix_pack::data::entry::Header::Tree => gix_object::Kind::Tree,
gix_pack::data::entry::Header::Commit => gix_object::Kind::Commit,
_ => gix_object::Kind::Tag,
};
let body = inflate_entry_body(&entry)?;
let oid = gix_object::compute_hash(gix_hash::Kind::Sha1, kind, &body)
.expect("the captured entry hashes");
full_oids_by_offset.insert(entry.pack_offset, oid);
sent.full_oids.push(oid);
}
gix_pack::data::entry::Header::OfsDelta { base_distance } => {
let base_offset = entry.pack_offset.checked_sub(*base_distance)?;
pending_ofs_bases.push((base_offset, sent.deltas.len()));
sent.deltas.push((
gix_hash::ObjectId::null(gix_hash::Kind::Sha1),
DeltaKind::InPack,
));
}
gix_pack::data::entry::Header::RefDelta { base_id } => {
sent.deltas.push((*base_id, DeltaKind::ThinBase));
}
}
}
for (base_offset, delta_index) in pending_ofs_bases {
let base = full_oids_by_offset
.get(&base_offset)
.copied()
.expect("the in-pack ofs-delta base resolves within the pack");
sent.deltas[delta_index] = (base, DeltaKind::InPack);
}
Some(sent)
}
fn inflate_entry_body(entry: &gix_pack::data::input::Entry) -> Option<Vec<u8>> {
let compressed = entry.compressed.as_ref()?;
let mut out: Vec<u8> = Vec::new();
let mut decompressor = gix_zlib::Decompress::new();
let mut reader = std::io::BufReader::new(std::io::Cursor::new(compressed.as_slice()));
let mut chunk = vec![0u8; 65536];
loop {
let written =
gix_zlib::stream::inflate::read(&mut reader, &mut decompressor, &mut chunk).ok()?;
out.extend_from_slice(&chunk[..written]);
if written < chunk.len() {
break;
}
}
Some(out)
}
pub fn rev_list_oids(stdout: &str) -> BTreeSet<gix_hash::ObjectId> {
stdout
.lines()
.filter_map(|line| line.split(' ').next())
.filter_map(|hex| gix_hash::ObjectId::from_hex(hex.as_bytes()).ok())
.collect()
}