new: add interactive wizard and remaining ecosystem templates
This commit is contained in:
+192
-1
@@ -21,6 +21,8 @@
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
use super::options::TemplateId;
|
||||
|
||||
/// Outcome of the detection.
|
||||
@@ -74,7 +76,7 @@ pub fn detect(dir: &Path) -> Detection {
|
||||
/// The single top-level script of `dir`, if there is exactly one: a file
|
||||
/// with the `.sh` extension, or whose first line starts with `#!`. Returns
|
||||
/// `None` when there are zero or several candidates.
|
||||
fn single_script(dir: &Path) -> Option<std::path::PathBuf> {
|
||||
pub fn single_script(dir: &Path) -> Option<std::path::PathBuf> {
|
||||
let mut found: Option<std::path::PathBuf> = None;
|
||||
let entries = std::fs::read_dir(dir).ok()?;
|
||||
for entry in entries.flatten() {
|
||||
@@ -107,6 +109,102 @@ fn has_shebang(path: &Path) -> bool {
|
||||
content.starts_with(b"#!")
|
||||
}
|
||||
|
||||
/// License files looked at by [`sniff_license`], in preference order.
|
||||
const LICENSE_FILES: [&str; 5] = [
|
||||
"LICENSE",
|
||||
"LICENSE.md",
|
||||
"LICENSE.txt",
|
||||
"COPYING",
|
||||
"COPYING.txt",
|
||||
];
|
||||
|
||||
/// Sniff the license of the project in `dir` from its `LICENSE`/`COPYING`
|
||||
/// file: an `SPDX-License-Identifier:` line wins, otherwise the text is
|
||||
/// matched against a short list of recognizable licenses (MIT, BSD-2/3,
|
||||
/// Apache-2.0, GPL-2/3, LGPL-2.1/3, ISC). `None` when no license file
|
||||
/// exists or nothing recognizable is found.
|
||||
pub fn sniff_license(dir: &Path) -> Option<String> {
|
||||
let content = LICENSE_FILES
|
||||
.iter()
|
||||
.find_map(|name| std::fs::read_to_string(dir.join(name)).ok())
|
||||
// Case variants and suffixes (LICENSE-MIT, LICENCE, cpYING…): the
|
||||
// first top-level file whose name looks like a license notice.
|
||||
.or_else(|| {
|
||||
let mut candidates: Vec<std::path::PathBuf> = std::fs::read_dir(dir)
|
||||
.ok()?
|
||||
.flatten()
|
||||
.map(|entry| entry.path())
|
||||
.filter(|path| {
|
||||
path.is_file()
|
||||
&& path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.is_some_and(|name| {
|
||||
let name = name.to_ascii_uppercase();
|
||||
// American and British spellings both count.
|
||||
name.starts_with("LICENSE")
|
||||
|| name.starts_with("LICENCE")
|
||||
|| name.starts_with("COPYING")
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
candidates.sort();
|
||||
std::fs::read_to_string(candidates.into_iter().next()?).ok()
|
||||
})?;
|
||||
|
||||
// An explicit SPDX identifier is the most reliable signal.
|
||||
static SPDX_REGEX: std::sync::OnceLock<Regex> = std::sync::OnceLock::new();
|
||||
let spdx = SPDX_REGEX.get_or_init(|| {
|
||||
Regex::new(r"(?i)SPDX-License-Identifier\s*:\s*([A-Za-z0-9+.\- ]+)").unwrap()
|
||||
});
|
||||
if let Some(id) = spdx
|
||||
.captures(&content)
|
||||
.and_then(|caps| caps.get(1))
|
||||
.map(|id| id.as_str().trim_end().to_string())
|
||||
.filter(|id| !id.is_empty())
|
||||
{
|
||||
return Some(id);
|
||||
}
|
||||
|
||||
let text = content.to_ascii_lowercase();
|
||||
if text.contains("apache license") && text.contains("version 2") {
|
||||
return Some("Apache-2.0".to_string());
|
||||
}
|
||||
if text.contains("lesser general public license") {
|
||||
return if text.contains("version 3") && !text.contains("version 2.1") {
|
||||
Some("LGPL-3.0+".to_string())
|
||||
} else {
|
||||
Some("LGPL-2.1+".to_string())
|
||||
};
|
||||
}
|
||||
if text.contains("general public license") {
|
||||
return if text.contains("version 3") {
|
||||
Some("GPL-3.0+".to_string())
|
||||
} else {
|
||||
Some("GPL-2.0+".to_string())
|
||||
};
|
||||
}
|
||||
if text.contains("mit license") || text.contains("permission is hereby granted, free of charge")
|
||||
{
|
||||
return Some("MIT".to_string());
|
||||
}
|
||||
if text.contains("isc license")
|
||||
|| text.contains("permission to use, copy, modify, and/or distribute this software")
|
||||
{
|
||||
return Some("ISC".to_string());
|
||||
}
|
||||
if text.contains("redistribution and use in source and binary forms") {
|
||||
// The third clause (name endorsement) is what sets BSD-3 apart
|
||||
// from BSD-2.
|
||||
return if text.contains("endorse or promote") {
|
||||
Some("BSD-3-Clause".to_string())
|
||||
} else {
|
||||
Some("BSD-2-Clause".to_string())
|
||||
};
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -203,4 +301,97 @@ mod tests {
|
||||
// The only "real" script candidate is in a subdir or hidden: no hit.
|
||||
assert_eq!(detect(dir.path()), Detection::Empty);
|
||||
}
|
||||
|
||||
/// Distinctive (shortened) excerpts of the recognizable license texts.
|
||||
const LICENSE_TEXTS: [(&str, &str); 9] = [
|
||||
(
|
||||
"MIT",
|
||||
"MIT License\n\nPermission is hereby granted, free of charge, to any person",
|
||||
),
|
||||
("Apache-2.0", "Apache License\nVersion 2.0, January 2004"),
|
||||
(
|
||||
"GPL-2.0+",
|
||||
"GNU GENERAL PUBLIC LICENSE\nVersion 2, June 1991\neither version 2 of the License",
|
||||
),
|
||||
(
|
||||
"GPL-3.0+",
|
||||
"GNU GENERAL PUBLIC LICENSE\nVersion 3, 29 June 2007",
|
||||
),
|
||||
(
|
||||
"LGPL-2.1+",
|
||||
"GNU LESSER GENERAL PUBLIC LICENSE\nVersion 2.1, February 1999",
|
||||
),
|
||||
(
|
||||
"LGPL-3.0+",
|
||||
"GNU LESSER GENERAL PUBLIC LICENSE\nVersion 3, 29 June 2007",
|
||||
),
|
||||
(
|
||||
"BSD-2-Clause",
|
||||
"Redistribution and use in source and binary forms, with or without\nmodification, are permitted",
|
||||
),
|
||||
(
|
||||
"BSD-3-Clause",
|
||||
"Redistribution and use in source and binary forms, with or without\nmay be used to endorse or promote products",
|
||||
),
|
||||
(
|
||||
"ISC",
|
||||
"ISC License\nPermission to use, copy, modify, and/or distribute this software",
|
||||
),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn sniff_license_recognizes_license_files() {
|
||||
for (expected, text) in LICENSE_TEXTS {
|
||||
// Every candidate file name is looked at.
|
||||
for name in ["LICENSE", "COPYING", "LICENSE.md", "COPYING.txt"] {
|
||||
let dir = tempdir().unwrap();
|
||||
std::fs::write(dir.path().join(name), text).unwrap();
|
||||
assert_eq!(
|
||||
sniff_license(dir.path()).as_deref(),
|
||||
Some(expected),
|
||||
"{name}: {expected}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sniff_license_prefers_spdx_identifier() {
|
||||
let dir = tempdir().unwrap();
|
||||
std::fs::write(
|
||||
dir.path().join("LICENSE"),
|
||||
"Custom terms here\nSPDX-License-Identifier: Zlib\n",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(sniff_license(dir.path()).as_deref(), Some("Zlib"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sniff_license_handles_case_variants_and_missing_files() {
|
||||
// Unusual spelling found through the directory scan.
|
||||
let dir = tempdir().unwrap();
|
||||
std::fs::write(
|
||||
dir.path().join("Licence.TXT"),
|
||||
"Permission is hereby granted, free of charge",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(sniff_license(dir.path()).as_deref(), Some("MIT"));
|
||||
|
||||
// Exact candidates win over the directory scan (LICENSE before
|
||||
// LICENSE.blurb).
|
||||
let dir = tempdir().unwrap();
|
||||
std::fs::write(
|
||||
dir.path().join("LICENSE.blurb"),
|
||||
"GNU GENERAL PUBLIC LICENSE\nVersion 3, 29 June 2007",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(dir.path().join("LICENSE"), "MIT License").unwrap();
|
||||
assert_eq!(sniff_license(dir.path()).as_deref(), Some("MIT"));
|
||||
|
||||
// Unrecognizable or missing text: silent None.
|
||||
let dir = tempdir().unwrap();
|
||||
std::fs::write(dir.path().join("LICENSE"), "do whatever you want\n").unwrap();
|
||||
assert_eq!(sniff_license(dir.path()), None);
|
||||
assert_eq!(sniff_license(&dir.path().join("missing")), None);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user