@@ -914,8 +914,9 @@ fn format_save_summary(
914914
915915/// The summary after a single-uuid save. `what` is `"Patch"` or `"Patch
916916/// record"`. `ends_run` says an unchanged record really ends the run (the
917- /// agent path skips apply); the vendored path still runs its vendor step,
918- /// so it must not promise "nothing to update".
917+ /// agent path under `--save-only`); the agent path still re-applies an
918+ /// unchanged record and the vendored path still runs its vendor step, so
919+ /// neither may promise "nothing to update".
919920fn format_single_save (
920921 what : & str ,
921922 action : & PatchAction ,
@@ -1653,6 +1654,10 @@ struct FetchBatch {
16531654 /// Selection size after installed-release narrowing.
16541655 found : usize ,
16551656 skipped : usize ,
1657+ /// Manifest store only: the `skipped` patches whose same uuid is
1658+ /// already recorded — still owed a nested apply, since the installed
1659+ /// copy may have been reinstalled since the record was written.
1660+ already_recorded : usize ,
16561661 failed : usize ,
16571662 /// Fetched, recordable patches in selection order.
16581663 fetched : Vec < FetchedPatch > ,
@@ -1843,6 +1848,7 @@ async fn fetch_selected_patches(
18431848 let mut batch = FetchBatch {
18441849 found : selected. len ( ) ,
18451850 skipped : 0 ,
1851+ already_recorded : 0 ,
18461852 failed : 0 ,
18471853 fetched : Vec :: new ( ) ,
18481854 reused : Vec :: new ( ) ,
@@ -1997,6 +2003,7 @@ async fn fetch_selected_patches(
19972003 "action" : "skipped" ,
19982004 } ) ) ;
19992005 batch. skipped += 1 ;
2006+ batch. already_recorded += 1 ;
20002007 continue ;
20012008 }
20022009
@@ -2432,10 +2439,16 @@ pub async fn download_and_apply_patches_with(
24322439 return ( 1 , serde_json:: json!( { "status" : "error" , "error" : msg } ) ) ;
24332440 }
24342441 }
2442+ // Every selected patch that is now recorded is owed the nested apply:
2443+ // the fetched ones AND the already-recorded (`skipped`) ones, whose
2444+ // installed copy may be pristine again after a reinstall or a failed
2445+ // earlier apply (#454). Apply is idempotent on already-patched files,
2446+ // so an in-sync re-run stays a no-op on disk.
2447+ let to_apply = downloaded + batch. already_recorded ;
24352448 // The lock outlives the manifest write only when a nested apply follows
24362449 // (it is handed the guard and releases it after its last mutation);
24372450 // otherwise nothing more is written and it is released here.
2438- let apply_lock = if !params. save_only && downloaded > 0 {
2451+ let apply_lock = if !params. save_only && to_apply > 0 {
24392452 Some ( guard)
24402453 } else {
24412454 drop ( guard) ;
@@ -2476,21 +2489,21 @@ pub async fn download_and_apply_patches_with(
24762489 . await ;
24772490 }
24782491
2479- // An apply step that ran (patches were added , not --save-only) but
2480- // failed is a partial failure too — not just download failures. The
2492+ // An apply step that ran (recorded patches selected , not --save-only)
2493+ // but failed is a partial failure too — not just download failures. The
24812494 // `status` field must agree with `exit_code`; reporting `success`
24822495 // alongside a non-zero exit code misleads JSON consumers (the scan
24832496 // wrapper recomputes status from the exit code for exactly this
24842497 // reason, but `get` surfaces this envelope directly).
2485- let apply_failed = !apply_succeeded && downloaded > 0 && !params. save_only ;
2498+ let apply_failed = !apply_succeeded && to_apply > 0 && !params. save_only ;
24862499 let ( status, exit_code) = run_outcome ( batch. failed > 0 , apply_failed) ;
24872500 let mut result_json = serde_json:: json!( {
24882501 "status" : status,
24892502 "found" : batch. found,
24902503 "downloaded" : downloaded,
24912504 "skipped" : batch. skipped,
24922505 "failed" : batch. failed,
2493- "applied" : if apply_succeeded { downloaded } else { 0 } ,
2506+ "applied" : if apply_succeeded { to_apply } else { 0 } ,
24942507 "updated" : updated,
24952508 "patches" : batch. patches_json,
24962509 } ) ;
@@ -2527,6 +2540,12 @@ pub async fn run(args: GetArgs) -> i32 {
25272540 } else {
25282541 super :: scan:: ScanMode :: Hosted
25292542 } ) ;
2543+ // Global installs have no project lockfile: an explicit hosted or
2544+ // vendored mode would rewire the cwd project, not the global copy.
2545+ if let Some ( conflict) = super :: global_mode_conflict ( & args. common , mode) {
2546+ report_error ( args. common . json , conflict) ;
2547+ return 2 ;
2548+ }
25302549 if args. save_only && mode != super :: scan:: ScanMode :: Agent {
25312550 report_error (
25322551 args. common . json ,
@@ -3390,9 +3409,12 @@ async fn save_and_apply_patch(args: &GetArgs, client: &ApiClient, patch: &PatchR
33903409 Err ( code) => return code,
33913410 } ;
33923411 let changed = action != PatchAction :: Skipped ;
3393- // Carried into the nested apply when one follows (it releases the lock
3394- // after its last mutation), released here otherwise.
3395- let apply_lock = if !args. save_only && changed {
3412+ // The record is now in the manifest whatever `action` says, so the
3413+ // nested apply follows unless `--save-only`: a same-uuid re-get must
3414+ // still reconcile an installed copy that was reinstalled pristine since
3415+ // it was recorded (#454). Carried into the nested apply (it releases
3416+ // the lock after its last mutation), released here otherwise.
3417+ let apply_lock = if !args. save_only {
33963418 Some ( guard)
33973419 } else {
33983420 drop ( guard) ;
@@ -3427,7 +3449,14 @@ async fn save_and_apply_patch(args: &GetArgs, client: &ApiClient, patch: &PatchR
34273449 if !quiet {
34283450 eprintln ! (
34293451 "{}" ,
3430- format_single_save( "Patch" , & action, & manifest_path, & patch. purl, true )
3452+ format_single_save(
3453+ "Patch" ,
3454+ & action,
3455+ & manifest_path,
3456+ & patch. purl,
3457+ // An unchanged record ends the run only when no apply follows.
3458+ apply_lock. is_none( )
3459+ )
34313460 ) ;
34323461 }
34333462
@@ -3446,11 +3475,11 @@ async fn save_and_apply_patch(args: &GetArgs, client: &ApiClient, patch: &PatchR
34463475 . await ;
34473476 }
34483477
3449- // The apply step ran (patch added, not --save-only) but failed →
3478+ // The apply step ran (not --save-only) but failed →
34503479 // partial failure. The `status` field must agree with the exit code
34513480 // returned below; a hardcoded `success` alongside a non-zero exit
34523481 // misleads JSON consumers.
3453- let apply_failed = !apply_succeeded && changed && !args. save_only ;
3482+ let apply_failed = !apply_succeeded && !args. save_only ;
34543483 // No "download failed" concept here — a blob failure early-returns
34553484 // with status `error` above — so only the apply step can degrade us.
34563485 let ( status, exit_code) = run_outcome ( false , apply_failed) ;
0 commit comments