1use crate::util::network::http::HttpTimeout;
5use crate::util::{human_readable_bytes, network, MetricsCounter, Progress};
6use crate::{CargoResult, GlobalContext};
7use cargo_util::paths;
8use gix::bstr::{BString, ByteSlice};
9use std::cell::RefCell;
10use std::path::Path;
11use std::sync::atomic::{AtomicBool, Ordering};
12use std::sync::{Arc, Weak};
13use std::time::{Duration, Instant};
14use tracing::debug;
15
16pub fn with_retry_and_progress(
19 repo_path: &std::path::Path,
20 gctx: &GlobalContext,
21 cb: &(dyn Fn(
22 &std::path::Path,
23 &AtomicBool,
24 &mut gix::progress::tree::Item,
25 &mut dyn FnMut(&gix::bstr::BStr),
26 ) -> Result<(), crate::sources::git::fetch::Error>
27 + Send
28 + Sync),
29) -> CargoResult<()> {
30 std::thread::scope(|s| {
31 let mut progress_bar = Progress::new("Fetch", gctx);
32 let is_shallow = gctx.cli_unstable().git.map_or(false, |features| {
33 features.shallow_deps || features.shallow_index
34 });
35 network::retry::with_retry(gctx, || {
36 let progress_root: Arc<gix::progress::tree::Root> =
37 gix::progress::tree::root::Options {
38 initial_capacity: 10,
39 message_buffer_capacity: 10,
40 }
41 .into();
42 let root = Arc::downgrade(&progress_root);
43 let thread = s.spawn(move || {
44 let mut progress = progress_root.add_child("operation");
45 let mut urls = RefCell::new(Default::default());
46 let res = cb(
47 &repo_path,
48 &AtomicBool::default(),
49 &mut progress,
50 &mut |url| {
51 *urls.borrow_mut() = Some(url.to_owned());
52 },
53 );
54 amend_authentication_hints(res, urls.get_mut().take())
55 });
56 translate_progress_to_bar(&mut progress_bar, root, is_shallow)?;
57 thread.join().expect("no panic in scoped thread")
58 })
59 })
60}
61
62fn translate_progress_to_bar(
63 progress_bar: &mut Progress<'_>,
64 root: Weak<gix::progress::tree::Root>,
65 is_shallow: bool,
66) -> CargoResult<()> {
67 let remote_progress: gix::progress::Id = gix::remote::fetch::ProgressId::RemoteProgress.into();
68 let read_pack_bytes: gix::progress::Id =
69 gix::odb::pack::bundle::write::ProgressId::ReadPackBytes.into();
70 let delta_index_objects: gix::progress::Id =
71 gix::odb::pack::index::write::ProgressId::IndexObjects.into();
72 let resolve_objects: gix::progress::Id =
73 gix::odb::pack::index::write::ProgressId::ResolveObjects.into();
74
75 let mut last_percentage_update = Instant::now();
78 let mut last_fast_update = Instant::now();
79 let mut counter = MetricsCounter::<10>::new(0, last_percentage_update);
80
81 let mut tasks = Vec::with_capacity(10);
82 let slow_check_interval = std::time::Duration::from_millis(300);
83 let fast_check_interval = Duration::from_millis(50);
84 let sleep_interval = Duration::from_millis(10);
85 debug_assert_eq!(
86 slow_check_interval.as_millis() % fast_check_interval.as_millis(),
87 0,
88 "progress should be smoother by keeping these as multiples of each other"
89 );
90 debug_assert_eq!(
91 fast_check_interval.as_millis() % sleep_interval.as_millis(),
92 0,
93 "progress should be smoother by keeping these as multiples of each other"
94 );
95
96 let num_phases = if is_shallow { 3 } else { 2 }; while let Some(root) = root.upgrade() {
98 std::thread::sleep(sleep_interval);
99 let needs_update = last_fast_update.elapsed() >= fast_check_interval;
100 if !needs_update {
101 continue;
102 }
103 let now = Instant::now();
104 last_fast_update = now;
105
106 root.sorted_snapshot(&mut tasks);
107
108 fn progress_by_id(
109 id: gix::progress::Id,
110 task: &gix::progress::Task,
111 ) -> Option<(&str, &gix::progress::Value)> {
112 (task.id == id)
113 .then(|| task.progress.as_ref())
114 .flatten()
115 .map(|value| (task.name.as_str(), value))
116 }
117 fn find_in<K>(
118 tasks: &[(K, gix::progress::Task)],
119 cb: impl Fn(&gix::progress::Task) -> Option<(&str, &gix::progress::Value)>,
120 ) -> Option<(&str, &gix::progress::Value)> {
121 tasks.iter().find_map(|(_, t)| cb(t))
122 }
123
124 if let Some((_, objs)) = find_in(&tasks, |t| progress_by_id(resolve_objects, t)) {
125 let objects = objs.step.load(Ordering::Relaxed);
127 let total_objects = objs.done_at.expect("known amount of objects");
128 let msg = format!(", ({objects}/{total_objects}) resolving deltas");
129
130 progress_bar.tick(
131 (total_objects * (num_phases - 1)) + objects,
132 total_objects * num_phases,
133 &msg,
134 )?;
135 } else if let Some((objs, read_pack)) =
136 find_in(&tasks, |t| progress_by_id(read_pack_bytes, t)).and_then(|read| {
137 find_in(&tasks, |t| progress_by_id(delta_index_objects, t))
138 .map(|delta| (delta.1, read.1))
139 })
140 {
141 let objects = objs.step.load(Ordering::Relaxed);
143 let total_objects = objs.done_at.expect("known amount of objects");
144 let received_bytes = read_pack.step.load(Ordering::Relaxed);
145
146 let needs_percentage_update = last_percentage_update.elapsed() >= slow_check_interval;
147 if needs_percentage_update {
148 counter.add(received_bytes, now);
149 last_percentage_update = now;
150 }
151 let (rate, unit) = human_readable_bytes(counter.rate() as u64);
152 let msg = format!(", {rate:.2}{unit}/s");
153
154 progress_bar.tick(
155 (total_objects * (num_phases - 2)) + objects,
156 total_objects * num_phases,
157 &msg,
158 )?;
159 } else if let Some((action, remote)) =
160 find_in(&tasks, |t| progress_by_id(remote_progress, t))
161 {
162 if !is_shallow {
163 continue;
164 }
165 let objects = remote.step.load(Ordering::Relaxed);
169 if let Some(total_objects) = remote.done_at {
170 let msg = format!(", ({objects}/{total_objects}) {action}");
171 progress_bar.tick(objects, total_objects * num_phases, &msg)?;
172 }
173 }
174 }
175 Ok(())
176}
177
178fn amend_authentication_hints(
179 res: Result<(), crate::sources::git::fetch::Error>,
180 last_url_for_authentication: Option<gix::bstr::BString>,
181) -> CargoResult<()> {
182 let Err(err) = res else { return Ok(()) };
183 let e = match &err {
184 crate::sources::git::fetch::Error::PrepareFetch(
185 gix::remote::fetch::prepare::Error::RefMap(gix::remote::ref_map::Error::Handshake(err)),
186 ) => Some(err),
187 _ => None,
188 };
189 if let Some(e) = e {
190 let auth_message = match e {
191 gix::protocol::handshake::Error::Credentials(_) => {
192 "\n* attempted to find username/password via \
193 git's `credential.helper` support, but failed"
194 .into()
195 }
196 gix::protocol::handshake::Error::InvalidCredentials { .. } => {
197 "\n* attempted to find username/password via \
198 `credential.helper`, but maybe the found \
199 credentials were incorrect"
200 .into()
201 }
202 gix::protocol::handshake::Error::Transport(_) => {
203 let msg = concat!(
204 "network failure seems to have happened\n",
205 "if a proxy or similar is necessary `net.git-fetch-with-cli` may help here\n",
206 "https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli"
207 );
208 return Err(anyhow::Error::from(err).context(msg));
209 }
210 _ => None,
211 };
212 if let Some(auth_message) = auth_message {
213 let mut msg = "failed to authenticate when downloading \
214 repository"
215 .to_string();
216 if let Some(url) = last_url_for_authentication {
217 msg.push_str(": ");
218 msg.push_str(url.to_str_lossy().as_ref());
219 }
220 msg.push('\n');
221 msg.push_str(auth_message);
222 msg.push_str("\n\n");
223 msg.push_str("if the git CLI succeeds then `net.git-fetch-with-cli` may help here\n");
224 msg.push_str(
225 "https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli",
226 );
227 return Err(anyhow::Error::from(err).context(msg));
228 }
229 }
230 Err(err.into())
231}
232
233pub enum OpenMode {
237 ForFetch,
242}
243
244impl OpenMode {
245 pub fn needs_git_binary_config(&self) -> bool {
248 match self {
249 OpenMode::ForFetch => true,
250 }
251 }
252}
253
254pub fn open_repo(
258 repo_path: &std::path::Path,
259 config_overrides: Vec<BString>,
260 purpose: OpenMode,
261) -> Result<gix::Repository, gix::open::Error> {
262 gix::open_opts(repo_path, {
263 let mut opts = gix::open::Options::default();
264 opts.permissions.config = gix::open::permissions::Config::all();
265 opts.permissions.config.git_binary = purpose.needs_git_binary_config();
266 opts.with(gix::sec::Trust::Full)
267 .config_overrides(config_overrides)
268 })
269}
270
271pub fn cargo_config_to_gitoxide_overrides(gctx: &GlobalContext) -> CargoResult<Vec<BString>> {
274 use gix::config::tree::{gitoxide, Core, Http, Key};
275 let timeout = HttpTimeout::new(gctx)?;
276 let http = gctx.http_config()?;
277
278 let mut values = vec![
279 gitoxide::Http::CONNECT_TIMEOUT.validated_assignment_fmt(&timeout.dur.as_millis())?,
280 Http::LOW_SPEED_LIMIT.validated_assignment_fmt(&timeout.low_speed_limit)?,
281 Http::LOW_SPEED_TIME.validated_assignment_fmt(&timeout.dur.as_secs())?,
282 Core::LOG_ALL_REF_UPDATES.validated_assignment_fmt(&false)?,
284 ];
285 if let Some(proxy) = &http.proxy {
286 values.push(Http::PROXY.validated_assignment_fmt(proxy)?);
287 }
288 if let Some(check_revoke) = http.check_revoke {
289 values.push(Http::SCHANNEL_CHECK_REVOKE.validated_assignment_fmt(&check_revoke)?);
290 }
291 if let Some(cainfo) = &http.cainfo {
292 values.push(
293 Http::SSL_CA_INFO.validated_assignment_fmt(&cainfo.resolve_path(gctx).display())?,
294 );
295 }
296
297 values.push(if let Some(user_agent) = &http.user_agent {
298 Http::USER_AGENT.validated_assignment_fmt(user_agent)
299 } else {
300 Http::USER_AGENT.validated_assignment_fmt(&format!("cargo {}", crate::version()))
301 }?);
302 if let Some(ssl_version) = &http.ssl_version {
303 use crate::util::context::SslVersionConfig;
304 match ssl_version {
305 SslVersionConfig::Single(version) => {
306 values.push(Http::SSL_VERSION.validated_assignment_fmt(&version)?);
307 }
308 SslVersionConfig::Range(range) => {
309 values.push(
310 gitoxide::Http::SSL_VERSION_MIN
311 .validated_assignment_fmt(&range.min.as_deref().unwrap_or("default"))?,
312 );
313 values.push(
314 gitoxide::Http::SSL_VERSION_MAX
315 .validated_assignment_fmt(&range.max.as_deref().unwrap_or("default"))?,
316 );
317 }
318 }
319 } else if cfg!(windows) {
320 values.push(gitoxide::Http::SSL_VERSION_MIN.validated_assignment_fmt(&"default")?);
338 values.push(gitoxide::Http::SSL_VERSION_MAX.validated_assignment_fmt(&"tlsv1.2")?);
339 }
340 if let Some(debug) = http.debug {
341 values.push(gitoxide::Http::VERBOSE.validated_assignment_fmt(&debug)?);
342 }
343 if let Some(multiplexing) = http.multiplexing {
344 let http_version = multiplexing.then(|| "HTTP/2").unwrap_or("HTTP/1.1");
345 values.push(Http::VERSION.validated_assignment_fmt(&http_version)?);
349 }
350
351 Ok(values)
352}
353
354pub fn reinitialize(git_dir: &Path) -> CargoResult<()> {
357 fn init(path: &Path, bare: bool) -> CargoResult<()> {
358 let mut opts = git2::RepositoryInitOptions::new();
359 opts.external_template(false);
363 opts.bare(bare);
364 git2::Repository::init_opts(&path, &opts)?;
365 Ok(())
366 }
367 debug!("reinitializing git repo at {:?}", git_dir);
372 let tmp = git_dir.join("tmp");
373 let bare = !git_dir.ends_with(".git");
374 init(&tmp, false)?;
375 for entry in git_dir.read_dir()? {
376 let entry = entry?;
377 if entry.file_name().to_str() == Some("tmp") {
378 continue;
379 }
380 let path = entry.path();
381 drop(paths::remove_file(&path).or_else(|_| paths::remove_dir_all(&path)));
382 }
383 init(git_dir, bare)?;
384 paths::remove_dir_all(&tmp)?;
385 Ok(())
386}