Skip to content

Commit 1759263

Browse files
committed
support minimal 'fetch' using gitoxide
This most notably excludes: * progress * SSH name guessing * retry on spurious timeout/connection issues
1 parent 0acd0eb commit 1759263

File tree

4 files changed

+185
-41
lines changed

4 files changed

+185
-41
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ filetime = "0.2.9"
2929
flate2 = { version = "1.0.3", default-features = false, features = ["zlib"] }
3030
git2 = "0.15.0"
3131
git2-curl = "0.16.0"
32+
git-repository = { version = "0.29.0", features = ["blocking-http-transport-curl"] }
3233
glob = "0.3.0"
3334
hex = "0.4"
3435
home = "0.5"

src/cargo/sources/git/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub use self::source::GitSource;
22
pub use self::utils::{fetch, GitCheckout, GitDatabase, GitRemote};
3+
mod oxide;
34
mod source;
45
mod utils;

src/cargo/sources/git/oxide.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//! This module contains all code sporting `gitoxide` for operations on `git` repositories and it mirrors
2+
//! `utils` closely for now. One day it can be renamed into `utils` once `git2` isn't required anymore.
3+
4+
use crate::util::{network, Progress};
5+
use crate::{CargoResult, Config};
6+
use git_repository as git;
7+
use std::sync::atomic::AtomicBool;
8+
use std::sync::Arc;
9+
use std::time::Duration;
10+
11+
/// For the time being, `repo_path` makes it easy to instantiate a gitoxide repo just for fetching.
12+
/// In future this may change to be the gitoxide repository itself.
13+
pub fn with_retry_and_progress(
14+
repo_path: &std::path::Path,
15+
config: &Config,
16+
cb: &mut (dyn FnMut(
17+
&git::Repository,
18+
&AtomicBool,
19+
&mut git::progress::tree::Item,
20+
) -> CargoResult<()>
21+
+ Send),
22+
) -> CargoResult<()> {
23+
let repo = git::open_opts(repo_path, {
24+
let mut opts = git::open::Options::default();
25+
// We need `git_binary` configuration as well for being able to see credential helpers
26+
// that are configured with the `git` installation itself.
27+
// However, this is slow on windows (~150ms) and most people won't need it as they use the
28+
// standard index which won't ever need authentication.
29+
// TODO: This is certainly something to make configurable, at the very least on windows.
30+
// Maybe it's also something that could be cached, all we need is the path to the configuration file
31+
// which usually doesn't change unless the installation changes. Maybe something keyed by the location of the
32+
// binary along with its fingerprint.
33+
opts.permissions.config = git::permissions::Config::all();
34+
opts
35+
})?;
36+
37+
let progress_root: Arc<git::progress::tree::Root> = git::progress::tree::root::Options {
38+
initial_capacity: 10,
39+
message_buffer_capacity: 10,
40+
}
41+
.into();
42+
let mut progress = progress_root.add_child("operation");
43+
44+
// For decent interrupts of long-running computations and removal of temp files we should handle interrupts, and this
45+
// is an easy way to do that. We will remove them later.
46+
// We intentionally swallow errors here as if for some reason we can't register handlers, `cargo` will just work like before and
47+
// abort on signals.
48+
let _deregister_signal_handlers_on_drop = git::interrupt::init_handler(|| {})
49+
.ok()
50+
.unwrap_or_default()
51+
.auto_deregister();
52+
let should_interrupt = AtomicBool::new(false);
53+
let _progress_bar = Progress::new("Fetch", config);
54+
std::thread::scope(move |s| {
55+
s.spawn({
56+
let root = Arc::downgrade(&progress_root);
57+
move || -> CargoResult<()> {
58+
let mut tasks = Vec::with_capacity(10);
59+
while let Some(root) = root.upgrade() {
60+
root.sorted_snapshot(&mut tasks);
61+
// dbg!(&tasks);
62+
std::thread::sleep(Duration::from_millis(300));
63+
}
64+
Ok(())
65+
}
66+
});
67+
network::with_retry(config, || cb(&repo, &should_interrupt, &mut progress))
68+
})
69+
}

src/cargo/sources/git/utils.rs

