feat: basic packfile retrieval
All checks were successful
CI checks / Clippy (push) Successful in 50s
CI checks / Format (push) Successful in 26s

This commit is contained in:
Patrick MARIE 2025-02-11 22:18:32 +01:00
parent ae3c1b23af
commit a0cbb8dcda
Signed by: mycroft
GPG Key ID: BB519E5CD8E7BFA7
4 changed files with 1611 additions and 4 deletions

1433
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,8 @@ clap = { version = "4.5.27", features = ["derive", "string"] }
flate2 = "1.0.35" flate2 = "1.0.35"
hex = "0.4.3" hex = "0.4.3"
nom = "8.0.0" nom = "8.0.0"
reqwest = "0.12.12"
sha1 = "0.10.6" sha1 = "0.10.6"
thiserror = "2.0.11" thiserror = "2.0.11"
tokio = { version = "1.43.0", features = ["full"] }
walkdir = "2.5.0" walkdir = "2.5.0"

167
src/http.rs Normal file
View File

@ -0,0 +1,167 @@
use std::io::Write;
use anyhow::{Error, Result};
use nom::AsBytes;
use reqwest::Client;
pub async fn clone(repo: &str) -> Result<(), Error> {
let (size, refs) = get_refs(repo).await?;
println!("Refs:");
for (sha1, name) in refs.iter() {
println!("{} {}", sha1, name);
}
println!("Downloaded file size: {}", size);
Ok(())
}
pub fn parse_refs(input: &[u8]) -> Result<Vec<(String, String)>> {
let mut refs = Vec::new();
let mut index: usize = 0;
loop {
if index >= input.len() {
break;
}
// pick the next 4 bytes and convert to u32 from hex
let mut bytes = [0; 4];
bytes.copy_from_slice(&input[index..index + 4]);
let hex_str = std::str::from_utf8(&bytes)?;
let res = usize::from_str_radix(hex_str, 16)?;
if res == 0 {
index += 4;
continue;
}
if input[index + 4] == b'#' {
index += res;
continue;
}
let mut sha1_bytes = [0; 40];
sha1_bytes.copy_from_slice(&input[index + 4..index + 44]);
let idx_0 = input[index + 45..index + res - 1]
.iter()
.position(|&x| x == 0);
let sha1 = std::str::from_utf8(&sha1_bytes)?;
let name = if let Some(idx_0) = idx_0 {
std::str::from_utf8(&input[index + 45..index + 45 + idx_0])?
} else {
std::str::from_utf8(&input[index + 45..index + res - 1])?
};
refs.push((name.to_string(), sha1.to_string()));
index += res;
}
Ok(refs)
}
pub async fn get_refs(repo_url: &str) -> Result<(usize, Vec<(String, String)>), Error> {
let info_refs_url = format!("{}/info/refs?service=git-upload-pack", repo_url);
let client = Client::new();
let response = client
.get(&info_refs_url)
.header("User-Agent", "git/2.30.0")
.send()
.await?;
response.error_for_status_ref()?;
let content = response.bytes().await?;
let refs = parse_refs(&content)?;
get_packfile(repo_url, refs).await
}
pub fn packet_line(data: &str) -> Vec<u8> {
let length = format!("{:04x}", data.len() + 4);
let mut line = Vec::new();
line.extend_from_slice(length.as_bytes());
line.extend_from_slice(data.as_bytes());
line
}
pub async fn get_packfile(
repo_url: &str,
refs: Vec<(String, String)>,
) -> Result<(usize, Vec<(String, String)>), Error> {
let upload_pack_url = format!("{}/git-upload-pack", repo_url);
let mut payload: Vec<u8> = Vec::new();
payload.extend(packet_line("command=fetch").as_slice());
payload.extend(packet_line("agent=git/2.30.0").as_slice());
payload.extend(packet_line("object-format=sha1").as_slice());
payload.extend("0001".as_bytes());
payload.extend(packet_line("ofs-delta").as_slice());
payload.extend(packet_line("no-progress").as_slice());
for (_, sha1) in refs.iter() {
let want = format!("want {}\n", sha1);
payload.extend(packet_line(want.as_str()).as_slice());
}
payload.extend("0000".as_bytes());
payload.extend(packet_line("done").as_slice());
let client = Client::new();
let response = client
.post(&upload_pack_url)
.header("User-Agent", "git/2.30.0")
.header("Content-Type", "application/x-git-upload-pack-request")
.header("Accept-Encoding", "deflate")
.header("Accept", "application/x-git-upload-pack-result")
.header("Git-Protocol", "version=2")
.body(payload)
.send()
.await?;
response.error_for_status_ref()?;
let content = response.bytes().await?;
decode_git_response(content.as_bytes())?;
Ok((content.len(), refs))
}
fn decode_git_response(content: &[u8]) -> Result<(), Error> {
let mut cursor = 0;
let mut pack_data = Vec::new();
while cursor < content.len() {
let length_str = std::str::from_utf8(&content[cursor..cursor + 4])?;
cursor += 4;
let length = usize::from_str_radix(length_str, 16)?;
if length == 0 {
break;
}
let payload = &content[cursor..cursor + length - 4];
cursor += length - 4;
let side_band = payload[0];
let data = &payload[1..];
if side_band == 1 {
pack_data.extend(data);
} else if side_band == 2 {
println!("Progress: {}", std::str::from_utf8(data)?);
} else if side_band == 3 {
println!("Error: {}", std::str::from_utf8(data)?);
}
}
if !pack_data.is_empty() {
let mut packfile = std::fs::File::create("downloaded.pack")?;
packfile.write_all(&pack_data)?;
println!("Packfile saved as 'downloaded.pack'");
}
Ok(())
}

View File

@ -8,6 +8,7 @@ use clap::Subcommand;
mod commit; mod commit;
mod error; mod error;
mod http;
mod index; mod index;
mod kind; mod kind;
mod log; mod log;
@ -16,6 +17,7 @@ mod pack;
mod repository; mod repository;
mod tree; mod tree;
use crate::http::clone;
use crate::repository::Repository; use crate::repository::Repository;
#[derive(Parser)] #[derive(Parser)]
@ -83,9 +85,14 @@ enum Command {
/// The object to hash /// The object to hash
file: PathBuf, file: PathBuf,
}, },
Clone {
/// The repository to clone
repo: String,
},
} }
fn main() -> Result<(), Error> { #[tokio::main]
async fn main() -> Result<(), Error> {
let cli = Cli::parse(); let cli = Cli::parse();
let mut repo = Repository::new()?; let mut repo = Repository::new()?;
@ -147,6 +154,10 @@ fn main() -> Result<(), Error> {
Ok(hash) => println!("{}", hex::encode(hash)), Ok(hash) => println!("{}", hex::encode(hash)),
Err(e) => eprintln!("Failed to hash object: {}", e), Err(e) => eprintln!("Failed to hash object: {}", e),
}, },
Command::Clone { repo } => match clone(&repo).await {
Ok(_) => (),
Err(e) => eprintln!("Failed to clone: {}", e),
},
} }
Ok(()) Ok(())