1pub mod error;
8mod git;
9pub mod provider;
10
11use std::fs;
12use std::fs::File;
13use std::path::Path;
14use std::path::PathBuf;
15
16use fs2::FileExt;
18
19use log::{debug, error, info, trace};
21
22use slug::slugify;
24
25#[macro_use]
27extern crate serde_derive;
28
29use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
31
32use time::OffsetDateTime;
34
35use junit_report::{ReportBuilder, TestCase, TestCaseBuilder, TestSuite, TestSuiteBuilder};
36
37use prometheus::register_gauge_vec;
39use prometheus::{Encoder, TextEncoder};
40
41use provider::{MirrorError, MirrorResult, Provider};
42
43use git::{Git, GitWrapper};
44
45use error::{GitMirrorError, Result};
46
47pub fn mirror_repo(
48 origin: &str,
49 destination: &str,
50 refspec: &Option<Vec<String>>,
51 lfs: bool,
52 opts: &MirrorOptions,
53) -> Result<()> {
54 if opts.dry_run {
55 return Ok(());
56 }
57
58 let origin_dir = Path::new(&opts.mirror_dir).join(slugify(origin));
59 debug!("Using origin dir: {origin_dir:?}");
60
61 let git = Git::new(opts.git_executable.clone(), opts.mirror_lfs);
62
63 git.git_version()?;
64
65 if opts.mirror_lfs {
66 git.git_lfs_version()?;
67 }
68
69 if origin_dir.is_dir() {
70 info!("Local Update for {origin}");
71
72 git.git_update_mirror(origin, &origin_dir, lfs)?;
73 } else if !origin_dir.exists() {
74 info!("Local Checkout for {origin}");
75
76 git.git_clone_mirror(origin, &origin_dir, lfs)?;
77 } else {
78 return Err(GitMirrorError::GenericError(format!(
79 "Local origin dir is a file: {origin_dir:?}"
80 )));
81 }
82
83 info!("Push to destination {destination}");
84
85 git.git_push_mirror(destination, &origin_dir, refspec, lfs)?;
86
87 if opts.remove_workrepo {
88 fs::remove_dir_all(&origin_dir).map_err(|e| {
89 GitMirrorError::GenericError(format!(
90 "Unable to delete working repository: {} because of error: {}",
91 &origin_dir.to_string_lossy(),
92 e
93 ))
94 })?;
95 }
96
97 Ok(())
98}
99
100fn run_sync_task(v: &[MirrorResult], label: &str, opts: &MirrorOptions) -> TestSuite {
101 rayon::ThreadPoolBuilder::new()
103 .num_threads(opts.worker_count)
104 .build_global()
105 .unwrap();
106
107 let proj_total =
108 register_gauge_vec!("git_mirror_total", "Total projects", &["mirror"]).unwrap();
109 let proj_skip =
110 register_gauge_vec!("git_mirror_skip", "Skipped projects", &["mirror"]).unwrap();
111 let proj_fail = register_gauge_vec!("git_mirror_fail", "Failed projects", &["mirror"]).unwrap();
112 let proj_ok = register_gauge_vec!("git_mirror_ok", "OK projects", &["mirror"]).unwrap();
113 let proj_start = register_gauge_vec!(
114 "git_mirror_project_start",
115 "Start of project mirror as unix timestamp",
116 &["origin", "destination", "mirror"]
117 )
118 .unwrap();
119 let proj_end = register_gauge_vec!(
120 "git_mirror_project_end",
121 "End of project mirror as unix timestamp",
122 &["origin", "destination", "mirror"]
123 )
124 .unwrap();
125
126 let total = v.len();
127 let results = v
128 .par_iter()
129 .enumerate()
130 .map(|(i, x)| {
131 proj_total.with_label_values(&[label]).inc();
132 let start = OffsetDateTime::now_utc();
133 match x {
134 Ok(x) => {
135 let name = format!("{} -> {}", x.origin, x.destination);
136 let proj_fail = proj_fail.clone();
137 let proj_ok = proj_ok.clone();
138 let proj_start = proj_start.clone();
139 let proj_end = proj_end.clone();
140 let label = label.to_string();
141 println!(
142 "START {}/{} [{}]: {}",
143 i,
144 total,
145 OffsetDateTime::now_utc(),
146 name
147 );
148 proj_start
149 .with_label_values(&[&x.origin, &x.destination, &label])
150 .set(OffsetDateTime::now_utc().unix_timestamp() as f64);
151 let refspec = match &x.refspec {
152 Some(r) => {
153 debug!("Using repo specific refspec: {r:?}");
154 &x.refspec
155 }
156 None => {
157 match opts.refspec.clone() {
158 Some(r) => {
159 debug!("Using global custom refspec: {r:?}");
160 }
161 None => {
162 debug!("Using no custom refspec.");
163 }
164 }
165 &opts.refspec
166 }
167 };
168 trace!("Refspec used: {refspec:?}");
169 match mirror_repo(&x.origin, &x.destination, refspec, x.lfs, opts) {
170 Ok(_) => {
171 println!(
172 "END(OK) {}/{} [{}]: {}",
173 i,
174 total,
175 OffsetDateTime::now_utc(),
176 name
177 );
178 proj_end
179 .with_label_values(&[&x.origin, &x.destination, &label])
180 .set(OffsetDateTime::now_utc().unix_timestamp() as f64);
181 proj_ok.with_label_values(&[&label]).inc();
182 TestCaseBuilder::success(&name, OffsetDateTime::now_utc() - start)
183 .build()
184 }
185 Err(e) => {
186 println!(
187 "END(FAIL) {}/{} [{}]: {} ({})",
188 i,
189 total,
190 OffsetDateTime::now_utc(),
191 name,
192 e
193 );
194 proj_end
195 .with_label_values(&[&x.origin, &x.destination, &label])
196 .set(OffsetDateTime::now_utc().unix_timestamp() as f64);
197 proj_fail.with_label_values(&[&label]).inc();
198 error!("Unable to sync repo {name} ({e})");
199 TestCaseBuilder::error(
200 &name,
201 OffsetDateTime::now_utc() - start,
202 "sync error",
203 &format!("{e:?}"),
204 )
205 .build()
206 }
207 }
208 }
209 Err(e) => {
210 proj_skip.with_label_values(&[label]).inc();
211 let duration = OffsetDateTime::now_utc() - start;
212
213 match e {
214 MirrorError::Description(d, se) => {
215 error!("Error parsing YAML: {d}, Error: {se:?}");
216 TestCaseBuilder::error("", duration, "parse error", &format!("{e:?}"))
217 .build()
218 }
219 MirrorError::Skip(url) => {
220 println!(
221 "SKIP {}/{} [{}]: {}",
222 i,
223 total,
224 OffsetDateTime::now_utc(),
225 url
226 );
227 TestCaseBuilder::skipped(url).build()
228 }
229 }
230 }
231 }
232 })
233 .collect::<Vec<TestCase>>();
234
235 let success = results.iter().filter(|x| x.is_success()).count();
236 let ts = TestSuiteBuilder::new("Sync Job")
237 .add_testcases(results)
238 .build();
239 println!(
240 "DONE [{2}]: {0}/{1}",
241 success,
242 total,
243 OffsetDateTime::now_utc()
244 );
245 ts
246}
247
248pub struct MirrorOptions {
249 pub mirror_dir: PathBuf,
250 pub dry_run: bool,
251 pub metrics_file: Option<PathBuf>,
252 pub junit_file: Option<PathBuf>,
253 pub worker_count: usize,
254 pub git_executable: String,
255 pub refspec: Option<Vec<String>>,
256 pub remove_workrepo: bool,
257 pub fail_on_sync_error: bool,
258 pub mirror_lfs: bool,
259}
260
261pub fn do_mirror(provider: Box<dyn Provider>, opts: &MirrorOptions) -> Result<()> {
262 let start_time = register_gauge_vec!(
263 "git_mirror_start_time",
264 "Start time of the sync as unix timestamp",
265 &["mirror"]
266 )
267 .unwrap();
268 let end_time = register_gauge_vec!(
269 "git_mirror_end_time",
270 "End time of the sync as unix timestamp",
271 &["mirror"]
272 )
273 .unwrap();
274
275 trace!("Create mirror directory at {:?}", opts.mirror_dir);
277 fs::create_dir_all(&opts.mirror_dir).map_err(|e| {
278 GitMirrorError::GenericError(format!(
279 "Unable to create mirror dir: {:?} ({})",
280 &opts.mirror_dir, e
281 ))
282 })?;
283
284 let lockfile_path = opts.mirror_dir.join("git-mirror.lock");
286 let lockfile = fs::File::create(&lockfile_path).map_err(|e| {
287 GitMirrorError::GenericError(format!(
288 "Unable to open lockfile: {:?} ({})",
289 &lockfile_path, e
290 ))
291 })?;
292
293 lockfile.try_lock_exclusive().map_err(|e| {
294 GitMirrorError::GenericError(format!(
295 "Another instance is already running against the same mirror directory: {:?} ({})",
296 &opts.mirror_dir, e
297 ))
298 })?;
299
300 trace!("Aquired lockfile: {:?}", &lockfile);
301
302 let v = provider.get_mirror_repos().map_err(|e| -> GitMirrorError {
304 GitMirrorError::GenericError(format!("Unable to get mirror repos ({e})"))
305 })?;
306
307 start_time
308 .with_label_values(&[&provider.get_label()])
309 .set(OffsetDateTime::now_utc().unix_timestamp() as f64);
310
311 let ts = run_sync_task(&v, &provider.get_label(), opts);
312
313 end_time
314 .with_label_values(&[&provider.get_label()])
315 .set(OffsetDateTime::now_utc().unix_timestamp() as f64);
316
317 match opts.metrics_file {
318 Some(ref f) => write_metrics(f),
319 None => trace!("Skipping metrics file creation"),
320 };
321
322 let error_count = ts.errors() + ts.failures();
324
325 match opts.junit_file {
326 Some(ref f) => write_junit_report(f, ts),
327 None => trace!("Skipping junit report"),
328 }
329
330 if opts.fail_on_sync_error && error_count > 0 {
331 Err(GitMirrorError::SyncError(error_count))
332 } else {
333 Ok(())
334 }
335}
336
337fn write_metrics(f: &Path) {
338 let mut file = File::create(f).unwrap();
339 let encoder = TextEncoder::new();
340 let metric_familys = prometheus::gather();
341 encoder.encode(&metric_familys, &mut file).unwrap();
342}
343
344fn write_junit_report(f: &Path, ts: TestSuite) {
345 let report = ReportBuilder::default().add_testsuite(ts).build();
346 let mut file = File::create(f).unwrap();
347 report.write_xml(&mut file).unwrap();
348}