Skip to content

Commit fb11423

Browse files
committed
enable all applicable git tests and fix them
1 parent 48b782a commit fb11423

File tree

8 files changed

+294
-74
lines changed

8 files changed

+294
-74
lines changed

.github/workflows/main.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ jobs:
8585
df -h
8686
rm -rf target/tmp
8787
df -h
88-
- name: gitoxide tests (git-related and enabled tests only)
89-
run: RUSTFLAGS='--cfg test_gitoxide' cargo test git
88+
- name: gitoxide tests (all git-related tests)
89+
run: RUSTFLAGS='--cfg always_test_gitoxide' cargo test git
9090
# The testsuite generates a huge amount of data, and fetch-smoke-test was
9191
# running out of disk space.
9292
- name: Clear test output

src/cargo/core/features.rs

+15
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,17 @@ impl GitoxideFeatures {
801801
shallow_deps: true,
802802
}
803803
}
804+
805+
/// Features we deem safe for everyday use - typically true when all tests pass with them
806+
/// AND they are backwards compatible.
807+
fn safe() -> Self {
808+
GitoxideFeatures {
809+
fetch: true,
810+
shallow_index: false,
811+
checkout: true,
812+
shallow_deps: false,
813+
}
814+
}
804815
}
805816

806817
fn parse_gitoxide(
@@ -880,6 +891,10 @@ impl CliUnstable {
880891
for flag in flags {
881892
self.add(flag, &mut warnings)?;
882893
}
894+
895+
if self.gitoxide.is_none() && cfg!(always_test_gitoxide) {
896+
self.gitoxide = GitoxideFeatures::safe().into();
897+
}
883898
Ok(warnings)
884899
}
885900

src/cargo/sources/git/oxide.rs

+103-18
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
//! This module contains all code sporting `gitoxide` for operations on `git` repositories and it mirrors
22
//! `utils` closely for now. One day it can be renamed into `utils` once `git2` isn't required anymore.
33
4+
use crate::ops::HttpTimeout;
45
use crate::util::{human_readable_bytes, network, MetricsCounter, Progress};
56
use crate::{CargoResult, Config};
67
use git_repository as git;
8+
use git_repository::bstr::ByteSlice;
9+
use std::cell::RefCell;
710
use std::sync::atomic::{AtomicBool, Ordering};
811
use std::sync::Arc;
912
use std::time::{Duration, Instant};
@@ -14,26 +17,12 @@ pub fn with_retry_and_progress(
1417
repo_path: &std::path::Path,
1518
config: &Config,
1619
cb: &mut (dyn FnMut(
17-
&git::Repository,
20+
&std::path::Path,
1821
&AtomicBool,
1922
&mut git::progress::tree::Item,
20-
) -> CargoResult<()>
21-
+ Send),
23+
&mut dyn FnMut(&git::bstr::BStr),
24+
) -> CargoResult<()>),
2225
) -> 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-
3726
let progress_root: Arc<git::progress::tree::Root> = git::progress::tree::root::Options {
3827
initial_capacity: 10,
3928
message_buffer_capacity: 10,
@@ -120,7 +109,103 @@ pub fn with_retry_and_progress(
120109
}
121110
});
122111
network::with_retry(config, || {
123-
cb(&repo, &git::interrupt::IS_INTERRUPTED, &mut progress)
112+
let mut urls = RefCell::new(Default::default());
113+
let res = cb(
114+
&repo_path,
115+
&git::interrupt::IS_INTERRUPTED,
116+
&mut progress,
117+
&mut |url| {
118+
*urls.borrow_mut() = Some(url.to_owned());
119+
},
120+
);
121+
amend_authentication_hints(res, urls.get_mut().take())
124122
})
125123
})
126124
}
125+
126+
fn amend_authentication_hints(
127+
res: CargoResult<()>,
128+
last_url_for_authentication: Option<git::bstr::BString>,
129+
) -> CargoResult<()> {
130+
match res {
131+
Ok(()) => Ok(()),
132+
Err(mut err) => {
133+
if let Some(e) = err
134+
.downcast_ref::<git::remote::fetch::prepare::Error>()
135+
.and_then(|e| match e {
136+
git::remote::fetch::prepare::Error::RefMap(
137+
git::remote::ref_map::Error::Handshake(e),
138+
) => Some(e),
139+
_ => None,
140+
})
141+
{
142+
let auth_message = match e {
143+
git::protocol::handshake::Error::Credentials(_) => {
144+
"\n* attempted to find username/password via \
145+
git's `credential.helper` support, but failed"
146+
.into()
147+
}
148+
git::protocol::handshake::Error::InvalidCredentials { url: _ } => {
149+
"\n* attempted to find username/password via \
150+
`credential.helper`, but maybe the found \
151+
credentials were incorrect"
152+
.into()
153+
}
154+
git::protocol::handshake::Error::Transport(_) => {
155+
let msg = concat!("network failure seems to have happened\n",
156+
"if a proxy or similar is necessary `net.git-fetch-with-cli` may help here\n",
157+
"https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli"
158+
);
159+
return Err(err.context(msg));
160+
}
161+
_ => None,
162+
};
163+
if let Some(auth_message) = auth_message {
164+
let mut msg = "failed to authenticate when downloading \
165+
repository"
166+
.to_string();
167+
if let Some(url) = last_url_for_authentication {
168+
msg.push_str(": ");
169+
msg.push_str(url.to_str_lossy().as_ref());
170+
}
171+
msg.push('\n');
172+
msg.push_str(auth_message);
173+
msg.push_str("\n\n");
174+
msg.push_str(
175+
"if the git CLI succeeds then `net.git-fetch-with-cli` may help here\n",
176+
);
177+
msg.push_str(
178+
"https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli",
179+
);
180+
err = err.context(msg);
181+
}
182+
}
183+
Err(err)
184+
}
185+
}
186+
}
187+
188+
/// Produce a repository with everything pre-configured according to `config`. Most notably this includes
189+
/// transport configuration.
190+
pub fn open_repo(repo_path: &std::path::Path, config: &Config) -> CargoResult<git::Repository> {
191+
git::open_opts(repo_path, {
192+
let mut opts = git::open::Options::default();
193+
let timeout = HttpTimeout::new(config)?;
194+
195+
// We need `git_binary` configuration as well for being able to see credential helpers
196+
// that are configured with the `git` installation itself.
197+
// However, this is slow on windows (~150ms) and most people won't need it as they use the
198+
// standard index which won't ever need authentication.
199+
// TODO: This is certainly something to make configurable, at the very least on windows.
200+
// Maybe it's also something that could be cached, all we need is the path to the configuration file
201+
// which usually doesn't change unless the installation changes. Maybe something keyed by the location of the
202+
// binary along with its fingerprint.
203+
opts.permissions.config = git::permissions::Config::all();
204+
opts.config_overrides([
205+
format!("gitoxide.http.connectTimeout={}", timeout.dur.as_millis()),
206+
format!("http.lowSpeedLimit={}", timeout.low_speed_limit),
207+
format!("http.lowSpeedTime={}", timeout.dur.as_secs()),
208+
])
209+
})
210+
.map_err(Into::into)
211+
}

