use crate::sources::IndexSummary;
use crate::sources::RecursivePathSource;
use crate::sources::git::utils::GitDatabase;
use crate::sources::git::utils::GitRemote;
use crate::sources::git::utils::rev_to_oid;
use crate::sources::source::MaybePackage;
use crate::sources::source::QueryKind;
use crate::sources::source::Source;
use crate::util::GlobalContext;
use crate::util::cache_lock::CacheLockMode;
use crate::util::errors::CargoResult;
use crate::util::hex::short_hash;
use crate::util::interning::InternedString;
use crate::workspace::GitReference;
use crate::workspace::SourceId;
use crate::workspace::global_cache_tracker;
use crate::workspace::{Dependency, Package, PackageId};
use anyhow::Context as _;
use cargo_util::paths::exclude_from_backups_and_indexing;
use std::cell::RefCell;
use std::fmt::{self, Debug, Formatter};
use tracing::trace;
use url::Url;
pub struct GitSource<'gctx> {
remote: GitRemote,
locked_rev: RefCell<Revision>,
source_id: RefCell<SourceId>,
path_source: RefCell<Option<RecursivePathSource<'gctx>>>,
short_id: RefCell<Option<InternedString>>,
ident: InternedString,
gctx: &'gctx GlobalContext,
quiet: bool,
}
impl<'gctx> GitSource<'gctx> {
pub fn new(source_id: SourceId, gctx: &'gctx GlobalContext) -> CargoResult<GitSource<'gctx>> {
let remote = GitRemote::new(source_id.url());
Self::new_with_remote(source_id, remote, gctx)
}
pub(super) fn new_for_submodule(
source_id: SourceId,
fetch_url: String,
gctx: &'gctx GlobalContext,
) -> CargoResult<GitSource<'gctx>> {
let remote = GitRemote::new_from_str(fetch_url);
Self::new_with_remote(source_id, remote, gctx)
}
fn new_with_remote(
source_id: SourceId,
remote: GitRemote,
gctx: &'gctx GlobalContext,
) -> CargoResult<GitSource<'gctx>> {
assert!(source_id.is_git(), "id is not git, id={}", source_id);
let locked_rev = source_id
.precise_git_fragment()
.map(|s| Revision::new(s.into()))
.unwrap_or_else(|| source_id.git_reference().unwrap().clone().into());
let ident = ident_shallow(
&source_id,
gctx.cli_unstable()
.git
.map_or(false, |features| features.shallow_deps),
);
let source = GitSource {
remote,
locked_rev: RefCell::new(locked_rev),
source_id: RefCell::new(source_id),
path_source: RefCell::new(None),
short_id: RefCell::new(None),
ident: ident.into(),
gctx,
quiet: false,
};
Ok(source)
}
pub fn url(&self) -> Url {
self.source_id.borrow().url().clone()
}
pub fn read_packages(&self) -> CargoResult<Vec<Package>> {
if self.path_source.borrow().is_none() {
self.invalidate_cache();
self.update()?;
}
self.path_source.borrow().as_ref().unwrap().read_packages()
}
fn mark_used(&self) -> CargoResult<()> {
self.gctx
.deferred_global_last_use()?
.mark_git_checkout_used(global_cache_tracker::GitCheckout {
encoded_git_name: self.ident,
short_name: self.short_id.borrow().expect("update before download"),
size: None,
});
Ok(())
}
pub(crate) fn fetch_db(&self, is_submodule: bool) -> CargoResult<(GitDatabase, git2::Oid)> {
let db_path = self.gctx.git_db_path().join(&self.ident);
let db_path = db_path.into_path_unlocked();
let db = self.remote.db_at(&db_path).ok();
let (db, actual_rev) = match (&*self.locked_rev.borrow(), db) {
(Revision::Locked(oid), Some(db)) if db.contains(*oid) => (db, *oid),
(Revision::Deferred(git_ref), Some(db)) if !self.gctx.network_allowed() => {
let offline_flag = self
.gctx
.offline_flag()
.expect("always present when `!network_allowed`");
let rev = db.resolve(&git_ref).with_context(|| {
format!(
"failed to lookup reference in preexisting repository, and \
can't check for updates in offline mode ({offline_flag})"
)
})?;
(db, rev)
}
(locked_rev, db) => {
if let Some(offline_flag) = self.gctx.offline_flag() {
anyhow::bail!(
"can't checkout from '{}': you are in the offline mode ({offline_flag})",
self.remote.url()
);
}
if !self.quiet {
let scope = if is_submodule {
"submodule"
} else {
"repository"
};
self.gctx
.shell()
.status("Updating", format!("git {scope} `{}`", self.remote.url()))?;
}
trace!("updating git source `{:?}`", self.remote);
let locked_rev = locked_rev.clone().into();
let manifest_reference = self.source_id.borrow().git_reference().unwrap();
self.remote
.checkout(&db_path, db, manifest_reference, &locked_rev, self.gctx)?
}
};
Ok((db, actual_rev))
}
fn update(&self) -> CargoResult<()> {
if self.path_source.borrow().is_some() {
self.mark_used()?;
return Ok(());
}
let git_fs = self.gctx.git_path();
let _ = git_fs.create_dir();
let git_path = self
.gctx
.assert_package_cache_locked(CacheLockMode::DownloadExclusive, &git_fs);
exclude_from_backups_and_indexing(&git_path);
let (db, actual_rev) = self.fetch_db(false)?;
let short_id = db.to_short_id(actual_rev)?;
let checkout_path = self
.gctx
.git_checkouts_path()
.join(&self.ident)
.join(short_id.as_str());
let checkout_path = checkout_path.into_path_unlocked();
db.copy_to(actual_rev, &checkout_path, self.gctx, self.quiet)?;
let source_id = self
.source_id
.borrow()
.with_git_precise(Some(actual_rev.to_string()));
let path_source = RecursivePathSource::new(&checkout_path, source_id, self.gctx);
self.path_source.replace(Some(path_source));
self.short_id.replace(Some(short_id.as_str().into()));
self.locked_rev.replace(Revision::Locked(actual_rev));
self.path_source.borrow().as_ref().unwrap().load()?;
self.mark_used()?;
Ok(())
}
}
#[derive(Clone, Debug)]
enum Revision {
Deferred(GitReference),
Locked(git2::Oid),
}
impl Revision {
fn new(rev: &str) -> Revision {
match rev_to_oid(rev) {
Some(oid) => Revision::Locked(oid),
None => Revision::Deferred(GitReference::Rev(rev.to_string())),
}
}
}
impl From<GitReference> for Revision {
fn from(value: GitReference) -> Self {
Revision::Deferred(value)
}
}
impl From<Revision> for GitReference {
fn from(value: Revision) -> Self {
match value {
Revision::Deferred(git_ref) => git_ref,
Revision::Locked(oid) => GitReference::Rev(oid.to_string()),
}
}
}
fn ident(id: &SourceId) -> String {
let ident = id
.canonical_url()
.raw_canonicalized_url()
.path_segments()
.and_then(|s| s.rev().next())
.unwrap_or("");
let ident = if ident.is_empty() { "_empty" } else { ident };
format!("{}-{}", ident, short_hash(id.canonical_url()))
}
fn ident_shallow(id: &SourceId, is_shallow: bool) -> String {
let mut ident = ident(id);
if is_shallow {
ident.push_str("-shallow");
}
ident
}
impl<'gctx> Debug for GitSource<'gctx> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "git repo at {}", self.source_id.borrow().url())?;
match &*self.locked_rev.borrow() {
Revision::Deferred(git_ref) => match git_ref.pretty_ref(true) {
Some(s) => write!(f, " ({})", s),
None => Ok(()),
},
Revision::Locked(oid) => write!(f, " ({oid})"),
}
}
}
#[async_trait::async_trait(?Send)]
impl<'gctx> Source for GitSource<'gctx> {
async fn query(
&self,
dep: &Dependency,
kind: QueryKind,
f: &mut dyn FnMut(IndexSummary),
) -> CargoResult<()> {
if self.path_source.borrow().is_none() {
self.update()?;
}
let src = self.path_source.borrow();
let src = src.as_ref().unwrap();
src.query(dep, kind, f).await
}
fn supports_checksums(&self) -> bool {
false
}
fn requires_precise(&self) -> bool {
true
}
fn source_id(&self) -> SourceId {
*self.source_id.borrow()
}
async fn download(&self, id: PackageId) -> CargoResult<MaybePackage> {
trace!(
"getting packages for package ID `{}` from `{:?}`",
id, self.remote
);
self.mark_used()?;
self.path_source
.borrow_mut()
.as_mut()
.expect("BUG: `update()` must be called before `get()`")
.download(id)
.await
}
async fn finish_download(&self, _id: PackageId, _data: Vec<u8>) -> CargoResult<Package> {
panic!("no download should have started")
}
fn fingerprint(&self, _pkg: &Package) -> CargoResult<String> {
match &*self.locked_rev.borrow() {
Revision::Locked(oid) => Ok(oid.to_string()),
_ => unreachable!("locked_rev must be resolved when computing fingerprint"),
}
}
fn describe(&self) -> String {
format!("Git repository {}", self.source_id.borrow())
}
fn invalidate_cache(&self) {}
fn set_quiet(&mut self, quiet: bool) {
self.quiet = quiet;
}
}
#[cfg(test)]
mod test {
use super::ident;
use crate::util::IntoUrl;
use crate::workspace::{GitReference, SourceId};
#[test]
pub fn test_url_to_path_ident_with_path() {
let ident = ident(&src("https://github.com/carlhuda/cargo"));
assert!(ident.starts_with("cargo-"));
}
#[test]
pub fn test_url_to_path_ident_without_path() {
let ident = ident(&src("https://github.com"));
assert!(ident.starts_with("_empty-"));
}
#[test]
fn test_canonicalize_idents_by_stripping_trailing_url_slash() {
let ident1 = ident(&src("https://github.com/PistonDevelopers/piston/"));
let ident2 = ident(&src("https://github.com/PistonDevelopers/piston"));
assert_eq!(ident1, ident2);
}
#[test]
fn test_canonicalize_idents_by_lowercasing_github_urls() {
let ident1 = ident(&src("https://github.com/PistonDevelopers/piston"));
let ident2 = ident(&src("https://github.com/pistondevelopers/piston"));
assert_eq!(ident1, ident2);
}
#[test]
fn test_canonicalize_idents_by_stripping_dot_git() {
let ident1 = ident(&src("https://github.com/PistonDevelopers/piston"));
let ident2 = ident(&src("https://github.com/PistonDevelopers/piston.git"));
assert_eq!(ident1, ident2);
}
#[test]
fn test_canonicalize_idents_different_protocols() {
let ident1 = ident(&src("https://github.com/PistonDevelopers/piston"));
let ident2 = ident(&src("git://github.com/PistonDevelopers/piston"));
assert_eq!(ident1, ident2);
}
#[test]
fn test_canonicalize_idents_does_not_strip_dot_git_for_sparse() {
let ident1 = ident(&src("sparse+https://crates.io/fake-registry"));
let ident2 = ident(&src("sparse+https://crates.io/fake-registry.git"));
assert_ne!(ident1, ident2);
}
fn src(s: &str) -> SourceId {
SourceId::for_git(&s.into_url().unwrap(), GitReference::DefaultBranch).unwrap()
}
}