Add support for --inbox/--inbox-folder

This commit is contained in:
southerntofu 2022-01-12 19:20:10 +01:00
parent 697173bb81
commit 34a5840aa0
4 changed files with 153 additions and 32 deletions

2
spec

@ -1 +1 @@
Subproject commit c0e3bc649743b53a19c549bb0bfd66e2b62f2866
Subproject commit 891b8aabbf4da2e25fc66df503b85266485b8020

View File

@ -22,6 +22,12 @@ pub struct Cli {
#[structopt(short = "b", long = "basedir")]
pub basedir: Option<String>,
#[structopt(long = "inbox")]
pub inbox: bool,
#[structopt(long = "inbox-folder")]
pub inboxdir: Option<String>,
//#[structopt(def)]
pub tasks: Vec<String>,
}

View File

@ -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() {
log::info("no_task", &context);
task::from_dir(&basedir, &context).expect("Could not load DB")
} 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);
}
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);
Select::All(basedir.clone())
} else {
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);
}
};

View File

@ -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<String>),
}
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<Vec<Task>, 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<String>,
/// The full Repo information
pub repo: Option<Repo>,
/// Variables for task config
pub config: HashMap<String, String>,
/// Potentially, a branch/commit to track
pub branch: Option<String>,
/// List of hosts on which this task should run
pub hosts: Vec<String>,
/// 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<String>) {
}
}
impl Task {
pub fn from_path(path: &Path, context: &log::Context) -> Option<Task> {
pub fn from_path(path: &Path, select: Select, context: &log::Context) -> Option<Task> {
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,
})
}
@ -153,6 +251,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)
.arg(&self.name)
@ -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<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(), 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<String>, context: &log::Context) -> Result<Vec<Task>, 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<String>, select: Select, context: &log::Context) -> Result<Vec<Task>, 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<String>, 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<Task> {
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.