• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

biscuitWizard / moor-hackercore / 18455940204

13 Oct 2025 05:16AM UTC coverage: 80.142% (-0.01%) from 80.154%
18455940204

push

github

biscuitWizard
Reorganized our tests

7579 of 9457 relevant lines covered (80.14%)

333.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.87
/vcs-worker/src/object_diff.rs
1
use crate::database::{DatabaseRef, ObjectsTreeError};
2
use crate::providers::index::IndexProvider;
3
use crate::providers::objects::ObjectsProvider;
4
use crate::providers::refs::RefsProvider;
5
use crate::types::{Change, VcsObjectType};
6
use moor_compiler::ObjectDefinition;
7
use moor_var::{Var, v_map, v_str, v_objid};
8
use serde::{Deserialize, Serialize};
9
use std::collections::{HashMap, HashSet};
10

11
/// Represents a single object change with detailed verb and property modifications
12
#[derive(Debug, Clone, Serialize, Deserialize)]
13
pub struct ObjectChange {
14
    /// Object ID - either the OID as string (e.g., "#4") or object name (e.g., "Foobar")
15
    /// if the name differs from the OID
16
    pub obj_id: String,
17
    /// Verbs that were modified (existing verbs with changes)
18
    pub verbs_modified: HashSet<String>,
19
    /// Verbs that were added (new verbs)
20
    pub verbs_added: HashSet<String>,
21
    /// Verbs that were renamed (old_name -> new_name mapping)
22
    pub verbs_renamed: HashMap<String, String>,
23
    /// Verbs that were deleted
24
    pub verbs_deleted: HashSet<String>,
25
    /// Properties that were modified (existing properties with changes)
26
    pub props_modified: HashSet<String>,
27
    /// Properties that were added (new properties)
28
    pub props_added: HashSet<String>,
29
    /// Properties that were renamed (old_name -> new_name mapping)
30
    pub props_renamed: HashMap<String, String>,
31
    /// Properties that were deleted
32
    pub props_deleted: HashSet<String>,
33
}
34

35
/// Represents a complete set of object changes/deltas for communication to MOO
36
#[derive(Debug, Clone, Serialize, Deserialize)]
37
pub struct ObjectDiffModel {
38
    /// Objects that were renamed (from_obj_id -> to_obj_id mapping)
39
    pub objects_renamed: HashMap<String, String>,
40
    /// Objects that were deleted
41
    pub objects_deleted: HashSet<String>,
42
    /// Objects that were added
43
    pub objects_added: HashSet<String>,
44
    /// Objects that were modified
45
    pub objects_modified: HashSet<String>,
46
    /// Detailed list of changes for each modified object
47
    pub changes: Vec<ObjectChange>,
48
}
49

50
impl ObjectChange {
51
    /// Create a new empty ObjectChange
52
    pub fn new(obj_id: String) -> Self {
35✔
53
        Self {
35✔
54
            obj_id,
35✔
55
            verbs_modified: HashSet::new(),
35✔
56
            verbs_added: HashSet::new(),
35✔
57
            verbs_renamed: HashMap::new(),
35✔
58
            verbs_deleted: HashSet::new(),
35✔
59
            props_modified: HashSet::new(),
35✔
60
            props_added: HashSet::new(),
35✔
61
            props_renamed: HashMap::new(),
35✔
62
            props_deleted: HashSet::new(),
35✔
63
        }
35✔
64
    }
35✔
65

66
    /// Convert this ObjectChange to a MOO v_map
67
    pub fn to_moo_var(&self) -> Var {
32✔
68
        let mut pairs = Vec::new();
32✔
69

70
        // obj_id - use helper to convert to v_obj if it's a numeric ID
71
        pairs.push((v_str("obj_id"), object_id_to_var(&self.obj_id)));
32✔
72

73
        // verbs_modified
74
        let verbs_modified_list: Vec<Var> = self.verbs_modified.iter().map(|v| v_str(v)).collect();
32✔
75
        pairs.push((
32✔
76
            v_str("verbs_modified"),
32✔
77
            moor_var::v_list(&verbs_modified_list),
32✔
78
        ));
32✔
79

80
        // verbs_added
81
        let verbs_added_list: Vec<Var> = self.verbs_added.iter().map(|v| v_str(v)).collect();
32✔
82
        pairs.push((v_str("verbs_added"), moor_var::v_list(&verbs_added_list)));
32✔
83

84
        // verbs_renamed
85
        let verbs_renamed_map: Vec<(Var, Var)> = self
32✔
86
            .verbs_renamed
32✔
87
            .iter()
32✔
88
            .map(|(k, v)| (v_str(k), v_str(v)))
32✔
89
            .collect();
32✔
90
        pairs.push((v_str("verbs_renamed"), v_map(&verbs_renamed_map)));
32✔
91

92
        // verbs_deleted
93
        let verbs_deleted_list: Vec<Var> = self.verbs_deleted.iter().map(|v| v_str(v)).collect();
32✔
94
        pairs.push((
32✔
95
            v_str("verbs_deleted"),
32✔
96
            moor_var::v_list(&verbs_deleted_list),
32✔
97
        ));
32✔
98

99
        // props_modified
100
        let props_modified_list: Vec<Var> = self.props_modified.iter().map(|v| v_str(v)).collect();
32✔
101
        pairs.push((
32✔
102
            v_str("props_modified"),
32✔
103
            moor_var::v_list(&props_modified_list),
32✔
104
        ));
32✔
105

106
        // props_added
107
        let props_added_list: Vec<Var> = self.props_added.iter().map(|v| v_str(v)).collect();
32✔
108
        pairs.push((v_str("props_added"), moor_var::v_list(&props_added_list)));
32✔
109

110
        // props_renamed
111
        let props_renamed_map: Vec<(Var, Var)> = self
32✔
112
            .props_renamed
32✔
113
            .iter()
32✔
114
            .map(|(k, v)| (v_str(k), v_str(v)))
32✔
115
            .collect();
32✔
116
        pairs.push((v_str("props_renamed"), v_map(&props_renamed_map)));
32✔
117

118
        // props_deleted
119
        let props_deleted_list: Vec<Var> = self.props_deleted.iter().map(|v| v_str(v)).collect();
32✔
120
        pairs.push((
32✔
121
            v_str("props_deleted"),
32✔
122
            moor_var::v_list(&props_deleted_list),
32✔
123
        ));
32✔
124

125
        v_map(&pairs)
32✔
126
    }
32✔
127
}
128

