pub mod backfill;
pub mod fold;
pub mod reconcile;
pub mod record;
#[cfg(test)]
mod tests;
use std::fs::{File, OpenOptions};
use std::io::{Read as _, Seek as _, SeekFrom, Write as _};
use std::path::{Path, PathBuf};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
pub const LEDGER_FILE: &str = "worktrees.jsonl";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Origin {
TmDaemon,
TmCli,
Backfill,
AgentIsolation,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "event", rename_all = "snake_case")]
pub enum EventKind {
Created {
repo: PathBuf,
#[serde(default, skip_serializing_if = "Option::is_none")]
branch: Option<String>,
origin: Origin,
#[serde(default, skip_serializing_if = "Option::is_none")]
session: Option<String>,
},
Observed {
repo: PathBuf,
#[serde(default, skip_serializing_if = "Option::is_none")]
branch: Option<String>,
origin: Origin,
},
Measured {
bytes: u64,
},
Removed,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LedgerEvent {
pub ts: DateTime<Utc>,
pub path: PathBuf,
#[serde(flatten)]
pub kind: EventKind,
}
impl LedgerEvent {
pub fn now(path: PathBuf, kind: EventKind) -> Self {
Self {
ts: Utc::now(),
path,
kind,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum LedgerError {
#[error("worktree ledger: no absolute home directory to hold {LEDGER_FILE}")]
NoHome,
#[error("worktree ledger {path}: {source}")]
Io {
path: PathBuf,
source: std::io::Error,
},
#[error("worktree ledger: cannot encode event: {0}")]
Encode(#[from] serde_json::Error),
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct LedgerRead {
pub present: bool,
pub events: Vec<LedgerEvent>,
pub malformed: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorktreeLedger {
path: PathBuf,
}
impl WorktreeLedger {
pub fn at(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
pub fn under_home(home: &Path) -> Self {
Self::at(
home.join(crate::core::paths::FRAMEWORK_DIR_NAME)
.join(LEDGER_FILE),
)
}
pub fn host() -> Result<Self, LedgerError> {
dirs::home_dir()
.filter(|h| h.is_absolute())
.map(|h| Self::under_home(&h))
.ok_or(LedgerError::NoHome)
}
pub fn path(&self) -> &Path {
&self.path
}
fn io(&self, source: std::io::Error) -> LedgerError {
LedgerError::Io {
path: self.path.clone(),
source,
}
}
pub fn open_appender(&self) -> Result<LedgerAppender, LedgerError> {
if let Some(parent) = self.path.parent() {
std::fs::create_dir_all(parent).map_err(|e| self.io(e))?;
}
let mut file = OpenOptions::new()
.create(true)
.read(true)
.append(true)
.open(&self.path)
.map_err(|e| self.io(e))?;
let needs_newline = ends_without_newline(&mut file).map_err(|e| self.io(e))?;
Ok(LedgerAppender {
ledger: self.clone(),
file,
needs_newline,
})
}
pub fn append(&self, event: &LedgerEvent) -> Result<(), LedgerError> {
self.open_appender()?.append(event)
}
pub fn read(&self) -> Result<LedgerRead, LedgerError> {
let bytes = match std::fs::read(&self.path) {
Ok(bytes) => bytes,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(LedgerRead::default());
}
Err(e) => return Err(self.io(e)),
};
let mut read = LedgerRead {
present: true,
..LedgerRead::default()
};
for line in bytes
.split(|b| *b == b'\n')
.filter(|l| !l.trim_ascii().is_empty())
{
match serde_json::from_slice::<LedgerEvent>(line) {
Ok(event) => read.events.push(event),
Err(_) => read.malformed += 1,
}
}
Ok(read)
}
}
fn ends_without_newline(file: &mut File) -> std::io::Result<bool> {
if file.metadata()?.len() == 0 {
return Ok(false);
}
let mut last = [0u8; 1];
file.seek(SeekFrom::End(-1))?;
file.read_exact(&mut last)?;
Ok(last[0] != b'\n')
}
#[derive(Debug)]
pub struct LedgerAppender {
ledger: WorktreeLedger,
file: File,
needs_newline: bool,
}
impl LedgerAppender {
pub fn append(&mut self, event: &LedgerEvent) -> Result<(), LedgerError> {
let mut line = Vec::new();
if self.needs_newline {
line.push(b'\n');
}
line.extend(serde_json::to_vec(event)?);
line.push(b'\n');
let written = self.file.write_all(&line).and_then(|()| self.file.flush());
self.needs_newline = written.is_err();
written.map_err(|e| self.ledger.io(e))
}
}
pub fn ledger_key(path: &Path) -> PathBuf {
std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
}