1use crate::context::ConfigRelativePathResolveExt;
5use crate::util::HumanBytes;
6use crate::util::network::http::HttpTimeout;
7use crate::util::{MetricsCounter, Progress, network};
8use crate::{CargoResult, GlobalContext};
9use cargo_util::paths;
10use gix::bstr::{BString, ByteSlice};
11use std::cell::RefCell;
12use std::path::Path;
13use std::sync::atomic::{AtomicBool, Ordering};
14use std::sync::{Arc, Weak};
15use std::time::{Duration, Instant};
16use tracing::debug;
17
18pub fn with_retry_and_progress(
21 repo_path: &std::path::Path,
22 gctx: &GlobalContext,
23 repo_remote_url: &str,
24 cb: &(
25 dyn Fn(
26 &std::path::Path,
27 &AtomicBool,
28 &mut gix::progress::tree::Item,
29 &mut dyn FnMut(&gix::bstr::BStr),
30 ) -> Result<(), crate::sources::git::fetch::Error>
31 + Send
32 + Sync
33 ),
34) -> CargoResult<()> {
35 std::thread::scope(|s| {
36 let mut progress_bar = Progress::new("Fetch", gctx);
37 let is_shallow = gctx.cli_unstable().git.map_or(false, |features| {
38 features.shallow_deps || features.shallow_index
39 });
40 network::retry::with_retry(gctx, || {
41 let progress_root: Arc<gix::progress::tree::Root> =
42 gix::progress::tree::root::Options {
43 initial_capacity: 10,
44 message_buffer_capacity: 10,
45 }
46 .into();
47 let root = Arc::downgrade(&progress_root);
48 let thread = s.spawn(move || {
49 let mut progress = progress_root.add_child("operation");
50 let mut urls = RefCell::new(Default::default());
51 let res = cb(
52 &repo_path,
53 &AtomicBool::default(),
54 &mut progress,
55 &mut |url| {
56 *urls.borrow_mut() = Some(url.to_owned());
57 },
58 );
59 amend_authentication_hints(res, repo_remote_url, urls.get_mut().take())
60 });
61 translate_progress_to_bar(&mut progress_bar, root, is_shallow)?;
62 thread.join().expect("no panic in scoped thread")
63 })
64 })
65}
66
67fn translate_progress_to_bar(
68 progress_bar: &mut Progress<'_>,
69 root: Weak<gix::progress::tree::Root>,
70 is_shallow: bool,
71) -> CargoResult<()> {
72 let remote_progress: gix::progress::Id = gix::remote::fetch::ProgressId::RemoteProgress.into();
73 let read_pack_bytes: gix::progress::Id =
74 gix::odb::pack::bundle::write::ProgressId::ReadPackBytes.into();
75 let delta_index_objects: gix::progress::Id =
76 gix::odb::pack::index::write::ProgressId::IndexObjects.into();
77 let resolve_objects: gix::progress::Id =
78 gix::odb::pack::index::write::ProgressId::ResolveObjects.into();
79
80 let mut last_percentage_update = Instant::now();
83 let mut last_fast_update = Instant::now();
84 let mut counter = MetricsCounter::<10>::new(0, last_percentage_update);
85
86 let mut tasks = Vec::with_capacity(10);
87 let slow_check_interval = std::time::Duration::from_millis(300);
88 let fast_check_interval = Duration::from_millis(50);
89 let sleep_interval = Duration::from_millis(10);
90 debug_assert_eq!(
91 slow_check_interval.as_millis() % fast_check_interval.as_millis(),
92 0,
93 "progress should be smoother by keeping these as multiples of each other"
94 );
95 debug_assert_eq!(
96 fast_check_interval.as_millis() % sleep_interval.as_millis(),
97 0,
98 "progress should be smoother by keeping these as multiples of each other"
99 );
100
101 let num_phases = if is_shallow { 3 } else { 2 }; while let Some(root) = root.upgrade() {
103 std::thread::sleep(sleep_interval);
104 let needs_update = last_fast_update.elapsed() >= fast_check_interval;
105 if !needs_update {
106 continue;
107 }
108 let now = Instant::now();
109 last_fast_update = now;
110
111 root.sorted_snapshot(&mut tasks);
112
113 fn progress_by_id(
114 id: gix::progress::Id,
115 task: &gix::progress::Task,
116 ) -> Option<(&str, &gix::progress::Value)> {
117 (task.id == id)
118 .then(|| task.progress.as_ref())
119 .flatten()
120 .map(|value| (task.name.as_str(), value))
121 }
122 fn find_in<K>(
123 tasks: &[(K, gix::progress::Task)],
124 cb: impl Fn(&gix::progress::Task) -> Option<(&str, &gix::progress::Value)>,
125 ) -> Option<(&str, &gix::progress::Value)> {
126 tasks.iter().find_map(|(_, t)| cb(t))
127 }
128
129 if let Some((_, objs)) = find_in(&tasks, |t| progress_by_id(resolve_objects, t)) {
130 let objects = objs.step.load(Ordering::Relaxed);
132 let total_objects = objs.done_at.expect("known amount of objects");
133 let msg = format!(", ({objects}/{total_objects}) resolving deltas");
134
135 progress_bar.tick(
136 (total_objects * (num_phases - 1)) + objects,
137 total_objects * num_phases,
138 &msg,
139 )?;
140 } else if let Some((objs, read_pack)) =
141 find_in(&tasks, |t| progress_by_id(read_pack_bytes, t)).and_then(|read| {
142 find_in(&tasks, |t| progress_by_id(delta_index_objects, t))
143 .map(|delta| (delta.1, read.1))
144 })
145 {
146 let objects = objs.step.load(Ordering::Relaxed);
148 let total_objects = objs.done_at.expect("known amount of objects");
149 let received_bytes = read_pack.step.load(Ordering::Relaxed);
150
151 let needs_percentage_update = last_percentage_update.elapsed() >= slow_check_interval;
152 if needs_percentage_update {
153 counter.add(received_bytes, now);
154 last_percentage_update = now;
155 }
156 let rate = HumanBytes(counter.rate() as u64);
157 let msg = format!(", {rate:.2}/s");
158
159 progress_bar.tick(
160 (total_objects * (num_phases - 2)) + objects,
161 total_objects * num_phases,
162 &msg,
163 )?;
164 } else if let Some((action, remote)) =
165 find_in(&tasks, |t| progress_by_id(remote_progress, t))
166 {
167 if !is_shallow {
168 continue;
169 }
170 let objects = remote.step.load(Ordering::Relaxed);
174 if let Some(total_objects) = remote.done_at {
175 let msg = format!(", ({objects}/{total_objects}) {action}");
176 progress_bar.tick(objects, total_objects * num_phases, &msg)?;
177 }
178 }
179 }
180 Ok(())
181}
182
183#[derive(Debug, thiserror::Error)]
184#[error("{0}")]
185pub(super) struct CredentialError(#[source] pub gix::Error);
186
187fn amend_authentication_hints(
188 res: Result<(), crate::sources::git::fetch::Error>,
189 remote_url: &str,
190 last_url_for_authentication: Option<gix::bstr::BString>,
191) -> CargoResult<()> {
192 let Err(err) = res else { return Ok(()) };
193 let auth_message = if err.downcast_any_ref::<CredentialError>().is_some()
194 || err.is_unauthenticated()
195 {
196 if err
197 .downcast_any_ref::<gix::protocol::transport::client::AuthenticationRequired>()
198 .is_none()
199 {
200 "\n* attempted to find username/password via \
201 git's `credential.helper` support, but failed"
202 .into()
203 } else {
204 "\n* attempted to find username/password via \
205 `credential.helper`, but maybe the found \
206 credentials were incorrect"
207 .into()
208 }
209 } else {
210 if err
211 .downcast_any_ref::<gix::protocol::transport::client::Error>()
212 .is_some()
213 {
214 let msg = format!(
215 concat!(
216 "network failure seems to have happened\n",
217 "if a proxy or similar is necessary `net.git-fetch-with-cli` may help here\n",
218 "https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli",
219 "{}"
220 ),
221 super::utils::note_github_pull_request(remote_url).unwrap_or_default()
222 );
223 return Err(anyhow::Error::from(err).context(msg));
224 }
225 None
226 };
227 if let Some(auth_message) = auth_message {
228 let mut msg = "failed to authenticate when downloading \
229 repository"
230 .to_string();
231 if let Some(url) = last_url_for_authentication {
232 msg.push_str(": ");
233 msg.push_str(url.to_str_lossy().as_ref());
234 }
235 msg.push('\n');
236 msg.push_str(auth_message);
237 msg.push_str("\n\n");
238 msg.push_str("if the git CLI succeeds then `net.git-fetch-with-cli` may help here\n");
239 msg.push_str("https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli");
240 return Err(anyhow::Error::from(err).context(msg));
241 }
242 Err(err.into())
243}
244
245pub enum OpenMode {
249 ForFetch,
254}
255
256impl OpenMode {
257 pub fn needs_git_binary_config(&self) -> bool {
260 match self {
261 OpenMode::ForFetch => true,
262 }
263 }
264}
265
266pub fn open_repo(
270 repo_path: &std::path::Path,
271 config_overrides: Vec<BString>,
272 purpose: OpenMode,
273) -> gix::Result<gix::Repository> {
274 gix::open_opts(repo_path, {
275 let mut opts = gix::open::Options::default();
276 opts.permissions.config = gix::open::permissions::Config::all();
277 opts.permissions.config.git_binary = purpose.needs_git_binary_config();
278 opts.with(gix::sec::Trust::Full)
279 .config_overrides(config_overrides)
280 })
281}
282
283pub fn cargo_config_to_gitoxide_overrides(gctx: &GlobalContext) -> CargoResult<Vec<BString>> {
286 use gix::config::tree::{Core, Http, Key, gitoxide};
287 let timeout = HttpTimeout::new(gctx)?;
288 let http = gctx.http_config()?;
289
290 let mut values = vec![
291 gitoxide::Http::CONNECT_TIMEOUT.validated_assignment_fmt(&timeout.dur.as_millis())?,
292 Http::LOW_SPEED_LIMIT.validated_assignment_fmt(&timeout.low_speed_limit)?,
293 Http::LOW_SPEED_TIME.validated_assignment_fmt(&timeout.dur.as_secs())?,
294 Core::LOG_ALL_REF_UPDATES.validated_assignment_fmt(&false)?,
296 ];
297 if let Some(proxy) = &http.proxy {
298 values.push(Http::PROXY.validated_assignment_fmt(proxy)?);
299 }
300 if let Some(check_revoke) = http.check_revoke {
301 values.push(Http::SCHANNEL_CHECK_REVOKE.validated_assignment_fmt(&check_revoke)?);
302 }
303 if let Some(cainfo) = &http.cainfo {
304 values.push(
305 Http::SSL_CA_INFO.validated_assignment_fmt(&cainfo.resolve_path(gctx).display())?,
306 );
307 }
308
309 values.push(if let Some(user_agent) = &http.user_agent {
310 Http::USER_AGENT.validated_assignment_fmt(user_agent)
311 } else {
312 Http::USER_AGENT.validated_assignment_fmt(&format!("cargo {}", crate::version()))
313 }?);
314 if let Some(ssl_version) = &http.ssl_version {
315 use crate::context::SslVersionConfig;
316 match ssl_version {
317 SslVersionConfig::Single(version) => {
318 values.push(Http::SSL_VERSION.validated_assignment_fmt(&version)?);
319 }
320 SslVersionConfig::Range(range) => {
321 values.push(
322 gitoxide::Http::SSL_VERSION_MIN
323 .validated_assignment_fmt(&range.min.as_deref().unwrap_or("default"))?,
324 );
325 values.push(
326 gitoxide::Http::SSL_VERSION_MAX
327 .validated_assignment_fmt(&range.max.as_deref().unwrap_or("default"))?,
328 );
329 }
330 }
331 } else if cfg!(windows) {
332 values.push(gitoxide::Http::SSL_VERSION_MIN.validated_assignment_fmt(&"default")?);
350 values.push(gitoxide::Http::SSL_VERSION_MAX.validated_assignment_fmt(&"tlsv1.2")?);
351 }
352 if let Some(debug) = http.debug {
353 values.push(gitoxide::Http::VERBOSE.validated_assignment_fmt(&debug)?);
354 }
355 if let Some(multiplexing) = http.multiplexing {
356 let http_version = multiplexing.then(|| "HTTP/2").unwrap_or("HTTP/1.1");
357 values.push(Http::VERSION.validated_assignment_fmt(&http_version)?);
361 }
362
363 Ok(values)
364}
365
366pub fn reinitialize(git_dir: &Path) -> CargoResult<()> {
369 fn init(path: &Path, bare: bool) -> CargoResult<()> {
370 let mut opts = git2::RepositoryInitOptions::new();
371 opts.external_template(false);
375 opts.bare(bare);
376 git2::Repository::init_opts(&path, &opts)?;
377 Ok(())
378 }
379 debug!("reinitializing git repo at {:?}", git_dir);
384 let tmp = git_dir.join("tmp");
385 let bare = !git_dir.ends_with(".git");
386 init(&tmp, false)?;
387 for entry in git_dir.read_dir()? {
388 let entry = entry?;
389 if entry.file_name().to_str() == Some("tmp") {
390 continue;
391 }
392 let path = entry.path();
393 drop(paths::remove_file(&path).or_else(|_| paths::remove_dir_all(&path)));
394 }
395 init(git_dir, bare)?;
396 paths::remove_dir_all(&tmp)?;
397 Ok(())
398}
399
400#[cfg(test)]
401mod tests {
402 use super::{CredentialError, amend_authentication_hints};
403 use gix::error::ErrorExt;
404 use gix::protocol::transport::client::{AuthenticationRequired, Error};
405
406 #[test]
407 fn authentication_hints_preserve_causes_and_distinguish_network_errors() {
408 let missing = gix::error::unauthenticated("missing identity").raise();
409 let helper =
410 CredentialError(gix::error::message("Terminal prompts are disabled").raise()).raise();
411 let rejected = std::io::Error::new(
412 std::io::ErrorKind::PermissionDenied,
413 AuthenticationRequired::default(),
414 )
415 .raise();
416 let network = Error::Io(std::io::Error::from(std::io::ErrorKind::ConnectionReset)).raise();
417 for (err, hint) in [
418 (missing, "git's `credential.helper` support, but failed"),
419 (helper, "git's `credential.helper` support, but failed"),
420 (rejected, "credentials were incorrect"),
421 (network, "network failure seems to have happened"),
422 ] {
423 let err = amend_authentication_hints(
424 Err(err),
425 "https://example.com/repo",
426 Some("https://example.com/auth".into()),
427 )
428 .unwrap_err();
429 assert!(err.to_string().contains(hint));
430 assert!(err.downcast_ref::<gix::Error>().is_some());
431 }
432 let cancelled = CredentialError(
433 std::io::Error::from(std::io::ErrorKind::ConnectionReset)
434 .and_raise(gix::error::cancelled("cancelled")),
435 )
436 .raise();
437 assert!(cancelled.is_cancelled());
438 assert!(!cancelled.can_retry_lenient());
439 }
440}