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
50#[derive(thiserror::Error, Debug)]
52pub enum Error {
53 #[error("Error fetching with the Git binary: {0}")]
55 Command(#[from] ExecutionError),
56
57 #[error("Error fetching with Libgit2: {0}")]
59 Git(#[from] GitError),
60}
61
62pub struct Fetch<'a> {
65 pub git_repo: &'a crate::workspace::Workspace,
67 pub target_url: &'a url::Url,
69 pub use_git_binary: Option<bool>,
71 pub git_config_path: &'a Path,
73 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 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}