Lines changed: 114 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
//! authentication/cloning.
33
44
use crate::core::GitReference;
5+
use crate::sources::git::oxide;
56
use crate::util::errors::CargoResult;
67
use crate::util::{human_readable_bytes, network, Config, IntoUrl, MetricsCounter, Progress};
78
use anyhow::{anyhow, Context as _};
@@ -855,51 +856,123 @@ pub fn fetch(
855856
if let Some(true) = config.net_config()?.git_fetch_with_cli {
856857
return fetch_with_cli(repo, url, &refspecs, tags, config);
857858
}
858-
859-
debug!("doing a fetch for {}", url);
860-
let git_config = git2::Config::open_default()?;
861-
with_fetch_options(&git_config, url, config, &mut |mut opts| {
862-
if tags {
863-
opts.download_tags(git2::AutotagOption::All);
864-
}
865-
// The `fetch` operation here may fail spuriously due to a corrupt
866-
// repository. It could also fail, however, for a whole slew of other
867-
// reasons (aka network related reasons). We want Cargo to automatically
868-
// recover from corrupt repositories, but we don't want Cargo to stomp
869-
// over other legitimate errors.
870-
//
871-
// Consequently we save off the error of the `fetch` operation and if it
872-
// looks like a "corrupt repo" error then we blow away the repo and try
873-
// again. If it looks like any other kind of error, or if we've already
874-
// blown away the repository, then we want to return the error as-is.
875-
let mut repo_reinitialized = false;
876-
loop {
877-
debug!("initiating fetch of {:?} from {}", refspecs, url);
878-
let res = repo
879-
.remote_anonymous(url)?
880-
.fetch(&refspecs, Some(&mut opts), None);
881-
let err = match res {
882-
Ok(()) => break,
883-
Err(e) => e,
884-
};
885-
debug!("fetch failed: {}", err);
886-
887-
if !repo_reinitialized && matches!(err.class(), ErrorClass::Reference | ErrorClass::Odb)
888-
{
889-
repo_reinitialized = true;
890-
debug!(
891-
"looks like this is a corrupt repository, reinitializing \
859+
if config
860+
.cli_unstable()
861+
.gitoxide
862+
.map_or(false, |git| git.fetch)
863+
{
864+
use git::remote::fetch::Error;
865+
use git_repository as git;
866+
let git2_repo = repo;
867+
oxide::with_retry_and_progress(
868+
&git2_repo.path().to_owned(),
869+
config,
870+
&mut |repo, should_interrupt, progress| {
871+
// The `fetch` operation here may fail spuriously due to a corrupt
872+
// repository. It could also fail, however, for a whole slew of other
873+
// reasons (aka network related reasons). We want Cargo to automatically
874+
// recover from corrupt repositories, but we don't want Cargo to stomp
875+
// over other legitimate errors.
876+
//
877+
// Consequently we save off the error of the `fetch` operation and if it
878+
// looks like a "corrupt repo" error then we blow away the repo and try
879+
// again. If it looks like any other kind of error, or if we've already
880+
// blown away the repository, then we want to return the error as-is.
881+
let mut repo_reinitialized = false;
882+
let mut repo_storage;
883+
let mut repo = &*repo;
884+
loop {
885+
debug!("initiating fetch of {:?} from {}", refspecs, url);
886+
let res = repo
887+
.remote_at(url)?
888+
.with_refspecs(
889+
refspecs.iter().map(|s| s.as_str()),
890+
git::remote::Direction::Fetch,
891+
)?
892+
.connect(git::remote::Direction::Fetch, progress.add_child("fetch"))?
893+
.prepare_fetch(git::remote::ref_map::Options::default())?
894+
.receive(should_interrupt);
895+
let err = match res {
896+
Ok(_) => break,
897+
Err(e) => e,
898+
};
899+
debug!("fetch failed: {}", err);
900+
901+
if !repo_reinitialized
902+
&& matches!(
903+
err,
904+
Error::Configuration { .. }
905+
| Error::IncompatibleObjectHash { .. }
906+
| Error::WritePack(_)
907+
| Error::UpdateRefs(_)
908+
| Error::RemovePackKeepFile { .. }
909+
)
910+
{
911+
repo_reinitialized = true;
912+
debug!(
913+
"looks like this is a corrupt repository, reinitializing \
892914
and trying again"
893-
);
894-
if reinitialize(repo).is_ok() {
895-
continue;
915+
);
916+
if reinitialize(git2_repo).is_ok() {
917+
repo_storage =
918+
git::open_opts(repo.path(), repo.open_options().to_owned())?;
919+
repo = &repo_storage;
920+
continue;
921+
}
922+
}
923+
924+
return Err(err.into());
896925
}
926+
Ok(())
927+
},
928+
)
929+
} else {
930+
debug!("doing a fetch for {}", url);
931+
let git_config = git2::Config::open_default()?;
932+
with_fetch_options(&git_config, url, config, &mut |mut opts| {
933+
if tags {
934+
opts.download_tags(git2::AutotagOption::All);
897935
}
936+
// The `fetch` operation here may fail spuriously due to a corrupt
937+
// repository. It could also fail, however, for a whole slew of other
938+
// reasons (aka network related reasons). We want Cargo to automatically
939+
// recover from corrupt repositories, but we don't want Cargo to stomp
940+
// over other legitimate errors.
941+
//
942+
// Consequently we save off the error of the `fetch` operation and if it
943+
// looks like a "corrupt repo" error then we blow away the repo and try
944+
// again. If it looks like any other kind of error, or if we've already
945+
// blown away the repository, then we want to return the error as-is.
946+
let mut repo_reinitialized = false;
947+
loop {
948+
debug!("initiating fetch of {:?} from {}", refspecs, url);
949+
let res = repo
950+
.remote_anonymous(url)?
951+
.fetch(&refspecs, Some(&mut opts), None);
952+
let err = match res {
953+
Ok(()) => break,
954+
Err(e) => e,
955+
};
956+
debug!("fetch failed: {}", err);
957+
958+
if !repo_reinitialized
959+
&& matches!(err.class(), ErrorClass::Reference | ErrorClass::Odb)
960+
{
961+
repo_reinitialized = true;
962+
debug!(
963+
"looks like this is a corrupt repository, reinitializing \
964+
and trying again"
965+
);
966+
if reinitialize(repo).is_ok() {
967+
continue;
968+
}
969+
}
898970

899-
return Err(err.into());
900-
}
901-
Ok(())
902-
})
971+
return Err(err.into());
972+
}
973+
Ok(())
974+
})
975+
}
903976
}
904977

905978
fn fetch_with_cli(

0 commit comments

Comments
 (0)