Refactor task database

This commit is contained in:
southerntofu 2020-11-28 11:14:20 +01:00
parent 9ae8eaf3c2
commit b28c97fdb1
3 changed files with 80 additions and 151 deletions

113
src/db.rs
View File

@ -1,83 +1,6 @@
use std::ffi::OsString;
use std::fs;
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub enum Error {
EntryNotFound(String),
}
#[derive(Debug, Clone)]
pub struct Entry {
pub name: OsString,
pub path: PathBuf,
pub base_dir: PathBuf,
}
impl Entry {
pub fn new(path: PathBuf, name: OsString, base_dir: PathBuf) -> Entry {
Entry {
path,
name,
base_dir,
}
}
pub fn read_setting(&self, setting: &str) -> Option<String> {
let mut path = self.path.clone();
path.set_extension(setting);
read_or_none(&path)
}
}
/// Load single entry from folder
pub fn entry(basedir: &str, filter: impl Fn(&Path) -> bool, name: &str) -> Option<Entry> {
let basepath = PathBuf::from(&basedir);
let path = basepath.clone().join(name);
if !path.exists() || !filter(&path) {
return None;
}
Some(Entry::new(
path.clone(),
path.file_name()
.expect("Failed to read file name")
.to_os_string(),
basepath,
))
}
/// Loads entire database from folder
pub fn from(path_name: &str, filter: impl Fn(&Path) -> bool) -> Result<Vec<Entry>, std::io::Error> {
let path = PathBuf::from(&path_name);
let mut entries = Vec::new();
for file in path.read_dir()? {
if file.is_err() {
// Dismiss individual errors (in case there's a permission problem)
// TODO: maybe print a warning? Actually a configurable error level
// (using enum variants) should be passed to the function to configure
// whether to continue silently or error out for individual files
continue;
}
let entry = file?.path();
if filter(&entry) {
entries.push(Entry::new(
entry.clone(),
entry
.file_name()
.expect("Failed to read file name")
.to_os_string(),
path.clone(),
));
}
}
return Ok(entries);
}
use std::path::Path;
/// Reads the file and strips whitespace (including newlines)
/// Useful for file-based key-value store
@ -97,6 +20,12 @@ pub fn read_or_none(path: &Path) -> Option<String> {
}
}
pub fn read_extension(path: &Path, setting: &str) -> Option<String> {
let mut path = path.to_path_buf();
path.set_extension(setting);
read_or_none(&path)
}
/// Returns true when the file exists and has user exec
/// permission. Returns false otherwise.
pub fn is_executable(path: &Path) -> bool {
@ -120,31 +49,3 @@ pub fn is_executable(path: &Path) -> bool {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn can_load_db() {
let base_dir = "tests/success";
let entries = from(base_dir, is_executable).expect("Could not load db");
let entries_names: Vec<String> = entries
.iter()
.map(|x| x.name.clone().into_string().unwrap())
.collect();
let expected: Vec<String> = vec!["task", "symlink", "no_source"]
.iter()
.map(|x| x.to_string())
.collect();
assert_eq!(expected.len(), entries_names.len());
for entry in expected {
if !entries_names.contains(&entry.to_string()) {
panic!("Could not find {}", &entry);
}
}
}
}

View File

@ -14,25 +14,35 @@ fn main() -> Result<(), std::io::Error> {
let mut context = HashMap::new();
let base_dir = cmd.basedir();
let base_dir = match base_dir.canonicalize() {
Ok(p) => p.to_str().unwrap().to_string(),
let basedir = cmd.basedir();
let basedir = match basedir.canonicalize() {
Ok(p) => p,
Err(_) => {
context.insert("i18n_basedir", base_dir.to_str().unwrap());
context.insert("i18n_basedir", basedir.to_str().unwrap());
log::error("missing_basedir", Some(&context));
std::process::exit(1);
}
};
set_var("GITBUILDDIR", &base_dir);
let basedir_str = basedir.to_str().unwrap().to_string();
set_var("GITBUILDDIR", &basedir);
let mut tasks = if cmd.tasks.is_empty() {
task::from_dir(&base_dir).expect("Could not load DB")
task::from_dir(&basedir).expect("Could not load DB")
} else {
task::from_dir_and_list(&base_dir, cmd.tasks).expect("Could not load given tasks")
match task::from_dir_and_list(&basedir, cmd.tasks) {
Ok(t) => t,
Err(task::MissingTask(t)) => {
context.insert("i18n_task", &t);
log::error("unknown_arg", Some(&context));
std::process::exit(1);
}
}
};
let (config_folder, ignored_tasks) = task::config(&std::path::Path::new(&base_dir));
let (config_folder, ignored_tasks) = task::config(&basedir);
set_var("GITBUILDCONF", &config_folder);
@ -53,7 +63,7 @@ fn main() -> Result<(), std::io::Error> {
// Maybe the task has a source we should clone?
if let Some(repo) = &task.repo {
context.insert("$source", &repo.source);
let source_dir = format!("{}/.{}", base_dir, &task.name);
let source_dir = format!("{}/.{}", basedir_str, &task.name);
if task.cloned == false {
log::info("clone", Some(&context));
if !repo.clone() {
@ -81,7 +91,7 @@ fn main() -> Result<(), std::io::Error> {
}
} else {
// No source, chaneg working dir to basedir
cd(&base_dir).expect("Failed to change working dir");
cd(&basedir).expect("Failed to change working dir");
println!("Taks {} doesn't have a source, run it", task.name);
task.run_once();
}

View File

@ -2,8 +2,7 @@ use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;
use crate::db;
use crate::db::{is_executable, Entry};
use crate::db::{is_executable, read_extension};
use crate::dvcs::{Backend, Repo};
use crate::log;
@ -47,23 +46,31 @@ pub fn config(basedir: &Path) -> (PathBuf, Vec<String>) {
(basedir.join("config"), Vec::new())
}
}
impl Task {
pub fn from_entry(entry: &Entry) -> Task {
let source = entry.read_setting("source");
let dest = entry
.base_dir
.join(&format!(".{}", entry.name.to_str().expect("WTF")));
let cloned = source.clone().map_or(false, |_| dest.is_dir());
let subupdates = entry.read_setting("subupdates").is_some();
pub fn from_path(path: &Path) -> Option<Task> {
let name = path.file_name().unwrap().to_str().unwrap().to_string();
Task {
name: entry.name.to_str().unwrap().to_string(),
bin: entry.path.clone(),
// We don't return a task if:
// - the path is not a file
// - the file is not executable (can't be run)
// - the file starts with . (hidden file)
if !path.is_file() || !is_executable(&path) || name.starts_with('.') {
return None;
}
let basedir = path.parent().unwrap(); // Calling a task in / (FS root) will panic
let source = read_extension(path, "source");
let dest = source_dir_from_basedir(&basedir, &name);
let cloned = source.clone().map_or(false, |_| dest.is_dir());
let subupdates = read_extension(path, "subupdates").is_some();
Some(Task {
name,
bin: path.to_path_buf(),
// None source = None repo
repo: source.as_ref().map(|s| {
Repo::new(
Backend::from_setting(entry.read_setting("dvcs")),
Backend::from_setting(read_extension(path, "dvcs")),
s,
&dest,
subupdates,
@ -71,13 +78,13 @@ impl Task {
}),
source,
config: HashMap::new(),
branch: entry.read_setting("checkout"),
hosts: entry.read_setting("hosts").map_or(Vec::new(), |c| {
branch: read_extension(path, "checkout"),
hosts: read_extension(path, "hosts").map_or(Vec::new(), |c| {
c.split("\n").map(|line| line.to_string()).collect()
}),
cloned,
subupdates: entry.read_setting("subupdates").is_some(),
}
subupdates: read_extension(path, "subupdates").is_some(),
})
}
pub fn checkout(&self) {
@ -151,29 +158,40 @@ impl Task {
}
}
// Takes an already instanced database (ie Vec<Entry>)
// to turn into a list of Tasks
pub fn from_entries(db: Vec<Entry>) -> Vec<Task> {
db.iter().map(|x| Task::from_entry(&x)).collect()
/// Loads a task list from a given base directory. Fails if the directory
/// is not readable with std::io::Error.
pub fn from_dir(basedir: &Path) -> Result<Vec<Task>, std::io::Error> {
Ok(
basedir.read_dir()?.filter_map(|f| {
if f.is_err() {
// Dismiss individual file errors
return None;
}
return Task::from_path(&f.unwrap().path());
}).collect()
)
}
/// Returns a list of tasks, or std::io::Error
/// Reads all entries in a directory
pub fn from_dir(base_dir: &str) -> Result<Vec<Task>, std::io::Error> {
Ok(from_entries(db::from(base_dir, is_executable)?))
}
pub struct MissingTask(pub String);
/// Returns a list of tasks, or std::io::Error
/// Reads entries in a given list from a directory, fails if a requested entry doesn't exist
/// (does not load the whole folder)
pub fn from_dir_and_list(basedir: &str, list: Vec<String>) -> Result<Vec<Task>, db::Error> {
/// Loads a task list from a given base directory, taking only tasks that are in requested list.
/// Fails if the directory is not readable with std::io::Error, or if a requested task does
/// not exist.
pub fn from_dir_and_list(basedir: &Path, list: Vec<String>) -> Result<Vec<Task>, MissingTask> {
let mut tasks = Vec::new();
for task in list {
if let Some(entry) = db::entry(&basedir, is_executable, &task) {
tasks.push(Task::from_entry(&entry));
for t in list {
if let Some(task) = Task::from_path(&basedir.join(&t)) {
tasks.push(task);
} else {
return Err(db::Error::EntryNotFound(task.clone()));
return Err(MissingTask(t));
}
}
Ok(tasks)
}
/// Takes a &Path to a basedir, and a &str task_name, and return
/// the corresponding source directory as a PathBuf. Does not check
/// if the target exists.
pub fn source_dir_from_basedir(basedir: &Path, task_name: &str) -> PathBuf {
basedir.join(&format!(".{}", task_name))
}