129
impl ObjectDiffModel {
130
    /// Create a new empty ObjectDiffModel
131
    pub fn new() -> Self {
179✔
132
        Self {
179✔
133
            objects_renamed: HashMap::new(),
179✔
134
            objects_deleted: HashSet::new(),
179✔
135
            objects_added: HashSet::new(),
179✔
136
            objects_modified: HashSet::new(),
179✔
137
            changes: Vec::new(),
179✔
138
        }
179✔
139
    }
179✔
140

141
    /// Convert this ObjectDiffModel to a MOO v_map
142
    pub fn to_moo_var(&self) -> Var {
158✔
143
        let mut pairs = Vec::new();
158✔
144

145
        // objects_renamed - use helper to convert object IDs to v_obj if they're numeric
146
        let objects_renamed_map: Vec<(Var, Var)> = self
158✔
147
            .objects_renamed
158✔
148
            .iter()
158✔
149
            .map(|(k, v)| (object_id_to_var(k), object_id_to_var(v)))
158✔
150
            .collect();
158✔
151
        pairs.push((v_str("objects_renamed"), v_map(&objects_renamed_map)));
158✔
152

153
        // objects_deleted - use helper to convert object IDs to v_obj if they're numeric
154
        let objects_deleted_list: Vec<Var> =
158✔
155
            self.objects_deleted.iter().map(|v| object_id_to_var(v)).collect();
158✔
156
        pairs.push((
158✔
157
            v_str("objects_deleted"),
158✔
158
            moor_var::v_list(&objects_deleted_list),
158✔
159
        ));
158✔
160

161
        // objects_added - use helper to convert object IDs to v_obj if they're numeric
162
        let objects_added_list: Vec<Var> = self.objects_added.iter().map(|v| object_id_to_var(v)).collect();
158✔
163
        pairs.push((
158✔
164
            v_str("objects_added"),
158✔
165
            moor_var::v_list(&objects_added_list),
158✔
166
        ));
158✔
167

168
        // objects_modified - use helper to convert object IDs to v_obj if they're numeric
169
        let objects_modified_list: Vec<Var> =
158✔
170
            self.objects_modified.iter().map(|v| object_id_to_var(v)).collect();
158✔
171
        pairs.push((
158✔
172
            v_str("objects_modified"),
158✔
173
            moor_var::v_list(&objects_modified_list),
158✔
174
        ));
158✔
175

176
        // changes
177
        let changes_list: Vec<Var> = self
158✔
178
            .changes
158✔
179
            .iter()
158✔
180
            .map(|change| change.to_moo_var())
158✔
181
            .collect();
158✔
182
        pairs.push((v_str("changes"), moor_var::v_list(&changes_list)));
158✔
183

184
        v_map(&pairs)
158✔
185
    }
158✔
186

187
    /// Add a renamed object to the model
188
    pub fn add_object_renamed(&mut self, from: String, to: String) {
×
189
        self.objects_renamed.insert(from, to);
×
190
    }
×
191

192
    /// Add a deleted object to the model
193
    pub fn add_object_deleted(&mut self, obj_id: String) {
30✔
194
        self.objects_deleted.insert(obj_id);
30✔
195
    }
30✔
196

197
    /// Add an added object to the model
198
    pub fn add_object_added(&mut self, obj_id: String) {
26✔
199
        self.objects_added.insert(obj_id);
26✔
200
    }
26✔
201

202
    /// Add a modified object to the model
203
    pub fn add_object_modified(&mut self, obj_id: String) {
5✔
204
        self.objects_modified.insert(obj_id);
5✔
205
    }
5✔
206

207
    /// Add or update an object change in the model
208
    pub fn add_object_change(&mut self, change: ObjectChange) {
35✔
209
        // Remove any existing change for this object
210
        self.changes.retain(|c| c.obj_id != change.obj_id);
35✔
211
        self.changes.push(change);
35✔
212
    }
35✔
213

214
    /// Merge another ObjectDiffModel into this one
215
    pub fn merge(&mut self, other: ObjectDiffModel) {
16✔
216
        // Merge renamed objects
217
        for (from, to) in other.objects_renamed {
16✔
218
            self.objects_renamed.insert(from, to);
×
219
        }
×
220

221
        // Merge deleted objects
222
        for obj_id in other.objects_deleted {
24✔
223
            self.objects_deleted.insert(obj_id);
8✔
224
        }
8✔
225

226
        // Merge added objects
227
        for obj_id in other.objects_added {
28✔
228
            self.objects_added.insert(obj_id);
12✔
229
        }
12✔
230

231
        // Merge modified objects
232
        for obj_id in other.objects_modified {
16✔
233
            self.objects_modified.insert(obj_id);
×
234
        }
×
235

236
        // Merge changes
237
        for change in other.changes {
26✔
238
            self.add_object_change(change);
10✔
239
        }
10✔
240
    }
16✔
241
}
242

