Skip to content

Commit 245a320

Browse files
fix: tighten readable diff review flow
1 parent 27871ff commit 245a320

4 files changed

Lines changed: 99 additions & 11 deletions

File tree

crates/loc-cli/src/commands.rs

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
40904125
fn 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));

crates/loc-cli/src/diff.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ pub struct DiffReport {
213213
pub unsupported: Vec<String>,
214214
pub message: Option<String>,
215215
pub suggested_fix: Option<String>,
216-
#[serde(skip_serializing_if = "Option::is_none")]
216+
#[serde(skip_serializing)]
217217
pub readable_diff: Option<ReadableDiffOutput>,
218218
pub completed_stages: Vec<String>,
219219
}

crates/loc-cli/tests/diff.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,26 @@ fn diff_report_includes_readable_patch_for_existing_page_edit() {
310310
);
311311
}
312312

313+
#[test]
314+
fn diff_json_output_omits_readable_patch() {
315+
let fixture = DiffFixture::new();
316+
let mut store = fixture.store();
317+
let path = fixture.write_page("Roadmap.md", "# Roadmap\n\nChanged paragraph.\n");
318+
store
319+
.save_shadow(&fixture.mount_id, shadow("# Roadmap\n\nOld paragraph.\n"))
320+
.expect("save shadow");
321+
322+
let report = run_diff(&store, &path).expect("diff report");
323+
assert!(
324+
report.readable_diff.is_some(),
325+
"expected internal readable diff"
326+
);
327+
328+
let json = serde_json::to_value(&report).expect("serialize report");
329+
330+
assert!(json.get("readable_diff").is_none(), "{json:#}");
331+
}
332+
313333
#[test]
314334
fn diff_plans_local_image_media_byte_update_from_manifest() {
315335
let fixture = DiffFixture::new();
@@ -517,6 +537,12 @@ fn diff_report_includes_readable_patch_for_created_entity() {
517537
"{}",
518538
readable.text
519539
);
540+
assert!(
541+
readable.text.contains("+title: New task"),
542+
"{}",
543+
readable.text
544+
);
545+
assert!(readable.text.contains("+Status: Todo"), "{}", readable.text);
520546
assert!(readable.text.contains("+# Notes"), "{}", readable.text);
521547
assert!(
522548
readable.text.contains("+Created locally."),

crates/localityd/src/push.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1334,7 +1334,7 @@ fn readable_diff_for_existing_entity(
13341334

13351335
fn readable_diff_for_created_entity(
13361336
source_path: &Path,
1337-
body: &str,
1337+
local_text: &str,
13381338
pipeline: &PushPipelineResult,
13391339
) -> Option<locality_core::readable_diff::ReadableDiffOutput> {
13401340
let plan = pipeline.plan.as_ref()?;
@@ -1344,7 +1344,7 @@ fn readable_diff_for_created_entity(
13441344
locality_core::readable_diff::readable_diff_for_file(
13451345
locality_platform::logical_path_display(source_path),
13461346
None,
1347-
Some(body),
1347+
Some(local_text),
13481348
)
13491349
}
13501350

@@ -1662,8 +1662,8 @@ where
16621662
},
16631663
schema_validation,
16641664
);
1665-
let readable_diff =
1666-
readable_diff_for_created_entity(&relative_path, &parsed.document.body, &pipeline);
1665+
let local_text = render_canonical_markdown(&parsed.document);
1666+
let readable_diff = readable_diff_for_created_entity(&relative_path, &local_text, &pipeline);
16671667
Ok(PreparedPush {
16681668
absolute_path,
16691669
mount,
@@ -1997,8 +1997,9 @@ where
19971997
},
19981998
schema_validation,
19991999
);
2000+
let local_text = render_canonical_markdown(&parsed.document);
20002001
let readable_diff =
2001-
readable_diff_for_created_entity(&pending.projected_path, &parsed.document.body, &pipeline);
2002+
readable_diff_for_created_entity(&pending.projected_path, &local_text, &pipeline);
20022003
Ok(PreparedPush {
20032004
absolute_path,
20042005
mount,

0 commit comments

Comments
 (0)