From fe9d877cdfc0a59bdb3a4a3b20365ce325bcc2c5 Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Fri, 13 Oct 2023 11:01:35 +0200 Subject: [PATCH] Add support for pushing custom git repositories (#574) --- config.example.toml | 20 ++++++- src/config.rs | 41 ++++++++++++-- src/main.rs | 51 +++++++++++------ src/steps/git.rs | 118 ++++++++++++++++++++++++++++++++++------ src/steps/os/windows.rs | 3 +- src/steps/zsh.rs | 2 +- 6 files changed, 192 insertions(+), 43 deletions(-) diff --git a/config.example.toml b/config.example.toml index b60f049a..fb8c5096 100644 --- a/config.example.toml +++ b/config.example.toml @@ -116,17 +116,31 @@ [git] #max_concurrency = 5 -# Additional git repositories to pull +# Git repositories that you want to pull and push #repos = [ # "~/src/*/", # "~/.config/something" #] +# Repositories that you only want to pull +#pull_only_repos = [ +# "~/.config/something_else" +#] + +# Repositories that you only want to push +#push_only_repos = [ +# "~/src/*/", +# "~/.config/something_third" +#] + # Don't pull the predefined git repos #pull_predefined = false -# Arguments to pass Git when pulling Repositories -#arguments = "--rebase --autostash" +# Arguments to pass Git when pulling repositories +#pull_arguments = "--rebase --autostash" + +# Arguments to pass Git when pushing repositories +#push_arguments = "--all" [windows] # Manually select Windows updates diff --git a/src/config.rs b/src/config.rs index c5bc7ed0..4272c9d0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -212,10 +212,17 @@ pub struct Git { max_concurrency: Option, #[merge(strategy = crate::utils::merge_strategies::string_append_opt)] - arguments: Option, + pull_arguments: Option, + + #[merge(strategy = crate::utils::merge_strategies::string_append_opt)] + push_arguments: Option, #[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)] repos: Option>, + #[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)] + pull_only_repos: Option>, + #[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)] + push_only_repos: Option>, pull_predefined: Option, } @@ -905,10 +912,24 @@ impl Config { &self.config_file.commands } - /// The list of additional git repositories to pull. + /// The list of git repositories to push and pull. pub fn git_repos(&self) -> &Option> { get_deprecated_moved_opt!(&self.config_file.misc, git_repos, &self.config_file.git, repos) } + /// The list of additional git repositories to pull. + pub fn git_pull_only_repos(&self) -> Option<&Vec> { + self.config_file + .git + .as_ref() + .and_then(|git| git.pull_only_repos.as_ref()) + } + /// The list of git repositories to push. + pub fn git_push_only_repos(&self) -> Option<&Vec> { + self.config_file + .git + .as_ref() + .and_then(|git| git.push_only_repos.as_ref()) + } /// Tell whether the specified step should run. /// @@ -1018,9 +1039,19 @@ impl Config { .and_then(|misc| misc.ssh_arguments.as_ref()) } - /// Extra Git arguments - pub fn git_arguments(&self) -> &Option { - get_deprecated_moved_opt!(&self.config_file.misc, git_arguments, &self.config_file.git, arguments) + /// Extra Git arguments for when pushing + pub fn push_git_arguments(&self) -> Option<&String> { + self.config_file + .git + .as_ref() + .and_then(|git| git.push_arguments.as_ref()) + } + /// Extra Git arguments for when pulling + pub fn pull_git_arguments(&self) -> Option<&String> { + self.config_file + .git + .as_ref() + .and_then(|git| git.pull_arguments.as_ref()) } /// Extra Tmux arguments diff --git a/src/main.rs b/src/main.rs index 7d275eb5..7ae916b5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,8 @@ use etcetera::base_strategy::{BaseStrategy, Xdg}; use once_cell::sync::Lazy; use tracing::debug; +use crate::steps::git::GitAction; + use self::config::{CommandLineArgs, Config, Step}; use self::error::StepFailed; #[cfg(all(windows, feature = "self-update"))] @@ -373,35 +375,35 @@ fn run() -> Result<()> { if config.should_run(Step::Emacs) { if !emacs.is_doom() { if let Some(directory) = emacs.directory() { - git_repos.insert_if_repo(directory); + git_repos.insert_if_repo(directory, GitAction::Pull); } } - git_repos.insert_if_repo(HOME_DIR.join(".doom.d")); + git_repos.insert_if_repo(HOME_DIR.join(".doom.d"), GitAction::Pull); } if config.should_run(Step::Vim) { - git_repos.insert_if_repo(HOME_DIR.join(".vim")); - git_repos.insert_if_repo(HOME_DIR.join(".config/nvim")); + git_repos.insert_if_repo(HOME_DIR.join(".vim"), GitAction::Pull); + git_repos.insert_if_repo(HOME_DIR.join(".config/nvim"), GitAction::Pull); } - git_repos.insert_if_repo(HOME_DIR.join(".ideavimrc")); - git_repos.insert_if_repo(HOME_DIR.join(".intellimacs")); + git_repos.insert_if_repo(HOME_DIR.join(".ideavimrc"), GitAction::Pull); + git_repos.insert_if_repo(HOME_DIR.join(".intellimacs"), GitAction::Pull); if config.should_run(Step::Rcm) { - git_repos.insert_if_repo(HOME_DIR.join(".dotfiles")); + git_repos.insert_if_repo(HOME_DIR.join(".dotfiles"), GitAction::Pull); } #[cfg(unix)] { - git_repos.insert_if_repo(zsh::zshrc()); + git_repos.insert_if_repo(zsh::zshrc(), GitAction::Pull); if config.should_run(Step::Tmux) { - git_repos.insert_if_repo(HOME_DIR.join(".tmux")); + git_repos.insert_if_repo(HOME_DIR.join(".tmux"), GitAction::Pull); } - git_repos.insert_if_repo(HOME_DIR.join(".config/fish")); - git_repos.insert_if_repo(XDG_DIRS.config_dir().join("openbox")); - git_repos.insert_if_repo(XDG_DIRS.config_dir().join("bspwm")); - git_repos.insert_if_repo(XDG_DIRS.config_dir().join("i3")); - git_repos.insert_if_repo(XDG_DIRS.config_dir().join("sway")); + git_repos.insert_if_repo(HOME_DIR.join(".config/fish"), GitAction::Pull); + git_repos.insert_if_repo(XDG_DIRS.config_dir().join("openbox"), GitAction::Pull); + git_repos.insert_if_repo(XDG_DIRS.config_dir().join("bspwm"), GitAction::Pull); + git_repos.insert_if_repo(XDG_DIRS.config_dir().join("i3"), GitAction::Pull); + git_repos.insert_if_repo(XDG_DIRS.config_dir().join("sway"), GitAction::Pull); } #[cfg(windows)] @@ -409,24 +411,39 @@ fn run() -> Result<()> { WINDOWS_DIRS .cache_dir() .join("Packages/Microsoft.WindowsTerminal_8wekyb3d8bbwe/LocalState"), + GitAction::Pull, ); #[cfg(windows)] windows::insert_startup_scripts(&mut git_repos).ok(); if let Some(profile) = powershell.profile() { - git_repos.insert_if_repo(profile); + git_repos.insert_if_repo(profile, GitAction::Pull); } } if config.should_run(Step::GitRepos) { if let Some(custom_git_repos) = config.git_repos() { for git_repo in custom_git_repos { - git_repos.glob_insert(git_repo); + git_repos.glob_insert(git_repo, GitAction::Pull); + git_repos.glob_insert(git_repo, GitAction::Push); } } + + if let Some(git_pull_only_repos) = config.git_pull_only_repos() { + for git_repo in git_pull_only_repos { + git_repos.glob_insert(git_repo, GitAction::Pull); + } + } + + if let Some(git_push_only_repos) = config.git_push_only_repos() { + for git_repo in git_push_only_repos { + git_repos.glob_insert(git_repo, GitAction::Push); + } + } + runner.execute(Step::GitRepos, "Git repositories", || { - git.multi_pull_step(&git_repos, &ctx) + git.multi_repo_step(&git_repos, &ctx) })?; } diff --git a/src/steps/git.rs b/src/steps/git.rs index 4657447f..f78fb7bc 100644 --- a/src/steps/git.rs +++ b/src/steps/git.rs @@ -27,9 +27,16 @@ pub struct Git { git: Option, } +#[derive(Clone, Copy)] +pub enum GitAction { + Push, + Pull, +} + pub struct Repositories<'a> { git: &'a Git, - repositories: HashSet, + pull_repositories: HashSet, + push_repositories: HashSet, glob_match_options: MatchOptions, bad_patterns: Vec, } @@ -44,6 +51,36 @@ fn output_checked_utf8(output: Output) -> Result<()> { Ok(()) } } +async fn push_repository(repo: String, git: &Path, ctx: &ExecutionContext<'_>) -> Result<()> { + let path = repo.to_string(); + + println!("{} {}", style("Pushing").cyan().bold(), path); + + let mut command = AsyncCommand::new(git); + + command + .stdin(Stdio::null()) + .current_dir(&repo) + .args(["push", "--porcelain"]); + if let Some(extra_arguments) = ctx.config().push_git_arguments() { + command.args(extra_arguments.split_whitespace()); + } + + let output = command.output().await?; + let result = match output.status.success() { + true => Ok(()), + false => Err(format!("Failed to push {repo}")), + }; + + if result.is_err() { + println!("{} pushing {}", style("Failed").red().bold(), &repo); + }; + + match result { + Ok(_) => Ok(()), + Err(e) => Err(eyre!(e)), + } +} async fn pull_repository(repo: String, git: &Path, ctx: &ExecutionContext<'_>) -> Result<()> { let path = repo.to_string(); @@ -58,7 +95,7 @@ async fn pull_repository(repo: String, git: &Path, ctx: &ExecutionContext<'_>) - .current_dir(&repo) .args(["pull", "--ff-only"]); - if let Some(extra_arguments) = ctx.config().git_arguments() { + if let Some(extra_arguments) = ctx.config().pull_git_arguments() { command.args(extra_arguments.split_whitespace()); } @@ -181,7 +218,7 @@ impl Git { None } - pub fn multi_pull_step(&self, repositories: &Repositories, ctx: &ExecutionContext) -> Result<()> { + pub fn multi_repo_step(&self, repositories: &Repositories, ctx: &ExecutionContext) -> Result<()> { // Warn the user about the bad patterns. // // NOTE: this should be executed **before** skipping the Git step or the @@ -192,12 +229,15 @@ impl Git { .iter() .for_each(|pattern| print_warning(format!("Path {pattern} did not contain any git repositories"))); - if repositories.repositories.is_empty() { - return Err(SkipStep(String::from("No repositories to pull")).into()); + if repositories.is_empty() { + return Err(SkipStep(String::from("No repositories to pull or push")).into()); } print_separator("Git repositories"); - self.multi_pull(repositories, ctx) + self.multi_push(repositories, ctx)?; + self.multi_pull(repositories, ctx)?; + + Ok(()) } pub fn multi_pull(&self, repositories: &Repositories, ctx: &ExecutionContext) -> Result<()> { @@ -205,7 +245,7 @@ impl Git { if ctx.run_type().dry() { repositories - .repositories + .pull_repositories .iter() .for_each(|repo| println!("Would pull {}", &repo)); @@ -213,7 +253,7 @@ impl Git { } let futures_iterator = repositories - .repositories + .pull_repositories .iter() .filter(|repo| match has_remotes(git, repo) { Some(false) => { @@ -240,6 +280,47 @@ impl Git { let error = results.into_iter().find(|r| r.is_err()); error.unwrap_or(Ok(())) } + + pub fn multi_push(&self, repositories: &Repositories, ctx: &ExecutionContext) -> Result<()> { + let git = self.git.as_ref().unwrap(); + + if ctx.run_type().dry() { + repositories + .push_repositories + .iter() + .for_each(|repo| println!("Would push {}", &repo)); + + return Ok(()); + } + + let futures_iterator = repositories + .push_repositories + .iter() + .filter(|repo| match has_remotes(git, repo) { + Some(false) => { + println!( + "{} {} because it has no remotes", + style("Skipping").yellow().bold(), + repo + ); + false + } + _ => true, // repo has remotes or command to check for remotes has failed. proceed to pull anyway. + }) + .map(|repo| push_repository(repo.clone(), git, ctx)); + + let stream_of_futures = if let Some(limit) = ctx.config().git_concurrency_limit() { + iter(futures_iterator).buffer_unordered(limit).boxed() + } else { + futures_iterator.collect::>().boxed() + }; + + let basic_rt = runtime::Runtime::new()?; + let results = basic_rt.block_on(async { stream_of_futures.collect::>>().await }); + + let error = results.into_iter().find(|r| r.is_err()); + error.unwrap_or(Ok(())) + } } impl<'a> Repositories<'a> { @@ -252,22 +333,27 @@ impl<'a> Repositories<'a> { Self { git, - repositories: HashSet::new(), bad_patterns: Vec::new(), glob_match_options, + pull_repositories: HashSet::new(), + push_repositories: HashSet::new(), } } - pub fn insert_if_repo>(&mut self, path: P) -> bool { + pub fn insert_if_repo>(&mut self, path: P, action: GitAction) -> bool { if let Some(repo) = self.git.get_repo_root(path) { - self.repositories.insert(repo); + match action { + GitAction::Push => self.push_repositories.insert(repo), + GitAction::Pull => self.pull_repositories.insert(repo), + }; + true } else { false } } - pub fn glob_insert(&mut self, pattern: &str) { + pub fn glob_insert(&mut self, pattern: &str, action: GitAction) { if let Ok(glob) = glob_with(pattern, self.glob_match_options) { let mut last_git_repo: Option = None; for entry in glob { @@ -283,7 +369,7 @@ impl<'a> Repositories<'a> { continue; } } - if self.insert_if_repo(&path) { + if self.insert_if_repo(&path, action) { last_git_repo = Some(path); } } @@ -301,14 +387,14 @@ impl<'a> Repositories<'a> { } } - #[cfg(unix)] pub fn is_empty(&self) -> bool { - self.repositories.is_empty() + self.pull_repositories.is_empty() && self.push_repositories.is_empty() } #[cfg(unix)] pub fn remove(&mut self, path: &str) { - let _removed = self.repositories.remove(path); + let _removed = self.pull_repositories.remove(path); + let _removed = self.push_repositories.remove(path); debug_assert!(_removed); } } diff --git a/src/steps/os/windows.rs b/src/steps/os/windows.rs index a459584f..1567876c 100644 --- a/src/steps/os/windows.rs +++ b/src/steps/os/windows.rs @@ -8,6 +8,7 @@ use tracing::debug; use crate::command::CommandExt; use crate::execution_context::ExecutionContext; +use crate::steps::git::GitAction; use crate::terminal::{print_separator, print_warning}; use crate::utils::{require, which}; use crate::{error::SkipStep, steps::git::Repositories}; @@ -239,7 +240,7 @@ pub fn insert_startup_scripts(git_repos: &mut Repositories) -> Result<()> { if let Ok(lnk) = parselnk::Lnk::try_from(Path::new(&path)) { debug!("Startup link: {:?}", lnk); if let Some(path) = lnk.relative_path() { - git_repos.insert_if_repo(&startup_dir.join(path)); + git_repos.insert_if_repo(&startup_dir.join(path), GitAction::Pull); } } } diff --git a/src/steps/zsh.rs b/src/steps/zsh.rs index 6e913afd..8274bd72 100644 --- a/src/steps/zsh.rs +++ b/src/steps/zsh.rs @@ -227,7 +227,7 @@ pub fn run_oh_my_zsh(ctx: &ExecutionContext) -> Result<()> { for entry in WalkDir::new(custom_dir).max_depth(2) { let entry = entry?; - custom_repos.insert_if_repo(entry.path()); + custom_repos.insert_if_repo(entry.path(), crate::steps::git::GitAction::Pull); } custom_repos.remove(&oh_my_zsh.to_string_lossy());