Compare commits
13 Commits
4b0902c648
...
main
Author | SHA1 | Date | |
---|---|---|---|
a0cbb8dcda
|
|||
ae3c1b23af
|
|||
de4c366ebb
|
|||
daf5fa3272
|
|||
6cd2907f0d
|
|||
2a4a411341
|
|||
1d8009cad1
|
|||
8c6994986c
|
|||
bed1464bb9
|
|||
f047bb5181
|
|||
62af81b28f
|
|||
49ed740014
|
|||
d1f83c18f3
|
1476
Cargo.lock
generated
1476
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -8,5 +8,9 @@ anyhow = "1.0.95"
|
|||||||
clap = { version = "4.5.27", features = ["derive", "string"] }
|
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"
|
||||||
|
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"
|
||||||
|
108
src/commit.rs
Normal file
108
src/commit.rs
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
use std::fs::{read_to_string, File};
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use hex::FromHex;
|
||||||
|
|
||||||
|
use crate::{kind::Kind, repository::Repository};
|
||||||
|
|
||||||
|
impl Repository {
|
||||||
|
pub fn read_head(&self) -> Result<String> {
|
||||||
|
let head_path = self.path.join(".git").join("HEAD");
|
||||||
|
read_to_string(head_path).context("reading head")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_branch(&self) -> Result<String> {
|
||||||
|
let head = self.read_head()?;
|
||||||
|
Ok(head
|
||||||
|
.trim_start_matches("ref: refs/heads/")
|
||||||
|
.trim_end()
|
||||||
|
.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_commit(&self) -> Result<[u8; 20]> {
|
||||||
|
let current_branch = self.current_branch()?;
|
||||||
|
let branch_path = self
|
||||||
|
.path
|
||||||
|
.join(".git")
|
||||||
|
.join("refs")
|
||||||
|
.join("heads")
|
||||||
|
.join(¤t_branch);
|
||||||
|
|
||||||
|
let r = read_to_string(branch_path).context("could not read current branch")?;
|
||||||
|
let r = r.trim();
|
||||||
|
|
||||||
|
Ok(<[u8; 20]>::from_hex(r)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_current_commit(&self) -> bool {
|
||||||
|
self.current_commit().is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_current_commit(&self, hash: &[u8; 20]) -> Result<()> {
|
||||||
|
let current_branch = self
|
||||||
|
.current_branch()
|
||||||
|
.context("could not find current branch")?;
|
||||||
|
|
||||||
|
let branch_path = self.path.join(".git").join("refs").join("heads");
|
||||||
|
|
||||||
|
if !branch_path.exists() {
|
||||||
|
std::fs::create_dir_all(&branch_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let branch_path = branch_path.join(¤t_branch);
|
||||||
|
|
||||||
|
// file does not exist
|
||||||
|
if !branch_path.exists() {
|
||||||
|
File::create(&branch_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::write(branch_path, hex::encode(hash))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn commit(&self, message: &str) -> Result<[u8; 20]> {
|
||||||
|
let has_current_commit = self.has_current_commit();
|
||||||
|
let mut out: Vec<u8> = Vec::new();
|
||||||
|
|
||||||
|
let tree_hash = self
|
||||||
|
.write_tree(&self.path)
|
||||||
|
.context("could not write_tree")?;
|
||||||
|
out.extend_from_slice(b"tree ");
|
||||||
|
out.extend_from_slice(hex::encode(tree_hash).as_bytes());
|
||||||
|
out.push(b'\n');
|
||||||
|
|
||||||
|
if has_current_commit {
|
||||||
|
let current_commit_id = self.current_commit()?;
|
||||||
|
out.extend_from_slice(b"parent ");
|
||||||
|
out.extend_from_slice(hex::encode(current_commit_id).as_bytes());
|
||||||
|
out.push(b'\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push(b'\n');
|
||||||
|
out.extend_from_slice(message.as_bytes());
|
||||||
|
out.push(b'\n');
|
||||||
|
|
||||||
|
let hash = self.write_object(Kind::Commit, &out).context("Write")?;
|
||||||
|
|
||||||
|
// update current branch's commit id
|
||||||
|
self.set_current_commit(&hash)?;
|
||||||
|
|
||||||
|
self.write_index()?;
|
||||||
|
|
||||||
|
Ok(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show(&self, hash: Option<String>) -> Result<()> {
|
||||||
|
let mut commit = if let Some(hash) = hash {
|
||||||
|
self.read_object(&hash)?
|
||||||
|
} else {
|
||||||
|
let current_commit = self.current_commit()?;
|
||||||
|
self.read_object(&hex::encode(current_commit))?
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("{}", commit.string()?);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
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(())
|
||||||
|
}
|
256
src/index.rs
Normal file
256
src/index.rs
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
use std::{os::linux::fs::MetadataExt, path::Path};
|
||||||
|
|
||||||
|
use nom::{
|
||||||
|
bytes::complete::take,
|
||||||
|
number::complete::{be_u16, be_u32},
|
||||||
|
IResult, Parser,
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Error, Result};
|
||||||
|
use sha1::{Digest, Sha1};
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
use crate::repository::Repository;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
struct IndexHeader {
|
||||||
|
signature: [u8; 4], // "DIRC"
|
||||||
|
version: u32, // 2, 3, or 4
|
||||||
|
entries_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
struct IndexEntry {
|
||||||
|
ctime_s: u32,
|
||||||
|
ctime_n: u32,
|
||||||
|
mtime_s: u32,
|
||||||
|
mtime_n: u32,
|
||||||
|
dev: u32,
|
||||||
|
ino: u32,
|
||||||
|
mode: u32,
|
||||||
|
uid: u32,
|
||||||
|
gid: u32,
|
||||||
|
size: u32,
|
||||||
|
sha1: [u8; 20],
|
||||||
|
flags: u16,
|
||||||
|
file_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
struct Index {
|
||||||
|
header: IndexHeader,
|
||||||
|
entries: Vec<IndexEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_index(input: &[u8]) -> IResult<&[u8], Index> {
|
||||||
|
let (mut input, header) = parse_header(input)?;
|
||||||
|
|
||||||
|
let mut entries = Vec::with_capacity(header.entries_count as usize);
|
||||||
|
|
||||||
|
for _ in 0..header.entries_count {
|
||||||
|
let (remaining, entry) = parse_entry(input)?;
|
||||||
|
entries.push(entry);
|
||||||
|
input = remaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((input, Index { header, entries }))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_header(input: &[u8]) -> IResult<&[u8], IndexHeader> {
|
||||||
|
let (input, (signature, version, entries_count)) =
|
||||||
|
(take(4usize), be_u32, be_u32).parse(input)?;
|
||||||
|
|
||||||
|
let mut sig = [0u8; 4];
|
||||||
|
sig.copy_from_slice(signature);
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
input,
|
||||||
|
IndexHeader {
|
||||||
|
signature: sig,
|
||||||
|
version,
|
||||||
|
entries_count,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_entry(input: &[u8]) -> IResult<&[u8], IndexEntry> {
|
||||||
|
let start_input_len = input.len();
|
||||||
|
let (
|
||||||
|
input,
|
||||||
|
(ctime_s, ctime_n, mtime_s, mtime_n, dev, ino, mode, uid, gid, size, sha1_bytes, flags),
|
||||||
|
) = (
|
||||||
|
be_u32,
|
||||||
|
be_u32,
|
||||||
|
be_u32,
|
||||||
|
be_u32,
|
||||||
|
be_u32,
|
||||||
|
be_u32,
|
||||||
|
be_u32,
|
||||||
|
be_u32,
|
||||||
|
be_u32,
|
||||||
|
be_u32,
|
||||||
|
take(20usize),
|
||||||
|
be_u16,
|
||||||
|
)
|
||||||
|
.parse(input)?;
|
||||||
|
let current_input_len = input.len();
|
||||||
|
|
||||||
|
let path_len = flags & 0xFFF;
|
||||||
|
let (input, path_bytes) = take(path_len as usize)(input)?;
|
||||||
|
let file_path = String::from_utf8_lossy(path_bytes).into_owned();
|
||||||
|
|
||||||
|
// between 1 and 8 NUL bytes to pad the entry.
|
||||||
|
let padding_len = 8 - ((start_input_len - current_input_len) + path_len as usize) % 8;
|
||||||
|
let (input, _) = take(padding_len)(input)?;
|
||||||
|
|
||||||
|
let mut sha1 = [0u8; 20];
|
||||||
|
sha1.copy_from_slice(sha1_bytes);
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
input,
|
||||||
|
IndexEntry {
|
||||||
|
ctime_s,
|
||||||
|
ctime_n,
|
||||||
|
mtime_s,
|
||||||
|
mtime_n,
|
||||||
|
dev,
|
||||||
|
ino,
|
||||||
|
mode,
|
||||||
|
uid,
|
||||||
|
gid,
|
||||||
|
size,
|
||||||
|
sha1,
|
||||||
|
flags,
|
||||||
|
file_path,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Index {
|
||||||
|
pub fn read_from_file(path: &Path) -> Result<Self, Error> {
|
||||||
|
let content = std::fs::read(path)?;
|
||||||
|
let (_remaining, index) =
|
||||||
|
parse_index(&content).map_err(|e| anyhow!("Failed to parse index: {}", e))?;
|
||||||
|
Ok(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Repository {
|
||||||
|
pub fn read_index(&self) -> Result<()> {
|
||||||
|
let index_path = self.path.join(".git").join("index");
|
||||||
|
let index = Index::read_from_file(&index_path)?;
|
||||||
|
|
||||||
|
for entry in index.entries {
|
||||||
|
println!("{} {}", hex::encode(entry.sha1), entry.file_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_index(&self) -> Result<()> {
|
||||||
|
let index_path = self.path.join(".git").join("index");
|
||||||
|
|
||||||
|
// list all files in the repository
|
||||||
|
let files = list_all_files(&self.path, &self.ignore)?;
|
||||||
|
|
||||||
|
let index = Index {
|
||||||
|
header: IndexHeader {
|
||||||
|
signature: *b"DIRC",
|
||||||
|
version: 2,
|
||||||
|
entries_count: files.len() as u32,
|
||||||
|
},
|
||||||
|
entries: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut content = Vec::new();
|
||||||
|
content.extend_from_slice(&index.header.signature);
|
||||||
|
content.extend_from_slice(&index.header.version.to_be_bytes());
|
||||||
|
content.extend_from_slice(&index.header.entries_count.to_be_bytes());
|
||||||
|
|
||||||
|
for file in files {
|
||||||
|
let metadata = std::fs::metadata(self.path.join(file.clone()))?;
|
||||||
|
|
||||||
|
let entry = IndexEntry {
|
||||||
|
ctime_s: metadata.st_ctime() as u32,
|
||||||
|
ctime_n: metadata.st_ctime_nsec() as u32,
|
||||||
|
mtime_s: metadata.st_mtime() as u32,
|
||||||
|
mtime_n: metadata.st_mtime_nsec() as u32,
|
||||||
|
dev: metadata.st_dev() as u32,
|
||||||
|
ino: metadata.st_ino() as u32,
|
||||||
|
mode: metadata.st_mode(),
|
||||||
|
uid: metadata.st_uid(),
|
||||||
|
gid: metadata.st_gid(),
|
||||||
|
size: metadata.st_size() as u32,
|
||||||
|
sha1: hash_file(&self.path.join(file.clone()))?,
|
||||||
|
flags: 0,
|
||||||
|
file_path: file,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut entry_content = Vec::new();
|
||||||
|
entry_content.extend_from_slice(&entry.ctime_s.to_be_bytes());
|
||||||
|
entry_content.extend_from_slice(&entry.ctime_n.to_be_bytes());
|
||||||
|
entry_content.extend_from_slice(&entry.mtime_s.to_be_bytes());
|
||||||
|
entry_content.extend_from_slice(&entry.mtime_n.to_be_bytes());
|
||||||
|
entry_content.extend_from_slice(&entry.dev.to_be_bytes());
|
||||||
|
entry_content.extend_from_slice(&entry.ino.to_be_bytes());
|
||||||
|
entry_content.extend_from_slice(&entry.mode.to_be_bytes());
|
||||||
|
entry_content.extend_from_slice(&entry.uid.to_be_bytes());
|
||||||
|
entry_content.extend_from_slice(&entry.gid.to_be_bytes());
|
||||||
|
entry_content.extend_from_slice(&entry.size.to_be_bytes());
|
||||||
|
entry_content.extend_from_slice(&entry.sha1);
|
||||||
|
//entry_content.extend_from_slice(&entry.flags.to_be_bytes());
|
||||||
|
|
||||||
|
let path_bytes = entry.file_path.as_bytes();
|
||||||
|
entry_content.extend_from_slice(&(path_bytes.len() as u16).to_be_bytes());
|
||||||
|
entry_content.extend_from_slice(path_bytes);
|
||||||
|
|
||||||
|
// between 1 and 8 NUL bytes to pad the entry.
|
||||||
|
let padding_len = 8 - entry_content.len() % 8;
|
||||||
|
entry_content.extend(vec![0u8; padding_len]);
|
||||||
|
|
||||||
|
content.extend(entry_content);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::write(index_path, content)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_all_files(path: &Path, ignore_list: &[String]) -> Result<Vec<String>> {
|
||||||
|
let mut files = Vec::new();
|
||||||
|
|
||||||
|
for entry in WalkDir::new(path).into_iter().filter_map(|e| e.ok()) {
|
||||||
|
if entry.file_type().is_file() {
|
||||||
|
if ignore_list.iter().any(|i| entry.path().ends_with(i)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let s = entry.path().to_path_buf().to_str().unwrap().to_string();
|
||||||
|
let s = s.strip_prefix(path.to_str().unwrap()).unwrap().to_string();
|
||||||
|
|
||||||
|
if ignore_list.iter().any(|i| s.starts_with(i)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
files.push(s.strip_prefix("/").unwrap().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
files.sort();
|
||||||
|
|
||||||
|
Ok(files)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hash_file(path: &Path) -> Result<[u8; 20]> {
|
||||||
|
let content = std::fs::read(path)?;
|
||||||
|
|
||||||
|
let mut hasher = Sha1::new();
|
||||||
|
hasher.update(format!("blob {}\0", content.len()).as_bytes());
|
||||||
|
hasher.update(content);
|
||||||
|
|
||||||
|
Ok(hasher.finalize().into())
|
||||||
|
}
|
11
src/kind.rs
11
src/kind.rs
@ -1,3 +1,5 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -30,13 +32,16 @@ impl Kind {
|
|||||||
Kind::Symlink => "120000",
|
Kind::Symlink => "120000",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn string(&self) -> &str {
|
impl fmt::Display for Kind {
|
||||||
match self {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let kind = match self {
|
||||||
Kind::Blob(_) => "blob",
|
Kind::Blob(_) => "blob",
|
||||||
Kind::Commit => "commit",
|
Kind::Commit => "commit",
|
||||||
Kind::Tree => "tree",
|
Kind::Tree => "tree",
|
||||||
Kind::Symlink => "symlink",
|
Kind::Symlink => "symlink",
|
||||||
}
|
};
|
||||||
|
write!(f, "{}", kind)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
36
src/log.rs
Normal file
36
src/log.rs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
use crate::repository::Repository;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use hex::FromHex;
|
||||||
|
|
||||||
|
impl Repository {
|
||||||
|
pub fn log(&self) -> Result<()> {
|
||||||
|
let mut current_commit = self.current_commit()?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut commit = self.read_object(&hex::encode(current_commit))?;
|
||||||
|
|
||||||
|
let commit_desc = commit.string()?;
|
||||||
|
let lines = commit_desc.lines().collect::<Vec<&str>>();
|
||||||
|
|
||||||
|
// find the first empty line
|
||||||
|
let first_empty_line = lines.iter().position(|line| line.is_empty());
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{} {}",
|
||||||
|
hex::encode(current_commit),
|
||||||
|
lines[first_empty_line.unwrap() + 1]
|
||||||
|
);
|
||||||
|
|
||||||
|
let parent_commit_id = lines.iter().find(|line| line.starts_with("parent "));
|
||||||
|
if parent_commit_id.is_none() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parent_commit_id = parent_commit_id.unwrap();
|
||||||
|
current_commit = <[u8; 20]>::from_hex(parent_commit_id.split_once(' ').unwrap().1)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
130
src/main.rs
130
src/main.rs
@ -1,17 +1,24 @@
|
|||||||
use anyhow::{Error, Result};
|
use anyhow::{Error, Result};
|
||||||
use std::env;
|
use object::hash_object;
|
||||||
use std::{fs, path::PathBuf};
|
use repository::default_init_path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use clap::Subcommand;
|
use clap::Subcommand;
|
||||||
|
|
||||||
|
mod commit;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod http;
|
||||||
|
mod index;
|
||||||
mod kind;
|
mod kind;
|
||||||
|
mod log;
|
||||||
mod object;
|
mod object;
|
||||||
|
mod pack;
|
||||||
|
mod repository;
|
||||||
mod tree;
|
mod tree;
|
||||||
|
|
||||||
use crate::object::{read_object, write_blob};
|
use crate::http::clone;
|
||||||
use crate::tree::write_tree;
|
use crate::repository::Repository;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "mg", about = "A simple git clone")]
|
#[command(name = "mg", about = "A simple git clone")]
|
||||||
@ -43,47 +50,114 @@ enum Command {
|
|||||||
/// The path to write
|
/// The path to write
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
},
|
},
|
||||||
|
/// Commit current changes
|
||||||
|
Commit {
|
||||||
|
/// The commit message
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
/// Get the current branch
|
||||||
|
Branch,
|
||||||
|
/// Get the latest commit
|
||||||
|
Show {
|
||||||
|
/// The commit to show
|
||||||
|
hash: Option<String>,
|
||||||
|
},
|
||||||
|
/// Show the commit log
|
||||||
|
Log,
|
||||||
|
/// List the index entries
|
||||||
|
LsIndex,
|
||||||
|
/// Write the index file
|
||||||
|
WriteIndex,
|
||||||
|
/// Dump a Pack File
|
||||||
|
DumpPack {
|
||||||
|
/// The pack file to dump
|
||||||
|
file: PathBuf,
|
||||||
|
},
|
||||||
|
/// Dump Pack Files
|
||||||
|
DumpPackFiles,
|
||||||
|
/// Dump Pack Index file
|
||||||
|
DumpPackIndexFile {
|
||||||
|
/// The pack index file to dump
|
||||||
|
pack_id: String,
|
||||||
|
},
|
||||||
|
/// Hash an object
|
||||||
|
HashObject {
|
||||||
|
/// The object to hash
|
||||||
|
file: PathBuf,
|
||||||
|
},
|
||||||
|
Clone {
|
||||||
|
/// The repository to clone
|
||||||
|
repo: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_init_path() -> PathBuf {
|
#[tokio::main]
|
||||||
env::var("REPO_PATH")
|
async fn main() -> Result<(), Error> {
|
||||||
.map(PathBuf::from)
|
|
||||||
.unwrap_or_else(|_| PathBuf::from("."))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_repository(path: PathBuf) -> Result<PathBuf> {
|
|
||||||
let git_dir = path.join(".git");
|
|
||||||
|
|
||||||
fs::create_dir(&git_dir)?;
|
|
||||||
fs::create_dir(git_dir.join("objects"))?;
|
|
||||||
fs::create_dir(git_dir.join("refs"))?;
|
|
||||||
|
|
||||||
fs::write(git_dir.join("HEAD"), "ref: refs/heads/main\n")?;
|
|
||||||
|
|
||||||
Ok(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<(), Error> {
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
let repo_path = default_init_path();
|
|
||||||
|
let mut repo = Repository::new()?;
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Command::Init { path } => match init_repository(path) {
|
Command::Init { path } => match repo.init_repository(&path) {
|
||||||
Ok(path) => println!("Initialized empty Git repository in {:?}", path),
|
Ok(path) => println!("Initialized empty Git repository in {:?}", path),
|
||||||
Err(e) => eprintln!("Failed to initialize repository: {}", e),
|
Err(e) => eprintln!("Failed to initialize repository: {}", e),
|
||||||
},
|
},
|
||||||
Command::CatFile { hash } => match read_object(&repo_path, &hash) {
|
Command::CatFile { hash } => match repo.read_object(&hash) {
|
||||||
Ok(mut obj) => print!("{}", obj.string()?),
|
Ok(mut obj) => print!("{}", obj.string()?),
|
||||||
Err(e) => eprintln!("Failed to read object: {}", e),
|
Err(e) => eprintln!("Failed to read object: {}", e),
|
||||||
},
|
},
|
||||||
Command::WriteBlob { file } => match write_blob(&repo_path, &file) {
|
Command::WriteBlob { file } => match repo.write_blob(&file) {
|
||||||
Ok(hash) => println!("{}", hex::encode(hash)),
|
Ok(hash) => println!("{}", hex::encode(hash)),
|
||||||
Err(e) => eprintln!("Failed to write object: {}", e),
|
Err(e) => eprintln!("Failed to write object: {}", e),
|
||||||
},
|
},
|
||||||
Command::WriteTree { path } => match write_tree(&repo_path, &path) {
|
Command::WriteTree { path } => match repo.write_tree(&path) {
|
||||||
Ok(hash) => println!("{}", hex::encode(hash)),
|
Ok(hash) => println!("{}", hex::encode(hash)),
|
||||||
Err(e) => eprintln!("Failed to write tree: {}", e),
|
Err(e) => eprintln!("Failed to write tree: {}", e),
|
||||||
},
|
},
|
||||||
|
Command::Commit { message } => match repo.commit(&message) {
|
||||||
|
Ok(hash) => println!("{}", hex::encode(hash)),
|
||||||
|
Err(e) => eprintln!("Failed to commit: {}", e),
|
||||||
|
},
|
||||||
|
Command::Branch => match repo.current_branch() {
|
||||||
|
Ok(branch) => println!("{}", branch),
|
||||||
|
Err(e) => eprintln!("Failed to get branch: {}", e),
|
||||||
|
},
|
||||||
|
Command::Show { hash } => match repo.show(hash) {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(e) => eprintln!("Failed to show: {}", e),
|
||||||
|
},
|
||||||
|
Command::Log => match repo.log() {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(e) => eprintln!("Failed to show log: {}", e),
|
||||||
|
},
|
||||||
|
Command::LsIndex => match repo.read_index() {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(e) => eprintln!("Failed to list index: {}", e),
|
||||||
|
},
|
||||||
|
Command::WriteIndex => match repo.write_index() {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(e) => eprintln!("Failed to write index: {}", e),
|
||||||
|
},
|
||||||
|
Command::DumpPackFiles => match repo.dump_pack_files() {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(e) => eprintln!("Failed to dump pack files: {}", e),
|
||||||
|
},
|
||||||
|
Command::DumpPack { file } => match repo.dump_pack(&file) {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(e) => eprintln!("Failed to dump pack: {}", e),
|
||||||
|
},
|
||||||
|
Command::DumpPackIndexFile { pack_id } => match repo.dump_pack_index_file(&pack_id) {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(e) => eprintln!("Failed to dump pack index file: {}", e),
|
||||||
|
},
|
||||||
|
Command::HashObject { file } => match hash_object(&file) {
|
||||||
|
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(())
|
Ok(())
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
use crate::repository::Repository;
|
||||||
use crate::{error::RuntimeError, kind::Kind};
|
use crate::{error::RuntimeError, kind::Kind};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use flate2::{write::ZlibEncoder, Compression};
|
use flate2::{write::ZlibEncoder, Compression};
|
||||||
|
|
||||||
use sha1::{Digest, Sha1};
|
use sha1::{Digest, Sha1};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::{
|
use std::{
|
||||||
@ -24,8 +26,10 @@ pub struct TreeObject {
|
|||||||
pub hash: [u8; 20],
|
pub hash: [u8; 20],
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_object(path: &Path, object: &str) -> Result<Object<impl BufRead>> {
|
impl Repository {
|
||||||
let object_path = path
|
pub fn read_object(&self, object: &str) -> Result<Object<impl BufRead>> {
|
||||||
|
let object_path = self
|
||||||
|
.path
|
||||||
.join(".git")
|
.join(".git")
|
||||||
.join("objects")
|
.join("objects")
|
||||||
.join(&object[..2])
|
.join(&object[..2])
|
||||||
@ -65,38 +69,26 @@ pub fn read_object(path: &Path, object: &str) -> Result<Object<impl BufRead>> {
|
|||||||
_size: object_size,
|
_size: object_size,
|
||||||
data: buf_reader,
|
data: buf_reader,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_path_in_repo(repo_path: &Path, file_path: &Path) -> Result<bool> {
|
pub fn write_blob(&self, file: &Path) -> Result<[u8; 20]> {
|
||||||
// Convert both paths to absolute paths
|
if !file.exists() || !is_path_in_repo(&self.path, file)? {
|
||||||
let repo_canonical = repo_path.canonicalize()?;
|
|
||||||
let file_canonical = match file_path.canonicalize() {
|
|
||||||
Ok(path) => path,
|
|
||||||
Err(_) => return Ok(false),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if file_path starts with repo_path
|
|
||||||
Ok(file_canonical.starts_with(repo_canonical))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write_blob(repo_path: &Path, file: &Path) -> Result<[u8; 20]> {
|
|
||||||
if !file.exists() || !is_path_in_repo(repo_path, file)? {
|
|
||||||
return Err(anyhow!("path does not exist"));
|
return Err(anyhow!("path does not exist"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = std::fs::read(file)?;
|
let content = std::fs::read(file)?;
|
||||||
|
|
||||||
Ok(write_object(repo_path, Kind::Blob(false), &content)?)
|
self.write_object(Kind::Blob(false), &content)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_object(repo_path: &Path, kind: Kind, content: &[u8]) -> Result<[u8; 20]> {
|
pub fn write_object(&self, kind: Kind, content: &[u8]) -> Result<[u8; 20]> {
|
||||||
let mut hasher = Sha1::new();
|
let mut hasher = Sha1::new();
|
||||||
hasher.update(format!("{} {}\0", kind.string(), content.len()).as_bytes());
|
hasher.update(format!("{} {}\0", kind, content.len()).as_bytes());
|
||||||
hasher.update(content);
|
hasher.update(content);
|
||||||
let hash = hasher.finalize().into();
|
let hash = hasher.finalize().into();
|
||||||
let hash_str = hex::encode(hash);
|
let hash_str = hex::encode(hash);
|
||||||
|
|
||||||
let target_dir = repo_path.join(".git").join("objects").join(&hash_str[..2]);
|
let target_dir = self.path.join(".git").join("objects").join(&hash_str[..2]);
|
||||||
if !target_dir.exists() {
|
if !target_dir.exists() {
|
||||||
create_dir(&target_dir).context("could not create directory in .git/objects")?;
|
create_dir(&target_dir).context("could not create directory in .git/objects")?;
|
||||||
}
|
}
|
||||||
@ -109,13 +101,38 @@ pub fn write_object(repo_path: &Path, kind: Kind, content: &[u8]) -> Result<[u8;
|
|||||||
let file_out_fd = File::create(target_file).context("could not open target file")?;
|
let file_out_fd = File::create(target_file).context("could not open target file")?;
|
||||||
|
|
||||||
let mut zlib_out = ZlibEncoder::new(file_out_fd, Compression::default());
|
let mut zlib_out = ZlibEncoder::new(file_out_fd, Compression::default());
|
||||||
write!(zlib_out, "{} {}\0", kind.string(), content.len()).context("could not write header")?;
|
write!(zlib_out, "{} {}\0", kind, content.len()).context("could not write header")?;
|
||||||
zlib_out.write(content)?;
|
zlib_out.write_all(content)?;
|
||||||
zlib_out
|
zlib_out
|
||||||
.finish()
|
.finish()
|
||||||
.context("could not compress or write file")?;
|
.context("could not compress or write file")?;
|
||||||
|
|
||||||
Ok(hash)
|
Ok(hash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hash_object(file: &Path) -> Result<[u8; 20]> {
|
||||||
|
let content = std::fs::read(file)?;
|
||||||
|
|
||||||
|
let kind = Kind::Blob(false);
|
||||||
|
let mut hasher = Sha1::new();
|
||||||
|
hasher.update(format!("{} {}\0", kind, content.len()).as_bytes());
|
||||||
|
hasher.update(content);
|
||||||
|
let hash = hasher.finalize().into();
|
||||||
|
|
||||||
|
Ok(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_path_in_repo(repo_path: &Path, file_path: &Path) -> Result<bool> {
|
||||||
|
// Convert both paths to absolute paths
|
||||||
|
let repo_canonical = repo_path.canonicalize()?;
|
||||||
|
let file_canonical = match file_path.canonicalize() {
|
||||||
|
Ok(path) => path,
|
||||||
|
Err(_) => return Ok(false),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if file_path starts with repo_path
|
||||||
|
Ok(file_canonical.starts_with(repo_canonical))
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R: BufRead> Object<R> {
|
impl<R: BufRead> Object<R> {
|
||||||
@ -173,7 +190,7 @@ impl<R: BufRead> Object<R> {
|
|||||||
format!(
|
format!(
|
||||||
"{:0>6} {} {} {:name_len$}",
|
"{:0>6} {} {} {:name_len$}",
|
||||||
entry.mode,
|
entry.mode,
|
||||||
entry.kind.string(),
|
entry.kind,
|
||||||
hash,
|
hash,
|
||||||
entry.name,
|
entry.name,
|
||||||
name_len = max_name_len
|
name_len = max_name_len
|
||||||
@ -188,3 +205,31 @@ impl<R: BufRead> Object<R> {
|
|||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use hex::FromHex;
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_object_string() {
|
||||||
|
let data = b"hello";
|
||||||
|
let _obj = Object {
|
||||||
|
kind: Kind::Blob(true),
|
||||||
|
_size: 5,
|
||||||
|
data: Cursor::new(data),
|
||||||
|
};
|
||||||
|
|
||||||
|
let temp_file = std::env::temp_dir().join("temp_file");
|
||||||
|
let mut file = File::create(&temp_file).unwrap();
|
||||||
|
file.write_all(data).unwrap();
|
||||||
|
|
||||||
|
let res = hash_object(&temp_file);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
res.unwrap(),
|
||||||
|
<[u8; 20]>::from_hex("b6fc4c620b67d95f953a5c1c1230aaab5db5a1b0").unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
427
src/pack.rs
Normal file
427
src/pack.rs
Normal file
@ -0,0 +1,427 @@
|
|||||||
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
io::{BufReader, Cursor, Read, Seek, SeekFrom},
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::Error;
|
||||||
|
use flate2::read::ZlibDecoder;
|
||||||
|
use sha1::{Digest, Sha1};
|
||||||
|
|
||||||
|
use crate::repository::Repository;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
struct PackHeader {
|
||||||
|
signature: [u8; 4],
|
||||||
|
version: u32,
|
||||||
|
num_objects: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
struct PackObject {
|
||||||
|
object_type: PackObjectType,
|
||||||
|
object_size: u32,
|
||||||
|
object_data: Vec<u8>,
|
||||||
|
pos: u64,
|
||||||
|
end_pos: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
enum PackObjectType {
|
||||||
|
Commit,
|
||||||
|
Tree,
|
||||||
|
Blob,
|
||||||
|
Tag,
|
||||||
|
OfsDelta,
|
||||||
|
RefDelta,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PackObjectType {
|
||||||
|
fn from_u8(value: u8) -> Result<PackObjectType, Error> {
|
||||||
|
match value {
|
||||||
|
1 => Ok(PackObjectType::Commit),
|
||||||
|
2 => Ok(PackObjectType::Tree),
|
||||||
|
3 => Ok(PackObjectType::Blob),
|
||||||
|
4 => Ok(PackObjectType::Tag),
|
||||||
|
6 => Ok(PackObjectType::OfsDelta),
|
||||||
|
7 => Ok(PackObjectType::RefDelta),
|
||||||
|
_ => Err(Error::msg("Unknown object type")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for PackObjectType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let s = match self {
|
||||||
|
PackObjectType::Commit => "commit",
|
||||||
|
PackObjectType::Tree => "tree",
|
||||||
|
PackObjectType::Blob => "blob",
|
||||||
|
PackObjectType::Tag => "tag",
|
||||||
|
PackObjectType::OfsDelta => "ofs-delta",
|
||||||
|
PackObjectType::RefDelta => "ref-delta",
|
||||||
|
};
|
||||||
|
|
||||||
|
write!(f, "{}", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_pack_header(file: &mut File) -> Result<PackHeader, Error> {
|
||||||
|
let mut header = [0; 12];
|
||||||
|
file.read_exact(&mut header)?;
|
||||||
|
|
||||||
|
let signature: &[u8] = &header[0..4];
|
||||||
|
if signature != b"PACK" {
|
||||||
|
return Err(Error::msg("Invalid pack file"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let version = u32::from_be_bytes([header[4], header[5], header[6], header[7]]);
|
||||||
|
if version != 2 {
|
||||||
|
return Err(Error::msg("Invalid pack file version"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let num_objects = u32::from_be_bytes([header[8], header[9], header[10], header[11]]);
|
||||||
|
let signature: [u8; 4] = signature[0..4].try_into().unwrap();
|
||||||
|
|
||||||
|
Ok(PackHeader {
|
||||||
|
signature,
|
||||||
|
version,
|
||||||
|
num_objects,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_vli_le<R>(file: &mut BufReader<R>) -> Result<u32, Error>
|
||||||
|
where
|
||||||
|
R: Read,
|
||||||
|
{
|
||||||
|
let mut val: u32 = 0;
|
||||||
|
let mut shift = 0;
|
||||||
|
loop {
|
||||||
|
let mut byte = [0; 1];
|
||||||
|
file.read_exact(&mut byte)?;
|
||||||
|
let byt = byte[0] as u32;
|
||||||
|
|
||||||
|
val |= (byt & 0x7f) << shift;
|
||||||
|
shift += 7;
|
||||||
|
|
||||||
|
if byt & 0x80 == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_vli_be(file: &mut File, offset: bool) -> Result<u32, Error> {
|
||||||
|
let mut val: u32 = 0;
|
||||||
|
loop {
|
||||||
|
let mut byte = [0; 1];
|
||||||
|
file.read_exact(&mut byte)?;
|
||||||
|
let byt = byte[0] as u32;
|
||||||
|
|
||||||
|
val = (val << 7) | (byt & 0x7f);
|
||||||
|
if byt & 0x80 == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if offset {
|
||||||
|
val += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decompress_file(file: &mut File) -> Result<Vec<u8>, Error> {
|
||||||
|
let mut object_data = Vec::new();
|
||||||
|
|
||||||
|
let pos = file.stream_position()?;
|
||||||
|
let mut zlib_decoder = ZlibDecoder::new(&mut *file);
|
||||||
|
zlib_decoder.read_to_end(&mut object_data)?;
|
||||||
|
let read_bytes = zlib_decoder.total_in();
|
||||||
|
file.seek(std::io::SeekFrom::Start(pos + read_bytes))?;
|
||||||
|
|
||||||
|
Ok(object_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_delta_obj(
|
||||||
|
file: &mut File,
|
||||||
|
base_obj: PackObject,
|
||||||
|
object_size: u32,
|
||||||
|
) -> Result<PackObject, Error> {
|
||||||
|
let current_pos = file.stream_position()?;
|
||||||
|
let object_data = decompress_file(file)?;
|
||||||
|
|
||||||
|
assert_eq!(object_data.len(), object_size as usize);
|
||||||
|
|
||||||
|
let mut fp2 = BufReader::new(Cursor::new(object_data.as_slice()));
|
||||||
|
|
||||||
|
let _base_obj_size = read_vli_le(&mut fp2)?;
|
||||||
|
let patched_obj_size = read_vli_le(&mut fp2)?;
|
||||||
|
|
||||||
|
// println!(
|
||||||
|
// "base_obj_size={}, obj_size={}",
|
||||||
|
// base_obj_size, patched_obj_size
|
||||||
|
// );
|
||||||
|
|
||||||
|
let mut obj_data = Vec::new();
|
||||||
|
while fp2.stream_position()? < object_data.len() as u64 {
|
||||||
|
let mut byte = [0; 1];
|
||||||
|
fp2.read_exact(&mut byte)?;
|
||||||
|
let byt = byte[0];
|
||||||
|
|
||||||
|
if byt == 0x00 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if byt & 0x80 != 0 {
|
||||||
|
// copy data from base object
|
||||||
|
let mut vals = [0; 6];
|
||||||
|
|
||||||
|
for (i, val) in vals.iter_mut().enumerate() {
|
||||||
|
let bmask = 1 << i;
|
||||||
|
if byt & bmask != 0 {
|
||||||
|
fp2.read_exact(&mut byte)?;
|
||||||
|
*val = byte[0];
|
||||||
|
} else {
|
||||||
|
*val = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = u32::from_le_bytes(vals[0..4].try_into().expect("4 bytes"));
|
||||||
|
let nbytes = u16::from_le_bytes(vals[4..6].try_into().expect("2 bytes"));
|
||||||
|
let nbytes = if nbytes == 0 { 0x10000 } else { nbytes as u32 };
|
||||||
|
|
||||||
|
obj_data.extend_from_slice(
|
||||||
|
&base_obj.object_data[start as usize..(start + nbytes) as usize],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// add new data
|
||||||
|
let nbytes = byt & 0x7f;
|
||||||
|
// println!("APPEND NEW BYTES #bytes={}", nbytes);
|
||||||
|
let mut data = vec![0; nbytes as usize];
|
||||||
|
fp2.read_exact(&mut data)?;
|
||||||
|
obj_data.extend_from_slice(&data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// println!("Final object data: #bytes={}", obj_data.len());
|
||||||
|
|
||||||
|
assert_eq!(obj_data.len(), patched_obj_size as usize);
|
||||||
|
|
||||||
|
Ok(PackObject {
|
||||||
|
object_type: base_obj.object_type,
|
||||||
|
object_size: patched_obj_size,
|
||||||
|
object_data: obj_data,
|
||||||
|
pos: current_pos,
|
||||||
|
end_pos: file.stream_position()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_pack_ofs_delta_object(
|
||||||
|
file: &mut File,
|
||||||
|
object_size: u32,
|
||||||
|
fpos: u64,
|
||||||
|
) -> Result<PackObject, Error> {
|
||||||
|
// println!("pos: 0x{:x}", file.seek(SeekFrom::Current(0))?);
|
||||||
|
|
||||||
|
// let mut reader = BufReader::new(&mut *file);
|
||||||
|
let offset = read_vli_be(file, true)?;
|
||||||
|
// let new_position = reader.stream_position()?;
|
||||||
|
// file.seek(SeekFrom::Start(new_position))?;
|
||||||
|
|
||||||
|
let base_obj_offset = fpos - offset as u64;
|
||||||
|
|
||||||
|
// println!(
|
||||||
|
// "offset:0x{:x} base_obj_offset:0x{:x}",
|
||||||
|
// offset, base_obj_offset
|
||||||
|
// );
|
||||||
|
|
||||||
|
let prev_pos = file.stream_position()?;
|
||||||
|
file.seek(SeekFrom::Start(base_obj_offset))?;
|
||||||
|
|
||||||
|
let base_obj = parse_pack_entry(file)?;
|
||||||
|
assert!([
|
||||||
|
PackObjectType::Commit,
|
||||||
|
PackObjectType::Tree,
|
||||||
|
PackObjectType::Blob,
|
||||||
|
PackObjectType::Tag
|
||||||
|
]
|
||||||
|
.contains(&base_obj.object_type));
|
||||||
|
|
||||||
|
file.seek(SeekFrom::Start(prev_pos))?;
|
||||||
|
|
||||||
|
make_delta_obj(file, base_obj, object_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_pack_entry(file: &mut File) -> Result<PackObject, Error> {
|
||||||
|
let object_pos = file.stream_position()?;
|
||||||
|
|
||||||
|
let mut byte = [0; 1];
|
||||||
|
file.read_exact(&mut byte)?;
|
||||||
|
let object_type: u8 = (byte[0] & 0x70) >> 4;
|
||||||
|
let object_data;
|
||||||
|
|
||||||
|
let mut object_size: u32 = (byte[0] & 0x0f) as u32;
|
||||||
|
let mut bshift = 4;
|
||||||
|
while (byte[0] & 0x80) == 0x80 {
|
||||||
|
file.read_exact(&mut byte)?;
|
||||||
|
object_size += (byte[0] as u32 & 0x7f) << bshift;
|
||||||
|
bshift += 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
// println!(
|
||||||
|
// "Reading object: fpos=0x{:x}, type:{} size:{}",
|
||||||
|
// object_pos,
|
||||||
|
// PackObjectType::from_u8(object_type)?,
|
||||||
|
// object_size
|
||||||
|
// );
|
||||||
|
|
||||||
|
match PackObjectType::from_u8(object_type)? {
|
||||||
|
PackObjectType::Commit
|
||||||
|
| PackObjectType::Tree
|
||||||
|
| PackObjectType::Blob
|
||||||
|
| PackObjectType::Tag => {
|
||||||
|
object_data = decompress_file(file)?;
|
||||||
|
assert_eq!(object_data.len(), object_size as usize);
|
||||||
|
}
|
||||||
|
PackObjectType::OfsDelta => {
|
||||||
|
return parse_pack_ofs_delta_object(file, object_size, object_pos);
|
||||||
|
}
|
||||||
|
PackObjectType::RefDelta => unimplemented!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(PackObject {
|
||||||
|
object_type: PackObjectType::from_u8(object_type)?,
|
||||||
|
object_size,
|
||||||
|
object_data,
|
||||||
|
pos: object_pos,
|
||||||
|
end_pos: file.stream_position()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Repository {
|
||||||
|
pub fn dump_pack_files(&self) -> Result<(), Error> {
|
||||||
|
let pack_dir = self.path.join(".git/objects/pack");
|
||||||
|
|
||||||
|
for entry in pack_dir.read_dir()? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.file_name();
|
||||||
|
let path_str = path.to_str().unwrap();
|
||||||
|
if path_str.starts_with("pack-") && path_str.ends_with(".pack") {
|
||||||
|
let pack_id = &path_str[5..path_str.len() - 5];
|
||||||
|
self.dump_pack_file(pack_id)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dump_pack(&self, path: &Path) -> Result<(), Error> {
|
||||||
|
let mut file = File::open(path)?;
|
||||||
|
|
||||||
|
let header = parse_pack_header(&mut file)?;
|
||||||
|
println!("{:?}", header);
|
||||||
|
|
||||||
|
for _ in 0..header.num_objects {
|
||||||
|
let obj = parse_pack_entry(&mut file)?;
|
||||||
|
|
||||||
|
let mut hasher = Sha1::new();
|
||||||
|
hasher.update(format!("{} {}\0", obj.object_type, obj.object_size).as_bytes());
|
||||||
|
hasher.update(obj.object_data);
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{} {} {} {} {}",
|
||||||
|
hex::encode(hasher.finalize()),
|
||||||
|
obj.object_type,
|
||||||
|
obj.object_size,
|
||||||
|
obj.end_pos - obj.pos,
|
||||||
|
obj.pos,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut checksum_pack = [0; 20];
|
||||||
|
file.read_exact(&mut checksum_pack)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dump_pack_file(&self, pack_id: &str) -> Result<(), Error> {
|
||||||
|
let file_path = self
|
||||||
|
.path
|
||||||
|
.join(format!(".git/objects/pack/pack-{}.pack", pack_id));
|
||||||
|
|
||||||
|
self.dump_pack(&file_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dump_pack_index_file(&self, pack_id: &str) -> Result<(), Error> {
|
||||||
|
let file_path = self
|
||||||
|
.path
|
||||||
|
.join(format!(".git/objects/pack/pack-{}.idx", pack_id));
|
||||||
|
|
||||||
|
let mut file = File::open(file_path)?;
|
||||||
|
|
||||||
|
let mut buf = [0; 4];
|
||||||
|
file.read_exact(&mut buf)?;
|
||||||
|
|
||||||
|
if buf[0] != 0xff || "t0c".as_bytes() == &buf[1..4] {
|
||||||
|
return Err(Error::msg("Invalid pack index magic"));
|
||||||
|
}
|
||||||
|
|
||||||
|
file.read_exact(&mut buf)?;
|
||||||
|
let version = u32::from_be_bytes(buf);
|
||||||
|
println!("{}", version);
|
||||||
|
if version != 2 {
|
||||||
|
return Err(Error::msg("Invalid pack index version"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut num_objects: u32 = 0;
|
||||||
|
let mut fanout_table = [0u32; 256];
|
||||||
|
|
||||||
|
// fanout table: 256 x 8 bytes
|
||||||
|
let mut buf = [0u8; 256 * 4];
|
||||||
|
file.read_exact(&mut buf)?;
|
||||||
|
|
||||||
|
for (idx, fanout_record) in fanout_table.iter_mut().enumerate() {
|
||||||
|
num_objects = u32::from_be_bytes(buf[idx * 4..idx * 4 + 4].try_into().unwrap());
|
||||||
|
*fanout_record = num_objects;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut names = vec![0u8; 20 * num_objects as usize];
|
||||||
|
file.read_exact(&mut names)?;
|
||||||
|
|
||||||
|
let mut crc32_buf = vec![0u8; 4 * num_objects as usize];
|
||||||
|
file.read_exact(&mut crc32_buf)?;
|
||||||
|
let crc32: Vec<u32> = crc32_buf
|
||||||
|
.chunks_exact(4)
|
||||||
|
.map(|chunk| u32::from_be_bytes(chunk.try_into().unwrap()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut offsets_buf = vec![0u8; 4 * num_objects as usize];
|
||||||
|
file.read_exact(&mut offsets_buf)?;
|
||||||
|
let offsets: Vec<u32> = offsets_buf
|
||||||
|
.chunks_exact(4)
|
||||||
|
.map(|chunk| u32::from_be_bytes(chunk.try_into().unwrap()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for i in 0..num_objects {
|
||||||
|
let offset = offsets[i as usize];
|
||||||
|
let crc32 = crc32[i as usize];
|
||||||
|
let name = &names[(i * 20) as usize..(i * 20 + 20) as usize];
|
||||||
|
println!(
|
||||||
|
"{} offset: 0x{:x} crc32: {}",
|
||||||
|
hex::encode(name),
|
||||||
|
offset,
|
||||||
|
crc32
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut checksum_pack = [0; 20];
|
||||||
|
file.read_exact(&mut checksum_pack)?;
|
||||||
|
let mut checksum_idx = [0; 20];
|
||||||
|
file.read_exact(&mut checksum_idx)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
58
src/repository.rs
Normal file
58
src/repository.rs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use std::{
|
||||||
|
env,
|
||||||
|
fs::{create_dir, read_to_string},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Repository {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub ignore: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_init_path() -> PathBuf {
|
||||||
|
env::var("REPO_PATH")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|_| PathBuf::from("."))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Repository {
|
||||||
|
pub fn new() -> Result<Repository> {
|
||||||
|
let path = default_init_path();
|
||||||
|
|
||||||
|
let mut repo = Repository {
|
||||||
|
path,
|
||||||
|
ignore: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
repo.load_ignore()?;
|
||||||
|
|
||||||
|
Ok(repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_ignore(&mut self) -> Result<bool> {
|
||||||
|
let ignore_path = self.path.join(".gitignore");
|
||||||
|
if !ignore_path.exists() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ignore_content = read_to_string(ignore_path)?;
|
||||||
|
self.ignore = ignore_content.lines().map(String::from).collect();
|
||||||
|
self.ignore.push("/.git/".to_string());
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_repository(&mut self, path: &Path) -> Result<PathBuf> {
|
||||||
|
self.path = path.to_path_buf();
|
||||||
|
let git_dir = self.path.join(".git");
|
||||||
|
|
||||||
|
create_dir(&git_dir)?;
|
||||||
|
create_dir(git_dir.join("objects"))?;
|
||||||
|
create_dir(git_dir.join("refs"))?;
|
||||||
|
|
||||||
|
std::fs::write(git_dir.join("HEAD"), "ref: refs/heads/main\n")?;
|
||||||
|
|
||||||
|
Ok(self.path.clone())
|
||||||
|
}
|
||||||
|
}
|
20
src/tree.rs
20
src/tree.rs
@ -3,9 +3,11 @@ use std::os::unix::fs::MetadataExt;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::kind::Kind;
|
use crate::kind::Kind;
|
||||||
use crate::object::{write_blob, write_object, TreeObject};
|
use crate::object::TreeObject;
|
||||||
|
use crate::repository::Repository;
|
||||||
|
|
||||||
pub fn write_tree(repo_path: &PathBuf, path: &PathBuf) -> Result<[u8; 20]> {
|
impl Repository {
|
||||||
|
pub fn write_tree(&self, path: &PathBuf) -> Result<[u8; 20]> {
|
||||||
let mut entries = Vec::new();
|
let mut entries = Vec::new();
|
||||||
|
|
||||||
let files = std::fs::read_dir(path)?;
|
let files = std::fs::read_dir(path)?;
|
||||||
@ -15,14 +17,21 @@ pub fn write_tree(repo_path: &PathBuf, path: &PathBuf) -> Result<[u8; 20]> {
|
|||||||
let file_name = file.file_name();
|
let file_name = file.file_name();
|
||||||
let file_path = file.path();
|
let file_path = file.path();
|
||||||
|
|
||||||
|
// Skip the .git directory
|
||||||
|
if file_name == ".git" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let hash: [u8; 20];
|
let hash: [u8; 20];
|
||||||
let kind;
|
let kind;
|
||||||
|
|
||||||
if file_type.is_dir() {
|
if file_type.is_dir() {
|
||||||
hash = write_tree(repo_path, &file_path).context("could not write_tree of subtree")?;
|
hash = self
|
||||||
|
.write_tree(&file_path)
|
||||||
|
.context("could not write_tree of subtree")?;
|
||||||
kind = Kind::Tree;
|
kind = Kind::Tree;
|
||||||
} else {
|
} else {
|
||||||
hash = write_blob(repo_path, &file_path).context(format!(
|
hash = self.write_blob(&file_path).context(format!(
|
||||||
"could not write object {:?}",
|
"could not write object {:?}",
|
||||||
file_path.file_name()
|
file_path.file_name()
|
||||||
))?;
|
))?;
|
||||||
@ -48,5 +57,6 @@ pub fn write_tree(repo_path: &PathBuf, path: &PathBuf) -> Result<[u8; 20]> {
|
|||||||
out.extend_from_slice(&entry.hash);
|
out.extend_from_slice(&entry.hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
write_object(repo_path, Kind::Tree, &out).context("Write")
|
self.write_object(Kind::Tree, &out).context("Write")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user