workerlib/
lib.rs

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