From 3035f8d52efe33749a8c027e193559ee7dd4c357 Mon Sep 17 00:00:00 2001 From: Justin Wernick Date: Mon, 13 Mar 2023 22:03:58 +0200 Subject: Git clone / fetch --- src/main.rs | 50 +++++++++++++--------- src/parser.rs | 132 +++++++++++++++++++++++++++++++++------------------------- 2 files changed, 106 insertions(+), 76 deletions(-) (limited to 'src') diff --git a/src/main.rs b/src/main.rs index ba29032..6f8c1d9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,8 +2,8 @@ mod git; mod parser; use clap::Parser; -use parser::Command; -use std::{io, io::Write, ops::ControlFlow, process}; +use parser::*; +use std::{io, io::Write, ops::ControlFlow, process::Command}; use thiserror::Error; /// Shackle Shell - A replacement for git-shell with repo management commands built in. @@ -42,29 +42,41 @@ fn main() -> Result<(), ShackleError> { } fn run_command(user_input: String) -> Result, ShackleError> { - match user_input.parse::() { - Err(unknown_input) => { - println!("Unknown input \"{}\"", unknown_input); + match user_input.parse::() { + Err(parse_error) => { + println!("{}", parse_error); } - Ok(Command::Whitespace) => {} - Ok(Command::Exit) => { + Ok(ShackleCommand::Whitespace) => {} + Ok(ShackleCommand::Exit) => { return Ok(ControlFlow::Break(())); } - Ok(Command::GitInit(repo_name)) => { + Ok(ShackleCommand::GitInit(GitInitArgs { repo_name })) => { git::init(&repo_name)?; // TODO should report this error differently println!("Successfully created {}.git", repo_name); } - Ok(Command::GitUploadPack(git_dir)) => { - process::Command::new("git") - .args(["upload-pack", &git_dir]) - .spawn()? - .wait()?; - } - Ok(Command::GitReceivePack(git_dir)) => { - process::Command::new("git") - .args(["receive-pack", &git_dir]) - .spawn()? - .wait()?; + Ok(ShackleCommand::GitUploadPack(upload_pack_args)) => { + let mut command = Command::new("git-upload-pack"); + + if upload_pack_args.strict { + command.arg("strict"); + } + if upload_pack_args.no_strict { + command.arg("no-strict"); + } + if let Some(timeout) = upload_pack_args.timeout { + command.args(["timeout", &timeout.to_string()]); + } + if upload_pack_args.stateless_rpc { + command.arg("stateless-rpc"); + } + if upload_pack_args.advertise_refs { + command.arg("advertise-refs"); + } + + // TODO: This should definitely be part of the arg parsing! + command.arg(&upload_pack_args.directory.trim_matches('\'')); + + command.spawn()?.wait()?; } } Ok(ControlFlow::Continue(())) diff --git a/src/parser.rs b/src/parser.rs index 75ac333..ef7d5f8 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,72 +1,90 @@ -use nom::{ - branch::alt, - bytes::complete::{is_not, tag}, - character::complete::{multispace0, multispace1}, - combinator::{eof, map, value}, - error::ParseError, - sequence::delimited, - sequence::tuple, - Finish, IResult, -}; +use clap::{Parser, Subcommand}; use std::str::FromStr; -// TODO: It might work well to use clap and parse_from for this particular case -#[derive(Clone)] -pub enum Command { +#[derive(Parser, Clone, Debug, PartialEq, Eq)] +#[command(name = "")] +pub enum ShackleCommand { + #[command(skip)] Whitespace, Exit, - GitInit(String), - GitUploadPack(String), - GitReceivePack(String), + GitInit(GitInitArgs), + GitUploadPack(GitUploadPackArgs), } -impl FromStr for Command { - // the error must be owned as well - type Err = String; +#[derive(Subcommand, Clone, Debug, PartialEq, Eq)] +pub enum GitCommand { + UploadPack(GitUploadPackArgs), +} + +#[derive(Parser, Clone, Debug, PartialEq, Eq)] +pub struct GitInitArgs { + pub repo_name: String, +} + +#[derive(Parser, Clone, Debug, PartialEq, Eq)] +pub struct GitUploadPackArgs { + #[arg(long)] + pub strict: bool, + #[arg(long)] + pub no_strict: bool, + #[arg(long)] + pub timeout: Option, + #[arg(long)] + pub stateless_rpc: bool, + #[arg(long)] + pub advertise_refs: bool, + pub directory: String, +} + +impl FromStr for ShackleCommand { + type Err = clap::error::Error; fn from_str(s: &str) -> Result { - match command_parser(s).finish() { - Ok((remaining, command)) => { - if remaining.trim().is_empty() { - Ok(command) - } else { - Err(s.trim().to_owned()) - } - } - Err(_) => Err(s.trim().to_owned()), + let trimmed = s.trim(); + if s.len() == 0 { + Ok(ShackleCommand::Exit) + } else if trimmed.len() == 0 { + Ok(ShackleCommand::Whitespace) + } else { + ShackleCommand::try_parse_from([""].into_iter().chain(trimmed.split_whitespace())) } } } -fn command_parser(input: &str) -> IResult<&str, Command> { - alt(( - value(Command::Exit, eof), - value(Command::Whitespace, tuple((multispace1, eof))), - value(Command::Exit, ws(tag("exit"))), - map( - ws(tuple((tag("git-init"), multispace1, not_space))), - |(_, _, repo_name)| Command::GitInit(repo_name.to_owned()), - ), - map( - ws(tuple((tag("git upload-pack"), multispace1, not_space))), - |(_, _, git_dir)| Command::GitUploadPack(git_dir.to_owned()), - ), - map( - ws(tuple((tag("git receive-pack"), multispace1, not_space))), - |(_, _, git_dir)| Command::GitReceivePack(git_dir.to_owned()), - ), - ))(input) -} +#[cfg(test)] +mod test { + use super::*; -/// A combinator that takes a parser `inner` and produces a parser that also consumes both leading and -/// trailing whitespace, returning the output of `inner`. -fn ws<'a, F, O, E: ParseError<&'a str>>(inner: F) -> impl FnMut(&'a str) -> IResult<&'a str, O, E> -where - F: FnMut(&'a str) -> IResult<&'a str, O, E>, -{ - delimited(multispace0, inner, multispace0) -} + #[test] + fn it_parses_exit_correctly() { + assert_eq!( + "exit".parse::().unwrap(), + ShackleCommand::Exit + ); + } -fn not_space(s: &str) -> IResult<&str, &str> { - is_not(" \t\r\n")(s) + #[test] + fn it_parses_git_upload_pack_correctly() { + assert_eq!( + "git-upload-pack --stateless-rpc foobar.git" + .parse::() + .unwrap(), + ShackleCommand::GitUploadPack(GitUploadPackArgs { + strict: false, + no_strict: false, + timeout: None, + stateless_rpc: true, + advertise_refs: false, + directory: "foobar.git".to_owned(), + }) + ); + } + + #[test] + fn it_parses_whitespace_correctly() { + assert_eq!( + " ".parse::().unwrap(), + ShackleCommand::Whitespace + ); + } } -- cgit v1.2.3