1use std::fmt;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4
5use anyhow::{anyhow, Result};
6use async_trait::async_trait;
7use futures::future::join_all;
8use git2::{Progress, Repository};
9use git2_credentials::CredentialHandler;
10use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
11use once_cell::sync::Lazy;
12use tokio::{
13 spawn,
14 sync::mpsc::{self, Sender},
15 task::spawn_blocking,
16};
17
18use crate::overlays::Overlay;
19use crate::{
20 exec::{Action, Ctx},
21 ui::{self, emojis, style},
22};
23
24pub async fn clone_repositories(ctx: Ctx, overlay: &Overlay, to: &Path) -> Result<()> {
25 if let Some(git_repos) = &overlay.git {
26 ui::info(format!(
27 "{} {}",
28 emojis::THREAD,
29 style::white("Cloning repositories"),
30 ))?;
31 let subctx = ctx.with_multiprogress(MultiProgress::new());
32 let _clones = join_all(git_repos.iter().map(|(path, url)| {
33 let target = to.join(path);
34 let url = url.to_string();
35 let ctx = subctx.clone();
36 spawn(async move {
37 let action = EnsureGitRepository::new(target, url.to_string());
38 action.execute(ctx).await
39 })
40 }))
41 .await;
42 };
43 Ok(())
44}
45
46pub struct EnsureGitRepository {
47 pub path: PathBuf,
48 pub remote: String,
49}
50
51impl EnsureGitRepository {
52 pub fn new(path: PathBuf, remote: String) -> Self {
53 Self { path, remote }
54 }
55
56 fn short_name(&self) -> &'static str {
57 let name = String::from(
58 self.remote
59 .split("/")
60 .last()
61 .unwrap()
62 .trim_end_matches(".git"),
63 );
64 Box::leak(name.into_boxed_str())
65 }
66}
67
68impl fmt::Display for EnsureGitRepository {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 write!(f, "{} -> {}", self.path.display(), self.remote)
71 }
72}
73
74#[async_trait]
75impl Action for EnsureGitRepository {
76 async fn execute(&self, ctx: Ctx) -> Result<()> {
77 let pb = ctx
78 .try_multiprogress()
79 .unwrap()
80 .add(ProgressBar::new(100))
81 .with_style(CLONE_PROGRESS_STYLE.clone())
82 .with_prefix(self.short_name());
83
84 if self.path.exists() {
85 if ctx.verbose {
86 pb.with_style(DONE_PROGRESS_STYLE.clone())
87 .finish_with_message("Repository exists");
88 } else {
89 pb.finish_and_clear();
90 }
91 } else if !ctx.dry_run {
92 let mut state = CloneState::default();
93 let url = self.remote.clone();
94 let into = self.path.clone();
95 let (tx, mut rx) = mpsc::channel(100);
96 let tx = Arc::new(tx);
97 let task = spawn_blocking(move || clone(&url, &into, &tx));
98
99 while let Some(msg) = rx.recv().await {
100 match msg {
101 CloneMessage::Progress(pr) => state.progress = pr,
102 CloneMessage::Stats(s) => state.stats = s,
103 }
104 state.update_bar(&pb)?;
105 }
106
107 if let Err(e) = task.await? {
108 pb.println(format!("{} {}", emojis::CROSSMARK, e));
109 pb.abandon_with_message(format!("{} Failed", emojis::CROSSMARK));
110 return Err(anyhow!(e));
111 } else {
112 pb.finish_and_clear();
113 }
114 };
115
116 Ok(())
117 }
118}
119
120static CLONE_PROGRESS_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
121 ProgressStyle::with_template("{spinner:.cyan} {prefix} [{bar:.green/yellow}] {msg}")
122 .unwrap()
123 .tick_chars(style::TICK_CHARS_BRAILLE_4_6_DOWN.as_str())
124 .progress_chars(style::THIN_PROGRESS.as_str())
125});
126
127static DONE_PROGRESS_STYLE: Lazy<ProgressStyle> =
128 Lazy::new(|| ProgressStyle::with_template("✅ {prefix}: {msg}").unwrap());
129
130fn clone(url: &str, dst: &Path, progress: &Sender<CloneMessage>) -> Result<Repository> {
131 let mut cb = git2::RemoteCallbacks::new();
132 let git_config = git2::Config::open_default().unwrap();
133
134 let mut ch = CredentialHandler::new(git_config);
136 cb.credentials(move |url, username, allowed| ch.try_next_credential(url, username, allowed));
137 cb.transfer_progress(|stats| {
138 let stats = CloneStats::from(stats);
139 progress.blocking_send(CloneMessage::Stats(stats)).unwrap();
140 true
141 });
142
143 let mut co = git2::build::CheckoutBuilder::new();
144 co.progress(|path, cur, total| {
145 let prog = CloneProgress {
146 path: path.map(|p| p.to_path_buf()),
147 current: cur,
148 total,
149 };
150 progress
151 .blocking_send(CloneMessage::Progress(prog))
152 .unwrap();
153 });
154
155 let mut fo = git2::FetchOptions::new();
157 fo.remote_callbacks(cb)
158 .download_tags(git2::AutotagOption::All)
159 .update_fetchhead(true);
160 let repo = git2::build::RepoBuilder::new()
163 .fetch_options(fo)
164 .with_checkout(co)
165 .clone(url, dst)?;
166
167 Ok(repo)
168}
169
170#[derive(Debug, Default)]
171struct CloneStats {
172 total_objects: usize,
173 indexed_objects: usize,
174 received_objects: usize,
175 local_objects: usize,
176 total_deltas: usize,
177 indexed_deltas: usize,
178 received_bytes: usize,
179}
180
181unsafe impl Send for CloneStats {}
182
183impl CloneStats {
184 fn from(stats: Progress) -> Self {
185 Self {
186 total_objects: stats.total_objects(),
187 indexed_objects: stats.indexed_objects(),
188 received_objects: stats.received_objects(),
189 local_objects: stats.local_objects(),
190 total_deltas: stats.total_deltas(),
191 indexed_deltas: stats.indexed_deltas(),
192 received_bytes: stats.received_bytes(),
193 }
194 }
195}
196
197#[derive(Debug, Default)]
198struct CloneProgress {
199 total: usize,
200 current: usize,
201 path: Option<PathBuf>,
202}
203
204#[derive(Debug)]
205enum CloneMessage {
206 Stats(CloneStats),
207 Progress(CloneProgress),
208}
209
210unsafe impl Send for CloneProgress {}
211
212#[derive(Debug, Default)]
213struct CloneState {
214 stats: CloneStats,
215 progress: CloneProgress,
216}
217
218impl CloneState {
219 fn update_bar(&self, bar: &ProgressBar) -> Result<()> {
220 let stats = &self.stats;
221 let network_pct = (100 * stats.received_objects) / stats.total_objects;
222 let index_pct = (100 * stats.indexed_objects) / stats.total_objects;
223 let co_pct = if self.progress.total > 0 {
224 (100 * self.progress.current) / self.progress.total
225 } else {
226 0
227 };
228 bar.set_length(u64::try_from(stats.total_objects)?);
229 bar.set_position(u64::try_from(stats.indexed_objects)?);
230 let kbytes = stats.received_bytes / 1024;
231 if stats.received_objects == stats.total_objects {
232 bar.set_message(format!(
233 "Resolving deltas {}/{}\r",
234 stats.indexed_deltas, stats.total_deltas
235 ));
236 } else {
237 bar.set_message(format!(
238 "net {:3}% ({:4} kb, {:5}/{:5}) / idx {:3}% ({:5}/{:5}) \
239 / chk {:3}% ({:4}/{:4}) {}\r",
240 network_pct,
241 kbytes,
242 stats.received_objects,
243 stats.total_objects,
244 index_pct,
245 stats.indexed_objects,
246 stats.total_objects,
247 co_pct,
248 self.progress.current,
249 self.progress.total,
250 self.progress
251 .path
252 .as_ref()
253 .map(|s| s.to_string_lossy().into_owned())
254 .unwrap_or_default()
255 ));
256 }
257 Ok(())
258 }
259}