feat(groups): allow nested group dirs

Instead of expecting all files to be located immediately under the group
dir, we allow them to be nested in additional folders. We walk the group
folder, and treat every file as a group file. The relative path of the
file as seen from the group dir becomes the group name.

Fixes #27.
This commit is contained in:
steven-omaha
2023-05-18 17:16:30 +02:00
parent ab913b4721
commit 0ffe0c345d
4 changed files with 122 additions and 13 deletions
Generated
+20
View File
@@ -363,6 +363,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
"termios", "termios",
"walkdir",
] ]
[[package]] [[package]]
@@ -481,6 +482,15 @@ version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "scratch" name = "scratch"
version = "1.0.5" version = "1.0.5"
@@ -618,6 +628,16 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "walkdir"
version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
dependencies = [
"same-file",
"winapi-util",
]
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.2.8" version = "0.2.8"
+1
View File
@@ -19,6 +19,7 @@ const_format = { version = "0.2", default-features = false }
path-absolutize = "3.0" path-absolutize = "3.0"
regex = { version = "1.7", default-features = false, features = ["std"] } regex = { version = "1.7", default-features = false, features = ["std"] }
termios = "0.3" termios = "0.3"
walkdir = "2.3"
serde = "1.0" serde = "1.0"
serde_derive = "1.0" serde_derive = "1.0"
+57 -12
View File
@@ -6,6 +6,9 @@ use std::io::Write;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use walkdir::WalkDir;
use crate::path::get_relative_path;
use super::{Package, Section}; use super::{Package, Section};
@@ -20,7 +23,7 @@ pub struct Group {
} }
impl Group { impl Group {
/// Load all group files from the pacdef group dir. /// Load all group files from the pacdef group dir by recursing through the group dir.
/// ///
/// This method will print a warning if /// This method will print a warning if
/// - there are no files under `group_dir`, or /// - there are no files under `group_dir`, or
@@ -39,19 +42,25 @@ impl Group {
create_dir(group_dir).context("group dir does not exist, creating")?; create_dir(group_dir).context("group dir does not exist, creating")?;
} }
for entry in group_dir.read_dir().context("reading group dir")? { for entry in WalkDir::new(group_dir).follow_links(true).min_depth(1) {
let file = entry.context("getting group file")?; let file = entry?;
let path = file.path(); let path = file.path();
if path.is_dir() {
continue;
}
if warn_not_symlinks && !path.is_symlink() { if warn_not_symlinks && !path.is_symlink() {
// TODO is there an efficient way to make sure *any* of the elements in the path
// is a symlink?
eprintln!( eprintln!(
"WARNING: group file {} is not a symlink", "WARNING: group file {} is not a symlink",
path.to_string_lossy() path.to_string_lossy()
); );
} }
let group = let group = Self::try_from(path, group_dir)
Self::try_from(&path).with_context(|| format!("reading group file {path:?}"))?; .with_context(|| format!("reading group file {path:?}"))?;
result.insert(group); result.insert(group);
} }
@@ -96,17 +105,14 @@ impl Eq for Group {
} }
impl Group { impl Group {
fn try_from<P>(p: P) -> Result<Self> fn try_from<P>(path: P, group_dir: P) -> Result<Self>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
let path = p.as_ref(); let path = path.as_ref();
let content = read_to_string(path).context("reading file content")?; let content = read_to_string(path).context("reading file content")?;
let name = path
.file_name() let name = extract_group_name(path, group_dir.as_ref());
.context("getting file name")?
.to_string_lossy()
.to_string();
let mut lines = content.lines().peekable(); let mut lines = content.lines().peekable();
let mut sections = HashSet::new(); let mut sections = HashSet::new();
@@ -158,6 +164,30 @@ impl Group {
} }
} }
/// Extract the group name from its path relative to the group path.
/// All subdirectories are concatenated using `'/'`.
///
/// # Example
///
/// If the group dir is `~/.config/pacdef/groups`, and the group file is
/// `~/.config/pacdef/groups/generic/base`, then the group name is
/// `"generic/base"`.
///
/// # Panics
///
/// Panics if `path` and `group_path` are identical.
fn extract_group_name(path: &Path, group_path: &Path) -> String {
get_relative_path(path, group_path)
.iter()
.map(|p| p.to_string_lossy().to_string())
.reduce(|mut a, b| {
a.push('/');
a.push_str(&b);
a
})
.expect("must have at least one element")
}
impl Display for Group { impl Display for Group {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut sections: Vec<_> = self.sections.iter().collect(); let mut sections: Vec<_> = self.sections.iter().collect();
@@ -220,3 +250,18 @@ fn add_new_section_with_packages(
group_file_content.push_str(&format!("{p}\n")); group_file_content.push_str(&format!("{p}\n"));
} }
} }
#[cfg(test)]
mod tests {
use std::path::PathBuf;
#[test]
fn extract_group_name() {
let path = PathBuf::from("/a/b/c/d/e");
let group_path = PathBuf::from("/a/b/c");
let expected = String::from("d/e");
let result = super::extract_group_name(&path, &group_path);
assert_eq!(result, expected);
}
}
+44 -1
View File
@@ -2,8 +2,8 @@
All functions related to `pacdef`'s internal paths. All functions related to `pacdef`'s internal paths.
*/ */
use std::env;
use std::path::PathBuf; use std::path::PathBuf;
use std::{env, path::Path};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
@@ -99,3 +99,46 @@ pub(crate) fn binary_in_path(name: &str) -> Result<bool> {
} }
Ok(false) Ok(false)
} }
/// Determine the relative path of `full_path` in relation to `base_path`.
///
/// # Panics
///
/// Panics if at least one element in `base_path` does not match the corresponding
/// element in `full_path`.
pub(crate) fn get_relative_path<P>(full_path: P, base_path: P) -> PathBuf
where
P: AsRef<Path>,
{
let mut file_iter = full_path.as_ref().iter();
base_path
.as_ref()
.iter()
.zip(&mut file_iter)
.for_each(|(a, b)| assert_eq!(a, b));
let relative_path: PathBuf = file_iter.collect();
relative_path
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::get_relative_path;
#[test]
fn relative_path() {
let full = PathBuf::from("/a/b/c/d/e");
let base = PathBuf::from("/a/b/c");
let relative = get_relative_path(full, base);
assert_eq!(relative, PathBuf::from("d/e"));
}
#[test]
#[should_panic]
fn relative_path_panic() {
let full = PathBuf::from("/a/b/z/d/e");
let base = PathBuf::from("/a/b/c");
get_relative_path(full, base);
}
}