feat: basic packfile retrieval
This commit is contained in:
parent
ae3c1b23af
commit
a0cbb8dcda
1433
Cargo.lock
generated
1433
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -9,6 +9,8 @@ clap = { version = "4.5.27", features = ["derive", "string"] }
|
||||
flate2 = "1.0.35"
|
||||
hex = "0.4.3"
|
||||
nom = "8.0.0"
|
||||
reqwest = "0.12.12"
|
||||
sha1 = "0.10.6"
|
||||
thiserror = "2.0.11"
|
||||
tokio = { version = "1.43.0", features = ["full"] }
|
||||
walkdir = "2.5.0"
|
||||
|
167
src/http.rs
Normal file
167
src/http.rs
Normal 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(())
|
||||
}
|
13
src/main.rs
13
src/main.rs
@ -8,6 +8,7 @@ use clap::Subcommand;
|
||||
|
||||
mod commit;
|
||||
mod error;
|
||||
mod http;
|
||||
mod index;
|
||||
mod kind;
|
||||
mod log;
|
||||
@ -16,6 +17,7 @@ mod pack;
|
||||
mod repository;
|
||||
mod tree;
|
||||
|
||||
use crate::http::clone;
|
||||
use crate::repository::Repository;
|
||||
|
||||
#[derive(Parser)]
|
||||
@ -83,9 +85,14 @@ enum Command {
|
||||
/// The object to hash
|
||||
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 mut repo = Repository::new()?;
|
||||
@ -147,6 +154,10 @@ fn main() -> Result<(), Error> {
|
||||
Ok(hash) => println!("{}", hex::encode(hash)),
|
||||
Err(e) => eprintln!("Failed to hash object: {}", e),
|
||||
},
|
||||
Command::Clone { repo } => match clone(&repo).await {
|
||||
Ok(_) => (),
|
||||
Err(e) => eprintln!("Failed to clone: {}", e),
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
Loading…
x
Reference in New Issue
Block a user