Compare commits

...

3 Commits

Author SHA1 Message Date
grimhilt
e66dc8d408 tests(add): introduce basics tests for add 2024-09-08 17:59:39 +02:00
grimhilt
baeef1a33a feat(reset): introduce reset and fix ROOT_PATH 2024-09-08 17:59:22 +02:00
grimhilt
bc6a23b76b feat(structs): set wrappers to path 2024-09-08 15:56:20 +02:00
21 changed files with 343 additions and 98 deletions

View File

@ -1,3 +1,4 @@
pub mod add;
pub mod init;
pub mod status;
pub mod reset;

View File

@ -3,7 +3,10 @@ use crate::store::{
ignorer::Ignorer,
indexer::Indexer,
nsobject::{self, NsObject},
structs::{self, to_obj_path},
};
use crate::utils::path::to_repo_relative;
use colored::Colorize;
use std::fs;
// use glob::glob;
use std::path::PathBuf;
@ -18,6 +21,8 @@ pub struct AddArgs {
}
pub fn exec(args: AddArgs, config: Config) {
structs::init(config.get_root());
// Init ignorer
let mut ignorer = Ignorer::new(config.get_root_unsafe());
ignorer.active_nsignore(!args.force);
@ -26,8 +31,6 @@ pub fn exec(args: AddArgs, config: Config) {
let mut indexer = Indexer::new(config.get_root_unsafe());
let _ = indexer.load();
nsobject::init(config.get_root_unsafe());
if args.all {
return add_dir(config.get_root_unsafe(), &mut ignorer, &mut indexer);
}
@ -37,13 +40,18 @@ pub fn exec(args: AddArgs, config: Config) {
path_to_add.push(PathBuf::from(obj_to_add));
if path_to_add.exists() {
// ignore object if it is ns config file or nsignored
if ignorer.should_ignore(&path_to_add) {
continue;
}
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);
}
} else if NsObject::from_local_path(&path_to_add).exists() {
} else if NsObject::from_local_path(&to_obj_path(&path_to_add)).exists() {
indexer.index_file(path_to_add);
} else {
// try globbing
@ -53,6 +61,20 @@ pub fn exec(args: AddArgs, config: Config) {
}
}
// print all path ignored
if ignorer.ignored_paths.len() > 0 {
println!("The following paths are ignored by one of your .nsignore files:");
for ignored_path in ignorer.ignored_paths {
println!("{}", to_repo_relative(&ignored_path).display());
}
println!(
"{}",
"hint: Use -f if you really want to add them.".bright_red()
);
// hint: Turn this message off by running
// hint: "git config advice.addIgnoredFile false"
}
dbg!(indexer.save());
/*
for all files

11
src/commands/reset.rs Normal file
View File

@ -0,0 +1,11 @@
use crate::config::config::Config;
use crate::store::indexer::Indexer;
pub struct ResetArgs {
}
pub fn exec(args: ResetArgs, config: Config) {
// Init ignorer
let indexer = Indexer::new(config.get_root_unsafe());
let _ = indexer.save();
}

View File

@ -3,6 +3,8 @@ use crate::store::{
indexer::Indexer,
nsobject::NsObject,
object::{Obj, ObjStatus, ObjType},
structs,
structs::to_obj_path,
};
use crate::utils::path;
use colored::{ColoredString, Colorize};
@ -108,6 +110,8 @@ pub fn exec(args: StatusArgs, config: Config) {
}
pub fn get_obj_changes(args: &StatusArgs, config: &Config) -> ObjStaged {
structs::init(config.get_root());
// 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()
@ -235,14 +239,13 @@ fn compare_dir(
}
};
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 local_obj = Obj::from_local_path(&entry.path().into());
if entry.path().is_dir() {
if local_obj.is_new() {
// TODO: opti move files in new directory
if indexer.is_staged_parent(&local_obj) {
@ -280,7 +283,7 @@ fn compare_dir(
}
// Read ns objects to find deleted
let entries = NsObject::from_local_path(&path);
let entries = NsObject::from_local_path(&to_obj_path(&path));
for entry in entries.iter() {
if entry.is_file() {
match local_files.get(entry.get_obj_path().to_str().unwrap()) {
@ -324,8 +327,7 @@ fn add_childs(root_path: &PathBuf, path: &PathBuf, res: Arc<ObjStatuses>) {
}
};
let repo_relative_entry = path::to_repo_relative(&entry.path(), root_path);
let local_obj = Obj::from_local_path(&repo_relative_entry);
let local_obj = Obj::from_local_path(&entry.path().into());
if entry.path().is_dir() {
if entry.path().ends_with(".nextsync") {
continue;

View File

@ -1,6 +1,7 @@
use std::env;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use crate::store::structs;
///
/// # Parameters

View File

@ -15,6 +15,7 @@ fn main() {
subcommands::init::create(),
subcommands::add::create(),
subcommands::status::create(),
subcommands::reset::create(),
]);
// .setting(clap::AppSettings::SubcommandRequiredElseHelp);
@ -24,6 +25,7 @@ fn main() {
Some(("init", args)) => subcommands::init::handler(args),
Some(("add", args)) => subcommands::add::handler(args),
Some(("status", args)) => subcommands::status::handler(args),
Some(("reset", args)) => subcommands::reset::handler(args),
Some((_, _)) => {}
None => {}
};

View File

@ -3,3 +3,4 @@ pub mod indexer;
pub mod nsignore;
pub mod nsobject;
pub mod object;
pub mod structs;

View File

@ -9,6 +9,8 @@ pub struct Ignorer {
use_nsignore: bool,
/// Nsignore's rules
rules: OnceLock<Vec<String>>,
/// Path that have been ignored by should_ignore function
pub ignored_paths: Vec<PathBuf>,
childs: Option<Vec<Box<Ignorer>>>,
}
@ -18,6 +20,7 @@ impl Ignorer {
path: path.to_path_buf(),
use_nsignore: true,
rules: OnceLock::new(),
ignored_paths: Vec::new(),
childs: None,
}
}
@ -52,6 +55,11 @@ impl Ignorer {
///
/// * `path`:
pub fn should_ignore(&mut self, path: &PathBuf) -> bool {
self.is_config_file(path) || (self.use_nsignore && self.is_ignored(path))
let should = self.is_config_file(path) || (self.use_nsignore && self.is_ignored(path));
if should {
self.ignored_paths.push(path.clone());
}
should
}
}

View File

@ -27,9 +27,9 @@ fn sort_paths_hierarchically(paths: &mut Vec<PathBuf>) {
}
#[derive(Debug)]
struct IndexedObj {
pub struct IndexedObj {
obj_type: ObjType,
path: PathBuf,
pub path: PathBuf,
}
pub struct Indexer {
@ -76,6 +76,14 @@ impl Indexer {
dbg!(&self.indexed_objs);
}
pub fn clear(&mut self) {
self.indexed_objs.clear();
}
pub fn get_indexed_objs(&self) -> &Vec<IndexedObj> {
&self.indexed_objs
}
fn is_indexed(&self, obj: &IndexedObj) -> bool {
self.indexed_objs
.iter()

View File

@ -1,66 +1,28 @@
use crate::store::object::{Obj, ObjMetadata, ObjType};
use crypto::digest::Digest;
use crypto::sha1::Sha1;
use crate::store::{
object::{Obj, ObjMetadata, ObjType},
structs::{NsObjPath, ObjPath},
};
use std::path::PathBuf;
use std::sync::OnceLock;
pub static REPO_ROOT: OnceLock<PathBuf> = OnceLock::new();
pub fn init(repo_root: &PathBuf) {
REPO_ROOT.set(repo_root.clone());
}
type NsObjectChilds = Vec<Box<NsObject>>;
struct NsObjectPath {
path: PathBuf,
}
impl From<&str> for NsObjectPath {
fn from(hash: &str) -> Self {
let (dir, res) = hash.split_at(2);
let mut ns_obj_path = match REPO_ROOT.get() {
Some(path) => path.clone(),
None => {
panic!("fatal: 'REPO_ROOT' not set, you must initialize nsobject before using it!")
}
};
ns_obj_path.push(dir);
ns_obj_path.push(res);
NsObjectPath { path: ns_obj_path }
}
}
impl From<&PathBuf> for NsObjectPath {
fn from(obj_path: &PathBuf) -> Self {
let mut hasher = Sha1::new();
hasher.input_str(
obj_path
.to_str()
.expect("Cannot contains non UTF-8 char in path"),
);
NsObjectPath::from(hasher.result_str().as_str())
}
}
pub struct NsObject {
pub obj_type: ObjType,
obj_path: OnceLock<PathBuf>,
nsobj_path: OnceLock<PathBuf>,
/// path of the obj in the repo
obj_path: OnceLock<ObjPath>,
/// path of the nsobj file in the store
nsobj_path: OnceLock<NsObjPath>,
childs: OnceLock<NsObjectChilds>,
index: usize,
}
impl NsObject {
pub fn from_local_path(path: &PathBuf) -> Self {
pub fn from_local_path(path: &ObjPath) -> Self {
NsObject {
obj_type: ObjType::Obj,
obj_path: OnceLock::from(path.to_path_buf()),
obj_path: OnceLock::from(path.clone()),
nsobj_path: OnceLock::new(),
childs: OnceLock::new(),
index: 0,
}
}
@ -68,19 +30,18 @@ impl NsObject {
NsObject {
obj_type: ObjType::Obj,
obj_path: OnceLock::new(),
nsobj_path: OnceLock::from(NsObjectPath::from(hash).path),
nsobj_path: OnceLock::from(NsObjPath::from(hash)),
childs: OnceLock::new(),
index: 0,
}
}
pub fn get_obj_path(&self) -> &PathBuf {
pub fn get_obj_path(&self) -> &ObjPath {
self.obj_path.get_or_init(|| todo!())
}
fn get_nsobj_path(&self) -> &PathBuf {
fn get_nsobj_path(&self) -> &NsObjPath {
self.nsobj_path
.get_or_init(|| NsObjectPath::from(self.get_obj_path()).path)
.get_or_init(|| NsObjPath::from(self.get_obj_path()))
}
/// Return the existence of the nsobj in the store

View File

@ -1,4 +1,4 @@
use crate::store::nsobject::NsObject;
use crate::store::{structs::ObjPath, nsobject::NsObject};
use crate::utils::path;
use std::fs;
use std::path::PathBuf;
@ -55,16 +55,16 @@ pub struct Obj {
obj_type: ObjType,
status: OnceLock<ObjStatus>,
/// path of the object from root
obj_path: PathBuf,
obj_path: ObjPath,
}
impl Obj {
pub fn from_local_path(path: &PathBuf) -> Self {
pub fn from_local_path(path: &ObjPath) -> Self {
// todo set state
Obj {
obj_type: ObjType::Obj,
status: OnceLock::new(),
obj_path: path.to_path_buf(),
obj_path: path.clone(),
}
}
@ -101,7 +101,7 @@ impl Obj {
}
pub fn get_metadata(&self) -> Option<ObjMetadata> {
let metadata = match fs::metadata(&self.obj_path) {
let metadata = match fs::metadata(&*self.obj_path) {
Ok(m) => m,
Err(err) => {
eprintln!(

120
src/store/structs.rs Normal file
View File

@ -0,0 +1,120 @@
use crate::utils::{path, tests};
use crypto::digest::Digest;
use crypto::sha1::Sha1;
use std::ops::{Deref, DerefMut};
use std::path::PathBuf;
use std::sync::{LazyLock, Mutex, OnceLock};
// for tests only as it is mutable
static REPO_ROOT_DEV: LazyLock<Mutex<Vec<PathBuf>>> =
LazyLock::new(|| Mutex::new(vec![PathBuf::new()]));
static REPO_ROOT: OnceLock<PathBuf> = OnceLock::new();
pub fn init(repo_root: &Option<PathBuf>) {
if tests::is_var_setup() {
REPO_ROOT_DEV.lock().unwrap()[0] = repo_root.clone().unwrap();
return;
}
if let Some(repo_root) = repo_root {
if REPO_ROOT.get().is_none() {
let _ = REPO_ROOT.set(repo_root.clone());
}
} else {
eprintln!("REPO_ROOT failed to initialize");
}
}
pub fn get_repo_root() -> PathBuf {
if tests::is_var_setup() {
return REPO_ROOT_DEV.lock().unwrap()[0].clone();
}
match REPO_ROOT.get() {
Some(path) => path.clone(),
None => {
panic!("fatal: 'REPO_ROOT' not set, you must initialize nsobject before using it!")
}
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct ObjPath {
path: PathBuf,
}
impl Deref for ObjPath {
type Target = PathBuf;
fn deref(&self) -> &PathBuf {
&self.path
}
}
impl DerefMut for ObjPath {
fn deref_mut(&mut self) -> &mut PathBuf {
&mut self.path
}
}
pub fn to_obj_path(path: &PathBuf) -> ObjPath {
ObjPath { path: path.clone() }
}
impl Into<ObjPath> for &PathBuf {
fn into(self) -> ObjPath {
ObjPath {
path: path::to_repo_relative(self),
}
}
}
impl Into<ObjPath> for PathBuf {
fn into(self) -> ObjPath {
ObjPath {
path: path::to_repo_relative(&self),
}
}
}
#[derive(Debug, PartialEq)]
pub struct NsObjPath {
path: PathBuf,
}
impl Deref for NsObjPath {
type Target = PathBuf;
fn deref(&self) -> &PathBuf {
&self.path
}
}
impl DerefMut for NsObjPath {
fn deref_mut(&mut self) -> &mut PathBuf {
&mut self.path
}
}
impl From<&str> for NsObjPath {
fn from(hash: &str) -> Self {
let (dir, res) = hash.split_at(2);
let mut ns_obj_path = get_repo_root();
ns_obj_path.push(dir);
ns_obj_path.push(res);
NsObjPath { path: ns_obj_path }
}
}
impl From<&ObjPath> for NsObjPath {
fn from(obj_path: &ObjPath) -> Self {
let mut hasher = Sha1::new();
hasher.input_str(
obj_path
.to_str()
.expect("Cannot contains non UTF-8 char in path"),
);
NsObjPath::from(hasher.result_str().as_str())
}
}

View File

@ -1,3 +1,4 @@
pub mod add;
pub mod init;
pub mod reset;
pub mod status;

17
src/subcommands/reset.rs Normal file
View File

@ -0,0 +1,17 @@
use clap::{Arg, ArgAction, ArgMatches, Command};
use crate::commands;
use crate::commands::reset::ResetArgs;
use crate::config::config::Config;
pub fn create() -> Command {
Command::new("reset")
.about("Clear the index")
}
pub fn handler(args: &ArgMatches) {
commands::reset::exec(
ResetArgs {},
Config::new(),
);
}

View File

@ -1 +1,2 @@
pub mod path;
pub mod tests;

View File

@ -1,7 +1,15 @@
use std::path::{Path, PathBuf, Component};
use std::path::{Component, Path, PathBuf};
use crate::store::structs;
pub fn to_repo_relative(path: &PathBuf, root: &PathBuf) -> PathBuf {
path.strip_prefix(root).unwrap().to_path_buf()
pub fn to_repo_relative(path: &PathBuf) -> PathBuf {
let root = structs::get_repo_root();
path.strip_prefix(&root)
.expect(&format!(
"Expect '{}' to be in the repo '{}'",
path.display(),
root.display()
))
.to_path_buf()
}
pub fn to_string(path: &PathBuf) -> String {
@ -18,9 +26,7 @@ pub fn to_string(path: &PathBuf) -> String {
/// This function ensures a given path ending with '/' still
/// ends with '/' after normalization.
pub fn normalize_path<P: AsRef<Path>>(path: P) -> PathBuf {
let ends_with_slash = path.as_ref()
.to_str()
.map_or(false, |s| s.ends_with('/'));
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 {

15
src/utils/tests.rs Normal file
View File

@ -0,0 +1,15 @@
use std::env;
#[cfg(test)]
pub fn is_running() -> bool {
true
}
#[cfg(not(test))]
pub fn is_running() -> bool {
false
}
pub fn is_var_setup() -> bool {
env::var("RUNNING_TESTS").is_ok()
}

49
tests/add_test.rs Normal file
View File

@ -0,0 +1,49 @@
mod common;
use common::client::ClientTest;
use nextsync::store::indexer::Indexer;
use nextsync::commands::status::{get_obj_changes, StatusArgs};
use nextsync::config::config::Config;
use std::io;
use std::path::PathBuf;
const DEFAULT_STATUS_ARG: StatusArgs = StatusArgs { nostyle: false };
fn indexed_expected(indexer: &Indexer, expected: Vec<&str>) {
let objs = indexer.get_indexed_objs();
assert_eq!(objs.len(), expected.len());
for obj in expected {
assert!(objs
.iter()
.position(|e| { e.path == PathBuf::from(obj) })
.is_some());
}
}
#[test]
fn add_ignored_file() -> io::Result<()> {
let mut client = ClientTest::new("add__simple_file").init();
client.add_ignore_rule("foo");
client.add_file("foo", "foo")?;
let mut indexer = Indexer::new(client.get_config().get_root_unsafe());
client.exec_ok("add foo");
let _ = indexer.load();
indexed_expected(&indexer, vec![]);
client.exec_ok("add foo -f");
let _ = indexer.load();
indexed_expected(&indexer, vec!["foo"]);
client.ok()
}
// add double globbing
// add all
// add folders
// add part of folders
// add all folder
// add automatic ignored if is tracked

View File

@ -1,4 +1,5 @@
use nextsync::config::config::Config;
use std::fs::OpenOptions;
use rand::{distributions::Alphanumeric, Rng};
use std::env;
use std::fs::{self, File};
@ -125,6 +126,18 @@ impl ClientTest {
Ok(())
}
pub fn add_ignore_rule(&self, rule: &str) {
let mut nsignore_path = self.volume.clone();
nsignore_path.push_str("/.nsignore");
let mut file = OpenOptions::new()
.write(true)
.create(true)
.append(true)
.open(nsignore_path).unwrap();
let _ = writeln!(file, "{rule}").unwrap();
}
// pub fn has_file(&mut self, file: &str, content: &str) -> bool {
// let full_path = PathBuf::from(self.volume.clone()).join(file);

4
tests/reset_test.rs Normal file
View File

@ -0,0 +1,4 @@
// reset all
// reset file
// reset folder
// reset unknown

View File

@ -8,7 +8,7 @@ use std::path::PathBuf;
const DEFAULT_STATUS_ARG: StatusArgs = StatusArgs { nostyle: false };
fn status_exepected(config: &Config, staged: Vec<&str>, not_staged: Vec<&str>) {
fn status_expected(config: &Config, staged: Vec<&str>, not_staged: Vec<&str>) {
let res = get_obj_changes(&DEFAULT_STATUS_ARG, config);
assert_eq!(res.staged.len(), staged.len());
@ -36,16 +36,15 @@ fn simple_file() -> io::Result<()> {
let mut client = ClientTest::new("status__simple_file").init();
client.add_file("foo", "foo")?;
status_exepected(&client.get_config(), vec![], vec!["foo"]);
status_expected(&client.get_config(), vec![], vec!["foo"]);
client.exec_ok("add foo");
status_exepected(&client.get_config(), vec!["foo"], vec![]);
status_expected(&client.get_config(), vec!["foo"], vec![]);
client.ok()
}
#[test]
#[ignore]
fn all_folder() -> io::Result<()> {
let mut client = ClientTest::new("status__all_folder").init();
@ -53,10 +52,10 @@ fn all_folder() -> io::Result<()> {
client.add_file("dir/foo", "foo")?;
client.add_file("dir/bar", "bar")?;
client.add_file("foo", "foo")?;
status_exepected(&client.get_config(), vec![], vec!["foo", "dir"]);
status_expected(&client.get_config(), vec![], vec!["foo", "dir"]);
client.exec_ok("add dir");
status_exepected(&client.get_config(), vec!["dir"], vec!["foo"]);
status_expected(&client.get_config(), vec!["dir/foo", "dir/bar"], vec!["foo"]);
client.ok()
}
@ -70,10 +69,10 @@ fn all_folder_current() -> io::Result<()> {
client.add_file("dir/foo", "foo")?;
client.add_file("dir/bar", "bar")?;
client.add_file("foor", "foor")?;
status_exepected(&client.get_config(), vec![], vec!["foor", "dir"]);
status_expected(&client.get_config(), vec![], vec!["foor", "dir"]);
client.exec_ok("add dir");
status_exepected(
status_expected(
&Config::from(Some(&String::from("./dir"))),
vec!["foor", "bar"],
vec!["../foo"],
@ -90,10 +89,10 @@ fn part_of_folder() -> io::Result<()> {
client.add_file("dir/foo", "foo")?;
client.add_file("dir/bar", "bar")?;
client.add_file("foor", "foor")?;
status_exepected(&client.get_config(), vec![], vec!["foor", "dir"]);
status_expected(&client.get_config(), vec![], vec!["foor", "dir"]);
client.exec_ok("add dir/foo");
status_exepected(
status_expected(
&client.get_config(),
vec!["dir/foo"],
vec!["foor", "dir/bar"],
@ -103,3 +102,6 @@ fn part_of_folder() -> io::Result<()> {
}
// ../folder/file add
// force add ignored file
// status without ignored file
// all folder without ignored file