use std::fs;
use std::io::Write;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use tempfile::TempDir;
use super::Catalog;
use crate::edge::Edge;
use crate::layout::Xdg;
use crate::log::{self, Level, Log};
#[cfg(unix)]
use crate::registry::Registry;
use crate::task::{Blocker, On, Task};
use crate::verb::Verb;
pub(crate) fn edge(tmp: &Path, depth: u32) -> Edge {
Edge {
xdg: Xdg::with(&tmp.join("home"), None, Some(tmp.join("state").to_str().unwrap())),
invocation_path: tmp.join("proj"),
default_actor: "me".into(),
depth,
exe_dir: None,
path_dirs: Vec::new(),
color: false,
log_level: None,
balls_clock: None,
}
}
pub(crate) fn landing_with(edge: &Edge, plugins_toml: &str) -> PathBuf {
let landing = edge.xdg.clone_dir(&edge.invocation_path).landing();
fs::create_dir_all(landing.join("config")).unwrap();
fs::write(landing.join("config").join("plugins.toml"), plugins_toml).unwrap();
landing
}
#[cfg(unix)]
pub(crate) fn bind_script(tmp: &Path, landing: &Path, name: &str, script: &str) {
let bin = tmp.join(name);
fs::write(&bin, script).unwrap();
fs::set_permissions(&bin, fs::Permissions::from_mode(0o755)).unwrap();
Registry::at(landing).bind(name, &bin).unwrap();
}
#[cfg(windows)]
pub(crate) fn bind_script(_tmp: &Path, _landing: &Path, _name: &str, _script: &str) {
panic!("bind_script: shell-script plugins are not supported on Windows");
}
pub(crate) fn log_at(tmp: &Path, level: Level, verb: Verb) -> Log {
Log::new(tmp.join("oplog"), level, verb, log::wall)
}
pub(crate) fn log_lines(tmp: &Path) -> String {
fs::read_to_string(tmp.join("oplog")).unwrap_or_default()
}
pub(crate) fn op_log(edge: &Edge) -> String {
fs::read_to_string(edge.xdg.clone_dir(&edge.invocation_path).op_log()).unwrap_or_default()
}
pub(crate) fn task(title: &str, created: i64) -> Task {
Task { title: title.into(), created, updated: created, ..Default::default() }
}
pub(crate) fn blocker(id: &str, on: On) -> Blocker {
Blocker { id: id.into(), on }
}
pub(crate) fn git_checkout(dir: &Path, seed: &str) -> String {
use crate::delivery_repo::Project;
fs::create_dir_all(dir).unwrap();
let g = |args: &[&str]| Project::run(dir, args).unwrap();
g(&["init", "-q", "-b", "main"]);
g(&["config", "user.name", "t"]);
g(&["config", "user.email", "t@e.com"]);
fs::write(dir.join("f.txt"), seed).unwrap();
g(&["add", "-A"]);
g(&["commit", "-q", "-m", "seed"]);
Project::at(dir).root_commit().unwrap()
}
pub(crate) fn catalog(tasks: &[(&str, Task)]) -> Catalog {
let tmp = TempDir::new().unwrap();
for (id, t) in tasks {
crate::taskfile::write_task(tmp.path(), id, t).unwrap();
}
Catalog::load(tmp.path()).unwrap()
}
pub(crate) fn legacy_store_at(r: &Path, files: &[(&str, &str)]) {
fs::create_dir_all(r).unwrap();
let git = |args: &[&str]| crate::git::run(r, args, None).unwrap();
git(&["init", "-q", "-b", "main"]);
git(&["config", "user.name", "test"]);
git(&["config", "user.email", "t@x"]);
fs::write(r.join("seed"), "s\n").unwrap();
git(&["add", "-A"]);
git(&["commit", "-qm", "seed"]);
git(&["checkout", "-q", "--orphan", "balls/tasks"]);
git(&["rm", "-rfq", "."]);
let td = r.join(".balls/tasks");
fs::create_dir_all(&td).unwrap();
for (name, body) in files {
fs::write(td.join(name), body).unwrap();
}
git(&["add", "-A"]);
git(&["commit", "-qm", "legacy"]);
git(&["checkout", "-q", "main"]);
}
pub(crate) struct GitStore {
_tmp: Option<TempDir>,
dir: PathBuf,
}
pub(crate) fn git_store() -> GitStore {
let tmp = TempDir::new().unwrap();
let dir = tmp.path().to_path_buf();
let store = GitStore { _tmp: Some(tmp), dir };
store.init();
store
}
pub(crate) fn git_store_at(dir: &Path) -> GitStore {
std::fs::create_dir_all(dir).unwrap();
let store = GitStore { _tmp: None, dir: dir.to_path_buf() };
store.init();
store
}
impl GitStore {
pub(crate) fn dir(&self) -> &Path {
&self.dir
}
fn init(&self) {
let git = |args: &[&str]| crate::git::run(&self.dir, args, None).unwrap();
git(&["init", "-q"]);
git(&["config", "user.name", "test"]);
git(&["config", "user.email", "t@x"]);
}
pub(crate) fn create(&self, id: &str, task: &Task, at: i64) -> &Self {
crate::taskfile::write_task(&self.dir, id, task).unwrap();
self.commit(id, "create", at);
self
}
pub(crate) fn create_raw(&self, id: &str, content: &str, at: i64) -> &Self {
let path = crate::taskfile::task_path(&self.dir, id);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, content).unwrap();
self.commit(id, "create", at);
self
}
pub(crate) fn retire(&self, id: &str, op: &str, at: i64) -> &Self {
std::fs::remove_file(crate::taskfile::task_path(&self.dir, id)).unwrap();
self.commit(id, op, at);
self
}
pub(crate) fn claim(&self, id: &str, task: &Task, at: i64) -> &Self {
let t = Task { updated: at, ..task.clone() };
crate::taskfile::write_task(&self.dir, id, &t).unwrap();
self.commit(id, "claim", at);
self
}
pub(crate) fn note(&self, id: &str, task: &Task, note: &str, at: i64) -> &Self {
let t = Task { updated: at, ..task.clone() };
crate::taskfile::write_task(&self.dir, id, &t).unwrap();
self.seal(id, "update", Some(note), at);
self
}
fn commit(&self, id: &str, op: &str, at: i64) {
self.seal(id, op, None, at);
}
fn seal(&self, id: &str, op: &str, note: Option<&str>, at: i64) {
crate::git::run(&self.dir, &["add", "-A"], None).unwrap();
let body = note.map(|n| format!("{n}\n\n")).unwrap_or_default();
let msg = format!("{op} {id}\n\n{body}bl-protocol: 1\nbl-op: {op}\nbl-id: {id}\nbl-actor: t\n");
let date = format!("@{at} +0000");
let mut child = Command::new("git")
.arg("-C")
.arg(&self.dir)
.args(["commit", "-q", "-F", "-"])
.env("GIT_COMMITTER_DATE", &date)
.env("GIT_AUTHOR_DATE", &date)
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.unwrap();
child.stdin.take().unwrap().write_all(msg.as_bytes()).unwrap();
assert!(child.wait().unwrap().success());
}
}