dot_over/actions/
git.rs

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    // Credentials management
135    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    // clone a repository
156    let mut fo = git2::FetchOptions::new();
157    fo.remote_callbacks(cb)
158        .download_tags(git2::AutotagOption::All)
159        .update_fetchhead(true);
160    // std::fs::create_dir_all(&dst.as_ref()).unwrap();
161
162    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}