workerlib/
lib.rs

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