Skip to content

Commit 79a08b0

Browse files
Finish vendored reverts through one shared step with a keep policy (#989) (#1245)
* Start refactor for #989 Assisted-by: Claude Code:claude-opus-5-5 * Finish vendored reverts through one shared step Every vendored backend copied the same revert finish sequence: return on a dry run, keep the artifact when a record drifted, return under --preserve-state, then delete the uuid dir and prune empty vendor levels. vendor::revert::finish now owns that sequence, and each backend names its keep rule as a KeepPolicy (OnDrift, OnDriftWhileReferenced, NpmFamily) instead of re-deciding it. gem, composer, Maven legacy, NuGet and pnpm move onto the helper and their copies are deleted. Revert output, warnings, error messages and exit codes are unchanged. Refs #989 Assisted-by: Claude Code:claude-opus-5-5 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 109dd0c commit 79a08b0

7 files changed

Lines changed: 404 additions & 157 deletions

File tree

‎crates/socket-patch-core/src/vendor/composer_lock.rs‎

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ use std::sync::Arc;
4444

4545
use serde_json::{json, Map, Value};
4646

47-
use crate::constants::SOCKET_DIR;
4847
use crate::manifest::schema::PatchRecord;
4948
use crate::patch::apply::PatchSources;
5049
use crate::patch::copy_tree::remove_tree;
@@ -53,16 +52,16 @@ use crate::utils::composer_version::composer_versions_equivalent;
5352
use crate::utils::fs::{atomic_write_bytes_preserving_mode, read_regular_to_string};
5453
use crate::utils::line_endings::LineEndings;
5554
use crate::utils::purl::{build_composer_purl, parse_composer_purl};
56-
use crate::utils::socket_dir::remove_tree_and_prune;
5755

5856
use super::common::{
59-
already_patched_result, any_live_file_references, copy_matches_after_hashes, done,
60-
inventory_or_warn, prune_empty_vendor_levels, refused, serialize_json,
61-
service_offline_conflict, stage_dir_for, swap_stage_into_place, synthesized_result,
57+
already_patched_result, copy_matches_after_hashes, done, inventory_or_warn,
58+
prune_empty_vendor_levels, refused, serialize_json, service_offline_conflict, stage_dir_for,
59+
swap_stage_into_place, synthesized_result,
6260
};
6361
use super::parse_memo::ParseMemo;
6462
use super::path::{parse_vendor_path, vendor_uuid_dir_rel};
6563
use super::registry_fetch::{extract_on_blocking_pool, extract_zip};
64+
use super::revert::{self, KeepPolicy};
6665
use super::service_fetch::{
6766
claim_prestaged, fetch_verified_archive, ServiceAttempt, ServicePolicy, ServiceTerminal,
6867
};
@@ -528,7 +527,6 @@ pub async fn revert_composer_opts(
528527
entry.uuid
529528
));
530529
};
531-
let uuid_dir = project_root.join(&uuid_dir_rel);
532530
let lock_path = project_root.join(COMPOSER_LOCK);
533531
let mut warnings = Vec::new();
534532

@@ -584,32 +582,18 @@ pub async fn revert_composer_opts(
584582
}
585583
}
586584