243
impl Default for ObjectDiffModel {
244
    fn default() -> Self {
×
245
        Self::new()
×
246
    }
×
247
}
248

249
/// Helper function to convert an object ID string to a MOO Var
250
/// If the string is in the format "#<number>", returns a v_obj (object reference)
251
/// Otherwise, returns a v_str (string)
252
pub fn object_id_to_var(obj_id: &str) -> Var {
84✔
253
    // Check if the string starts with '#' and the rest is a valid number
254
    if let Some(stripped) = obj_id.strip_prefix('#') {
84✔
255
        if let Ok(num) = stripped.parse::<i32>() {
10✔
256
            // This is a numeric object ID like "#73", return as v_obj
257
            return v_objid(num);
10✔
258
        }
×
259
    }
74✔
260
    // Otherwise, return as a string
261
    v_str(obj_id)
74✔
262
}
84✔
263

264
/// Helper function to convert an object ID to an object name
265
/// Returns the object name if it's different from the OID, otherwise returns the OID
266
pub fn obj_id_to_object_name(obj_id: &str, object_name: Option<&str>) -> String {
59✔
267
    match object_name {
57✔
268
        Some(name) if name != obj_id => {
57✔
269
            // Capitalize first letter if it's a name
270
            if let Some(first_char) = name.chars().next() {
2✔
271
                let mut result = String::with_capacity(name.len());
2✔
272
                result.push(first_char.to_uppercase().next().unwrap_or(first_char));
2✔
273
                result.push_str(&name[1..]);
2✔
274
                result
2✔
275
            } else {
276
                name.to_string()
×
277
            }
278
        }
279
        _ => obj_id.to_string(),
57✔
280
    }
281
}
59✔
282

283
/// Compare object versions to determine detailed changes
284
pub fn compare_object_versions(
33✔
285
    database: &DatabaseRef,
33✔
286
    obj_name: &str,
33✔
287
    local_version: u64,
33✔
288
    verb_hints: Option<&[crate::types::VerbRenameHint]>,
33✔
289
    prop_hints: Option<&[crate::types::PropertyRenameHint]>,
33✔
290
) -> Result<ObjectChange, ObjectsTreeError> {
33✔
291
    let mut object_change = ObjectChange::new(obj_name.to_string());
33✔
292

293
    // Get the local version content
294
    let local_sha256 = database
33✔
295
        .refs()
33✔
296
        .get_ref(VcsObjectType::MooObject, obj_name, Some(local_version))
33✔
297
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?
33✔
298
        .ok_or_else(|| {
33✔
299
            ObjectsTreeError::SerializationError(format!(
×
300
                "Local version {local_version} of object '{obj_name}' not found"
×
301
            ))
×
302
        })?;
×
303

304
    let local_content = database
33✔
305
        .objects()
33✔
306
        .get(&local_sha256)
33✔
307
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?
33✔
308
        .ok_or_else(|| {
33✔
309
            ObjectsTreeError::SerializationError(format!(
×
310
                "Object content for SHA256 '{local_sha256}' not found"
×
311
            ))
×
312
        })?;
×
313

314
    // Parse local object definition
315
    let local_def = database
33✔
316
        .objects()
33✔
317
        .parse_object_dump(&local_content)
33✔
318
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?;
33✔
319

320
    // Get the baseline version (previous version)
321
    // For version 1 (new object), there is no baseline (version 0 doesn't exist)
322
    // For version 2+, the baseline is the previous version
323
    let baseline_version = local_version.saturating_sub(1);
33✔
324
    let baseline_sha256 = database
33✔
325
        .refs()
33✔
326
        .get_ref(VcsObjectType::MooObject, obj_name, Some(baseline_version))
33✔
327
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?;
33✔
328

329
    if let Some(baseline_sha256) = baseline_sha256 {
33✔
330
        // Get baseline content and parse it
331
        let baseline_content = database
8✔
332
            .objects()
8✔
333
            .get(&baseline_sha256)
8✔
334
            .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?
8✔
335
            .ok_or_else(|| {
8✔
336
                ObjectsTreeError::SerializationError(format!(
×
337
                    "Baseline object content for SHA256 '{baseline_sha256}' not found"
×
338
                ))
×
339
            })?;
×
340

341
        let baseline_def = database
8✔
342
            .objects()
8✔
343
            .parse_object_dump(&baseline_content)
8✔
344
            .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?;
8✔
345

346
        // Compare the two object definitions with meta filtering and hints
347
        compare_object_definitions_with_meta(
8✔
348
            &baseline_def,
8✔
349
            &local_def,
8✔
350
            &mut object_change,
8✔
351
            Some(database),
8✔
352
            Some(obj_name),
8✔
353
            verb_hints,
8✔
354
            prop_hints,
8✔
355
        );
356
    } else {
357
        // No baseline version - this is a new object, mark all as added
358
        for verb in &local_def.verbs {
25✔
359
            for verb_name in &verb.names {
×
360
                object_change.verbs_added.insert(verb_name.as_string());
×
361
            }
×
362
        }
363
        for prop_def in &local_def.property_definitions {
26✔
364
            object_change.props_added.insert(prop_def.name.as_string());
1✔
365
        }
1✔
366
        for prop_override in &local_def.property_overrides {
25✔
367
            object_change
×
368
                .props_added
×
369
                .insert(prop_override.name.as_string());
×
370
        }
×
371
    }
372

373
    Ok(object_change)
33✔
374
}
33✔
375

