Skip to main content

git_workspace/commands/
mod.rs

1pub mod add_provider;
2pub mod archive;
3pub mod completion;
4pub mod fetch;
5pub mod list;
6pub mod lock;
7pub mod run;
8pub mod switch_and_pull;
9pub mod update;
10
11pub use add_provider::add_provider_to_config;
12pub use archive::archive;
13pub use completion::completion;
14pub use fetch::fetch;
15pub use list::list;
16pub use lock::lock;
17pub use run::execute_cmd;
18pub use switch_and_pull::pull_all_repositories;
19pub use update::update;
20
21use crate::repository::Repository;
22use anyhow::{anyhow, Context};
23use atomic_counter::{AtomicCounter, RelaxedCounter};
24use indicatif::{MultiProgress, ParallelProgressIterator, ProgressBar, ProgressStyle};
25use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
26use std::collections::HashSet;
27use std::path::{Path, PathBuf};
28use std::sync::Arc;
29use std::time::Duration;
30use walkdir::WalkDir;
31
32/// Take any number of repositories and apply `f` on each one.
33/// This method takes care of displaying progress bars and displaying
34/// any errors that may arise.
35pub fn map_repositories<F>(repositories: &[Repository], threads: usize, f: F) -> anyhow::Result<()>
36where
37    F: Fn(&Repository, &ProgressBar) -> anyhow::Result<()> + std::marker::Sync,
38{
39    // Create our progress bar. We use Arc here as we need to share the MultiProgress across
40    // more than 1 thread (described below)
41    let progress = Arc::new(MultiProgress::new());
42    // Create our total progress bar used with `.progress_iter()`.
43    let total_bar = progress.add(ProgressBar::new(repositories.len() as u64));
44    total_bar.set_style(
45        ProgressStyle::default_bar()
46            .template("[{elapsed_precise}] {percent}% [{wide_bar:.cyan/blue}] {pos}/{len} (ETA: {eta_precise})").expect("Invalid template")
47            .progress_chars("#>-"),
48    );
49
50    // user_attended() means a tty is attached to the output.
51    let is_attended = console::user_attended();
52    let total_repositories = repositories.len();
53    // Use a counter here if there is no tty, to show a stream of progress messages rather than
54    // a dynamic progress bar.
55    let counter = RelaxedCounter::new(1);
56
57    // Create our thread pool. We do this rather than use `.par_iter()` on any iterable as it
58    // allows us to customize the number of threads.
59    let pool = rayon::ThreadPoolBuilder::new()
60        .num_threads(threads)
61        .build()
62        .with_context(|| "Error creating the thread pool")?;
63
64    // pool.install means that `.par_iter()` will use the thread pool we've built above.
65    let errors: Vec<(&Repository, anyhow::Error)> = pool.install(|| {
66        repositories
67            .par_iter()
68            // Update our progress bar with each iteration
69            .map(|repo| {
70                // Create a progress bar and configure some defaults
71                let progress_bar = progress.add(ProgressBar::new_spinner());
72                progress_bar.set_message("waiting...");
73                progress_bar.enable_steady_tick(Duration::from_millis(500));
74                // Increment our counter for use if the console is not a tty.
75                let idx = counter.inc();
76                if !is_attended {
77                    println!("[{}/{}] Starting {}", idx, total_repositories, repo.name());
78                }
79                // Run our given function. If the result is an error then attach the
80                // erroring Repository object to it.
81                let result = match f(repo, &progress_bar) {
82                    Ok(_) => Ok(()),
83                    Err(e) => Err((repo, e)),
84                };
85                if !is_attended {
86                    println!("[{}/{}] Finished {}", idx, total_repositories, repo.name());
87                }
88                // Clear the progress bar and return the result
89                progress_bar.finish_and_clear();
90                result
91            })
92            .progress_with(total_bar)
93            // We only care about errors here, so filter them out.
94            .filter_map(Result::err)
95            // Collect the results into a Vec
96            .collect()
97    });
98
99    // Print out each repository that failed to run.
100    if !errors.is_empty() {
101        eprintln!("{} repositories failed:", errors.len());
102        for (repo, error) in errors {
103            eprintln!("{}:", repo.name());
104            error
105                .chain()
106                .for_each(|cause| eprintln!("because: {}", cause));
107        }
108    }
109
110    Ok(())
111}
112
113/// Find all projects that have been archived or deleted on our providers
114pub fn get_all_repositories_to_archive(
115    workspace: &Path,
116    repositories: Vec<Repository>,
117) -> anyhow::Result<Vec<(PathBuf, PathBuf)>> {
118    // The logic here is as follows:
119    // 1. Iterate through all directories. If it's a "safe" directory (one that contains a project
120    //    in our lockfile), we skip it entirely.
121    // 2. If the directory is not, and contains a `.git` directory, then we mark it for archival and
122    //    skip processing.
123    // This assumes nobody deletes a .git directory in one of their projects.
124
125    // Windows doesn't like .archive.
126    let archive_directory = if cfg!(windows) {
127        workspace.join("_archive")
128    } else {
129        workspace.join(".archive")
130    };
131
132    // Create a set of all repository paths that currently exist.
133    let mut repository_paths: HashSet<PathBuf> = repositories
134        .iter()
135        .filter(|r| r.exists(workspace))
136        .map(|r| r.get_path(workspace))
137        .filter_map(Result::ok)
138        .collect();
139
140    // If the archive directory does not exist then we create it
141    if !archive_directory.exists() {
142        fs_extra::dir::create(&archive_directory, false).with_context(|| {
143            format!(
144                "Error creating archive directory {}",
145                archive_directory.display()
146            )
147        })?;
148    }
149
150    // Make sure we add our archive directory to the set of repository paths. This ensures that
151    // it's not traversed below!
152    repository_paths.insert(
153        archive_directory
154            .canonicalize()
155            .with_context(|| "Error canoncalizing archive directory")?,
156    );
157
158    let mut to_archive = Vec::new();
159    let mut it = WalkDir::new(workspace).into_iter();
160
161    // Waldir provides a `filter_entry` method, but I couldn't work out how to use it
162    // correctly here. So we just roll our own loop:
163    loop {
164        // Find the next directory. This can throw an error, in which case we bail out.
165        // Perhaps we shouldn't bail here?
166        let entry = match it.next() {
167            None => break,
168            Some(Err(err)) => return Err(anyhow!("Error iterating through directory: {}", err)),
169            Some(Ok(entry)) => entry,
170        };
171        // If the current path is in the set of repository paths then we skip processing it entirely.
172        if repository_paths.contains(entry.path()) {
173            it.skip_current_dir();
174            continue;
175        }
176        // If the entry has a .git directory inside it then we add it to the `to_archive` list
177        // and skip the current directory.
178        if entry.path().join(".git").is_dir() {
179            let path = entry.path();
180            // Find the relative path of the directory from the workspace. So if you have something
181            // like `workspace/github/repo-name`, it will be `github/repo-name`.
182            let relative_dir = path.strip_prefix(workspace).with_context(|| {
183                format!(
184                    "Failed to strip the prefix '{}' from {}",
185                    workspace.display(),
186                    path.display()
187                )
188            })?;
189            // Join the relative directory (`github/repo-name`) with the archive directory.
190            let to_dir = archive_directory.join(relative_dir);
191            to_archive.push((path.to_path_buf(), to_dir));
192            it.skip_current_dir();
193            continue;
194        }
195    }
196
197    Ok(to_archive)
198}