workerlib/
fetch.rs

1//!
2//! Fetch git sources into a workspace
3//!
4
5use crate::execute::{execute, Command, CommandBuilder, Error as ExecutionError};
6use crate::redact::redact;
7use crate::remote::Ref;
8use git2::{Error as GitError, FetchOptions, FetchPrune, Repository};
9use std::path::Path;
10
11const ALL_HEADS: &str = "+refs/heads/*:refs/heads/*";
12const ALL_TAGS: &str = "+refs/tags/*:refs/tags/*";
13
14/// Fetch all refs from the an upstream source into a file system directory
15struct FetchCommand<'a> {
16    pub url: &'a url::Url,
17
18    // Path to the global git configuration file
19    pub config_path: &'a Path,
20    pub ref_names: Option<&'a [Ref]>,
21}
22
23impl CommandBuilder for FetchCommand<'_> {
24    fn build(&self, current_dir: &Path) -> Command {
25        let mut cmd = Command::new("git");
26        cmd.current_dir(current_dir);
27        let mut args = vec![
28            String::from("--bare"),
29            String::from("fetch"),
30            String::from("--no-tags"),
31            // String::from("--prune"),
32            self.url.to_string(),
33        ];
34        cmd.envs([(
35            crate::git_config::GIT_CONFIG_GLOBAL,
36            self.config_path.to_string_lossy().as_ref(),
37        )]);
38
39        if let Some(ref_names) = self.ref_names {
40            args.extend(ref_names.iter().map(|remote_ref| remote_ref.git_ref_spec()));
41        } else {
42            args.extend(vec![String::from(ALL_HEADS), String::from(ALL_TAGS)])
43        }
44
45        cmd.args(args);
46        cmd
47    }
48}
49
50/// An error occurred while fetching from the upstream repository
51#[derive(thiserror::Error, Debug)]
52pub enum Error {
53    /// Error fetching with git binary
54    #[error("Error fetching with the Git binary: {0}")]
55    Command(#[from] ExecutionError),
56
57    /// Error fetching with libgit2
58    #[error("Error fetching with Libgit2: {0}")]
59    Git(#[from] GitError),
60}
61
62/// Mirrors a git repo by pulling down the upstream. Depending on the
63/// configuration it will use libgit2 or the git binary
64pub struct Fetch<'a> {
65    /// The repository on disk to fetch into
66    pub git_repo: &'a crate::workspace::Workspace,
67    /// URL of the server being fetched from, must be a git compliant URL
68    pub target_url: &'a url::Url,
69    /// Whether to use git binary or libgit2 as the engine for fetch operations
70    pub use_git_binary: Option<bool>,
71    /// The path to Lorry's git configuration
72    pub git_config_path: &'a Path,
73    /// Refs to pull from
74    pub refs: Option<&'a [Ref]>,
75}
76
77impl Fetch<'_> {
78    async fn fetch_with_git(&self) -> Result<(), Error> {
79        tracing::debug!("running FetchCommand command");
80        execute(
81            &FetchCommand {
82                url: self.target_url,
83                config_path: self.git_config_path,
84                ref_names: self.refs,
85            },
86            &self.git_repo.repository_path(),
87        )
88        .await?;
89        Ok(())
90    }
91
92    async fn fetch_with_libgit2(&self) -> Result<(), Error> {
93        let git_repo_thread = self
94            .git_repo
95            .repository_path()
96            .to_string_lossy()
97            .to_string();
98        let upstream_url_thread = self.target_url.to_string();
99        let ref_names = if let Some(ref_names) = self.refs {
100            ref_names
101                .iter()
102                .map(|ref_name| ref_name.git_ref_spec())
103                .collect()
104        } else {
105            vec![String::from(ALL_HEADS), String::from(ALL_TAGS)]
106        };
107        let handle = tokio::task::spawn_blocking(move || {
108            let repository = Repository::open(git_repo_thread).unwrap();
109            let mut remote = repository.remote_anonymous(&upstream_url_thread).unwrap();
110            remote.fetch(
111                ref_names.as_slice(),
112                Some(
113                    FetchOptions::new()
114                        .prune(FetchPrune::On)
115                        .custom_headers(&[crate::LORRY_VERSION_HEADER]),
116                ),
117                None,
118            )
119        });
120        handle.await.unwrap()?;
121        Ok(())
122    }
123
124    /// Fetch refs and git objects from a target repository
125    ///
126    /// Switches the engine used for fetch operations depending on arguments.
127    /// Git binary | Libgit2
128    pub async fn fetch(&self) -> Result<(), Error> {
129        if self
130            .use_git_binary
131            .is_some_and(|use_git_binary| use_git_binary)
132        {
133            tracing::info!(
134                "fetching repository {} with the git binary into {}",
135                redact(&self.target_url),
136                self.git_repo.repository_path().to_string_lossy(),
137            );
138            self.fetch_with_git().await
139        } else {
140            tracing::info!(
141                "fetching repository {} with libgit2 into {}",
142                redact(&self.target_url),
143                self.git_repo.repository_path().to_string_lossy(),
144            );
145            self.fetch_with_libgit2().await
146        }
147    }
148}
149
150#[cfg(test)]
151mod test {
152    use super::*;
153    use crate::git_config::Config;
154    use crate::test_server::{spawn_test_server, TestBuilder};
155
156    #[tokio::test]
157    async fn test_fetch_git_binary() {
158        let test = TestBuilder::default()
159            .git_config()
160            .test_repo("hello.git")
161            .workspace("test_fetch");
162        let test_repo = test.test_repo.unwrap();
163        let git_config_path = test.git_config.unwrap();
164        let repos_dir = test.dir.join("repos");
165        let address = spawn_test_server(repos_dir.as_path(), &[test_repo.clone()])
166            .await
167            .unwrap();
168        let fetch = Fetch {
169            git_repo: &test.workspace.unwrap(),
170            target_url: &test_repo.address(&address),
171            use_git_binary: Some(true),
172            git_config_path: git_config_path.as_path(),
173            refs: None,
174        };
175        fetch.fetch().await.unwrap();
176    }
177
178    #[tokio::test]
179    async fn test_fetch_libgit2() {
180        let test = TestBuilder::default()
181            .test_repo("hello.git")
182            .workspace("test_fetch");
183        let test_repo = test.test_repo.unwrap();
184        let git_config_path = test.dir.join("gitconfig");
185        let git_config = Config(git_config_path.to_path_buf());
186        git_config
187            .setup(
188                ("Lorry", "hello@example.org"),
189                1,
190                false,
191                Some("HTTP/2"),
192                Path::new("/dev/null"),
193                None,
194            )
195            .unwrap();
196        let repos_dir = test.dir.join("repos");
197        let address = spawn_test_server(repos_dir.as_path(), &[test_repo.clone()])
198            .await
199            .unwrap();
200        let fetch = Fetch {
201            git_repo: &test.workspace.unwrap(),
202            target_url: &test_repo.address(&address),
203            use_git_binary: Some(false),
204            git_config_path: git_config_path.as_path(),
205            refs: None,
206        };
207        fetch.fetch().await.unwrap();
208    }
209
210    #[tokio::test]
211    async fn fetch_git_with_refs() {
212        let test = TestBuilder::default()
213            .test_repo("hello.git")
214            .workspace("test_fetch");
215        let test_repo = test.test_repo.unwrap();
216        let git_config_path = test.dir.join("gitconfig");
217        let git_config = Config(git_config_path.to_path_buf());
218        git_config
219            .setup(
220                ("Lorry", "hello@example.org"),
221                1,
222                false,
223                Some("HTTP/2"),
224                Path::new("/dev/null"),
225                None,
226            )
227            .unwrap();
228        let repos_dir = test.dir.join("repos");
229        let address = spawn_test_server(repos_dir.as_path(), &[test_repo.clone()])
230            .await
231            .unwrap();
232        let refs = vec![Ref(String::from("refs/heads/main"))];
233        let fetch = Fetch {
234            git_repo: &test.workspace.unwrap(),
235            target_url: &test_repo.address(&address),
236            use_git_binary: Some(false),
237            git_config_path: git_config_path.as_path(),
238            refs: Some(refs.as_slice()),
239        };
240        fetch.fetch().await.unwrap();
241    }
242}