Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
472 changes: 472 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
16 changes: 3 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -128,17 +118,17 @@ To share a dependency (or dev-dependency) version across multiple crates, declar

```toml
[workspace.dependencies]
<CRATE_NAME> = <CRATE_VERSION>
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]
<CRATE_NAME> = { workspace = true }
regex = { workspace = true }

[dev-dependencies]
<CRATE_NAME>.workspace = true # alternative syntax
rstest.workspace = true # alternative syntax
```

### Generating docs
Expand Down
7 changes: 7 additions & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
172 changes: 172 additions & 0 deletions cli/src/commands/init/mod.rs
Original file line number Diff line number Diff line change
@@ -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 <BRANCH>`: Name of the initial branch (default: "master")
/// - `<PATH>`: 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::<String>(Self::ARG_BRANCH).unwrap();
let target = matches.get_one::<PathBuf>(Self::ARG_PATH).unwrap();

let repository = MevaRepository::new(target);
repository.init(branch).into_diagnostic()?;

println!("Repository initialized successfully!");

Ok(())
}
}

#[cfg(test)]
Comment thread
adamgracikowski marked this conversation as resolved.
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::<String>(InitCommand::ARG_BRANCH).unwrap();
let path = matches.get_one::<PathBuf>(InitCommand::ARG_PATH).unwrap();

assert_eq!(branch, expected_branch);
assert_eq!(path.to_str().unwrap(), expected_path);
}
}
58 changes: 58 additions & 0 deletions cli/src/commands/meva_command.rs
Original file line number Diff line number Diff line change
@@ -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<Box<dyn MevaCommand>> {
Vec::new()
}
}
5 changes: 5 additions & 0 deletions cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub mod init;
pub mod meva_command;

pub use init::InitCommand;
pub use meva_command::MevaCommand;
15 changes: 13 additions & 2 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -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()
}
Loading