@@ -3829,7 +3829,13 @@ fn push(args: &[String], json: bool) -> i32 {
38293829 let spinner_config =
38303830 spinner_config_for_command ( "push" , & target_label, json, stderr_is_terminal) ;
38313831 let report = match with_terminal_spinner ( spinner_config. clone ( ) , || {
3832- run_push_target_command ( & mut store, & state_root, target. clone ( ) , options. clone ( ) )
3832+ run_push_target_command (
3833+ & mut store,
3834+ & state_root,
3835+ target. clone ( ) ,
3836+ options. clone ( ) ,
3837+ None ,
3838+ )
38333839 } ) {
38343840 Ok ( report) => report,
38353841 Err ( error) => {
@@ -3855,7 +3861,13 @@ fn push(args: &[String], json: bool) -> i32 {
38553861 let mut approved = options. clone ( ) ;
38563862 approved. assume_yes = true ;
38573863 match with_terminal_spinner ( spinner_config, || {
3858- run_push_target_command ( & mut store, & state_root, target. clone ( ) , approved)
3864+ run_push_target_command (
3865+ & mut store,
3866+ & state_root,
3867+ target. clone ( ) ,
3868+ approved,
3869+ Some ( & report) ,
3870+ )
38593871 } ) {
38603872 Ok ( report) => report,
38613873 Err ( error) => {
@@ -3993,9 +4005,19 @@ fn run_push_target_command(
39934005 state_root : & Path ,
39944006 target_path : PathBuf ,
39954007 options : PushOptions ,
4008+ expected_confirmation_preview : Option < & PushReport > ,
39964009) -> Result < PushReport , PushCommandError > {
39974010 let preview = run_push_with_state_root ( store, & target_path, options. clone ( ) , Some ( state_root) )
39984011 . map_err ( PushCommandError :: from_diff) ?;
4012+ if let Some ( expected) = expected_confirmation_preview {
4013+ if !push_confirmation_preview_matches_displayed ( expected, & preview) {
4014+ return Err ( PushCommandError :: new (
4015+ "push_plan_changed" ,
4016+ "push plan changed after the confirmation preview; rerun `loc push` to review the current diff" ,
4017+ 4 ,
4018+ ) ) ;
4019+ }
4020+ }
39994021 if preview. pipeline_action != "proceed_to_apply" {
40004022 return Ok ( preview) ;
40014023 }
@@ -4087,6 +4109,19 @@ fn push_preview_plan_matches(cli_preview: &PushReport, daemon_preview: &PushRepo
40874109 && cli_preview. guardrail == daemon_preview. guardrail
40884110}
40894111
4112+ fn push_confirmation_preview_matches_displayed (
4113+ displayed : & PushReport ,
4114+ refreshed : & PushReport ,
4115+ ) -> bool {
4116+ displayed. path == refreshed. path
4117+ && displayed. mount_id == refreshed. mount_id
4118+ && displayed. entity_id == refreshed. entity_id
4119+ && displayed. validation == refreshed. validation
4120+ && displayed. plan == refreshed. plan
4121+ && displayed. guardrail == refreshed. guardrail
4122+ && displayed. readable_diff == refreshed. readable_diff
4123+ }
4124+
40904125fn should_prompt_for_push_confirmation (
40914126 report : & PushReport ,
40924127 options : & PushOptions ,
@@ -7156,9 +7191,10 @@ mod tests {
71567191 mounted_projection_preflight_error, notion_authorize_url, notion_oauth_broker_config,
71577192 print_push_confirmation_preview, projection_mode_for_target,
71587193 projection_usage_options_for_target, prompt_for_push_confirmation,
7159- pull_direct_fallback_error, should_prompt_for_push_confirmation,
7160- should_refresh_notion_url_search, spinner_config_for_command, spinner_enabled,
7161- status as run_status_command, validate_virtual_projection_registration, write_log_report,
7194+ pull_direct_fallback_error, push_confirmation_preview_matches_displayed,
7195+ should_prompt_for_push_confirmation, should_refresh_notion_url_search,
7196+ spinner_config_for_command, spinner_enabled, status as run_status_command,
7197+ validate_virtual_projection_registration, write_log_report,
71627198 } ;
71637199
71647200 #[ test]
@@ -7868,6 +7904,31 @@ mod tests {
78687904 ) ;
78697905 }
78707906
7907+ #[ test]
7908+ fn push_confirmation_preview_match_includes_readable_diff ( ) {
7909+ let mut displayed = push_report ( "confirm_plan" ) ;
7910+ displayed. readable_diff = Some ( locality_core:: readable_diff:: ReadableDiffOutput {
7911+ files : Vec :: new ( ) ,
7912+ text : "diff --locality a/Roadmap.md b/Roadmap.md\n -Old\n +New\n " . to_string ( ) ,
7913+ } ) ;
7914+ let mut refreshed = displayed. clone ( ) ;
7915+ refreshed. action = "apply_not_implemented" . to_string ( ) ;
7916+ refreshed. pipeline_action = "proceed_to_apply" . to_string ( ) ;
7917+
7918+ assert ! ( push_confirmation_preview_matches_displayed(
7919+ & displayed, & refreshed
7920+ ) ) ;
7921+
7922+ refreshed. readable_diff = Some ( locality_core:: readable_diff:: ReadableDiffOutput {
7923+ files : Vec :: new ( ) ,
7924+ text : "diff --locality a/Roadmap.md b/Roadmap.md\n -Old\n +Different\n " . to_string ( ) ,
7925+ } ) ;
7926+
7927+ assert ! ( !push_confirmation_preview_matches_displayed(
7928+ & displayed, & refreshed
7929+ ) ) ;
7930+ }
7931+
78717932 #[ test]
78727933 fn spinner_is_only_enabled_for_human_terminal_output ( ) {
78737934 assert ! ( spinner_enabled( false , true ) ) ;
0 commit comments