feat: Support install directly from git repo (#1162)

Fixed #3

Signed-off-by: Jiahao XU <Jiahao_XU@outlook.com>
This commit is contained in:
Jiahao XU 2023-06-24 11:01:31 +10:00 committed by GitHub
parent dd35fba232
commit ca00cbaccc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1810 additions and 28 deletions

View file

@ -49,7 +49,9 @@ tracing-subscriber = { version = "0.3.17", features = ["fmt", "json", "ansi"], d
embed-resource = "2.1.1"
[features]
default = ["static", "rustls", "trust-dns", "fancy-no-backtrace", "zstd-thin"]
default = ["static", "rustls", "trust-dns", "fancy-no-backtrace", "zstd-thin", "git"]
git = ["binstalk/git"]
mimalloc = ["dep:mimalloc"]
@ -74,3 +76,6 @@ log_max_level_debug = ["log/max_level_debug", "tracing/max_level_debug", "log_re
log_release_max_level_info = ["log/release_max_level_info", "tracing/release_max_level_info"]
log_release_max_level_debug = ["log/release_max_level_debug", "tracing/release_max_level_debug"]
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]

View file

@ -38,7 +38,7 @@ pub struct Args {
/// install. The version syntax is as with the --version option.
///
/// When multiple names are provided, the --version option and override option
/// `manifest_path` is unavailable due to ambiguity.
/// `--manifest-path` and `--git` are unavailable due to ambiguity.
///
/// If duplicate names are provided, the last one (and their version requirement)
/// is kept.
@ -88,9 +88,21 @@ pub struct Args {
/// This skips searching crates.io for a manifest and uses the specified path directly, useful
/// for debugging and when adding Binstall support. This may be either the path to the folder
/// containing a Cargo.toml file, or the Cargo.toml file itself.
///
/// This option cannot be used with `--git`.
#[clap(help_heading = "Overrides", long)]
pub manifest_path: Option<PathBuf>,
#[cfg(feature = "git")]
/// Override how to fetch Cargo.toml package manifest.
///
/// This skip searching crates.io and instead clone the repository specified and
/// runs as if `--manifest-path $cloned_repo` is passed to binstall.
///
/// This option cannot be used with `--manifest-path`.
#[clap(help_heading = "Overrides", long)]
pub git: Option<binstalk::helpers::git::GitUrl>,
/// Override Cargo.toml package manifest bin-dir.
#[clap(help_heading = "Overrides", long)]
pub bin_dir: Option<String>,
@ -391,13 +403,36 @@ pub fn parse() -> Args {
// Ensure no conflict
let mut command = Args::command();
#[cfg(feature = "git")]
if opts.manifest_path.is_some() && opts.git.is_some() {
command
.error(
ErrorKind::ArgumentConflict,
format_args!(
r#"Multiple override options for Cargo.toml fetching.
You cannot use --manifest-path and --git. Do one or the other."#
),
)
.exit();
}
if opts.crate_names.len() > 1 {
let option = if opts.version_req.is_some() {
"version"
} else if opts.manifest_path.is_some() {
"manifest-path"
} else {
""
#[cfg(not(feature = "git"))]
{
""
}
#[cfg(feature = "git")]
if opts.git.is_some() {
"git"
} else {
""
}
};
if !option.is_empty() {

View file

@ -19,7 +19,7 @@ use binstalk::{
ops::{
self,
resolve::{CrateName, Resolution, ResolutionFetch, VersionReqExt},
Resolver,
CargoTomlFetchOverride, Options, Resolver,
},
};
use binstalk_manifests::cargo_toml_binstall::PkgOverride;
@ -95,7 +95,7 @@ pub fn install_crates(
let gh_api_client = GhApiClient::new(client.clone(), args.github_token);
// Create binstall_opts
let binstall_opts = Arc::new(ops::Options {
let binstall_opts = Arc::new(Options {
no_symlinks: args.no_symlinks,
dry_run: args.dry_run,
force: args.force,
@ -104,7 +104,16 @@ pub fn install_crates(
no_track: args.no_track,
version_req: args.version_req,
manifest_path: args.manifest_path,
#[cfg(feature = "git")]
cargo_toml_fetch_override: match (args.manifest_path, args.git) {
(Some(manifest_path), None) => Some(CargoTomlFetchOverride::Path(manifest_path)),
(None, Some(git_url)) => Some(CargoTomlFetchOverride::Git(git_url)),
(None, None) => None,
_ => unreachable!("manifest_path and git cannot be specified at the same time"),
},
#[cfg(not(feature = "git"))]
cargo_toml_fetch_override: args.manifest_path.map(CargoTomlFetchOverride::Path),
cli_overrides,
desired_targets,
@ -326,7 +335,7 @@ fn do_install_fetches(
resolution_fetchs: Vec<Box<ResolutionFetch>>,
// Take manifests by value to drop the `FileLock`.
manifests: Option<Manifests>,
binstall_opts: &ops::Options,
binstall_opts: &Options,
dry_run: bool,
temp_dir: tempfile::TempDir,
no_cleanup: bool,

View file

@ -1,3 +1,5 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
pub mod args;
pub mod bin_util;
pub mod entry;

View file

@ -18,6 +18,8 @@ command-group = { version = "2.1.0", features = ["with-tokio"] }
compact_str = { version = "0.7.0", features = ["serde"] }
detect-targets = { version = "0.1.7", path = "../detect-targets" }
either = "1.8.1"
gix = { version = "0.47.0", features = ["blocking-http-transport-reqwest-rust-tls"], optional = true }
glob = "0.3.1"
home = "0.5.5"
itertools = "0.11.0"
jobslot = { version = "0.2.11", features = ["tokio"] }
@ -43,7 +45,9 @@ xz2 = "0.1.7"
windows = { version = "0.48.0", features = ["Win32_Storage_FileSystem", "Win32_Foundation"] }
[features]
default = ["static", "rustls"]
default = ["static", "rustls", "git"]
git = ["dep:gix"]
static = ["binstalk-downloader/static"]
pkg-config = ["binstalk-downloader/pkg-config"]
@ -57,3 +61,6 @@ trust-dns = ["binstalk-downloader/trust-dns"]
zstd-thin = ["binstalk-downloader/zstd-thin"]
cross-lang-fat-lto = ["binstalk-downloader/cross-lang-fat-lto"]
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]

View file

@ -15,6 +15,8 @@ use thiserror::Error;
use tokio::task;
use tracing::{error, warn};
use crate::helpers::cargo_toml_workspace::LoadManifestFromWSError;
#[derive(Debug, Error)]
#[error("crates.io API error for {crate_name}: {err}")]
pub struct CratesIoApiError {
@ -323,6 +325,23 @@ pub enum BinstallError {
#[diagnostic(severity(error), code(binstall::target_triple_parse_error))]
TargetTripleParseError(#[source] Box<TargetTripleParseError>),
/// Failed to shallow clone git repository
///
/// - Code: `binstall::git`
/// - Exit: 98
#[cfg(feature = "git")]
#[error("Failed to shallow clone git repository: {0}")]
#[diagnostic(severity(error), code(binstall::git))]
GitError(#[from] crate::helpers::git::GitError),
/// Failed to load manifest from workspace
///
/// - Code: `binstall::load_manifest_from_workspace`
/// - Exit: 99
#[error(transparent)]
#[diagnostic(severity(error), code(binstall::load_manifest_from_workspace))]
LoadManifestFromWSError(#[from] Box<LoadManifestFromWSError>),
/// A wrapped error providing the context of which crate the error is about.
#[error(transparent)]
#[diagnostic(transparent)]
@ -358,6 +377,9 @@ impl BinstallError {
InvalidPkgFmt(..) => 95,
GhApiErr(..) => 96,
TargetTripleParseError(..) => 97,
#[cfg(feature = "git")]
GitError(_) => 98,
LoadManifestFromWSError(_) => 99,
CrateContext(context) => context.err.exit_number(),
};

View file

@ -1,4 +1,7 @@
pub mod cargo_toml_workspace;
pub mod futures_resolver;
#[cfg(feature = "git")]
pub mod git;
pub mod jobserver_client;
pub mod remote;
pub mod signal;

View file

@ -0,0 +1,263 @@
use std::{
io, mem,
path::{Path, PathBuf},
};
use cargo_toml::{Error as CargoTomlError, Manifest};
use compact_str::CompactString;
use glob::PatternError;
use normalize_path::NormalizePath;
use thiserror::Error as ThisError;
use tracing::{debug, instrument, warn};
use crate::{errors::BinstallError, manifests::cargo_toml_binstall::Meta};
/// Load binstall metadata `Cargo.toml` from workspace at the provided path
///
/// WARNING: This is a blocking operation.
///
/// * `workspace_path` - should be a directory
pub fn load_manifest_from_workspace(
workspace_path: impl AsRef<Path>,
crate_name: impl AsRef<str>,
) -> Result<Manifest<Meta>, BinstallError> {
fn inner(workspace_path: &Path, crate_name: &str) -> Result<Manifest<Meta>, BinstallError> {
load_manifest_from_workspace_inner(workspace_path, crate_name).map_err(|inner| {
Box::new(LoadManifestFromWSError {
workspace_path: workspace_path.into(),
crate_name: crate_name.into(),
inner,
})
.into()
})
}
inner(workspace_path.as_ref(), crate_name.as_ref())
}
#[derive(Debug, ThisError)]
#[error("Failed to load {crate_name} from {}: {inner}", workspace_path.display())]
pub struct LoadManifestFromWSError {
workspace_path: Box<Path>,
crate_name: CompactString,
#[source]
inner: LoadManifestFromWSErrorInner,
}
#[derive(Debug, ThisError)]
enum LoadManifestFromWSErrorInner {
#[error("Invalid pattern in workspace.members or workspace.exclude: {0}")]
PatternError(#[from] PatternError),
#[error("Invalid pattern `{0}`: It must be relative and point within current dir")]
InvalidPatternError(CompactString),
#[error("Failed to parse cargo manifest: {0}")]
CargoManifest(#[from] CargoTomlError),
#[error("I/O error: {0}")]
Io(#[from] io::Error),
#[error("Not found")]
NotFound,
}
#[instrument]
fn load_manifest_from_workspace_inner(
workspace_path: &Path,
crate_name: &str,
) -> Result<Manifest<Meta>, LoadManifestFromWSErrorInner> {
debug!(
"Loading manifest of crate {crate_name} from workspace: {}",
workspace_path.display()
);
let mut workspace_paths = vec![workspace_path.to_owned()];
while let Some(workspace_path) = workspace_paths.pop() {
let p = workspace_path.join("Cargo.toml");
let manifest = Manifest::<Meta>::from_path_with_metadata(&p)?;
let name = manifest.package.as_ref().map(|p| &*p.name);
debug!(
"Loading from {}, manifest.package.name = {:#?}",
p.display(),
name
);
if name == Some(crate_name) {
return Ok(manifest);
}
if let Some(ws) = manifest.workspace {
let excludes = ws.exclude;
let members = ws.members;
if members.is_empty() {
continue;
}
let exclude_patterns = excludes
.into_iter()
.map(|pat| Pattern::new(&pat))
.collect::<Result<Vec<_>, _>>()?;
for member in members {
for path in Pattern::new(&member)?.glob_dirs(&workspace_path)? {
if !exclude_patterns
.iter()
.any(|exclude| exclude.matches_with_trailing(&path))
{
workspace_paths.push(workspace_path.join(path));
}
}
}
}
}
Err(LoadManifestFromWSErrorInner::NotFound)
}
struct Pattern(Vec<glob::Pattern>);
impl Pattern {
fn new(pat: &str) -> Result<Self, LoadManifestFromWSErrorInner> {
Path::new(pat)
.try_normalize()
.ok_or_else(|| LoadManifestFromWSErrorInner::InvalidPatternError(pat.into()))?
.iter()
.map(|c| glob::Pattern::new(c.to_str().unwrap()))
.collect::<Result<Vec<_>, _>>()
.map_err(Into::into)
.map(Self)
}
/// * `glob_path` - path to dir to glob for
/// return paths relative to `glob_path`.
fn glob_dirs(&self, glob_path: &Path) -> Result<Vec<PathBuf>, LoadManifestFromWSErrorInner> {
let mut paths = vec![PathBuf::new()];
for pattern in &self.0 {
if paths.is_empty() {
break;
}
for path in mem::take(&mut paths) {
let p = glob_path.join(&path);
let res = p.read_dir();
if res.is_err() && !p.is_dir() {
continue;
}
drop(p);
for res in res? {
let entry = res?;
let is_dir = entry
.file_type()
.map(|file_type| file_type.is_dir() || file_type.is_symlink())
.unwrap_or(false);
if !is_dir {
continue;
}
let filename = entry.file_name();
if filename != "." // Ignore current dir
&& filename != ".." // Ignore parent dir
&& pattern.matches(&filename.to_string_lossy())
{
paths.push(path.join(filename));
}
}
}
}
Ok(paths)
}
/// Return `true` if `path` matches the pattern.
/// It will still return `true` even if there are some trailing components.
fn matches_with_trailing(&self, path: &Path) -> bool {
let mut iter = path.iter().map(|os_str| os_str.to_string_lossy());
for pattern in &self.0 {
match iter.next() {
Some(s) if pattern.matches(&s) => (),
_ => return false,
}
}
true
}
}
#[cfg(test)]
mod test {
use std::fs::create_dir_all as mkdir;
use tempfile::TempDir;
use super::*;
#[test]
fn test_glob_dirs() {
let pattern = Pattern::new("*/*/q/*").unwrap();
let tempdir = TempDir::new().unwrap();
mkdir(tempdir.as_ref().join("a/b/c/efe")).unwrap();
mkdir(tempdir.as_ref().join("a/b/q/ww")).unwrap();
mkdir(tempdir.as_ref().join("d/233/q/d")).unwrap();
let mut paths = pattern.glob_dirs(tempdir.as_ref()).unwrap();
paths.sort_unstable();
assert_eq!(
paths,
vec![PathBuf::from("a/b/q/ww"), PathBuf::from("d/233/q/d")]
);
}
#[test]
fn test_matches_with_trailing() {
let pattern = Pattern::new("*/*/q/*").unwrap();
assert!(pattern.matches_with_trailing(Path::new("a/b/q/d/")));
assert!(pattern.matches_with_trailing(Path::new("a/b/q/d")));
assert!(pattern.matches_with_trailing(Path::new("a/b/q/d/234")));
assert!(pattern.matches_with_trailing(Path::new("a/234/q/d/234")));
assert!(!pattern.matches_with_trailing(Path::new("")));
assert!(!pattern.matches_with_trailing(Path::new("a/")));
assert!(!pattern.matches_with_trailing(Path::new("a/234")));
assert!(!pattern.matches_with_trailing(Path::new("a/234/q")));
}
#[test]
fn test_load() {
let p = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("e2e-tests/manifests/workspace");
let manifest = load_manifest_from_workspace(&p, "cargo-binstall").unwrap();
let package = manifest.package.unwrap();
assert_eq!(package.name, "cargo-binstall");
assert_eq!(package.version.as_ref().unwrap(), "0.12.0");
assert_eq!(manifest.bin.len(), 1);
assert_eq!(manifest.bin[0].name.as_deref().unwrap(), "cargo-binstall");
assert_eq!(manifest.bin[0].path.as_deref().unwrap(), "src/main.rs");
let err = load_manifest_from_workspace_inner(&p, "cargo-binstall2").unwrap_err();
assert!(
matches!(err, LoadManifestFromWSErrorInner::NotFound),
"{:#?}",
err
);
let manifest = load_manifest_from_workspace(&p, "cargo-watch").unwrap();
let package = manifest.package.unwrap();
assert_eq!(package.name, "cargo-watch");
assert_eq!(package.version.as_ref().unwrap(), "8.4.0");
assert_eq!(manifest.bin.len(), 1);
assert_eq!(manifest.bin[0].name.as_deref().unwrap(), "cargo-watch");
}
}

View file

@ -0,0 +1,89 @@
use std::{num::NonZeroU32, path::Path, str::FromStr, sync::atomic::AtomicBool};
use compact_str::CompactString;
use gix::{clone, create, open, remote, Url};
use thiserror::Error as ThisError;
use tracing::debug;
mod progress_tracing;
use progress_tracing::TracingProgress;
#[derive(Debug, ThisError)]
#[non_exhaustive]
pub enum GitError {
#[error("Failed to prepare for fetch: {0}")]
PrepareFetchError(#[source] Box<clone::Error>),
#[error("Failed to fetch: {0}")]
FetchError(#[source] Box<clone::fetch::Error>),
#[error("Failed to checkout: {0}")]
CheckOutError(#[source] Box<clone::checkout::main_worktree::Error>),
}
impl From<clone::Error> for GitError {
fn from(e: clone::Error) -> Self {
Self::PrepareFetchError(Box::new(e))
}
}
impl From<clone::fetch::Error> for GitError {
fn from(e: clone::fetch::Error) -> Self {
Self::FetchError(Box::new(e))
}
}
impl From<clone::checkout::main_worktree::Error> for GitError {
fn from(e: clone::checkout::main_worktree::Error) -> Self {
Self::CheckOutError(Box::new(e))
}
}
#[derive(Clone, Debug)]
pub struct GitUrl(Url);
impl FromStr for GitUrl {
type Err = gix::url::parse::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Url::try_from(s).map(Self)
}
}
#[derive(Debug)]
pub struct Repository(gix::Repository);
impl Repository {
/// 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> {
let url_bstr = url.0.to_bstring();
let url_str = String::from_utf8_lossy(&url_bstr);
debug!("Shallow cloning {url_str} to {}", 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,
))
}
}

View file

@ -0,0 +1,144 @@
use std::{
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
time::Duration,
};
use compact_str::{format_compact, CompactString};
use gix::progress::{
prodash::messages::MessageLevel, Id, Progress, Step, StepShared, Unit, UNKNOWN,
};
use tokio::time;
use tracing::{error, info};
pub(super) struct TracingProgress {
name: CompactString,
id: Id,
max: Option<usize>,
unit: Option<Unit>,
step: usize,
trigger: Arc<AtomicBool>,
}
const EMIT_LOG_EVERY_S: f32 = 0.5;
const SEP: &str = "::";
impl TracingProgress {
/// Create a new instanCompactce from `name`.
pub fn new(name: CompactString) -> Self {
let trigger = Arc::new(AtomicBool::new(true));
tokio::spawn({
let mut interval = time::interval(Duration::from_secs_f32(EMIT_LOG_EVERY_S));
interval.set_missed_tick_behavior(time::MissedTickBehavior::Skip);
let trigger = Arc::clone(&trigger);
async move {
while Arc::strong_count(&trigger) > 1 {
trigger.store(true, Ordering::Relaxed);
interval.tick().await;
}
}
});
Self {
name,
id: UNKNOWN,
max: None,
step: 0,
unit: None,
trigger,
}
}
}
impl Progress for TracingProgress {
type SubProgress = TracingProgress;
fn add_child(&mut self, name: impl Into<String>) -> Self::SubProgress {
self.add_child_with_id(name, UNKNOWN)
}
fn add_child_with_id(&mut self, name: impl Into<String>, id: Id) -> Self::SubProgress {
Self {
name: format_compact!("{}{}{}", self.name, SEP, Into::<String>::into(name)),
id,
step: 0,
max: None,
unit: None,
trigger: Arc::clone(&self.trigger),
}
}
fn init(&mut self, max: Option<usize>, unit: Option<Unit>) {
self.max = max;
self.unit = unit;
}
fn set(&mut self, step: usize) {
self.step = step;
if self.trigger.swap(false, Ordering::Relaxed) {
match (self.max, &self.unit) {
(max, Some(unit)) => {
info!("{} → {}", self.name, unit.display(step, max, None))
}
(Some(max), None) => info!("{} → {} / {}", self.name, step, max),
(None, None) => info!("{} → {}", self.name, step),
}
}
}
fn unit(&self) -> Option<Unit> {
self.unit.clone()
}
fn max(&self) -> Option<usize> {
self.max
}
fn set_max(&mut self, max: Option<Step>) -> Option<Step> {
let prev = self.max;
self.max = max;
prev
}
fn step(&self) -> usize {
self.step
}
fn inc_by(&mut self, step: usize) {
self.set(self.step + step)
}
fn set_name(&mut self, name: impl Into<String>) {
let name = name.into();
self.name = self
.name
.split("::")
.next()
.map(|parent| format_compact!("{}{}{}", parent.to_owned(), SEP, name))
.unwrap_or_else(|| name.into());
}
fn name(&self) -> Option<String> {
self.name.split(SEP).nth(1).map(ToOwned::to_owned)
}
fn id(&self) -> Id {
self.id
}
fn message(&self, level: MessageLevel, message: impl Into<String>) {
let message: String = message.into();
match level {
MessageLevel::Info => info!("{} → {}", self.name, message),
MessageLevel::Failure => error!("𐄂{} → {}", self.name, message),
MessageLevel::Success => info!("✓{} → {}", self.name, message),
}
}
fn counter(&self) -> Option<StepShared> {
None
}
}

View file

@ -1,3 +1,5 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
pub mod bins;
pub mod drivers;
pub mod errors;

View file

@ -10,7 +10,9 @@ use tokio::{
use crate::{
fetchers::{Data, Fetcher, TargetData},
helpers::{gh_api_client::GhApiClient, jobserver_client::LazyJobserverClient, remote::Client},
helpers::{
self, gh_api_client::GhApiClient, jobserver_client::LazyJobserverClient, remote::Client,
},
manifests::cargo_toml_binstall::PkgOverride,
DesiredTargets,
};
@ -19,6 +21,13 @@ pub mod resolve;
pub type Resolver = fn(Client, GhApiClient, Arc<Data>, Arc<TargetData>) -> Arc<dyn Fetcher>;
#[non_exhaustive]
pub enum CargoTomlFetchOverride {
#[cfg(feature = "git")]
Git(helpers::git::GitUrl),
Path(PathBuf),
}
pub struct Options {
pub no_symlinks: bool,
pub dry_run: bool,
@ -28,7 +37,7 @@ pub struct Options {
pub no_track: bool,
pub version_req: Option<VersionReq>,
pub manifest_path: Option<PathBuf>,
pub cargo_toml_fetch_override: Option<CargoTomlFetchOverride>,
pub cli_overrides: PkgOverride,
pub desired_targets: DesiredTargets,

View file

@ -13,17 +13,18 @@ use itertools::Itertools;
use leon::Template;
use maybe_owned::MaybeOwned;
use semver::{Version, VersionReq};
use tokio::task::block_in_place;
use tempfile::TempDir;
use tokio::task::{block_in_place, spawn_blocking};
use tracing::{debug, info, instrument, warn};
use super::Options;
use crate::{
bins,
drivers::fetch_crate_cratesio,
errors::{BinstallError, VersionParseError},
fetchers::{Data, Fetcher, TargetData},
helpers::{download::ExtractedFiles, remote::Client, target_triple::TargetTriple},
helpers::{self, download::ExtractedFiles, remote::Client, target_triple::TargetTriple},
manifests::cargo_toml_binstall::{Meta, PkgMeta, PkgOverride},
ops::{CargoTomlFetchOverride, Options},
};
mod crate_name;
@ -359,9 +360,24 @@ impl PackageInfo {
version_req: &VersionReq,
client: Client,
) -> Result<Option<Self>, BinstallError> {
use CargoTomlFetchOverride::*;
// Fetch crate via crates.io, git, or use a local manifest path
let manifest = match opts.manifest_path.as_ref() {
Some(manifest_path) => load_manifest_path(manifest_path)?,
let manifest = match opts.cargo_toml_fetch_override.as_ref() {
Some(Path(manifest_path)) => load_manifest_path(manifest_path)?,
#[cfg(feature = "git")]
Some(Git(git_url)) => {
let git_url = git_url.clone();
let name = name.clone();
spawn_blocking(move || {
let dir = TempDir::new()?;
helpers::git::Repository::shallow_clone(git_url, dir.as_ref())?;
helpers::cargo_toml_workspace::load_manifest_from_workspace(dir.as_ref(), &name)
})
.await??
}
None => {
Box::pin(fetch_crate_cratesio(
client,

View file

@ -29,6 +29,11 @@ pub trait NormalizePath {
/// However, this does not resolve links.
fn normalize(&self) -> PathBuf;
/// Same as [`NormalizePath::normalize`] except that if
/// `Component::Prefix`/`Component::RootDir` is encountered,
/// or if the path points outside of current dir, returns `None`.
fn try_normalize(&self) -> Option<PathBuf>;
/// Return `true` if the path is normalized.
///
/// # Quirk
@ -68,6 +73,27 @@ impl NormalizePath for Path {
ret
}
fn try_normalize(&self) -> Option<PathBuf> {
let mut ret = PathBuf::new();
for component in self.components() {
match component {
Component::Prefix(..) | Component::RootDir => return None,
Component::CurDir => {}
Component::ParentDir => {
if !ret.pop() {
return None;
}
}
Component::Normal(c) => {
ret.push(c);
}
}
}
Some(ret)
}
fn is_normalized(&self) -> bool {
for component in self.components() {
match component {