stowe 0.4.2

where git chokes, stowe stows - versioned, deduped big and binary files, pushed to backups you can still play (mirror) or compact blob stores (S3)
//! On-disk data model: config, file entries, manifests, commits.
//!
//! Everything is plain JSON so the repo (and the remote) stays inspectable
//! with nothing more than `cat`. The remote is a *bare* stowe repo: the same
//! `commits/` + `objects/` + `refs/main`, just without a working tree.

use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

/// `.stowe/config` - named remotes (`name -> url`) and, optionally, an explicit
/// on-disk format per remote (`name -> "mirror"|"backup"`). A remote with no
/// entry in `formats` falls back to the scheme default (local → mirror, s3 →
/// backup). `serde(default)` keeps configs written before `formats` existed
/// readable.
/// `mounts` holds an optional shell command per remote that makes it available
/// (mount the drive, bring up an sshfs). Local remotes only: an `s3://` store
/// has no path to mount.
#[derive(Serialize, Deserialize, Default)]
pub struct Config {
    #[serde(default)]
    pub remotes: BTreeMap<String, String>,
    #[serde(default)]
    pub formats: BTreeMap<String, String>,
    #[serde(default)]
    pub mounts: BTreeMap<String, String>,
}

/// One tracked file in a snapshot.
///
/// `hash` is the blake3 of the file's full content - it *is* the object's name
/// in the store, so identical content is stored once (that's the dedup).
/// `size` + `mtime` are only used to skip re-hashing files that look unchanged.
///
/// `fp` is an optional *audio* fingerprint: the blake3 of the file's decoded
/// PCM, which (unlike `hash`) is stable across tag edits. It's `None` for
/// non-audio files and only ever used by `diff` to recognise a re-tagged song
/// as a move. `serde(default)` keeps manifests written before `fp` existed
/// readable.
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct Entry {
    pub path: String,
    pub size: u64,
    pub mtime: i64,
    pub hash: String,
    #[serde(default)]
    pub fp: Option<String>,
}

/// A snapshot of the whole working tree, sorted by path.
pub type Manifest = Vec<Entry>;

/// A commit: a message, a time, the parent it descends from, and the full
/// snapshot of files at that point. Linear history only (one parent, no merges).
#[derive(Serialize, Deserialize, Clone)]
pub struct Commit {
    pub parent: Option<String>,
    pub message: String,
    /// Unix seconds.
    pub time: i64,
    pub files: Manifest,
}

/// True if `path` is `prefix` itself or sits beneath it (or `prefix` is the
/// repo root, i.e. empty).
pub(crate) fn under_prefix(path: &str, prefix: &str) -> bool {
    prefix.is_empty() || path == prefix || path.starts_with(&format!("{prefix}/"))
}

pub(crate) fn short(hash: &str) -> &str {
    &hash[..hash.len().min(10)]
}