diff --git a/src/commands/branch.rs b/src/commands/branch.rs index 6d5d445d..7effebdb 100644 --- a/src/commands/branch.rs +++ b/src/commands/branch.rs @@ -118,9 +118,8 @@ mod tests { let args: ArgMatches = cmd.app().get_matches_from(vec!["branch", "feature/test"]); - match cmd.run(&core, &args).await { - Ok(_) => panic!("This command should not have succeeded"), - _ => {} - } + cmd.run(&core, &args) + .await + .expect_err("this command should not have succeeded"); } } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 46daa1db..25d0270d 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -27,6 +27,7 @@ mod open; mod scratch; mod services; mod shell_init; +mod switch; mod update; pub trait Command: Send + Sync { @@ -61,5 +62,6 @@ pub fn commands() -> Vec>> { Arc::new(services::ServicesCommand {}), Arc::new(shell_init::ShellInitCommand {}), Arc::new(update::UpdateCommand {}), + Arc::new(switch::SwitchCommand {}), ] } diff --git a/src/commands/switch.rs b/src/commands/switch.rs new file mode 100644 index 00000000..bfcf805b --- /dev/null +++ b/src/commands/switch.rs @@ -0,0 +1,189 @@ +use super::super::errors; +use super::*; +use crate::core::Target; +use crate::git; +use crate::tasks::*; +use clap::{App, Arg}; + +pub struct SwitchCommand {} + +impl Command for SwitchCommand { + fn name(&self) -> String { + String::from("switch") + } + + fn app<'a>(&self) -> App<'a> { + App::new(self.name().as_str()) + .version("1.0") + .alias("s") + .about("switches to the specified branch.") + .long_about( + "This command switches to the specified branch within the current repository.", + ) + .arg( + Arg::new("branch") + .about("The name of the branch to switch to.") + .index(1), + ) + .arg( + Arg::new("create") + .short('c') + .long("create") + .about("creates a new branch before switching to it."), + ) + } +} + +#[async_trait] +impl CommandRunnable for SwitchCommand { + async fn run(&self, core: &C, matches: &ArgMatches) -> Result { + let repo = core.resolver().get_current_repo()?; + + match matches.value_of("branch") { + Some(branch) => { + let task = tasks::GitSwitch { + branch: branch.to_string(), + create_if_missing: matches.is_present("create"), + }; + + task.apply_repo(core, &repo).await?; + } + None => { + let branches = git::git_branches(&repo.get_path()).await?; + for branch in branches { + println!("{}", branch); + } + } + }; + + Ok(0) + } + + async fn complete(&self, core: &C, completer: &Completer, _matches: &ArgMatches) { + if let Ok(repo) = core.resolver().get_current_repo() { + completer.offer("--create"); + if let Ok(branches) = git::git_branches(&repo.get_path()).await { + completer.offer_many(branches); + } + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::core::*; + use tempfile::tempdir; + + #[tokio::test] + async fn switch_branch_exists() { + let cmd: SwitchCommand = SwitchCommand {}; + + let temp = tempdir().unwrap(); + let repo: Repo = core::Repo::new( + "github.com/sierrasoftworks/test-git-switch-command", + temp.path().join("repo").into(), + ); + + let core = core::CoreBuilder::default() + .with_config(&core::Config::for_dev_directory(temp.path())) + .with_mock_resolver(|r| r.set_repo(repo.clone())) + .build(); + + sequence!( + // Run a `git init` to setup the repo + tasks::GitInit {}, + // Create the branch we want to switch to + tasks::GitCheckout { + branch: "feature/test".into(), + }, + tasks::WriteFile { + path: "README.md".into(), + content: "This is an example README file.", + }, + tasks::GitAdd { + paths: vec!["README.md"], + }, + tasks::GitCommit { + message: "Add README.md", + paths: vec!["README.md"], + }, + tasks::GitCheckout { + branch: "main".into(), + } + ) + .apply_repo(&core, &repo) + .await + .unwrap(); + + assert!(repo.valid(), "the repository should exist and be valid"); + + let args: ArgMatches = cmd.app().get_matches_from(vec!["switch", "feature/test"]); + cmd.run(&core, &args).await.unwrap(); + + assert!(repo.valid(), "the repository should still be valid"); + assert_eq!( + git::git_current_branch(&repo.get_path()).await.unwrap(), + "feature/test" + ); + } + + #[tokio::test] + async fn switch_branch_not_exists() { + let cmd: SwitchCommand = SwitchCommand {}; + + let temp = tempdir().unwrap(); + let repo: Repo = core::Repo::new( + "github.com/sierrasoftworks/test-git-switch-command", + temp.path().join("repo").into(), + ); + + let core = core::CoreBuilder::default() + .with_config(&core::Config::for_dev_directory(temp.path())) + .with_mock_resolver(|r| r.set_repo(repo.clone())) + .build(); + + // Run a `git init` to setup the repo + tasks::GitInit {}.apply_repo(&core, &repo).await.unwrap(); + + assert!(repo.valid(), "the repository should exist and be valid"); + + let args: ArgMatches = cmd.app().get_matches_from(vec!["switch", "feature/test"]); + cmd.run(&core, &args) + .await + .expect_err("this command should not have succeeded"); + } + + #[tokio::test] + async fn switch_create_branch() { + let cmd: SwitchCommand = SwitchCommand {}; + + let temp = tempdir().unwrap(); + let repo: Repo = core::Repo::new( + "github.com/sierrasoftworks/test-git-switch-command", + temp.path().join("repo").into(), + ); + + let core = core::CoreBuilder::default() + .with_config(&core::Config::for_dev_directory(temp.path())) + .with_mock_resolver(|r| r.set_repo(repo.clone())) + .build(); + + // Run a `git init` to setup the repo + tasks::GitInit {}.apply_repo(&core, &repo).await.unwrap(); + + assert!(repo.valid(), "the repository should exist and be valid"); + + let args: ArgMatches = cmd + .app() + .get_matches_from(vec!["switch", "-c", "feature/test"]); + cmd.run(&core, &args).await.unwrap(); + + assert!(repo.valid(), "the repository should still be valid"); + assert_eq!( + git::git_current_branch(&repo.get_path()).await.unwrap(), + "feature/test" + ); + } +} diff --git a/src/git/mod.rs b/src/git/mod.rs index 430d41bb..7d0e9b62 100644 --- a/src/git/mod.rs +++ b/src/git/mod.rs @@ -7,6 +7,7 @@ mod commit; mod init; mod refs; mod remote; +mod switch; pub use add::git_add; pub use branch::{git_branches, git_current_branch}; @@ -17,3 +18,4 @@ pub use commit::git_commit; pub use init::git_init; pub use refs::{git_rev_parse, git_update_ref}; pub use remote::{git_remote_add, git_remote_list, git_remote_set_url}; +pub use switch::git_switch; diff --git a/src/git/switch.rs b/src/git/switch.rs new file mode 100644 index 00000000..2e5d1d12 --- /dev/null +++ b/src/git/switch.rs @@ -0,0 +1,28 @@ +use tokio::process::Command; + +use super::git_cmd; +use crate::errors; +use std::path; + +pub async fn git_switch(repo: &path::Path, name: &str, create: bool) -> Result<(), errors::Error> { + if create { + git_cmd( + Command::new("git") + .current_dir(repo) + .arg("switch") + .arg("-c") + .arg(name), + ) + .await?; + } else { + git_cmd( + Command::new("git") + .current_dir(repo) + .arg("switch") + .arg(name), + ) + .await?; + } + + Ok(()) +} diff --git a/src/tasks/git_switch.rs b/src/tasks/git_switch.rs new file mode 100644 index 00000000..f2ff2f4e --- /dev/null +++ b/src/tasks/git_switch.rs @@ -0,0 +1,137 @@ +use super::*; +use crate::{core::Target, git}; + +pub struct GitSwitch { + pub branch: String, + pub create_if_missing: bool, +} + +#[async_trait::async_trait] +impl Task for GitSwitch { + async fn apply_repo(&self, _core: &C, repo: &core::Repo) -> Result<(), core::Error> { + git::git_switch(&repo.get_path(), &self.branch, self.create_if_missing).await + } + + async fn apply_scratchpad( + &self, + _core: &C, + _scratch: &core::Scratchpad, + ) -> Result<(), core::Error> { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::core::{Config, Repo}; + + use super::*; + use crate::tasks::GitInit; + use tempfile::tempdir; + + #[tokio::test] + async fn test_repo() { + let temp = tempdir().unwrap(); + let repo = core::Repo::new( + "github.com/sierrasoftworks/test-git-switch", + temp.path().join("repo").into(), + ); + + let core = core::CoreBuilder::default() + .with_config(&Config::for_dev_directory(temp.path())) + .with_mock_output() + .with_mock_resolver(|r| { + r.set_repo(Repo::new( + "example.com/test/cmd-branch", + temp.path().to_path_buf(), + )) + }) + .build(); + + sequence![ + GitInit {}, + GitCheckout { + branch: "main".into(), + }, + GitSwitch { + branch: "test".into(), + create_if_missing: true, + } + ] + .apply_repo(&core, &repo) + .await + .unwrap(); + assert!(repo.valid()); + + assert_eq!( + git::git_current_branch(&repo.get_path()).await.unwrap(), + "test" + ); + } + + #[tokio::test] + async fn test_repo_no_create() { + let temp = tempdir().unwrap(); + let repo = core::Repo::new( + "github.com/sierrasoftworks/test-git-switch", + temp.path().join("repo").into(), + ); + + let core = core::CoreBuilder::default() + .with_config(&Config::for_dev_directory(temp.path())) + .with_mock_output() + .with_mock_resolver(|r| { + r.set_repo(Repo::new( + "example.com/test/cmd-branch", + temp.path().to_path_buf(), + )) + }) + .build(); + + sequence![ + GitInit {}, + GitCheckout { + branch: "main".into(), + }, + GitSwitch { + branch: "test".into(), + create_if_missing: false, + } + ] + .apply_repo(&core, &repo) + .await + .expect_err("this command should fail"); + assert!(repo.valid()); + + assert_eq!( + git::git_current_branch(&repo.get_path()).await.unwrap(), + "main" + ); + } + + #[tokio::test] + async fn test_scratch() { + let temp = tempdir().unwrap(); + let scratch = core::Scratchpad::new("2019w15", temp.path().join("scratch").into()); + + let core = core::CoreBuilder::default() + .with_config(&Config::for_dev_directory(temp.path())) + .with_mock_output() + .with_mock_resolver(|r| { + r.set_repo(Repo::new( + "example.com/test/cmd-branch", + temp.path().to_path_buf(), + )) + }) + .build(); + + let task = GitSwitch { + branch: "test".into(), + create_if_missing: true, + }; + + task.apply_scratchpad(&core, &scratch).await.unwrap(); + assert_eq!(scratch.get_path().join(".git").exists(), false); + assert_eq!(scratch.exists(), false); + } +} diff --git a/src/tasks/mod.rs b/src/tasks/mod.rs index a9f84b29..692ffe9e 100644 --- a/src/tasks/mod.rs +++ b/src/tasks/mod.rs @@ -25,6 +25,7 @@ mod git_clone; mod git_commit; mod git_init; mod git_remote; +mod git_switch; mod new_folder; mod write_file; @@ -35,6 +36,7 @@ pub use git_clone::GitClone; pub use git_commit::GitCommit; pub use git_init::GitInit; pub use git_remote::GitRemote; +pub use git_switch::GitSwitch; pub use new_folder::NewFolder; pub use sequence::Sequence; pub use write_file::WriteFile;