use gy_ledger::{Actor, Error, FileStore, FormatVersion, Repository, Result, format};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
const READ_ACTOR: &str = "gy-read";
fn here(start: Option<&Path>) -> Result<PathBuf> {
match start {
Some(dir) => dir
.canonicalize()
.map_err(|_| Error::invalid(format!("no directory {}", dir.display()))),
None => Ok(std::env::current_dir()?),
}
}
pub fn init_dir(start: Option<&Path>) -> Result<PathBuf> {
here(start)
}
pub fn find_root(start: Option<&Path>) -> Result<Option<PathBuf>> {
let mut dir = here(start)?;
loop {
if dir.join("gy.toml").is_file() {
return Ok(Some(dir));
}
match dir.parent() {
Some(parent) => dir = parent.to_path_buf(),
None => return Ok(None),
}
}
}
pub fn root(start: Option<&Path>) -> Result<PathBuf> {
find_root(start)?.ok_or_else(|| {
Error::invalid(
"no gy.toml found; pass -C <dir> to name the repository,\n\
or run gy init <scope> to start one",
)
})
}
pub fn open(ledger: &Path) -> Result<Repository<FileStore>> {
if !ledger.join(format::FILE).is_file() {
return Err(Error::invalid(format!("no ledger at {}", ledger.display())));
}
let store = FileStore::open_with(ledger, |_| Some(READ_ACTOR.to_string()))?;
announce_migration(&store);
Ok(Repository::new(store))
}
pub fn open_write(ledger: &Path) -> Result<Repository<FileStore>> {
Actor::from_env()?;
if !ledger.join(format::FILE).is_file() {
std::fs::create_dir_all(ledger)?;
format::write(ledger, FormatVersion::CURRENT)?;
}
let store = FileStore::open(ledger)?;
announce_migration(&store);
Ok(Repository::new(store))
}
fn announce_migration(store: &FileStore) {
let Some((from, to)) = store.migrated() else {
return;
};
let version = env!("CARGO_PKG_VERSION");
eprintln!(
"gy {version} migrated this ledger from format {from} to {to}; what changed for you: CHANGELOG {version} Updating (https://github.com/aq2bq/gy/blob/main/CHANGELOG.md)"
);
}
pub fn background_sync(root: &Path, ledger: &Path) -> Result<()> {
if !ledger.join("remote").is_file() {
return Ok(());
}
let pid_path = ledger.join("sync.pid");
if read_pid(&pid_path).is_some_and(process_alive) {
return Ok(());
}
let log = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(ledger.join("sync.log"))?;
let mut command = Command::new(std::env::current_exe()?);
command
.arg("-C")
.arg(root)
.arg("remote")
.arg("sync")
.env("GY_SYNC_BACKGROUND", "1")
.env("GIT_TERMINAL_PROMPT", "0")
.stdin(Stdio::null())
.stdout(log.try_clone()?)
.stderr(log);
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
command.process_group(0);
}
let child = command.spawn()?;
std::fs::write(&pid_path, child.id().to_string())?;
Ok(())
}
pub fn announce_rejected(ledger: &Path) {
for notice in gy_ledger::rejected_notices(ledger) {
eprintln!("{notice}");
}
}
pub fn refresh(root: &Path, ledger: &Path) -> Result<()> {
if !ledger.join("remote").is_file() {
return Ok(());
}
let mut command = Command::new(std::env::current_exe()?);
command
.arg("-C")
.arg(root)
.arg("remote")
.arg("sync")
.env("GIT_TERMINAL_PROMPT", "0")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
command.process_group(0);
}
let mut child = command.spawn()?;
let deadline = Instant::now() + Duration::from_secs(5);
loop {
if child.try_wait()?.is_some() {
return Ok(());
}
if Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
gy_ledger::record_timeout_after(ledger, 5);
return Ok(());
}
std::thread::sleep(Duration::from_millis(50));
}
}
pub enum Tick {
Out(String),
Failed(String),
Silent,
Skipped,
}
pub fn sync_tick(root: &Path, ledger: &Path) -> Tick {
if read_pid(&ledger.join("sync.pid")).is_some_and(process_alive) {
return Tick::Skipped;
}
let Some(mut child) = spawn_sync(root) else {
return Tick::Failed("could not start gy remote sync".to_string());
};
let _ = std::fs::write(ledger.join("sync.pid"), child.id().to_string());
if !wait_capped(&mut child, ledger) {
return Tick::Failed(last_error(ledger));
}
let Ok(output) = child.wait_with_output() else {
return Tick::Failed(last_error(ledger));
};
if !output.status.success() {
return Tick::Failed(last_error(ledger));
}
let first = String::from_utf8_lossy(&output.stdout)
.lines()
.next()
.unwrap_or_default()
.trim()
.to_string();
if first.starts_with("up to date") {
Tick::Silent
} else {
Tick::Out(first)
}
}
fn spawn_sync(root: &Path) -> Option<std::process::Child> {
let mut command = Command::new(std::env::current_exe().ok()?);
command
.arg("-C")
.arg(root)
.arg("remote")
.arg("sync")
.env("GY_SYNC_BACKGROUND", "1")
.env("GIT_TERMINAL_PROMPT", "0")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null());
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
command.process_group(0);
}
command.spawn().ok()
}
fn wait_capped(child: &mut std::process::Child, ledger: &Path) -> bool {
let deadline = Instant::now() + Duration::from_secs(10);
while child.try_wait().ok().flatten().is_none() {
if Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
gy_ledger::record_timeout_after(ledger, 10);
return false;
}
std::thread::sleep(Duration::from_millis(50));
}
true
}
fn last_error(ledger: &Path) -> String {
std::fs::read_to_string(ledger.join("sync.state"))
.ok()
.and_then(|text| serde_json::from_str::<gy_ledger::SyncStatus>(&text).ok())
.and_then(|state| state.last_error)
.map(|error| error.lines().next().unwrap_or_default().to_string())
.unwrap_or_else(|| "sync failed".to_string())
}
fn read_pid(path: &Path) -> Option<u32> {
std::fs::read_to_string(path).ok()?.trim().parse().ok()
}
fn process_alive(pid: u32) -> bool {
Command::new("kill")
.arg("-0")
.arg(pid.to_string())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|status| status.success())
.unwrap_or(false)
}