workerlib/
lib.rs

1//!
2//! This is the workerlib crate
3//!
4
5#![deny(missing_docs)]
6
7mod arguments;
8mod download;
9pub mod fetch;
10mod get_head;
11mod importer;
12pub mod local;
13mod ls_remote;
14mod push;
15mod raw_files;
16pub mod redact;
17mod remote;
18
19#[cfg(test)]
20mod test_server;
21
22pub mod execute;
23pub mod git_config;
24pub mod git_fsck;
25pub mod git_gc;
26pub mod lorry_specs;
27pub mod workspace;
28
29use crate::execute::{execute, Error as ExecutionError};
30use git2::{Error as Libgit2Error, Repository};
31use git_config::DEFAULT_GIT_BRANCH;
32use glob::Pattern;
33use lorry_specs::SingleLorry;
34use remote::Ref;
35use remote::Remote;
36use std::collections::BTreeMap;
37use thiserror::Error;
38use url::Url;
39
40pub use arguments::Arguments;
41pub use lorry_specs::extract_lorry_specs;
42pub use lorry_specs::LorrySpec;
43
44/// Extra header exposed on Git fetch uperations to upstream servers and
45/// push operations on downstream.
46pub const LORRY_VERSION_HEADER: &str = concat!("Lorry-Version: ", env!("CARGO_PKG_VERSION"));
47
48/// Default branch if HEAD is missing or unresolvable from the target repository.
49pub const DEFAULT_BRANCH_NAME: &str = "main";
50
51/// Default ref is missing from the target repository.
52pub const DEFAULT_REF_NAME: &str = "refs/heads/main";
53
54/// Non-fatal error encountered when running a Lorry.
55#[derive(Clone, Debug)]
56pub enum Warning {
57    /// Anytime a refspec is specified but it doesn't match any refs in the
58    /// upstream repository.
59    NonMatchingRefSpecs {
60        /// Pattern from the configuration that did not match any refs
61        pattern: String,
62    },
63    /// If only tags have been pulled from upstream, the repo will look broken
64    /// so we display a warning.
65    OnlyRefTags,
66}
67// TODO: Perhaps add a feature to the web ui to suppress warnings.
68
69impl Warning {
70    /// Return a nicely formatted name
71    pub fn name(&self) -> String {
72        match self {
73            Warning::NonMatchingRefSpecs { pattern: _ } => String::from("NonMatchingRefSpec"),
74            Warning::OnlyRefTags => String::from("OnlyRefTags"),
75        }
76    }
77
78    /// Return the body of the error message
79    pub fn message(&self) -> String {
80        match self {
81            Warning::NonMatchingRefSpecs { pattern } => pattern.clone(),
82            Warning::OnlyRefTags => String::from(
83                "Only tags have been pulled from upstream, repository will appear empty",
84            ),
85        }
86    }
87}
88
89/// Results of a parsing refs from porcelain git push output
90///
91/// Example command:
92/// ```txt
93/// git push --porcelain origin 'refs/heads/*' 2>/dev/null`
94/// ```
95#[derive(Clone, Debug, Default)]
96pub struct PushRefs(pub Vec<(String, String)>, pub Vec<(String, String)>);
97
98/// Contains the status of a single mirror operation
99#[derive(Default)]
100pub struct MirrorStatus {
101    /// Refs we have attempted to push to upstream.
102    pub push_refs: PushRefs,
103    /// Warnings for display on Lorry's Web UI e.g. NoMatchingRefSpecs.
104    pub warnings: Vec<Warning>,
105}
106
107/// Top-level error type containing all possible errors during a mirroring operation.
108#[derive(Error, Debug)]
109pub enum Error {
110    /// The upstream was a git repo. There was an error pulling from the remote.
111    #[error("Failed to fetch upstream: {0}")]
112    Fetch(#[from] fetch::Error),
113
114    /// Indicates that the job exceeded the time it was allocated in the
115    /// Lorry configuration
116    #[error("job exceeded the allocated timeout: {seconds}")]
117    TimeoutExceeded {
118        /// Display elapsed time
119        seconds: u64,
120    },
121
122    /// An error occurred that originated in the workspace module.
123    #[error("Workspace Error: {0}")]
124    WorkspaceError(#[from] workspace::Error),
125
126    /// An error occured when importing raw files into a working directory.
127    #[error("LFS Importer Error: {0}")]
128    LFSImporter(#[from] importer::Error),
129
130    /// Generic command execution error not specific to any particular module.
131    #[error("Command Execution Error: {0}")]
132    Command(#[from] ExecutionError),
133
134    /// Generic IO error
135    #[error("IO Error")]
136    Io {
137        /// Display command
138        command: String,
139    },
140    /// Tried to access a repository on the local filesystem as the downstream,
141    /// but the given path is not a valid one.
142    #[error(
143        "Downstream is specified as a path on the local filesystem, but the path is malformed."
144    )]
145    InvalidPath(String),
146
147    /// Problem parsing a downstream git url
148    #[error("Failed to construct path to downstream git repo")]
149    ParseError(#[from] url::ParseError),
150
151    /// Generic Libgit2 failure not specific to any particular module.
152    #[error("Internal git failure: {0}")]
153    Libgit2Error(#[from] Libgit2Error),
154
155    /// Indicates that 100% of the desired refs failed to push to the
156    /// downstream.
157    #[error("All ({n_attempted}) refspecs failed")]
158    AllRefspecsFailed {
159        /// Display refs
160        refs: PushRefs,
161        /// Number of attempted refs
162        n_attempted: i64,
163    },
164
165    /// Indicates some but not all of the desired refspecs were not able to
166    /// be pushed downstream for some reason.
167    #[error("{n_failed} refspecs failed")]
168    SomeRefspecsFailed {
169        /// Refs pushed to upstream
170        refs: PushRefs,
171        /// Number of failed refs
172        n_failed: i64,
173    },
174
175    /// Indicates a refspec was malformed
176    #[error("Invalid Glob: {pattern} - {error}")]
177    InvalidGlobPattern {
178        /// Invalid pattern
179        pattern: String,
180        /// Reason for refspec malformity
181        error: String,
182    },
183
184    /// An error related to raw-file management occurred.
185    #[error("Raw File Related Error: {0}")]
186    RawFiles(#[from] raw_files::Error),
187
188    /// Indicates none of the refs in the mirror configuration matches those
189    /// that were available in the repository.
190    #[error("No Matching Refspecs")]
191    NoMatchingRefspecs,
192
193    /// Indicates that a push command failed and we could not understand the
194    /// the reason why. This may be due to a downstream server error.
195    #[error("Cannot parse push output: {0}")]
196    CannotParsePushOutput(String),
197
198    /// Indicates that the server requires Sha256sum values for all aw-files
199    /// but the file did not have one specified.
200    #[error("Sha256sums are missing: \n{0}")]
201    Sha256sumsNotSpecified(String),
202
203    /// Indicates the ignore pattern was not a valid regular expression
204    #[error("Invalid ignore pattern: {0}")]
205    InvalidIgnorePattern(String),
206
207    /// Raw file mirrors are not supported
208    #[error("Raw file mirror skipped: {0}")]
209    RawFileNotSupported(String),
210
211    /// An operation has failed on a remote
212    #[error("Remote failed: {0}")]
213    Remote(#[from] remote::Error),
214}
215
216impl Error {
217    /// Return the underlying status code from the error if the error is the
218    /// result of an command execution failure otherwise return nothing.
219    #[allow(clippy::collapsible_match)]
220    pub fn status(&self) -> Option<i32> {
221        match self {
222            Error::Fetch(e) => match e {
223                fetch::Error::Command(e) => e.status(),
224                _ => None,
225            },
226            Error::Command(e) => match e {
227                ExecutionError::IO {
228                    command: _,
229                    source: _,
230                } => None,
231                ExecutionError::CommandError {
232                    command: _,
233                    status,
234                    stderr: _,
235                    stdout: _,
236                } => Some(status.code().unwrap_or(-1)),
237            },
238            _ => None,
239        }
240    }
241}
242
243/// Fetch [PushRefs] from the result of a git push operation
244///
245/// If there are any failed refs, an error is recorded - otherwise return
246/// the push refs from the result of parsing stdout.
247fn get_refs(n_refs: usize, stdout: &str) -> Result<PushRefs, Error> {
248    match crate::push::Push::parse_output(stdout) {
249        Ok(results) => {
250            if results.1.len() == n_refs {
251                return Err(Error::AllRefspecsFailed {
252                    refs: results.clone(),
253                    n_attempted: n_refs as i64,
254                });
255            }
256            if !results.1.is_empty() {
257                Err(Error::SomeRefspecsFailed {
258                    refs: results.clone(),
259                    n_failed: results.1.len() as i64,
260                })
261            } else {
262                Ok(results)
263            }
264        }
265        Err(message) => {
266            tracing::warn!("Failed to parse git push output:\n{}", message);
267            Err(Error::CannotParsePushOutput(message))
268        }
269    }
270}
271
272/// Match refs ignoring refs/{heads,tags}/ unless they're explicitly specified
273fn match_ref(pattern: &Pattern, input: &str) -> bool {
274    let pattern_str = pattern.as_str();
275    if pattern_str.starts_with("refs/heads/") || pattern_str.starts_with("refs/tags/") {
276        pattern.matches(input)
277    } else {
278        pattern.matches(
279            input
280                .trim_start_matches("refs/heads/")
281                .trim_start_matches("refs/tags/"),
282        )
283    }
284}
285
286/// Given all of the refs return a vec of those with some matches and a vec of
287/// any patterns that didn't result in any matches at all.
288///
289/// # Example
290/// ```yaml
291///
292/// ref-patterns:
293/// - 'ma*'
294/// - 'refs/tags/*'
295/// ignore-patterns:
296/// - 'refs/tags/*rc*
297/// ```
298///
299/// -> Parses the ignore-patterns and ref-patterns into globs
300/// * Matches refs against ignore patterns, removing all matching refs
301/// * Matches valid against ref-patterns and returns a vector of matching refs.
302fn parse_refs(
303    refs: &[Ref],
304    ref_patterns: Option<&[String]>,
305    ignore_patterns: Option<&[String]>,
306) -> Result<(Vec<Ref>, Vec<Ref>), Error> {
307    let ignore_globs = ignore_patterns
308        .as_ref()
309        .map(|ignore_patterns| {
310            ignore_patterns
311                .iter()
312                .try_fold(Vec::new(), |mut accm, pattern| {
313                    let glob_pattern =
314                        Pattern::new(pattern).map_err(|e| Error::InvalidGlobPattern {
315                            pattern: pattern.clone(),
316                            error: e.to_string(),
317                        })?;
318                    accm.push(glob_pattern);
319                    Ok::<Vec<Pattern>, Error>(accm)
320                })
321        })
322        .transpose()?;
323
324    let refs: Vec<Ref> = refs
325        .iter()
326        .filter_map(|ref_name| {
327            if ignore_globs.as_ref().is_some_and(|ignore_globs| {
328                ignore_globs
329                    .iter()
330                    .any(|pattern| match_ref(pattern, &ref_name.0))
331            }) {
332                None
333            } else {
334                Some(ref_name.clone())
335            }
336        })
337        .collect();
338
339    if let Some(ref_specs) = ref_patterns {
340        let mut patterns_with_matches: BTreeMap<String, bool> = BTreeMap::new();
341        let globs: Vec<Pattern> = ref_specs.iter().try_fold(Vec::new(), |mut accm, pattern| {
342            match Pattern::new(pattern) {
343                Ok(regex) => {
344                    patterns_with_matches.insert(pattern.clone(), false);
345                    accm.push(regex);
346                    Ok(accm)
347                }
348                Err(e) => Err(Error::InvalidGlobPattern {
349                    pattern: pattern.clone(),
350                    error: e.to_string(),
351                }),
352            }
353        })?;
354
355        Ok((
356            refs.iter().fold(Vec::new(), |mut accm, ref_name| {
357                if let Some(matching_glob) = globs.iter().find_map(|glob_pattern| {
358                    if match_ref(glob_pattern, &ref_name.0) {
359                        Some(glob_pattern.to_string())
360                    } else {
361                        None
362                    }
363                }) {
364                    patterns_with_matches.insert(matching_glob, true);
365                    accm.push(ref_name.clone());
366                };
367                accm
368            }),
369            patterns_with_matches
370                .iter()
371                .filter_map(|(pattern, had_match)| {
372                    if !had_match {
373                        Some(Ref(pattern.clone()))
374                    } else {
375                        None
376                    }
377                })
378                .collect(),
379        ))
380    } else {
381        Ok((refs.to_vec(), vec![]))
382    }
383}
384
385/// Push a git based mirror to the downstream server
386///
387/// * If the downstream host is local, the default branch is set to match the
388///   HEAD of the associated workspace.
389///
390/// * refs on `repository` are parsed against the Lorry spec - valid refs are
391///   then pushed to the Lorry mirror server.
392///
393/// TODO: Add option to use libgit2 for push in addition to pull on git mirrors.
394#[allow(deprecated)]
395async fn push_to_mirror_server(
396    lorry_spec: &SingleLorry,
397    lorry_name: &str,
398    url_to_push_to: &Url,
399    active_repo: &workspace::Workspace,
400    arguments: &arguments::Arguments,
401) -> Result<MirrorStatus, Error> {
402    tracing::debug!("Pushing {} to mirror at {:?}", lorry_name, &url_to_push_to);
403
404    // The workspace on disk that Lorry stores the result of fetch operation to
405    let repository = Repository::open(active_repo.repository_path())?;
406    // If we are pushing to a local downstream
407    if url_to_push_to.scheme() == "file" {
408        // This repo is being pushed to the local filesystem
409        let file = url_to_push_to
410            .to_file_path()
411            .map_err(|_| Error::InvalidPath(url_to_push_to.to_string()))?;
412
413        let local_repo = Repository::open(file)?;
414        let head = repository.head()?;
415        // Set remote HEAD on local mirror to match workspace.
416        local_repo.set_head(
417            head.name()
418                .unwrap_or(&format!("refs/heads/{DEFAULT_GIT_BRANCH}")),
419        )?;
420    }
421    let ref_names = repository
422        .references()?
423        .try_fold(Vec::new(), |mut accm, reference| {
424            let ref_name = match reference {
425                Ok(ref_name) => {
426                    let name = ref_name.name().unwrap();
427                    name.to_string()
428                }
429                Err(err) => return Err(err),
430            };
431            accm.push(Ref(ref_name));
432            Ok(accm)
433        })?;
434
435    let (refs, missing) = parse_refs(
436        ref_names.as_slice(),
437        lorry_spec.ref_patterns.as_deref(),
438        lorry_spec.ignore_patterns.as_deref(),
439    )?;
440    tracing::info!("pushing {} refs", refs.len());
441
442    if refs.is_empty() {
443        return Err(Error::NoMatchingRefspecs);
444    }
445
446    let repository_path = active_repo.repository_path();
447    let push = &crate::push::Push {
448        url: url_to_push_to,
449        ref_names: refs.iter().map(|s| s.0.as_str()).collect(),
450        config_path: &arguments.git_config_path,
451    };
452
453    tracing::debug!("running Push command");
454    let push_refs = match execute(push, repository_path.as_path()).await {
455        Ok((stdout, _)) => get_refs(refs.len(), &stdout),
456        Err(err) => match err {
457            ExecutionError::IO { command, source } => {
458                tracing::warn!("Command failed to spawn: {:?}", source.to_string());
459                Err(Error::Io {
460                    command: command.clone(),
461                })
462            }
463            ExecutionError::CommandError {
464                command: _,
465                status: _,
466                stderr: _,
467                stdout,
468            } => get_refs(refs.len(), &stdout),
469        },
470    }?;
471
472    Ok(MirrorStatus {
473        push_refs,
474        warnings: {
475            let mut warnings = vec![];
476            if !refs.iter().any(|refs| refs.0.contains("refs/heads")) {
477                warnings.push(Warning::OnlyRefTags)
478            }
479            warnings.extend(
480                missing
481                    .iter()
482                    .map(|pattern| Warning::NonMatchingRefSpecs {
483                        pattern: pattern.0.to_string(),
484                    })
485                    .collect::<Vec<Warning>>(),
486            );
487            warnings
488        },
489    })
490}
491
492/// Attempt to mirror a repository from an upstream host into the configured
493/// downstream server.
494///
495/// Rough outline of the order of operations is:
496///
497/// * Attempt to fetch repository data from the downstream into the working
498///   directory of the local mirror.
499///
500/// * Run git remote-ls on upstream to find a list of refs in the upstream.
501///
502/// * Fetch the upstream repository on top of the working directory of the
503///   local mirror applying new updates.
504///
505/// * Push the local mirror back into the downstream updating it with new refs.
506///
507/// Fetching the downstream repository initially ensures that Lorry can work
508/// in a stateless environment and will consider the downstream mirror the
509/// source of truth.
510///
511/// This workflow applies to both LFS and normal Git mirrors. In the case of
512/// LFS the raw files are also downloaded into the working directory prior to
513/// running download operations.
514///
515/// In the event that the downstream repository becomes corrupted in someway
516/// the procedure is to delete and reinitialize an empty repository in which
517/// case the upstream mirror will be re-imported from scratch.
518///
519/// TODO: This code can be factored out better
520pub async fn try_mirror(
521    lorry_details: &LorrySpec,
522    lorry_name: &str,
523    downstream_url: &url::Url,
524    workspace: &workspace::Workspace,
525    arguments: &Arguments,
526) -> Result<MirrorStatus, Error> {
527    match lorry_details {
528        LorrySpec::Git(single_lorry) => {
529            tracing::info!("Ensuring local mirror is consistent with downstream");
530            crate::fetch::Fetch {
531                git_repo: workspace,
532                target_url: downstream_url,
533                use_git_binary: arguments.use_git_binary,
534                git_config_path: arguments.git_config_path.as_path(),
535                // all refs considered in downstream
536                refs: None,
537            }
538            .fetch()
539            .await?;
540            let target_url = url::Url::parse(single_lorry.url.as_str()).unwrap(); // FIXME
541            let remote = Remote {
542                workspace,
543                use_git_binary: arguments.use_git_binary,
544                git_config_path: arguments.git_config_path.as_path(),
545            };
546            tracing::info!("Matching ref-patterns with refs on upstream");
547            let refs = remote.list(&target_url).await?;
548
549            tracing::info!("Ensuring local mirror default branch is consistent with upstream");
550            remote.set_head(&target_url).await?;
551
552            tracing::info!("Fetching upstream repository into local mirror");
553            // match upstream's remote ls response but do not error yet since
554            // although the Lorry configuration may ask for refs which are
555            // missing, we still want to pull and push what is available into
556            // our downstream. push_to_mirror_server will flag any errors.
557            let (matches, _) = parse_refs(
558                refs.0.as_slice(),
559                single_lorry.ref_patterns.as_deref(),
560                single_lorry.ignore_patterns.as_deref(),
561            )?;
562            tracing::info!("Upstream contains {} matching refs", matches.len());
563            crate::fetch::Fetch {
564                git_repo: workspace,
565                target_url: &target_url,
566                use_git_binary: arguments.use_git_binary,
567                git_config_path: arguments.git_config_path.as_path(),
568                refs: Some(matches.as_slice()),
569            }
570            .fetch()
571            .await?;
572            push_to_mirror_server(
573                single_lorry,
574                lorry_name,
575                downstream_url,
576                workspace,
577                arguments,
578            )
579            .await
580        }
581        LorrySpec::RawFiles(raw_files) => {
582            if matches!(downstream_url.scheme(), "file") {
583                tracing::warn!("Raw file mirrors are not supported for local downstream");
584                return Err(Error::RawFileNotSupported(lorry_name.to_string()));
585            }
586            // check for missing sha256sums if configuration disallows them
587            if arguments.sha256sum_required {
588                let missing_sha256sums = raw_files.missing_sha256sums();
589                if !missing_sha256sums.is_empty() {
590                    let mut message = String::default();
591                    missing_sha256sums
592                        .iter()
593                        .for_each(|url| message.push_str(&format!("{url}\n")));
594                    return Err(Error::Sha256sumsNotSpecified(message));
595                }
596            }
597            tracing::info!("Fetching raw files from downstream to ensure consistency");
598            let mut lfs_url = downstream_url.clone();
599            lfs_url.set_path(&format!("{}/info/lfs", lfs_url.path()));
600            tracing::debug!("running FetchDownstreamRawFiles command");
601            let fetch_err = execute(
602                &raw_files::FetchDownstreamRawFiles {
603                    url: downstream_url,
604                    lfs_url: &lfs_url,
605                    worktree: workspace.lfs_data_path().as_path(),
606                    config_path: arguments.git_config_path.as_path(),
607                },
608                &workspace.repository_path(),
609            )
610            .await;
611            // TODO: I would prefer to explicitly detect if the repository
612            // exists or not but the code calling the Gitlab API needs work.
613            // BUG: Be aware that if the downstream fails for some other reason
614            // like the server being down that this will create a new
615            // repository and push its contents up causing a rejection error.
616            // If this happens then you need to manually delete the mirror
617            // directory and on the next run it will properly import the
618            // downstream sources.
619            if let Err(err) = fetch_err {
620                tracing::warn!("Fetch failed but might not be an error: {}", err);
621            } else {
622                tracing::info!("Local mirror is consistent with downstream");
623            }
624            let importer = importer::Importer(raw_files.clone());
625            let helper = raw_files::Helper(workspace.clone());
626
627            // Initialize the LFS repository if it hasn't been already creating
628            // the first commit which enables tracking of everything within.
629            helper
630                .initial_commit_if_missing(arguments.git_config_path.as_path())
631                .await?;
632
633            // Download any new raw files
634            let modified = importer
635                .ensure(&workspace.lfs_data_path(), arguments)
636                .await?;
637            if modified {
638                // Import new data that was just downloaded
639                helper
640                    .import_data(arguments.git_config_path.as_path())
641                    .await?;
642            } else {
643                tracing::info!("No new files were added or removed, nothing to do")
644            };
645
646            tracing::info!("Synchronizing local raw-file mirror to downstream");
647            let mut lfs_url = downstream_url.clone();
648            lfs_url.set_path(&format!("{}/info/lfs", lfs_url.path()));
649            tracing::debug!("running PushRawFiles command");
650            execute(
651                &raw_files::PushRawFiles {
652                    url: downstream_url,
653                    lfs_url: &lfs_url,
654                    worktree: workspace.lfs_data_path().as_path(),
655                    config_path: arguments.git_config_path.as_path(),
656                },
657                workspace.repository_path().as_path(),
658            )
659            .await?;
660            // LFS breaks porcelain git output so we just fake it since we only
661            // supporting syncing to a single branch anyway.
662            Ok(MirrorStatus {
663                push_refs: PushRefs(
664                    vec![(
665                        DEFAULT_BRANCH_NAME.to_string(),
666                        String::from("raw files pushed successfully"),
667                    )],
668                    vec![],
669                ),
670                warnings: vec![],
671            })
672        }
673    }
674}
675
676#[cfg(test)]
677mod test {
678    use super::*;
679
680    use git2::Repository;
681
682    use crate::local::LocalRepositoryBuilder;
683    use crate::test_server::{spawn_test_server, TestBuilder, TestRepo};
684
685    #[test]
686    fn test_parse_refs() {
687        let (refs, missing) = parse_refs(
688            &[
689                Ref::from("refs/heads/master"),
690                Ref::from("refs/tags/v1.0.0"),
691                Ref::from("refs/tags/v1.0.1"),
692                Ref::from("refs/tags/v1.0.2-rc1"),
693                Ref::from("some-random-string"),
694            ],
695            Some(&[
696                String::from("refs/heads/ma*"),
697                String::from("v1.0.1"),
698                String::from("refs/tags/v*"),
699                String::from("notgonnamatch"),
700            ]),
701            // ignore RC releases
702            Some(&[
703                String::from("refs/tags/*-rc*"),
704                String::from("*-rc*"),
705                String::from("*ef*"), // Will not match refs/...
706            ]),
707        )
708        .unwrap();
709        assert!(refs.iter().any(|key| *key == "refs/heads/master".into()));
710        assert!(refs.iter().any(|key| *key == "refs/tags/v1.0.0".into()));
711        assert!(refs.iter().any(|key| *key == "refs/tags/v1.0.1".into()));
712        assert!(!refs.iter().any(|key| *key == "some-random-string".into()));
713        assert!(!refs.iter().any(|key| *key == "refs/tags/v1.0.2-rc1".into()));
714        assert!(*missing.first().unwrap() == Ref::from("notgonnamatch"));
715        assert!(missing.len() == 1);
716    }
717
718    #[test]
719    fn test_parse_refs_conflict() {
720        let (refs, missing) = parse_refs(
721            &[
722                Ref::from("refs/heads/master"),
723                Ref::from("refs/heads/v1.0.0"),
724                Ref::from("refs/tags/v1.0.0"),
725            ],
726            Some(&[String::from("v1.0.0")]),
727            Some(&[String::from("master")]), // pointless
728        )
729        .unwrap();
730        assert!(refs.len() == 2);
731        assert!(refs.iter().any(|key| *key == "refs/tags/v1.0.0".into()));
732        assert!(refs.iter().any(|key| *key == "refs/heads/v1.0.0".into()));
733        assert!(missing.is_empty());
734    }
735
736    #[tokio::test]
737    async fn test_try_mirror() {
738        let upstream = TestBuilder::default().test_repo("hello.git");
739        let upstream_repo = upstream.test_repo.unwrap();
740
741        let downstream = TestBuilder::default().git_config().workspace("test-repo");
742        let downstream_repo = TestRepo((String::from("hello.git"), vec![]));
743        let repos_upstream_dir = upstream.dir.join("repos_upstream");
744        let repos_downstream_dir = downstream.dir.join("repos_downstream");
745        // Upstream server
746        let upstream_address =
747            spawn_test_server(repos_upstream_dir.as_path(), &[upstream_repo.clone()])
748                .await
749                .unwrap();
750        // Downstream server
751        let downstream_address =
752            spawn_test_server(repos_downstream_dir.as_path(), &[downstream_repo.clone()])
753                .await
754                .unwrap();
755
756        try_mirror(
757            &LorrySpec::Git(SingleLorry {
758                url: upstream_repo.address(&upstream_address).to_string(),
759                ..Default::default()
760            }),
761            "test_lorry",
762            &downstream_repo.address(&downstream_address),
763            &downstream.workspace.unwrap(),
764            &Arguments {
765                working_area: downstream.dir,
766                use_git_binary: Some(true),
767                git_config_path: downstream.git_config.unwrap(),
768                ..Default::default()
769            },
770        )
771        .await
772        .unwrap();
773
774        // ensure that downstream contains the git from the upstream
775        let repository = Repository::open_bare(repos_downstream_dir.join("hello.git")).unwrap();
776        let mut walk = repository.revwalk().unwrap();
777        walk.push_head().unwrap();
778        let last_commit_id = walk.next().unwrap().unwrap();
779        let last_commit = repository.find_commit(last_commit_id).unwrap();
780        let last_commit_message = last_commit.message().unwrap();
781        assert!(last_commit_message == "Test Commit: 1/1")
782    }
783
784    #[tokio::test]
785    async fn test_try_local_mirror() {
786        let upstream = TestBuilder::default().test_repo("hello.git");
787        let upstream_repo = upstream.test_repo.unwrap();
788
789        let downstream = TestBuilder::default().git_config().workspace("test-repo");
790        let downstream_repo = downstream.dir.join("mirrors");
791
792        LocalRepositoryBuilder::new(&downstream_repo)
793            .build("test")
794            .unwrap();
795
796        let repos_upstream_dir = upstream.dir.join("repos_upstream");
797        // Upstream server
798        let upstream_address =
799            spawn_test_server(repos_upstream_dir.as_path(), &[upstream_repo.clone()])
800                .await
801                .unwrap();
802
803        // Expect to mirror ref/heads/<DEFAULT_GIT_BRANCH_NAME>
804        try_mirror(
805            &LorrySpec::Git(SingleLorry {
806                url: upstream_repo.address(&upstream_address).to_string(),
807                ..Default::default()
808            }),
809            "test_lorry",
810            &Url::from_file_path(downstream_repo.join("test/git-repository")).unwrap(),
811            &downstream.workspace.unwrap(),
812            &Arguments {
813                working_area: downstream.dir,
814                use_git_binary: Some(true),
815                git_config_path: downstream.git_config.unwrap(),
816                ..Default::default()
817            },
818        )
819        .await
820        .unwrap();
821    }
822}