Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

avoid worktree checkouts #1207

Merged
merged 6 commits into from
Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 30 additions & 28 deletions crates/binstalk/src/drivers/registry/git_registry.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
use std::{
fs::File,
io::{self, BufReader, Read},
path::PathBuf,
sync::Arc,
};
use std::{io, path::PathBuf, sync::Arc};

use cargo_toml::Manifest;
use compact_str::{CompactString, ToCompactString};
Expand All @@ -29,23 +24,33 @@ use crate::{

#[derive(Debug)]
struct GitIndex {
path: TempDir,
_tempdir: TempDir,
repo: Repository,
dl_template: CompactString,
}

impl GitIndex {
fn new(url: GitUrl) -> Result<Self, BinstallError> {
let tempdir = TempDir::new()?;

Repository::shallow_clone(url, tempdir.as_ref())?;
let repo = Repository::shallow_clone_bare(url.clone(), tempdir.as_ref())?;

let mut v = Vec::with_capacity(100);
File::open(tempdir.as_ref().join("config.json"))?.read_to_end(&mut v)?;
let config: RegistryConfig = {
let config = repo
.get_head_commit_entry_data_by_path("config.json")?
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("config.json not found in repository `{url}`"),
)
})?;

let config: RegistryConfig = json_from_slice(&v).map_err(RegistryError::from)?;
json_from_slice(&config).map_err(RegistryError::from)?
};

