use std::fs;
use std::io;
use std::path::Path;
use crate::enforce;
use crate::lifecycle::BaseChange;
use crate::message::Message;
use crate::task::Task;
use crate::taskfile::{read_task, task_path, write_task};
use crate::verb::Verb;
#[path = "change_create.rs"]
mod create;
pub use create::Create;
pub struct Occupancy {
pub verb: Verb,
pub id: String,
pub claimant: Option<String>,
pub actor: String,
pub now: i64,
pub message: Option<String>,
pub current_roots: Vec<String>,
}
impl Occupancy {
pub fn claim(id: String, actor: String, now: i64) -> Self {
Self { verb: Verb::Claim, id, claimant: Some(actor.clone()), actor, now, message: None, current_roots: Vec::new() }
}
pub fn unclaim(id: String, actor: String, now: i64) -> Self {
Self { verb: Verb::Unclaim, id, claimant: None, actor, now, message: None, current_roots: Vec::new() }
}
}
impl BaseChange for Occupancy {
fn stage(&self, dir: &Path) -> io::Result<()> {
let mut task = read_task(dir, &self.id)?;
if self.verb == Verb::Claim {
if let Some(who) = &task.claimant {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("claim: {} is already claimed by {who}", self.id),
));
}
guard_repo(&task, &self.current_roots, &self.id)?;
enforce::claim(&task, &self.id, dir)?;
} else {
enforce::gate(&task, Verb::Unclaim, &self.id, dir)?;
}
task.claimant.clone_from(&self.claimant);
task.updated = self.now;
write_task(dir, &self.id, &task)
}
fn finalize(&self, dir: &Path) -> io::Result<String> {
finalize_titled(dir, self.verb, &self.actor, &self.id, self.message.as_deref())
}
}
#[must_use]
pub(crate) fn admits(root_commit: Option<&str>, current_roots: &[String]) -> bool {
root_commit.is_none_or(|recorded| current_roots.is_empty() || current_roots.iter().any(|r| r == recorded))
}
fn guard_repo(task: &Task, current_roots: &[String], id: &str) -> io::Result<()> {
let recorded = task.root_commit.as_deref();
if admits(recorded, current_roots) {
return Ok(());
}
Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"claim: {id} belongs to the project rooted at {}, but this checkout is \
rooted at {} — claim it from that project's checkout",
recorded.unwrap_or_default(),
current_roots.join(", ")
),
))
}
pub struct Update {
pub id: String,
pub actor: String,
pub now: i64,
pub edits: Vec<FieldEdit>,
pub message: Option<String>,
}
impl BaseChange for Update {
fn stage(&self, dir: &Path) -> io::Result<()> {
let mut task = read_task(dir, &self.id)?;
enforce::gate(&task, Verb::Update, &self.id, dir)?;
for edit in &self.edits {
edit.apply(&mut task);
}
task.updated = self.now;
write_task(dir, &self.id, &task)
}
fn finalize(&self, dir: &Path) -> io::Result<String> {
finalize_titled(dir, Verb::Update, &self.actor, &self.id, self.message.as_deref())
}
fn narrated(&self) -> bool {
self.message.is_some()
}
}
#[path = "change_field.rs"]
mod field;
pub use field::FieldEdit;
pub struct Retire {
pub id: String,
pub title: String,
pub actor: String,
pub message: Option<String>,
}
impl Retire {
pub fn close(id: String, title: String, actor: String) -> Self {
Self { id, title, actor, message: None }
}
}
impl BaseChange for Retire {
fn stage(&self, dir: &Path) -> io::Result<()> {
let task = read_task(dir, &self.id)?;
enforce::close(&task, &self.id, dir)?;
fs::remove_file(task_path(dir, &self.id))
}
fn finalize(&self, _dir: &Path) -> io::Result<String> {
commit_message(Verb::Close, &self.actor, &self.id, &self.title, self.message.as_deref())
}
}
fn finalize_titled(dir: &Path, verb: Verb, actor: &str, id: &str, message: Option<&str>) -> io::Result<String> {
let title = read_task(dir, id)?.title;
commit_message(verb, actor, id, &title, message)
}
fn commit_message(verb: Verb, actor: &str, id: &str, title: &str, message: Option<&str>) -> io::Result<String> {
Message {
verb,
actor: actor.to_string(),
id: Some(id.to_string()),
subject: title.to_string(),
body: message.map(str::to_string),
}
.render()
}
#[cfg(test)]
#[path = "change_tests.rs"]
mod tests;