Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 39 additions & 25 deletions crates/socket-patch-cli/CLI_CONTRACT.md

Large diffs are not rendered by default.

67 changes: 52 additions & 15 deletions crates/socket-patch-cli/src/commands/remove.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use clap::Args;
use socket_patch_core::api::blob_fetcher::{DIFF_ARCHIVE, PACKAGE_ARCHIVE};
use socket_patch_core::api::client::get_api_client_with_overrides;
use socket_patch_core::ledgers::hosted_pins_matching;
use socket_patch_core::manifest::cleanup_blobs::{format_bytes, ArtifactReferences};
use socket_patch_core::manifest::cleanup_blobs::{
format_bytes, format_cleanup_result_for, ArtifactReferences,
};
use socket_patch_core::manifest::operations::{read_manifest, write_manifest};
use socket_patch_core::manifest::schema::PatchManifest;
use socket_patch_core::patch::redirect::upstream::HostedPin;
Expand All @@ -21,7 +24,9 @@ use crate::commands::lock_cli::acquire_or_emit;
use crate::commands::vendored_backend::{
KeepCause, RevertedEntry, VendorRevertStep, VendoredBackend,
};
use crate::json_envelope::{Command, Envelope, EnvelopeError, PatchAction, PatchEvent, Status};
use crate::json_envelope::{
Command, Envelope, EnvelopeError, GcReport, PatchAction, PatchEvent, Status,
};
use crate::ui::short_uuid;
use crate::ui::{plural, sweep_failure};

