use crate::output::emit;
use crate::repo;
use crate::{Cli, Command};
use gy_ledger::{
Actor, Error, Filter, FormatVersion, Listing, MemoryStore, NodeKind, Publication, Repository,
Result, Store, config, file, format, list, local_day_start, publish,
};
use gy_serve::server::Opened;
use std::path::{Path, PathBuf};
use std::time::Duration;
pub fn read_list(cli: &Cli, ledger: &Path) -> Result<()> {
let Command::List {
kind,
status,
targets,
grep,
actor,
since,
} = &cli.command
else {
return Err(Error::invalid("not a list"));
};
let repository = repo::open(ledger)?;
let filter = Filter {
kind: kind.as_deref().map(parse_kind).transpose()?,
status: status.clone(),
targets: targets
.as_deref()
.map(|text| repository.resolve(text))
.transpose()?,
grep: grep.clone(),
actor: actor.clone(),
since: since
.as_deref()
.map(|text| parse_since(&repository, text))
.transpose()?,
};
emit(cli.json, &list(&repository, &filter)?)
}
pub fn sync_command(cli: &Cli, root: &Path, ledger: &Path) -> Result<()> {
watchdog(ledger);
let remote = config::read(root)?.remote.ok_or_else(|| {
Error::invalid("gy.toml has no remote; gy remote sync needs one (add `remote = \"…\"`)")
})?;
emit(cli.json, &gy_ledger::sync(ledger, &remote)?)
}
fn watchdog(ledger: &Path) {
if std::env::var("GY_SYNC_BACKGROUND").ok().as_deref() != Some("1") {
return;
}
let ledger = ledger.to_path_buf();
std::thread::spawn(move || {
std::thread::sleep(Duration::from_secs(30));
gy_ledger::record_timeout(&ledger);
std::process::exit(1);
});
}
pub fn handover(cli: &Cli, root: &Path, ledger: &Path) -> Result<()> {
repo::refresh(root, ledger)?;
let repository = repo::open(ledger)?;
emit(
cli.json,
&gy_ledger::handover(&repository, cli.scope.as_deref())?,
)
}
pub fn serve(root: &Path, ledger: &Path, name: String) -> Result<()> {
let remote = config::read(root)?.remote;
if ledger.join("remote").is_file() {
let (root, ledger) = (root.to_path_buf(), ledger.to_path_buf());
std::thread::spawn(move || sync_log(&root, &ledger));
}
let watched = ledger.to_path_buf();
let ledger = ledger.to_path_buf();
gy_serve::server::serve(
Box::new(move |at| {
if !ledger.join(format::FILE).is_file() {
return Ok(empty());
}
match at {
None => repo::open(&ledger).map(Opened::Now),
Some(seq) => {
let reader = |_: &str| Some("gy-read".to_string());
file::open_at(&ledger, seq, reader)
.map(|store| Opened::At(Repository::new(store)))
}
}
}),
watched,
name,
remote,
)
}
fn sync_log(root: &Path, ledger: &Path) {
let mut rounds = gy_ledger::Rounds::new();
loop {
let at = clock();
match repo::sync_tick(root, ledger) {
repo::Tick::Out(line) => {
for note in rounds.recovered(&at) {
eprintln!("{note}");
}
eprintln!("{at} sync: {line}");
}
repo::Tick::Failed(fallback) => {
let (first, advice) = gy_ledger::sync_error(ledger).unwrap_or((fallback, None));
for note in rounds.failed(&first, advice.as_deref(), &at) {
eprintln!("{note}");
}
}
repo::Tick::Silent => {
for note in rounds.recovered(&at) {
eprintln!("{note}");
}
}
repo::Tick::Skipped => {}
}
std::thread::sleep(Duration::from_secs(10));
}
}
fn clock() -> String {
gy_ledger::local_clock()
}
fn empty() -> Opened {
let actor = Actor::new("gy-read").expect("gy-read is a valid actor");
Opened::At(Repository::new(MemoryStore::with_actor(
FormatVersion::CURRENT,
actor,
)))
}
pub fn write_publish(
cli: &Cli,
root: &Path,
ledger: &Path,
since: Option<u64>,
out: Option<&Path>,
) -> Result<()> {
if since.is_some() {
eprintln!("--since has no effect on the wiki and will be removed in a later version");
}
let repository = repo::open(ledger)?;
let writer = Actor::from_env()
.map(|actor| actor.name().to_string())
.unwrap_or_default();
let publication = publish(
&repository,
cli.scope.as_deref(),
since,
&writer,
&ledger.display().to_string(),
)?;
let out = match out {
Some(path) => path.to_path_buf(),
None => output_path(root)?
.ok_or_else(|| Error::invalid("publish needs --out or gy.toml output"))?,
};
write_publication(&out, &publication)
}
fn write_publication(out: &Path, publication: &Publication) -> Result<()> {
for scope in &publication.scopes {
let dir = out.join(&scope.name);
if dir.exists() {
std::fs::remove_dir_all(&dir)?;
}
std::fs::create_dir_all(&dir)?;
for file in &scope.files {
let path = dir.join(&file.path);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&path, &file.text)?;
}
}
Ok(())
}
fn output_path(root: &Path) -> Result<Option<PathBuf>> {
Ok(config::read(root)?.output.map(|out| root.join(out)))
}
fn parse_kind(text: &str) -> Result<NodeKind> {
let wanted = text.to_lowercase();
NodeKind::ALL
.into_iter()
.find(|kind| kind.name() == wanted || kind.prefix() == wanted)
.ok_or_else(|| {
Error::invalid(format!(
"unknown type {text}; expected one of Need, Question, Decision, Requirement, Criterion (or n, q, d, r, ac)"
))
})
}
fn parse_since<S: Store>(repository: &Repository<S>, text: &str) -> Result<u64> {
if let Ok(seq) = text.parse::<u64>() {
return Ok(seq);
}
let start = local_day_start(text)?;
let rows = match list(
repository,
&Filter {
since: Some(0),
..Default::default()
},
)? {
Listing::History(rows) => rows,
Listing::Nodes(_) => Vec::new(),
};
Ok(rows
.iter()
.filter(|row| row.at < start)
.map(|row| row.seq)
.max()
.unwrap_or(0))
}