refactor: remove legacy review JSON migration

This commit is contained in:
Valdemar Lorenzen
2026-10-16 15:07:03 +02:00
parent 2259d61633
commit 5b8f8c3f07
6 changed files with 6 additions and 197 deletions
+1 -1
View File
@@ -84,7 +84,7 @@ Diff: :diff (Normal); choose Before then After; Working checkout After edits liv
Remote refs/history load into temporary comparison repos without changing active refs/checkout
Changed files: normal picker/explorer bindings, Ctrl+w close; A/M/D/R status and rename paths
Local reviews: :diff.comment anchors exact Visual text, Visual Line rows, or the current line
Selected comments highlight their precise text; old line-only JSON reviews migrate on load
Selected comments highlight their precise text
:diff.review.edit/delete [ID], :diff.review.next/previous, :diff.review.overview
:diff.review.summary; :diff.review.verdict looks_good|changes_requested|none
:diff.review.save PATH / :diff.review.open PATH; :w in floating review editors saves local JSON
+1 -192
View File
@@ -99,90 +99,6 @@ pub struct Review {
saved: RefCell<Option<(PathBuf, Arc<Vec<u8>>)>>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct LegacyReview {
version: u32,
root: PathBuf,
summary: String,
verdict: Option<Verdict>,
comments: Vec<LegacyComment>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct LegacyComment {
id: u64,
anchor: LegacyAnchor,
body: String,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct LegacyAnchor {
revision: String,
path: PathBuf,
side: Side,
start_line: usize,
end_line: usize,
content: String,
}
impl LegacyReview {
fn migrate(self) -> Result<Review, String> {
if self.version != 1 {
return Err(format!("Unsupported review version {}", self.version));
}
let comments = self
.comments
.into_iter()
.map(|comment| {
let old = comment.anchor;
if old.start_line == 0 || old.end_line < old.start_line {
return Err("Legacy review ranges must be inclusive and nonzero".into());
}
let mut lines = old.content.split('\n');
let last = lines
.nth(old.end_line - 1)
.ok_or("Legacy review range is outside the anchor text")?;
let end = if lines.next().is_some() {
crate::plugin::TextPosition {
row: old.end_line,
column: 0,
}
} else {
crate::plugin::TextPosition {
row: old.end_line - 1,
column: last.graphemes(true).count(),
}
};
Ok(Comment {
id: comment.id,
anchor: Anchor {
revision: old.revision,
path: old.path,
side: old.side,
start: crate::plugin::TextPosition {
row: old.start_line - 1,
column: 0,
},
end,
content: old.content,
},
body: comment.body,
})
})
.collect::<Result<_, String>>()?;
Ok(Review {
version: 2,
root: self.root,
summary: self.summary,
verdict: self.verdict,
comments,
saved: RefCell::new(None),
})
}
}
impl Default for Review {
fn default() -> Self {
Self::new(PathBuf::new())
@@ -264,20 +180,8 @@ impl Review {
pub fn load(path: &Path, root: &Path) -> Result<Self, String> {
let bytes = read_file(path)?.ok_or("Review file does not exist")?;
#[derive(Deserialize)]
struct Version {
version: u32,
}
let Version { version } = serde_json::from_slice(&bytes)
let mut review: Self = serde_json::from_slice(&bytes)
.map_err(|error| format!("Invalid review JSON: {error}"))?;
let mut review: Self = match version {
1 => serde_json::from_slice::<LegacyReview>(&bytes)
.map_err(|error| format!("Invalid review JSON: {error}"))?
.migrate()?,
2 => serde_json::from_slice(&bytes)
.map_err(|error| format!("Invalid review JSON: {error}"))?,
_ => return Err(format!("Unsupported review version {version}")),
};
review.validate()?;
let expected = fs::canonicalize(root).map_err(|error| error.to_string())?;
let actual = fs::canonicalize(&review.root).map_err(|error| error.to_string())?;
@@ -517,101 +421,6 @@ mod tests {
}
}
#[test]
fn legacy_lines_migrate_losslessly_and_save_exact_v2_ranges() {
use crate::plugin::TextPosition;
let directory = directory();
let root = directory.0.canonicalize().unwrap();
let path = root.join("review.json");
let cases = [
("first\nlast\n", 1, 2, (0, 0), (2, 0), 0..=1),
("first\nlast", 2, 2, (1, 0), (1, 4), 1..=1),
("é👩‍🔬e\u{301}", 1, 1, (0, 0), (0, 3), 0..=0),
("first\n\n", 2, 2, (1, 0), (2, 0), 1..=1),
("first\n", 2, 2, (1, 0), (1, 0), 1..=1),
("", 1, 1, (0, 0), (0, 0), 0..=0),
];
let comments: Vec<_> = cases
.iter()
.enumerate()
.map(|(index, case)| {
serde_json::json!({
"id": index + 11,
"anchor": {
"revision": "base:immutable",
"path": "old/name.rs",
"side": "before",
"start_line": case.1,
"end_line": case.2,
"content": case.0,
},
"body": format!("Comment {index}"),
})
})
.collect();
let original = serde_json::json!({
"version": 1,
"root": root,
"summary": "Keep this summary",
"verdict": "looks_good",
"comments": comments,
});
fs::write(&path, serde_json::to_vec(&original).unwrap()).unwrap();
let migrated = Review::load(&path, &root).unwrap();
assert_eq!(migrated.version, 2);
assert_eq!(migrated.root, root);
assert_eq!(migrated.summary, "Keep this summary");
assert_eq!(migrated.verdict, Some(Verdict::LooksGood));
for (index, (comment, case)) in migrated.comments.iter().zip(&cases).enumerate() {
assert_eq!(comment.id, index as u64 + 11);
assert_eq!(comment.body, format!("Comment {index}"));
assert_eq!(comment.anchor.revision, "base:immutable");
assert_eq!(comment.anchor.path, Path::new("old/name.rs"));
assert_eq!(comment.anchor.side, Side::Before);
assert_eq!(comment.anchor.content, case.0);
assert_eq!(
comment.anchor.start,
TextPosition {
row: case.3.0,
column: case.3.1
}
);
assert_eq!(
comment.anchor.end,
TextPosition {
row: case.4.0,
column: case.4.1
}
);
assert_eq!(comment.anchor.rows(), case.5);
}
migrated.save(&path).unwrap();
let saved: serde_json::Value = serde_json::from_slice(&fs::read(&path).unwrap()).unwrap();
assert_eq!(saved["version"], 2);
assert!(saved["comments"][0]["anchor"].get("start_line").is_none());
assert!(saved["comments"][0]["anchor"].get("end_line").is_none());
assert_eq!(
Review::load(&path, &root).unwrap().comments,
migrated.comments
);
for (start, end) in [(0, 1), (2, 1), (1, 4)] {
let mut invalid = original.clone();
invalid["comments"][0]["anchor"]["start_line"] = start.into();
invalid["comments"][0]["anchor"]["end_line"] = end.into();
fs::write(&path, serde_json::to_vec(&invalid).unwrap()).unwrap();
assert!(Review::load(&path, &root).is_err());
}
let mut mixed = original.clone();
mixed["comments"][0]["anchor"]["start"] = serde_json::json!({"row": 0, "column": 0});
fs::write(&path, serde_json::to_vec(&mixed).unwrap()).unwrap();
assert!(Review::load(&path, &root).is_err());
let mut wrong_version = original;
wrong_version["version"] = 2.into();
fs::write(&path, serde_json::to_vec(&wrong_version).unwrap()).unwrap();
assert!(Review::load(&path, &root).is_err());
}
#[test]
fn exact_grapheme_ranges_roundtrip_and_reject_invalid_coordinates() {
use crate::plugin::TextPosition;