376
/// Apply hints to convert added/deleted verbs/properties to renames
377
fn apply_hints_to_object_change(
8✔
378
    object_change: &mut ObjectChange,
8✔
379
    obj_name: &str,
8✔
380
    verb_hints: &[crate::types::VerbRenameHint],
8✔
381
    prop_hints: &[crate::types::PropertyRenameHint],
8✔
382
) {
8✔
383
    // Apply verb rename hints
384
    for hint in verb_hints {
8✔
385
        if hint.object_name != obj_name {
×
386
            continue; // Skip hints for other objects
×
387
        }
×
388

389
        // Check if both from_verb and to_verb are in the expected sets
390
        let from_in_deleted = object_change.verbs_deleted.contains(&hint.from_verb);
×
391
        let to_in_added = object_change.verbs_added.contains(&hint.to_verb);
×
392

393
        if from_in_deleted && to_in_added {
×
394
            // This is a valid rename hint - apply it
395
            object_change.verbs_deleted.remove(&hint.from_verb);
×
396
            object_change.verbs_added.remove(&hint.to_verb);
×
397
            object_change.verbs_renamed.insert(hint.from_verb.clone(), hint.to_verb.clone());
×
398
            
399
            tracing::debug!(
×
400
                "Applied verb rename hint for object '{}': '{}' -> '{}'",
×
401
                obj_name, hint.from_verb, hint.to_verb
402
            );
403
        }
×
404
    }
405

406
    // Apply property rename hints
407
    for hint in prop_hints {
8✔
408
        if hint.object_name != obj_name {
×
409
            continue; // Skip hints for other objects
×
410
        }
×
411

412
        // Check if both from_prop and to_prop are in the expected sets
413
        let from_in_deleted = object_change.props_deleted.contains(&hint.from_prop);
×
414
        let to_in_added = object_change.props_added.contains(&hint.to_prop);
×
415

416
        if from_in_deleted && to_in_added {
×
417
            // This is a valid rename hint - apply it
418
            object_change.props_deleted.remove(&hint.from_prop);
×
419
            object_change.props_added.remove(&hint.to_prop);
×
420
            object_change.props_renamed.insert(hint.from_prop.clone(), hint.to_prop.clone());
×
421
            
422
            tracing::debug!(
×
423
                "Applied property rename hint for object '{}': '{}' -> '{}'",
×
424
                obj_name, hint.from_prop, hint.to_prop
425
            );
426
        }
×
427
    }
428
}
8✔
429

