feat: resolve local review comments manually

This commit is contained in:
Valdemar Lorenzen
2026-10-16 15:42:33 +02:00
parent b558f82094
commit 6ae8bf06a8
6 changed files with 267 additions and 15 deletions
+81 -8
View File
@@ -87,6 +87,7 @@ pub struct Comment {
pub id: u64,
pub anchor: Anchor,
pub body: String,
pub resolved: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -130,7 +131,7 @@ impl Review {
fs::canonicalize(&root).unwrap_or(root)
};
Self {
version: 2,
version: 3,
root,
summary: String::new(),
verdict: None,
@@ -142,7 +143,7 @@ impl Review {
}
fn validate(&self) -> Result<(), String> {
if self.version != 2 {
if self.version != 3 {
return Err(format!("Unsupported review version {}", self.version));
}
if !self.root.is_absolute() || !self.root.is_dir() {
@@ -188,10 +189,22 @@ impl Review {
Ok(())
}
fn from_json(bytes: &[u8]) -> Result<Self, String> {
#[derive(Deserialize)]
struct Header {
version: u32,
}
let header: Header = serde_json::from_slice(bytes)
.map_err(|error| format!("Invalid review JSON: {error}"))?;
if header.version != 3 {
return Err(format!("Unsupported review version {}", header.version));
}
serde_json::from_slice(bytes).map_err(|error| format!("Invalid review JSON: {error}"))
}
pub fn load(path: &Path, root: &Path) -> Result<Self, String> {
let bytes = read_file(path)?.ok_or("Review file does not exist")?;
let mut review: Self = serde_json::from_slice(&bytes)
.map_err(|error| format!("Invalid review JSON: {error}"))?;
let mut review = Self::from_json(&bytes)?;
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())?;
@@ -483,8 +496,7 @@ fn stored_review(row: &rusqlite::Row<'_>, root: &Path) -> Result<Review, String>
if bytes.len() > LIMIT {
return Err("Review exceeds the 64 MiB limit".into());
}
let mut review: Review = serde_json::from_slice(bytes)
.map_err(|error| format!("Invalid stored review JSON: {error}"))?;
let mut review = Review::from_json(bytes)?;
review.validate()?;
if review.root != root {
return Err("Stored review belongs to a different repository".into());
@@ -578,6 +590,7 @@ mod tests {
content: "a👩‍🔬e\u{301}\r\n界x\n".into(),
},
body: "Please preserve 👩‍🔬".into(),
resolved: true,
});
{
let mut store = Store::open(&storage).unwrap();
@@ -606,7 +619,8 @@ mod tests {
loaded.save(&path).unwrap();
let exported = fs::read(&path).unwrap();
let json: serde_json::Value = serde_json::from_slice(&exported).unwrap();
assert_eq!(json["version"], 2);
assert_eq!(json["comments"][0]["resolved"], true);
assert_eq!(json["version"], 3);
assert!(json.get("storage_id").is_none());
assert!(json.get("storage_revision").is_none());
loaded.summary = "Database only".into();
@@ -857,6 +871,7 @@ mod tests {
content: "first\nlast\n".into(),
},
body: "Please adjust this".into(),
resolved: false,
});
review.save(&path).unwrap();
let mut loaded = Review::load(&path, &root).unwrap();
@@ -895,7 +910,7 @@ mod tests {
assert!(Review::new(root.clone()).save(&path).is_err());
let valid = fs::read(&path).unwrap();
for (field, value) in [
("version", serde_json::json!(3)),
("version", serde_json::json!(2)),
("root", serde_json::json!(root.join("other"))),
(
"comments",
@@ -948,6 +963,46 @@ mod tests {
}
}
#[test]
fn stored_version_two_is_rejected_without_rewriting_it() {
let directory = directory();
let root = directory.0.canonicalize().unwrap();
let mut store = Store::open(&root.join("storage")).unwrap();
let mut review = Review::new(root.clone());
store.save(&mut review).unwrap();
let mut old = serde_json::to_value(&review).unwrap();
old["version"] = 2.into();
let document = serde_json::to_string(&old).unwrap();
store
.connection
.execute(
"UPDATE reviews SET document = ?1 WHERE id = ?2",
params![document, review.storage_id.unwrap()],
)
.unwrap();
assert!(
store
.load(&root, review.storage_id.unwrap())
.unwrap_err()
.contains("Unsupported review version 2")
);
assert!(
store
.list(&root)
.unwrap_err()
.contains("Unsupported review version 2")
);
let preserved: String = store
.connection
.query_row(
"SELECT document FROM reviews WHERE id = ?1",
[review.storage_id.unwrap()],
|row| row.get(0),
)
.unwrap();
assert_eq!(preserved, document);
}
#[test]
fn exact_grapheme_ranges_roundtrip_and_reject_invalid_coordinates() {
use crate::plugin::TextPosition;
@@ -966,12 +1021,30 @@ mod tests {
content: "a👩‍🔬e\u{301}\n界x\n".into(),
},
body: "Exact text".into(),
resolved: false,
});
review.save(&path).unwrap();
let mut loaded = Review::load(&path, &root).unwrap();
assert_eq!(loaded.comments, review.comments);
assert_eq!(loaded.comments[0].anchor.rows(), 0..=1);
let valid = serde_json::to_value(&review).unwrap();
let mut missing_resolution = valid.clone();
missing_resolution["comments"][0]
.as_object_mut()
.unwrap()
.remove("resolved");
for version in [2, 3] {
missing_resolution["version"] = version.into();
let bytes = serde_json::to_vec(&missing_resolution).unwrap();
fs::write(&path, &bytes).unwrap();
let error = Review::load(&path, &root).unwrap_err();
assert!(error.contains(if version == 2 {
"Unsupported review version 2"
} else {
"missing field `resolved`"
}));
assert_eq!(fs::read(&path).unwrap(), bytes);
}
for (field, value) in [
("start", serde_json::json!({"row": 0, "column": 4})),
("end", serde_json::json!({"row": 1, "column": 3})),