feat: scope local reviews to comparisons

This commit is contained in:
2026-10-25 01:00:58 +02:00
parent 815fcccbe8
commit 448c3bf477
15 changed files with 760 additions and 72 deletions
+323 -2
View File
@@ -3017,6 +3017,15 @@ fn diff_preserves_parked_buffers_visual_carets_and_insert_history() {
}
fn working_diff(editor: &mut Editor, root: &Path, paths: &[&str]) -> Box<[(PathBuf, String)]> {
working_diff_with_revisions(editor, root, paths, ["base", "working-snapshot"])
}
fn working_diff_with_revisions(
editor: &mut Editor,
root: &Path,
paths: &[&str],
revisions: [&str; 2],
) -> Box<[(PathBuf, String)]> {
use crate::diff::{Catalog, Category, ChangedFile, Comparison, FileStatus, RepoKind, Revision};
editor.execute("diff");
let (id, _) = editor.take_diff_request().unwrap();
@@ -3070,7 +3079,7 @@ fn working_diff(editor: &mut Editor, root: &Path, paths: &[&str]) -> Box<[(PathB
Ok(Outcome::Comparison(Comparison {
blame: Default::default(),
root: root.canonicalize().unwrap(),
revisions: ["base".into(), "working-snapshot".into()],
revisions: revisions.map(str::to_owned),
before: "base".into(),
after: "Working".into(),
editable_paths: paths.iter().map(|path| PathBuf::from(*path)).collect(),
@@ -3081,6 +3090,16 @@ fn working_diff(editor: &mut Editor, root: &Path, paths: &[&str]) -> Box<[(PathB
}
fn historical_diff(editor: &mut Editor, root: &Path, before: Vec<String>, after: Vec<String>) {
historical_diff_with_revisions(editor, root, before, after, ["base", "head"]);
}
fn historical_diff_with_revisions(
editor: &mut Editor,
root: &Path,
before: Vec<String>,
after: Vec<String>,
revisions: [&str; 2],
) {
use crate::diff::{Catalog, Category, ChangedFile, Comparison, FileStatus, RepoKind, Revision};
editor.execute("diff");
let (id, _) = editor.take_diff_request().unwrap();
@@ -3108,7 +3127,7 @@ fn historical_diff(editor: &mut Editor, root: &Path, before: Vec<String>, after:
Ok(Outcome::Comparison(Comparison {
blame: Default::default(),
root: root.to_owned(),
revisions: ["base".into(), "head".into()],
revisions: revisions.map(str::to_owned),
before: "base".into(),
after: "head".into(),
editable_paths: Default::default(),
@@ -3154,6 +3173,305 @@ fn source_review_editor(directory: &Directory, text: &str) -> (Editor, PathBuf,
(editor, root, path)
}
#[test]
fn comparison_reviews_use_ordered_resolved_identities_and_resume_only_the_same_pair() {
use crate::review::{Store, Verdict};
let directory = Directory::new();
let (mut editor, root, _) = source_review_editor(&directory, "source\n");
historical_diff(&mut editor, &root, vec!["old".into()], vec!["new".into()]);
editor.execute("comment");
type_text(&mut editor, "iPair feedback");
editor.handle(Input::Escape);
editor.execute("w");
editor.handle(Input::Escape);
editor.execute("diff.review.toggle");
editor.execute("review.verdict looks_good");
let original = editor.local_review.document.clone();
editor.handle(Input::Escape);
for pair in [["other", "head"], ["base", "other"], ["head", "base"]] {
historical_diff_with_revisions(
&mut editor,
&root,
vec!["old".into()],
vec!["new".into()],
pair,
);
assert_eq!(
editor.local_review.document.comparison,
Some(pair.map(str::to_owned))
);
assert!(editor.local_review.document.comments.is_empty());
assert!(editor.local_review.document.verdict.is_none());
assert!(editor.local_review.document.reviewed_files.is_empty());
assert!(editor.workspace_diff().unwrap().reviewed_files.is_empty());
editor.execute("review.verdict changes_requested");
assert_ne!(editor.local_review.document.storage_id, original.storage_id);
editor.handle(Input::Escape);
}
historical_diff(&mut editor, &root, vec!["old".into()], vec!["new".into()]);
assert_eq!(editor.local_review.document.storage_id, original.storage_id);
assert_eq!(
editor.local_review.document.comments[0].body,
"Pair feedback"
);
assert_eq!(
editor.local_review.document.verdict,
Some(Verdict::LooksGood)
);
assert_eq!(
editor.workspace_diff().unwrap().reviewed_files,
BTreeSet::from([0])
);
assert_eq!(
Store::open(&root.join("reviews"))
.unwrap()
.list(&root)
.unwrap()
.len(),
4
);
}
#[test]
fn source_and_comparison_reviews_remain_separate_in_shared_panes_and_tabs() {
let directory = Directory::new();
let (mut editor, root, _) = source_review_editor(&directory, "source\n");
let source = editor.pane_id;
editor.execute("comment");
type_text(&mut editor, "iSource feedback");
editor.handle(Input::Escape);
editor.execute("w");
let source_id = editor.local_review.document.storage_id;
editor.handle(Input::Escape);
editor.split_pane(SplitAxis::Vertical);
let comparison_pane = editor.pane_id;
historical_diff(&mut editor, &root, vec!["old".into()], vec!["new".into()]);
editor.execute("comment");
type_text(&mut editor, "iComparison feedback");
editor.handle(Input::Escape);
editor.execute("w");
let comparison_id = editor.local_review.document.storage_id;
assert_ne!(comparison_id, source_id);
editor.handle(Input::Escape);
editor.select_pane(source);
editor.execute("review.edit 1");
assert_eq!(editor.local_review.document.storage_id, source_id);
assert!(editor.local_review.document.comparison.is_none());
assert_eq!(editor.context().text, "Source feedback");
editor.handle(Input::Escape);
editor.select_pane(comparison_pane);
editor.execute("review.edit 1");
assert_eq!(editor.local_review.document.storage_id, comparison_id);
assert_eq!(editor.context().text, "Comparison feedback");
editor.handle(Input::Escape);
editor.select_pane(source);
editor.execute("tab.new");
editor.execute("review.edit 1");
assert_eq!(editor.local_review.document.storage_id, source_id);
assert_eq!(editor.context().text, "Source feedback");
editor.handle(Input::Escape);
historical_diff(&mut editor, &root, vec!["old".into()], vec!["new".into()]);
assert_eq!(editor.local_review.document.storage_id, comparison_id);
editor.execute("tab.previous");
editor.select_pane(comparison_pane);
editor.execute("review.edit 1");
assert_eq!(editor.context().text, "Comparison feedback");
}
#[test]
fn review_selectors_and_explicit_opens_reject_other_comparison_and_source_scopes() {
use crate::review::{Review, Store};
let directory = Directory::new();
let (mut editor, root, _) = source_review_editor(&directory, "source\n");
let mut store = Store::open(&root.join("reviews")).unwrap();
let mut source = Review::new(root.clone());
source.summary = "source-only".into();
store.save(&mut source).unwrap();
let mut other = Review::new(root.clone());
other.comparison = Some(["different".into(), "head".into()]);
other.summary = "other-pair-only".into();
store.save(&mut other).unwrap();
let other_json = root.join("other.json");
other.save(&other_json).unwrap();
let source_json = root.join("source.json");
source.save(&source_json).unwrap();
historical_diff(&mut editor, &root, vec!["old".into()], vec!["new".into()]);
editor.execute("review.verdict looks_good");
let current = editor.local_review.document.clone();
for command in [
format!("review.open {}", source.storage_id.unwrap()),
format!("review.open {}", other.storage_id.unwrap()),
format!("review.open {}", source_json.display()),
format!("review.open {}", other_json.display()),
] {
editor.execute(&command);
assert!(
editor
.message()
.contains("different comparison or source scope")
);
assert_eq!(editor.local_review.document.storage_id, current.storage_id);
assert_eq!(editor.local_review.document.comparison, current.comparison);
assert_eq!(editor.local_review.document.verdict, current.verdict);
}
editor.execute("review.new");
let second_id = editor.local_review.document.storage_id;
editor.execute("review.open");
assert!(!editor.context().text.contains("source-only"));
assert!(!editor.context().text.contains("other-pair-only"));
editor.handle(Input::Enter);
assert_eq!(editor.local_review.document.storage_id, second_id);
editor.execute(&format!("review.open {}", current.storage_id.unwrap()));
assert_eq!(editor.local_review.document.storage_id, current.storage_id);
}
#[test]
fn dirty_review_drafts_guard_scope_replacement_new_reviews_and_mode_changes() {
let directory = Directory::new();
let (mut editor, root, _) = source_review_editor(&directory, "source\n");
let source = editor.pane_id;
editor.split_pane(SplitAxis::Vertical);
let comparison_pane = editor.pane_id;
historical_diff(&mut editor, &root, vec!["old".into()], vec!["new".into()]);
editor.execute("comment");
type_text(&mut editor, "iUnsaved feedback");
editor.handle(Input::Escape);
let draft = editor.buffer_id();
let panel = editor.review_pane.unwrap();
let scope = editor.local_review.document.comparison.clone();
editor.execute("review.new");
assert!(editor.message().contains("Save the current local review"));
assert_eq!(editor.context().text, "Unsaved feedback");
editor.select_pane(source);
editor.execute("review.overview");
assert!(editor.message().contains("changing review scopes"));
assert_eq!(editor.local_review.document.comparison, scope);
editor.select_pane(comparison_pane);
let comparison = editor.workspace_diff().unwrap().comparison_id;
editor.act(Action::DiffModeToggle);
assert_eq!(editor.workspace_diff().unwrap().comparison_id, comparison);
assert!(editor.workspace_diff().unwrap().comparison.is_some());
editor.execute("diff");
assert_eq!(editor.workspace_diff().unwrap().comparison_id, comparison);
editor.select_pane(panel);
assert_eq!(editor.buffer_id(), draft);
assert_eq!(editor.context().text, "Unsaved feedback");
editor.execute("w");
editor.handle(Input::Escape);
editor.select_pane(source);
editor.execute("review.overview");
assert!(editor.local_review.document.comparison.is_none());
assert!(editor.local_review.document.comments.is_empty());
}
#[test]
fn working_refresh_keeps_review_scope_and_drafts_but_explicit_reopen_uses_new_identities() {
let directory = Directory::new();
let (mut editor, root, _) = source_review_editor(&directory, "source\n");
fs::write(root.join("file.txt"), "original\n").unwrap();
working_diff(&mut editor, &root, &["file.txt"]);
editor.handle(Input::Enter);
editor.execute("comment");
type_text(&mut editor, "iWorking feedback");
editor.handle(Input::Escape);
editor.execute("w");
editor.handle(Input::Escape);
let original_id = editor.local_review.document.storage_id;
let scope = editor.local_review.document.comparison.clone();
editor.execute("review.summary");
type_text(&mut editor, "iUnsaved working summary");
editor.handle(Input::Escape);
let panel = editor.review_pane.unwrap();
let owner = editor.diff_owner.unwrap();
editor.queue_checkout_refreshes(true);
let (_, request, _) = editor.take_next_diff_request().unwrap();
let mut replacement = editor.with_pane(owner, |editor| {
editor.diff.as_mut().unwrap().comparison.take().unwrap()
});
replacement.revisions[1] = "working:changed".into();
replacement.files[0].after = vec!["refreshed".into()];
assert!(editor.apply_diff_result(request, Ok(Outcome::CheckoutRefresh(Some(replacement)))));
assert_eq!(editor.local_review.document.comparison, scope);
assert_eq!(editor.local_review.document.storage_id, original_id);
assert_eq!(
editor.local_review.document.comments[0].body,
"Working feedback"
);
assert!(editor.current_review_dirty());
editor.select_pane(panel);
assert_eq!(editor.context().text, "Unsaved working summary");
editor.execute("w");
editor.handle(Input::Escape);
working_diff_with_revisions(
&mut editor,
&root,
&["file.txt"],
["base", "working:changed"],
);
assert!(editor.local_review.document.comments.is_empty());
assert!(editor.local_review.document.summary.is_empty());
editor.execute("review.verdict looks_good");
assert_ne!(editor.local_review.document.storage_id, original_id);
editor.handle(Input::Escape);
working_diff(&mut editor, &root, &["file.txt"]);
assert_eq!(editor.local_review.document.storage_id, original_id);
assert_eq!(
editor.local_review.document.summary,
"Unsaved working summary"
);
}
#[test]
fn moved_bookmark_selects_new_review_and_dirty_draft_blocks_endpoint_replacement() {
use crate::diff::Comparison;
let directory = Directory::new();
let (mut editor, root, _) = source_review_editor(&directory, "source\n");
historical_diff(&mut editor, &root, vec!["old".into()], vec!["new".into()]);
editor.execute("review.verdict looks_good");
let original_id = editor.local_review.document.storage_id;
editor.handle(Input::Escape);
editor.execute("review.summary");
type_text(&mut editor, "iBookmark draft");
editor.handle(Input::Escape);
let panel = editor.review_pane.unwrap();
let moved = || Comparison {
revisions: ["base".into(), "moved-head".into()],
root: root.clone(),
before: "base".into(),
after: "head".into(),
blame: Default::default(),
editable_paths: Default::default(),
files: Vec::new(),
};
editor.queue_checkout_refreshes(false);
let (_, request, _) = editor.take_next_diff_request().unwrap();
assert!(editor.apply_diff_result(request, Ok(Outcome::BookmarkRefresh(moved()))));
assert_eq!(editor.local_review.document.storage_id, original_id);
assert_eq!(
editor
.workspace_diff()
.unwrap()
.comparison
.as_ref()
.unwrap()
.revisions[1],
"head"
);
editor.select_pane(panel);
assert_eq!(editor.context().text, "Bookmark draft");
editor.execute("w");
editor.handle(Input::Escape);
editor.queue_checkout_refreshes(false);
let (_, request, _) = editor.take_next_diff_request().unwrap();
assert!(editor.apply_diff_result(request, Ok(Outcome::BookmarkRefresh(moved()))));
assert_eq!(
editor.local_review.document.comparison,
Some(["base".into(), "moved-head".into()])
);
assert!(editor.local_review.document.verdict.is_none());
assert!(editor.local_review.document.summary.is_empty());
}
#[test]
fn source_review_drafts_guard_source_pane_close_and_saved_panels_retarget() {
let directory = Directory::new();
@@ -3443,6 +3761,8 @@ fn review_navigation_maps_working_ranges_and_preserves_stale_snapshots() {
.unwrap()
.revisions[1] = "working:old".into();
editor.local_review.document = crate::review::Review::new(root);
editor.local_review.document.comparison =
editor.workspace_diff().unwrap().review_comparison.clone();
let anchor = Anchor {
revision: "working:old".into(),
path: "file.txt".into(),
@@ -7595,6 +7915,7 @@ mod system_clipboard {
let root = fixture.0.0.canonicalize().unwrap();
let storage = root.join("reviews");
let mut review = Review::new(root.clone());
review.comparison = Some(["base".into(), "head".into()]);
review.summary = "Review summary".into();
review.verdict = Some(Verdict::ChangesRequested);
review.comments = vec![