Expand Down Expand Up @@ -962,8 +967,8 @@ pub async fn run(args: RemoveArgs) -> i32 {
&updated_manifest,
retained_not_installed.iter().copied(),
);
let mut blobs_removed = 0;
let mut archives_removed = 0;
// `None` under `--preserve-state` (no sweep ran): no `gc` in the JSON.
let mut gc: Option<GcReport> = None;
if !args.preserve_state {
let sweep = references.sweep(&socket_dir, args.common.dry_run).await;
// repair's posture: a failed pass (or a pass that could not unlink
Expand All @@ -974,9 +979,11 @@ pub async fn run(args: RemoveArgs) -> i32 {
eprintln!("Warning: {detail}");
}
}
if let Ok(r) = sweep.blobs {
blobs_removed = r.blobs_removed;
// The GC lines are one block, opened by a blank line.
let mut gc_printed = false;
if let Ok(r) = &sweep.blobs {
if loud && r.blobs_removed > 0 {
gc_printed = true;
println!(
"\n{}",
format_blob_sweep(
Expand All @@ -990,16 +997,35 @@ pub async fn run(args: RemoveArgs) -> i32 {
}
// Obsolete diff and package archives are swept whole (parity with
// repair and scan --prune).
for (dir, result) in [("diffs", sweep.diffs), ("packages", sweep.packages)] {
if let Some(detail) = sweep_failure(dir, &result) {
for (dir, noun, result) in [
("diffs", DIFF_ARCHIVE, &sweep.diffs),
("packages", PACKAGE_ARCHIVE, &sweep.packages),
] {
if let Some(detail) = sweep_failure(dir, result) {
if loud {
eprintln!("Warning: {detail}");
}
}
// The archives the sweep took are named like repair names them,
// so the human run accounts for everything `gc` reports.
if let Ok(r) = result {
archives_removed += r.blobs_removed;
if loud && r.blobs_removed > 0 {
if !gc_printed {
println!();
}
gc_printed = true;
println!(
"{}",
format_cleanup_result_for(r, args.common.dry_run, noun)
);
}
}
}
gc = Some(GcReport::from_passes(
sweep.blobs.as_ref().ok(),
sweep.diffs.as_ref().ok(),
sweep.packages.as_ref().ok(),
));
}

// The dry-run footer closes the whole preview, the blob-cleanup
Expand Down Expand Up @@ -1097,14 +1123,25 @@ pub async fn run(args: RemoveArgs) -> i32 {
// single-patch removal that happened to sweep an orphan blob.
// Consumers read the blob/rollback totals from `details`, never
// from `summary.removed`.
if blobs_removed > 0 || rollback_count > 0 || archives_removed > 0 {
env.events.push(
// The sweep's per-kind totals and byte count are also the
// envelope's `gc` (`summary.bytesFreed`), the shape every GC-running
// command prints.
let report = gc.unwrap_or_default();
if report.total_removed() > 0 || rollback_count > 0 {
let mut carrier =
PatchEvent::artifact(removal_action).with_details(serde_json::json!({
"blobsRemoved": blobs_removed,
"blobsRemoved": report.removed_blobs,
"rolledBack": rollback_count,
"archivesRemoved": archives_removed,
})),
);
"archivesRemoved": report.removed_diff_archives
+ report.removed_package_archives,
}));
if report.total_removed() > 0 {
carrier = carrier.with_bytes(report.bytes_freed);
}
env.events.push(carrier);
}
if let Some(gc) = gc {
env.set_gc(gc);
}
// Any drift-kept entry means part of the requested removal did
// NOT happen: the run is a partialFailure (exit 1) even when
Expand Down
90 changes: 64 additions & 26 deletions crates/socket-patch-cli/src/commands/repair.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use std::time::Duration;

use crate::args::{apply_env_toggles, parse_bool_flag, GlobalArgs};
use crate::commands::lock_cli::{acquire_or_emit, error_envelope};
use crate::json_envelope::{Command, Envelope, PatchAction, PatchEvent, Status};
use crate::json_envelope::{Command, Envelope, GcReport, PatchAction, PatchEvent, Status};
use crate::ui::sweep_failure;

#[derive(Args)]
Expand Down Expand Up @@ -477,9 +477,8 @@ async fn repair_inner(

let mut downloaded_count = 0usize;
let mut download_failed_count = 0usize;
let mut blobs_cleaned = 0usize;
let mut blobs_checked = 0usize;
let mut bytes_freed = 0u64;
let mut gc: Option<GcReport> = None;

// The envelope is built up-front: the vendored-artifact phase records
// its events inline; the download/cleanup aggregates are appended at
Expand Down Expand Up @@ -597,7 +596,8 @@ async fn repair_inner(
("package", PACKAGE_ARCHIVE, sweep.packages),
];
let mut results: Vec<(ArtifactNoun, CleanupResult)> = Vec::new();
for (label, noun, result) in passes {
let mut ok: [Option<usize>; 3] = [None, None, None];
for (slot, (label, noun, result)) in ok.iter_mut().zip(passes) {
// A failed cleanup — the pass aborted, or it could not unlink
// every orphan — is error output: `--silent` (suppress
// NON-error output) must not mute it, and the JSON envelope
Expand All @@ -618,15 +618,13 @@ async fn repair_inner(
);
}
if let Ok(cleanup_result) = result {
blobs_checked += cleanup_result.blobs_checked;
*slot = Some(results.len());
results.push((noun, cleanup_result));
}
}

for (_, r) in &results {
blobs_checked += r.blobs_checked;
blobs_cleaned += r.blobs_removed;
bytes_freed += r.bytes_freed;
}
let pass = |i: usize| ok[i].map(|at| &results[at].1);
gc = Some(GcReport::from_passes(pass(0), pass(1), pass(2)));
if !quiet {
if stdout_started {
println!();
Expand Down Expand Up @@ -690,25 +688,37 @@ async fn repair_inner(
));
env.mark_partial_failure();
}
if blobs_cleaned > 0 {
// `None` when no sweep ran (`--download-only`, no manifest): no `gc`.
let swept = gc.is_some();
let gc = gc.unwrap_or_default();
if gc.total_removed() > 0 {
let cleanup_action = if args.common.dry_run {
PatchAction::Verified
} else {
PatchAction::Removed
};
env.record(
PatchEvent::artifact(cleanup_action).with_details(serde_json::json!({
"count": blobs_cleaned,
"checked": blobs_checked,
})),
// Pushed directly rather than via `env.record`, as remove's GC
// carrier is: `summary.removed` / `summary.verified` count patch
// entries, and the sweep's totals live in `gc` (with the byte
// count mirrored into `summary.bytesFreed`).
env.events.push(
PatchEvent::artifact(cleanup_action)
.with_bytes(gc.bytes_freed)
.with_details(serde_json::json!({
"count": gc.total_removed(),
"checked": blobs_checked,
})),
);
}
if swept {
env.set_gc(gc);
}
Ok((
env,
RepairCounts {
downloaded: downloaded_count,
cleaned: blobs_cleaned,
bytes_freed,
cleaned: gc.total_removed(),
bytes_freed: gc.bytes_freed,
},
))
}
Expand Down Expand Up @@ -893,8 +903,26 @@ mod tests {
// The referenced blob survives; the orphan is gone.
assert!(socket.join("blobs").join(REFERENCED_HASH).exists());
assert!(!socket.join("blobs").join(&orphan_hash).exists());
// A Removed event is recorded for the swept orphan.
assert_eq!(env.summary.removed, 1);
// The sweep is the envelope's `gc` (bytes mirrored into the
// summary) plus one carrier event; `summary.removed` counts patch
// entries, so the carrier does not bump it.
assert_eq!(
env.gc,
Some(GcReport {
removed_blobs: 1,
removed_diff_archives: 0,
removed_package_archives: 0,
bytes_freed: orphan_bytes.len() as u64,
})
);
assert_eq!(env.summary.bytes_freed, orphan_bytes.len() as u64);
assert_eq!(env.summary.removed, 0);
let carrier = env
.events
.iter()
.find(|e| e.action == PatchAction::Removed)
.expect("a Removed carrier event");
assert_eq!(carrier.bytes, Some(orphan_bytes.len() as u64));
}

/// `--download-only` skips the cleanup pass, so an orphan blob survives
Expand All @@ -913,12 +941,13 @@ mod tests {
args.common.offline = false;
args.download_only = true;

let (_env, counts) =
let (env, counts) =
repair_inner(&args, &socket.join("manifest.json"), &mut None, Vec::new())
.await
.expect("repair_inner");

assert_eq!(counts.cleaned, 0, "download-only must skip cleanup");
assert_eq!(env.gc, None, "no sweep ran, so no `gc`");
assert_eq!(counts.bytes_freed, 0);
assert!(
socket.join("blobs").join(&orphan_hash).exists(),
Expand Down Expand Up @@ -974,11 +1003,20 @@ mod tests {
(orphan_diff.len() + orphan_pkg.len() + legacy_pkg.len() + stale_diff.len()) as u64,
"bytes_freed must aggregate diff + package reclaim"
);
// Cleanup is reported as a SINGLE batched `removed` artifact event whose
// `details.count` carries the tally — so the event-count summary is 1
// (`Summary::bump` increments once per event), and the 4-artifact count
// is asserted via `counts.cleaned` above and the event details here.
assert_eq!(env.summary.removed, 1, "one batched removal event");
// Cleanup is reported once, per kind, in `gc`, plus a SINGLE batched
// `removed` carrier event whose `details.count` carries the tally.
// The carrier is not a removed patch entry, so `summary.removed`
// stays 0.
assert_eq!(
env.gc,
Some(GcReport {
removed_blobs: 0,
removed_diff_archives: 2,
removed_package_archives: 2,
bytes_freed: counts.bytes_freed,
})
);
assert_eq!(env.summary.removed, 0, "the carrier bumps no counter");
let removed = env
.events
.iter()
Expand Down
Loading
Loading