Compare commits
3 Commits
index_parsing
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 72d0d5f1e2 | |||
| c118829c11 | |||
| 3027a99b5f |
Generated
+24
@@ -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",
|
||||
]
|
||||
|
||||
+5
-3
@@ -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"
|
||||
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
@@ -0,0 +1,395 @@
|
||||
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<Vec<PathBuf>> {
|
||||
// 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<GitIgnoreRule>) -> Vec<PathBuf> {
|
||||
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<GitIgnoreRule> {
|
||||
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<Vec<GitIgnoreRule>> {
|
||||
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));
|
||||
}
|
||||
}
|
||||
+19
-28
@@ -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<Self, ()> {
|
||||
let path = Path::new(GIT_DIR).join("HEAD");
|
||||
pub fn load() -> Result<Self> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
+442
-37
@@ -1,56 +1,461 @@
|
||||
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<u8>) -> io::Result<Self> {
|
||||
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() + 1, 8) * 8;
|
||||
|
||||
Ok((
|
||||
IndexEntry {
|
||||
object_type,
|
||||
permissions,
|
||||
hash,
|
||||
name,
|
||||
},
|
||||
entry_size,
|
||||
))
|
||||
}
|
||||
|
||||
fn to_bytes(&self) -> Result<Vec<u8>> {
|
||||
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<IndexEntry>,
|
||||
}
|
||||
|
||||
impl Index {
|
||||
pub fn from_bytes(bytes: Vec<u8>) -> io::Result<Self> {
|
||||
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<IndexEntry> = 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<Self> {
|
||||
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<u8> = 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(())
|
||||
}
|
||||
|
||||
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 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),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_bytes(bytes: Vec<u8>) -> Result<Self> {
|
||||
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<IndexEntry> = 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<Vec<u8>> {
|
||||
let mut bytes: Vec<u8> = 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<u8> = 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<u8> = 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<u8> = 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, len1) = 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, len2) = 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, len3) = 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);
|
||||
|
||||
assert_eq!(len1, 88);
|
||||
assert_eq!(len2, 80);
|
||||
assert_eq!(len3, 88);
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn index_remove_entry() {
|
||||
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 mut index = Index {
|
||||
entries: vec![entry3.clone(), entry1, entry2.clone()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
index.remove_file(String::from("a")).unwrap();
|
||||
|
||||
let expected = Index {
|
||||
entries: vec![entry3, entry2],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(index, expected);
|
||||
}
|
||||
}
|
||||
|
||||
+60
-31
@@ -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<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
|
||||
where P: AsRef<Path> {
|
||||
fn read_lines<P>(filename: P) -> Result<io::Lines<io::BufReader<File>>>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let file = File::open(filename)?;
|
||||
Ok(io::BufReader::new(file).lines())
|
||||
}
|
||||
|
||||
pub fn normalize_path_in_worktree(path: &Path) -> Result<PathBuf> {
|
||||
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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
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<Self, ()> {
|
||||
let path = Path::new(GIT_DIR).join(&name);
|
||||
pub fn load(name: String) -> Result<Self> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
+61
-76
@@ -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<GitObjectType> {
|
||||
pub fn load(name: String) -> Result<GitObjectType> {
|
||||
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::<usize>() {
|
||||
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::<usize>()?),
|
||||
};
|
||||
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<u8>;
|
||||
fn hash(&self, write: bool) -> io::Result<Vec<u8>>;
|
||||
fn save(&self, hash: &String, bytes: Vec<u8>) -> io::Result<()> {
|
||||
fn hash(&self, write: bool) -> Result<Vec<u8>>;
|
||||
fn save(&self, hash: &str, bytes: Vec<u8>) -> 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<Self> {
|
||||
pub fn create(filename: String) -> Result<Self> {
|
||||
let path = Path::new(&filename);
|
||||
|
||||
let mut file = File::open(path)?;
|
||||
@@ -125,7 +113,10 @@ impl Blob {
|
||||
let mut content: Vec<u8> = 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<u8> {
|
||||
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<u8> = Vec::from(header.as_bytes());
|
||||
to_compress.extend(&self.content);
|
||||
|
||||
return to_compress
|
||||
to_compress
|
||||
}
|
||||
|
||||
fn hash(&self, write: bool) -> io::Result<Vec<u8>> {
|
||||
fn hash(&self, write: bool) -> Result<Vec<u8>> {
|
||||
let bytes = self.raw();
|
||||
|
||||
let mut hasher = Sha1::new();
|
||||
@@ -174,31 +165,27 @@ pub struct TreeEntry {
|
||||
}
|
||||
|
||||
impl TreeEntry {
|
||||
fn from_bytes(bytes: Vec<u8>) -> io::Result<Self> {
|
||||
fn from_bytes(bytes: Vec<u8>) -> Result<Self> {
|
||||
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<u8> {
|
||||
@@ -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<u8> = Vec::from(header.as_bytes());
|
||||
bytes.extend(self.hash);
|
||||
|
||||
return bytes;
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Tree {
|
||||
pub entries: Vec<TreeEntry>
|
||||
pub entries: Vec<TreeEntry>,
|
||||
}
|
||||
|
||||
impl Tree {
|
||||
pub fn from_bytes(bytes: Vec<u8>) -> io::Result<Self> {
|
||||
pub fn from_bytes(bytes: Vec<u8>) -> Result<Self> {
|
||||
let mut entries: Vec<TreeEntry> = 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::<Vec<usize>>();
|
||||
|
||||
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<u8> {
|
||||
@@ -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<u8> = Vec::from(header.as_bytes());
|
||||
raw.extend(self.as_bytes());
|
||||
|
||||
return raw;
|
||||
raw
|
||||
}
|
||||
|
||||
fn hash(&self, write: bool) -> io::Result<Vec<u8>> {
|
||||
fn hash(&self, write: bool) -> Result<Vec<u8>> {
|
||||
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()))
|
||||
}
|
||||
}
|
||||
|
||||
+5
-49
@@ -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::{GitSubcommand, SubcommandType};
|
||||
|
||||
const GIT_DIR: &str = ".rgit";
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about)]
|
||||
struct CmdArgs{
|
||||
struct CmdArgs {
|
||||
#[command(subcommand)]
|
||||
cmd: SubcommandType,
|
||||
}
|
||||
@@ -27,39 +17,5 @@ struct CmdArgs{
|
||||
fn main() {
|
||||
let args = CmdArgs::parse();
|
||||
|
||||
match args.cmd.run() {
|
||||
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"),
|
||||
// }
|
||||
if let Err(str) = args.cmd.run() { eprintln!("Some error occured: {str}") }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
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::GitSubcommand;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct AddSubcommand {
|
||||
pub paths: Vec<String>,
|
||||
}
|
||||
|
||||
impl GitSubcommand for AddSubcommand {
|
||||
fn run(&self) -> Result<()> {
|
||||
if self.paths.is_empty() {
|
||||
println!("Nothing specified, nothing added");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
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 {
|
||||
let path_str = String::from(p.to_str().unwrap());
|
||||
let name = String::from(normalize_path_in_worktree(&p)?.to_str().unwrap());
|
||||
let hash = HashObjectSubcommand {
|
||||
write: true,
|
||||
path: path_str,
|
||||
}
|
||||
.run_raw()?;
|
||||
index.add_file(name, hash)?;
|
||||
}
|
||||
}
|
||||
|
||||
index.save()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,39 @@
|
||||
use anyhow::{Result, bail};
|
||||
use clap::Parser;
|
||||
use hex::encode;
|
||||
|
||||
use crate::{
|
||||
git_fs::object::{Blob, GitObject},
|
||||
subcommands::Subcommand
|
||||
subcommands::GitSubcommand,
|
||||
};
|
||||
|
||||
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 GitSubcommand for HashObjectSubcommand {
|
||||
fn run(&self) -> Result<()> {
|
||||
let hash = self.run_raw()?;
|
||||
println!("{}", encode(hash));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+27
-31
@@ -1,60 +1,56 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use clap::Parser;
|
||||
use std::{
|
||||
fs,
|
||||
path::Path
|
||||
};
|
||||
use std::{fs, path::Path};
|
||||
|
||||
use crate::git_fs::head::Head;
|
||||
use crate::subcommands::Subcommand;
|
||||
use crate::GIT_DIR;
|
||||
|
||||
use super::CmdResult;
|
||||
use crate::git_fs::head::Head;
|
||||
use crate::subcommands::GitSubcommand;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct InitSubcommand {
|
||||
directory: Option<String>,
|
||||
}
|
||||
|
||||
impl Subcommand for InitSubcommand {
|
||||
fn run(&self) -> CmdResult {
|
||||
impl GitSubcommand for InitSubcommand {
|
||||
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()))
|
||||
println!(
|
||||
"Reinitialized exisiting Git repo in {}",
|
||||
canonical_path?.display()
|
||||
);
|
||||
} else {
|
||||
Ok(format!("Initialized empty Git repo in {}", canonical_path.unwrap().display()))
|
||||
}
|
||||
println!(
|
||||
"Initialized empty Git repo in {}",
|
||||
canonical_path?.display()
|
||||
);
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+18
-9
@@ -1,30 +1,39 @@
|
||||
use crate::subcommands::{
|
||||
init::InitSubcommand,
|
||||
add::AddSubcommand, hash_object::HashObjectSubcommand, init::InitSubcommand, rm::RmSubcommand,
|
||||
test::TestSubcommand,
|
||||
hash_object::HashObjectSubcommand,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
|
||||
mod add;
|
||||
mod hash_object;
|
||||
mod init;
|
||||
mod rm;
|
||||
mod test;
|
||||
|
||||
pub type CmdResult = Result<String, String>;
|
||||
|
||||
#[derive(clap::Parser, Debug)]
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum SubcommandType {
|
||||
/// Add file(s) to index
|
||||
Add(AddSubcommand),
|
||||
/// Remove file(s) from the index
|
||||
Rm(RmSubcommand),
|
||||
/// Init a Git repository
|
||||
Init(InitSubcommand),
|
||||
#[clap(hide = true)]
|
||||
HashObject(HashObjectSubcommand),
|
||||
#[clap(hide = true)]
|
||||
Test(TestSubcommand),
|
||||
}
|
||||
|
||||
pub trait Subcommand {
|
||||
fn run(&self) -> CmdResult;
|
||||
pub trait GitSubcommand {
|
||||
fn run(&self) -> Result<()>;
|
||||
}
|
||||
|
||||
impl Subcommand for SubcommandType {
|
||||
fn run(&self) -> CmdResult {
|
||||
impl GitSubcommand for SubcommandType {
|
||||
fn run(&self) -> Result<()> {
|
||||
match self {
|
||||
Self::Add(cmd) => cmd.run(),
|
||||
Self::Rm(cmd) => cmd.run(),
|
||||
Self::Init(cmd) => cmd.run(),
|
||||
Self::HashObject(cmd) => cmd.run(),
|
||||
Self::Test(cmd) => cmd.run(),
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::git_fs::{gitignore::expands_and_filter_path, index::Index};
|
||||
|
||||
use super::GitSubcommand;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct RmSubcommand {
|
||||
#[arg(short, long, default_value_t = false)]
|
||||
/// Only remove object in index
|
||||
pub cached: bool,
|
||||
|
||||
pub paths: Vec<String>,
|
||||
}
|
||||
|
||||
impl GitSubcommand for RmSubcommand {
|
||||
fn run(&self) -> Result<()> {
|
||||
if self.paths.is_empty() {
|
||||
println!("Nothing specified, nothing removed");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
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 {
|
||||
index.remove_file(String::from(p.to_str().unwrap()))?;
|
||||
}
|
||||
}
|
||||
|
||||
index.save()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user