use clap::Parser; use std::{ path::{Path, PathBuf}, str::FromStr, }; use thiserror::Error; #[derive(Parser, Clone, Debug, PartialEq, Eq)] #[command(name = "")] pub enum ShackleCommand { /// Create a new repository Init(InitArgs), /// List all repositories available List(ListArgs), /// Sets the description of a repository, as shown in the CLI listing and web interfaces SetDescription(SetDescriptionArgs), /// Sets the main branch of the repository SetBranch(SetBranchArgs), /// Deletes a repository Delete(DeleteArgs), /// Does any housekeeping, like deleting unreachable objects and repacking more efficiently Housekeeping(HousekeepingArgs), /// Quit the shell Exit, /// Server side command required to git fetch from the server #[command(hide = true)] GitUploadPack(GitUploadPackArgs), /// Server side command required by git push to the server #[command(hide = true)] GitReceivePack(GitReceivePackArgs), } #[derive(Parser, Clone, Debug, PartialEq, Eq)] pub struct InitArgs { /// Share repository ownership with the specified group (user must be a member of the group) #[arg(long)] pub group: Option, /// Sets the description of the repository, as shown in the CLI listing and web interfaces #[arg(long)] pub description: Option, /// Sets the main branch of the repository #[arg(long, default_value = "main")] pub branch: String, /// Name of the new repository pub repo_name: String, } #[derive(Parser, Clone, Debug, PartialEq, Eq)] pub struct ListArgs { /// List extra metadata, like the repo's size on disk #[arg(short, long)] pub verbose: bool, } #[derive(Parser, Clone, Debug, PartialEq, Eq)] pub struct SetDescriptionArgs { /// The full relative path of the repository, for example git/shuckie/repo.git #[arg(value_parser = RelativePathParser)] pub directory: PathBuf, /// The new description pub description: String, } #[derive(Parser, Clone, Debug, PartialEq, Eq)] pub struct SetBranchArgs { /// The full relative path of the repository, for example git/shuckie/repo.git #[arg(value_parser = RelativePathParser)] pub directory: PathBuf, /// The new branch name pub branch: String, } #[derive(Parser, Clone, Debug, PartialEq, Eq)] pub struct DeleteArgs { /// The full relative path of the repository, for example git/shuckie/repo.git #[arg(value_parser = RelativePathParser)] pub directory: PathBuf, } #[derive(Parser, Clone, Debug, PartialEq, Eq)] pub struct HousekeepingArgs { /// The full relative path of the repository, for example /// git/shuckie/repo.git. If omitted, all repos will be checked. pub directory: Option, } #[derive(Parser, Clone, Debug, PartialEq, Eq)] pub struct GitUploadPackArgs { /// Do not try /.git/ if is no Git directory #[arg(long, default_value_t = true)] pub strict: bool, /// Always try /.git/ if is no Git directory - this argument is accepted for compatability with git, but is ignored #[arg(long)] pub no_strict: bool, /// Interrupt transfer after seconds of inactivity #[arg(long)] pub timeout: Option, /// Perform only a single read-write cycle with stdin and stdout #[arg(long)] pub stateless_rpc: bool, /// Only the initial ref advertisement is output, and the program exits immediately #[arg(long)] pub advertise_refs: bool, /// The full relative path of the repository for example git/shuckie/repo.git #[arg(value_parser = RelativePathParser)] pub directory: PathBuf, } #[derive(Parser, Clone, Debug, PartialEq, Eq)] pub struct GitReceivePackArgs { /// The full relative path of the repository for example git/shuckie/repo.git #[arg(value_parser = RelativePathParser)] pub directory: PathBuf, } #[derive(Error, Debug)] pub enum ParserError { #[error(transparent)] ClapError(#[from] clap::error::Error), #[error("`{0}`")] LexerError(String), } impl FromStr for ShackleCommand { type Err = ParserError; fn from_str(s: &str) -> Result { let trimmed = s.trim(); let lexed = shlex::split(trimmed); match lexed { None => Err(ParserError::LexerError("Incomplete input".to_string())), Some(lexed) => { let parsed = ShackleCommand::try_parse_from(["".to_owned()].into_iter().chain(lexed))?; Ok(parsed) } } } } #[derive(Clone)] struct RelativePathParser; impl clap::builder::TypedValueParser for RelativePathParser { type Value = std::path::PathBuf; fn parse_ref( &self, cmd: &clap::Command, arg: Option<&clap::Arg>, value: &std::ffi::OsStr, ) -> Result { clap::builder::TypedValueParser::parse(self, cmd, arg, value.to_owned()) } fn parse( &self, cmd: &clap::Command, arg: Option<&clap::Arg>, value: std::ffi::OsString, ) -> Result { let raw = clap::builder::PathBufValueParser::default().parse(cmd, arg, value)?; Ok(raw .strip_prefix(Path::new("/~")) .or_else(|_| raw.strip_prefix(Path::new("~"))) .map(|m| m.to_owned()) .unwrap_or(raw)) } } #[cfg(test)] mod test { use super::*; #[test] fn it_parses_exit_correctly() { assert_eq!( "exit".parse::().unwrap(), ShackleCommand::Exit ); } #[test] fn it_parses_git_upload_pack_correctly() { assert_eq!( "git-upload-pack --stateless-rpc foobar.git" .parse::() .unwrap(), ShackleCommand::GitUploadPack(GitUploadPackArgs { strict: true, no_strict: false, timeout: None, stateless_rpc: true, advertise_refs: false, directory: PathBuf::from("foobar.git"), }) ); } }