-
Notifications
You must be signed in to change notification settings - Fork 0
CLI Design Pattern #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 8 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
9d16b9d
add initial cli abstractions with exemplary init dummy command
adamgracikowski 3d08a1d
add unit tests for dummy init command
adamgracikowski 65b4d55
add repository layout, meva repository, init logic , simplify meva co…
adamgracikowski f13d88d
update readme
adamgracikowski 3693a9b
linting
adamgracikowski b76d887
add missing initial branch related files creation
adamgracikowski 8e0c03c
move fs extension function to shared
adamgracikowski 65320b0
linting
adamgracikowski 60b4b29
refactor file structure for modules and submodules
adamgracikowski b3fbdf5
add rollback feature to the init command with tempdir crate
adamgracikowski 28d5225
resolve comments
adamgracikowski File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)] | ||
| 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); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.