Ok(Self {
path: tempdir,
_tempdir: tempdir,
repo,
dl_template: config.dl,
})
}
Expand All @@ -70,27 +75,24 @@ impl GitRegistry {

/// WARNING: This is a blocking operation.
fn find_crate_matched_ver(
mut path: PathBuf,
repo: &Repository,
crate_name: &str,
(c1, c2): &(CompactString, Option<CompactString>),
version_req: &VersionReq,
) -> Result<MatchedVersion, BinstallError> {
let mut path = PathBuf::with_capacity(128);
path.push(&**c1);
if let Some(c2) = c2 {
path.push(&**c2);
}

path.push(&*crate_name.to_lowercase());

let f = File::open(path)
.map_err(|e| match e.kind() {
io::ErrorKind::NotFound => RegistryError::NotFound(crate_name.into()).into(),
_ => BinstallError::from(e),
})
.map(BufReader::new)?;
let crate_versions = repo
.get_head_commit_entry_data_by_path(path)?
.ok_or_else(|| RegistryError::NotFound(crate_name.into()))?;

MatchedVersion::find(
&mut JsonDeserializer::from_reader(f).into_iter(),
&mut JsonDeserializer::from_slice(&crate_versions).into_iter(),
version_req,
)
}
Expand All @@ -107,17 +109,17 @@ impl GitRegistry {
let this = self.clone();

let (version, dl_url) = spawn_blocking(move || {
let GitIndex { path, dl_template } = this
let GitIndex {
_tempdir: _,
repo,
dl_template,
} = this
.0
.git_index
.get_or_try_init(|| GitIndex::new(this.0.url.clone()))?;

let MatchedVersion { version, cksum } = Self::find_crate_matched_ver(
path.as_ref().to_owned(),
&crate_name,
&crate_prefix,
&version_req,
)?;
let MatchedVersion { version, cksum } =
Self::find_crate_matched_ver(repo, &crate_name, &crate_prefix, &version_req)?;

let url = Url::parse(&render_dl_template(
dl_template,
Expand Down
130 changes: 107 additions & 23 deletions crates/binstalk/src/helpers/git.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{num::NonZeroU32, path::Path, str::FromStr, sync::atomic::AtomicBool};
use std::{fmt, mem, num::NonZeroU32, path::Path, str::FromStr, sync::atomic::AtomicBool};

use compact_str::CompactString;
use gix::{clone, create, open, remote, Url};
Expand All @@ -21,6 +21,15 @@ pub enum GitError {

#[error("Failed to checkout: {0}")]
CheckOutError(#[source] Box<clone::checkout::main_worktree::Error>),

#[error("HEAD ref was corrupt in crates-io index repository clone")]
HeadCommit(#[source] Box<gix::reference::head_commit::Error>),

#[error("tree of head commit wasn't present in crates-io index repository clone")]
GetTreeOfCommit(#[source] Box<gix::object::commit::Error>),

#[error("An object was missing in the crates-io index repository clone")]
ObjectLookup(#[source] Box<gix::object::find::existing::Error>),
}

impl From<clone::Error> for GitError {
Expand All @@ -41,9 +50,36 @@ impl From<clone::checkout::main_worktree::Error> for GitError {
}
}

impl From<gix::reference::head_commit::Error> for GitError {
fn from(e: gix::reference::head_commit::Error) -> Self {
Self::HeadCommit(Box::new(e))
}
}

impl From<gix::object::commit::Error> for GitError {
fn from(e: gix::object::commit::Error) -> Self {
Self::GetTreeOfCommit(Box::new(e))
}
}

impl From<gix::object::find::existing::Error> for GitError {
fn from(e: gix::object::find::existing::Error) -> Self {
Self::ObjectLookup(Box::new(e))
}
}

#[derive(Clone, Debug)]
pub struct GitUrl(Url);

impl fmt::Display for GitUrl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let url_bstr = self.0.to_bstring();
let url_str = String::from_utf8_lossy(&url_bstr);

f.write_str(&url_str)
}
}

impl FromStr for GitUrl {
type Err = GitUrlParseError;

Expand All @@ -53,39 +89,87 @@ impl FromStr for GitUrl {
}

#[derive(Debug)]
pub struct Repository(gix::Repository);
pub struct Repository(gix::ThreadSafeRepository);

impl Repository {
fn prepare_fetch(
url: GitUrl,
path: &Path,
kind: create::Kind,
) -> Result<clone::PrepareFetch, GitError> {
Ok(clone::PrepareFetch::new(
url.0,
path,
kind,
Byron marked this conversation as resolved.
Show resolved Hide resolved
create::Options {
destination_must_be_empty: true,
..Default::default()
},
open::Options::isolated(),
)?
.with_shallow(remote::fetch::Shallow::DepthAtRemote(
NonZeroU32::new(1).unwrap(),
)))
}

/// WARNING: This is a blocking operation, if you want to use it in
NobodyXu marked this conversation as resolved.
Show resolved Hide resolved
/// async context then you must wrap the call in [`tokio::task::spawn_blocking`].
///
/// WARNING: This function must be called after tokio runtime is initialized.
pub fn shallow_clone(url: GitUrl, path: &Path) -> Result<Self, GitError> {
let url_bstr = url.0.to_bstring();
let url_str = String::from_utf8_lossy(&url_bstr);
pub fn shallow_clone_bare(url: GitUrl, path: &Path) -> Result<Self, GitError> {
debug!("Shallow cloning {url} to {}", path.display());

debug!("Shallow cloning {url_str} to {}", path.display());
Ok(Self(
Self::prepare_fetch(url, path, create::Kind::Bare)?
.fetch_only(
&mut TracingProgress::new(CompactString::new("Cloning")),
&AtomicBool::new(false),
)?
.0
.into(),
))
}

/// WARNING: This is a blocking operation, if you want to use it in
/// async context then you must wrap the call in [`tokio::task::spawn_blocking`].
///
/// WARNING: This function must be called after tokio runtime is initialized.
pub fn shallow_clone(url: GitUrl, path: &Path) -> Result<Self, GitError> {
debug!("Shallow cloning {url} to {} with worktree", path.display());

let mut progress = TracingProgress::new(CompactString::new("Cloning"));

Ok(Self(
clone::PrepareFetch::new(
url.0,
path,
create::Kind::WithWorktree,
create::Options {
destination_must_be_empty: true,
..Default::default()
},
open::Options::isolated(),
)?
.with_shallow(remote::fetch::Shallow::DepthAtRemote(
NonZeroU32::new(1).unwrap(),
))
.fetch_then_checkout(&mut progress, &AtomicBool::new(false))?
.0
.main_worktree(&mut progress, &AtomicBool::new(false))?
.0,
Self::prepare_fetch(url, path, create::Kind::WithWorktree)?
.fetch_then_checkout(&mut progress, &AtomicBool::new(false))?
.0
.main_worktree(&mut progress, &AtomicBool::new(false))?
.0
.into(),
))
}
NobodyXu marked this conversation as resolved.
Show resolved Hide resolved

#[inline(always)]
pub fn get_head_commit_entry_data_by_path(
&self,
path: impl AsRef<Path>,
) -> Result<Option<Vec<u8>>, GitError> {
fn inner(this: &Repository, path: &Path) -> Result<Option<Vec<u8>>, GitError> {
Ok(
if let Some(entry) = this
.0
.to_thread_local()
.head_commit()?
NobodyXu marked this conversation as resolved.
Show resolved Hide resolved
.tree()?
.lookup_entry_by_path(path)?
{
Some(mem::take(&mut entry.object()?.data))
NobodyXu marked this conversation as resolved.
Show resolved Hide resolved
} else {
None
},
)
}

inner(self, path.as_ref())
}
}