diff --git a/Cargo.lock b/Cargo.lock index bc014a0..a6695fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,6 +74,24 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "anyhow" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + [[package]] name = "backtrace" version = "0.3.71" @@ -107,6 +125,19 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +[[package]] +name = "blake3" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cca6d3674597c30ddf2c587bf8d9d65c9a84d2326d941cc79c9842dfe0ef52" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + [[package]] name = "byteorder" version = "1.5.0" @@ -214,6 +245,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "constant_time_eq" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" + [[package]] name = "core-foundation" version = "0.9.4" @@ -239,6 +276,31 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + [[package]] name = "directories" version = "5.0.1" @@ -272,6 +334,12 @@ dependencies = [ "shared_child", ] +[[package]] +name = "either" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" + [[package]] name = "equivalent" version = "1.0.1" @@ -444,6 +512,18 @@ version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +[[package]] +name = "merkle_hash" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fad8dc52477aa6f1751748a5ee1c6d50db7092e8dab1d687840dfa23e2ae4e5" +dependencies = [ + "anyhow", + "blake3", + "camino", + "rayon", +] + [[package]] name = "miniz_oxide" version = "0.7.2" @@ -536,6 +616,26 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -842,6 +942,7 @@ dependencies = [ "duct", "flate2", "keyring", + "merkle_hash", "once_cell", "relative-path", "rpassword", diff --git a/Cargo.toml b/Cargo.toml index 3a29528..685d11b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,4 +30,5 @@ duct = "=0.13.7" camino = "1.1.6" once_cell = "1.19.0" tar = "0.4.40" -flate2 = "1.0.30" \ No newline at end of file +flate2 = "1.0.30" +merkle_hash = "3.6" \ No newline at end of file diff --git a/examples/config/end/here/there/example_im_there.sh b/examples/config/end/here/there/example_im_there.sh new file mode 100755 index 0000000..c9571d8 --- /dev/null +++ b/examples/config/end/here/there/example_im_there.sh @@ -0,0 +1,2 @@ +#!/bin/sh +echo "I'm there!" \ No newline at end of file diff --git a/examples/config/end/here/there/tidploy.toml b/examples/config/end/here/there/tidploy.toml new file mode 100644 index 0000000..27441ad --- /dev/null +++ b/examples/config/end/here/there/tidploy.toml @@ -0,0 +1,2 @@ +[argument] +executable = "here/there/example_im_there.sh" \ No newline at end of file diff --git a/examples/config/end/here/tidploy.toml b/examples/config/end/here/tidploy.toml new file mode 100644 index 0000000..241f01c --- /dev/null +++ b/examples/config/end/here/tidploy.toml @@ -0,0 +1,2 @@ +[argument] +executable = "here/there/example_im_not_here.sh" \ No newline at end of file diff --git a/examples/config/start/tidploy.toml b/examples/config/start/tidploy.toml new file mode 100644 index 0000000..8ee7c97 --- /dev/null +++ b/examples/config/start/tidploy.toml @@ -0,0 +1,3 @@ +[state.address] +path = "../end" +state_path = "here/there" \ No newline at end of file diff --git a/src/archives.rs b/src/archives.rs index 73f91e2..970a520 100644 --- a/src/archives.rs +++ b/src/archives.rs @@ -1,8 +1,8 @@ use crate::errors::{RepoError, TarError}; use crate::filesystem::{FileError, FileErrorKind}; use crate::process::process_out; -use std::fs; use camino::{Utf8Path, Utf8PathBuf}; +use std::fs; use std::process::Command as Cmd; use tracing::debug; diff --git a/src/commands.rs b/src/commands.rs index d457ce8..ff94357 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -112,11 +112,15 @@ enum ErrorRepr { } fn create_repo(repo: Repo) -> Result { - - let cache_dir = get_dirs().map_err(|e| RepoError::File(FileError { - msg: "Error getting dirs!".to_owned(), - source: e - }))?.cache.clone(); + let cache_dir = get_dirs() + .map_err(|e| { + RepoError::File(FileError { + msg: "Error getting dirs!".to_owned(), + source: e, + }) + })? + .cache + .clone(); let repo_name = repo.dir_name(); let repo_path = cache_dir.join(&repo_name); @@ -158,10 +162,15 @@ fn switch_to_revision( } fn prepare_from_state(state: &State, repo_path: &Utf8Path) -> Result<(), ErrorRepr> { - let cache_dir = get_dirs().map_err(|e| ErrorRepr::Repo(RepoError::File(FileError { - msg: "Cache dir not UTF-8!".to_owned(), - source: e - })))?.cache.clone(); + let cache_dir = get_dirs() + .map_err(|e| { + ErrorRepr::Repo(RepoError::File(FileError { + msg: "Cache dir not UTF-8!".to_owned(), + source: e, + })) + })? + .cache + .clone(); let archives = cache_dir.join("archives"); let deploy_encoded = B64USNP.encode(state.deploy_path.as_str()); let archive_name = format!( @@ -219,10 +228,15 @@ fn prepare_command( let _prep_enter = prepare_san.enter(); let repo_path = if no_create { - let cache_dir = get_dirs().map_err(|e| ErrorRepr::Repo(RepoError::File(FileError { - msg: "Cache dir not UTF-8!".to_owned(), - source: e - })))?.cache.clone(); + let cache_dir = get_dirs() + .map_err(|e| { + ErrorRepr::Repo(RepoError::File(FileError { + msg: "Cache dir not UTF-8!".to_owned(), + source: e, + })) + })? + .cache + .clone(); let repo_path = cache_dir.join(repo.dir_name()); if !repo_path.exists() { @@ -297,10 +311,12 @@ pub fn run_cli() -> Result { enter_dl.exit(); let state = prepare_command(cli_state.clone(), no_create, state.repo)?.unwrap(); - let dirs = get_dirs().map_err(|e| ErrorRepr::Repo(RepoError::File(FileError { - msg: "Cache dir not UTF-8!".to_owned(), - source: e - })))?; + let dirs = get_dirs().map_err(|e| { + ErrorRepr::Repo(RepoError::File(FileError { + msg: "Cache dir not UTF-8!".to_owned(), + source: e, + })) + })?; let cache_dir = dirs.cache.as_path(); let tmp_dir = dirs.tmp.as_path(); let deploy_encoded = B64USNP.encode(state.deploy_path.as_str()); diff --git a/src/filesystem.rs b/src/filesystem.rs index 8ce3e68..ba90726 100644 --- a/src/filesystem.rs +++ b/src/filesystem.rs @@ -1,9 +1,9 @@ use camino::{Utf8Path, Utf8PathBuf}; use directories::ProjectDirs; +use once_cell::sync::OnceCell; use relative_path::{RelativePath, RelativePathBuf}; use std::{env, io::Error as StdIOError}; use thiserror::Error as ThisError; -use once_cell::sync::OnceCell; #[derive(ThisError, Debug)] #[error("{msg} {source}")] @@ -44,26 +44,58 @@ pub(crate) fn get_dirs() -> Result<&'static Dirs, FileErrorKind> { }) } +#[derive(Debug, ThisError)] +pub(crate) enum RelativePathError { + #[error("The full path {0} is not a child of the root (did you use too many ..?")] + Child(String), + #[error( + "An error occurred when canonicalizing the full path. Does it exist and is it UTF-8? {0}" + )] + Canonicalize(#[from] std::io::Error), +} + pub trait WrapToPath { fn to_utf8_path>(&self, path: P) -> Utf8PathBuf; + + // fn to_path_canon_checked(&self, root: &Utf8Path) -> Result; } -impl WrapToPath for RelativePath -{ +impl WrapToPath for RelativePath { fn to_utf8_path>(&self, path: P) -> Utf8PathBuf { let path = path.as_ref().as_std_path(); let std_path = self.to_path(path); // Since we started with Utf8Path, we know this will work Utf8PathBuf::from_path_buf(std_path).unwrap() } + + // fn to_path_canon_checked(&self, root: &Utf8Path) -> Result { + // let full = self.to_utf8_path(root); + + + // if !full_canon.starts_with(root) { + // Err(RelativePathError::Child(full_canon.to_string())) + // } else { + // Ok(full_canon) + // } + // } } -impl WrapToPath for RelativePathBuf -{ +impl WrapToPath for RelativePathBuf { fn to_utf8_path>(&self, path: P) -> Utf8PathBuf { let path = path.as_ref().as_std_path(); let std_path = self.to_path(path); // Since we started with Utf8Path, we know this will work Utf8PathBuf::from_path_buf(std_path).unwrap() } -} \ No newline at end of file + + // fn to_path_canon_checked(&self, root: &Utf8Path) -> Result { + // let full = self.to_utf8_path(root); + // let full_canon = full.canonicalize_utf8()?; + + // if !full_canon.starts_with(root) { + // Err(RelativePathError::Child(full_canon.to_string())) + // } else { + // Ok(full_canon) + // } + // } +} diff --git a/src/git.rs b/src/git.rs index 89bb701..e6258bd 100644 --- a/src/git.rs +++ b/src/git.rs @@ -227,11 +227,17 @@ pub(crate) fn checkout(repo_path: &Utf8Path, commit_sha: &str) -> Result<(), Rep Ok(()) } -pub(crate) fn checkout_path(repo_path: &Utf8Path, deploy_path: &RelativePath) -> Result<(), RepoError> { +pub(crate) fn checkout_path( + repo_path: &Utf8Path, + deploy_path: &RelativePath, +) -> Result<(), RepoError> { checkout_paths(repo_path, vec![deploy_path]) } -pub(crate) fn checkout_paths(repo_path: &Utf8Path, paths: Vec<&RelativePath>) -> Result<(), RepoError> { +pub(crate) fn checkout_paths( + repo_path: &Utf8Path, + paths: Vec<&RelativePath>, +) -> Result<(), RepoError> { if !repo_path.exists() { return Err(RepoError::NotCreated); } diff --git a/src/next/api.rs b/src/next/api.rs index 8316762..fa6c5b9 100644 --- a/src/next/api.rs +++ b/src/next/api.rs @@ -1,3 +1,4 @@ +use super::archives::archive_command as inner_archive_command; use super::run::run_command_input as inner_run_command; use super::secrets::secret_command as inner_secret_command; use super::state::StateIn; @@ -26,16 +27,16 @@ pub use crate::state::StateContext; #[derive(Default)] pub struct GlobalArguments { pub cwd_context: bool, + pub resolve_root: Option, pub state_root: Option, - pub state_path: Option - // pub repo_url: Option, - // pub deploy_path: Option, - // pub tag: Option, + pub state_path: Option, // pub repo_url: Option, + // pub deploy_path: Option, + // pub tag: Option, } impl From for StateIn { fn from(value: GlobalArguments) -> Self { - Self::from_args(value.cwd_context, value.state_path, value.state_root) + Self::from_args(value.cwd_context, value.resolve_root, value.state_path, value.state_root) } } @@ -97,3 +98,17 @@ pub fn secret_command( } }) } + +#[non_exhaustive] +#[derive(Default)] +pub struct ArchiveArguments {} + +pub fn archive_command( + global_args: GlobalArguments, + args: ArchiveArguments, +) -> Result<(), CommandError> { + inner_archive_command().map_err(|e| CommandError { + msg: "An error occurred in the inner application layer.".to_owned(), + source: e, + }) +} diff --git a/src/next/archives.rs b/src/next/archives.rs index ddd8340..74fdd6c 100644 --- a/src/next/archives.rs +++ b/src/next/archives.rs @@ -1,12 +1,35 @@ -use std::fs::File; -use flate2::Compression; +use camino::Utf8Path; +use color_eyre::eyre::Report; +use flate2::read::GzDecoder; use flate2::write::GzEncoder; +use flate2::Compression; +use std::fs::File; +use std::io; +use tar::Archive; -fn archive() -> Result<(), ArchiveError> { - let tar_gz = File::create("archive.tar.gz")?; +fn archive(target_path: &Utf8Path, source_path: &Utf8Path) -> Result<(), io::Error> { + let tar_gz = File::create(target_path)?; let enc = GzEncoder::new(tar_gz, Compression::default()); let mut tar = tar::Builder::new(enc); - tar.append_dir_all("backup/logs", "/var/log")?; - let a = tar.into_inner(); + tar.append_dir_all("", source_path)?; + tar.into_inner()?; + Ok(()) +} + +fn unarchive(target_path: &Utf8Path, archive_path: &Utf8Path) -> Result<(), std::io::Error> { + let tar_gz = File::open(archive_path)?; + let tar = GzDecoder::new(tar_gz); + let mut archive = Archive::new(tar); + archive.unpack(target_path)?; + + Ok(()) +} + +pub(crate) fn archive_command() -> Result<(), Report> { + let source = Utf8Path::new("/tmp/tidploy/a.tar.gz"); + let target = Utf8Path::new("/tmp/tidploy/something2"); + + unarchive(target, source)?; + Ok(()) -} \ No newline at end of file +} diff --git a/src/next/commands.rs b/src/next/commands.rs index 9d6ede0..4a66482 100644 --- a/src/next/commands.rs +++ b/src/next/commands.rs @@ -28,15 +28,20 @@ pub struct NextSub { // /// The path inside the repository that should be used as the primary config source. // #[arg(short, long, global = true)] // deploy_pth: Option, - /// By default, tidploy searches for the root directory of the Git repository that the command is called /// from and takes all other inputs as relative to there. To instead ignore the current Git repository /// and simply take the current working directory as the root, enable this flag. #[arg(short = 'c', long = "cwd")] cwd_context: bool, - /// Location relative to context root where you want to begin reading configs. Defaults to be equal - /// to context root. + + /// Directory to start resolving from. Can either be an absolute path (this requires --cwd), or relative to + /// the current directory or Git root dir + #[arg(long = "resolve-root")] + resolve_root: Option, + + /// Location relative to resolve root where you want to begin reading configs. Defaults to be equal + /// to resolve root. #[arg(long = "state-root")] state_root: Option, @@ -71,10 +76,11 @@ pub fn match_command(next_sub: NextSub) -> Result { subcommand, cwd_context, state_path, - state_root + state_root, + resolve_root } = next_sub; - let state_in = StateIn::from_args(cwd_context, state_path, state_root); + let state_in = StateIn::from_args(cwd_context, resolve_root, state_path, state_root); match subcommand { crate::next::commands::NextCommands::Secret { key } => { @@ -85,7 +91,7 @@ pub fn match_command(next_sub: NextSub) -> Result { crate::next::commands::NextCommands::Run { executable, variables, - execution_path + execution_path, } => { let out = run_command(state_in, None, executable, execution_path, variables)?; // If [process::ExitCode::from_raw] gets stabilized this can be simplified diff --git a/src/next/config.rs b/src/next/config.rs index 0f57287..9754b3d 100644 --- a/src/next/config.rs +++ b/src/next/config.rs @@ -1,8 +1,4 @@ -use std::{ - collections::HashMap, - fs, - ops::ControlFlow, -}; +use std::{collections::HashMap, fs, ops::ControlFlow}; use camino::{Utf8Path, Utf8PathBuf}; use relative_path::{RelativePath, RelativePathBuf}; @@ -24,7 +20,7 @@ pub(crate) struct ConfigScope { pub(crate) name: Option, pub(crate) sub: Option, pub(crate) service: Option, - pub(crate) require_hash: Option + pub(crate) require_hash: Option, } #[derive(Deserialize, Debug, Default)] @@ -38,8 +34,17 @@ pub(crate) struct ArgumentConfig { #[derive(Deserialize, Debug)] #[serde(untagged)] pub(crate) enum ConfigAddress { - Local { path: String }, - Git { url: String, git_ref: String } + Local { + path: String, + state_path: Option, + state_root: Option, + }, + Git { + url: String, + git_ref: String, + state_path: Option, + state_root: Option, + }, } #[derive(Deserialize, Debug)] @@ -52,7 +57,7 @@ pub(crate) struct StateConfig { #[derive(Deserialize, Debug, Default)] pub(crate) struct Config { pub(crate) argument: Option, - pub(crate) state: Option + pub(crate) state: Option, } pub(crate) fn load_dploy_config(config_dir_path: &Utf8Path) -> Result { @@ -112,7 +117,7 @@ fn overwrite_scope(original: ConfigScope, replacing: ConfigScope) -> ConfigScope name: overwrite_option(original.name, replacing.name), sub: overwrite_option(original.sub, replacing.sub), service: overwrite_option(original.service, replacing.service), - require_hash: overwrite_option(original.require_hash, replacing.require_hash) + require_hash: overwrite_option(original.require_hash, replacing.require_hash), } } @@ -161,7 +166,7 @@ fn overwrite_state_config(base: StateConfig, replacing: StateConfig) -> StateCon StateConfig { state_path: replacing.state_path.or(base.state_path), state_root: replacing.state_root.or(base.state_root), - address: replacing.address.or(base.address) + address: replacing.address.or(base.address), } } @@ -172,10 +177,30 @@ fn overwrite_config(root_config: Config, overwrite_config: Config) -> Config { overwrite_config.argument, &overwrite_arguments, ), - state: merge_option(root_config.state, overwrite_config.state, &overwrite_state_config) + state: merge_option( + root_config.state, + overwrite_config.state, + &overwrite_state_config, + ), } } +/// The relative path is normalized, so if it contains symlinks unexpected behavior might happen. +/// This is designed to work only for simple descent down a directory. +fn get_component_paths(start_path: &Utf8Path, final_path: &RelativePath) -> Vec { + let paths: Vec = final_path.normalize() + .components() + .scan(RelativePathBuf::new(), |state, component| { + state.push(component); + Some(state.to_utf8_path(start_path)) + }) + .collect(); + + paths +} + +/// Be sure the relative path is just a simple ./child/child/child2 ...etc relative path (the leading +/// ./ is optional) pub(crate) fn traverse_configs( start_path: &Utf8Path, final_path: &RelativePath, @@ -187,13 +212,7 @@ pub(crate) fn traverse_configs( let root_config = load_dploy_config(start_path)?; - let paths: Vec = final_path - .components() - .scan(RelativePathBuf::new(), |state, component| { - state.push(component); - Some(state.to_utf8_path(start_path)) - }) - .collect(); + let paths = get_component_paths(start_path, final_path); let combined_config = paths.iter().try_fold(root_config, |state, path| { let inner_config = load_dploy_config(path); @@ -209,3 +228,29 @@ pub(crate) fn traverse_configs( ControlFlow::Continue(config) => Ok(config), } } + + +#[cfg(test)] +mod tests { + use std::env; + + use camino::Utf8PathBuf; + use relative_path::RelativePathBuf; + + use super::get_component_paths; + + #[test] + fn paths_simple() { + let path = Utf8PathBuf::from_path_buf(env::current_dir().unwrap()).unwrap(); + let relative1 = RelativePathBuf::from("./this/that"); + let relative2 = RelativePathBuf::from("this/that"); + + let paths1 = get_component_paths(&path, &relative1); + let paths2 = get_component_paths(&path, &relative2); + let comp2 = path.join("this").join("that"); + let comp1 = path.join("this"); + + assert_eq!(vec![comp1.clone(), comp2.clone()], paths1); + assert_eq!(vec![comp1, comp2], paths2); + } +} diff --git a/src/next/errors.rs b/src/next/errors.rs index b8f8f40..e1ead4e 100644 --- a/src/next/errors.rs +++ b/src/next/errors.rs @@ -3,7 +3,7 @@ use std::io::Error as IOError; use thiserror::Error as ThisError; use tracing_error::TracedError; -use super::state::Address; +// use crate::filesystem::RelativePathError; #[derive(ThisError, Debug)] pub(crate) enum SecretError { @@ -45,7 +45,9 @@ pub(crate) enum StateErrorKind { #[error("{0}")] Config(#[from] ConfigError), #[error("{0}")] - Address(#[from] AddressError) + Address(#[from] AddressError), + // #[error("{0}")] + // RelativePath(#[from] RelativePathError) } pub trait WrapStateErr { diff --git a/src/next/fs.rs b/src/next/fs.rs new file mode 100644 index 0000000..39458fe --- /dev/null +++ b/src/next/fs.rs @@ -0,0 +1,23 @@ +use camino::{Utf8Path, Utf8PathBuf}; +use directories::ProjectDirs; +use std::{env, sync::OnceLock}; + + +pub(crate) struct Dirs { + pub(crate) cache: Utf8PathBuf, + pub(crate) tmp: Utf8PathBuf, +} + +pub(crate) fn get_dirs() -> &'static Dirs { + static DIRS: OnceLock = OnceLock::new(); + DIRS.get_or_init(|| { + let project_dirs = ProjectDirs::from("", "", "tidploy").unwrap(); + + let cache = project_dirs.cache_dir().to_owned(); + let tmp = env::temp_dir(); + let cache = Utf8PathBuf::from_path_buf(cache).unwrap(); + let tmp = Utf8PathBuf::from_path_buf(tmp).unwrap().join("tidploy"); + + Dirs { cache, tmp } + }) +} \ No newline at end of file diff --git a/src/next/git.rs b/src/next/git.rs index d27372f..44d5fe7 100644 --- a/src/next/git.rs +++ b/src/next/git.rs @@ -1,8 +1,10 @@ use camino::Utf8Path; +use spinoff::{spinners, Spinner}; +use tracing::debug; +use base64::{engine::general_purpose::URL_SAFE_NO_PAD as B64USNP, Engine}; use super::{ - errors::{GitError, GitProcessError}, - process::process_complete_output, + errors::{GitError, GitProcessError}, fs::{get_dirs, Dirs}, process::process_complete_output, state::parse_url_repo_name }; use core::fmt::Debug; use std::ffi::OsStr; @@ -36,16 +38,49 @@ pub(crate) fn git_root_dir(path: &Utf8Path) -> Result { run_git(path, args, "get git root dir") } -#[derive(Debug, PartialEq)] -pub(crate) struct Repo { - pub(crate) name: String, - pub(crate) encoded_url: String, - pub(crate) url: String, +pub(crate) fn repo_clone( + current_dir: &Utf8Path, + target_name: &str, + repo_url: &str, +) -> Result<(), GitError> { + debug!( + "Cloning repository {} directory at target {}", + repo_url, target_name + ); + let mut sp = Spinner::new(spinners::Line, "Cloning repository...", None); + + let clone_args = vec!["clone", "--filter=tree:0", "--sparse", repo_url, target_name]; + run_git(current_dir, clone_args, "partial clone sparse")?; + let target_dir = current_dir.join(target_name); + let checkout_args = vec!["sparse-checkout", "init", "--cone"]; + run_git(&target_dir, checkout_args, "partial clone sparse")?; + + sp.success("Repository cloned!"); + + Ok(()) } -impl Repo { - pub(crate) fn dir_name(&self) -> String { - format!("{}_{}", self.name, self.encoded_url) - } +fn do_clone() { + let dirs = get_dirs(); + let a = dirs.cache.as_path(); + let b = dirs.tmp.as_path(); + + let url = "https://github.com/tiptenbrink/tidploy.git"; + let encoded_url = B64USNP.encode(url); + let name = parse_url_repo_name(&url).unwrap(); + let dir_name = format!("{}_{}", name, encoded_url); + + let t = b; + repo_clone(t, &dir_name, url).unwrap(); } + +// mod tests { + +// use super::do_clone; + +// #[test] +// fn test_do_clone() { +// do_clone(); +// } +// } \ No newline at end of file diff --git a/src/next/mod.rs b/src/next/mod.rs index 687548e..10a7beb 100644 --- a/src/next/mod.rs +++ b/src/next/mod.rs @@ -1,4 +1,5 @@ pub mod api; +pub(crate) mod archives; pub(crate) mod commands; pub(crate) mod config; pub(crate) mod errors; @@ -8,4 +9,4 @@ pub(crate) mod resolve; pub(crate) mod run; pub(crate) mod secrets; pub(crate) mod state; -pub(crate) mod archives; \ No newline at end of file +pub(crate) mod fs; diff --git a/src/next/process.rs b/src/next/process.rs index 93d3659..ebd2bd9 100644 --- a/src/next/process.rs +++ b/src/next/process.rs @@ -88,11 +88,7 @@ pub(crate) fn run_entrypoint( let reader = cmd_expr.reader()?; - let entry_span = span!( - Level::DEBUG, - "entrypoint", - path = entrypoint.as_str() - ); + let entry_span = span!(Level::DEBUG, "entrypoint", path = entrypoint.as_str()); let _enter = entry_span.enter(); let mut out: String = String::with_capacity(128); diff --git a/src/next/resolve.rs b/src/next/resolve.rs index d4edbe4..ecaa8a9 100644 --- a/src/next/resolve.rs +++ b/src/next/resolve.rs @@ -1,16 +1,15 @@ -use std::{ - env, -}; +use std::{env, fmt::Debug}; use camino::{Utf8Path, Utf8PathBuf}; use relative_path::{RelativePath, RelativePathBuf}; -use tracing::instrument; +use tracing::{debug, instrument}; use crate::filesystem::WrapToPath; use super::{ config::{merge_vars, traverse_configs, ArgumentConfig, ConfigScope, ConfigVar}, - errors::ResolutionError, state::ResolveState, + errors::ResolutionError, + state::ResolveState, }; #[derive(Default)] @@ -28,7 +27,7 @@ impl SecretScopeArguments { service: other.service.or(self.service), sub: other.sub.or(self.sub), name: other.name.or(self.name), - require_hash: other.require_hash.or(self.require_hash) + require_hash: other.require_hash.or(self.require_hash), } } } @@ -39,7 +38,7 @@ impl From for SecretScopeArguments { service: value.service, name: value.name, sub: value.sub, - require_hash: value.require_hash + require_hash: value.require_hash, } } } @@ -80,6 +79,7 @@ pub(crate) struct SecretArguments { pub(crate) scope_args: SecretScopeArguments, } +#[derive(Debug)] pub(crate) struct SecretScope { pub(crate) service: String, pub(crate) name: String, @@ -87,6 +87,7 @@ pub(crate) struct SecretScope { pub(crate) hash: String, } +#[derive(Debug)] pub(crate) struct RunResolved { pub(crate) executable: Utf8PathBuf, pub(crate) execution_path: Utf8PathBuf, @@ -94,6 +95,7 @@ pub(crate) struct RunResolved { pub(crate) scope: SecretScope, } +#[derive(Debug)] pub(crate) struct SecretResolved { pub(crate) key: String, pub(crate) scope: SecretScope, @@ -142,8 +144,7 @@ fn env_run_args() -> RunArguments { run_arguments } -pub(crate) trait Resolve: Sized -{ +pub(crate) trait Resolve: Sized { fn merge_env_config( self, state_root: &Utf8Path, @@ -163,7 +164,11 @@ fn resolve_scope( service: scope_args.service.unwrap_or("tidploy".to_owned()), name: scope_args.name.unwrap_or(name.to_owned()), sub: scope_args.sub.unwrap_or(sub.to_owned()), - hash: if scope_args.require_hash.unwrap_or(false) { hash.to_owned() } else { "tidploy_default_hash".to_owned() }, + hash: if scope_args.require_hash.unwrap_or(false) { + hash.to_owned() + } else { + "tidploy_default_hash".to_owned() + }, } } @@ -179,10 +184,7 @@ impl Resolve for RunArguments { let merged_args = run_args_env.merge(self); - let config_run = config - .argument - .map(RunArguments::from) - .unwrap_or_default(); + let config_run = config.argument.map(RunArguments::from).unwrap_or_default(); Ok(config_run.merge(merged_args)) } @@ -227,7 +229,13 @@ impl Resolve for SecretArguments { Ok(merged_args) } - fn resolve(self, _resolve_root: &Utf8Path, name: &str, sub: &str, hash: &str) -> SecretResolved { + fn resolve( + self, + _resolve_root: &Utf8Path, + name: &str, + sub: &str, + hash: &str, + ) -> SecretResolved { let scope = resolve_scope(self.scope_args, name, sub, hash); SecretResolved { @@ -239,8 +247,13 @@ impl Resolve for SecretArguments { /// Loads config, environment variables and resolves the final arguments to make them ready for final use #[instrument(name = "merge_resolve", level = "debug", skip_all)] -pub(crate) fn merge_and_resolve(unresolved_args: impl Resolve, state: ResolveState) -> Result { +pub(crate) fn merge_and_resolve( + unresolved_args: impl Resolve, + state: ResolveState, +) -> Result { let merged_args = unresolved_args.merge_env_config(&state.state_root, &state.state_path)?; - Ok(merged_args.resolve(&state.resolve_root, &state.name, &state.sub, &state.hash)) -} \ No newline at end of file + let resolved = merged_args.resolve(&state.resolve_root, &state.name, &state.sub, &state.hash); + debug!("Resolved as {:?}", resolved); + Ok(resolved) +} diff --git a/src/next/run.rs b/src/next/run.rs index 0484a6b..fa8c370 100644 --- a/src/next/run.rs +++ b/src/next/run.rs @@ -26,7 +26,14 @@ pub(crate) fn run_command( execution_path: Option, variables: Vec, ) -> Result { - run_command_input(state_in, service, executable, execution_path, variables, None) + run_command_input( + state_in, + service, + executable, + execution_path, + variables, + None, + ) } #[instrument(name = "run", level = "debug", skip_all)] diff --git a/src/next/secrets.rs b/src/next/secrets.rs index 743f02f..f244cc7 100644 --- a/src/next/secrets.rs +++ b/src/next/secrets.rs @@ -6,9 +6,9 @@ use rpassword::prompt_password; use tracing::{debug, instrument}; use crate::next::{ - resolve::{merge_and_resolve, SecretArguments, SecretScopeArguments}, - state::create_resolve_state, - }; + resolve::{merge_and_resolve, SecretArguments, SecretScopeArguments}, + state::create_resolve_state, +}; use super::{ config::ConfigVar, @@ -46,8 +46,12 @@ fn set_keyring_secret(secret: &str, key: &str, service: &str) -> Result<(), Keyr } fn key_from_scope(scope: &SecretScope, key: &str, require_hash_match: bool) -> String { - let hash = if require_hash_match { &scope.hash } else { "tidploy_default_hash" }; - + let hash = if require_hash_match { + &scope.hash + } else { + "tidploy_default_hash" + }; + format!("{}::{}::{}:{}", scope.name, scope.sub, hash, key) } @@ -88,9 +92,11 @@ fn secret_prompt( let store_key_default_hash = key_from_scope(scope, key, false); - set_keyring_secret(&password, &store_key_default_hash, &scope.service).map_err(|e| SecretKeyringError { - msg: format!("Failed to set key {}", &store_key), - source: e, + set_keyring_secret(&password, &store_key_default_hash, &scope.service).map_err(|e| { + SecretKeyringError { + msg: format!("Failed to set key {}", &store_key), + source: e, + } })?; Ok(store_key) @@ -111,10 +117,7 @@ pub(crate) fn secret_command( service, ..Default::default() }; - let secret_args = SecretArguments { - key, - scope_args, - }; + let secret_args = SecretArguments { key, scope_args }; let resolve_state = create_resolve_state(state_in)?; let secret_resolved = merge_and_resolve(secret_args, resolve_state)?; diff --git a/src/next/state.rs b/src/next/state.rs index b2004d0..76b375a 100644 --- a/src/next/state.rs +++ b/src/next/state.rs @@ -1,13 +1,15 @@ -use std::{env::current_dir}; +use std::env::{self, current_dir}; use camino::{Utf8Path, Utf8PathBuf}; -use relative_path::RelativePathBuf; +use relative_path::{RelativePath, RelativePathBuf}; use tracing::{debug, instrument}; use crate::filesystem::WrapToPath; use super::{ - config::{traverse_configs, ConfigAddress, ConfigVar, StateConfig}, errors::{AddressError, StateError, StateErrorKind, WrapStateErr}, git::git_root_dir + config::{traverse_configs, ConfigAddress, ConfigVar, StateConfig}, + errors::{AddressError, StateError, StateErrorKind, WrapStateErr}, + git::git_root_dir, }; #[derive(Debug)] @@ -25,29 +27,36 @@ impl Default for InferContext { #[derive(Default, Debug)] pub(crate) struct StateIn { pub(crate) context: InferContext, + pub(crate) resolve_root: Option, pub(crate) state_path: Option, - pub(crate) state_root: Option + pub(crate) state_root: Option, } impl StateIn { - pub(crate) fn from_args(cwd_context: bool, state_path: Option, state_root: Option) -> Self { + pub(crate) fn from_args( + cwd_context: bool, + resolve_root: Option, + state_path: Option, + state_root: Option, + ) -> Self { let context = if cwd_context { InferContext::Cwd } else { InferContext::Git }; - + Self { context, + resolve_root, state_path, - state_root + state_root, } } } #[derive(Debug)] pub(crate) struct StatePaths { - pub(crate) context_root: Utf8PathBuf, + pub(crate) resolve_root: Utf8PathBuf, pub(crate) state_root: RelativePathBuf, pub(crate) state_path: RelativePathBuf, } @@ -60,20 +69,57 @@ impl StatePaths { current_dir().to_state_err("Getting current dir for new StatePaths".to_owned())?; let current_dir = Utf8PathBuf::from_path_buf(current_dir).map_err(|_e| StateError { msg: "Current directory is not valid UTF-8!".to_owned(), - source: StateErrorKind::InvalidPath.into() + source: StateErrorKind::InvalidPath.into(), })?; - let context_root = match state_in.context { - InferContext::Cwd => current_dir, - InferContext::Git => Utf8PathBuf::from( - git_root_dir(¤t_dir) - .to_state_err("Getting Git root dir for new StatePaths".to_owned())?, - ), + let resolve_root_path = state_in + .resolve_root + .map(|s| Utf8PathBuf::from(s)); + let resolve_root = resolve_root_path.unwrap_or_default(); + let resolve_root_rel = RelativePathBuf::from_path(&resolve_root).ok(); + + + let resolve_root = match state_in.context { + InferContext::Cwd => { + match resolve_root_rel { + Some(resolve_root_rel) => resolve_root_rel.to_utf8_path(¤t_dir), + None => resolve_root + } + }, + InferContext::Git => { + let git_dir = Utf8PathBuf::from( + git_root_dir(¤t_dir) + .to_state_err("Getting Git root dir for new StatePaths".to_owned())?, + ); + match resolve_root_rel { + Some(resolve_root_rel) => resolve_root_rel.to_utf8_path(&git_dir), + None => git_dir + } + + }, }; - let state_root = state_in.state_root.map(|s| RelativePathBuf::from(s)).unwrap_or_default(); - let state_path = state_in.state_path.map(|s| RelativePathBuf::from(s)).unwrap_or_default(); + + // let resolve_root_path = state_in + // .resolve_root + // .map(|s| Utf8PathBuf::from(s)) + // .unwrap_or_default(); + // resolve_root_rel.is_relative() + // let resolve_root_rel = state_in + // .resolve_root + // .map(|s| RelativePathBuf::from(s)) + // .unwrap_or_default(); + // let resolve_root = (&resolve_root_rel).to_path_canon_checked(&context_root) + // .to_state_err(format!("Error interpreting resolve_root {} as relative to the context_root {}", &resolve_root_rel, &context_root))?; + let state_root = state_in + .state_root + .map(|s| RelativePathBuf::from(s)) + .unwrap_or_default(); + let state_path = state_in + .state_path + .map(|s| RelativePathBuf::from(s)) + .unwrap_or_default(); Ok(StatePaths { - context_root, + resolve_root, state_path, state_root, }) @@ -93,35 +139,69 @@ pub(crate) fn parse_cli_vars(envs: Vec) -> Vec { .collect() } -#[derive(Debug, Clone, PartialEq)] -pub(crate) enum Address { +#[derive(Debug, Clone)] +pub(crate) struct Address { + root: AddressRoot, + state_root: RelativePathBuf, + state_path: RelativePathBuf, +} + +#[derive(Debug, Clone)] +pub(crate) enum AddressRoot { + /// An address is: either absolute or relative to the previous resolve_root Local(Utf8PathBuf), - Git(GitAddress) + Git(GitAddress), } -impl From for Address { - fn from(value: ConfigAddress) -> Self { +impl Address { + fn from_config_addr(value: ConfigAddress, resolve_root: &Utf8Path) -> Self { + debug!("Converting config_adress {:?} to address!", value); + match value { - ConfigAddress::Git { url, git_ref } => Self::Git(GitAddress { + ConfigAddress::Git { url, - git_ref - }), - ConfigAddress::Local { path } => Self::Local(Utf8PathBuf::from(path)) + git_ref, + state_path, + state_root, + } => Address { + root: AddressRoot::Git(GitAddress { url, git_ref }), + state_path: RelativePathBuf::from(state_path.unwrap_or_default()), + state_root: RelativePathBuf::from(state_root.unwrap_or_default()), + }, + ConfigAddress::Local { + path, + state_path, + state_root, + } => { + let address_root = Utf8PathBuf::from(path.clone()); + let address_rel = RelativePathBuf::from_path(&address_root).ok(); + let root = if let Some(address_rel) = address_rel { + address_rel.to_utf8_path(resolve_root) + } else { + address_root + }; + + Address { + root: AddressRoot::Local(root), + state_path: RelativePathBuf::from(state_path.unwrap_or_default()), + state_root: RelativePathBuf::from(state_root.unwrap_or_default()), + } + }, } } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub(crate) struct GitAddress { pub(crate) url: String, - pub(crate) git_ref: String + pub(crate) git_ref: String, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub(crate) struct State { pub(crate) state_root: RelativePathBuf, pub(crate) state_path: RelativePathBuf, - pub(crate) context_root: Utf8PathBuf, + pub(crate) resolve_root: Utf8PathBuf, pub(crate) address: Option
, } @@ -130,8 +210,8 @@ impl From for State { State { state_path: value.state_path, state_root: value.state_root, - context_root: value.context_root, - address: None + resolve_root: value.resolve_root, + address: None, } } } @@ -147,13 +227,28 @@ impl State { // } fn merge_config(&self, other: StateConfig) -> Self { + let address = other.address.map(|a| Address::from_config_addr(a, &self.resolve_root)).or(self.address.clone()); + Self { - state_path: other.state_path.map(Into::into).unwrap_or(self.state_path.clone()), - state_root: other.state_root.map(Into::into).unwrap_or(self.state_root.clone()), - context_root: self.context_root.clone(), - address: other.address.map(Into::into).or(self.address.clone()) + state_path: other + .state_path + .map(Into::into) + .unwrap_or(self.state_path.clone()), + state_root: other + .state_root + .map(Into::into) + .unwrap_or(self.state_root.clone()), + resolve_root: self.resolve_root.clone(), + address, } } + + /// Checks if a state is different to another one for the purposes of converging to a state. + fn same(&self, other: &Self) -> bool { + self.resolve_root == other.resolve_root + && self.state_path.normalize() == other.state_path.normalize() + && self.state_root.normalize() == other.state_root.normalize() + } } #[derive(Debug)] @@ -166,30 +261,38 @@ pub(crate) struct ResolveState { pub(crate) hash: String, } -#[instrument(name = "converge_state", level = "debug", skip_all)] +#[instrument(name = "converge", level = "debug", skip_all)] fn converge_state(state: &State) -> Result { let mut state = state.clone(); let mut i = 0; let iter = loop { - let state_root_path = state.state_root.to_utf8_path(&state.context_root); - let config = traverse_configs(&state_root_path, &state.state_path).to_state_err("Failed to read configs for determining new state.".to_owned())?; - let new_state = config.state.map(|c| (&state).merge_config(c)).unwrap_or(state.clone()); - if new_state == state { - break i+1 - } else if i > 99 { - break 100 + let state_root_path = state.state_root.to_utf8_path(&state.resolve_root); + let config = traverse_configs(&state_root_path, &state.state_path) + .to_state_err("Failed to read configs for determining new state.".to_owned())?; + let new_state = if let Some(config_state) = config.state { + (&state).merge_config(config_state) + } else { + break i + 1; + }; + debug!("New intermediate state {:?}", &new_state); + + let do_break = new_state.same(&state); + state = new_state; + if do_break { + break i + 1; } + i += 1; - state = new_state; + }; - debug!("Converged to state in {} iterations.", iter); - + debug!("Converged to state {:?} in {} iterations.", &state, iter); + Ok(state) } /// Parse a repo URL to extract a "name" from it, as well as encode the part before the name to still uniquely /// identify it. Only supports forward slashes as path seperator. -pub(crate) fn parse_url_repo_name(url: String) -> Result { +pub(crate) fn parse_url_repo_name(url: &str) -> Result { let url = url.strip_suffix('/').unwrap_or(&url).to_owned(); // We want the final part, after the slash, as the "file name" let split_parts: Vec<&str> = url.split('/').collect(); @@ -216,40 +319,59 @@ pub(crate) fn parse_url_repo_name(url: String) -> Result { } fn resolve_address(address: Address) -> Result { - match address { - Address::Git(GitAddress { url, git_ref }) => { - let name = parse_url_repo_name(url)?; + let Address { + state_path, + state_root, + root, + } = address; + + match root { + AddressRoot::Git(GitAddress { url, git_ref }) => { + let name = parse_url_repo_name(&url)?; - todo!() - }, - Address::Local(path) => { todo!() } + AddressRoot::Local(path) => Ok(State { + resolve_root: path, + state_path, + state_root, + address: None, + }), } - - Ok(todo!()) } +// fn set_current_dir(resolve_root: &Utf8Path) -> Result<(), StateError> { +// debug!("Setting current dir to resolve root {}", resolve_root); + +// env::set_current_dir(resolve_root).to_state_err(format!("Failed to set current dir to context root {}", resolve_root))?; + +// Ok(()) +// } + +#[instrument(name = "state", level = "debug", skip_all)] pub(crate) fn create_resolve_state(state_in: StateIn) -> Result { let paths = StatePaths::new(state_in)?; - let state = converge_state(&paths.into())?; + let mut state = converge_state(&paths.into())?; + + while let Some(address) = state.address.clone() { + state = resolve_address(address) + .to_state_err("Error resolving address for state!".to_owned())?; + debug!("Moved to address, new state is {:?}", state); + state = converge_state(&state)?; + } let name = state - .context_root + .resolve_root .file_name() .map(|s| s.to_string()) - .ok_or_else(|| { - StateErrorKind::InvalidRoot(state.context_root.to_string()) - }) + .ok_or_else(|| StateErrorKind::InvalidRoot(state.resolve_root.to_string())) .to_state_err("Getting context name from context root path for new state.".to_owned())?; - - Ok(ResolveState { - state_root: state.state_root.to_utf8_path(&state.context_root), + state_root: state.state_root.to_utf8_path(&state.resolve_root), state_path: state.state_path, - resolve_root: state.context_root, + resolve_root: state.resolve_root, name, sub: "tidploy_root".to_owned(), hash: "todo_hash".to_owned(), diff --git a/tests/run_tests.rs b/tests/run_tests.rs index ab400c9..ff1137a 100644 --- a/tests/run_tests.rs +++ b/tests/run_tests.rs @@ -3,7 +3,8 @@ use keyring::Entry; use test_log::test; use tidploy::{ - run_command, secret_command, CommandError, GlobalArguments, RunArguments, SecretArguments, + archive_command, run_command, secret_command, ArchiveArguments, CommandError, GlobalArguments, + RunArguments, SecretArguments, }; #[test] @@ -139,3 +140,28 @@ fn test_secret_get() -> Result<(), CommandError> { Ok(()) } + +#[test] +fn test_config_address() -> Result<(), CommandError> { + let mut global_args = GlobalArguments::default(); + let mut args = RunArguments::default(); + //global_args.context = Some(StateContext::None); + global_args.resolve_root = Some("examples/config/start".to_owned()); + + let output = run_command(global_args, args)?; + assert!(output.exit.success()); + + assert_eq!("I'm there!\n", output.out); + + Ok(()) +} + +// #[test] +// fn test_archive() -> Result<(), CommandError> { +// let global_args = GlobalArguments::default(); +// let mut args = ArchiveArguments::default(); + +// archive_command(global_args, args)?; + +// Ok(()) +// }