summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--readme.org2
-rw-r--r--src/main.rs50
-rw-r--r--src/parser.rs132
-rw-r--r--tests/cli.rs4
-rw-r--r--tests/server_shell.rs8
5 files changed, 113 insertions, 83 deletions
diff --git a/readme.org b/readme.org
index 94edde5..fbfe7db 100644
--- a/readme.org
+++ b/readme.org
@@ -19,7 +19,7 @@ Pijul.
- [X] git init of private repo
- [X] responds to unknown commands
- [X] Isolation of workdir between tests
-- [ ] git fetch with git upload-pack <argument>
+- [X] git fetch with git upload-pack <argument>
- [ ] extra args for git upload-pack
- [ ] git push with git receive-pack <argument>
- [ ] git archive with git upload-archive <argument>
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<ControlFlow<(), ()>, ShackleError> {
- match user_input.parse::<Command>() {
- Err(unknown_input) => {
- println!("Unknown input \"{}\"", unknown_input);
+ match user_input.parse::<ShackleCommand>() {
+ 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<u32>,
+ #[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<Self, Self::Err> {
- 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::<ShackleCommand>().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::<ShackleCommand>()
+ .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::<ShackleCommand>().unwrap(),
+ ShackleCommand::Whitespace
+ );
+ }
}
diff --git a/tests/cli.rs b/tests/cli.rs
index e642781..6ace151 100644
--- a/tests/cli.rs
+++ b/tests/cli.rs
@@ -72,7 +72,7 @@ fn quits_when_exit_command_is_sent() -> Result<()> {
fn reports_error_with_unsupported_shell_commands() -> Result<()> {
let mut c = spawn_interactive_process()?;
c.p.send_line("ls")?;
- c.p.exp_string("Unknown input \"ls\"")?;
+ c.p.exp_string("error: unrecognized subcommand 'ls'")?;
expect_prompt(&mut c.p)?;
Ok(())
}
@@ -81,7 +81,7 @@ fn reports_error_with_unsupported_shell_commands() -> Result<()> {
fn reports_error_with_nonsense_input() -> Result<()> {
let mut c = spawn_interactive_process()?;
c.p.send_line(" asd fg ")?;
- c.p.exp_string("Unknown input \"asd fg\"")?;
+ c.p.exp_string("error: unrecognized subcommand 'asd'")?;
expect_prompt(&mut c.p)?;
Ok(())
}
diff --git a/tests/server_shell.rs b/tests/server_shell.rs
index 8cf9bda..5028ac9 100644
--- a/tests/server_shell.rs
+++ b/tests/server_shell.rs
@@ -150,7 +150,6 @@ fn shows_a_prompt() -> Result<()> {
}
#[test]
-#[ignore] // requires non-interactive commands
fn git_clone_works_with_an_empty_repo() -> Result<()> {
let c = spawn_ssh_server()?;
let repo_name = "my-new-repo";
@@ -159,9 +158,10 @@ fn git_clone_works_with_an_empty_repo() -> Result<()> {
Command::new("git")
.args([
"clone",
- "-v",
- "--progress",
- &format!("ssh://shukkie@localhost:{}/{}.git", c.ssh_port, repo_name),
+ &format!(
+ "ssh://shukkie@localhost:{}/home/shukkie/git/{}.git",
+ c.ssh_port, repo_name
+ ),
])
.current_dir(&c.workdir)
.timeout(std::time::Duration::from_secs(3))