use super::git::git;
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::time::UNIX_EPOCH;
const MARK: &str = "balls-published";
pub(super) fn mark(store: &Path, sha: &str) {
if let Ok(path) = mark_path(store) {
let _ = fs::write(path, format!("{sha}\n"));
}
}
fn mark_path(store: &Path) -> io::Result<PathBuf> {
let dir = git(store, &["rev-parse", "--git-dir"])?;
Ok(store.join(dir).join(MARK))
}
pub(super) struct Drift {
pub ahead: u64,
pub behind: u64,
pub fetched: i64,
}
pub(super) fn read(store: &Path, id: Option<&str>) -> Option<Drift> {
let path = mark_path(store).ok()?;
let sha = fs::read_to_string(&path).ok()?.trim().to_string();
let fetched = fs::metadata(&path).ok()?.modified().ok()?.duration_since(UNIX_EPOCH).ok()?.as_secs();
let count = |range: String| -> Option<u64> {
let mut args = vec!["rev-list", "--count", &range];
let file = id.map(|id| format!("tasks/{id}.md"));
if let Some(f) = &file {
args.extend(["--", f]);
}
git(store, &args).ok()?.parse().ok()
};
let fetched = i64::try_from(fetched).unwrap_or(i64::MAX);
let drift = Drift { ahead: count(format!("{sha}..HEAD"))?, behind: count(format!("HEAD..{sha}"))?, fetched };
Some(drift)
}
fn seals(n: u64) -> String {
format!("{n} seal{}", if n == 1 { "" } else { "s" })
}
fn stamp(fetched: i64, now: i64) -> String {
format!("(last fetch {} ago)", crate::reads::claim_age::humanize(now - fetched))
}
pub(super) fn op_line(store: &Path, id: Option<&str>) -> Option<String> {
let drift = read(store, id)?;
(drift.ahead > 0).then(|| format!("{}: {} unpublished", id.unwrap_or("store"), seals(drift.ahead)))
}
pub(super) fn show_line(store: &Path, id: &str, now: i64) -> String {
let body = match read(store, Some(id)) {
None => "unknown — never synced with the remote (bl sync)".to_string(),
Some(d) => format!("{} {}", state(&d), stamp(d.fetched, now)),
};
format!(" published {body}\n")
}
fn state(d: &Drift) -> String {
match (d.ahead, d.behind) {
(0, 0) => "current".to_string(),
(a, 0) => format!("{} unpublished", seals(a)),
(0, b) => format!("{} to sync", seals(b)),
(a, b) => format!("{} unpublished, {} to sync", seals(a), seals(b)),
}
}
pub(super) fn list_header(store: &Path, remote: &str, branch: &str, now: i64) -> String {
match read(store, None) {
None => format!("store: publication unknown — never synced with `{remote}` {branch} (bl sync)\n"),
Some(d) => format!("store: {} ahead, {} behind `{remote}` {branch} {}\n", d.ahead, d.behind, stamp(d.fetched, now)),
}
}
pub(super) fn render(op: &str, b: &super::Binding, id: Option<&str>, out: &mut impl Write) -> io::Result<()> {
let Some(remote) = b.remote.as_deref() else {
return Ok(());
};
let store = Path::new(&b.store);
let now = crate::log::wall();
match (op, id) {
("show", Some(id)) => write!(out, "{}", show_line(store, id, now)),
("list", _) => write!(out, "{}", list_header(store, remote, &b.tasks_branch, now)),
_ => Ok(()),
}
}
#[cfg(test)]
#[path = "drift_tests.rs"]
mod tests;