From 250018c4bf4175a81d844bd4ac4aa882c7e9cb11 Mon Sep 17 00:00:00 2001 From: grimhilt Date: Sun, 8 Sep 2024 00:18:33 +0200 Subject: [PATCH] feat(indexer+status): make some basics case work --- src/commands/add.rs | 7 +- src/commands/status.rs | 199 ++++++++++++++++++++++++++++++++++++++--- src/lib.rs | 5 ++ src/store/indexer.rs | 144 +++++++++++++++++------------ src/store/object.rs | 20 +++-- src/utils/path.rs | 34 ++++++- 6 files changed, 331 insertions(+), 78 deletions(-) create mode 100644 src/lib.rs diff --git a/src/commands/add.rs b/src/commands/add.rs index 2d89f33..cb8ed92 100644 --- a/src/commands/add.rs +++ b/src/commands/add.rs @@ -24,9 +24,7 @@ pub fn exec(args: AddArgs, config: Config) { // Init indexer let mut indexer = Indexer::new(config.get_root_unsafe()); - dbg!(indexer.save()); - dbg!(indexer.load()); - return; + let _ = indexer.load(); nsobject::init(config.get_root_unsafe()); @@ -40,6 +38,7 @@ pub fn exec(args: AddArgs, config: Config) { if path_to_add.exists() { if path_to_add.is_dir() { + indexer.index_dir(path_to_add.clone()); add_dir(&path_to_add, &mut ignorer, &mut indexer); } else { indexer.index_file(path_to_add); @@ -53,6 +52,8 @@ pub fn exec(args: AddArgs, config: Config) { panic!("fatal: pathspec '{}' did not match any files", obj_to_add); } } + + dbg!(indexer.save()); /* for all files if globbing diff --git a/src/commands/status.rs b/src/commands/status.rs index 1b01297..026d334 100644 --- a/src/commands/status.rs +++ b/src/commands/status.rs @@ -1,12 +1,14 @@ use crate::config::config::Config; use crate::store::{ + indexer::Indexer, nsobject::NsObject, - object::{Obj, ObjStatus}, + object::{Obj, ObjStatus, ObjType}, }; use crate::utils::path; +use colored::{ColoredString, Colorize}; use std::collections::HashMap; use std::fs; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::sync::{Arc, Mutex}; use threadpool::ThreadPool; @@ -14,10 +16,19 @@ pub struct StatusArgs { pub nostyle: bool, } +type HashMapObj = HashMap; +type HashMapObjStatuses = Arc>; + struct ObjStatuses { - created: Arc>>, - modified: Arc>>, - deleted: Arc>>, + created: HashMapObjStatuses, + modified: HashMapObjStatuses, + deleted: HashMapObjStatuses, + // staged: Arc>>, + // not_staged: Arc>>, + // untracked: Arc>>, + staged: Vec, + not_staged: Vec, + untracked: Vec, } impl ObjStatuses { @@ -26,6 +37,12 @@ impl ObjStatuses { created: Arc::new(Mutex::new(HashMap::new())), modified: Arc::new(Mutex::new(HashMap::new())), deleted: Arc::new(Mutex::new(HashMap::new())), + // staged: Arc::new(Mutex::new(Vec::new())), + // not_staged: Arc::new(Mutex::new(Vec::new())), + // untracked: Arc::new(Mutex::new(Vec::new())) + staged: Vec::new(), + not_staged: Vec::new(), + untracked: Vec::new(), } } @@ -40,9 +57,57 @@ impl ObjStatuses { fn push_deleted(&self, key: String, value: Obj) { self.deleted.lock().unwrap().insert(key, value); } + + fn get_created(&self) -> HashMapObj { + self.created.lock().unwrap().clone() + } +} + +pub struct ObjStaged { + pub staged: Vec, + pub not_staged: Vec, +} + +fn setup_staged(obj_statuses: Arc, indexer: &Indexer) -> ObjStaged { + let mut staged = Vec::new(); + let mut not_staged = Vec::new(); + + for (_, mut obj) in obj_statuses.get_created() { + obj.set_status(ObjStatus::Created); + if indexer.is_staged(&obj) { + staged.push(obj); + } else { + not_staged.push(obj); + } + } + + ObjStaged { staged, not_staged } + + // for (_, mut obj) in self.modified.lock().unwrap().iter() { + // obj.set_status(ObjStatus::Modified); + // if indexer.is_staged(&obj) { + // self.staged.lock().unwrap().push(obj); + // } else { + // self.not_staged.lock().unwrap().push(obj); + // } + // } + + // for (_, mut obj)in self.deleted.lock().unwrap().iter() { + // obj.set_status(ObjStatus::Deleted); + // if indexer.is_staged(&obj) { + // self.staged.lock().unwrap().push(obj); + // } else { + // self.not_staged.lock().unwrap().push(obj); + // } + // } } pub fn exec(args: StatusArgs, config: Config) { + let status = get_obj_changes(&args, &config); + print_status(&status); +} + +pub fn get_obj_changes(args: &StatusArgs, config: &Config) -> ObjStaged { // use root of repo if no custom path has been set by the command let root = if config.is_custom_execution_path { config.execution_path.clone() @@ -50,14 +115,21 @@ pub fn exec(args: StatusArgs, config: Config) { config.get_root_unsafe().to_path_buf() }; + let indexer = Arc::new({ + let mut indexer = Indexer::new(config.get_root_unsafe()); + indexer.load_unsafe(); + indexer + }); + let pool = ThreadPool::new(4); let repo_root = config.get_root_unsafe().clone(); let res = Arc::new(ObjStatuses::new()); let pool_clone = pool.clone(); let res_clone = Arc::clone(&res); + let indexer_clone = Arc::clone(&indexer); pool.execute(move || { - compare_dir(pool_clone, &repo_root, &root, res_clone); + compare_dir(pool_clone, indexer_clone, &repo_root, &root, res_clone); }); pool.join(); @@ -71,7 +143,62 @@ pub fn exec(args: StatusArgs, config: Config) { for entry in res.deleted.lock().unwrap().iter() { println!("deleted: {}", entry.0); } + // find moved and copied + + // find staged + + setup_staged(Arc::clone(&res), &indexer) +} + +/// +fn print_status(objs: &ObjStaged) { + if objs.staged.len() == 0 && objs.not_staged.len() == 0 { + println!("Nothing to push, working tree clean"); + return; + } + + if objs.staged.len() != 0 { + println!("Changes to be pushed:"); + println!(" (Use \"nextsync reset\" to unstage)"); + // (use "git restore --staged ..." to unstage) + // by alphabetical order + for obj in objs.staged.iter() { + print_object(&obj, |status: &str| status.green()); + } + } + // modified + // deleted + // renamed + // new file + println!("Changes not staged for push:"); + println!(" (Use \"nextsync add ...\" to update what will be pushed)"); + for obj in objs.not_staged.iter() { + print_object(&obj, |status: &str| status.red()); + } + + // println!("Untracked files:"); + // for obj in objs.untracked.iter() { + // println!("{}", obj.cpy_path()); + // } +} + +fn print_object(obj: &Obj, color: impl Fn(&str) -> ColoredString) { + println!( + " {}{}", + match obj.get_status() { + ObjStatus::Created => + if obj.get_obj_type() == &ObjType::Blob { + color("new file: ") + } else { + color("new: ") + }, + ObjStatus::Modified => color("modified: "), + ObjStatus::Deleted => color("deleted: "), + _ => "unknown".red(), + }, + color(&obj.cpy_path().to_string()) + ); } /// @@ -80,7 +207,13 @@ pub fn exec(args: StatusArgs, config: Config) { /// * `root_path`: path of the repo's root /// * `path`: path we should analyze /// * `res`: the struct in which we should store the response -fn compare_dir(pool: ThreadPool, root_path: &PathBuf, path: &PathBuf, res: Arc) { +fn compare_dir( + pool: ThreadPool, + indexer: Arc, + root_path: &PathBuf, + path: &PathBuf, + res: Arc, +) { let entries = match fs::read_dir(path) { Ok(entries) => entries, Err(err) => { @@ -105,23 +238,36 @@ fn compare_dir(pool: ThreadPool, root_path: &PathBuf, path: &PathBuf, res: Arc) { + let entries = match fs::read_dir(path) { + Ok(entries) => entries, + Err(err) => { + eprintln!("Failed to read {} ({err})", path.display()); + return; + } + }; + for entry in entries { + let entry = match entry { + Ok(entry) => entry, + Err(err) => { + eprintln!("Failed to read entry {err}"); + continue; + } + }; + + let repo_relative_entry = path::to_repo_relative(&entry.path(), root_path); + let local_obj = Obj::from_local_path(&repo_relative_entry); + if entry.path().is_dir() { + if entry.path().ends_with(".nextsync") { + continue; + } + + let res_clone = Arc::clone(&res); + add_childs(root_path, &entry.path(), res_clone); + res.push_created(local_obj.cpy_path(), local_obj); + } else { + res.push_created(local_obj.cpy_path(), local_obj); + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..9c1dbc1 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,5 @@ +pub mod commands; +pub mod config; +pub mod store; +pub mod subcommands; +pub mod utils; diff --git a/src/store/indexer.rs b/src/store/indexer.rs index 7cdc32a..2751ce5 100644 --- a/src/store/indexer.rs +++ b/src/store/indexer.rs @@ -1,38 +1,40 @@ -use crate::store::object::ObjType; +use crate::store::object::{Obj, ObjType}; +use crate::utils::path::normalize_path; +use std::cmp::Ordering; use std::fs::File; -use std::io::{self, Read, Seek, SeekFrom, Write}; +use std::io::{self, Write}; +use std::io::{BufRead, BufReader}; use std::path::PathBuf; -fn get_level(previous_level: u16, path: &PathBuf) -> (u16, IndexLevel) { - let mut level = 0; - let mut path = path.clone(); - while path.pop() { - level += 1; - } +// Custom sorting function to handle paths hierarchically +fn sort_paths_hierarchically(paths: &mut Vec) { + paths.sort_by(|a, b| { + // Split paths into components for comparison + let a_components: Vec<_> = a.components().collect(); + let b_components: Vec<_> = b.components().collect(); - let index_level = if previous_level < level { - IndexLevel::Down - } else { - IndexLevel::Up - }; + // Compare components one by one + for (a_component, b_component) in a_components.iter().zip(b_components.iter()) { + match a_component.cmp(b_component) { + Ordering::Equal => continue, + ordering => return ordering, + } + } - (level, index_level) -} - -enum IndexLevel { - Up, - Down, + // If one path is a prefix of the other, the shorter path comes first + a_components.len().cmp(&b_components.len()) + }); } +#[derive(Debug)] struct IndexedObj { obj_type: ObjType, - level: IndexLevel, path: PathBuf, } pub struct Indexer { + repo_root: PathBuf, index_file: PathBuf, - previous_level: u16, indexed_objs: Vec, } @@ -43,47 +45,76 @@ impl Indexer { index_file.push("index"); Indexer { + repo_root: repo_root.clone(), index_file, - previous_level: 0, indexed_objs: Vec::new(), } } - pub fn load(&self) -> io::Result<()> { - let mut file = File::open(&self.index_file)?; - let mut str = String::new(); + pub fn load(&mut self) -> io::Result<()> { + let file = File::open(&self.index_file)?; + let reader = BufReader::new(file); - // Skip reserved bytes - let mut byte = [0; 1]; - file.read_exact(&mut byte)?; - file.seek(SeekFrom::Start(1))?; + for line in reader.lines() { + let line = line?; + let line = line.as_bytes(); - // Read usize value - let mut usize_bytes = [0; std::mem::size_of::()]; - file.read_exact(&mut usize_bytes)?; - dbg!(ObjType::try_from(byte[0])); - let usize_value = usize::from_le_bytes(usize_bytes); + let path_str = String::from_utf8(line[1..].to_vec()).unwrap(); + let path = PathBuf::from(path_str); - // Read PathBuf as string - let mut buffer = Vec::new(); - file.read_to_end(&mut buffer)?; - let path_str = String::from_utf8(buffer).unwrap(); - let path = PathBuf::from(path_str); + self.indexed_objs.push(IndexedObj { + obj_type: ObjType::try_from(line[0]).unwrap(), + path, + }); + } - println!("usize value: {}", usize_value); - println!("Path: {:?}", path); Ok(()) } - fn index_obj(&mut self, path: PathBuf, obj_type: ObjType) { - let (level, index_level) = get_level(self.previous_level, &path); - self.previous_level = level; + pub fn load_unsafe(&mut self) { + let _ = self.load(); + dbg!(&self.indexed_objs); + } - self.indexed_objs.push(IndexedObj { + fn is_indexed(&self, obj: &IndexedObj) -> bool { + self.indexed_objs + .iter() + .position(|o| o.obj_type == obj.obj_type && o.path == obj.path) + .is_some() + } + + pub fn is_staged(&self, obj: &Obj) -> bool { + dbg!(obj); + // self.indexed_objs.iter().position(|o| &o.obj_type == obj.get_obj_type() && &o.path == obj.get_obj_path()).is_some() + self.indexed_objs + .iter() + .position(|o| &o.path == obj.get_obj_path()) + .is_some() + } + + pub fn is_staged_parent(&self, obj: &Obj) -> bool { + self.indexed_objs + .iter() + .position(|o| o.path.starts_with(obj.get_obj_path())) + .is_some() + } + + fn index_obj(&mut self, path: PathBuf, obj_type: ObjType) { + let mut path = path; + path = normalize_path(path); // normalize path (/foo/./bar => /foo/bar) + // change path to be relative to repo's root + path = path.strip_prefix(&self.repo_root).unwrap().to_path_buf(); + + let indexed_obj = IndexedObj { obj_type, - level: index_level, path: path.clone(), - }); + }; + + if self.is_indexed(&indexed_obj) { + return; + } + + self.indexed_objs.push(indexed_obj); } pub fn index_file(&mut self, path: PathBuf) { @@ -97,20 +128,15 @@ impl Indexer { pub fn save(&self) -> io::Result<()> { let mut file = File::create(&self.index_file)?; - // Write reserved bytes - let variant = ObjType::Blob; - let byte: u8 = variant.into(); - file.write_all(&[byte])?; + for obj in self.indexed_objs.iter() { + // write obj_type + file.write_all(&[obj.obj_type.clone().into()])?; - // Write usize value - let usize_value: usize = 12; - let usize_bytes = usize_value.to_le_bytes(); - file.write_all(&usize_bytes)?; + // write path + file.write_all(obj.path.to_str().unwrap().as_bytes())?; - // Write PathBuf as string - let path = PathBuf::from("/jodi/"); - let path_str = path.to_str().unwrap(); - file.write_all(path_str.as_bytes())?; + file.write_all(b"\n")?; + } Ok(()) } diff --git a/src/store/object.rs b/src/store/object.rs index 28c9ddb..d8c9ebd 100644 --- a/src/store/object.rs +++ b/src/store/object.rs @@ -40,16 +40,17 @@ impl TryFrom for ObjType { } } -#[derive(PartialEq, Clone)] +#[derive(PartialEq, Clone, Debug)] pub enum ObjStatus { Undefined, Created, + Modified, Moved, Copied, Deleted, } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Obj { obj_type: ObjType, status: OnceLock, @@ -67,15 +68,16 @@ impl Obj { } } - fn get_status(&self) -> &ObjStatus { + pub fn get_status(&self) -> &ObjStatus { self.status.get_or_init(|| { - // read path + // todo!(); + // TODO read path ObjStatus::Created }) } pub fn set_status(&mut self, status: ObjStatus) { - todo!() + self.status = OnceLock::from(status); } pub fn set_type(&mut self, obj_type: ObjType) { @@ -86,6 +88,14 @@ impl Obj { self.get_status() == &ObjStatus::Created } + pub fn get_obj_type(&self) -> &ObjType { + &self.obj_type + } + + pub fn get_obj_path(&self) -> &PathBuf { + &self.obj_path + } + pub fn cpy_path(&self) -> String { path::to_string(&self.obj_path) } diff --git a/src/utils/path.rs b/src/utils/path.rs index 9a6ff13..d5c565b 100644 --- a/src/utils/path.rs +++ b/src/utils/path.rs @@ -1,4 +1,4 @@ -use std::path::{Path, PathBuf}; +use std::path::{Path, PathBuf, Component}; pub fn to_repo_relative(path: &PathBuf, root: &PathBuf) -> PathBuf { path.strip_prefix(root).unwrap().to_path_buf() @@ -7,3 +7,35 @@ pub fn to_repo_relative(path: &PathBuf, root: &PathBuf) -> PathBuf { pub fn to_string(path: &PathBuf) -> String { path.to_str().unwrap().to_string() } + +/// Improve the path to try remove and solve .. token. +/// Taken from https://stackoverflow.com/questions/68231306/stdfscanonicalize-for-files-that-dont-exist +/// +/// This assumes that `a/b/../c` is `a/c` which might be different from +/// what the OS would have chosen when b is a link. This is OK +/// for broot verb arguments but can't be generally used elsewhere +/// +/// This function ensures a given path ending with '/' still +/// ends with '/' after normalization. +pub fn normalize_path>(path: P) -> PathBuf { + let ends_with_slash = path.as_ref() + .to_str() + .map_or(false, |s| s.ends_with('/')); + let mut normalized = PathBuf::new(); + for component in path.as_ref().components() { + match &component { + Component::ParentDir => { + if !normalized.pop() { + normalized.push(component); + } + } + _ => { + normalized.push(component); + } + } + } + if ends_with_slash { + normalized.push(""); + } + normalized +}