diff --git a/registry/src/endpoints/package_version.rs b/registry/src/endpoints/package_version.rs index d991ec1..bd161ae 100644 --- a/registry/src/endpoints/package_version.rs +++ b/registry/src/endpoints/package_version.rs @@ -72,7 +72,7 @@ pub async fn get_package_version( let versions: IndexFile = { let source = app_state.source.lock().unwrap(); - match source.read_file([scope, name_part], &app_state.project)? { + match source.read_file([scope, name_part], &app_state.project, None)? { Some(versions) => toml::de::from_str(&versions)?, None => return Ok(HttpResponse::NotFound().finish()), } diff --git a/registry/src/endpoints/package_versions.rs b/registry/src/endpoints/package_versions.rs index 82dc7c3..e48a113 100644 --- a/registry/src/endpoints/package_versions.rs +++ b/registry/src/endpoints/package_versions.rs @@ -15,10 +15,11 @@ pub async fn get_package_versions( let (scope, name_part) = name.as_str(); let source = app_state.source.lock().unwrap(); - let versions: IndexFile = match source.read_file([scope, name_part], &app_state.project)? { - Some(versions) => toml::de::from_str(&versions)?, - None => return Ok(HttpResponse::NotFound().finish()), - }; + let versions: IndexFile = + match source.read_file([scope, name_part], &app_state.project, None)? { + Some(versions) => toml::de::from_str(&versions)?, + None => return Ok(HttpResponse::NotFound().finish()), + }; Ok(HttpResponse::Ok().json( versions diff --git a/registry/src/endpoints/publish_version.rs b/registry/src/endpoints/publish_version.rs index 55373e0..2e025d7 100644 --- a/registry/src/endpoints/publish_version.rs +++ b/registry/src/endpoints/publish_version.rs @@ -142,7 +142,7 @@ pub async fn publish_package( let (dep_scope, dep_name) = specifier.name.as_str(); if source - .read_file([dep_scope, dep_name], &app_state.project)? + .read_file([dep_scope, dep_name], &app_state.project, None)? .is_none() { return Err(Error::InvalidArchive); @@ -153,6 +153,11 @@ pub async fn publish_package( return Err(Error::InvalidArchive); } } + DependencySpecifiers::Git(_) => { + if !config.git_allowed { + return Err(Error::InvalidArchive); + } + } }; } @@ -161,7 +166,7 @@ pub async fn publish_package( let (scope, name) = manifest.name.as_str(); let mut oids = vec![]; - match source.read_file([scope, SCOPE_INFO_FILE], &app_state.project)? { + match source.read_file([scope, SCOPE_INFO_FILE], &app_state.project, None)? { Some(info) => { let info: ScopeInfo = toml::de::from_str(&info)?; if !info.owners.contains(&user_id.0) { @@ -181,7 +186,7 @@ pub async fn publish_package( let mut entries: IndexFile = toml::de::from_str( &source - .read_file([scope, name], &app_state.project)? + .read_file([scope, name], &app_state.project, None)? .unwrap_or_default(), )?; diff --git a/registry/src/endpoints/search.rs b/registry/src/endpoints/search.rs index cf8c4bd..8dfe3ef 100644 --- a/registry/src/endpoints/search.rs +++ b/registry/src/endpoints/search.rs @@ -73,7 +73,7 @@ pub async fn search_packages( let mut versions: IndexFile = toml::de::from_str( &source - .read_file([scope, name], &app_state.project) + .read_file([scope, name], &app_state.project, None) .unwrap() .unwrap(), ) diff --git a/src/cli/commands/add.rs b/src/cli/commands/add.rs index c0997ba..02e01e8 100644 --- a/src/cli/commands/add.rs +++ b/src/cli/commands/add.rs @@ -181,6 +181,9 @@ impl AddCommand { dependency_key ); } + DependencySpecifiers::Git(_) => { + unreachable!("git dependencies are not supported in the add command"); + } } project diff --git a/src/cli/commands/outdated.rs b/src/cli/commands/outdated.rs index 07cb6ce..87f885c 100644 --- a/src/cli/commands/outdated.rs +++ b/src/cli/commands/outdated.rs @@ -35,6 +35,10 @@ impl OutdatedCommand { continue; }; + if matches!(specifier, DependencySpecifiers::Git(_)) { + continue; + } + let source = node.node.pkg_ref.source(); if refreshed_sources.insert(source.clone()) { @@ -50,6 +54,7 @@ impl OutdatedCommand { DependencySpecifiers::Wally(ref mut spec) => { spec.version = VersionReq::STAR; } + DependencySpecifiers::Git(_) => {} }; } diff --git a/src/download.rs b/src/download.rs index a9865fd..50e01f1 100644 --- a/src/download.rs +++ b/src/download.rs @@ -71,7 +71,7 @@ impl Project { let (fs, target) = match source.download(&node.pkg_ref, &project, &reqwest) { Ok(target) => target, Err(e) => { - tx.send(Err(e.into())).unwrap(); + tx.send(Err(Box::new(e).into())).unwrap(); return; } }; @@ -124,7 +124,7 @@ pub mod errors { /// Error downloading a package #[error("failed to download package")] - DownloadFailed(#[from] crate::source::errors::DownloadError), + DownloadFailed(#[from] Box), /// Error writing package contents #[error("failed to write package contents")] diff --git a/src/linking/mod.rs b/src/linking/mod.rs index 4706214..03e3a7b 100644 --- a/src/linking/mod.rs +++ b/src/linking/mod.rs @@ -88,7 +88,7 @@ impl Project { #[cfg(feature = "roblox")] if let Some(Target::Roblox { build_files, .. }) = - Some(&node.target).filter(|_| !node.node.pkg_ref.is_wally()) + Some(&node.target).filter(|_| !node.node.pkg_ref.like_wally()) { let script_name = ScriptName::RobloxSyncConfigGenerator.to_string(); diff --git a/src/resolver.rs b/src/resolver.rs index a5352b0..ccb3787 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -4,6 +4,7 @@ use crate::{ names::PackageNames, source::{ pesde::PesdePackageSource, + refs::PackageRefs, specifiers::DependencySpecifiers, traits::{PackageRef, PackageSource}, version_id::VersionId, @@ -176,6 +177,9 @@ impl Project { PackageSources::Wally(crate::source::wally::WallyPackageSource::new(index_url)) } + DependencySpecifiers::Git(specifier) => PackageSources::Git( + crate::source::git::GitPackageSource::new(specifier.repo.clone()), + ), }; if refreshed_sources.insert(source.clone()) { @@ -219,6 +223,8 @@ impl Project { }); } + let pkg_ref = &resolved[&target_version_id]; + if let Some(already_resolved) = graph .get_mut(&name) .and_then(|versions| versions.get_mut(&target_version_id)) @@ -230,6 +236,14 @@ impl Project { target_version_id ); + if matches!(already_resolved.pkg_ref, PackageRefs::Git(_)) + != matches!(pkg_ref, PackageRefs::Git(_)) + { + log::warn!( + "resolved package {name}@{target_version_id} has a different source than the previously resolved one, this may cause issues", + ); + } + if already_resolved.ty == DependencyType::Peer && ty == DependencyType::Standard { already_resolved.ty = ty; } @@ -237,7 +251,6 @@ impl Project { continue; } - let pkg_ref = &resolved[&target_version_id]; let node = DependencyGraphNode { direct: if depth == 0 { Some((alias.clone(), specifier.clone())) diff --git a/src/scripts.rs b/src/scripts.rs index 6fef047..1f463f0 100644 --- a/src/scripts.rs +++ b/src/scripts.rs @@ -43,7 +43,7 @@ pub fn execute_script, S: AsRef, P: AsRef .arg("--") .args(args) .current_dir(cwd) - .stdin(Stdio::null()) + .stdin(Stdio::inherit()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() diff --git a/src/source/git/mod.rs b/src/source/git/mod.rs new file mode 100644 index 0000000..108c40b --- /dev/null +++ b/src/source/git/mod.rs @@ -0,0 +1,466 @@ +use std::{collections::BTreeMap, fmt::Debug, hash::Hash, path::PathBuf}; + +use crate::{ + manifest::{ + target::{Target, TargetKind}, + Manifest, + }, + names::PackageNames, + source::{ + fs::{store_in_cas, FSEntry, PackageFS}, + git::{pkg_ref::GitPackageRef, specifier::GitDependencySpecifier}, + git_index::GitBasedSource, + PackageSource, ResolveResult, VersionId, + }, + util::hash, + Project, MANIFEST_FILE_NAME, +}; +use gix::{bstr::BStr, traverse::tree::Recorder, Url}; +use relative_path::RelativePathBuf; + +/// The Git package reference +pub mod pkg_ref; +/// The Git dependency specifier +pub mod specifier; + +/// The Git package source +#[derive(Debug, Hash, PartialEq, Eq, Clone)] +pub struct GitPackageSource { + repo_url: Url, +} + +impl GitBasedSource for GitPackageSource { + fn path(&self, project: &Project) -> PathBuf { + project + .data_dir + .join("git_repos") + .join(hash(self.as_bytes())) + } + + fn repo_url(&self) -> &Url { + &self.repo_url + } +} + +impl GitPackageSource { + /// Creates a new Git package source + pub fn new(repo_url: Url) -> Self { + Self { repo_url } + } + + fn as_bytes(&self) -> Vec { + self.repo_url.to_bstring().to_vec() + } +} + +impl PackageSource for GitPackageSource { + type Specifier = GitDependencySpecifier; + type Ref = GitPackageRef; + type RefreshError = crate::source::git_index::errors::RefreshError; + type ResolveError = errors::ResolveError; + type DownloadError = errors::DownloadError; + + fn refresh(&self, project: &Project) -> Result<(), Self::RefreshError> { + GitBasedSource::refresh(self, project) + } + + fn resolve( + &self, + specifier: &Self::Specifier, + project: &Project, + _project_target: TargetKind, + ) -> Result, Self::ResolveError> { + let repo = gix::open(self.path(project)) + .map_err(|e| errors::ResolveError::OpenRepo(Box::new(self.repo_url.clone()), e))?; + let rev = repo + .rev_parse_single(BStr::new(&specifier.rev)) + .map_err(|e| { + errors::ResolveError::ParseRev( + specifier.rev.clone(), + Box::new(self.repo_url.clone()), + e, + ) + })?; + let tree = rev + .object() + .map_err(|e| { + errors::ResolveError::ParseRevToObject(Box::new(self.repo_url.clone()), e) + })? + .peel_to_tree() + .map_err(|e| { + errors::ResolveError::ParseObjectToTree(Box::new(self.repo_url.clone()), e) + })?; + + let manifest = match self + .read_file([MANIFEST_FILE_NAME], project, Some(tree.clone())) + .map_err(|e| errors::ResolveError::ReadManifest(Box::new(self.repo_url.clone()), e))? + { + Some(m) => match toml::from_str::(&m) { + Ok(m) => Some(m), + Err(e) => { + return Err(errors::ResolveError::DeserManifest( + Box::new(self.repo_url.clone()), + e, + )) + } + }, + None => None, + }; + + let (name, version_id, dependencies) = match manifest { + Some(manifest) => { + let dependencies = manifest.all_dependencies().map_err(|e| { + errors::ResolveError::CollectDependencies(Box::new(self.repo_url.clone()), e) + })?; + let name = PackageNames::Pesde(manifest.name); + let version_id = VersionId(manifest.version, manifest.target.kind()); + + (name, version_id, dependencies) + } + + #[cfg(feature = "wally-compat")] + None => { + match self + .read_file(["wally.toml"], project, Some(tree)) + .map_err(|e| { + errors::ResolveError::ReadManifest(Box::new(self.repo_url.clone()), e) + })? { + Some(m) => { + match toml::from_str::(&m) { + Ok(manifest) => { + let dependencies = manifest.all_dependencies().map_err(|e| { + errors::ResolveError::CollectDependencies( + Box::new(self.repo_url.clone()), + e, + ) + })?; + let name = PackageNames::Wally(manifest.package.name); + let version_id = + VersionId(manifest.package.version, TargetKind::Roblox); + + (name, version_id, dependencies) + } + Err(e) => { + return Err(errors::ResolveError::DeserManifest( + Box::new(self.repo_url.clone()), + e, + )) + } + } + } + None => { + return Err(errors::ResolveError::NoManifest(Box::new( + self.repo_url.clone(), + ))) + } + } + } + #[cfg(not(feature = "wally-compat"))] + None => { + return Err(errors::ResolveError::NoManifest(Box::new( + self.repo_url.clone(), + ))) + } + }; + + let target = *version_id.target(); + let new_structure = matches!(name, PackageNames::Pesde(_)); + + Ok(( + name, + BTreeMap::from([( + version_id, + GitPackageRef { + repo: self.repo_url.clone(), + rev: rev.to_string(), + target, + new_structure, + dependencies, + }, + )]), + )) + } + + fn download( + &self, + pkg_ref: &Self::Ref, + project: &Project, + _reqwest: &reqwest::blocking::Client, + ) -> Result<(PackageFS, Target), Self::DownloadError> { + let index_file = project + .cas_dir + .join("git_index") + .join(hash(self.as_bytes())) + .join(&pkg_ref.rev) + .join(pkg_ref.target.to_string()); + + match std::fs::read_to_string(&index_file) { + Ok(s) => { + log::debug!( + "using cached index file for package {}#{} {}", + pkg_ref.repo, + pkg_ref.rev, + pkg_ref.target + ); + + let fs = toml::from_str::(&s).map_err(|e| { + errors::DownloadError::DeserializeFile(Box::new(self.repo_url.clone()), e) + })?; + + let manifest = match fs.0.get(&RelativePathBuf::from(MANIFEST_FILE_NAME)) { + Some(FSEntry::File(hash)) => match fs + .read_file(hash, project.cas_dir()) + .map(|m| toml::de::from_str::(&m)) + { + Some(Ok(m)) => Some(m), + Some(Err(e)) => { + return Err(errors::DownloadError::DeserializeFile( + Box::new(self.repo_url.clone()), + e, + )) + } + None => None, + }, + _ => None, + }; + + let target = match manifest { + Some(manifest) => manifest.target, + #[cfg(feature = "wally-compat")] + None if !pkg_ref.new_structure => { + let tempdir = tempfile::tempdir()?; + fs.write_to(tempdir.path(), project.cas_dir(), false)?; + + crate::source::wally::compat_util::get_target(project, &tempdir)? + } + None => { + return Err(errors::DownloadError::NoManifest(Box::new( + self.repo_url.clone(), + ))) + } + }; + + return Ok((fs, target)); + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} + Err(e) => return Err(errors::DownloadError::Io(e)), + } + + let repo = gix::open(self.path(project)) + .map_err(|e| errors::DownloadError::OpenRepo(Box::new(self.repo_url.clone()), e))?; + let rev = repo + .rev_parse_single(BStr::new(&pkg_ref.rev)) + .map_err(|e| { + errors::DownloadError::ParseRev( + pkg_ref.rev.clone(), + Box::new(self.repo_url.clone()), + e, + ) + })?; + let tree = rev + .object() + .map_err(|e| { + errors::DownloadError::ParseEntryToObject(Box::new(self.repo_url.clone()), e) + })? + .peel_to_tree() + .map_err(|e| { + errors::DownloadError::ParseObjectToTree(Box::new(self.repo_url.clone()), e) + })?; + + let mut recorder = Recorder::default(); + tree.traverse() + .breadthfirst(&mut recorder) + .map_err(|e| errors::DownloadError::TraverseTree(Box::new(self.repo_url.clone()), e))?; + + let mut entries = BTreeMap::new(); + let mut manifest = None; + + for entry in recorder.records { + let path = RelativePathBuf::from(entry.filepath.to_string()); + let object = repo.find_object(entry.oid).map_err(|e| { + errors::DownloadError::ParseEntryToObject(Box::new(self.repo_url.clone()), e) + })?; + + if matches!(object.kind, gix::object::Kind::Tree) { + entries.insert(path, FSEntry::Directory); + + continue; + } + + let data = object.into_blob().data.clone(); + let hash = store_in_cas(project.cas_dir(), &data)?.0; + + if path == MANIFEST_FILE_NAME { + manifest = Some(data); + } + + entries.insert(path, FSEntry::File(hash)); + } + + let manifest = match manifest { + Some(data) => match String::from_utf8(data.to_vec()) { + Ok(s) => match toml::from_str::(&s) { + Ok(m) => Some(m), + Err(e) => { + return Err(errors::DownloadError::DeserializeFile( + Box::new(self.repo_url.clone()), + e, + )) + } + }, + Err(e) => return Err(errors::DownloadError::ParseManifest(e)), + }, + None => None, + }; + + let fs = PackageFS(entries); + + let target = match manifest { + Some(manifest) => manifest.target, + #[cfg(feature = "wally-compat")] + None if !pkg_ref.new_structure => { + let tempdir = tempfile::tempdir()?; + fs.write_to(tempdir.path(), project.cas_dir(), false)?; + + crate::source::wally::compat_util::get_target(project, &tempdir)? + } + None => { + return Err(errors::DownloadError::NoManifest(Box::new( + self.repo_url.clone(), + ))) + } + }; + + if let Some(parent) = index_file.parent() { + std::fs::create_dir_all(parent)?; + } + + std::fs::write( + &index_file, + toml::to_string(&fs).map_err(|e| { + errors::DownloadError::SerializeIndex(Box::new(self.repo_url.clone()), e) + })?, + ) + .map_err(errors::DownloadError::Io)?; + + Ok((fs, target)) + } +} + +/// Errors that can occur when interacting with the Git package source +pub mod errors { + use relative_path::RelativePathBuf; + use thiserror::Error; + + /// Errors that can occur when resolving a package from a Git package source + #[derive(Debug, Error)] + #[non_exhaustive] + pub enum ResolveError { + /// An error occurred opening the Git repository + #[error("error opening Git repository for url {0}")] + OpenRepo(Box, #[source] gix::open::Error), + + /// An error occurred parsing rev + #[error("error parsing rev {0} for repository {1}")] + ParseRev( + String, + Box, + #[source] gix::revision::spec::parse::single::Error, + ), + + /// An error occurred parsing rev to object + #[error("error parsing rev to object for repository {0}")] + ParseRevToObject(Box, #[source] gix::object::find::existing::Error), + + /// An error occurred parsing object to tree + #[error("error parsing object to tree for repository {0}")] + ParseObjectToTree(Box, #[source] gix::object::peel::to_kind::Error), + + /// An error occurred reading repository file + #[error("error reading repository {0} file")] + ReadManifest( + Box, + #[source] crate::source::git_index::errors::ReadFile, + ), + + /// An error occurred collecting all manifest dependencies + #[error("error collecting all manifest dependencies for repository {0}")] + CollectDependencies( + Box, + #[source] crate::manifest::errors::AllDependenciesError, + ), + + /// An error occurred deserializing a manifest + #[error("error deserializing manifest for repository {0}")] + DeserManifest(Box, #[source] toml::de::Error), + + /// No manifest was found + #[error("no manifest found in repository {0}")] + NoManifest(Box), + } + + /// Errors that can occur when downloading a package from a Git package source + #[derive(Debug, Error)] + #[non_exhaustive] + pub enum DownloadError { + /// An error occurred deserializing a file + #[error("error deserializing file in repository {0}")] + DeserializeFile(Box, #[source] toml::de::Error), + + /// An error occurred interacting with the file system + #[error("error interacting with the file system")] + Io(#[from] std::io::Error), + + /// An error occurred while searching for a Wally lib export + #[cfg(feature = "wally-compat")] + #[error("error searching for Wally lib export")] + FindLibPath(#[from] crate::source::wally::compat_util::errors::FindLibPathError), + + /// No manifest was found + #[error("no manifest found in repository {0}")] + NoManifest(Box), + + /// An error occurred opening the Git repository + #[error("error opening Git repository for url {0}")] + OpenRepo(Box, #[source] gix::open::Error), + + /// An error occurred parsing rev + #[error("error parsing rev {0} for repository {1}")] + ParseRev( + String, + Box, + #[source] gix::revision::spec::parse::single::Error, + ), + + /// An error occurred while traversing the tree + #[error("error traversing tree for repository {0}")] + TraverseTree( + Box, + #[source] gix::traverse::tree::breadthfirst::Error, + ), + + /// An error occurred parsing an entry to object + #[error("error parsing an entry to object for repository {0}")] + ParseEntryToObject(Box, #[source] gix::object::find::existing::Error), + + /// An error occurred parsing object to tree + #[error("error parsing object to tree for repository {0}")] + ParseObjectToTree(Box, #[source] gix::object::peel::to_kind::Error), + + /// An error occurred reading a tree entry + #[error("error reading tree entry for repository {0} at {1}")] + ReadTreeEntry( + Box, + RelativePathBuf, + #[source] gix::objs::decode::Error, + ), + + /// An error occurred parsing the pesde manifest to UTF-8 + #[error("error parsing the manifest for repository {0} to UTF-8")] + ParseManifest(#[source] std::string::FromUtf8Error), + + /// An error occurred while serializing the index file + #[error("error serializing the index file for repository {0}")] + SerializeIndex(Box, #[source] toml::ser::Error), + } +} diff --git a/src/source/git/pkg_ref.rs b/src/source/git/pkg_ref.rs new file mode 100644 index 0000000..c1b9f78 --- /dev/null +++ b/src/source/git/pkg_ref.rs @@ -0,0 +1,45 @@ +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; + +use crate::{ + manifest::{target::TargetKind, DependencyType}, + source::{git::GitPackageSource, DependencySpecifiers, PackageRef, PackageSources}, +}; + +/// A Git package reference +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] +pub struct GitPackageRef { + /// The repository of the package + #[serde( + serialize_with = "crate::util::serialize_gix_url", + deserialize_with = "crate::util::deserialize_gix_url" + )] + pub repo: gix::Url, + /// The revision of the package + pub rev: String, + /// The dependencies of the package + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub dependencies: BTreeMap, + /// Whether this package uses the new structure + pub new_structure: bool, + /// The target of the package + pub target: TargetKind, +} +impl PackageRef for GitPackageRef { + fn dependencies(&self) -> &BTreeMap { + &self.dependencies + } + + fn use_new_structure(&self) -> bool { + self.new_structure + } + + fn target_kind(&self) -> TargetKind { + self.target + } + + fn source(&self) -> PackageSources { + PackageSources::Git(GitPackageSource::new(self.repo.clone())) + } +} diff --git a/src/source/git/specifier.rs b/src/source/git/specifier.rs new file mode 100644 index 0000000..feedc4a --- /dev/null +++ b/src/source/git/specifier.rs @@ -0,0 +1,28 @@ +use std::fmt::Display; + +use serde::{Deserialize, Serialize}; + +use crate::{manifest::target::TargetKind, source::DependencySpecifier}; + +/// The specifier for a Git dependency +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] +pub struct GitDependencySpecifier { + /// The repository of the package + #[serde( + serialize_with = "crate::util::serialize_gix_url", + deserialize_with = "crate::util::deserialize_git_like_url" + )] + pub repo: gix::Url, + /// The revision of the package + pub rev: String, + /// The target to use for the package + #[serde(default, skip_serializing_if = "Option::is_none")] + pub target: Option, +} +impl DependencySpecifier for GitDependencySpecifier {} + +impl Display for GitDependencySpecifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}#{}", self.repo, self.rev) + } +} diff --git a/src/source/git_index.rs b/src/source/git_index.rs index e98bc96..c9649f7 100644 --- a/src/source/git_index.rs +++ b/src/source/git_index.rs @@ -64,6 +64,7 @@ pub trait GitBasedSource { &self, file_path: I, project: &Project, + tree: Option, ) -> Result, errors::ReadFile> { let path = self.path(project); @@ -72,7 +73,7 @@ pub trait GitBasedSource { Err(e) => return Err(errors::ReadFile::Open(path, Box::new(e))), }; - let tree = match self.tree(&repo) { + let tree = match tree.map_or_else(|| self.tree(&repo), Ok) { Ok(tree) => tree, Err(e) => return Err(errors::ReadFile::Tree(path, Box::new(e))), }; diff --git a/src/source/mod.rs b/src/source/mod.rs index 32945f9..f1ad362 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -12,6 +12,8 @@ use crate::{ /// Packages' filesystems pub mod fs; +/// The Git package source +pub mod git; /// Git index-based package source utilities pub mod git_index; /// The pesde package source @@ -39,6 +41,8 @@ pub enum PackageSources { /// A Wally package source #[cfg(feature = "wally-compat")] Wally(wally::WallyPackageSource), + /// A Git package source + Git(git::GitPackageSource), } impl PackageSource for PackageSources { @@ -53,6 +57,7 @@ impl PackageSource for PackageSources { PackageSources::Pesde(source) => source.refresh(project).map_err(Into::into), #[cfg(feature = "wally-compat")] PackageSources::Wally(source) => source.refresh(project).map_err(Into::into), + PackageSources::Git(source) => source.refresh(project).map_err(Into::into), } } @@ -90,6 +95,19 @@ impl PackageSource for PackageSources { }) .map_err(Into::into), + (PackageSources::Git(source), DependencySpecifiers::Git(specifier)) => source + .resolve(specifier, project, project_target) + .map(|(name, results)| { + ( + name, + results + .into_iter() + .map(|(version, pkg_ref)| (version, PackageRefs::Git(pkg_ref))) + .collect(), + ) + }) + .map_err(Into::into), + _ => Err(errors::ResolveError::Mismatch), } } @@ -110,6 +128,10 @@ impl PackageSource for PackageSources { .download(pkg_ref, project, reqwest) .map_err(Into::into), + (PackageSources::Git(source), PackageRefs::Git(pkg_ref)) => source + .download(pkg_ref, project, reqwest) + .map_err(Into::into), + _ => Err(errors::DownloadError::Mismatch), } } @@ -144,6 +166,10 @@ pub mod errors { #[cfg(feature = "wally-compat")] #[error("error resolving wally package")] Wally(#[from] crate::source::wally::errors::ResolveError), + + /// A Git package source failed to resolve + #[error("error resolving git package")] + Git(#[from] crate::source::git::errors::ResolveError), } /// Errors that can occur when downloading a package @@ -162,5 +188,9 @@ pub mod errors { #[cfg(feature = "wally-compat")] #[error("error downloading wally package")] Wally(#[from] crate::source::wally::errors::DownloadError), + + /// A Git package source failed to download + #[error("error downloading git package")] + Git(#[from] crate::source::git::errors::DownloadError), } } diff --git a/src/source/pesde/mod.rs b/src/source/pesde/mod.rs index 418d467..d7e7e51 100644 --- a/src/source/pesde/mod.rs +++ b/src/source/pesde/mod.rs @@ -68,7 +68,9 @@ impl PesdePackageSource { /// Reads the config file pub fn config(&self, project: &Project) -> Result { - let file = self.read_file(["config.toml"], project).map_err(Box::new)?; + let file = self + .read_file(["config.toml"], project, None) + .map_err(Box::new)?; let string = match file { Some(s) => s, @@ -192,7 +194,7 @@ impl PackageSource for PesdePackageSource { project_target: TargetKind, ) -> Result, Self::ResolveError> { let (scope, name) = specifier.name.as_str(); - let string = match self.read_file([scope, name], project) { + let string = match self.read_file([scope, name], project, None) { Ok(Some(s)) => s, Ok(None) => return Err(Self::ResolveError::NotFound(specifier.name.to_string())), Err(e) => { diff --git a/src/source/pesde/pkg_ref.rs b/src/source/pesde/pkg_ref.rs index 896a7f9..8701c44 100644 --- a/src/source/pesde/pkg_ref.rs +++ b/src/source/pesde/pkg_ref.rs @@ -48,15 +48,3 @@ impl PackageRef for PesdePackageRef { PackageSources::Pesde(PesdePackageSource::new(self.index_url.clone())) } } - -impl Ord for PesdePackageRef { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.version.cmp(&other.version) - } -} - -impl PartialOrd for PesdePackageRef { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} diff --git a/src/source/refs.rs b/src/source/refs.rs index 0cdadd8..7d24443 100644 --- a/src/source/refs.rs +++ b/src/source/refs.rs @@ -14,14 +14,17 @@ pub enum PackageRefs { /// A Wally package reference #[cfg(feature = "wally-compat")] Wally(crate::source::wally::pkg_ref::WallyPackageRef), + /// A Git package reference + Git(crate::source::git::pkg_ref::GitPackageRef), } impl PackageRefs { - /// Returns whether this package reference is a Wally package reference - pub fn is_wally(&self) -> bool { + /// Returns whether this package reference should be treated as a Wally package + pub fn like_wally(&self) -> bool { match self { #[cfg(feature = "wally-compat")] PackageRefs::Wally(_) => true, + PackageRefs::Git(git) => !git.use_new_structure(), _ => false, } } @@ -33,6 +36,7 @@ impl PackageRef for PackageRefs { PackageRefs::Pesde(pkg_ref) => pkg_ref.dependencies(), #[cfg(feature = "wally-compat")] PackageRefs::Wally(pkg_ref) => pkg_ref.dependencies(), + PackageRefs::Git(pkg_ref) => pkg_ref.dependencies(), } } @@ -41,6 +45,7 @@ impl PackageRef for PackageRefs { PackageRefs::Pesde(pkg_ref) => pkg_ref.use_new_structure(), #[cfg(feature = "wally-compat")] PackageRefs::Wally(pkg_ref) => pkg_ref.use_new_structure(), + PackageRefs::Git(pkg_ref) => pkg_ref.use_new_structure(), } } @@ -49,6 +54,7 @@ impl PackageRef for PackageRefs { PackageRefs::Pesde(pkg_ref) => pkg_ref.target_kind(), #[cfg(feature = "wally-compat")] PackageRefs::Wally(pkg_ref) => pkg_ref.target_kind(), + PackageRefs::Git(pkg_ref) => pkg_ref.target_kind(), } } @@ -57,6 +63,7 @@ impl PackageRef for PackageRefs { PackageRefs::Pesde(pkg_ref) => pkg_ref.source(), #[cfg(feature = "wally-compat")] PackageRefs::Wally(pkg_ref) => pkg_ref.source(), + PackageRefs::Git(pkg_ref) => pkg_ref.source(), } } } diff --git a/src/source/specifiers.rs b/src/source/specifiers.rs index 063603c..cf0d10e 100644 --- a/src/source/specifiers.rs +++ b/src/source/specifiers.rs @@ -11,6 +11,8 @@ pub enum DependencySpecifiers { /// A Wally dependency specifier #[cfg(feature = "wally-compat")] Wally(crate::source::wally::specifier::WallyDependencySpecifier), + /// A Git dependency specifier + Git(crate::source::git::specifier::GitDependencySpecifier), } impl DependencySpecifier for DependencySpecifiers {} @@ -20,6 +22,7 @@ impl Display for DependencySpecifiers { DependencySpecifiers::Pesde(specifier) => write!(f, "{specifier}"), #[cfg(feature = "wally-compat")] DependencySpecifiers::Wally(specifier) => write!(f, "{specifier}"), + DependencySpecifiers::Git(specifier) => write!(f, "{specifier}"), } } } diff --git a/src/source/wally/manifest.rs b/src/source/wally/manifest.rs index 64fe24a..132bb57 100644 --- a/src/source/wally/manifest.rs +++ b/src/source/wally/manifest.rs @@ -5,12 +5,14 @@ use serde::{Deserialize, Deserializer}; use crate::{ manifest::{errors, DependencyType}, + names::wally::WallyPackageName, source::{specifiers::DependencySpecifiers, wally::specifier::WallyDependencySpecifier}, }; #[derive(Deserialize, Clone, Debug)] #[serde(rename_all = "kebab-case")] pub struct WallyPackage { + pub name: WallyPackageName, pub version: Version, } diff --git a/src/source/wally/mod.rs b/src/source/wally/mod.rs index 68086c8..d0019d0 100644 --- a/src/source/wally/mod.rs +++ b/src/source/wally/mod.rs @@ -22,7 +22,7 @@ use crate::{ Project, }; -mod compat_util; +pub(crate) mod compat_util; pub(crate) mod manifest; /// The Wally package reference pub mod pkg_ref; @@ -60,7 +60,9 @@ impl WallyPackageSource { /// Reads the config file pub fn config(&self, project: &Project) -> Result { - let file = self.read_file(["config.json"], project).map_err(Box::new)?; + let file = self + .read_file(["config.json"], project, None) + .map_err(Box::new)?; let string = match file { Some(s) => s, @@ -93,7 +95,7 @@ impl PackageSource for WallyPackageSource { _project_target: TargetKind, ) -> Result, Self::ResolveError> { let (scope, name) = specifier.name.as_str(); - let string = match self.read_file([scope, name], project) { + let string = match self.read_file([scope, name], project, None) { Ok(Some(s)) => s, Ok(None) => return Err(Self::ResolveError::NotFound(specifier.name.to_string())), Err(e) => { diff --git a/src/source/wally/pkg_ref.rs b/src/source/wally/pkg_ref.rs index 6ed8f17..363c0fc 100644 --- a/src/source/wally/pkg_ref.rs +++ b/src/source/wally/pkg_ref.rs @@ -44,15 +44,3 @@ impl PackageRef for WallyPackageRef { PackageSources::Wally(WallyPackageSource::new(self.index_url.clone())) } } - -impl Ord for WallyPackageRef { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.version.cmp(&other.version) - } -} - -impl PartialOrd for WallyPackageRef { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} diff --git a/src/util.rs b/src/util.rs index 7a17c1b..228e4e9 100644 --- a/src/util.rs +++ b/src/util.rs @@ -61,6 +61,18 @@ pub fn deserialize_gix_url_map<'de, D: Deserializer<'de>>( .collect() } +pub fn deserialize_git_like_url<'de, D: Deserializer<'de>>( + deserializer: D, +) -> Result { + let s = String::deserialize(deserializer)?; + if s.contains(':') { + gix::Url::from_bytes(BStr::new(&s)).map_err(serde::de::Error::custom) + } else { + gix::Url::from_bytes(BStr::new(format!("https://github.com/{s}").as_bytes())) + .map_err(serde::de::Error::custom) + } +} + pub fn hash>(struc: S) -> String { let mut hasher = Sha256::new(); hasher.update(struc.as_ref());