diff --git a/Cargo.lock b/Cargo.lock index 08b95b9..92b803e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "block-buffer" version = "0.10.4" @@ -152,6 +158,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "heck" version = "0.5.0" @@ -164,6 +176,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "injectorpp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d377a64bbe42f7a086ed630fbc66d84b43944f278ef42de53af79aaec6c21687" +dependencies = [ + "libc", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -204,8 +225,11 @@ dependencies = [ name = "rgit" version = "0.1.0" dependencies = [ + "anyhow", "clap", + "glob", "hex", + "injectorpp", "sha1", "zlib-rs", ] diff --git a/Cargo.toml b/Cargo.toml index 5e77de2..d910bb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,11 +4,13 @@ version = "0.1.0" description = "Git implementation in Rust" edition = "2024" -[toolchain] -channel = "nightly" - [dependencies] +anyhow = "1.0.102" clap = { version = "4.5.59", features = ["derive"] } +glob = "0.3.3" hex = "0.4.3" sha1 = "0.10.6" zlib-rs = "0.6.1" + +[dev-dependencies] +injectorpp = "0.4.0" \ No newline at end of file diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..271800c --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" \ No newline at end of file diff --git a/src/git_fs/gitignore.rs b/src/git_fs/gitignore.rs new file mode 100644 index 0000000..3491d53 --- /dev/null +++ b/src/git_fs/gitignore.rs @@ -0,0 +1,396 @@ +use anyhow::{Context, Result}; +use glob::{MatchOptions, Pattern}; +use std::{ + fmt::Debug, + path::{Component, Path, PathBuf}, +}; + +use crate::{ + GIT_DIR, + git_fs::{normalize_path_in_worktree, read_lines}, +}; + +use super::get_worktree_root; + +const MATCH_OPTIONS: MatchOptions = MatchOptions { + case_sensitive: true, + require_literal_separator: true, + require_literal_leading_dot: false, +}; + +#[derive(Clone)] +struct GitIgnoreRule { + pattern: Pattern, + _inverted: bool, + only_dirs: bool, + relative: bool, +} + +impl PartialEq for GitIgnoreRule { + fn eq(&self, other: &Self) -> bool { + self.pattern == other.pattern + && self._inverted == other._inverted + && self.only_dirs == other.only_dirs + && self.relative == other.relative + } +} + +impl Debug for GitIgnoreRule { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GitIgnoreRule") + .field("pattern", &self.pattern.as_str()) + .field("_inverted", &self._inverted) + .field("only_dirs", &self.only_dirs) + .field("relative", &self.relative) + .finish() + } +} + +impl Default for GitIgnoreRule { + fn default() -> Self { + GitIgnoreRule { + pattern: Pattern::new("").unwrap(), + _inverted: false, + only_dirs: false, + relative: false, + } + } +} + +impl GitIgnoreRule { + fn match_path(&self, path: &Path) -> bool { + if self.only_dirs && !path.is_dir() { + return false; + } + + if self.relative { + return self.pattern.matches_path_with(path, MATCH_OPTIONS); + } + + path.components() + .filter_map(|comp| match comp { + Component::Normal(os_str) => os_str.to_str(), + _ => None, + }) + .any(|comp| self.pattern.matches_with(comp, MATCH_OPTIONS)) + } +} + +/// Returns a vector of PathBuf containing files expanded from a given path +/// Returned files are also filtered according to gitignore rules +pub fn expands_and_filter_path(path: PathBuf) -> Result> { + // Absolute rule + let mut rules = vec![ + GitIgnoreRule { + pattern: Pattern::new(GIT_DIR).unwrap(), + relative: true, + only_dirs: true, + _inverted: false, + }, + GitIgnoreRule { + pattern: Pattern::new(".git").unwrap(), + relative: true, + only_dirs: true, + _inverted: false, + }, + ]; + + let wt_root = get_worktree_root()?; + + if let Ok(r) = load_rules(&wt_root) { + rules.extend(r) + }; + + for comp in normalize_path_in_worktree(&path)? + .components() + .filter_map(|c| match c { + Component::Normal(os_str) => Some(os_str), + _ => None, + }) + { + if let Ok(r) = load_rules(&wt_root.join(comp)) { + rules.extend(r) + }; + } + + let expanded = expand_and_filter(path, rules.clone()); + + Ok(expanded) +} + +fn expand_and_filter(path: PathBuf, upper_rules: Vec) -> Vec { + for rule in upper_rules.iter() { + if rule.match_path(&normalize_path_in_worktree(&path).unwrap()) { + return Vec::new(); + } + } + + if path.is_dir() { + let mut local_rules = load_rules(&path).unwrap_or_default(); + local_rules.extend(upper_rules); + path.read_dir() + .expect("read_dir call failed") + .flat_map(|child| match child { + Ok(c) => expand_and_filter(c.path().to_path_buf(), local_rules.clone()), + Err(_) => Vec::new(), + }) + .collect() + } else { + vec![path] + } +} + +fn parse_rule(raw: String, rel_to: &Path) -> Option { + if raw.starts_with("#") { + return None; + } + + let mut trimmed = raw.trim(); + + if trimmed.is_empty() { + return None; + } + + let inverted = trimmed.starts_with("!"); + if inverted { + trimmed = &trimmed[1..]; + } + + let only_dirs = trimmed.ends_with("/"); + if only_dirs { + trimmed = &trimmed[..trimmed.len() - 1] + } + + let start_relative = trimmed.starts_with("/"); + if start_relative { + trimmed = &trimmed[1..]; + } + + let relative = start_relative || trimmed.contains("/"); + let pattern_str = if relative { + String::from(rel_to.with_trailing_sep().to_str().unwrap()) + trimmed + } else { + String::from(trimmed) + }; + + let pattern = Pattern::new(&pattern_str).ok(); + + pattern.map(|pattern| GitIgnoreRule { + pattern, + _inverted: inverted, + only_dirs, + relative, + }) +} + +fn load_rules(rel_to: &Path) -> Result> { + println!("Loading rule for {}", rel_to.display()); + let path = rel_to.join(".gitignore"); + let n_rel_to = normalize_path_in_worktree(rel_to)?; + + Ok(read_lines(&path) + .with_context(|| format!("Reading from {}", path.display()))? + .map_while(Result::ok) + .filter_map(|l| parse_rule(l, &n_rel_to)) + .collect()) +} + +#[cfg(test)] +mod tests { + use super::*; + use injectorpp::interface::injector::*; + use std::path::Path; + + #[test] + fn test_parse_rule() { + let rel_to = Path::new("").to_path_buf(); + assert!(parse_rule(String::from("# Some comment"), &rel_to).is_none()); + assert!(parse_rule(String::from(""), &rel_to).is_none()); + assert!(parse_rule(String::from(" "), &rel_to).is_none()); + + let test_data = [ + ( + "absolute_dir/", + GitIgnoreRule { + pattern: Pattern::new("absolute_dir").unwrap(), + only_dirs: true, + _inverted: false, + relative: false, + }, + ), + ( + "relative/dir/", + GitIgnoreRule { + pattern: Pattern::new("relative/dir").unwrap(), + only_dirs: true, + _inverted: false, + relative: true, + }, + ), + ( + "absolute_file", + GitIgnoreRule { + pattern: Pattern::new("absolute_file").unwrap(), + only_dirs: false, + _inverted: false, + relative: false, + }, + ), + ( + "relative/file", + GitIgnoreRule { + pattern: Pattern::new("relative/file").unwrap(), + only_dirs: false, + _inverted: false, + relative: true, + }, + ), + ( + "!_inverted", + GitIgnoreRule { + pattern: Pattern::new("_inverted").unwrap(), + only_dirs: false, + _inverted: true, + relative: false, + }, + ), + ( + "/relative", + GitIgnoreRule { + pattern: Pattern::new("relative").unwrap(), + only_dirs: false, + _inverted: false, + relative: true, + }, + ), + ( + "!/relative", + GitIgnoreRule { + pattern: Pattern::new("relative").unwrap(), + only_dirs: false, + _inverted: true, + relative: true, + }, + ), + ]; + + for (raw, expected) in test_data { + let rule = parse_rule(String::from(raw), &rel_to).unwrap(); + + debug_assert_eq!(rule, expected); + } + } + + #[test] + fn test_parse_rule_with_rel_to() { + let rel_to = Path::new("src").to_path_buf(); + + let test_data = [ + ( + "/some_file.c", + GitIgnoreRule { + pattern: Pattern::new("src/some_file.c").unwrap(), + only_dirs: false, + _inverted: false, + relative: true, + }, + ), + ( + "some_file.c", + GitIgnoreRule { + pattern: Pattern::new("some_file.c").unwrap(), + only_dirs: false, + _inverted: false, + relative: false, + }, + ), + ]; + + for (raw, expected) in test_data { + let rule = parse_rule(String::from(raw), &rel_to).unwrap(); + + debug_assert_eq!(rule, expected); + } + } + + #[test] + fn test_rule_matching_non_relative() { + let rule1 = GitIgnoreRule { + pattern: Pattern::new("some_file.c").unwrap(), + ..Default::default() + }; + let rule2 = GitIgnoreRule { + pattern: Pattern::new("*.o").unwrap(), + ..Default::default() + }; + let rule3 = GitIgnoreRule { + pattern: Pattern::new("some_file.*").unwrap(), + ..Default::default() + }; + + let path1 = Path::new("some_file.c").to_path_buf(); + let path2 = Path::new("another_file.c").to_path_buf(); + let path3 = Path::new("some_file.o").to_path_buf(); + let path4 = Path::new("another_file.o").to_path_buf(); + let path5 = Path::new("src/some_file.c").to_path_buf(); + + assert!(rule1.match_path(&path1)); + assert!(!rule1.match_path(&path2)); + assert!(!rule1.match_path(&path3)); + assert!(!rule1.match_path(&path4)); + assert!(rule1.match_path(&path5)); + + assert!(!rule2.match_path(&path1)); + assert!(!rule2.match_path(&path2)); + assert!(rule2.match_path(&path3)); + assert!(rule2.match_path(&path4)); + + assert!(rule3.match_path(&path1)); + assert!(!rule3.match_path(&path2)); + assert!(rule3.match_path(&path3)); + assert!(!rule3.match_path(&path4)); + assert!(rule3.match_path(&path5)); + } + + #[test] + fn test_rule_matching_relative() { + let rule1 = GitIgnoreRule { + pattern: Pattern::new("some_file").unwrap(), + relative: true, + ..Default::default() + }; + let rule2 = GitIgnoreRule { + pattern: Pattern::new("src/ignored").unwrap(), + relative: true, + ..Default::default() + }; + + let path1 = Path::new("some_file").to_path_buf(); + let path2 = Path::new("target/some_file").to_path_buf(); + let path3 = Path::new("src/ignored").to_path_buf(); + let path4 = Path::new("target/src/ignored").to_path_buf(); + + assert!(rule1.match_path(&path1)); + assert!(!rule1.match_path(&path2)); + assert!(rule2.match_path(&path3)); + assert!(!rule2.match_path(&path4)); + } + + #[test] + fn test_rule_matching_dir() { + let rule1 = GitIgnoreRule { + pattern: Pattern::new("some_folder").unwrap(), + only_dirs: true, + ..Default::default() + }; + + let path1 = Path::new("some_folder").to_path_buf(); + assert!(!rule1.match_path(&path1)); + + let mut injector = InjectorPP::new(); + injector + .when_called(injectorpp::func!(fn (Path::is_dir)(&Path) -> bool)) + .will_return_boolean(true); + assert!(rule1.match_path(&path1)); + } +} diff --git a/src/git_fs/head.rs b/src/git_fs/head.rs index 201f8c9..7e2ccda 100644 --- a/src/git_fs/head.rs +++ b/src/git_fs/head.rs @@ -1,11 +1,7 @@ -use std::{ - fs::File, - io::prelude::*, - path::Path, -}; +use anyhow::{Result, bail}; +use std::{fs::File, io::prelude::*}; -use crate::GIT_DIR; -use crate::git_fs::read_lines; +use crate::git_fs::{get_git_root, read_lines}; #[derive(Debug)] pub struct Head { @@ -13,39 +9,34 @@ pub struct Head { } impl Head { - pub fn load() -> Result { - let path = Path::new(GIT_DIR).join("HEAD"); + pub fn load() -> Result { + let path = get_git_root()?.join("HEAD"); - let mut lines = match read_lines(path) { - Err(_) => return Err(()), - Ok(lines) => lines, - }; + let mut lines = read_lines(path)?; let content = match lines.next() { - Some(Ok(line)) => line, - _ => return Err(()), + Some(line) => line?, + _ => bail!("HEAD is empty"), }; match content.split_once(": ") { - None => Err(()), - Some((_, r)) => Ok(Head { ref_to: String::from(r) }) + None => bail!("HEAD has invalid format"), + Some((_, r)) => Ok(Head { + ref_to: String::from(r), + }), } } - pub fn save(&self) -> Result<(), ()> { - let path = Path::new(GIT_DIR).join("HEAD"); + pub fn save(&self) -> Result<()> { + let path = get_git_root()?.join("HEAD"); let mut content = String::from("ref: "); content.push_str(&self.ref_to); - content.push_str("\n"); + content.push('\n'); - let mut file = match File::create(&path) { - Err(_) => return Err(()), - Ok(file) => file, - }; + let mut file = File::create(&path)?; - match file.write(content.as_bytes()) { - Err(_) => Err(()), - Ok(_) => Ok(()), - } + file.write_all(content.as_bytes())?; + + Ok(()) } } diff --git a/src/git_fs/index.rs b/src/git_fs/index.rs index 05bfcef..2b98784 100644 --- a/src/git_fs/index.rs +++ b/src/git_fs/index.rs @@ -1,56 +1,421 @@ -use std::{ - io::{ - self, - Error, - }, -}; +use anyhow::Result; +use anyhow::bail; +use sha1::{Digest, Sha1}; +use std::fs; +use std::{ffi::CStr, fmt::Display, fs::File, io::prelude::*, os::unix::fs::PermissionsExt}; -use crate::GIT_DIR; +use crate::git_fs::get_git_root; -#[derive(Debug)] +#[derive(Clone, Debug, PartialEq)] +pub enum ObjectType { + Regular, + SymLink, + GitLink, +} + +impl Display for ObjectType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self { + Self::Regular => write!(f, "Regular"), + Self::SymLink => write!(f, "Symlink"), + Self::GitLink => write!(f, "Gitlink"), + } + } +} + +#[derive(Clone, Debug, PartialEq)] pub struct IndexEntry { - file_size: u32, + object_type: ObjectType, + permissions: u16, hash: [u8; 20], name: String, } impl IndexEntry { - fn from_bytes(bytes: Vec) -> io::Result { + fn from_bytes(bytes: &[u8]) -> Result<(Self, usize)> { + let mode = &bytes[24..28]; + let ot_bin = mode[2] >> 4; + let object_type = match ot_bin { + 0b1000 => ObjectType::Regular, + 0b1010 => ObjectType::SymLink, + 0b1110 => ObjectType::GitLink, + _ => bail!("Invalid object type: {}", ot_bin), + }; + let permissions = u16::from_be_bytes(mode[2..4].try_into()?) & !0xFE00; + match (permissions, &object_type) { + (0, ObjectType::GitLink) => (), + (0, ObjectType::SymLink) => (), + (0o755, ObjectType::Regular) => (), + (0o644, ObjectType::Regular) => (), + _ => bail!( + "Invalid permissions (0o{:o}) for type {}", + permissions, + object_type + ), + }; - return Err(Error::other("Not implemented")) + let hash: [u8; 20] = bytes[40..60].try_into()?; + let cname = CStr::from_bytes_until_nul(&bytes[62..])?; + let name = String::from(cname.to_str()?); + + let entry_size = usize::div_ceil(62 + cname.count_bytes(), 8) * 8; + + Ok(( + IndexEntry { + object_type, + permissions, + hash, + name, + }, + entry_size, + )) + } + + fn to_bytes(&self) -> Result> { + let mut bytes = Vec::new(); + + // ctime + bytes.extend([0, 0, 0, 0]); + // ctime nano + bytes.extend([0, 0, 0, 0]); + // mtime + bytes.extend([0, 0, 0, 0]); + // mtime nano + bytes.extend([0, 0, 0, 0]); + // dev + bytes.extend([0, 0, 0, 0]); + // ino + bytes.extend([0, 0, 0, 0]); + + // mode + let ot_bin: u16 = match self.object_type { + ObjectType::Regular => 0b1000, + ObjectType::SymLink => 0b1010, + ObjectType::GitLink => 0b1110, + }; + let perms: u16 = self.permissions | (ot_bin << 12); + + bytes.extend([0, 0]); + bytes.extend(perms.to_be_bytes()); + + // uid + bytes.extend([0, 0, 0, 0]); + // gid + bytes.extend([0, 0, 0, 0]); + // file size + bytes.extend([0, 0, 0, 0]); + // object name + bytes.extend(self.hash); + // flags + let flags = u16::min(0xFFF, self.name.len() as u16); + bytes.extend(flags.to_be_bytes()); + // entry path name + bytes.extend(self.name.as_bytes()); + + let padding = ((bytes.len() + 1).div_ceil(8) * 8) - bytes.len(); + bytes.extend(vec![0; padding]); + + Ok(bytes) } } -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub struct Index { version: u32, entries: Vec, } -impl Index { - pub fn from_bytes(bytes: Vec) -> io::Result { - let magic: [u8; 4] = match bytes.first_chunk() { - None => return Err(Error::other("Error parsing index")), - Some(magic) => *magic, - }; - - match str::from_utf8(&magic) { - Ok("DIRC") => (), - _ => return Err(Error::other("Invalid index")) - }; - - let version_bytes = <[u8; 4]>::try_from(&bytes.as_slice()[4..8]).unwrap(); - let version = u32::from_be_bytes(version_bytes); - - let count_bytes = <[u8; 4]>::try_from(&bytes.as_slice()[8..12]).unwrap(); - let count = u32::from_be_bytes(count_bytes); - - let entries: Vec = Vec::with_capacity(usize::try_from(count).unwrap()); - - for i in 0..=count { - - }; - - return Ok(Index { version, entries}); +impl Default for Index { + fn default() -> Self { + Index { + version: 2, + entries: Vec::new(), + } + } +} + +impl Index { + pub fn load() -> Result { + let path = get_git_root()?.join("index"); + + if !path.exists() { + return Ok(Index { + version: 2u32, + entries: Vec::new(), + }); + } + + let mut file = File::open(&path)?; + let mut content: Vec = Vec::new(); + file.read_to_end(&mut content)?; + + Index::from_bytes(content) + } + + pub fn save(&self) -> Result<()> { + let path = get_git_root()?.join("index"); + let mut file = File::create(&path)?; + file.write_all(&self.to_bytes()?)?; + Ok(()) + } + + fn insert_entry(&mut self, entry: IndexEntry) { + match self + .entries + .binary_search_by_key(&entry.name, |e| e.name.clone()) + { + Ok(pos) => self.entries[pos] = entry, + Err(pos) => self.entries.insert(pos, entry), + } + } + + pub fn add_file(&mut self, name: String, hash: [u8; 20]) -> Result<()> { + let metadata = fs::metadata(&name)?; + if metadata.is_dir() { + bail!("Cannot add a directory to index") + }; + + let object_type = if metadata.is_symlink() { + ObjectType::SymLink + } else { + ObjectType::Regular + }; + + let permissions = (metadata.permissions().mode() as u16) & !0xFE00; + let entry = IndexEntry { + name, + hash, + object_type, + permissions, + }; + + self.insert_entry(entry); + + Ok(()) + } + + pub fn remove_file(&mut self, name: String) -> Result<()> { + let entry = IndexEntry { + name: name.clone(), + hash: [0u8; 20], + object_type: ObjectType::Regular, + permissions: 0, + }; + match self + .entries + .binary_search_by_key(&entry.name, |a| a.name.clone()) + { + Ok(pos) => self.entries.remove(pos), + Err(_) => bail!("No file {} in index", name), + }; + + Ok(()) + } + + fn from_bytes(bytes: Vec) -> Result { + match &bytes[0..4] { + b"DIRC" => (), + _ => bail!("Invalid index signature"), + }; + + let version = u32::from_be_bytes(bytes[4..8].try_into()?); + let count = u32::from_be_bytes(bytes[8..12].try_into()?); + + let mut entries: Vec = Vec::with_capacity(usize::try_from(count)?); + + let mut offset = 12; + + for _i in 0..count { + let (entry, entry_size) = IndexEntry::from_bytes(&bytes[offset..])?; + offset += entry_size; + entries.push(entry); + } + + let mut hasher = Sha1::new(); + hasher.update(&bytes[..offset]); + let hash = hasher.finalize(); + + if bytes[offset..] != *hash { + bail!("Index does not match its checksum") + } + Ok(Index { version, entries }) + } + + fn to_bytes(&self) -> Result> { + let mut bytes: Vec = Vec::new(); + bytes.extend("DIRC".as_bytes()); + bytes.extend(self.version.to_be_bytes()); + let count: u32 = self.entries.len() as u32; + bytes.extend(count.to_be_bytes()); + + for entry in &self.entries { + bytes.extend(entry.to_bytes()?); + } + + let mut hasher = Sha1::new(); + hasher.update(&bytes); + let hash = hasher.finalize(); + + bytes.extend(hash); + + Ok(bytes) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn index_entry_to_bytes() { + let entry1 = IndexEntry { + object_type: ObjectType::Regular, + permissions: 0o644u16, + hash: [0x19u8; 20], + name: String::from("src/git_fs/head.rs"), + }; + let expected1: Vec = vec![ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, 0xa4, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0x19, + 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, + 0x19, 0x19, 0x19, 0x19, 0x00, 0x12, 0x73, 0x72, 0x63, 0x2f, 0x67, 0x69, 0x74, 0x5f, + 0x66, 0x73, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x2e, 0x72, 0x73, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + ]; + + let entry2 = IndexEntry { + object_type: ObjectType::GitLink, + permissions: 0, + hash: [0x19u8; 20], + name: String::from("src/git_fs/head.r"), + }; + let expected2: Vec = vec![ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0x19, + 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, + 0x19, 0x19, 0x19, 0x19, 0x00, 0x11, 0x73, 0x72, 0x63, 0x2f, 0x67, 0x69, 0x74, 0x5f, + 0x66, 0x73, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x2e, 0x72, 0x00, + ]; + + let entry3 = IndexEntry { + object_type: ObjectType::SymLink, + permissions: 0, + hash: [0x19u8; 20], + name: String::from("src/git_fs/head.rst"), + }; + let expected3: Vec = vec![ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0x19, + 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, + 0x19, 0x19, 0x19, 0x19, 0x00, 0x13, 0x73, 0x72, 0x63, 0x2f, 0x67, 0x69, 0x74, 0x5f, + 0x66, 0x73, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x2e, 0x72, 0x73, 0x74, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + ]; + + assert_eq!(entry1.to_bytes().unwrap(), expected1); + assert_eq!(entry2.to_bytes().unwrap(), expected2); + assert_eq!(entry3.to_bytes().unwrap(), expected3); + } + + #[test] + fn index_entry_from_bytes() { + let expected1 = IndexEntry { + object_type: ObjectType::Regular, + permissions: 0o644u16, + hash: [0x19u8; 20], + name: String::from("src/git_fs/head.rs"), + }; + let (entry1, _) = IndexEntry::from_bytes(&vec![ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, 0xa4, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0x19, + 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, + 0x19, 0x19, 0x19, 0x19, 0x00, 0x12, 0x73, 0x72, 0x63, 0x2f, 0x67, 0x69, 0x74, 0x5f, + 0x66, 0x73, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x2e, 0x72, 0x73, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + ]) + .unwrap(); + + let expected2 = IndexEntry { + object_type: ObjectType::GitLink, + permissions: 0, + hash: [0x19u8; 20], + name: String::from("src/git_fs/head.r"), + }; + let (entry2, _) = IndexEntry::from_bytes(&vec![ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0x19, + 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, + 0x19, 0x19, 0x19, 0x19, 0x00, 0x11, 0x73, 0x72, 0x63, 0x2f, 0x67, 0x69, 0x74, 0x5f, + 0x66, 0x73, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x2e, 0x72, 0x00, + ]) + .unwrap(); + + let expected3 = IndexEntry { + object_type: ObjectType::SymLink, + permissions: 0, + hash: [0x19u8; 20], + name: String::from("src/git_fs/head.rst"), + }; + let (entry3, _) = IndexEntry::from_bytes(&vec![ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0x19, + 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, + 0x19, 0x19, 0x19, 0x19, 0x00, 0x13, 0x73, 0x72, 0x63, 0x2f, 0x67, 0x69, 0x74, 0x5f, + 0x66, 0x73, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x2e, 0x72, 0x73, 0x74, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + ]) + .unwrap(); + + assert_eq!(entry1, expected1); + assert_eq!(entry2, expected2); + assert_eq!(entry3, expected3); + } + + #[test] + fn index_insert_entry() { + let mut index = Index::default(); + + let entry1 = IndexEntry { + object_type: ObjectType::Regular, + permissions: 0o644u16, + hash: [0x19u8; 20], + name: String::from("a"), + }; + let entry2 = IndexEntry { + object_type: ObjectType::Regular, + permissions: 0o644u16, + hash: [0x19u8; 20], + name: String::from("b"), + }; + let entry3 = IndexEntry { + object_type: ObjectType::SymLink, + permissions: 0, + hash: [0x19u8; 20], + name: String::from(".hello"), + }; + let entry4 = IndexEntry { + object_type: ObjectType::Regular, + permissions: 0o644u16, + hash: [0x20u8; 20], + name: String::from("b"), + }; + + index.insert_entry(entry1.clone()); + index.insert_entry(entry2.clone()); + index.insert_entry(entry3.clone()); + index.insert_entry(entry4.clone()); + + let expected = Index { + entries: vec![entry3, entry1, entry4], + ..Default::default() + }; + + assert_eq!(index, expected); } } diff --git a/src/git_fs/mod.rs b/src/git_fs/mod.rs index 48cd265..66f57e6 100644 --- a/src/git_fs/mod.rs +++ b/src/git_fs/mod.rs @@ -1,62 +1,91 @@ +use anyhow::{Result, bail}; use std::{ fs::File, - io::{ - self, prelude::*, BufRead - }, path::Path, result + io::{self, BufRead, prelude::*}, + path::{Path, PathBuf}, }; use crate::GIT_DIR; +pub mod gitignore; pub mod head; pub mod index; pub mod object; -fn read_lines