430
/// Compare two ObjectDefinitions with optional meta filtering and rename hints
431
pub fn compare_object_definitions_with_meta(
8✔
432
    baseline: &ObjectDefinition,
8✔
433
    local: &ObjectDefinition,
8✔
434
    object_change: &mut ObjectChange,
8✔
435
    database: Option<&DatabaseRef>,
8✔
436
    obj_name: Option<&str>,
8✔
437
    verb_hints: Option<&[crate::types::VerbRenameHint]>,
8✔
438
    prop_hints: Option<&[crate::types::PropertyRenameHint]>,
8✔
439
) {
8✔
440
    // Load meta if database and obj_name are provided
441
    let meta = if let (Some(db), Some(name)) = (database, obj_name) {
8✔
442
        match db.refs().get_ref(VcsObjectType::MooMetaObject, name, None) {
8✔
443
            Ok(Some(meta_sha256)) => match db.objects().get(&meta_sha256) {
5✔
444
                Ok(Some(yaml)) => db.objects().parse_meta_dump(&yaml).ok(),
5✔
445
                _ => None,
×
446
            },
447
            _ => None,
3✔
448
        }
449
    } else {
450
        None
×
451
    };
452
    // Compare verbs
453
    let baseline_verbs: HashMap<String, &moor_compiler::ObjVerbDef> = baseline
8✔
454
        .verbs
8✔
455
        .iter()
8✔
456
        .flat_map(|v| v.names.iter().map(move |name| (name.as_string(), v)))
10✔
457
        .collect();
8✔
458

459
    let local_verbs: HashMap<String, &moor_compiler::ObjVerbDef> = local
8✔
460
        .verbs
8✔
461
        .iter()
8✔
462
        .flat_map(|v| v.names.iter().map(move |name| (name.as_string(), v)))
8✔
463
        .collect();
8✔
464

465
    // Find added, modified, and deleted verbs
466
    for (verb_name, local_verb) in &local_verbs {
16✔
467
        if let Some(baseline_verb) = baseline_verbs.get(verb_name) {
8✔
468
            // Verb exists in both - check if it's modified
469
            if verbs_differ(baseline_verb, local_verb) {
8✔
470
                object_change.verbs_modified.insert(verb_name.clone());
×
471
            }
8✔
472
        } else {
×
473
            // Verb is new
×
474
            object_change.verbs_added.insert(verb_name.clone());
×
475
        }
×
476
    }
477

478
    for verb_name in baseline_verbs.keys() {
10✔
479
        if !local_verbs.contains_key(verb_name) {
10✔
480
            // Verb is missing - check if it's ignored before marking as deleted
481
            let is_ignored = meta
2✔
482
                .as_ref()
2✔
483
                .map(|m| m.ignored_verbs.contains(verb_name))
2✔
484
                .unwrap_or(false);
2✔
485

486
            if !is_ignored {
2✔
487
                // Verb was actually deleted (not just ignored)
×
488
                object_change.verbs_deleted.insert(verb_name.clone());
×
489
            } else {
×
490
                tracing::debug!(
2✔
491
                    "Verb '{}' is missing but ignored in meta, not marking as deleted",
×
492
                    verb_name
493
                );
494
            }
495
        }
8✔
496
    }
497

498
    // Compare property definitions
499
    let baseline_props: HashMap<String, &moor_compiler::ObjPropDef> = baseline
8✔
500
        .property_definitions
8✔
501
        .iter()
8✔
502
        .map(|p| (p.name.as_string(), p))
10✔
503
        .collect();
8✔
504

505
    let local_props: HashMap<String, &moor_compiler::ObjPropDef> = local
8✔
506
        .property_definitions
8✔
507
        .iter()
8✔
508
        .map(|p| (p.name.as_string(), p))
8✔
509
        .collect();
8✔
510

511
    // Find added, modified, and deleted property definitions
512
    for (prop_name, local_prop) in &local_props {
14✔
513
        if let Some(baseline_prop) = baseline_props.get(prop_name) {
6✔
514
            // Property exists in both - check if it's modified
515
            if property_definitions_differ(baseline_prop, local_prop) {
6✔
516
                object_change.props_modified.insert(prop_name.clone());
×
517
            }
6✔
518
        } else {
×
519
            // Property is new
×
520
            object_change.props_added.insert(prop_name.clone());
×
521
        }
×
522
    }
523

524
    for prop_name in baseline_props.keys() {
10✔
525
        if !local_props.contains_key(prop_name) {
10✔
526
            // Property is missing - check if it's ignored before marking as deleted
527
            let is_ignored = meta
4✔
528
                .as_ref()
4✔
529
                .map(|m| m.ignored_properties.contains(prop_name))
4✔
530
                .unwrap_or(false);
4✔
531

532
            if !is_ignored {
4✔
533
                // Property was actually deleted (not just ignored)
1✔
534
                object_change.props_deleted.insert(prop_name.clone());
1✔
535
            } else {
1✔
536
                tracing::debug!(
3✔
537
                    "Property '{}' is missing but ignored in meta, not marking as deleted",
×
538
                    prop_name
539
                );
540
            }
541
        }
6✔
542
    }
543

544
    // Compare property overrides
545
    let baseline_overrides: HashMap<String, &moor_compiler::ObjPropOverride> = baseline
8✔
546
        .property_overrides
8✔
547
        .iter()
8✔
548
        .map(|p| (p.name.as_string(), p))
8✔
549
        .collect();
8✔
550

551
    let local_overrides: HashMap<String, &moor_compiler::ObjPropOverride> = local
8✔
552
        .property_overrides
8✔
553
        .iter()
8✔
554
        .map(|p| (p.name.as_string(), p))
8✔
555
        .collect();
8✔
556

557
    // Find added, modified, and deleted property overrides
558
    for (prop_name, local_override) in &local_overrides {
8✔
559
        if let Some(baseline_override) = baseline_overrides.get(prop_name) {
×
560
            // Override exists in both - check if it's modified
561
            if property_overrides_differ(baseline_override, local_override) {
×
562
                object_change.props_modified.insert(prop_name.clone());
×
563
            }
×
564
        } else {
×
565
            // Override is new
×
566
            object_change.props_added.insert(prop_name.clone());
×
567
        }
×
568
    }
569

570
    for prop_name in baseline_overrides.keys() {
8✔
571
        if !local_overrides.contains_key(prop_name) {
×
572
            // Override is missing - check if it's ignored before marking as deleted
573
            let is_ignored = meta
×
574
                .as_ref()
×
575
                .map(|m| m.ignored_properties.contains(prop_name))
×
576
                .unwrap_or(false);
×
577

578
            if !is_ignored {
×
579
                // Override was actually deleted (not just ignored)
×
580
                object_change.props_deleted.insert(prop_name.clone());
×
581
            } else {
×
582
                tracing::debug!(
×
583
                    "Property override '{}' is missing but ignored in meta, not marking as deleted",
×
584
                    prop_name
585
                );
586
            }
587
        }
×
588
    }
589

590
    // Apply hints if provided
591
    if let Some(name) = obj_name {
8✔
592
        if let (Some(v_hints), Some(p_hints)) = (verb_hints, prop_hints) {
8✔
593
            apply_hints_to_object_change(object_change, name, v_hints, p_hints);
8✔
594
        }
8✔
595
    }
×
596
}
8✔
597

