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_yaml",
"termios",
"walkdir",
]
[[package]]
@@ -481,6 +482,15 @@ version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "scratch"
version = "1.0.5"
@@ -618,6 +628,16 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "winapi"
version = "0.2.8"
+1
View File
@@ -19,6 +19,7 @@ const_format = { version = "0.2", default-features = false }
path-absolutize = "3.0"
regex = { version = "1.7", default-features = false, features = ["std"] }
termios = "0.3"
walkdir = "2.3"
serde = "1.0"
serde_derive = "1.0"
+57 -12
View File
@@ -6,6 +6,9 @@ use std::io::Write;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use walkdir::WalkDir;
use crate::path::get_relative_path;
use super::{Package, Section};
@@ -20,7 +23,7 @@ pub struct 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
/// - 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")?;
}
for entry in group_dir.read_dir().context("reading group dir")? {
let file = entry.context("getting group file")?;
for entry in WalkDir::new(group_dir).follow_links(true).min_depth(1) {
let file = entry?;
let path = file.path();
if path.is_dir() {
continue;
}
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!(
"WARNING: group file {} is not a symlink",
path.to_string_lossy()
);
}
let group =
Self::try_from(&path).with_context(|| format!("reading group file {path:?}"))?;
let group = Self::try_from(path, group_dir)
.with_context(|| format!("reading group file {path:?}"))?;
result.insert(group);
}
@@ -96,17 +105,14 @@ impl Eq for Group {
}
impl Group {
fn try_from<P>(p: P) -> Result<Self>
fn try_from<P>(path: P, group_dir: P) -> Result<Self>
where
P: AsRef<Path>,
{
let path = p.as_ref();
let path = path.as_ref();
let content = read_to_string(path).context("reading file content")?;
let name = path
.file_name()
.context("getting file name")?
.to_string_lossy()
.to_string();
let name = extract_group_name(path, group_dir.as_ref());
let mut lines = content.lines().peekable();
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 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
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"));
}
}
#[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.
*/
use std::env;
use std::path::PathBuf;
use std::{env, path::Path};
use anyhow::{Context, Result};
@@ -99,3 +99,46 @@ pub(crate) fn binary_in_path(name: &str) -> Result<bool> {
}
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);
}
}