(filename: P) -> io::Result>> -where P: AsRef { +fn read_lines

(filename: P) -> Result>> +where + P: AsRef, +{ let file = File::open(filename)?; Ok(io::BufReader::new(file).lines()) } +pub fn normalize_path_in_worktree(path: &Path) -> Result { + let wt_root = get_worktree_root()?; + let canon = path.canonicalize()?; + let mut canon_it = canon.components(); + + for wt_c in wt_root.components() { + if let Some(c) = canon_it.next() + && wt_c == c + { + continue; + } else { + bail!( + "'{}' is outside repository at '{}'", + path.display(), + wt_root.display() + ); + } + } + + Ok(canon_it.as_path().to_path_buf()) +} + +pub fn get_worktree_root() -> Result { + let canon = Path::new(".").canonicalize()?; + for ancestor in canon.ancestors() { + let potential = ancestor.join(GIT_DIR); + if potential.is_dir() { + return Ok(ancestor.to_path_buf()); + } + } + bail!("not a git repository (or any parent up to mount point)") +} + +pub fn get_git_root() -> Result { + Ok(get_worktree_root()?.join(GIT_DIR)) +} + pub struct Ref { ref_name: String, commit_hash: String, } impl Ref { - pub fn load(name: String) -> result::Result { - let path = Path::new(GIT_DIR).join(&name); + pub fn load(name: String) -> Result { + let path = get_git_root()?.join(&name); - let mut lines = match read_lines(path) { - Ok(lines) => lines, - Err(_) => return Err(()), - }; + let mut lines = read_lines(path)?; let content = match lines.next() { - Some(Ok(line)) => line, - _ => return Err(()), + Some(line) => line?, + _ => bail!(""), }; - Ok( - Ref { - ref_name: name, - commit_hash: content - } - ) + Ok(Ref { + ref_name: name, + commit_hash: content, + }) } - pub fn save(&self) -> result::Result<(), ()> { - let path = Path::new(GIT_DIR) - .join("refs") - .join(&self.ref_name); + pub fn save(&self) -> Result<()> { + let path = get_git_root()?.join("refs").join(&self.ref_name); - let mut file = match File::create(&path) { - Err(_) => return Err(()), - Ok(file) => file, - }; + let mut file = File::create(&path)?; - match file.write(self.commit_hash.as_bytes()) { - Err(_) => Err(()), - Ok(_) => Ok(()), - } + file.write_all(self.commit_hash.as_bytes())?; + + Ok(()) } } diff --git a/src/git_fs/object.rs b/src/git_fs/object.rs index 65a4f70..35a4be0 100644 --- a/src/git_fs/object.rs +++ b/src/git_fs/object.rs @@ -1,19 +1,17 @@ +use anyhow::Result; +use anyhow::bail; use hex::encode; -use sha1::{Sha1, Digest}; +use sha1::{Digest, Sha1}; use std::{ - fs::{create_dir_all, File}, - io::{ - self, - prelude::*, - Error, - }, + fs::{File, create_dir_all}, + io::prelude::*, path::Path, }; use zlib_rs::{ - compress_bound, compress_slice, decompress_slice, DeflateConfig, InflateConfig, ReturnCode + DeflateConfig, InflateConfig, ReturnCode, compress_bound, compress_slice, decompress_slice, }; -use crate::GIT_DIR; +use crate::git_fs::get_git_root; #[derive(Debug)] pub enum GitObjectType { @@ -22,17 +20,14 @@ pub enum GitObjectType { } impl GitObjectType { - pub fn load(name: String) -> io::Result { + pub fn load(name: String) -> Result { let prefix = &name[..2]; let suffix = &name[2..]; - let path = Path::new(GIT_DIR) - .join("objects") - .join(prefix) - .join(suffix); + let path = get_git_root()?.join("objects").join(prefix).join(suffix); if !path.exists() { - return Err(Error::other("Object does not exists")) + bail!("Object {} does not exists", name); } let mut file = File::open(path)?; @@ -44,23 +39,17 @@ impl GitObjectType { match decompress_slice(&mut header_buf, &content, InflateConfig::default()) { (_, ReturnCode::Ok) => (), (_, ReturnCode::BufError) => (), - _ => return Err(Error::other("Error while decompressing")) + _ => bail!("Error while decompressing"), }; - let header = match str::from_utf8(&header_buf) { - Ok(s) => match s.split_once("\0") { - None => return Err(Error::other("Invalid format")), - Some((header, _)) => header, - } - Err(_) => return Err(Error::other("Utf-8 error")) + let header = match str::from_utf8(&header_buf)?.split_once("\0") { + None => bail!("Object invalid format, did not found '\0'"), + Some((header, _)) => header, }; let (t, size) = match header.split_once(" ") { - None => return Err(Error::other("Invalid format")), - Some((t, s_str)) => match s_str.parse::() { - Ok(s) => (t, s), - _ => return Err(Error::other("Invalid format")), - } + None => bail!("Object head is invalid, did not found ' '"), + Some((t, s_str)) => (t, s_str.parse::()?), }; let header_size = header.len() + 1; let total_size = size + header_size; @@ -69,42 +58,41 @@ impl GitObjectType { match decompress_slice(&mut deflated, &content, InflateConfig::default()) { (_, ReturnCode::Ok) => (), - _ => return Err(Error::other("Error while decompressing")) + _ => bail!("Error while decompressing {}", name), } let obj_bytes = deflated[header_size..].to_vec(); match t { - "blob" => Ok(GitObjectType::Blob(Blob{ - size, - content: obj_bytes, + "blob" => Ok(GitObjectType::Blob(Blob { + size, + content: obj_bytes, })), "tree" => Ok(GitObjectType::Tree(Tree::from_bytes(obj_bytes)?)), - _ => Err(Error::other("Invalid type")) + _ => bail!("Invalid object type: {}", t), } } } pub trait GitObject { fn raw(&self) -> Vec; - fn hash(&self, write: bool) -> io::Result>; - fn save(&self, hash: &String, bytes: Vec) -> io::Result<()> { + fn hash(&self, write: bool) -> Result>; + fn save(&self, hash: &str, bytes: Vec) -> Result<()> { let mut compressed_buf = vec![0u8; compress_bound(bytes.len())]; - let compressed = match compress_slice(&mut compressed_buf, &bytes, DeflateConfig::default()) { + let compressed = match compress_slice(&mut compressed_buf, &bytes, DeflateConfig::default()) + { (compressed, ReturnCode::Ok) => compressed, - (_, _) => return Err(Error::other("Error while compressing objects")), + (_, _) => bail!("Error while compressing object {}", hash), }; let prefix = &hash[..2]; let suffix = &hash[2..]; - let path = Path::new(GIT_DIR) - .join("objects") - .join(prefix); + let path = get_git_root()?.join("objects").join(prefix); create_dir_all(&path)?; - let mut file = File::create(&path.join(suffix))?; - file.write_all(&compressed)?; + let mut file = File::create(path.join(suffix))?; + file.write_all(compressed)?; Ok(()) } @@ -117,7 +105,7 @@ pub struct Blob { } impl Blob { - pub fn create(filename: String) -> io::Result { + pub fn create(filename: String) -> Result { let path = Path::new(&filename); let mut file = File::open(path)?; @@ -125,7 +113,10 @@ impl Blob { let mut content: Vec = Vec::new(); let read = file.read_to_end(&mut content)?; - Ok(Self { size: read, content }) + Ok(Self { + size: read, + content, + }) } } @@ -133,15 +124,15 @@ impl GitObject for Blob { fn raw(&self) -> Vec { let mut header = String::from("blob "); header.push_str(&self.size.to_string()); - header.push_str("\0"); + header.push('\0'); let mut to_compress: Vec = Vec::from(header.as_bytes()); to_compress.extend(&self.content); - return to_compress + to_compress } - fn hash(&self, write: bool) -> io::Result> { + fn hash(&self, write: bool) -> Result> { let bytes = self.raw(); let mut hasher = Sha1::new(); @@ -174,31 +165,27 @@ pub struct TreeEntry { } impl TreeEntry { - fn from_bytes(bytes: Vec) -> io::Result { + fn from_bytes(bytes: Vec) -> Result { let split = match bytes.as_slice().iter().position(|x| *x == 0) { - None => return Err(Error::other("Error parsing tree object")), + None => bail!("Error parsing tree entry, did not found '\0'"), Some(s) => s, }; - let (header, hash_v) = bytes.split_at(split+1); + let (header, hash_v) = bytes.split_at(split + 1); if hash_v.len() != 20 { - return Err(Error::other("Digest length not valid")); + bail!("Digest length not valid"); } let hash: [u8; 20] = match hash_v.first_chunk() { - None => return Err(Error::other("Error parsing tree object")), + None => bail!("Error parsing tree entry, incomplete entry"), Some(h) => *h, }; - - let header_str = match str::from_utf8(&header[..header.len()-1]) { - Ok(s) => s, - _ => return Err(Error::other("Error parsing tree object")) - }; + let header_str = str::from_utf8(&header[..header.len() - 1])?; let (ftype, name) = match header_str.split_once(" ") { - None => return Err(Error::other("Error parsing tree object")), + None => bail!("Error parsing tree entry"), Some((l, name)) => { let ftype = match l { "0040000" => FileType::Directory, @@ -206,17 +193,13 @@ impl TreeEntry { "0100755" => FileType::RegExeFile, "0120000" => FileType::SymLink, "0160000" => FileType::GitLink, - _ => return Err(Error::other("Invalid file type")) + _ => bail!("Invalid file type"), }; (ftype, String::from(name)) } }; - return Ok(TreeEntry { - ftype, - name, - hash, - }); + Ok(TreeEntry { ftype, name, hash }) } fn as_bytes(&self) -> Vec { @@ -227,28 +210,30 @@ impl TreeEntry { FileType::SymLink => "0120000", FileType::GitLink => "0160000", }); - header.push_str(" "); + header.push(' '); header.push_str(&self.name); - header.push_str("\0"); + header.push('\0'); let mut bytes: Vec = Vec::from(header.as_bytes()); bytes.extend(self.hash); - return bytes; + bytes } } #[derive(Debug)] pub struct Tree { - pub entries: Vec + pub entries: Vec, } impl Tree { - pub fn from_bytes(bytes: Vec) -> io::Result { + pub fn from_bytes(bytes: Vec) -> Result { let mut entries: Vec = Vec::new(); - let splits = bytes.iter() + let splits = bytes + .iter() .enumerate() - .filter_map(|(idx, b)| (*b == 0).then(|| idx + 21)) + .filter(|&(_, b)| *b == 0) + .map(|(idx, _)| idx + 21) .collect::>(); let bytes_slice = bytes.as_slice(); @@ -263,7 +248,7 @@ impl Tree { left_i = right_i; } - return Ok(Tree { entries }); + Ok(Tree { entries }) } fn as_bytes(&self) -> Vec { @@ -272,7 +257,7 @@ impl Tree { result.extend(entry.as_bytes()); } - return result; + result } } @@ -281,15 +266,15 @@ impl GitObject for Tree { let bytes = self.as_bytes(); let mut header = String::from("tree "); header.push_str(&bytes.len().to_string()); - header.push_str("\0"); + header.push('\0'); let mut raw: Vec = Vec::from(header.as_bytes()); raw.extend(self.as_bytes()); - return raw; + raw } - fn hash(&self, write: bool) -> io::Result> { + fn hash(&self, write: bool) -> Result> { let bytes = self.raw(); let mut hasher = Sha1::new(); @@ -301,6 +286,6 @@ impl GitObject for Tree { self.save(&encode(hash), bytes)?; } - return Ok(Vec::from(hash.as_slice())); + Ok(Vec::from(hash.as_slice())) } } diff --git a/src/main.rs b/src/main.rs index 8d0f5f7..78c8a1a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,25 +1,15 @@ +#![feature(path_trailing_sep)] use clap::Parser; -// use std::{ -// fs::File, -// io::prelude::*, -// path::Path, -// }; -// use zlib_rs::{ -// ReturnCode, -// DeflateConfig, compress_bound, compress_slice, -// }; - -use subcommands::{Subcommand, SubcommandType}; - -mod subcommands; mod git_fs; +mod subcommands; +use subcommands::{Subcommand, SubcommandType}; const GIT_DIR: &str = ".rgit"; #[derive(Parser, Debug)] #[command(version, about, long_about)] -struct CmdArgs{ +struct CmdArgs { #[command(subcommand)] cmd: SubcommandType, } @@ -31,35 +21,4 @@ fn main() { Err(str) => println!("Some error occured: {str}"), Ok(str) => println!("{str}"), } - - // let path = Path::new("sample/file.txt"); - // let display = path.display(); - - // let mut file = match File::open(&path) { - // Err(err) => panic!("couldn't open {}: {}", display, err), - // Ok(file) => file, - // }; - - // let mut s = String::new(); - // match file.read_to_string(&mut s) { - // Err(err) => panic!("couldn't read {}: {}", display, err), - // Ok(_) => print!("content: \n{}", s), - // } - - // let mut compressed_buf = vec![0u8; compress_bound(s.len())]; - // let compressed = match compress_slice(&mut compressed_buf, s.as_bytes(), DeflateConfig::default()) { - // (compressed, ReturnCode::Ok) => compressed, - // (_, _) => panic!("Error while compressing"), - // }; - - // let path_w = Path::new("sample/write"); - // file = match File::create(&path_w) { - // Err(_) => panic!("error while opening file"), - // Ok(file) => file, - // }; - - // match file.write_all(compressed) { - // Err(_) => panic!("error while writing"), - // Ok(_) => println!("File written"), - // } } diff --git a/src/subcommands/add.rs b/src/subcommands/add.rs new file mode 100644 index 0000000..fa813fc --- /dev/null +++ b/src/subcommands/add.rs @@ -0,0 +1,45 @@ +use anyhow::Result; +use clap::Parser; +use std::path::Path; + +use crate::{ + git_fs::{gitignore::expands_and_filter_path, index::Index, normalize_path_in_worktree}, + subcommands::hash_object::HashObjectSubcommand, +}; + +use super::Subcommand; + +#[derive(Parser, Debug)] +pub struct AddSubcommand { + pub paths: Vec, +} + +impl Subcommand for AddSubcommand { + fn run(&self) -> Result { + if self.paths.is_empty() { + return Ok(String::from("Nothing specified, nothing added")); + } + + let mut index = Index::load()?; + + for path in self.paths.iter().map(|p| Path::new(p).to_path_buf()) { + let expanded = expands_and_filter_path(path)?; + for p in expanded { + println!("Adding {}", p.display()); + let path_str = String::from(p.to_str().unwrap()); + let name = String::from(normalize_path_in_worktree(&p)?.to_str().unwrap()); + println!("adding {}", name); + let hash = HashObjectSubcommand { + write: true, + path: path_str, + } + .run_raw()?; + index.add_file(name, hash)?; + } + } + + index.save()?; + + Ok(String::from("")) + } +} diff --git a/src/subcommands/hash_object.rs b/src/subcommands/hash_object.rs index b696329..ade71ae 100644 --- a/src/subcommands/hash_object.rs +++ b/src/subcommands/hash_object.rs @@ -1,32 +1,38 @@ +use anyhow::{Result, bail}; use clap::Parser; use hex::encode; use crate::{ git_fs::object::{Blob, GitObject}, - subcommands::Subcommand + subcommands::Subcommand, }; -use super::CmdResult; - #[derive(Parser, Debug)] pub struct HashObjectSubcommand { - #[arg(short)] + #[arg(short, default_value_t = false)] /// Save object in database pub write: bool, pub path: String, } -impl Subcommand for HashObjectSubcommand { - fn run (&self) -> CmdResult { - let object = match Blob::create(self.path.clone()) { - Ok(o) => o, - _ => return Err("".to_owned()) +impl HashObjectSubcommand { + pub fn run_raw(&self) -> Result<[u8; 20]> { + let object = Blob::create(self.path.clone())?; + let hash = object.hash(self.write)?; + + let hash_final: [u8; 20] = match hash.first_chunk() { + Some(h) => *h, + None => bail!("Hash length not valid"), }; - match object.hash(self.write) { - Ok(hash) => Ok(encode(hash)), - _ => return Err("".to_owned()) - } + Ok(hash_final) + } +} + +impl Subcommand for HashObjectSubcommand { + fn run(&self) -> Result { + let hash = self.run_raw()?; + Ok(encode(hash)) } } diff --git a/src/subcommands/init.rs b/src/subcommands/init.rs index ba470ea..1785b3b 100644 --- a/src/subcommands/init.rs +++ b/src/subcommands/init.rs @@ -1,14 +1,11 @@ +use anyhow::Result; +use anyhow::bail; use clap::Parser; -use std::{ - fs, - path::Path -}; +use std::{fs, path::Path}; +use crate::GIT_DIR; use crate::git_fs::head::Head; use crate::subcommands::Subcommand; -use crate::GIT_DIR; - -use super::CmdResult; #[derive(Parser, Debug)] pub struct InitSubcommand { @@ -16,45 +13,42 @@ pub struct InitSubcommand { } impl Subcommand for InitSubcommand { - fn run(&self) -> CmdResult { + fn run(&self) -> Result { let path = match &self.directory { None => Path::new("."), Some(path) => Path::new(path), - }.join(GIT_DIR); + } + .join(GIT_DIR); let new_repo = path.exists(); - match fs::create_dir_all(&path) { - Err(_) => return Err("Error while creating dir".to_owned()), - Ok(()) => (), + if fs::create_dir_all(&path).is_err() { + bail!("Error while creating dir") }; - let folders = [ - "objects/info", - "objects/pack", - "refs/heads", - "refs/tags", - ]; + let folders = ["objects/info", "objects/pack", "refs/heads", "refs/tags"]; for folder in folders { - match fs::create_dir_all(&path.join(folder)) { - Err(_) => return Err("".to_owned()), - Ok(()) => (), - }; + fs::create_dir_all(path.join(folder))?; } - let head = Head { ref_to: String::from("refs/heads/master") }; - match head.save() { - Err(_) => return Err("".to_owned()), - Ok(()) => (), + let head = Head { + ref_to: String::from("refs/heads/master"), }; + head.save()?; let canonical_path = path.canonicalize(); if new_repo { - Ok(format!("Reinitialized exisiting Git repo in {}", canonical_path.unwrap().display())) + Ok(format!( + "Reinitialized exisiting Git repo in {}", + canonical_path?.display() + )) } else { - Ok(format!("Initialized empty Git repo in {}", canonical_path.unwrap().display())) + Ok(format!( + "Initialized empty Git repo in {}", + canonical_path?.display() + )) } } } diff --git a/src/subcommands/mod.rs b/src/subcommands/mod.rs index c7979e7..103dcc9 100644 --- a/src/subcommands/mod.rs +++ b/src/subcommands/mod.rs @@ -1,17 +1,21 @@ use crate::subcommands::{ - init::InitSubcommand, - test::TestSubcommand, - hash_object::HashObjectSubcommand, + add::AddSubcommand, hash_object::HashObjectSubcommand, init::InitSubcommand, + remove::RemoveSubcommand, test::TestSubcommand, }; +use anyhow::Result; +mod add; mod hash_object; mod init; +mod remove; mod test; -pub type CmdResult = Result; - #[derive(clap::Parser, Debug)] pub enum SubcommandType { + /// Add file(s) to index + Add(AddSubcommand), + /// Remove file from the working and the index + Remove(RemoveSubcommand), /// Init a Git repository Init(InitSubcommand), HashObject(HashObjectSubcommand), @@ -19,12 +23,14 @@ pub enum SubcommandType { } pub trait Subcommand { - fn run(&self) -> CmdResult; + fn run(&self) -> Result; } impl Subcommand for SubcommandType { - fn run(&self) -> CmdResult { + fn run(&self) -> Result { match self { + Self::Add(cmd) => cmd.run(), + Self::Remove(cmd) => cmd.run(), Self::Init(cmd) => cmd.run(), Self::HashObject(cmd) => cmd.run(), Self::Test(cmd) => cmd.run(),