598
/// Check if two verb definitions differ
599
pub fn verbs_differ(
8✔
600
    baseline: &moor_compiler::ObjVerbDef,
8✔
601
    local: &moor_compiler::ObjVerbDef,
8✔
602
) -> bool {
8✔
603
    baseline.argspec != local.argspec
8✔
604
        || baseline.owner != local.owner
8✔
605
        || baseline.flags != local.flags
8✔
606
        || baseline.program != local.program
8✔
607
}
8✔
608

609
/// Check if two property definitions differ
610
pub fn property_definitions_differ(
6✔
611
    baseline: &moor_compiler::ObjPropDef,
6✔
612
    local: &moor_compiler::ObjPropDef,
6✔
613
) -> bool {
6✔
614
    baseline.perms != local.perms || baseline.value != local.value
6✔
615
}
6✔
616

617
/// Check if two property overrides differ
618
pub fn property_overrides_differ(
×
619
    baseline: &moor_compiler::ObjPropOverride,
×
620
    local: &moor_compiler::ObjPropOverride,
×
621
) -> bool {
×
622
    baseline.value != local.value || baseline.perms_update != local.perms_update
×
623
}
×
624

625
/// Build an ObjectDiffModel by comparing a change against the compiled state
626
/// This is the shared logic used by approve and status operations
627
pub fn build_object_diff_from_change(
23✔
628
    database: &DatabaseRef,
23✔
629
    change: &Change,
23✔
630
) -> Result<ObjectDiffModel, ObjectsTreeError> {
23✔
631
    let mut diff_model = ObjectDiffModel::new();
23✔
632

633
    // Get the complete object list from the index state (excluding the local change)
634
    let complete_object_list = database
23✔
635
        .index()
23✔
636
        .compute_complete_object_list()
23✔
637
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?;
23✔
638

639
    tracing::info!(
23✔
640
        "Using complete object list with {} objects as baseline for change '{}'",
×
641
        complete_object_list.len(),
×
642
        change.name
643
    );
644

645
    // Process the change to build the diff
646
    process_change_for_diff(database, &mut diff_model, change)?;
23✔
647

648
    Ok(diff_model)
23✔
649
}
23✔
650

