git_workspace/commands/
mod.rs

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