diff --git a/Cargo.lock b/Cargo.lock index e6c2f96..497769b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -135,6 +135,12 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "either" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" + [[package]] name = "flate2" version = "1.0.28" @@ -165,6 +171,8 @@ dependencies = [ "chrono", "flate2", "hex", + "indoc", + "itertools", "sha1", ] @@ -197,6 +205,21 @@ dependencies = [ "cc", ] +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "js-sys" version = "0.3.67" diff --git a/Cargo.toml b/Cargo.toml index 2294fb3..d324ee3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,8 @@ byteorder = "1.5.0" bpaf = "0.9.8" anyhow = "1.0.79" chrono = "0.4.31" +itertools = "0.12.1" +indoc = "2.0.5" [lints.clippy] pedantic = "deny" diff --git a/src/command/add.rs b/src/command/add.rs index fe822d5..314c6c4 100644 --- a/src/command/add.rs +++ b/src/command/add.rs @@ -1,66 +1,47 @@ use core::panic; use std::collections; +use std::ffi::OsStr; use std::fs::{self, File}; use std::io::{self, Read, Write}; use std::os::linux::fs::MetadataExt; +use std::os::unix::ffi::OsStrExt; +use std::path::{Path, PathBuf}; use byteorder::{BigEndian, ByteOrder}; -use hex; use crate::util; -struct IndexHeader { - signature: [u8; 4], - version: [u8; 4], - entries: [u8; 4], -} - -struct IndexEntry { - ctime: [u8; 4], - ctime_nsec: [u8; 4], - mtime: [u8; 4], - mtime_nsec: [u8; 4], - dev: [u8; 4], - ino: [u8; 4], - mode: [u8; 4], - uid: [u8; 4], - gid: [u8; 4], - file_size: [u8; 4], - oid: String, // 20byte - flags: [u8; 2], - path: String, -} - fn travel_dir( - file_name: &str, - file_path_list: &mut Vec, + file_name: impl AsRef, + file_path_list: &mut Vec, hash_list: &mut Vec, ) -> io::Result<()> { - if fs::metadata(file_name)?.is_dir() { - // 再帰的にaddする - for entry in fs::read_dir(file_name)? { - let path = entry?.path(); - if path.starts_with("./.git") { - continue; - } - let file_name = path.to_str().unwrap().to_string(); - if path.is_dir() { - travel_dir(&file_name, file_path_list, hash_list)?; - continue; - } - let hash = generate_blob_object(&file_name)?; - file_path_list.push(file_name); - hash_list.push(hash); + if !fs::metadata(&file_name)?.is_dir() { + let hash = generate_blob_object(&file_name)?; + file_path_list.push(file_name.as_ref().to_path_buf()); + hash_list.push(hash); + return Ok(()); + } + + // 再帰的にaddする + for entry in fs::read_dir(file_name)? { + let path = entry?.path(); + if path.starts_with("./.git") { + continue; } - } else { - let hash = generate_blob_object(file_name)?; - file_path_list.push(file_name.to_string()); + + if path.is_dir() { + travel_dir(&path, file_path_list, hash_list)?; + continue; + } + let hash = generate_blob_object(&path)?; + file_path_list.push(path); hash_list.push(hash); } Ok(()) } -pub fn add(file_names: &[String]) -> anyhow::Result<()> { +pub fn add(file_names: &[PathBuf]) -> anyhow::Result<()> { let mut hash_list = Vec::new(); let mut file_path_list = Vec::new(); for file_name in file_names { @@ -69,7 +50,7 @@ pub fn add(file_names: &[String]) -> anyhow::Result<()> { update_index(&file_path_list, &hash_list) } -fn generate_blob_object(file_name: &str) -> Result { +fn generate_blob_object(file_name: impl AsRef) -> Result { let contents = fs::read_to_string(file_name)?; let file_length = contents.len(); @@ -95,7 +76,7 @@ fn generate_blob_object(file_name: &str) -> Result { #[derive(Clone)] struct IndexEntrySummary { index_entry: Vec, - path: String, + path: PathBuf, } // 既存のentriesと新しく追加されるentriesをmergeする @@ -130,9 +111,9 @@ fn merge_entries( result } -fn decode_index_file() -> anyhow::Result>> { +fn decode_index_file() -> Option> { let Ok(mut file) = File::open(".git/index") else { - return Ok(None); + return None; }; let mut content = Vec::new(); let mut index_entry_summaries = Vec::::new(); @@ -142,89 +123,83 @@ fn decode_index_file() -> anyhow::Result>> { let entry_count = BigEndian::read_u32(&content[8..12]); let mut entries = &content[12..]; for _ in 0..entry_count { - let (next_byte, index_entry_summary) = decode_index_entry(entries)?; + let (next_byte, index_entry_summary) = decode_index_entry(entries); index_entry_summaries.push(index_entry_summary); entries = &entries[next_byte..]; } - Ok(Some(index_entry_summaries)) + Some(index_entry_summaries) } -fn decode_index_entry(entry: &[u8]) -> Result<(usize, IndexEntrySummary), std::str::Utf8Error> { +fn decode_index_entry(entry: &[u8]) -> (usize, IndexEntrySummary) { let flags = BigEndian::read_u16(&entry[60..62]); let file_path_end_byte = (62 + flags) as usize; - let path = std::str::from_utf8(&entry[62..file_path_end_byte])?; + let path: &Path = OsStr::from_bytes(&entry[62..file_path_end_byte]).as_ref(); let padding = 4 - (file_path_end_byte % 4); let next_byte = file_path_end_byte + padding; let index_entry_summary = IndexEntrySummary { index_entry: entry[..next_byte].to_vec(), - path: path.to_string(), + path: path.to_path_buf(), }; - Ok((next_byte, index_entry_summary)) + (next_byte, index_entry_summary) } -fn update_index(file_names: &[String], hash_list: &[String]) -> anyhow::Result<()> { +fn update_index(file_names: &[PathBuf], hash_list: &[String]) -> anyhow::Result<()> { // 既にindex fileが存在したらそれを読み込み、entriesをdecode // headerは新しく作る(entryの数が違うため) // 更新されるファイルのentries - let exists = decode_index_file()?; + let exists = decode_index_file(); // 新しく追加されるファイルのentries let mut new_entries = Vec::::new(); for (index, file_name) in file_names.iter().enumerate() { - let mut content: Vec = Vec::new(); let metadata = fs::metadata(file_name)?; - let new_file_name = match file_name.strip_prefix("./") { - Some(file_name) => file_name, - None => file_name, - }; - // スライスで長さが保証できているのでunwrapのまま - let index_entry = IndexEntry { - ctime: metadata.st_ctime().to_be_bytes()[4..8].try_into().unwrap(), - ctime_nsec: metadata.st_ctime_nsec().to_be_bytes()[4..8] - .try_into() - .unwrap(), - mtime: metadata.st_mtime().to_be_bytes()[4..8].try_into().unwrap(), - mtime_nsec: metadata.st_mtime_nsec().to_be_bytes()[4..8] - .try_into() - .unwrap(), - dev: metadata.st_dev().to_be_bytes()[4..8].try_into().unwrap(), - ino: metadata.st_ino().to_be_bytes()[4..8].try_into().unwrap(), - mode: metadata.st_mode().to_be_bytes(), - uid: metadata.st_uid().to_be_bytes(), - gid: metadata.st_gid().to_be_bytes(), - file_size: metadata.st_size().to_be_bytes()[4..8].try_into().unwrap(), - oid: hash_list[index].clone(), - // TODO: 正しく計算 - flags: new_file_name.len().to_be_bytes()[6..8].try_into().unwrap(), - path: new_file_name.to_string(), - }; + let new_file_name = &file_name.strip_prefix("./").unwrap_or(file_name); + let change_time = &metadata.st_ctime().to_be_bytes()[4..8]; + let change_time_nsec = &metadata.st_ctime_nsec().to_be_bytes()[4..8]; + let modification_time = &metadata.st_mtime().to_be_bytes()[4..8]; + let modification_time_nsec = &metadata.st_mtime_nsec().to_be_bytes()[4..8]; + let dev = &metadata.st_dev().to_be_bytes()[4..8]; + let ino = &metadata.st_ino().to_be_bytes()[4..8]; + let mode = &metadata.st_mode().to_be_bytes(); + let user_id = &metadata.st_uid().to_be_bytes(); + let group_id = &metadata.st_gid().to_be_bytes(); + let file_size = &metadata.st_size().to_be_bytes()[4..8]; + let oid = &hash_list[index]; + let decoded_oid = hex::decode(oid)?; + let decoded_oid_slice = decoded_oid.as_slice(); + // TODO: 正しく計算 + let flags = &new_file_name.as_os_str().len().to_be_bytes()[6..8]; + let path = new_file_name.to_path_buf(); + + let mut content: Vec = [ + change_time, + change_time_nsec, + modification_time, + modification_time_nsec, + dev, + ino, + mode, + user_id, + group_id, + file_size, + decoded_oid_slice, + flags, + path.as_os_str().as_bytes(), + ] + .concat(); - content.extend(index_entry.ctime.to_vec()); - content.extend(index_entry.ctime_nsec.to_vec()); - content.extend(index_entry.mtime.to_vec()); - content.extend(index_entry.mtime_nsec.to_vec()); - content.extend(index_entry.dev.to_vec()); - content.extend(index_entry.ino.to_vec()); - content.extend(index_entry.mode.to_vec()); - content.extend(index_entry.uid.to_vec()); - content.extend(index_entry.gid.to_vec()); - content.extend(index_entry.file_size.to_vec()); - let decoded_oid = hex::decode(&index_entry.oid)?; - content.extend(decoded_oid); - content.extend(index_entry.flags.to_vec()); - content.extend(index_entry.path.as_bytes().to_vec()); let padding = 4 - (content.len() % 4); content.resize(content.len() + padding, 0); let index_entry_summary = IndexEntrySummary { - index_entry: content.clone(), - path: index_entry.path.to_string(), + index_entry: content, + path, }; new_entries.push(index_entry_summary); } @@ -234,16 +209,12 @@ fn update_index(file_names: &[String], hash_list: &[String]) -> anyhow::Result<( None => new_entries, }; - let mut contents: Vec = Vec::new(); // header - let index_header = IndexHeader { - signature: "DIRC".as_bytes().try_into().unwrap(), - version: 2u32.to_be_bytes(), - entries: merged_entries.len().to_be_bytes()[4..8].try_into().unwrap(), - }; - contents.extend(index_header.signature.to_vec()); - contents.extend(index_header.version.to_vec()); - contents.extend(index_header.entries.to_vec()); + let signature = b"DIRC"; + let version = &2u32.to_be_bytes(); + let entrie_count = &merged_entries.len().to_be_bytes()[4..8]; + + let mut contents: Vec = [signature, version, entrie_count].concat(); // entries for entry in merged_entries { diff --git a/src/command/commit.rs b/src/command/commit.rs index 568d1dd..367a9cb 100644 --- a/src/command/commit.rs +++ b/src/command/commit.rs @@ -5,11 +5,12 @@ use std::{fs::File, io::Write}; use byteorder::{BigEndian, ByteOrder}; use chrono::Local; use hex; +use itertools::Itertools; use crate::object::commit::{Commit, Sign}; use crate::util; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] enum NodeType { Blob, Tree, @@ -54,28 +55,29 @@ fn travel_tree(node: &mut Node, path: &[&std::ffi::OsStr], children_mode: u32, h return; } - // TODO: let-elseの方が平坦になる - if let Some((first, rest)) = path.split_first() { - if let Some(child_node) = node - .children - .iter_mut() - .find(|child| child.name == first.to_str().unwrap()) - { - // childrenにディレクトリがある場合はそのまま移動 - travel_tree(child_node, rest, children_mode, hash); - } else { - // ない場合は作成して追加して移動 - let new_node = Node { - r#type: NodeType::Tree, - mode: 0o04_0000, - name: first.to_str().unwrap().to_string(), - hash: String::new(), - children: Vec::new(), - }; - node.children.push(new_node); - let new_node = node.children.last_mut().unwrap(); - travel_tree(new_node, rest, children_mode, hash); - } + let Some((first, rest)) = path.split_first() else { + return; + }; + + if let Some(child_node) = node + .children + .iter_mut() + .find(|child| child.name == first.to_str().unwrap()) + { + // childrenにディレクトリがある場合はそのまま移動 + travel_tree(child_node, rest, children_mode, hash); + } else { + // ない場合は作成して追加して移動 + let new_node = Node { + r#type: NodeType::Tree, + mode: 0o04_0000, + name: first.to_str().unwrap().to_string(), + hash: String::new(), + children: Vec::new(), + }; + node.children.push(new_node); + let new_node = node.children.last_mut().unwrap(); + travel_tree(new_node, rest, children_mode, hash); } } @@ -154,15 +156,10 @@ fn generate_tree_object(node: &Node) -> anyhow::Result { fn generate_tree_objects(index_tree: &mut Node) -> anyhow::Result<()> { // childrenを左から探索していく深さ優先探索 for child in &mut index_tree.children { - match child.r#type { - NodeType::Blob => { - // blobの場合は何もしない - } - NodeType::Tree => { - // treeの場合は再帰的に呼び出す - generate_tree_objects(child)?; - } + if child.r#type == NodeType::Blob { + continue; } + generate_tree_objects(child)?; } let hash = generate_tree_object(index_tree)?; index_tree.hash = hash; @@ -173,7 +170,7 @@ fn generate_commit_object(tree_hash: String, message: String) -> anyhow::Result< let parent = util::path::get_head_commit_hash(); let now = Local::now(); - let mut commit = Commit { + let commit = Commit { hash: String::new(), size: 0, tree: tree_hash, @@ -194,35 +191,42 @@ fn generate_commit_object(tree_hash: String, message: String) -> anyhow::Result< message, }; - let mut content: Vec = Vec::new(); - content.extend(format!("tree {}\n", commit.tree).as_bytes()); - for parent in commit.parents { - content.extend(format!("parent {parent}\n").as_bytes()); - } - content.extend(format!("author {}\n", commit.author).as_bytes()); - content.extend(format!("committer {}\n", commit.commiter).as_bytes()); - content.extend(format!("\n{}\n", commit.message).as_bytes()); + let Commit { + tree, + parents, + author, + commiter, + message, + .. + } = &commit; + let parents: String = parents.iter().map(|p| format!("\nparent {p}")).join(""); + + let content = indoc::formatdoc! {r#" + tree {tree}{parents} + author {author} + commiter {commiter} - commit.size = content.len(); - let header = format!("commit {}\0", commit.size); - let content = format!("{}{}", header, String::from_utf8(content)?); + {message} + "#}; + + let header = format!("commit {}\0", content.len()); + let content = format!("{header}{content}"); let commit_hash = util::compress::hash(content.as_bytes()); - commit.hash = commit_hash; - - let file_directory = format!(".git/objects/{}", &commit.hash[0..2]); - let file_path = format!("{}/{}", file_directory, &commit.hash[2..]); - match std::fs::create_dir(file_directory) { - Ok(()) => {} - Err(ref e) if e.kind() == ErrorKind::AlreadyExists => {} - Err(e) => panic!("{}", e), - } + + let file_directory = format!(".git/objects/{}", &commit_hash[0..2]); + let file_path = format!("{}/{}", file_directory, &commit_hash[2..]); + // TODO: create_nested_fileでいい気がする + std::fs::create_dir(file_directory).or_else(|e| match e.kind() { + ErrorKind::AlreadyExists => Ok(()), + _ => Err(e), + })?; let mut file = File::create(file_path)?; // zlib圧縮 let compressed_contents = util::compress::with_zlib(content.as_bytes())?; file.write_all(&compressed_contents)?; - Ok(commit.hash) + Ok(commit_hash) } fn update_head(commit_hash: &str) -> std::io::Result<()> { diff --git a/src/main.rs b/src/main.rs index 439c2c9..ac0469a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::env; +use std::{env, path::PathBuf}; mod command; mod object; @@ -7,20 +7,10 @@ mod util; #[derive(Debug, Clone)] enum Command { Init, - Add { - files: Vec, /* it's better to use PathBuf here! */ - }, - Commit { - message: String, - }, - Branch { - name: String, - delete: bool, - }, - Checkout { - name: String, - new_branch: bool, - }, + Add { files: Vec }, + Commit { message: String }, + Branch { name: String, delete: bool }, + Checkout { name: String, new_branch: bool }, Log, }