Add support for pushing custom git repositories (#574)

This commit is contained in:
Sam Vente
2023-10-13 11:01:35 +02:00
committed by GitHub
parent 60e7aa8f03
commit fe9d877cdf
6 changed files with 192 additions and 43 deletions

View File

@@ -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

View File

@@ -212,10 +212,17 @@ pub struct Git {
max_concurrency: Option<usize>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
arguments: Option<String>,
pull_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::string_append_opt)]
push_arguments: Option<String>,
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
repos: Option<Vec<String>>,
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
pull_only_repos: Option<Vec<String>>,
#[merge(strategy = crate::utils::merge_strategies::vec_prepend_opt)]
push_only_repos: Option<Vec<String>>,
pull_predefined: Option<bool>,
}
@@ -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<Vec<String>> {
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<String>> {
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<String>> {
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<String> {
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

View File

@@ -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)
})?;
}

View File

@@ -27,9 +27,16 @@ pub struct Git {
git: Option<PathBuf>,
}
#[derive(Clone, Copy)]
pub enum GitAction {
Push,
Pull,
}
pub struct Repositories<'a> {
git: &'a Git,
repositories: HashSet<String>,
pull_repositories: HashSet<String>,
push_repositories: HashSet<String>,
glob_match_options: MatchOptions,
bad_patterns: Vec<String>,
}
@@ -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::<FuturesUnordered<_>>().boxed()
};
let basic_rt = runtime::Runtime::new()?;
let results = basic_rt.block_on(async { stream_of_futures.collect::<Vec<Result<()>>>().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<P: AsRef<Path>>(&mut self, path: P) -> bool {
pub fn insert_if_repo<P: AsRef<Path>>(&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<PathBuf> = 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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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());