#![cfg_attr(not(test), warn(unused_crate_dependencies))]
use std::{
collections::{HashMap, HashSet},
fs::FileType,
path::{Path, PathBuf},
};
use futures::StreamExt;
use tokio::fs::read_dir;
use tokio_stream::wrappers::ReadDirStream;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ProjectType {
Bazaar,
Darcs,
Fossil,
Git,
Mercurial,
Pijul,
Subversion,
Bundler,
C,
Cargo,
Docker,
Elixir,
Go,
Gradle,
JavaScript,
Leiningen,
Maven,
Perl,
PHP,
Pip,
V,
Zig,
}
impl ProjectType {
#[must_use]
pub const fn is_vcs(self) -> bool {
matches!(
self,
Self::Bazaar
| Self::Darcs
| Self::Fossil
| Self::Git | Self::Mercurial
| Self::Pijul
| Self::Subversion
)
}
#[must_use]
pub const fn is_soft(self) -> bool {
matches!(
self,
Self::Bundler
| Self::C | Self::Cargo
| Self::Docker
| Self::Elixir
| Self::Gradle
| Self::JavaScript
| Self::Leiningen
| Self::Maven
| Self::Perl | Self::PHP
| Self::Pip | Self::V
)
}
}
pub async fn origins(path: impl AsRef<Path> + Send) -> HashSet<PathBuf> {
fn check_list(list: &DirList) -> bool {
if list.is_empty() {
return false;
}
[
list.has_dir("_darcs"),
list.has_dir(".bzr"),
list.has_dir(".fossil-settings"),
list.has_dir(".git"),
list.has_dir(".github"),
list.has_dir(".hg"),
list.has_dir(".svn"),
list.has_file(".asf.yaml"),
list.has_file(".bzrignore"),
list.has_file(".codecov.yml"),
list.has_file(".ctags"),
list.has_file(".editorconfig"),
list.has_file(".git"),
list.has_file(".gitattributes"),
list.has_file(".gitmodules"),
list.has_file(".hgignore"),
list.has_file(".hgtags"),
list.has_file(".perltidyrc"),
list.has_file(".travis.yml"),
list.has_file("appveyor.yml"),
list.has_file("build.gradle"),
list.has_file("build.properties"),
list.has_file("build.xml"),
list.has_file("Cargo.toml"),
list.has_file("Cargo.lock"),
list.has_file("cgmanifest.json"),
list.has_file("CMakeLists.txt"),
list.has_file("composer.json"),
list.has_file("COPYING"),
list.has_file("docker-compose.yml"),
list.has_file("Dockerfile"),
list.has_file("Gemfile"),
list.has_file("LICENSE.txt"),
list.has_file("LICENSE"),
list.has_file("Makefile.am"),
list.has_file("Makefile.pl"),
list.has_file("Makefile.PL"),
list.has_file("Makefile"),
list.has_file("mix.exs"),
list.has_file("moonshine-dependencies.xml"),
list.has_file("package.json"),
list.has_file("package-lock.json"),
list.has_file("pnpm-lock.yaml"),
list.has_file("yarn.lock"),
list.has_file("pom.xml"),
list.has_file("project.clj"),
list.has_file("requirements.txt"),
list.has_file("v.mod"),
list.has_file("CONTRIBUTING.md"),
list.has_file("go.mod"),
list.has_file("go.sum"),
list.has_file("Pipfile"),
list.has_file("build.zig"),
]
.into_iter()
.any(|f| f)
}
let mut origins = HashSet::new();
let path = path.as_ref();
let mut current = path;
if check_list(&DirList::obtain(current).await) {
origins.insert(current.to_owned());
}
while let Some(parent) = current.parent() {
current = parent;
if check_list(&DirList::obtain(current).await) {
origins.insert(current.to_owned());
}
}
origins
}
pub async fn types(path: impl AsRef<Path> + Send) -> HashSet<ProjectType> {
let path = path.as_ref();
let list = DirList::obtain(path).await;
[
list.if_has_dir("_darcs", ProjectType::Darcs),
list.if_has_dir(".bzr", ProjectType::Bazaar),
list.if_has_dir(".fossil-settings", ProjectType::Fossil),
list.if_has_dir(".git", ProjectType::Git),
list.if_has_dir(".hg", ProjectType::Mercurial),
list.if_has_dir(".svn", ProjectType::Subversion),
list.if_has_file(".bzrignore", ProjectType::Bazaar),
list.if_has_file(".ctags", ProjectType::C),
list.if_has_file(".git", ProjectType::Git),
list.if_has_file(".gitattributes", ProjectType::Git),
list.if_has_file(".gitmodules", ProjectType::Git),
list.if_has_file(".hgignore", ProjectType::Mercurial),
list.if_has_file(".hgtags", ProjectType::Mercurial),
list.if_has_file(".perltidyrc", ProjectType::Perl),
list.if_has_file("build.gradle", ProjectType::Gradle),
list.if_has_file("Cargo.toml", ProjectType::Cargo),
list.if_has_file("cgmanifest.json", ProjectType::JavaScript),
list.if_has_file("composer.json", ProjectType::PHP),
list.if_has_file("Dockerfile", ProjectType::Docker),
list.if_has_file("Gemfile", ProjectType::Bundler),
list.if_has_file("Makefile.PL", ProjectType::Perl),
list.if_has_file("mix.exs", ProjectType::Elixir),
list.if_has_file("package.json", ProjectType::JavaScript),
list.if_has_file("pom.xml", ProjectType::Maven),
list.if_has_file("project.clj", ProjectType::Leiningen),
list.if_has_file("requirements.txt", ProjectType::Pip),
list.if_has_file("v.mod", ProjectType::V),
list.if_has_file("go.mod", ProjectType::Go),
list.if_has_file("go.sum", ProjectType::Go),
list.if_has_file("Pipfile", ProjectType::Pip),
list.if_has_file("build.zig", ProjectType::Zig),
]
.into_iter()
.flatten()
.collect()
}
#[derive(Debug, Default)]
struct DirList(HashMap<PathBuf, FileType>);
impl DirList {
async fn obtain(path: &Path) -> Self {
if let Ok(s) = read_dir(path).await {
Self(
ReadDirStream::new(s)
.filter_map(|entry| async move {
match entry {
Err(_) => None,
Ok(entry) => {
if let (Ok(path), Ok(file_type)) =
(entry.path().strip_prefix(path), entry.file_type().await)
{
Some((path.to_owned(), file_type))
} else {
None
}
}
}
})
.collect::<HashMap<_, _>>()
.await,
)
} else {
Self::default()
}
}
#[inline]
fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[inline]
fn has_file(&self, name: impl AsRef<Path>) -> bool {
let name = name.as_ref();
self.0.get(name).map_or(false, std::fs::FileType::is_file)
}
#[inline]
fn has_dir(&self, name: impl AsRef<Path>) -> bool {
let name = name.as_ref();
self.0.get(name).map_or(false, std::fs::FileType::is_dir)
}
#[inline]
fn if_has_file(&self, name: impl AsRef<Path>, project: ProjectType) -> Option<ProjectType> {
if self.has_file(name) {
Some(project)
} else {
None
}
}
#[inline]
fn if_has_dir(&self, name: impl AsRef<Path>, project: ProjectType) -> Option<ProjectType> {
if self.has_dir(name) {
Some(project)
} else {
None
}
}
}