use std::io;
use std::path::Path;
use crate::actions::verbs::{Outcome, collect};
use crate::cli_outbound::{Binary, Cli};
use crate::opslog::{self, OpEntry};
use crate::xdg::Env;
const BL: &str = "bl";
const CONF: &str = "conf";
const SET: &str = "set";
const TASK_REMOTE: &str = "task-remote";
const TASK_BRANCH: &str = "task-branch";
const SHARED_REMOTE: &str = "origin";
const STEALTH_REMOTE: &str = "none";
const SHARED_BRANCH: &str = "balls/tasks";
const NONE_STDOUT: &str = "(none)";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Mode {
Shared,
Stealth,
CustomBranch(String),
}
impl Mode {
pub fn custom_branch(name: &str) -> Option<Mode> {
let name = name.trim();
if name.is_empty() || name == SHARED_BRANCH {
None
} else {
Some(Mode::CustomBranch(name.to_owned()))
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MarksState {
pub mode: Mode,
pub provenance: String,
}
pub trait ConfRunner {
fn conf(&self, project: &Path, args: &[&str]) -> io::Result<Outcome>;
}
pub struct BlConf {
cli: Cli,
}
impl BlConf {
pub fn new(cli: Cli) -> Self {
Self { cli }
}
pub fn resolve(world: &Env) -> Self {
Self::new(Cli::resolve_in_world(
Binary::Bl,
&crate::world::overrides(world),
))
}
}
impl ConfRunner for BlConf {
fn conf(&self, project: &Path, args: &[&str]) -> io::Result<Outcome> {
collect(self.cli.run_in(project, args))
}
}
fn conf_set(key: &str, value: &str) -> Vec<String> {
vec![CONF.into(), SET.into(), key.into(), value.into()]
}
pub fn plan(mode: &Mode) -> Vec<Vec<String>> {
match mode {
Mode::Shared => vec![
conf_set(TASK_REMOTE, SHARED_REMOTE),
conf_set(TASK_BRANCH, SHARED_BRANCH),
],
Mode::Stealth => vec![conf_set(TASK_REMOTE, STEALTH_REMOTE)],
Mode::CustomBranch(name) => vec![
conf_set(TASK_BRANCH, name),
conf_set(TASK_REMOTE, SHARED_REMOTE),
],
}
}
fn classify(remote: &str, branch: &str) -> Mode {
if is_stealth(remote) {
Mode::Stealth
} else if branch == SHARED_BRANCH {
Mode::Shared
} else {
Mode::CustomBranch(branch.to_owned())
}
}
fn is_stealth(remote: &str) -> bool {
remote.is_empty() || remote == NONE_STDOUT
}
fn provenance(stderr: &str) -> String {
let line = stderr.trim();
match line.rsplit_once(" from ") {
Some((_, layer)) => layer.trim().to_owned(),
None => line.to_owned(),
}
}
pub fn read(runner: &dyn ConfRunner, project: &Path) -> io::Result<Option<MarksState>> {
let remote = runner.conf(project, &[CONF, TASK_REMOTE])?;
if !remote.ok() {
return Ok(None);
}
let branch = runner.conf(project, &[CONF, TASK_BRANCH])?;
Ok(Some(MarksState {
mode: classify(remote.stdout.trim(), branch.stdout.trim()),
provenance: provenance(&remote.stderr),
}))
}
pub fn apply(
runner: &dyn ConfRunner,
state_root: &Path,
ts: &str,
project: &Path,
mode: &Mode,
) -> io::Result<Option<MarksState>> {
for argv in plan(mode) {
let refs: Vec<&str> = argv.iter().map(String::as_str).collect();
let out = runner.conf(project, &refs)?;
log_op(state_root, ts, project, &argv, &out)?;
if !out.ok() {
break;
}
}
read(runner, project)
}
fn log_op(
state_root: &Path,
ts: &str,
project: &Path,
argv: &[String],
out: &Outcome,
) -> io::Result<()> {
let mut full = Vec::with_capacity(argv.len() + 1);
full.push(BL.to_owned());
full.extend(argv.iter().cloned());
opslog::append(
state_root,
&OpEntry {
ts: ts.to_owned(),
argv: full,
cwd: project.display().to_string(),
exit: out.exit,
stdout: out.stdout.clone(),
stderr: out.stderr.clone(),
},
)
}
#[cfg(test)]
mod tests;