src/cargo/sources/git/utils.rs

+80-32
Original file line numberDiff line numberDiff line change
@@ -776,7 +776,7 @@ pub fn with_fetch_options(
776776

777777
pub fn fetch(
778778
repo: &mut git2::Repository,
779-
url: &str,
779+
orig_url: &str,
780780
reference: &GitReference,
781781
config: &Config,
782782
) -> CargoResult<()> {
@@ -792,7 +792,7 @@ pub fn fetch(
792792

793793
// If we're fetching from GitHub, attempt GitHub's special fast path for
794794
// testing if we've already got an up-to-date copy of the repository
795-
let oid_to_fetch = match github_fast_path(repo, url, reference, config) {
795+
let oid_to_fetch = match github_fast_path(repo, orig_url, reference, config) {
796796
Ok(FastPathRev::UpToDate) => return Ok(()),
797797
Ok(FastPathRev::NeedsFetch(rev)) => Some(rev),
798798
Ok(FastPathRev::Indeterminate) => None,
@@ -854,20 +854,19 @@ pub fn fetch(
854854
// flavors of authentication possible while also still giving us all the
855855
// speed and portability of using `libgit2`.
856856
if let Some(true) = config.net_config()?.git_fetch_with_cli {
857-
return fetch_with_cli(repo, url, &refspecs, tags, config);
857+
return fetch_with_cli(repo, orig_url, &refspecs, tags, config);
858858
}
859859
if config
860860
.cli_unstable()
861861
.gitoxide
862862
.map_or(false, |git| git.fetch)
863863
{
864-
use git::remote::fetch::Error;
865864
use git_repository as git;
866865
let git2_repo = repo;
867866
oxide::with_retry_and_progress(
868867
&git2_repo.path().to_owned(),
869868
config,
870-
&mut |repo, should_interrupt, mut progress| {
869+
&mut |repo_path, should_interrupt, mut progress, url_for_authentication| {
871870
// The `fetch` operation here may fail spuriously due to a corrupt
872871
// repository. It could also fail, however, for a whole slew of other
873872
// reasons (aka network related reasons). We want Cargo to automatically
@@ -879,44 +878,93 @@ pub fn fetch(
879878
// again. If it looks like any other kind of error, or if we've already
880879
// blown away the repository, then we want to return the error as-is.
881880
let mut repo_reinitialized = false;
882-
let mut repo_storage;
883-
let mut repo = &*repo;
884881
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, &mut progress)?
893-
.prepare_fetch(git::remote::ref_map::Options::default())?
894-
.receive(should_interrupt);
882+
let repo = oxide::open_repo(repo_path, config);
883+
let res = match repo {
884+
Ok(repo) => {
885+
debug!("initiating fetch of {:?} from {}", refspecs, orig_url);
886+
({
887+
let url_for_authentication = &mut *url_for_authentication;
888+
|| -> CargoResult<_> {
889+
let remote = repo.remote_at(orig_url)?.with_refspecs(
890+
refspecs.iter().map(|s| s.as_str()),
891+
git::remote::Direction::Fetch,
892+
)?;
893+
let url = remote
894+
.url(git::remote::Direction::Fetch)
895+
.expect("set at init")
896+
.to_owned();
897+
let connection = remote
898+
.connect(git::remote::Direction::Fetch, &mut progress)?;
899+
let mut authenticate =
900+
connection.configured_credentials(url)?;
901+
let connection = connection.with_credentials(
902+
move |action: git::protocol::credentials::helper::Action| {
903+
if let Some(url) =
904+
action.context().and_then(|ctx| ctx.url.as_ref().filter(|url| *url != orig_url))
905+
{
906+
url_for_authentication(url.as_ref());
907+
}
908+
authenticate(action)
909+
},
910+
);
911+
let outcome = connection
912+
.prepare_fetch(git::remote::ref_map::Options::default())?
913+
.receive(should_interrupt)?;
914+
Ok(outcome)
915+
}
916+
})()
917+
}
918+
Err(err) => Err(err),
919+
};
895920
let err = match res {
896921
Ok(_) => break,
897922
Err(e) => e,
898923
};
899924
debug!("fetch failed: {}", err);
900925

901926
if !repo_reinitialized
902-
&& matches!(
903-
err,
904-
Error::Configuration { .. }
905-
| Error::IncompatibleObjectHash { .. }
906-
| Error::WritePack(_)
907-
| Error::UpdateRefs(_)
908-
| Error::RemovePackKeepFile { .. }
909-
)
927+
// We check for errors that could occour if the configuration, refs or odb files are corrupted.
928+
// We don't check for errors related to writing as `gitoxide` is expected to create missing leading
929+
// folder before writing files into it, or else not even open a directory as git repository (which is
930+
// also handled here).
931+
&& err.downcast_ref::<git::open::Error>().map_or(false, |err| {
932+
use git::open::Error;
933+
matches!(
934+
err,
935+
Error::NotARepository { .. }| Error::Config(_)
936+
)
937+
})
938+
|| err
939+
.downcast_ref::<git::remote::fetch::prepare::Error>()
940+
.map_or(false, |err| match err {
941+
git::remote::fetch::prepare::Error::RefMap(err) => {
942+
use git::remote::ref_map::Error;
943+
matches!(
944+
err,
945+
Error::ListRefs(_)
946+
| Error::GatherTransportConfig { .. }
947+
| Error::ConfigureCredentials(_)
948+
)
949+
}
950+
_ => false,
951+
})
952+
|| err
953+
.downcast_ref::<git::remote::fetch::Error>()
954+
.map_or(false, |err| {
955+
use git::remote::fetch::Error;
956+
matches!(
957+
err,
958+
Error::Configuration { .. } | Error::RemovePackKeepFile { .. }
959+
)
960+
})
910961
{
911962
repo_reinitialized = true;
912963
debug!(
913964
"looks like this is a corrupt repository, reinitializing \
914965
and trying again"
915966
);
916967
if reinitialize(git2_repo).is_ok() {
917-
repo_storage =
918-
git::open_opts(repo.path(), repo.open_options().to_owned())?;
919-
repo = &repo_storage;
920968
continue;
921969
}
922970
}
@@ -927,9 +975,9 @@ pub fn fetch(
927975
},
928976
)
929977
} else {
930-
debug!("doing a fetch for {}", url);
978+
debug!("doing a fetch for {}", orig_url);
931979
let git_config = git2::Config::open_default()?;
932-
with_fetch_options(&git_config, url, config, &mut |mut opts| {
980+
with_fetch_options(&git_config, orig_url, config, &mut |mut opts| {
933981
if tags {
934982
opts.download_tags(git2::AutotagOption::All);
935983
}
@@ -945,9 +993,9 @@ pub fn fetch(
945993
// blown away the repository, then we want to return the error as-is.
946994
let mut repo_reinitialized = false;
947995
loop {
948-
debug!("initiating fetch of {:?} from {}", refspecs, url);
996+
debug!("initiating fetch of {:?} from {}", refspecs, orig_url);
949997
let res = repo
950-
.remote_anonymous(url)?
998+
.remote_anonymous(orig_url)?
951999
.fetch(&refspecs, Some(&mut opts), None);
9521000
let err = match res {
9531001
Ok(()) => break,

0 commit comments

Comments
 (0)