bakery: add search, completions, rollback, verify, purge, dry-run, self-update
New CLI surface, approved for review before merge: - search <query>: case-insensitive name/description substring match - completions <shell>: bash/zsh/fish/elvish/powershell via clap_complete - rollback <pkg>: restore the previously installed version from a local pre-update binary backup (not a network re-fetch — index.json's minisign signature only covers the current published version, so pinning an old version from the server would only be checkable against its unsigned per-version .sha256 sidecar, a materially weaker trust path) - verify [pkg]: recompute installed binaries' sha256 and compare against the hash recorded at install time, not a fresh index lookup (the index only has the latest release's checksum, which may not match what's actually installed) - remove --purge: additionally remove the license dir, desktop entry, and data dir, each gated through the existing confirm() prompt; config is still deliberately left alone - self-update: documented entry point for updating bakery itself - --dry-run: global flag, short-circuits right before install:: install_package in both the install and update paths - download progress: chunked read loop in manifest::fetch_bytes prints periodic \r progress on stderr when Content-Length is present and stderr is a tty - update --all output: "already at X" is now DIM with a neutral glyph instead of GREEN, plus a bold one-line summary count, so unchanged packages don't visually compete with ones that actually changed InstalledPackage gained previous_version and binary_sha256 (both #[serde(default)]) to back rollback/verify. fetch_and_place now returns the verified sha256 instead of discarding it. Also fixes a handful of pre-existing clippy lints in files this touches (manual split_once, &PathBuf-vs-&Path, derivable Default, unnecessary unwrap) surfaced by a clippy version newer than when that code was last touched — confirmed via git stash that they predate this branch. bread- utils has one more of these (suspicious_open_options in singleton.rs) left alone: the mechanical fix would truncate the PID file before the lock-held-by-another-process branch reads its contents, which would break toggle_or_kill's PID lookup, so cargo clippy -p bakery needs --no-deps until that one's fixed with actual thought.
This commit is contained in:
parent
d45fc422f2
commit
eba8cb6c44
9 changed files with 785 additions and 69 deletions
|
|
@ -17,6 +17,20 @@ pub struct InstalledPackage {
|
|||
// convention as `State.track` above.
|
||||
#[serde(default)]
|
||||
pub track: Track,
|
||||
/// The version this package was upgraded from, if any — `bakery
|
||||
/// rollback` uses this to find the matching local backup dir. `None` on
|
||||
/// a fresh first-time install. `#[serde(default)]` for the same
|
||||
/// old-shape-json reason as `track` above.
|
||||
#[serde(default)]
|
||||
pub previous_version: Option<String>,
|
||||
/// SHA-256 (hex) of each installed binary, captured at install time.
|
||||
/// `bakery verify` recomputes these from disk and compares against this
|
||||
/// recorded value rather than a fresh index lookup — the index only
|
||||
/// carries the checksum for whatever the *current latest* release is,
|
||||
/// which may not match what's actually installed. Empty on installs
|
||||
/// that predate this field.
|
||||
#[serde(default)]
|
||||
pub binary_sha256: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
|
|
@ -91,14 +105,28 @@ impl State {
|
|||
}
|
||||
}
|
||||
|
||||
fn state_base_dir() -> PathBuf {
|
||||
dirs::state_dir().unwrap_or_else(|| {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("~"))
|
||||
.join(".local/state")
|
||||
})
|
||||
}
|
||||
|
||||
fn state_path() -> PathBuf {
|
||||
dirs::state_dir()
|
||||
.unwrap_or_else(|| {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("~"))
|
||||
.join(".local/state")
|
||||
})
|
||||
.join("bakery/installed.json")
|
||||
state_base_dir().join("bakery/installed.json")
|
||||
}
|
||||
|
||||
/// Local backup dir for `pkg_name`'s `version` binaries, populated by
|
||||
/// `install::install_package` right before an update overwrites the
|
||||
/// previous binaries and consumed by `bakery rollback`. See
|
||||
/// `install::backup_current_binary`'s doc comment for why this is a local
|
||||
/// snapshot rather than a re-fetch of the old version from the server.
|
||||
pub fn backup_dir(pkg_name: &str, version: &str) -> PathBuf {
|
||||
state_base_dir()
|
||||
.join("bakery/backups")
|
||||
.join(pkg_name)
|
||||
.join(version)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -113,6 +141,8 @@ mod tests {
|
|||
services: vec![],
|
||||
installed_at: "2026-01-01T00:00:00Z".to_string(),
|
||||
track: Track::Stable,
|
||||
previous_version: None,
|
||||
binary_sha256: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,6 +196,8 @@ mod tests {
|
|||
services: vec!["bar.service".to_string()],
|
||||
installed_at: "2026-06-01T00:00:00Z".to_string(),
|
||||
track: Track::Beta,
|
||||
previous_version: Some("1.0.0".to_string()),
|
||||
binary_sha256: HashMap::from([("bar".to_string(), "abc123".to_string())]),
|
||||
});
|
||||
let json = serde_json::to_string(&state).unwrap();
|
||||
let restored: State = serde_json::from_str(&json).unwrap();
|
||||
|
|
@ -173,6 +205,8 @@ mod tests {
|
|||
assert_eq!(restored.packages["bar"].version, "2.0.0");
|
||||
assert_eq!(restored.packages["bar"].services, ["bar.service"]);
|
||||
assert_eq!(restored.packages["bar"].track, Track::Beta);
|
||||
assert_eq!(restored.packages["bar"].previous_version.as_deref(), Some("1.0.0"));
|
||||
assert_eq!(restored.packages["bar"].binary_sha256["bar"], "abc123");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -184,6 +218,26 @@ mod tests {
|
|||
assert_eq!(installed.track, Track::Stable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installed_package_previous_version_and_binary_sha256_default_on_old_shape_json() {
|
||||
// Simulates an installed.json entry written before rollback/verify
|
||||
// support existed.
|
||||
let old_shape = r#"{"name":"foo","version":"1.0.0","binaries":[],"services":[],"installed_at":"2026-01-01T00:00:00Z","track":"stable"}"#;
|
||||
let installed: InstalledPackage = serde_json::from_str(old_shape).unwrap();
|
||||
assert!(installed.previous_version.is_none());
|
||||
assert!(installed.binary_sha256.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backup_dir_is_distinct_per_package_and_version() {
|
||||
let a = backup_dir("bakery", "0.3.1");
|
||||
let b = backup_dir("bakery", "0.3.2");
|
||||
let c = backup_dir("breadhelp", "0.3.1");
|
||||
assert_ne!(a, b);
|
||||
assert_ne!(a, c);
|
||||
assert!(a.ends_with("bakery/backups/bakery/0.3.1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_lock_persists_mutation_across_reload() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue