1use 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
14struct FetchCommand<'a> {
16 pub url: &'a url::Url,
17
18 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 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
50struct LfsFetchCommand<'a> {
52 pub url: &'a url::Url,
53
54 pub config_path: &'a Path,
56 pub ref_names: &'a [Ref],
57}
58
59impl CommandBuilder for LfsFetchCommand<'_> {
60 fn build(&self, current_dir: &Path) -> Command {
61 let mut cmd = Command::new("git");
62 cmd.current_dir(current_dir);
63 cmd.envs([(
64 crate::git_config::GIT_CONFIG_GLOBAL,
65 self.config_path.to_string_lossy().as_ref(),
66 )]);
67 cmd.args(["lfs", "fetch", self.url.as_str()]);
68 cmd.args(self.ref_names.iter().map(|r| r.0.as_str()));
69 cmd
70 }
71}
72
73#[derive(thiserror::Error, Debug)]
75pub enum Error {
76 #[error("Error fetching with the Git binary: {0}")]
78 Command(#[from] ExecutionError),
79
80 #[error("Error fetching with Libgit2: {0}")]
82 Git(#[from] GitError),
83}
84
85pub struct Fetch<'a> {
88 pub git_repo: &'a crate::workspace::Workspace,
90 pub target_url: &'a url::Url,
92 pub use_git_binary: Option<bool>,
94 pub git_config_path: &'a Path,
96 pub refs: Option<&'a [Ref]>,
98 pub lfs: bool,
100}
101
102impl Fetch<'_> {
103 async fn fetch_with_git(&self) -> Result<(), Error> {
104 tracing::debug!("running FetchCommand command");
105 execute(
106 &FetchCommand {
107 url: self.target_url,
108 config_path: self.git_config_path,
109 ref_names: self.refs,
110 },
111 &self.git_repo.repository_path(),
112 )
113 .await?;
114 Ok(())
115 }
116
117 async fn fetch_with_libgit2(&self) -> Result<(), Error> {
118 let git_repo_thread = self
119 .git_repo
120 .repository_path()
121 .to_string_lossy()
122 .to_string();
123 let upstream_url_thread = self.target_url.to_string();
124 let ref_names = if let Some(ref_names) = self.refs {
125 ref_names
126 .iter()
127 .map(|ref_name| ref_name.git_ref_spec())
128 .collect()
129 } else {
130 vec![String::from(ALL_HEADS), String::from(ALL_TAGS)]
131 };
132 let handle = tokio::task::spawn_blocking(move || {
133 let repository = Repository::open(git_repo_thread).unwrap();
134 let mut remote = repository.remote_anonymous(&upstream_url_thread).unwrap();
135 remote.fetch(
136 ref_names.as_slice(),
137 Some(
138 FetchOptions::new()
139 .prune(FetchPrune::On)
140 .custom_headers(&[crate::LORRY_VERSION_HEADER]),
141 ),
142 None,
143 )
144 });
145 handle.await.unwrap()?;
146 Ok(())
147 }
148
149 async fn fetch_lfs(&self) -> Result<(), Error> {
150 let Some(refs) = self.refs else {
151 return Ok(());
152 };
153 tracing::info!("Fetching LFS objects from {}", redact(&self.target_url),);
154 execute(
155 &LfsFetchCommand {
156 url: self.target_url,
157 config_path: self.git_config_path,
158 ref_names: refs,
159 },
160 &self.git_repo.repository_path(),
161 )
162 .await?;
163 Ok(())
164 }
165
166 pub async fn fetch(&self) -> Result<(), Error> {
173 if self
174 .use_git_binary
175 .is_some_and(|use_git_binary| use_git_binary)
176 {
177 tracing::info!(
178 "fetching repository {} with the git binary into {}",
179 redact(&self.target_url),
180 self.git_repo.repository_path().to_string_lossy(),
181 );
182 self.fetch_with_git().await
183 } else {
184 tracing::info!(
185 "fetching repository {} with libgit2 into {}",
186 redact(&self.target_url),
187 self.git_repo.repository_path().to_string_lossy(),
188 );
189 self.fetch_with_libgit2().await
190 }?;
191
192 if self.lfs {
193 self.fetch_lfs().await?;
194 }
195
196 Ok(())
197 }
198}
199
200#[cfg(test)]
201mod test {
202 use super::*;
203 use crate::git_config::Config;
204 use crate::test_server::{spawn_test_server, TestBuilder};
205
206 #[tokio::test]
207 async fn test_fetch_git_binary() {
208 let test = TestBuilder::default()
209 .git_config()
210 .test_repo("hello.git")
211 .workspace("test_fetch");
212 let test_repo = test.test_repo.unwrap();
213 let git_config_path = test.git_config.unwrap();
214 let repos_dir = test.dir.join("repos");
215 let address = spawn_test_server(repos_dir.as_path(), &[test_repo.clone()])
216 .await
217 .unwrap();
218 let fetch = Fetch {
219 git_repo: &test.workspace.unwrap(),
220 target_url: &test_repo.address(&address),
221 use_git_binary: Some(true),
222 git_config_path: git_config_path.as_path(),
223 refs: None,
224 lfs: false,
225 };
226 fetch.fetch().await.unwrap();
227 }
228
229 #[tokio::test]
230 async fn test_fetch_libgit2() {
231 let test = TestBuilder::default()
232 .test_repo("hello.git")
233 .workspace("test_fetch");
234 let test_repo = test.test_repo.unwrap();
235 let git_config_path = test.dir.join("gitconfig");
236 let git_config = Config(git_config_path.to_path_buf());
237 git_config
238 .setup(
239 ("Lorry", "hello@example.org"),
240 1,
241 false,
242 Some("HTTP/2"),
243 Path::new("/dev/null"),
244 None,
245 )
246 .unwrap();
247 let repos_dir = test.dir.join("repos");
248 let address = spawn_test_server(repos_dir.as_path(), &[test_repo.clone()])
249 .await
250 .unwrap();
251 let fetch = Fetch {
252 git_repo: &test.workspace.unwrap(),
253 target_url: &test_repo.address(&address),
254 use_git_binary: Some(false),
255 git_config_path: git_config_path.as_path(),
256 refs: None,
257 lfs: false,
258 };
259 fetch.fetch().await.unwrap();
260 }
261
262 #[tokio::test]
263 async fn fetch_git_with_refs() {
264 let test = TestBuilder::default()
265 .test_repo("hello.git")
266 .workspace("test_fetch");
267 let test_repo = test.test_repo.unwrap();
268 let git_config_path = test.dir.join("gitconfig");
269 let git_config = Config(git_config_path.to_path_buf());
270 git_config
271 .setup(
272 ("Lorry", "hello@example.org"),
273 1,
274 false,
275 Some("HTTP/2"),
276 Path::new("/dev/null"),
277 None,
278 )
279 .unwrap();
280 let repos_dir = test.dir.join("repos");
281 let address = spawn_test_server(repos_dir.as_path(), &[test_repo.clone()])
282 .await
283 .unwrap();
284 let refs = vec![Ref(String::from("refs/heads/main"))];
285 let fetch = Fetch {
286 git_repo: &test.workspace.unwrap(),
287 target_url: &test_repo.address(&address),
288 use_git_binary: Some(false),
289 git_config_path: git_config_path.as_path(),
290 refs: Some(refs.as_slice()),
291 lfs: false,
292 };
293 fetch.fetch().await.unwrap();
294 }
295}