651
/// Process a single change and add its modifications to the diff model
652
/// This is the shared logic used by approve and status operations
653
pub fn process_change_for_diff(
23✔
654
    database: &DatabaseRef,
23✔
655
    diff_model: &mut ObjectDiffModel,
23✔
656
    change: &Change,
23✔
657
) -> Result<(), ObjectsTreeError> {
23✔
658
    // Use hints from the change (they're kept permanently now)
659
    let verb_hints_ref = Some(change.verb_rename_hints.as_slice());
23✔
660
    let prop_hints_ref = Some(change.property_rename_hints.as_slice());
23✔
661

662
    // Process added objects (filter to only MooObject types)
663
    for obj_info in change
23✔
664
        .added_objects
23✔
665
        .iter()
23✔
666
        .filter(|o| o.object_type == VcsObjectType::MooObject)
23✔
667
    {
668
        let obj_name = obj_id_to_object_name(&obj_info.name, Some(&obj_info.name));
20✔
669
        diff_model.add_object_added(obj_name.clone());
20✔
670

671
        // Get detailed object changes by comparing local vs baseline (which will be empty for new objects)
672
        let object_change = compare_object_versions(
20✔
673
            database,
20✔
674
            &obj_name,
20✔
675
            obj_info.version,
20✔
676
            verb_hints_ref,
20✔
677
            prop_hints_ref,
20✔
678
        )?;
×
679
        diff_model.add_object_change(object_change);
20✔
680
    }
681

682
    // Process deleted objects (filter to only MooObject types)
683
    for obj_info in change
23✔
684
        .deleted_objects
23✔
685
        .iter()
23✔
686
        .filter(|o| o.object_type == VcsObjectType::MooObject)
23✔
687
    {
×
688
        let obj_name = obj_id_to_object_name(&obj_info.name, Some(&obj_info.name));
×
689
        diff_model.add_object_deleted(obj_name);
×
690
    }
×
691

692
    // Process renamed objects (filter to only MooObject types)
693
    for renamed in change.renamed_objects.iter().filter(|r| {
23✔
694
        r.from.object_type == VcsObjectType::MooObject
×
695
            && r.to.object_type == VcsObjectType::MooObject
×
696
    }) {
×
697
        let from_name = obj_id_to_object_name(&renamed.from.name, Some(&renamed.from.name));
×
698
        let to_name = obj_id_to_object_name(&renamed.to.name, Some(&renamed.to.name));
×
699
        diff_model.add_object_renamed(from_name, to_name);
×
700
    }
×
701

702
    // Process modified objects with detailed comparison (filter to only MooObject types)
703
    for obj_info in change
23✔
704
        .modified_objects
23✔
705
        .iter()
23✔
706
        .filter(|o| o.object_type == VcsObjectType::MooObject)
23✔
707
    {
708
        let obj_name = obj_id_to_object_name(&obj_info.name, Some(&obj_info.name));
5✔
709
        diff_model.add_object_modified(obj_name.clone());
5✔
710

711
        // Get detailed object changes by comparing local vs baseline
712
        let object_change = compare_object_versions(
5✔
713
            database,
5✔
714
            &obj_name,
5✔
715
            obj_info.version,
5✔
716
            verb_hints_ref,
5✔
717
            prop_hints_ref,
5✔
718
        )?;
×
719
        diff_model.add_object_change(object_change);
5✔
720
    }
721

722
    Ok(())
23✔
723
}
23✔
724

725
/// Build an ObjectDiffModel for abandoning a change (undo operations)
726
/// This creates the reverse operations needed to undo the change
727
pub fn build_abandon_diff_from_change(
27✔
728
    database: &DatabaseRef,
27✔
729
    change: &Change,
27✔
730
) -> Result<ObjectDiffModel, ObjectsTreeError> {
27✔
731
    // Get the complete object list from the index state for comparison
732
    let complete_object_list = database
27✔
733
        .index()
27✔
734
        .compute_complete_object_list()
27✔
735
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?;
27✔
736

737
    tracing::info!(
27✔
738
        "Using complete object list with {} objects as baseline for abandoning change '{}'",
×
739
        complete_object_list.len(),
×
740
        change.name
741
    );
742

743
    // Create a delta model showing what needs to be undone
744
    let mut undo_delta = ObjectDiffModel::new();
27✔
745

746
    // Get object name mappings for better display names
747
    let object_names = get_object_names_for_change(change);
27✔
748

749
    // Process added objects - to undo, we need to delete them (filter to only MooObject types)
750
    for added_obj in change
27✔
751
        .added_objects
27✔
752
        .iter()
27✔
753
        .filter(|o| o.object_type == VcsObjectType::MooObject)
27✔
754
    {
755
        let object_name = obj_id_to_object_name(
26✔
756
            &added_obj.name,
26✔
757
            object_names.get(&added_obj.name).map(|s| s.as_str()),
26✔
758
        );
759
        undo_delta.add_object_deleted(object_name);
26✔
760
    }
761

762
    // Process deleted objects - to undo, we need to add them back (filter to only MooObject types)
763
    for deleted_obj in change
27✔
764
        .deleted_objects
27✔
765
        .iter()
27✔
766
        .filter(|o| o.object_type == VcsObjectType::MooObject)
27✔
767
    {
768
        let object_name = obj_id_to_object_name(
×
769
            &deleted_obj.name,
×
770
            object_names.get(&deleted_obj.name).map(|s| s.as_str()),
×
771
        );
772
        undo_delta.add_object_added(object_name);
×
773
    }
774

775
    // Process renamed objects - to undo, we need to rename them back (filter to only MooObject types)
776
    for renamed in change.renamed_objects.iter().filter(|r| {
27✔
777
        r.from.object_type == VcsObjectType::MooObject
×
778
            && r.to.object_type == VcsObjectType::MooObject
×
779
    }) {
×
780
        let from_name = obj_id_to_object_name(
×
781
            &renamed.from.name,
×
782
            object_names.get(&renamed.from.name).map(|s| s.as_str()),
×
783
        );
784
        let to_name = obj_id_to_object_name(
×
785
            &renamed.to.name,
×
786
            object_names.get(&renamed.to.name).map(|s| s.as_str()),
×
787
        );
788
        undo_delta.add_object_renamed(to_name, from_name);
×
789
    }
790

791
    // Process modified objects - to undo, we need to mark them as modified
792
    // and create basic ObjectChange entries (filter to only MooObject types)
793
    for modified_obj in change
27✔
794
        .modified_objects
27✔
795
        .iter()
27✔
796
        .filter(|o| o.object_type == VcsObjectType::MooObject)
27✔
797
    {
798
        let object_name = obj_id_to_object_name(
×
799
            &modified_obj.name,
×
800
            object_names.get(&modified_obj.name).map(|s| s.as_str()),
×
801
        );
802
        undo_delta.add_object_modified(object_name.clone());
×
803

804
        // Create a basic ObjectChange for modified objects
805
        // In a real implementation, you'd want to track what specifically changed
806
        let mut object_change = ObjectChange::new(object_name);
×
807
        object_change.props_modified.insert("content".to_string());
×
808
        undo_delta.add_object_change(object_change);
×
809
    }
810

811
    Ok(undo_delta)
27✔
812
}
27✔
813

