Skip to content

Commit 3d190b3

Browse files
committed
Adjusting diff return format; adding additional functionality /w diff baselines
1 parent a8718b2 commit 3d190b3

4 files changed

Lines changed: 152 additions & 52 deletions

File tree

vcs-worker/src/operations/object/object_diff_op.rs

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,41 @@ impl ObjectDiffOperation {
105105
}
106106

107107
/// Get baseline object definition (before the change)
108-
fn get_baseline_object(&self, object_name: &str, change: &Change) -> Result<Option<moor_compiler::ObjectDefinition>, ObjectsTreeError> {
108+
/// If baseline_change_id is provided, use that specific change as the baseline
109+
/// Otherwise, use the default logic (preceding change for merged, or index_change_id for non-merged)
110+
fn get_baseline_object(&self, object_name: &str, change: &Change, baseline_change_id: Option<&str>) -> Result<Option<moor_compiler::ObjectDefinition>, ObjectsTreeError> {
111+
// If a specific baseline change was requested, use that
112+
if let Some(baseline_id) = baseline_change_id {
113+
let resolved_baseline_id = self.database.resolve_change_id(baseline_id)?;
114+
115+
// Try to find the baseline change in index first
116+
let baseline_change = self
117+
.database
118+
.index()
119+
.get_change(&resolved_baseline_id)
120+
.map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?;
121+
122+
if let Some(base_change) = baseline_change {
123+
return self.get_object_at_change(object_name, &base_change);
124+
} else {
125+
// Try workspace if not in index
126+
let baseline_change = self
127+
.database
128+
.workspace()
129+
.get_workspace_change(&resolved_baseline_id)
130+
.map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?;
131+
132+
if let Some(base_change) = baseline_change {
133+
return self.get_object_at_change(object_name, &base_change);
134+
} else {
135+
return Err(ObjectsTreeError::SerializationError(format!(
136+
"Baseline change '{}' not found in index or workspace",
137+
resolved_baseline_id
138+
)));
139+
}
140+
}
141+
}
142+
109143
// If change is merged, we need to get the state before this change
110144
if change.status == ChangeStatus::Merged {
111145
// Get the change order and find the position of this change
@@ -159,12 +193,12 @@ impl ObjectDiffOperation {
159193
}
160194

161195
/// Process the object diff request
162-
fn process_object_diff(&self, change_id: &str, object_name: &str) -> Result<Vec<VerbDiff>, ObjectsTreeError> {
196+
fn process_object_diff(&self, object_name: &str, change_id: &str, baseline_change_id: Option<&str>) -> Result<Vec<VerbDiff>, ObjectsTreeError> {
163197
// Resolve short or full hash to full hash
164198
let resolved_change_id = self.database.resolve_change_id(change_id)?;
165199
info!(
166-
"Computing object diff for '{}' at change ID '{}'",
167-
object_name, resolved_change_id
200+
"Computing object diff for '{}' at change ID '{}' (baseline: {:?})",
201+
object_name, resolved_change_id, baseline_change_id
168202
);
169203

170204
// Get the change
@@ -191,7 +225,7 @@ impl ObjectDiffOperation {
191225

192226
// Get current and baseline object definitions
193227
let current_obj = self.get_object_at_change(object_name, &change)?;
194-
let baseline_obj = self.get_baseline_object(object_name, &change)?;
228+
let baseline_obj = self.get_baseline_object(object_name, &change, baseline_change_id)?;
195229

196230

197231
let mut verb_diffs = Vec::new();
@@ -319,17 +353,23 @@ impl Operation for ObjectDiffOperation {
319353

320354
fn parameters(&self) -> Vec<OperationParameter> {
321355
vec![
356+
OperationParameter {
357+
name: "object_name".to_string(),
358+
description: "The name of the object to diff (e.g., '$player', '#123')"
359+
.to_string(),
360+
required: true,
361+
},
322362
OperationParameter {
323363
name: "change_id".to_string(),
324364
description: "The change ID (short or long Blake3 hash) to compare against"
325365
.to_string(),
326366
required: true,
327367
},
328368
OperationParameter {
329-
name: "object_name".to_string(),
330-
description: "The name of the object to diff (e.g., '$player', '#123')"
369+
name: "baseline_change_id".to_string(),
370+
description: "Optional baseline change ID to compare from. If not provided, uses the preceding change for merged changes or index_change_id for non-merged changes."
331371
.to_string(),
332-
required: true,
372+
required: false,
333373
},
334374
]
335375
}
@@ -338,18 +378,26 @@ impl Operation for ObjectDiffOperation {
338378
vec![
339379
OperationExample {
340380
description: "Get diff for an object at a specific change".to_string(),
341-
moocode: r#"diff = worker_request("vcs", {"object/diff", "abc123def456", "$player"});
381+
moocode: r#"diff = worker_request("vcs", {"object/diff", "$player", "abc123def456"});
342382
// Returns a list of verb diffs showing what changed"#.to_string(),
343383
http_curl: Some(r#"curl -X POST http://localhost:8081/api/object/diff \
344384
-H "Content-Type: application/json" \
345-
-d '{"operation": "object/diff", "args": ["abc123def456", "$player"]}'"#.to_string()),
385+
-d '{"operation": "object/diff", "args": ["$player", "abc123def456"]}'"#.to_string()),
346386
},
347387
OperationExample {
348388
description: "Use short hash for convenience".to_string(),
349-
moocode: r#"diff = worker_request("vcs", {"object/diff", "abc123", "obj123"});
389+
moocode: r#"diff = worker_request("vcs", {"object/diff", "obj123", "abc123"});
350390
// Short hashes are automatically resolved to full hashes"#.to_string(),
351391
http_curl: None,
352392
},
393+
OperationExample {
394+
description: "Compare against a specific baseline change".to_string(),
395+
moocode: r#"diff = worker_request("vcs", {"object/diff", "$player", "abc123def456", "baseline123"});
396+
// Compares $player at change abc123def456 against the state at baseline123"#.to_string(),
397+
http_curl: Some(r#"curl -X POST http://localhost:8081/api/object/diff \
398+
-H "Content-Type: application/json" \
399+
-d '{"operation": "object/diff", "args": ["$player", "abc123def456", "baseline123"]}'"#.to_string()),
400+
},
353401
]
354402
}
355403

@@ -363,18 +411,20 @@ impl Operation for ObjectDiffOperation {
363411

364412
fn execute(&self, args: Vec<String>, _user: &User) -> Var {
365413
// For RPC calls, we expect the args to contain:
366-
// args[0] = change_id
367-
// args[1] = object_name
414+
// args[0] = object_name
415+
// args[1] = change_id
416+
// args[2] = baseline_change_id (optional)
368417

369418
if args.len() < 2 {
370-
error!("Object diff operation requires change_id and object_name");
371-
return v_error(E_INVARG.msg("Change ID and object name are required"));
419+
error!("Object diff operation requires object_name and change_id");
420+
return v_error(E_INVARG.msg("Object name and change ID are required"));
372421
}
373422

374-
let change_id = args[0].clone();
375-
let object_name = args[1].clone();
423+
let object_name = args[0].clone();
424+
let change_id = args[1].clone();
425+
let baseline_change_id = args.get(2).map(|s| s.as_str());
376426

377-
match self.process_object_diff(&change_id, &object_name) {
427+
match self.process_object_diff(&object_name, &change_id, baseline_change_id) {
378428
Ok(verb_diffs) => {
379429
info!("Object diff operation completed successfully");
380430
// Convert each VerbDiff to MOO Var and return as list

vcs-worker/src/types.rs

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -423,33 +423,57 @@ pub struct VerbDiff {
423423
pub hunks: Vec<DiffHunk>,
424424
}
425425

426-
impl DiffLine {
427-
/// Convert to MOO Var format
428-
pub fn to_moo_var(&self) -> Var {
429-
let (line_type, content) = match self {
430-
DiffLine::Context(line) => ("context", line),
431-
DiffLine::Added(line) => ("added", line),
432-
DiffLine::Removed(line) => ("removed", line),
433-
};
434-
435-
let mut pairs = Vec::new();
436-
pairs.push((moor_var::v_str("type"), moor_var::v_str(line_type)));
437-
pairs.push((moor_var::v_str("line"), moor_var::v_str(content)));
438-
moor_var::v_map(&pairs)
439-
}
440-
}
441426

442427
impl DiffHunk {
443428
/// Convert to MOO Var format
429+
/// Groups consecutive lines of the same type together for efficiency
444430
pub fn to_moo_var(&self) -> Var {
445431
let mut pairs = Vec::new();
446432
pairs.push((moor_var::v_str("old_start"), moor_var::v_int(self.old_start as i64)));
447433
pairs.push((moor_var::v_str("old_lines"), moor_var::v_int(self.old_lines as i64)));
448434
pairs.push((moor_var::v_str("new_start"), moor_var::v_int(self.new_start as i64)));
449435
pairs.push((moor_var::v_str("new_lines"), moor_var::v_int(self.new_lines as i64)));
450436

451-
let lines: Vec<Var> = self.lines.iter().map(|line| line.to_moo_var()).collect();
452-
pairs.push((moor_var::v_str("lines"), moor_var::v_list(&lines)));
437+
// Group consecutive lines of the same type together
438+
let mut grouped_lines = Vec::new();
439+
let mut current_group: Option<(String, Vec<String>)> = None;
440+
441+
for line in &self.lines {
442+
let (line_type, content) = match line {
443+
DiffLine::Context(line) => ("context", line.clone()),
444+
DiffLine::Added(line) => ("added", line.clone()),
445+
DiffLine::Removed(line) => ("removed", line.clone()),
446+
};
447+
448+
match &mut current_group {
449+
Some((group_type, lines)) if group_type == line_type => {
450+
// Same type, add to current group
451+
lines.push(content);
452+
}
453+
_ => {
454+
// Different type or first line, finalize previous group and start new one
455+
if let Some((group_type, lines)) = current_group.take() {
456+
let line_vars: Vec<Var> = lines.iter().map(|l| moor_var::v_str(l)).collect();
457+
let mut group_pairs = Vec::new();
458+
group_pairs.push((moor_var::v_str("type"), moor_var::v_str(&group_type)));
459+
group_pairs.push((moor_var::v_str("lines"), moor_var::v_list(&line_vars)));
460+
grouped_lines.push(moor_var::v_map(&group_pairs));
461+
}
462+
current_group = Some((line_type.to_string(), vec![content]));
463+
}
464+
}
465+
}
466+
467+
// Don't forget the last group
468+
if let Some((group_type, lines)) = current_group {
469+
let line_vars: Vec<Var> = lines.iter().map(|l| moor_var::v_str(l)).collect();
470+
let mut group_pairs = Vec::new();
471+
group_pairs.push((moor_var::v_str("type"), moor_var::v_str(&group_type)));
472+
group_pairs.push((moor_var::v_str("lines"), moor_var::v_list(&line_vars)));
473+
grouped_lines.push(moor_var::v_map(&group_pairs));
474+
}
475+
476+
pairs.push((moor_var::v_str("lines"), moor_var::v_list(&grouped_lines)));
453477

454478
moor_var::v_map(&pairs)
455479
}

vcs-worker/tests/common/client.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,14 +157,32 @@ impl VcsTestClient {
157157
/// Get object diff for a specific change
158158
pub async fn object_diff(
159159
&self,
160-
change_id: &str,
161160
object_name: &str,
161+
change_id: &str,
162162
) -> Result<Value, Box<dyn std::error::Error>> {
163163
self.rpc_call(
164164
"object/diff",
165165
vec![
166+
Value::String(object_name.to_string()),
166167
Value::String(change_id.to_string()),
168+
],
169+
)
170+
.await
171+
}
172+
173+
/// Get object diff with optional baseline change ID
174+
pub async fn object_diff_with_baseline(
175+
&self,
176+
object_name: &str,
177+
change_id: &str,
178+
baseline_change_id: &str,
179+
) -> Result<Value, Box<dyn std::error::Error>> {
180+
self.rpc_call(
181+
"object/diff",
182+
vec![
167183
Value::String(object_name.to_string()),
184+
Value::String(change_id.to_string()),
185+
Value::String(baseline_change_id.to_string()),
168186
],
169187
)
170188
.await

vcs-worker/tests/operations/object/object_diff_tests.rs

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ end"#;
4747
// Test diff with full change ID
4848
println!("\nTesting diff with full change ID...");
4949
let result = client
50-
.object_diff(&change_id, "test_object")
50+
.object_diff("test_object", &change_id)
5151
.await
5252
.expect("Failed to get object diff");
5353

@@ -89,7 +89,7 @@ end"#;
8989
// Test diff with short change ID
9090
println!("\nTesting diff with short change ID...");
9191
let result = client
92-
.object_diff(short_id, "test_object")
92+
.object_diff("test_object", short_id)
9393
.await
9494
.expect("Failed to get object diff with short ID");
9595

@@ -115,7 +115,7 @@ async fn test_object_diff_nonexistent_change() {
115115

116116
println!("Test: Object diff with non-existent change should return error");
117117

118-
let result = client.object_diff("nonexistent123", "test_object").await;
118+
let result = client.object_diff("test_object", "nonexistent123").await;
119119

120120
println!("Result: {:?}", result);
121121

@@ -149,7 +149,7 @@ async fn test_object_diff_nonexistent_object() {
149149

150150
let (change_id, _) = db.require_top_change();
151151

152-
let result = client.object_diff(&change_id, "nonexistent_object").await;
152+
let result = client.object_diff("nonexistent_object", &change_id).await;
153153

154154
println!("Result: {:?}", result);
155155

@@ -184,7 +184,7 @@ async fn test_object_diff_missing_arguments() {
184184
println!("Test: Object diff with missing arguments should return error");
185185

186186
// Test with missing object name (empty string)
187-
let result = client.object_diff("some_change_id", "").await;
187+
let result = client.object_diff("", "some_change_id").await;
188188

189189
// Should return a successful response with an error message in the result
190190
let response = result.expect("Expected successful HTTP response");
@@ -228,7 +228,7 @@ end"#;
228228
.await
229229
.expect("Failed to update object v1");
230230

231-
let (change_id_1, _) = db.require_top_change();
231+
let (_change_id_1, _) = db.require_top_change();
232232

233233
// Submit the first change
234234
println!("\nSubmitting first change...");
@@ -262,7 +262,7 @@ end"#;
262262
// Now test the diff between the two changes
263263
println!("\nTesting diff between changes...");
264264
let result = client
265-
.object_diff(&change_id_2, "test_object_v2")
265+
.object_diff("test_object_v2", &change_id_2)
266266
.await
267267
.expect("Failed to get object diff");
268268

@@ -300,16 +300,24 @@ end"#;
300300
assert!(first_hunk.get("new_start").is_some(), "Expected new_start in hunk");
301301
assert!(first_hunk.get("lines").is_some(), "Expected lines in hunk");
302302

303-
if let Some(lines) = first_hunk.get("lines").and_then(|v| v.as_array()) {
304-
println!("✅ Found {} lines in hunk", lines.len());
305-
assert!(!lines.is_empty(), "Expected at least one line in hunk");
303+
if let Some(line_groups) = first_hunk.get("lines").and_then(|v| v.as_array()) {
304+
println!("✅ Found {} line groups in hunk", line_groups.len());
305+
assert!(!line_groups.is_empty(), "Expected at least one line group in hunk");
306306

307-
// Check line types
308-
for (i, line) in lines.iter().enumerate() {
309-
if let Some(line_type) = line.get("type").and_then(|v| v.as_str()) {
310-
println!("Line {}: type={}", i, line_type);
311-
assert!(matches!(line_type, "context" | "added" | "removed"),
312-
"Expected line type to be context, added, or removed, got: {}", line_type);
307+
// Check line group structure (new grouped format)
308+
for (i, group) in line_groups.iter().enumerate() {
309+
if let Some(group_type) = group.get("type").and_then(|v| v.as_str()) {
310+
println!("Group {}: type={}", i, group_type);
311+
assert!(matches!(group_type, "context" | "added" | "removed"),
312+
"Expected group type to be context, added, or removed, got: {}", group_type);
313+
314+
// Check that the group has a "lines" array
315+
if let Some(lines_in_group) = group.get("lines").and_then(|v| v.as_array()) {
316+
println!(" Group {} has {} lines", i, lines_in_group.len());
317+
assert!(!lines_in_group.is_empty(), "Expected at least one line in group");
318+
} else {
319+
panic!("Expected 'lines' array in group, got: {:?}", group);
320+
}
313321
}
314322
}
315323
}

0 commit comments

Comments
 (0)