diff --git a/spec b/spec index c0e3bc6..891b8aa 160000 --- a/spec +++ b/spec @@ -1 +1 @@ -Subproject commit c0e3bc649743b53a19c549bb0bfd66e2b62f2866 +Subproject commit 891b8aabbf4da2e25fc66df503b85266485b8020 diff --git a/src/cli.rs b/src/cli.rs index 2324247..31265f4 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -22,6 +22,12 @@ pub struct Cli { #[structopt(short = "b", long = "basedir")] pub basedir: Option, + #[structopt(long = "inbox")] + pub inbox: bool, + + #[structopt(long = "inbox-folder")] + pub inboxdir: Option, + //#[structopt(def)] pub tasks: Vec, } diff --git a/src/main.rs b/src/main.rs index fe5ff9f..1032aef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use std::env::{set_current_dir as cd, set_var}; +use std::process::exit; mod cli; mod db; @@ -7,6 +8,7 @@ mod log; mod task; use log::Context; +use task::Select; fn main() -> Result<(), std::io::Error> { let mut context = Context::new(); @@ -16,20 +18,30 @@ fn main() -> Result<(), std::io::Error> { set_var("GITBUILDDIR", &basedir); - let mut tasks = if cmd.tasks.is_empty() { + if (cmd.force && cmd.inbox) || (cmd.inbox && cmd.inboxdir.is_some()) || (cmd.force && cmd.inboxdir.is_some()) { + println!("CONFLICTING COMMANDS: You can only run --inbox, --inbox-dir or --forge. These options cannot be combined"); + exit(2); + } + + // Setup a filter for the tasks to load/run + let select = if cmd.inbox { + Select::Inbox(basedir.clone()) + } else if let Some(inboxdir) = cmd.inboxdir { + Select::InboxDir(basedir.clone(), inboxdir) + } else if cmd.tasks.is_empty() { log::info("no_task", &context); - task::from_dir(&basedir, &context).expect("Could not load DB") + Select::All(basedir.clone()) } else { - match task::from_dir_and_list(&basedir, cmd.tasks, &context) { - Ok(t) => t, - Err(task::MissingTask(t)) => { - // Temporarily override the global context - let mut context = context.clone(); - context.insert("$i18n_arg".to_string(), t); - log::error("unknown_arg", &context); - std::process::exit(1); - } - + Select::List(basedir.clone(), cmd.tasks.clone()) + }; + + // Load requested tasks + let mut tasks = match select.apply(&context) { + Ok(t) => t, + Err(task::MissingTask(t)) => { + context.insert("$i18n_arg".to_string(), t); + log::error("unknown_arg", &context); + exit(1); } }; diff --git a/src/task.rs b/src/task.rs index 5dc7695..4b7198f 100644 --- a/src/task.rs +++ b/src/task.rs @@ -6,18 +6,115 @@ use crate::db::{is_executable, read_extension, read_or_none}; use crate::backend::{Repo, backend}; use crate::log; +/// Defines how tasks are selected to be run +#[derive(Clone, Debug)] +pub enum Select { + /// Select all tasks (executable files) from a given basedir + All(PathBuf), + /// Select all tasks with a matching TASK.inbox from a basedir + Inbox(PathBuf), + /// Select all tasks from a basedir with a matching TASK.inbox + /// in a separate inbox directory. + InboxDir(PathBuf, String), + /// Select all tasks from a basedir that are also in a list + List(PathBuf, Vec), +} + +impl Select { + /// Removes the inbox file for the given task + pub fn clean(&self, task: &str) { + match self { + Select::Inbox(basedir) => { + std::fs::remove_file(&basedir.join(&format!("{}.inbox", task))).unwrap(); + }, + Select::InboxDir(_basedir, inboxdir) => { + std::fs::remove_file(&PathBuf::from(inboxdir).join(&format!("{}.inbox", task))).unwrap() + }, + _ => {}, + } + } + + /// Find tasks matched by a selection + pub fn apply(&self, context: &log::Context) -> Result, MissingTask> { + // TODO: Dedicated error type for specific IO errors: + // - missing basedir/select + // - permissions problem for basedir/select + match self { + // We load all executable entries from basedir + Select::All(basedir) => { + Ok(from_dir(basedir, self.clone(), context)) + }, + + // We load all entries ending with .inbox from basedir + Select::Inbox(basedir) => { + let inbox_entries = basedir.read_dir().unwrap().filter_map(|f| { + if f.is_err() { + // Dismiss individual file errors + return None; + } + let f_string = f.unwrap().file_name().to_str().unwrap().to_string(); + if ! f_string.ends_with(".inbox") || f_string.starts_with(".") { + // We're only looking for non-hidden *.inbox files + return None; + } + return Some(f_string.trim_end_matches(".inbox").to_string()); + }).collect(); + from_dir_and_list(basedir, inbox_entries, self.clone(), context) + }, + + // We load all entries ending with .inbox from inboxdir + Select::InboxDir(basedir, inboxdir) => { + let inbox_entries = PathBuf::from(inboxdir).read_dir().unwrap().filter_map(|f| { + if f.is_err() { + // Dismiss individual file errors + return None; + } + let f_string = f.unwrap().file_name().to_str().unwrap().to_string(); + if ! f_string.ends_with(".inbox") || f_string.starts_with(".") { + // We're only looking for non-hidden *.inbox files + return None; + } + return Some(f_string.trim_end_matches(".inbox").to_string()); + }).collect(); + from_dir_and_list(basedir, inbox_entries, self.clone(), context) + }, + + // Load all entries from list + Select::List(basedir, list) => { + from_dir_and_list(basedir, list.clone(), self.clone(), context) + }, + + } + } +} + +// TODO: Maybe all source/DVCS information should be moved to Repo +// so that task structure is simpler. + #[derive(Debug)] pub struct Task { + /// The filename for the task pub name: String, + /// Full path to the task pub bin: PathBuf, + /// Potentially, a source repository to track for updates pub source: Option, + /// The full Repo information pub repo: Option, + /// Variables for task config pub config: HashMap, + /// Potentially, a branch/commit to track pub branch: Option, + /// List of hosts on which this task should run pub hosts: Vec, + /// Whether the source, if any, has been cloned already pub cloned: bool, + /// Whether to track subrepository/submodule updates pub subupdates: bool, + /// The context in which to store variables for translations pub context: log::Context, + /// The selection context in which a task was created, so that running it can remove it from inbox + pub select: Select, } /// config returns an option of (settings directory, ignored tasks) as @@ -48,7 +145,7 @@ pub fn config(basedir: &Path) -> (PathBuf, Vec) { } } impl Task { - pub fn from_path(path: &Path, context: &log::Context) -> Option { + pub fn from_path(path: &Path, select: Select, context: &log::Context) -> Option { let name = path.file_name().unwrap().to_str().unwrap().to_string(); // We don't return a task if: @@ -103,6 +200,7 @@ impl Task { cloned, subupdates: read_extension(path, "subupdates").is_some(), context, + select, }) } @@ -152,6 +250,9 @@ impl Task { } self.info("run"); + + // TODO: debug message for removing inbox + self.select.clean(&self.name); let cmd_out = Command::new("bash") // TODO: no need to call bash? .arg(&self.bin) @@ -197,19 +298,6 @@ impl Task { } } -/// 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, context: &log::Context) -> Result, 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(), context); - }).collect() - ) -} pub struct MissingTask(pub String); @@ -259,19 +347,21 @@ impl SourceSet { /// Loads a task list from a given base directory, taking only tasks that are in requested list. /// Given tasks can be either a task name or a task URL. This function will panic if the basedir /// does not exist, or error if a requested task/source does not exist. -pub fn from_dir_and_list(basedir: &Path, list: Vec, context: &log::Context) -> Result, MissingTask> { - // Safe unwrap because we checked that basedir existed before - // or maybe can crash for permission problem? TODO: write tests +pub fn from_dir_and_list(basedir: &Path, list: Vec, select: Select, context: &log::Context) -> Result, MissingTask> { + // TODO: Write tests for permissions problems + + // If we're looking up specific tasks, maybe they're referenced by source + // and not by name. SourceSet allows for a source->name mapping. let sourceset = SourceSet::from(basedir).unwrap(); let mut tasks = Vec::new(); for t in list { - if let Some(task) = Task::from_path(&basedir.join(&t), context) { + if let Some(task) = Task::from_path(&basedir.join(&t), select.clone(), context) { tasks.push(task); } else { // Maybe it's not a task name, but a task URL? if let Some(list) = sourceset.tasks_for(&t) { // Hopefully safe unwrap (unless there's a source without a corresponding task?) - let task_list = list.iter().map(|t_name| Task::from_path(&basedir.join(&t_name), context).unwrap()); + let task_list = list.iter().map(|t_name| Task::from_path(&basedir.join(&t_name), select.clone(), context).unwrap()); tasks.extend(task_list); } else { return Err(MissingTask(t)); @@ -281,6 +371,19 @@ pub fn from_dir_and_list(basedir: &Path, list: Vec, context: &log::Conte Ok(tasks) } +/// 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, select: Select, context: &log::Context) -> Vec { + basedir.read_dir().unwrap().filter_map(|f| { + if f.is_err() { + // Dismiss individual file errors + return None; + } + return Task::from_path(&f.unwrap().path(), select.clone(), context); + }).collect() +} + + /// 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.