814
/// Get object names for the change objects to improve display names
815
/// This is a simplified implementation - in practice you'd want to
816
/// query the actual object names from the MOO database
817
pub fn get_object_names_for_change(change: &Change) -> HashMap<String, String> {
27✔
818
    let mut object_names = HashMap::new();
27✔
819

820
    // Try to get object names from workspace provider (filter to only MooObject types)
821
    for obj_info in change
27✔
822
        .added_objects
27✔
823
        .iter()
27✔
824
        .chain(change.modified_objects.iter())
27✔
825
        .chain(change.deleted_objects.iter())
27✔
826
        .filter(|o| o.object_type == VcsObjectType::MooObject)
27✔
827
    {
26✔
828
        // For now, we'll just use the object name as the name
26✔
829
        // In a real implementation, you'd query the actual object names
26✔
830
        object_names.insert(obj_info.name.clone(), obj_info.name.clone());
26✔
831
    }
26✔
832

833
    for renamed in change.renamed_objects.iter().filter(|r| {
27✔
834
        r.from.object_type == VcsObjectType::MooObject
×
835
            && r.to.object_type == VcsObjectType::MooObject
×
836
    }) {
×
837
        object_names.insert(renamed.from.name.clone(), renamed.from.name.clone());
×
838
        object_names.insert(renamed.to.name.clone(), renamed.to.name.clone());
×
839
    }
×
840

841
    object_names
27✔
842
}
27✔
843

844
#[cfg(test)]
845
mod tests {
846
    use super::*;
847

848
    #[test]
849
    fn test_object_change_to_moo_var() {
2✔
850
        let mut change = ObjectChange::new("TestObject".to_string());
2✔
851
        change.verbs_added.insert("new_verb".to_string());
2✔
852
        change.props_modified.insert("existing_prop".to_string());
2✔
853

854
        let moo_var = change.to_moo_var();
2✔
855

856
        // Verify it's a map
857
        assert!(matches!(moo_var.variant(), moor_var::Variant::Map(_)));
2✔
858
    }
2✔
859

860
    #[test]
861
    fn test_object_diff_model_to_moo_var() {
2✔
862
        let mut model = ObjectDiffModel::new();
2✔
863
        model.add_object_added("NewObject".to_string());
2✔
864
        model.add_object_deleted("OldObject".to_string());
2✔
865

866
        let moo_var = model.to_moo_var();
2✔
867

868
        // Verify it's a map
869
        assert!(matches!(moo_var.variant(), moor_var::Variant::Map(_)));
2✔
870
    }
2✔
871

872
    #[test]
873
    fn test_obj_id_to_object_name() {
2✔
874
        assert_eq!(obj_id_to_object_name("#4", Some("foobar")), "Foobar");
2✔
875
        assert_eq!(obj_id_to_object_name("#4", Some("#4")), "#4");
2✔
876
        assert_eq!(obj_id_to_object_name("#4", None), "#4");
2✔
877
        assert_eq!(
2✔
878
            obj_id_to_object_name("TestObject", Some("TestObject")),
2✔
879
            "TestObject"
880
        );
881
    }
2✔
882

883
    #[test]
884
    fn test_merge_object_diff_models() {
2✔
885
        let mut model1 = ObjectDiffModel::new();
2✔
886
        model1.add_object_added("Object1".to_string());
2✔
887

888
        let mut model2 = ObjectDiffModel::new();
2✔
889
        model2.add_object_added("Object2".to_string());
2✔
890
        model2.add_object_deleted("Object3".to_string());
2✔
891

892
        model1.merge(model2);
2✔
893

894
        assert!(model1.objects_added.contains("Object1"));
2✔
895
        assert!(model1.objects_added.contains("Object2"));
2✔
896
        assert!(model1.objects_deleted.contains("Object3"));
2✔
897
    }
2✔
898
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc