diff --git a/crates/gitnow/src/commands/root.rs b/crates/gitnow/src/commands/root.rs index 31613c6..67e76b8 100644 --- a/crates/gitnow/src/commands/root.rs +++ b/crates/gitnow/src/commands/root.rs @@ -4,6 +4,7 @@ use crate::{ app::App, cache::CacheApp, fuzzy_matcher::{FuzzyMatcher, FuzzyMatcherApp}, + git_clone::GitCloneApp, git_provider::Repository, interactive::InteractiveApp, projects_list::ProjectsListApp, @@ -23,6 +24,7 @@ impl RootCommand { &mut self, search: Option>, cache: bool, + clone: bool, ) -> anyhow::Result<()> { tracing::debug!("executing"); @@ -41,7 +43,8 @@ impl RootCommand { } else { self.app.projects_list().get_projects().await? }; - match search { + + let repo = match search { Some(needle) => { let matched_repos = self .app @@ -51,7 +54,9 @@ impl RootCommand { let repo = matched_repos .first() .ok_or(anyhow::anyhow!("failed to find repository"))?; - tracing::info!("selected repo: {}", repo.to_rel_path().display()); + tracing::debug!("selected repo: {}", repo.to_rel_path().display()); + + repo.to_owned() } None => { let repo = self @@ -60,8 +65,14 @@ impl RootCommand { .interactive_search(&repositories)? .ok_or(anyhow::anyhow!("failed to find a repository"))?; - tracing::info!("selected repo: {}", repo.to_rel_path().display()); + tracing::debug!("selected repo: {}", repo.to_rel_path().display()); + + repo } + }; + + if clone { + self.app.git_clone().clone_repo(&repo).await?; } Ok(()) diff --git a/crates/gitnow/src/config.rs b/crates/gitnow/src/config.rs index 1dc89b8..97d03e9 100644 --- a/crates/gitnow/src/config.rs +++ b/crates/gitnow/src/config.rs @@ -14,10 +14,49 @@ pub struct Config { #[derive(Debug, Default, Serialize, Deserialize, PartialEq)] pub struct Settings { + #[serde(default)] + pub projects: Projects, + #[serde(default)] pub cache: Cache, } +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct Projects { + pub directory: ProjectLocation, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ProjectLocation(PathBuf); + +impl From for ProjectLocation { + fn from(value: PathBuf) -> Self { + Self(value) + } +} + +impl From for PathBuf { + fn from(value: ProjectLocation) -> Self { + value.0 + } +} + +impl Default for ProjectLocation { + fn default() -> Self { + let home = dirs::home_dir().unwrap_or_default(); + + Self(home.join("git")) + } +} + +impl std::ops::Deref for ProjectLocation { + type Target = PathBuf; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + #[derive(Debug, Default, Serialize, Deserialize, PartialEq)] pub struct Cache { #[serde(default)] @@ -231,6 +270,9 @@ mod test { #[test] fn test_can_parse_config() -> anyhow::Result<()> { let content = r#" + [settings] + projects = { directory = "git" } + [settings.cache] location = ".cache/gitnow" duration = { days = 2 } @@ -316,6 +358,9 @@ mod test { hours: 0, minutes: 0 } + }, + projects: Projects { + directory: PathBuf::from("git").into() } } }, @@ -340,7 +385,8 @@ mod test { gitea: vec![] }, settings: Settings { - cache: Cache::default() + cache: Cache::default(), + projects: Projects::default() } }, config diff --git a/crates/gitnow/src/git_clone.rs b/crates/gitnow/src/git_clone.rs new file mode 100644 index 0000000..f187faf --- /dev/null +++ b/crates/gitnow/src/git_clone.rs @@ -0,0 +1,74 @@ +use crate::{app::App, git_provider::Repository}; + +pub struct GitClone { + app: &'static App, +} + +impl GitClone { + pub fn new(app: &'static App) -> Self { + Self { app } + } + + pub async fn clone_repo(&self, repository: &Repository) -> anyhow::Result<()> { + let project_path = self + .app + .config + .settings + .projects + .directory + .join(repository.to_rel_path()); + + if project_path.exists() { + tracing::info!( + "project: {} already exists, skipping clone", + repository.to_rel_path().display() + ); + return Ok(()); + } + + tracing::info!( + "cloning: {} into {}", + repository.ssh_url.as_str(), + &project_path.display().to_string(), + ); + + let mut cmd = tokio::process::Command::new("git"); + cmd.args([ + "clone", + repository.ssh_url.as_str(), + &project_path.display().to_string(), + ]); + + let output = cmd.output().await?; + match output.status.success() { + true => tracing::debug!( + "cloned {} into {}", + repository.ssh_url.as_str(), + &project_path.display().to_string(), + ), + false => { + let stdout = std::str::from_utf8(&output.stdout).unwrap_or_default(); + let stderr = std::str::from_utf8(&output.stderr).unwrap_or_default(); + tracing::error!( + "failed to clone {} into {}, with output: {}, err: {}", + repository.ssh_url.as_str(), + &project_path.display().to_string(), + stdout, + stderr + ) + } + } + + Ok(()) + } +} + +pub trait GitCloneApp { + fn git_clone(&self) -> GitClone; +} + +impl GitCloneApp for &'static App { + fn git_clone(&self) -> GitClone { + GitClone::new(self) + } +} diff --git a/crates/gitnow/src/main.rs b/crates/gitnow/src/main.rs index e39d44b..a3f926c 100644 --- a/crates/gitnow/src/main.rs +++ b/crates/gitnow/src/main.rs @@ -13,6 +13,7 @@ mod cache_codec; mod commands; mod config; mod fuzzy_matcher; +mod git_clone; mod git_provider; mod interactive; mod projects_list; @@ -28,6 +29,9 @@ struct Command { #[arg(long = "no-cache", default_value = "false")] no_cache: bool, + + #[arg(long = "no-clone", default_value = "false")] + no_clone: bool, } #[derive(Subcommand)] @@ -60,7 +64,7 @@ async fn main() -> anyhow::Result<()> { Some(_) => todo!(), None => { RootCommand::new(app) - .execute(cli.search.as_ref(), !cli.no_cache) + .execute(cli.search.as_ref(), !cli.no_cache, !cli.no_clone) .await?; } }