587-
let mut outcome = RevertOutcome {
585+
let outcome = RevertOutcome {
588586
kept_artifact: false,
589587
success: true,
590588
warnings,
591589
error: None,
592590
};
593-
if !dry_run {
594-
if outcome.drift_skipped()
595-
&& any_live_file_references(project_root, &[COMPOSER_LOCK], &uuid_dir_rel).await
596-
{
597-
// Drift-keep (see the fn doc): never delete a uuid dir the live
598-
// lock still routes composer at.
599-
outcome.keep_artifact(&uuid_dir_rel);
600-
} else if !keep_artifact {
601-
// `--preserve-state` (`keep_artifact`) skips only this deletion:
602-
// the artifact dir stays behind and the caller keeps the ledger
603-
// entry. The last composer entry leaves `.socket/vendor/composer/`
604-
// (and `.socket/vendor/`) empty: the shared helper prunes them so
605-
// a reverted project carries no vendor residue (non-recursive:
606-
// siblings keep them).
607-
if let Err(e) = remove_tree_and_prune(&uuid_dir, &project_root.join(SOCKET_DIR)).await {
608-
outcome.success = false;
609-
outcome.error = Some(format!("failed to remove {}: {e}", uuid_dir.display()));
610-
return outcome;
611-
}
612-
}
591+
// Drift-keep (see the fn doc): never delete a uuid dir the live lock
592+
// still routes composer at.
593+
let policy = KeepPolicy::OnDriftWhileReferenced(&[COMPOSER_LOCK]);
594+
let mut outcome = revert::finish(outcome, project_root, &uuid_dir_rel, opts, policy).await;
595+
if outcome.error.is_some() {
596+
return outcome;
613597
}
614598

615599
outcome.warnings.push(VendorWarning::new(

‎crates/socket-patch-core/src/vendor/gem.rs‎

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ use std::path::{Path, PathBuf};
5555

5656
use serde_json::Value;
5757

58-
use crate::constants::SOCKET_DIR;
5958
use crate::formats::gem::gemfile;
6059
use crate::manifest::schema::PatchRecord;
6160
use crate::patch::apply::{ApplyResult, PatchSources};
@@ -64,7 +63,6 @@ use crate::patch::path_safety::is_safe_single_segment;
6463
use crate::patch::redirect::gem_line_tail_blocks_edit;
6564
use crate::utils::fs::{atomic_write_bytes_preserving_mode, read_regular_to_string};
6665
use crate::utils::purl::{build_gem_purl, parse_gem_purl, purl_qualifier};
67-
use crate::utils::socket_dir::remove_tree_and_prune;
6866

6967
use super::common::{
7068
already_patched_result, copy_matches_after_hashes, done, failed_result, inventory_or_warn,
@@ -73,6 +71,7 @@ use super::common::{
7371
};
7472
use super::path::{parse_vendor_path, vendor_uuid_dir_rel};
7573
use super::registry_fetch::{extract_gem_data, extract_on_blocking_pool};
74+
use super::revert::{self, KeepPolicy};
7675
use super::service_fetch::{
7776
claim_prestaged, fetch_verified_archive, fetch_verified_secondary, SecondaryArtifactResult,
7877
ServiceAttempt, ServicePolicy, ServiceTerminal,
@@ -1207,10 +1206,7 @@ pub async fn revert_gem_opts(
12071206
project_root: &Path,
12081207
opts: RevertOpts,
12091208
) -> RevertOutcome {
1210-
let RevertOpts {
1211-
dry_run,
1212-
keep_artifact,
1213-
} = opts;
1209+
let dry_run = opts.dry_run;
12141210
// SECURITY: state.json is committed and tamper-able; the uuid keys the
12151211
// directory we are about to delete. Anything but the canonical uuid
12161212
// grammar is rejected fail-closed before any disk access.
@@ -1220,7 +1216,6 @@ pub async fn revert_gem_opts(
12201216
entry.uuid
12211217
));
12221218
};
1223-
let uuid_dir = project_root.join(&uuid_dir_rel);
12241219
let mut warnings = Vec::new();
12251220

12261221
// Fail-closed guard: an entry with NO wiring records (one an older
@@ -1297,36 +1292,22 @@ pub async fn revert_gem_opts(
12971292
}
12981293
}
12991294

1300-
let mut outcome = RevertOutcome {
1295+
let outcome = RevertOutcome {
13011296
kept_artifact: false,
13021297
success: true,
13031298
warnings,
13041299
error: None,
13051300
};
1306-
if dry_run {
1307-
return outcome;
1308-
}
13091301
// Drift-keep (see the fn doc): never delete a copy dir a left-alone
13101302
// record may still reference.
1311-
if outcome.drift_skipped() {
1312-
outcome.keep_artifact(&uuid_dir_rel);
1313-
return outcome;
1314-
}
1315-
// `--preserve-state` (`keep_artifact`): the artifact dir stays behind
1316-
// (and the caller keeps the ledger entry), so only the deletion is
1317-
// skipped.
1318-
if keep_artifact {
1319-
return outcome;
1320-
}
1321-
// The last gem entry leaves `.socket/vendor/gem/` (and `.socket/vendor/`)
1322-
// empty: the shared helper prunes them so a reverted project carries no
1323-
// vendor residue (non-recursive: siblings keep them).
1324-
if let Err(e) = remove_tree_and_prune(&uuid_dir, &project_root.join(SOCKET_DIR)).await {
1325-
outcome.success = false;
1326-
outcome.error = Some(format!("failed to remove {}: {e}", uuid_dir.display()));
1327-
return outcome;
1328-
}
1329-
outcome
1303+
revert::finish(
1304+
outcome,
1305+
project_root,
1306+
&uuid_dir_rel,
1307+
opts,
1308+
KeepPolicy::OnDrift,
1309+
)
1310+
.await
13301311
}
13311312

13321313
// ── Gemfile editing ──────────────────────────────────────────────────────────

‎crates/socket-patch-core/src/vendor/maven_repo.rs‎

Lines changed: 7 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,21 @@ use serde_json::Value;
1616
use sha1::Sha1;
1717
use sha2::{Digest as _, Sha256};
1818

19-
use crate::constants::SOCKET_DIR;
2019
use crate::manifest::schema::PatchRecord;
2120
use crate::patch::apply::PatchSources;
2221
use crate::utils::fs::{
2322
atomic_write_artifact, atomic_write_bytes_preserving_mode, read_regular_to_bytes,
2423
read_regular_to_string,
2524
};
2625
use crate::utils::purl::{build_maven_purl, parse_maven_purl};
27-
use crate::utils::socket_dir::remove_tree_and_prune;
2826
use crate::vendor::jvm::layout;
2927

3028
use super::common::{
31-
already_patched_result, any_live_file_references, done, failed_result, refused,
32-
synthesized_result, zip_bytes_match_after_hashes,
29+
already_patched_result, done, failed_result, refused, synthesized_result,
30+
zip_bytes_match_after_hashes,
3331
};
3432
use super::path::vendor_uuid_dir_rel;
33+
use super::revert::{self, KeepPolicy};
3534
use super::service_fetch::{service_archive_copy, ServiceCopy};
3635
use super::state::{VendorArtifact, VendorEntry, WiringAction, WiringRecord};
3736
use super::{RevertOpts, RevertOutcome, VendorOutcome, VendorServiceConfig, VendorWarning};
@@ -222,10 +221,7 @@ pub async fn revert_maven_opts(
222221
project_root: &Path,
223222
opts: RevertOpts,
224223
) -> RevertOutcome {
225-
let RevertOpts {
226-
dry_run,
227-
keep_artifact,
228-
} = opts;
224+
let dry_run = opts.dry_run;
229225
// Routed only when EVERY record is a JVM kind; the JVM revert validates
230226
// the uuid, the coordinates and each recorded path before any disk
231227
// access (state.json is tamper-able).
@@ -241,7 +237,6 @@ pub async fn revert_maven_opts(
241237
entry.uuid
242238
));
243239
};
244-
let uuid_dir = project_root.join(&uuid_dir_rel);
245240
let mut warnings = Vec::new();
246241

247242
// One wiring record today; reverse-order iteration keeps parity with the
@@ -280,38 +275,16 @@ pub async fn revert_maven_opts(
280275
}
281276
}
282277

283-
let mut outcome = RevertOutcome {
278+
let outcome = RevertOutcome {
284279
kept_artifact: false,
285280
success: true,
286281
warnings,
287282
error: None,
288283
};
289-
if dry_run {
290-
return outcome;
291-
}
292284
// Drift-keep (see the fn doc): never delete a uuid dir the live pom
293285
// still routes Maven at.
294-
if outcome.drift_skipped()
295-
&& any_live_file_references(project_root, &[PROJECT_POM], &uuid_dir_rel).await
296-
{
297-
outcome.keep_artifact(&uuid_dir_rel);
298-
return outcome;
299-
}
300-
// `--preserve-state` (`keep_artifact`): the artifact dir stays behind
301-
// (and the caller keeps the ledger entry), so only the deletion is
302-
// skipped.
303-
if keep_artifact {
304-
return outcome;
305-
}
306-
// The last maven entry leaves `.socket/vendor/maven/` (and
307-
// `.socket/vendor/`) empty: the shared helper prunes them so a
308-
// reverted project carries no vendor residue (non-recursive:
309-
// siblings keep them).
310-
if let Err(e) = remove_tree_and_prune(&uuid_dir, &project_root.join(SOCKET_DIR)).await {
311-
outcome.success = false;
312-
outcome.error = Some(format!("failed to remove {}: {e}", uuid_dir.display()));
313-
}
314-
outcome
286+
let policy = KeepPolicy::OnDriftWhileReferenced(&[PROJECT_POM]);
287+
revert::finish(outcome, project_root, &uuid_dir_rel, opts, policy).await
315288
}
316289

317290
// ── v5 JVM backend ─────────────────────────────────────────────────

‎crates/socket-patch-core/src/vendor/mod.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ mod pypi_wheel;
9595
pub mod redownload;
9696
pub mod registry_fetch;
9797
pub(crate) mod reuse;
98+
pub(crate) mod revert;
9899
pub(crate) mod service_fetch;
99100
pub mod source;
100101
#[cfg(any(test, feature = "test-fixtures"))]

‎crates/socket-patch-core/src/vendor/nuget_feed.rs‎

Lines changed: 7 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ use std::sync::Arc;
44
use crate::utils::digest::sha512_base64_of;
55
use serde_json::Value;
66

7-
use crate::constants::SOCKET_DIR;
87
use crate::manifest::schema::PatchRecord;
98
use crate::patch::apply::{ApplyResult, PatchSources};
109
use crate::patch::copy_tree::remove_tree;
@@ -13,14 +12,14 @@ use crate::utils::fs::{
1312
atomic_write_artifact, atomic_write_bytes_preserving_mode, read_regular_to_string,
1413
};
1514
use crate::utils::purl::{build_nuget_purl, parse_nuget_purl};
16-
use crate::utils::socket_dir::remove_tree_and_prune;
1715

1816
use super::common::{
19-
already_patched_result, any_live_file_references, done, prune_empty_vendor_levels,
20-
read_zip_artifact, refused, synthesized_result, zip_bytes_match_after_hashes,
17+
already_patched_result, done, prune_empty_vendor_levels, read_zip_artifact, refused,
18+
synthesized_result, zip_bytes_match_after_hashes,
2119
};
2220
use super::parse_memo::ParseMemo;
2321
use super::path::vendor_uuid_dir_rel;
22+
use super::revert::{self, KeepPolicy};
2423
use super::service_fetch::{service_archive_copy, ServiceCopy};
2524
use super::state::{
2625
write_marker_or_warn, VendorArtifact, VendorEntry, VendorMarker, WiringAction, WiringRecord,
@@ -708,10 +707,7 @@ pub async fn revert_nuget_opts(
708707
project_root: &Path,
709708
opts: RevertOpts,
710709
) -> RevertOutcome {
711-
let RevertOpts {
712-
dry_run,
713-
keep_artifact,
714-
} = opts;
710+
let dry_run = opts.dry_run;
715711
// SECURITY: state.json is committed and tamper-able; the uuid keys the
716712
// directory we are about to delete. Anything but the canonical uuid
717713
// grammar is rejected fail-closed before any disk access.
@@ -721,7 +717,6 @@ pub async fn revert_nuget_opts(
721717
entry.uuid
722718
));
723719
};
724-
let uuid_dir = project_root.join(&uuid_dir_rel);
725720
let mut warnings = Vec::new();
726721

727722
// Reverse application order: lock pin, then the (no-op) mapping audit
@@ -766,15 +761,12 @@ pub async fn revert_nuget_opts(
766761
}
767762
}
768763

769-
let mut outcome = RevertOutcome {
764+
let outcome = RevertOutcome {
770765
kept_artifact: false,
771766
success: true,
772767
warnings,
773768
error: None,
774769
};
775-
if dry_run {
776-
return outcome;
777-
}
778770
// Drift-keep (see the fn doc): never delete a uuid dir a live wiring
779771
// file still routes NuGet at. Only the root-level basenames vendor
780772
// records are probed (a tampered `../` path is never read).
@@ -786,27 +778,8 @@ pub async fn revert_nuget_opts(
786778
.collect();
787779
wired_files.sort_unstable();
788780
wired_files.dedup();
789-
if outcome.drift_skipped()
790-
&& any_live_file_references(project_root, &wired_files, &uuid_dir_rel).await
791-
{
792-
outcome.keep_artifact(&uuid_dir_rel);
793-
return outcome;
794-
}
795-
// `--preserve-state` (`keep_artifact`): the artifact dir stays behind
796-
// (and the caller keeps the ledger entry), so only the deletion is
797-
// skipped.
798-
if keep_artifact {
799-
return outcome;
800-
}
801-
// The last nuget entry leaves `.socket/vendor/nuget/` (and
802-
// `.socket/vendor/`) empty: the shared helper prunes them so a
803-
// reverted project carries no vendor residue (non-recursive:
804-
// siblings keep them).
805-
if let Err(e) = remove_tree_and_prune(&uuid_dir, &project_root.join(SOCKET_DIR)).await {
806-
outcome.success = false;
807-
outcome.error = Some(format!("failed to remove {}: {e}", uuid_dir.display()));
808-
}
809-
outcome
781+
let policy = KeepPolicy::OnDriftWhileReferenced(&wired_files);
782+
revert::finish(outcome, project_root, &uuid_dir_rel, opts, policy).await
810783
}
811784

812785
// ── materialisation (service download) ─────────────────────────

0 commit comments

Comments
 (0)