diff --git a/Cargo.lock b/Cargo.lock index 8bf84e6..60eb553 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.3" @@ -11,30 +26,140 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + [[package]] name = "cfg-if" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +[[package]] +name = "clap" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + [[package]] name = "cli" version = "0.1.0" dependencies = [ + "clap", "engine", + "miette", "mockall", "plugins", "pretty_assertions", "rstest", "shared", + "thiserror", ] +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "diff" version = "0.1.13" @@ -55,6 +180,8 @@ dependencies = [ "pretty_assertions", "rstest", "shared", + "tempfile", + "thiserror", ] [[package]] @@ -63,6 +190,22 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "fragile" version = "2.0.1" @@ -112,6 +255,24 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "glob" version = "0.3.2" @@ -146,12 +307,75 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "memchr" version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + [[package]] name = "mockall" version = "0.13.1" @@ -178,6 +402,33 @@ dependencies = [ "syn", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "owo-colors" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -263,6 +514,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "regex" version = "1.11.1" @@ -328,6 +585,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "rustc-demangle" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" + [[package]] name = "rustc_version" version = "0.4.1" @@ -337,6 +600,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "semver" version = "1.0.26" @@ -350,6 +626,7 @@ dependencies = [ "mockall", "pretty_assertions", "rstest", + "tempfile", ] [[package]] @@ -358,6 +635,33 @@ version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + [[package]] name = "syn" version = "2.0.104" @@ -369,12 +673,65 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "terminal_size" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" +dependencies = [ + "rustix", + "windows-sys", +] + [[package]] name = "termtree" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "unicode-linebreak", + "unicode-width 0.2.1", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "toml_datetime" version = "0.6.11" @@ -398,6 +755,112 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "winnow" version = "0.7.12" @@ -407,6 +870,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 4f0d42c..7c3b31b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,8 @@ members = ["engine", "cli", "gui", "plugins", "shared"] authors = ["Mikołaj Karbowski", "Adam Grącikowski"] [workspace.dependencies] +thiserror = "2" rstest = "0.25.0" mockall = "0.13.1" pretty_assertions = "1.4.1" +tempfile = "3.20.0" diff --git a/README.md b/README.md index f270385..ad9c610 100644 --- a/README.md +++ b/README.md @@ -36,16 +36,6 @@ meva/ └── ... ``` -It consists of 5 crates: - -- Library crates: - - `shared`, - - `engine`, - - `plugins`. -- Binary crates: - - `cli`, - - `gui`. - ## Getting Started > The installation guide assumes you have already installed [Rust](https://www.rust-lang.org/learn/get-started). @@ -128,17 +118,17 @@ To share a dependency (or dev-dependency) version across multiple crates, declar ```toml [workspace.dependencies] - = +regex = "1.11.1" ``` In any crate that should use a workspace-provided dependency, reference it like so in its own `Cargo.toml`: ```toml [dependencies] - = { workspace = true } +regex = { workspace = true } [dev-dependencies] -.workspace = true # alternative syntax +rstest.workspace = true # alternative syntax ``` ### Generating docs diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 0917a37..9d1c2ed 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -4,10 +4,17 @@ version = "0.1.0" edition = "2024" authors.workspace = true +[[bin]] +name = "meva" +path = "src/main.rs" + [dependencies] shared = { path = "../shared" } engine = { path = "../engine" } plugins = { path = "../plugins" } +clap = "4.5.41" +thiserror.workspace = true +miette = { version = "7.6.0", features = ["fancy"] } [dev-dependencies] rstest.workspace = true diff --git a/cli/src/commands.rs b/cli/src/commands.rs new file mode 100644 index 0000000..65a828b --- /dev/null +++ b/cli/src/commands.rs @@ -0,0 +1,5 @@ +pub mod init; +pub mod meva_command; + +pub use init::InitCommand; +pub use meva_command::MevaCommand; diff --git a/cli/src/commands/init.rs b/cli/src/commands/init.rs new file mode 100644 index 0000000..e6e1b3d --- /dev/null +++ b/cli/src/commands/init.rs @@ -0,0 +1,172 @@ +use clap::{Arg, ArgMatches, Command}; +use miette::{IntoDiagnostic, Result}; +use std::path::PathBuf; + +use crate::commands::MevaCommand; + +use engine::MevaRepository; + +/// Represents the `init` command for Meva DVCS. +/// +/// This command initializes a new empty repository at the specified path, +/// optionally setting the initial branch name. +pub struct InitCommand; + +impl InitCommand { + /// Creates a new instance of the `InitCommand`. + pub fn new() -> Self { + Self + } + + /// Argument name for specifying the initial branch. + const ARG_BRANCH: &'static str = "initial-branch"; + + /// Argument name for specifying the repository path. + const ARG_PATH: &'static str = "path"; +} + +impl MevaCommand for InitCommand { + fn name(&self) -> &'static str { + "init" + } + + fn about(&self) -> &'static str { + "Create an empty Meva repository" + } + + fn version(&self) -> &'static str { + "1.0.0" + } + + /// Builds the CLI argument parser for the `init` command using `clap`. + /// + /// Adds two arguments: + /// - `-b, --initial-branch `: Name of the initial branch (default: "master") + /// - ``: Path to initialize the repository (default: current directory) + fn build_command(&self) -> Command { + self.build_base_command() + .arg( + Arg::new(Self::ARG_BRANCH) + .short('b') + .long("initial-branch") + .value_name("BRANCH") + .help("Name of the initial branch") + .default_value("master") + .require_equals(false), + ) + .arg( + Arg::new(Self::ARG_PATH) + .value_name("PATH") + .help("Path to initialize repository") + .default_value(".") + .value_parser(clap::value_parser!(PathBuf)) + .index(1), + ) + } + + /// Executes the `init` command. + /// + /// Initializes a new Meva repository at the specified path using the provided + /// initial branch name. If the repository already exists or an error occurs + /// during initialization, the error is reported. + /// + /// # Parameters + /// - `matches`: Parsed command-line arguments containing: + /// - `initial-branch`: The name of the initial branch (defaults to "master"). + /// - `path`: The target directory to initialize the repository (defaults to current dir). + /// + /// # Returns + /// - `Result<()>`: Indicates success or detailed error if initialization fails. + fn execute(&self, matches: &ArgMatches) -> Result<()> { + let branch = matches.get_one::(Self::ARG_BRANCH).unwrap(); + let target = matches.get_one::(Self::ARG_PATH).unwrap(); + + let repository = MevaRepository::new(target); + repository.init(branch).into_diagnostic()?; + + println!("Repository initialized successfully!"); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + use rstest::rstest; + use std::path::PathBuf; + + fn get_matches_from(args: &[&str]) -> ArgMatches { + let cmd = InitCommand::new(); + cmd.build_command().try_get_matches_from(args).unwrap() + } + + #[rstest] + fn test_command_name_about_version() { + let cmd = InitCommand::new(); + assert_eq!(cmd.name(), "init"); + assert_eq!(cmd.about(), "Create an empty Meva repository"); + assert_eq!(cmd.version(), "1.0.0"); + } + + #[rstest] + fn test_command_builds_with_expected_args() { + let cmd = InitCommand::new(); + let clap_cmd = cmd.build_command(); + + // Check command name + assert_eq!(clap_cmd.get_name(), "init"); + + // Check arguments exist + assert!( + clap_cmd + .get_arguments() + .any(|a| a.get_id() == InitCommand::ARG_BRANCH) + ); + assert!( + clap_cmd + .get_arguments() + .any(|a| a.get_id() == InitCommand::ARG_PATH) + ); + + // Check default values for args + let branch_arg = clap_cmd + .get_arguments() + .find(|a| a.get_id() == InitCommand::ARG_BRANCH) + .unwrap(); + assert_eq!( + branch_arg.get_default_values().first().map(|v| v.to_str()), + Some(Some("master")) + ); + + let path_arg = clap_cmd + .get_arguments() + .find(|a| a.get_id() == InitCommand::ARG_PATH) + .unwrap(); + assert_eq!( + path_arg.get_default_values().first().map(|v| v.to_str()), + Some(Some(".")) + ); + } + + #[rstest] + #[case(&["init"], "master", ".")] + #[case(&["init", "-b", "develop"], "develop", ".")] + #[case(&["init", "--initial-branch=feature"], "feature", ".")] + #[case(&["init", "-b", "dev", "./repo_path"], "dev", "./repo_path")] + #[case(&["init", "./some_path"], "master", "./some_path")] + fn test_execute_parses_args_correctly( + #[case] args: &[&str], + #[case] expected_branch: &str, + #[case] expected_path: &str, + ) { + let matches = get_matches_from(args); + + let branch = matches.get_one::(InitCommand::ARG_BRANCH).unwrap(); + let path = matches.get_one::(InitCommand::ARG_PATH).unwrap(); + + assert_eq!(branch, expected_branch); + assert_eq!(path.to_str().unwrap(), expected_path); + } +} diff --git a/cli/src/commands/meva_command.rs b/cli/src/commands/meva_command.rs new file mode 100644 index 0000000..ffaa470 --- /dev/null +++ b/cli/src/commands/meva_command.rs @@ -0,0 +1,58 @@ +use clap::{ArgMatches, Command}; +use miette::Result; + +/// A trait representing a top-level command in the Meva CLI. +pub trait MevaCommand { + /// Returns the unique name of the command. + fn name(&self) -> &'static str; + + /// Returns a brief description of what the command does. + fn about(&self) -> &'static str; + + /// Returns the version string for the command. + fn version(&self) -> &'static str; + + /// Builds and returns the `clap::Command` for this command. + fn build_command(&self) -> Command { + self.build_base_command() + } + + /// Builds a basic `clap::Command` with name, description, and version. + fn build_base_command(&self) -> Command { + Command::new(self.name()) + .about(self.about()) + .version(self.version()) + } + + /// Executes this command or delegates to a matching subcommand if present. + /// + /// This method inspects the parsed `ArgMatches` to determine whether a subcommand + /// was invoked. If so, it looks for a matching registered subcommand and calls its + /// `execute` method with the corresponding argument matches. + /// + /// If no subcommand is matched, it returns `Ok(())` by default, meaning no operation was performed. + /// + /// # Parameters + /// - `matches`: The parsed CLI arguments for this command, including any subcommand matches. + /// + /// # Returns + /// - `Result<()>`: Indicates whether the execution succeeded or an error occurred during dispatch. + fn execute(&self, matches: &ArgMatches) -> Result<()> { + if let Some((name, sub_matches)) = matches.subcommand() { + for sub_command in self.subcommands() { + if sub_command.name() == name { + return sub_command.execute(sub_matches); + } + } + } + + Ok(()) + } + + /// Returns a vector of boxed subcommands for this command. + /// + /// Default implementation returns an empty vector. + fn subcommands(&self) -> Vec> { + Vec::new() + } +} diff --git a/cli/src/main.rs b/cli/src/main.rs index e7a11a9..d043f6e 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,3 +1,14 @@ -fn main() { - println!("Hello, world!"); +mod commands; +mod meva_cli; + +use crate::meva_cli::MevaCli; +use commands::InitCommand; +use miette::Result; + +fn main() -> Result<()> { + miette::set_panic_hook(); + + let mut cli = MevaCli::new(); + cli.add_command(Box::new(InitCommand::new())); + cli.run() } diff --git a/cli/src/meva_cli.rs b/cli/src/meva_cli.rs new file mode 100644 index 0000000..54c45be --- /dev/null +++ b/cli/src/meva_cli.rs @@ -0,0 +1,84 @@ +use clap::{Command, error::ErrorKind}; +use miette::{IntoDiagnostic, Result, WrapErr, miette}; + +use crate::commands::MevaCommand; + +/// Represents the top-level CLI application for Meva DVCS. +/// +/// This struct manages registration of commands, building the CLI parser, +/// and dispatching command execution based on user input. +pub struct MevaCli { + /// Registered commands available in the CLI. + commands: Vec>, +} + +impl MevaCli { + /// Creates a new instance of the Meva CLI application with no commands registered. + pub fn new() -> Self { + Self { + commands: Vec::new(), + } + } + + /// Adds a new command to the CLI application. + /// + /// Commands must implement the `MevaCommand` trait and are stored as trait objects. + /// + /// # Parameters + /// - `command`: The boxed command to register. + pub fn add_command(&mut self, command: Box) { + self.commands.push(command); + } + + /// Builds the complete CLI parser using `clap::Command`. + /// + /// Registers all added commands as subcommands, + /// configures top-level metadata like name, version, and help behavior. + /// + /// # Returns + /// - A fully configured `clap::Command` ready to parse CLI arguments. + fn build_cli(&self) -> Command { + let mut cli = Command::new("meva") + .about("Meva distributed version control system") + .version("1.0.0") + .subcommand_required(true) + .arg_required_else_help(true); + + for command in &self.commands { + cli = cli.subcommand(command.build_command()); + } + + cli + } + + /// Runs the CLI application. + /// + /// This method builds the CLI parser, parses the command-line arguments, + /// dispatches execution to the matched command, and returns the result. + pub fn run(&self) -> Result<()> { + let matches = match self.build_cli().try_get_matches() { + Ok(m) => m, + Err(err) => { + return match err.kind() { + ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => { + err.print().into_diagnostic()?; + Ok(()) + } + _ => Err(miette!("Failed to parse command-line arguments:\n{}", err)), + }; + } + }; + + if let Some((name, sub_matches)) = matches.subcommand() { + for cmd in &self.commands { + if cmd.name() == name { + return cmd + .execute(sub_matches) + .wrap_err_with(|| format!("Error running `{name}` command")); + } + } + } + + Ok(()) + } +} diff --git a/engine/Cargo.toml b/engine/Cargo.toml index d29147f..deda1a6 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -6,6 +6,8 @@ authors.workspace = true [dependencies] shared = { path = "../shared" } +tempfile.workspace = true +thiserror.workspace = true [dev-dependencies] rstest.workspace = true diff --git a/engine/src/errors.rs b/engine/src/errors.rs new file mode 100644 index 0000000..abb79fc --- /dev/null +++ b/engine/src/errors.rs @@ -0,0 +1,6 @@ +pub mod engine_error; +pub mod init_error; + +pub use engine_error::EngineError; +pub use engine_error::Result as EngineResult; +pub use init_error::InitError; diff --git a/engine/src/errors/engine_error.rs b/engine/src/errors/engine_error.rs new file mode 100644 index 0000000..84a0864 --- /dev/null +++ b/engine/src/errors/engine_error.rs @@ -0,0 +1,28 @@ +use std::io; +use thiserror::Error; + +use crate::errors::InitError; + +/// A convenient result type alias for engine-related operations. +pub type Result = std::result::Result; + +/// Represents all possible errors that can occur in the engine. +/// +/// These errors are unified under a single type to simplify error handling +/// across components. +#[derive(Error, Debug)] +pub enum EngineError { + /// An I/O error occurred, such as failing to read from or write to disk. + /// Automatically converted from `std::io::Error`. + #[error(transparent)] + Io(#[from] io::Error), + + /// An error occurred during the initialization of the Meva repository. + #[error(transparent)] + Init(#[from] InitError), + + /// A catch-all variant for any unknown or unexpected engine error. + /// Accepts a descriptive string message. + #[error("Unknown Engine error: {0}")] + Unknown(String), +} diff --git a/engine/src/errors/init_error.rs b/engine/src/errors/init_error.rs new file mode 100644 index 0000000..6c65a1c --- /dev/null +++ b/engine/src/errors/init_error.rs @@ -0,0 +1,13 @@ +use thiserror::Error; + +/// Represents errors that can occur during Meva repository initialization. +#[derive(Error, Debug)] +pub enum InitError { + /// Attempted to initialize a repository at a location that already contains one. + #[error("Repository already initialized at `{path}`")] + AlreadyInitialized { path: String }, + + /// The provided working directory either does not exist or is not a directory. + #[error("Invalid working directory: `{path}`")] + InvalidWorkingDir { path: String }, +} diff --git a/engine/src/lib.rs b/engine/src/lib.rs index ab976c8..944d4d5 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -1,15 +1,8 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} +pub mod errors; +pub mod repositories; -#[cfg(test)] -mod tests { - use super::*; - use rstest::*; +pub use errors::EngineError; +pub use errors::EngineResult; +use errors::InitError; - #[rstest] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +pub use repositories::MevaRepository; diff --git a/engine/src/repositories.rs b/engine/src/repositories.rs new file mode 100644 index 0000000..906a8da --- /dev/null +++ b/engine/src/repositories.rs @@ -0,0 +1,5 @@ +pub mod meva_repository; +mod repository_layout; + +pub use meva_repository::MevaRepository; +use repository_layout::RepositoryLayout; diff --git a/engine/src/repositories/meva_repository.rs b/engine/src/repositories/meva_repository.rs new file mode 100644 index 0000000..65e8346 --- /dev/null +++ b/engine/src/repositories/meva_repository.rs @@ -0,0 +1,230 @@ +use shared::fs::create_file_with_dirs; +use tempfile::TempDir; + +use crate::repositories::RepositoryLayout; +use crate::{EngineError, InitError}; + +use std::fs; +use std::io::Write; +use std::path::{Path, PathBuf}; + +use crate::EngineResult; + +/// Represents a Meva repository on disk. +/// +/// Provides functionality to initialize repository layout and manage +/// directory structure and configuration files. +pub struct MevaRepository { + working_dir: PathBuf, +} + +impl MevaRepository { + /// Creates a new `MevaRepository` for the given working directory. + pub fn new>(working_dir: P) -> Self { + Self { + working_dir: working_dir.into(), + } + } + + /// Initializes a new repository with the given initial branch. + /// + /// Creates required directories and files under `.meva/`. + /// Returns an error if the repository is already initialized + /// or the working directory is invalid. + pub fn init(&self, initial_branch: &str) -> EngineResult<()> { + if !self.working_dir.exists() || !self.working_dir.is_dir() { + return Err(InitError::InvalidWorkingDir { + path: self.working_dir.to_string_lossy().into(), + } + .into()); + } + + let repository_dir = self.repository_dir(); + + if repository_dir.exists() { + return Err(InitError::AlreadyInitialized { + path: repository_dir.to_string_lossy().into(), + } + .into()); + } + + let tmp_parent = &self.working_dir; + let tmp_dir = TempDir::new_in(tmp_parent).map_err(EngineError::Io)?; + + let tmp_repo = tmp_dir.path().join(Self::REPOSITORY_DIR); + + self.create_dirs_at(&tmp_dir)?; + self.create_files_at(&tmp_dir, initial_branch)?; + + let final_repo = self.repository_dir(); + + fs::rename(&tmp_repo, &final_repo).map_err(EngineError::Io)?; + + Ok(()) + } + + /// Creates the internal repository directories. + fn create_dirs_at>(&self, root: P) -> EngineResult<()> { + fs::create_dir_all(root.as_ref().join(self.objects_dir_rel()))?; + fs::create_dir_all(root.as_ref().join(self.refs_dir_rel()))?; + fs::create_dir_all(root.as_ref().join(self.heads_refs_dir_rel()))?; + fs::create_dir_all(root.as_ref().join(self.logs_dir_rel()))?; + fs::create_dir_all(root.as_ref().join(self.heads_logs_dir_rel()))?; + Ok(()) + } + + /// Creates the internal repository files. + fn create_files_at>(&self, root: P, initial_branch: &str) -> EngineResult<()> { + let ref_path = root + .as_ref() + .join(self.heads_refs_dir_rel()) + .join(initial_branch); + let log_path = root + .as_ref() + .join(self.heads_logs_dir_rel()) + .join(initial_branch); + + create_file_with_dirs(ref_path)?; + create_file_with_dirs(log_path)?; + + let head_path = root.as_ref().join(self.head_file_rel()); + let mut head = fs::File::create(&head_path)?; + + write!( + head, + "ref: {}/{}/{}", + Self::REFS_DIR, + Self::HEADS_DIR, + initial_branch + )?; + + let head_log_path = root.as_ref().join(self.head_logs_file_rel()); + fs::File::create(head_log_path)?; + + Ok(()) + } +} + +impl RepositoryLayout for MevaRepository { + const REPOSITORY_DIR: &'static str = ".meva"; + + const OBJECTS_DIR: &'static str = "objects"; + + const REFS_DIR: &'static str = "refs"; + + const LOGS_DIR: &'static str = "logs"; + + const HEADS_DIR: &'static str = "heads"; + + const HEAD_FILE: &'static str = "HEAD"; + + /// Returns the repository’s working directory path. + fn working_dir(&self) -> &std::path::Path { + &self.working_dir + } +} + +#[cfg(test)] +mod tests { + use crate::EngineError; + + use super::*; + use pretty_assertions::assert_eq; + use rstest::rstest; + use std::fs::{self, File}; + use std::io::Read; + use tempfile::TempDir; + + fn read_file(path: &PathBuf) -> String { + let mut content = String::new(); + let mut file = fs::File::open(path).expect("File should exist"); + file.read_to_string(&mut content) + .expect("Failed to read file"); + content + } + + #[rstest] + fn init_creates_expected_structure() { + let tmp = TempDir::new().unwrap(); + let repo = MevaRepository::new(tmp.path()); + + let result = repo.init("main"); + assert!(result.is_ok()); + + let repo_dir = tmp.path().join(".meva"); + assert!(repo_dir.exists()); + + assert!(repo.objects_dir().exists()); + assert!(repo.refs_dir().exists()); + assert!(repo.heads_refs_dir().exists()); + assert!(repo.logs_dir().exists()); + assert!(repo.heads_logs_dir().exists()); + + let head_path = repo.head_file(); + assert!(head_path.exists()); + + let head_contents = read_file(&head_path); + assert_eq!(head_contents, "ref: refs/heads/main"); + + assert!(repo.head_logs_file().exists()); + + let branch_ref = repo.heads_refs_dir().join("main"); + assert!(branch_ref.exists()); + + let branch_log = repo.heads_logs_dir().join("main"); + assert!(branch_log.exists()); + } + + #[rstest] + fn init_fails_if_repo_already_exists() { + let tmp = TempDir::new().expect("failed to create TempDir"); + let repo = MevaRepository::new(tmp.path()); + + // First init succeeds + assert!(repo.init("dev").is_ok()); + + // Second init should fail + let result = repo.init("dev"); + assert!(result.is_err()); + + if let Err(err) = result { + let message = format!("{err}"); + assert!( + message.contains("already initialized"), + "Unexpected error: {message}" + ); + } + } + + #[rstest] + fn init_fails_if_nonexistent_working_dir() { + let tmp = TempDir::new().expect("failed to create TempDir"); + let bad_path = tmp.path().join("does_not_exist"); + + let repo = MevaRepository::new(&bad_path); + + let err = repo.init("main").unwrap_err(); + assert!(matches!( + err, + EngineError::Init(InitError::InvalidWorkingDir { path }) + if path == bad_path.to_string_lossy() + )); + } + + #[rstest] + fn init_fails_if_path_is_a_file() { + let tmp = TempDir::new().expect("failed to create TempDir"); + let file_path = tmp.path().join("not_a_dir.txt"); + + File::create(&file_path).expect("failed to create test file"); + + let repo = MevaRepository::new(&file_path); + let err = repo.init("main").unwrap_err(); + + assert!(matches!( + err, + EngineError::Init(InitError::InvalidWorkingDir { path }) + if path == file_path.to_string_lossy() + )); + } +} diff --git a/engine/src/repositories/repository_layout.rs b/engine/src/repositories/repository_layout.rs new file mode 100644 index 0000000..00a2101 --- /dev/null +++ b/engine/src/repositories/repository_layout.rs @@ -0,0 +1,180 @@ +use std::path::{Path, PathBuf}; + +/// Defines the directory layout of a Meva repository. +/// +/// Provides methods to compute paths to internal repository components +/// i.e. absolute and relative to a working directory. +#[allow(dead_code)] +pub trait RepositoryLayout { + /// Name of the root repository directory. + const REPOSITORY_DIR: &'static str; + + /// Subdirectory for storing objects. + const OBJECTS_DIR: &'static str; + + /// Subdirectory for storing references. + const REFS_DIR: &'static str; + + /// Subdirectory for reference logs. + const LOGS_DIR: &'static str; + + /// Subdirectory for storing heads references and logs. + const HEADS_DIR: &'static str; + + /// Name of the HEAD file. + const HEAD_FILE: &'static str; + + /// Returns the working directory where the repository is located. + fn working_dir(&self) -> &Path; + + // --- relative paths --- + + /// Returns the relative path to the repository directory. + fn repository_dir_rel(&self) -> PathBuf { + PathBuf::from(Self::REPOSITORY_DIR) + } + + /// Path to the objects directory relative to `working_dir`. + fn objects_dir_rel(&self) -> PathBuf { + self.repository_dir_rel().join(Self::OBJECTS_DIR) + } + + /// Path to the refs directory relative to `working_dir`. + fn refs_dir_rel(&self) -> PathBuf { + self.repository_dir_rel().join(Self::REFS_DIR) + } + + /// Path to heads in refs relative to `working_dir`. + fn heads_refs_dir_rel(&self) -> PathBuf { + self.refs_dir_rel().join(Self::HEADS_DIR) + } + + /// Path to the logs directory relative to `working_dir`. + fn logs_dir_rel(&self) -> PathBuf { + self.repository_dir_rel().join(Self::LOGS_DIR) + } + + /// Path to heads in logs relative to `working_dir`. + fn heads_logs_dir_rel(&self) -> PathBuf { + self.logs_dir_rel().join(Self::HEADS_DIR) + } + + /// Path to the HEAD file relative to `working_dir`. + fn head_file_rel(&self) -> PathBuf { + self.repository_dir_rel().join(Self::HEAD_FILE) + } + + /// Path to the HEAD log file relative to `working_dir`. + fn head_logs_file_rel(&self) -> PathBuf { + self.logs_dir_rel().join(Self::HEAD_FILE) + } + + // --- absolute paths --- + + /// Absolute path to the repository directory. + fn repository_dir(&self) -> PathBuf { + self.working_dir().join(self.repository_dir_rel()) + } + + /// Absolute path to the objects directory. + fn objects_dir(&self) -> PathBuf { + self.working_dir().join(self.objects_dir_rel()) + } + + /// Absolute path to the refs directory. + fn refs_dir(&self) -> PathBuf { + self.working_dir().join(self.refs_dir_rel()) + } + + /// Absolute path to heads in refs. + fn heads_refs_dir(&self) -> PathBuf { + self.working_dir().join(self.heads_refs_dir_rel()) + } + + /// Absolute path to the logs directory. + fn logs_dir(&self) -> PathBuf { + self.working_dir().join(self.logs_dir_rel()) + } + + /// Absolute path to heads in logs. + fn heads_logs_dir(&self) -> PathBuf { + self.working_dir().join(self.heads_logs_dir_rel()) + } + + /// Absolute path to the HEAD file. + fn head_file(&self) -> PathBuf { + self.working_dir().join(self.head_file_rel()) + } + + /// Absolute path to the HEAD log file. + fn head_logs_file(&self) -> PathBuf { + self.working_dir().join(self.head_logs_file_rel()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + use rstest::rstest; + use std::path::PathBuf; + + struct TestRepoLayout { + dir: PathBuf, + } + + impl TestRepoLayout { + fn new>(path: P) -> Self { + Self { dir: path.into() } + } + } + + impl RepositoryLayout for TestRepoLayout { + const REPOSITORY_DIR: &'static str = ".meva"; + const OBJECTS_DIR: &'static str = "objects"; + const REFS_DIR: &'static str = "refs"; + const LOGS_DIR: &'static str = "logs"; + const HEADS_DIR: &'static str = "heads"; + + const HEAD_FILE: &'static str = "HEAD"; + + fn working_dir(&self) -> &Path { + &self.dir + } + } + + #[rstest] + fn repository_paths_are_correct() { + let base = PathBuf::from("/tmp/project"); + let layout = TestRepoLayout::new(&base); + + // relative paths + assert_eq!(layout.repository_dir_rel(), PathBuf::from(".meva")); + assert_eq!(layout.objects_dir_rel(), PathBuf::from(".meva/objects")); + assert_eq!(layout.refs_dir_rel(), PathBuf::from(".meva/refs")); + assert_eq!( + layout.heads_refs_dir_rel(), + PathBuf::from(".meva/refs/heads") + ); + assert_eq!(layout.logs_dir_rel(), PathBuf::from(".meva/logs")); + assert_eq!( + layout.heads_logs_dir_rel(), + PathBuf::from(".meva/logs/heads") + ); + assert_eq!(layout.head_file_rel(), PathBuf::from(".meva/HEAD")); + assert_eq!( + layout.head_logs_file_rel(), + PathBuf::from(".meva/logs/HEAD") + ); + + // absolute paths + assert_eq!(layout.repository_dir(), base.join(".meva")); + assert_eq!(layout.objects_dir(), base.join(".meva/objects")); + assert_eq!(layout.refs_dir(), base.join(".meva/refs")); + assert_eq!(layout.heads_refs_dir(), base.join(".meva/refs/heads")); + assert_eq!(layout.logs_dir(), base.join(".meva/logs")); + assert_eq!(layout.heads_logs_dir(), base.join(".meva/logs/heads")); + assert_eq!(layout.head_file(), base.join(".meva/HEAD")); + assert_eq!(layout.head_logs_file(), base.join(".meva/logs/HEAD")); + } +} diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 17916db..0239b43 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -4,6 +4,9 @@ version = "0.1.0" edition = "2024" authors.workspace = true +[dependencies] +tempfile.workspace = true + [dev-dependencies] rstest.workspace = true mockall.workspace = true diff --git a/shared/src/extensions.rs b/shared/src/extensions.rs new file mode 100644 index 0000000..8114773 --- /dev/null +++ b/shared/src/extensions.rs @@ -0,0 +1,3 @@ +pub mod fs; + +pub use fs::create_file_with_dirs; diff --git a/shared/src/extensions/fs.rs b/shared/src/extensions/fs.rs new file mode 100644 index 0000000..2bf3638 --- /dev/null +++ b/shared/src/extensions/fs.rs @@ -0,0 +1,38 @@ +use std::fs; +use std::io; +use std::path::Path; + +/// Creates a file at the given path, ensuring all parent directories exist. +/// +/// # Arguments +/// +/// * `path` - Target path for the file (including filename). +/// +/// # Errors +/// +/// Returns an `io::Error` if creating parent directories or the file itself fails. +pub fn create_file_with_dirs>(path: P) -> io::Result<()> { + let path = path.as_ref(); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + fs::File::create(path)?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + use tempfile::TempDir; + + #[rstest] + fn creates_file_and_nested_directories() { + let tmp = TempDir::new().unwrap(); + let path = tmp.path().join("a").join("b").join("c.txt"); + + let result = create_file_with_dirs(&path); + assert!(result.is_ok()); + assert!(path.exists()); + } +} diff --git a/shared/src/lib.rs b/shared/src/lib.rs index ab976c8..44b9248 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -1,15 +1,3 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} +pub mod extensions; -#[cfg(test)] -mod tests { - use super::*; - use rstest::*; - - #[rstest] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +pub use extensions::fs;