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

biscuitWizard / moor-hackercore / 18516930916

15 Oct 2025 03:33AM UTC coverage: 80.621% (+0.1%) from 80.516%
18516930916

push

github

biscuitWizard
Moved to https calls

7917 of 9820 relevant lines covered (80.62%)

357.35 hits per line

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

79.74
/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 {
82✔
53
        Self {
82✔
54
            obj_id,
82✔
55
            verbs_modified: HashSet::new(),
82✔
56
            verbs_added: HashSet::new(),
82✔
57
            verbs_renamed: HashMap::new(),
82✔
58
            verbs_deleted: HashSet::new(),
82✔
59
            props_modified: HashSet::new(),
82✔
60
            props_added: HashSet::new(),
82✔
61
            props_renamed: HashMap::new(),
82✔
62
            props_deleted: HashSet::new(),
82✔
63
        }
82✔
64
    }
82✔
65

66
    /// Invert this ObjectChange to create the reverse operations needed to undo it
67
    /// 
68
    /// This is used when abandoning a change - we need to return the inverse operations
69
    /// so the MOO database can undo the changes. For example:
70
    /// - verbs_added becomes verbs_deleted (to undo an addition, we delete)
71
    /// - verbs_deleted becomes verbs_added (to undo a deletion, we add back)
72
    /// - verbs_renamed is reversed (old->new becomes new->old)
73
    /// - verbs_modified stays the same (modifications need to be reverted)
74
    pub fn invert(&self) -> ObjectChange {
37✔
75
        ObjectChange {
76
            obj_id: self.obj_id.clone(),
37✔
77
            // Modifications are symmetric - still need to modify to revert
78
            verbs_modified: self.verbs_modified.clone(),
37✔
79
            // Swap added ↔ deleted to invert the operation
80
            verbs_added: self.verbs_deleted.clone(),
37✔
81
            verbs_deleted: self.verbs_added.clone(),
37✔
82
            // Reverse rename direction (old->new becomes new->old)
83
            verbs_renamed: self
37✔
84
                .verbs_renamed
37✔
85
                .iter()
37✔
86
                .map(|(k, v)| (v.clone(), k.clone()))
37✔
87
                .collect(),
37✔
88
            // Properties follow the same pattern
89
            props_modified: self.props_modified.clone(),
37✔
90
            props_added: self.props_deleted.clone(),
37✔
91
            props_deleted: self.props_added.clone(),
37✔
92
            props_renamed: self
37✔
93
                .props_renamed
37✔
94
                .iter()
37✔
95
                .map(|(k, v)| (v.clone(), k.clone()))
37✔
96
                .collect(),
37✔
97
        }
98
    }
37✔
99

100
    /// Convert this ObjectChange to a MOO v_map
101
    pub fn to_moo_var(&self) -> Var {
79✔
102
        let mut pairs = Vec::new();
79✔
103

104
        // obj_id - use helper to convert to v_obj if it's a numeric ID
105
        pairs.push((v_str("obj_id"), object_id_to_var(&self.obj_id)));
79✔
106

107
        // verbs_modified
108
        let verbs_modified_list: Vec<Var> = self.verbs_modified.iter().map(|v| v_str(v)).collect();
79✔
109
        pairs.push((
79✔
110
            v_str("verbs_modified"),
79✔
111
            moor_var::v_list(&verbs_modified_list),
79✔
112
        ));
79✔
113

114
        // verbs_added
115
        let verbs_added_list: Vec<Var> = self.verbs_added.iter().map(|v| v_str(v)).collect();
79✔
116
        pairs.push((v_str("verbs_added"), moor_var::v_list(&verbs_added_list)));
79✔
117

118
        // verbs_renamed
119
        let verbs_renamed_map: Vec<(Var, Var)> = self
79✔
120
            .verbs_renamed
79✔
121
            .iter()
79✔
122
            .map(|(k, v)| (v_str(k), v_str(v)))
79✔
123
            .collect();
79✔
124
        pairs.push((v_str("verbs_renamed"), v_map(&verbs_renamed_map)));
79✔
125

126
        // verbs_deleted
127
        let verbs_deleted_list: Vec<Var> = self.verbs_deleted.iter().map(|v| v_str(v)).collect();
79✔
128
        pairs.push((
79✔
129
            v_str("verbs_deleted"),
79✔
130
            moor_var::v_list(&verbs_deleted_list),
79✔
131
        ));
79✔
132

133
        // props_modified
134
        let props_modified_list: Vec<Var> = self.props_modified.iter().map(|v| v_str(v)).collect();
79✔
135
        pairs.push((
79✔
136
            v_str("props_modified"),
79✔
137
            moor_var::v_list(&props_modified_list),
79✔
138
        ));
79✔
139

140
        // props_added
141
        let props_added_list: Vec<Var> = self.props_added.iter().map(|v| v_str(v)).collect();
79✔
142
        pairs.push((v_str("props_added"), moor_var::v_list(&props_added_list)));
79✔
143

144
        // props_renamed
145
        let props_renamed_map: Vec<(Var, Var)> = self
79✔
146
            .props_renamed
79✔
147
            .iter()
79✔
148
            .map(|(k, v)| (v_str(k), v_str(v)))
79✔
149
            .collect();
79✔
150
        pairs.push((v_str("props_renamed"), v_map(&props_renamed_map)));
79✔
151

152
        // props_deleted
153
        let props_deleted_list: Vec<Var> = self.props_deleted.iter().map(|v| v_str(v)).collect();
79✔
154
        pairs.push((
79✔
155
            v_str("props_deleted"),
79✔
156
            moor_var::v_list(&props_deleted_list),
79✔
157
        ));
79✔
158

159
        v_map(&pairs)
79✔
160
    }
79✔
161
}
162

163
impl ObjectDiffModel {
164
    /// Create a new empty ObjectDiffModel
165
    pub fn new() -> Self {
220✔
166
        Self {
220✔
167
            objects_renamed: HashMap::new(),
220✔
168
            objects_deleted: HashSet::new(),
220✔
169
            objects_added: HashSet::new(),
220✔
170
            objects_modified: HashSet::new(),
220✔
171
            changes: Vec::new(),
220✔
172
        }
220✔
173
    }
220✔
174

175
    /// Convert this ObjectDiffModel to a MOO v_map
176
    pub fn to_moo_var(&self) -> Var {
193✔
177
        let mut pairs = Vec::new();
193✔
178

179
        // objects_renamed - use helper to convert object IDs to v_obj if they're numeric
180
        let objects_renamed_map: Vec<(Var, Var)> = self
193✔
181
            .objects_renamed
193✔
182
            .iter()
193✔
183
            .map(|(k, v)| (object_id_to_var(k), object_id_to_var(v)))
193✔
184
            .collect();
193✔
185
        pairs.push((v_str("objects_renamed"), v_map(&objects_renamed_map)));
193✔
186

187
        // objects_deleted - use helper to convert object IDs to v_obj if they're numeric
188
        let objects_deleted_list: Vec<Var> =
193✔
189
            self.objects_deleted.iter().map(|v| object_id_to_var(v)).collect();
193✔
190
        pairs.push((
193✔
191
            v_str("objects_deleted"),
193✔
192
            moor_var::v_list(&objects_deleted_list),
193✔
193
        ));
193✔
194

195
        // objects_added - use helper to convert object IDs to v_obj if they're numeric
196
        let objects_added_list: Vec<Var> = self.objects_added.iter().map(|v| object_id_to_var(v)).collect();
193✔
197
        pairs.push((
193✔
198
            v_str("objects_added"),
193✔
199
            moor_var::v_list(&objects_added_list),
193✔
200
        ));
193✔
201

202
        // objects_modified - use helper to convert object IDs to v_obj if they're numeric
203
        let objects_modified_list: Vec<Var> =
193✔
204
            self.objects_modified.iter().map(|v| object_id_to_var(v)).collect();
193✔
205
        pairs.push((
193✔
206
            v_str("objects_modified"),
193✔
207
            moor_var::v_list(&objects_modified_list),
193✔
208
        ));
193✔
209

210
        // changes
211
        let changes_list: Vec<Var> = self
193✔
212
            .changes
193✔
213
            .iter()
193✔
214
            .map(|change| change.to_moo_var())
193✔
215
            .collect();
193✔
216
        pairs.push((v_str("changes"), moor_var::v_list(&changes_list)));
193✔
217

218
        v_map(&pairs)
193✔
219
    }
193✔
220

221
    /// Add a renamed object to the model
222
    pub fn add_object_renamed(&mut self, from: String, to: String) {
×
223
        self.objects_renamed.insert(from, to);
×
224
    }
×
225

226
    /// Add a deleted object to the model
227
    pub fn add_object_deleted(&mut self, obj_id: String) {
41✔
228
        self.objects_deleted.insert(obj_id);
41✔
229
    }
41✔
230

231
    /// Add an added object to the model
232
    pub fn add_object_added(&mut self, obj_id: String) {
31✔
233
        self.objects_added.insert(obj_id);
31✔
234
    }
31✔
235

236
    /// Add a modified object to the model
237
    pub fn add_object_modified(&mut self, obj_id: String) {
11✔
238
        self.objects_modified.insert(obj_id);
11✔
239
    }
11✔
240

241
    /// Add or update an object change in the model
242
    pub fn add_object_change(&mut self, change: ObjectChange) {
93✔
243
        // Remove any existing change for this object
244
        self.changes.retain(|c| c.obj_id != change.obj_id);
93✔
245
        self.changes.push(change);
93✔
246
    }
93✔
247

248
    /// Merge another ObjectDiffModel into this one
249
    pub fn merge(&mut self, other: ObjectDiffModel) {
22✔
250
        // Merge renamed objects
251
        for (from, to) in other.objects_renamed {
22✔
252
            self.objects_renamed.insert(from, to);
×
253
        }
×
254

255
        // Merge deleted objects
256
        for obj_id in other.objects_deleted {
31✔
257
            self.objects_deleted.insert(obj_id);
9✔
258
        }
9✔
259

260
        // Merge added objects
261
        for obj_id in other.objects_added {
38✔
262
            self.objects_added.insert(obj_id);
16✔
263
        }
16✔
264

265
        // Merge modified objects
266
        for obj_id in other.objects_modified {
23✔
267
            self.objects_modified.insert(obj_id);
1✔
268
        }
1✔
269

270
        // Merge changes
271
        for change in other.changes {
43✔
272
            self.add_object_change(change);
21✔
273
        }
21✔
274
    }
22✔
275
}
276

277
impl Default for ObjectDiffModel {
278
    fn default() -> Self {
×
279
        Self::new()
×
280
    }
×
281
}
282

283
/// Helper function to convert an object ID string to a MOO Var
284
/// If the string is in the format "#<number>", returns a v_obj (object reference)
285
/// Otherwise, returns a v_str (string)
286
pub fn object_id_to_var(obj_id: &str) -> Var {
153✔
287
    // Check if the string starts with '#' and the rest is a valid number
288
    if let Some(stripped) = obj_id.strip_prefix('#') {
153✔
289
        if let Ok(num) = stripped.parse::<i32>() {
10✔
290
            // This is a numeric object ID like "#73", return as v_obj
291
            return v_objid(num);
10✔
292
        }
×
293
    }
143✔
294
    // Otherwise, return as a string
295
    v_str(obj_id)
143✔
296
}
153✔
297

298
/// Helper function to convert an object ID to an object name
299
/// Returns the object name if it's different from the OID, otherwise returns the OID
300
pub fn obj_id_to_object_name(obj_id: &str, object_name: Option<&str>) -> String {
77✔
301
    match object_name {
75✔
302
        Some(name) if name != obj_id => {
75✔
303
            // Capitalize first letter if it's a name
304
            if let Some(first_char) = name.chars().next() {
2✔
305
                let mut result = String::with_capacity(name.len());
2✔
306
                result.push(first_char.to_uppercase().next().unwrap_or(first_char));
2✔
307
                result.push_str(&name[1..]);
2✔
308
                result
2✔
309
            } else {
310
                name.to_string()
×
311
            }
312
        }
313
        _ => obj_id.to_string(),
75✔
314
    }
315
}
77✔
316

317
/// Compare object versions to determine detailed changes
318
pub fn compare_object_versions(
75✔
319
    database: &DatabaseRef,
75✔
320
    obj_name: &str,
75✔
321
    local_version: u64,
75✔
322
    verb_hints: Option<&[crate::types::VerbRenameHint]>,
75✔
323
    prop_hints: Option<&[crate::types::PropertyRenameHint]>,
75✔
324
) -> Result<ObjectChange, ObjectsTreeError> {
75✔
325
    let mut object_change = ObjectChange::new(obj_name.to_string());
75✔
326

327
    // Get the local version content
328
    let local_sha256 = database
75✔
329
        .refs()
75✔
330
        .get_ref(VcsObjectType::MooObject, obj_name, Some(local_version))
75✔
331
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?
75✔
332
        .ok_or_else(|| {
75✔
333
            ObjectsTreeError::SerializationError(format!(
×
334
                "Local version {local_version} of object '{obj_name}' not found"
×
335
            ))
×
336
        })?;
×
337

338
    let local_content = database
75✔
339
        .objects()
75✔
340
        .get(&local_sha256)
75✔
341
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?
75✔
342
        .ok_or_else(|| {
75✔
343
            ObjectsTreeError::SerializationError(format!(
×
344
                "Object content for SHA256 '{local_sha256}' not found"
×
345
            ))
×
346
        })?;
×
347

348
    // Parse local object definition
349
    let local_def = database
75✔
350
        .objects()
75✔
351
        .parse_object_dump(&local_content)
75✔
352
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?;
75✔
353

354
    // Get the baseline version (previous version)
355
    // For version 1 (new object), there is no baseline (version 0 doesn't exist)
356
    // For version 2+, the baseline is the previous version
357
    let baseline_version = local_version.saturating_sub(1);
75✔
358
    let baseline_sha256 = database
75✔
359
        .refs()
75✔
360
        .get_ref(VcsObjectType::MooObject, obj_name, Some(baseline_version))
75✔
361
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?;
75✔
362

363
    if let Some(baseline_sha256) = baseline_sha256 {
75✔
364
        // Get baseline content and parse it
365
        let baseline_content = database
13✔
366
            .objects()
13✔
367
            .get(&baseline_sha256)
13✔
368
            .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?
13✔
369
            .ok_or_else(|| {
13✔
370
                ObjectsTreeError::SerializationError(format!(
×
371
                    "Baseline object content for SHA256 '{baseline_sha256}' not found"
×
372
                ))
×
373
            })?;
×
374

375
        let baseline_def = database
13✔
376
            .objects()
13✔
377
            .parse_object_dump(&baseline_content)
13✔
378
            .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?;
13✔
379

380
        // Compare the two object definitions with meta filtering and hints
381
        compare_object_definitions_with_meta(
13✔
382
            &baseline_def,
13✔
383
            &local_def,
13✔
384
            &mut object_change,
13✔
385
            Some(database),
13✔
386
            Some(obj_name),
13✔
387
            verb_hints,
13✔
388
            prop_hints,
13✔
389
        );
390
    } else {
391
        // No baseline version - this is a new object, mark all as added
392
        for verb in &local_def.verbs {
63✔
393
            for verb_name in &verb.names {
2✔
394
                object_change.verbs_added.insert(verb_name.as_string());
1✔
395
            }
1✔
396
        }
397
        for prop_def in &local_def.property_definitions {
64✔
398
            object_change.props_added.insert(prop_def.name.as_string());
2✔
399
        }
2✔
400
        for prop_override in &local_def.property_overrides {
62✔
401
            object_change
×
402
                .props_added
×
403
                .insert(prop_override.name.as_string());
×
404
        }
×
405
    }
406

407
    Ok(object_change)
75✔
408
}
75✔
409

410
/// Apply hints to convert added/deleted verbs/properties to renames
411
fn apply_hints_to_object_change(
13✔
412
    object_change: &mut ObjectChange,
13✔
413
    obj_name: &str,
13✔
414
    verb_hints: &[crate::types::VerbRenameHint],
13✔
415
    prop_hints: &[crate::types::PropertyRenameHint],
13✔
416
) {
13✔
417
    // Apply verb rename hints
418
    for hint in verb_hints {
13✔
419
        if hint.object_name != obj_name {
×
420
            continue; // Skip hints for other objects
×
421
        }
×
422

423
        // Check if both from_verb and to_verb are in the expected sets
424
        let from_in_deleted = object_change.verbs_deleted.contains(&hint.from_verb);
×
425
        let to_in_added = object_change.verbs_added.contains(&hint.to_verb);
×
426

427
        if from_in_deleted && to_in_added {
×
428
            // This is a valid rename hint - apply it
429
            object_change.verbs_deleted.remove(&hint.from_verb);
×
430
            object_change.verbs_added.remove(&hint.to_verb);
×
431
            object_change.verbs_renamed.insert(hint.from_verb.clone(), hint.to_verb.clone());
×
432
            
433
            tracing::debug!(
×
434
                "Applied verb rename hint for object '{}': '{}' -> '{}'",
×
435
                obj_name, hint.from_verb, hint.to_verb
436
            );
437
        }
×
438
    }
439

440
    // Apply property rename hints
441
    for hint in prop_hints {
13✔
442
        if hint.object_name != obj_name {
×
443
            continue; // Skip hints for other objects
×
444
        }
×
445

446
        // Check if both from_prop and to_prop are in the expected sets
447
        let from_in_deleted = object_change.props_deleted.contains(&hint.from_prop);
×
448
        let to_in_added = object_change.props_added.contains(&hint.to_prop);
×
449

450
        if from_in_deleted && to_in_added {
×
451
            // This is a valid rename hint - apply it
452
            object_change.props_deleted.remove(&hint.from_prop);
×
453
            object_change.props_added.remove(&hint.to_prop);
×
454
            object_change.props_renamed.insert(hint.from_prop.clone(), hint.to_prop.clone());
×
455
            
456
            tracing::debug!(
×
457
                "Applied property rename hint for object '{}': '{}' -> '{}'",
×
458
                obj_name, hint.from_prop, hint.to_prop
459
            );
460
        }
×
461
    }
462
}
13✔
463

464
/// Compare two ObjectDefinitions with optional meta filtering and rename hints
465
pub fn compare_object_definitions_with_meta(
13✔
466
    baseline: &ObjectDefinition,
13✔
467
    local: &ObjectDefinition,
13✔
468
    object_change: &mut ObjectChange,
13✔
469
    database: Option<&DatabaseRef>,
13✔
470
    obj_name: Option<&str>,
13✔
471
    verb_hints: Option<&[crate::types::VerbRenameHint]>,
13✔
472
    prop_hints: Option<&[crate::types::PropertyRenameHint]>,
13✔
473
) {
13✔
474
    // Load meta if database and obj_name are provided
475
    let meta = if let (Some(db), Some(name)) = (database, obj_name) {
13✔
476
        match db.refs().get_ref(VcsObjectType::MooMetaObject, name, None) {
13✔
477
            Ok(Some(meta_sha256)) => match db.objects().get(&meta_sha256) {
5✔
478
                Ok(Some(yaml)) => db.objects().parse_meta_dump(&yaml).ok(),
5✔
479
                _ => None,
×
480
            },
481
            _ => None,
8✔
482
        }
483
    } else {
484
        None
×
485
    };
486
    // Compare verbs
487
    let baseline_verbs: HashMap<String, &moor_compiler::ObjVerbDef> = baseline
13✔
488
        .verbs
13✔
489
        .iter()
13✔
490
        .flat_map(|v| v.names.iter().map(move |name| (name.as_string(), v)))
13✔
491
        .collect();
13✔
492

493
    let local_verbs: HashMap<String, &moor_compiler::ObjVerbDef> = local
13✔
494
        .verbs
13✔
495
        .iter()
13✔
496
        .flat_map(|v| v.names.iter().map(move |name| (name.as_string(), v)))
13✔
497
        .collect();
13✔
498

499
    // Find added, modified, and deleted verbs
500
    for (verb_name, local_verb) in &local_verbs {
23✔
501
        if let Some(baseline_verb) = baseline_verbs.get(verb_name) {
10✔
502
            // Verb exists in both - check if it's modified
503
            if verbs_differ(baseline_verb, local_verb) {
9✔
504
                object_change.verbs_modified.insert(verb_name.clone());
1✔
505
            }
8✔
506
        } else {
1✔
507
            // Verb is new
1✔
508
            object_change.verbs_added.insert(verb_name.clone());
1✔
509
        }
1✔
510
    }
511

512
    for verb_name in baseline_verbs.keys() {
13✔
513
        if !local_verbs.contains_key(verb_name) {
12✔
514
            // Verb is missing - check if it's ignored before marking as deleted
515
            let is_ignored = meta
3✔
516
                .as_ref()
3✔
517
                .map(|m| m.ignored_verbs.contains(verb_name))
3✔
518
                .unwrap_or(false);
3✔
519

520
            if !is_ignored {
3✔
521
                // Verb was actually deleted (not just ignored)
1✔
522
                object_change.verbs_deleted.insert(verb_name.clone());
1✔
523
            } else {
1✔
524
                tracing::debug!(
2✔
525
                    "Verb '{}' is missing but ignored in meta, not marking as deleted",
×
526
                    verb_name
527
                );
528
            }
529
        }
9✔
530
    }
531

532
    // Compare property definitions
533
    let baseline_props: HashMap<String, &moor_compiler::ObjPropDef> = baseline
13✔
534
        .property_definitions
13✔
535
        .iter()
13✔
536
        .map(|p| (p.name.as_string(), p))
13✔
537
        .collect();
13✔
538

539
    let local_props: HashMap<String, &moor_compiler::ObjPropDef> = local
13✔
540
        .property_definitions
13✔
541
        .iter()
13✔
542
        .map(|p| (p.name.as_string(), p))
13✔
543
        .collect();
13✔
544

545
    // Find added, modified, and deleted property definitions
546
    for (prop_name, local_prop) in &local_props {
19✔
547
        if let Some(baseline_prop) = baseline_props.get(prop_name) {
6✔
548
            // Property exists in both - check if it's modified
549
            if property_definitions_differ(baseline_prop, local_prop) {
6✔
550
                object_change.props_modified.insert(prop_name.clone());
×
551
            }
6✔
552
        } else {
×
553
            // Property is new
×
554
            object_change.props_added.insert(prop_name.clone());
×
555
        }
×
556
    }
557

558
    for prop_name in baseline_props.keys() {
13✔
559
        if !local_props.contains_key(prop_name) {
10✔
560
            // Property is missing - check if it's ignored before marking as deleted
561
            let is_ignored = meta
4✔
562
                .as_ref()
4✔
563
                .map(|m| m.ignored_properties.contains(prop_name))
4✔
564
                .unwrap_or(false);
4✔
565

566
            if !is_ignored {
4✔
567
                // Property was actually deleted (not just ignored)
1✔
568
                object_change.props_deleted.insert(prop_name.clone());
1✔
569
            } else {
1✔
570
                tracing::debug!(
3✔
571
                    "Property '{}' is missing but ignored in meta, not marking as deleted",
×
572
                    prop_name
573
                );
574
            }
575
        }
6✔
576
    }
577

578
    // Compare property overrides
579
    let baseline_overrides: HashMap<String, &moor_compiler::ObjPropOverride> = baseline
13✔
580
        .property_overrides
13✔
581
        .iter()
13✔
582
        .map(|p| (p.name.as_string(), p))
13✔
583
        .collect();
13✔
584

585
    let local_overrides: HashMap<String, &moor_compiler::ObjPropOverride> = local
13✔
586
        .property_overrides
13✔
587
        .iter()
13✔
588
        .map(|p| (p.name.as_string(), p))
13✔
589
        .collect();
13✔
590

591
    // Find added, modified, and deleted property overrides
592
    for (prop_name, local_override) in &local_overrides {
13✔
593
        if let Some(baseline_override) = baseline_overrides.get(prop_name) {
×
594
            // Override exists in both - check if it's modified
595
            if property_overrides_differ(baseline_override, local_override) {
×
596
                object_change.props_modified.insert(prop_name.clone());
×
597
            }
×
598
        } else {
×
599
            // Override is new
×
600
            object_change.props_added.insert(prop_name.clone());
×
601
        }
×
602
    }
603

604
    for prop_name in baseline_overrides.keys() {
13✔
605
        if !local_overrides.contains_key(prop_name) {
×
606
            // Override is missing - check if it's ignored before marking as deleted
607
            let is_ignored = meta
×
608
                .as_ref()
×
609
                .map(|m| m.ignored_properties.contains(prop_name))
×
610
                .unwrap_or(false);
×
611

612
            if !is_ignored {
×
613
                // Override was actually deleted (not just ignored)
×
614
                object_change.props_deleted.insert(prop_name.clone());
×
615
            } else {
×
616
                tracing::debug!(
×
617
                    "Property override '{}' is missing but ignored in meta, not marking as deleted",
×
618
                    prop_name
619
                );
620
            }
621
        }
×
622
    }
623

624
    // Apply hints if provided
625
    if let Some(name) = obj_name {
13✔
626
        if let (Some(v_hints), Some(p_hints)) = (verb_hints, prop_hints) {
13✔
627
            apply_hints_to_object_change(object_change, name, v_hints, p_hints);
13✔
628
        }
13✔
629
    }
×
630
}
13✔
631

632
/// Check if two verb definitions differ
633
pub fn verbs_differ(
9✔
634
    baseline: &moor_compiler::ObjVerbDef,
9✔
635
    local: &moor_compiler::ObjVerbDef,
9✔
636
) -> bool {
9✔
637
    baseline.argspec != local.argspec
9✔
638
        || baseline.owner != local.owner
9✔
639
        || baseline.flags != local.flags
9✔
640
        || baseline.program != local.program
9✔
641
}
9✔
642

643
/// Check if two property definitions differ
644
pub fn property_definitions_differ(
6✔
645
    baseline: &moor_compiler::ObjPropDef,
6✔
646
    local: &moor_compiler::ObjPropDef,
6✔
647
) -> bool {
6✔
648
    baseline.perms != local.perms || baseline.value != local.value
6✔
649
}
6✔
650

651
/// Check if two property overrides differ
652
pub fn property_overrides_differ(
×
653
    baseline: &moor_compiler::ObjPropOverride,
×
654
    local: &moor_compiler::ObjPropOverride,
×
655
) -> bool {
×
656
    baseline.value != local.value || baseline.perms_update != local.perms_update
×
657
}
×
658

659
/// Build an ObjectDiffModel by comparing a change against the compiled state
660
/// This is the shared logic used by approve and status operations
661
pub fn build_object_diff_from_change(
29✔
662
    database: &DatabaseRef,
29✔
663
    change: &Change,
29✔
664
) -> Result<ObjectDiffModel, ObjectsTreeError> {
29✔
665
    let mut diff_model = ObjectDiffModel::new();
29✔
666

667
    // Get the complete object list from the index state (excluding the local change)
668
    let complete_object_list = database
29✔
669
        .index()
29✔
670
        .compute_complete_object_list()
29✔
671
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?;
29✔
672

673
    tracing::info!(
29✔
674
        "Using complete object list with {} objects as baseline for change '{}'",
×
675
        complete_object_list.len(),
×
676
        change.name
677
    );
678

679
    // Process the change to build the diff
680
    process_change_for_diff(database, &mut diff_model, change)?;
29✔
681

682
    Ok(diff_model)
29✔
683
}
29✔
684

685
/// Process a single change and add its modifications to the diff model
686
/// This is the shared logic used by approve and status operations
687
pub fn process_change_for_diff(
29✔
688
    database: &DatabaseRef,
29✔
689
    diff_model: &mut ObjectDiffModel,
29✔
690
    change: &Change,
29✔
691
) -> Result<(), ObjectsTreeError> {
29✔
692
    // Use hints from the change (they're kept permanently now)
693
    let verb_hints_ref = Some(change.verb_rename_hints.as_slice());
29✔
694
    let prop_hints_ref = Some(change.property_rename_hints.as_slice());
29✔
695

696
    // Process added objects (filter to only MooObject types)
697
    for obj_info in change
29✔
698
        .added_objects
29✔
699
        .iter()
29✔
700
        .filter(|o| o.object_type == VcsObjectType::MooObject)
29✔
701
    {
702
        let obj_name = obj_id_to_object_name(&obj_info.name, Some(&obj_info.name));
24✔
703
        diff_model.add_object_added(obj_name.clone());
24✔
704

705
        // Get detailed object changes by comparing local vs baseline (which will be empty for new objects)
706
        let object_change = compare_object_versions(
24✔
707
            database,
24✔
708
            &obj_name,
24✔
709
            obj_info.version,
24✔
710
            verb_hints_ref,
24✔
711
            prop_hints_ref,
24✔
712
        )?;
×
713
        diff_model.add_object_change(object_change);
24✔
714
    }
715

716
    // Process deleted objects (filter to only MooObject types)
717
    for obj_info in change
29✔
718
        .deleted_objects
29✔
719
        .iter()
29✔
720
        .filter(|o| o.object_type == VcsObjectType::MooObject)
29✔
721
    {
1✔
722
        let obj_name = obj_id_to_object_name(&obj_info.name, Some(&obj_info.name));
1✔
723
        diff_model.add_object_deleted(obj_name);
1✔
724
    }
1✔
725

726
    // Process renamed objects (filter to only MooObject types)
727
    for renamed in change.renamed_objects.iter().filter(|r| {
29✔
728
        r.from.object_type == VcsObjectType::MooObject
×
729
            && r.to.object_type == VcsObjectType::MooObject
×
730
    }) {
×
731
        let from_name = obj_id_to_object_name(&renamed.from.name, Some(&renamed.from.name));
×
732
        let to_name = obj_id_to_object_name(&renamed.to.name, Some(&renamed.to.name));
×
733
        diff_model.add_object_renamed(from_name, to_name);
×
734
    }
×
735

736
    // Process modified objects with detailed comparison (filter to only MooObject types)
737
    for obj_info in change
29✔
738
        .modified_objects
29✔
739
        .iter()
29✔
740
        .filter(|o| o.object_type == VcsObjectType::MooObject)
29✔
741
    {
742
        let obj_name = obj_id_to_object_name(&obj_info.name, Some(&obj_info.name));
6✔
743
        diff_model.add_object_modified(obj_name.clone());
6✔
744

745
        // Get detailed object changes by comparing local vs baseline
746
        let object_change = compare_object_versions(
6✔
747
            database,
6✔
748
            &obj_name,
6✔
749
            obj_info.version,
6✔
750
            verb_hints_ref,
6✔
751
            prop_hints_ref,
6✔
752
        )?;
×
753
        diff_model.add_object_change(object_change);
6✔
754
    }
755

756
    Ok(())
29✔
757
}
29✔
758

759
/// Build an ObjectDiffModel for abandoning a change (undo operations)
760
/// This creates the reverse operations needed to undo the change
761
pub fn build_abandon_diff_from_change(
45✔
762
    database: &DatabaseRef,
45✔
763
    change: &Change,
45✔
764
) -> Result<ObjectDiffModel, ObjectsTreeError> {
45✔
765
    // Get the complete object list from the index state for comparison
766
    let complete_object_list = database
45✔
767
        .index()
45✔
768
        .compute_complete_object_list()
45✔
769
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?;
45✔
770

771
    tracing::info!(
45✔
772
        "Using complete object list with {} objects as baseline for abandoning change '{}'",
×
773
        complete_object_list.len(),
×
774
        change.name
775
    );
776

777
    // Create a delta model showing what needs to be undone
778
    let mut undo_delta = ObjectDiffModel::new();
45✔
779

780
    // Get object name mappings for better display names
781
    let object_names = get_object_names_for_change(change);
45✔
782

783
    // Use hints from the change for proper rename tracking
784
    let verb_hints_ref = Some(change.verb_rename_hints.as_slice());
45✔
785
    let prop_hints_ref = Some(change.property_rename_hints.as_slice());
45✔
786

787
    // Process added objects - to undo, we need to delete them (filter to only MooObject types)
788
    for added_obj in change
45✔
789
        .added_objects
45✔
790
        .iter()
45✔
791
        .filter(|o| o.object_type == VcsObjectType::MooObject)
45✔
792
    {
793
        let object_name = obj_id_to_object_name(
33✔
794
            &added_obj.name,
33✔
795
            object_names.get(&added_obj.name).map(|s| s.as_str()),
33✔
796
        );
797
        undo_delta.add_object_deleted(object_name.clone());
33✔
798

799
        // Get the detailed changes for this added object, then invert them
800
        // This gives us the verb/property details for the deletion
801
        let object_change = compare_object_versions(
33✔
802
            database,
33✔
803
            &object_name,
33✔
804
            added_obj.version,
33✔
805
            verb_hints_ref,
33✔
806
            prop_hints_ref,
33✔
807
        )?;
×
808
        
809
        // Invert the change to show what needs to be deleted/undone
810
        let inverted_change = object_change.invert();
33✔
811
        undo_delta.add_object_change(inverted_change);
33✔
812
    }
813

814
    // Process deleted objects - to undo, we need to add them back (filter to only MooObject types)
815
    for deleted_obj in change
45✔
816
        .deleted_objects
45✔
817
        .iter()
45✔
818
        .filter(|o| o.object_type == VcsObjectType::MooObject)
45✔
819
    {
820
        let object_name = obj_id_to_object_name(
1✔
821
            &deleted_obj.name,
1✔
822
            object_names.get(&deleted_obj.name).map(|s| s.as_str()),
1✔
823
        );
824
        undo_delta.add_object_added(object_name.clone());
1✔
825

826
        // For deleted objects, we need to get the baseline version (the version before deletion)
827
        // and show what needs to be added back
828
        // The deleted_obj.version represents the last version before deletion
829
        let baseline_version = deleted_obj.version;
1✔
830
        
831
        // Get the baseline object to see what needs to be re-added
832
        if let Ok(Some(baseline_sha256)) = database.refs().get_ref(
1✔
833
            VcsObjectType::MooObject,
1✔
834
            &deleted_obj.name,
1✔
835
            Some(baseline_version),
1✔
836
        ) {
837
            if let Ok(Some(baseline_content)) = database.objects().get(&baseline_sha256) {
1✔
838
                if let Ok(baseline_def) = database.objects().parse_object_dump(&baseline_content) {
1✔
839
                    // Create an ObjectChange showing what needs to be added back
840
                    let mut object_change = ObjectChange::new(object_name);
1✔
841
                    
842
                    // Mark all verbs as needing to be added back
843
                    for verb in &baseline_def.verbs {
1✔
844
                        for verb_name in &verb.names {
×
845
                            object_change.verbs_added.insert(verb_name.as_string());
×
846
                        }
×
847
                    }
848
                    
849
                    // Mark all properties as needing to be added back
850
                    for prop_def in &baseline_def.property_definitions {
1✔
851
                        object_change.props_added.insert(prop_def.name.as_string());
×
852
                    }
×
853
                    for prop_override in &baseline_def.property_overrides {
1✔
854
                        object_change.props_added.insert(prop_override.name.as_string());
×
855
                    }
×
856
                    
857
                    undo_delta.add_object_change(object_change);
1✔
858
                }
×
859
            }
×
860
        }
×
861
    }
862

863
    // Process renamed objects - to undo, we need to rename them back (filter to only MooObject types)
864
    for renamed in change.renamed_objects.iter().filter(|r| {
45✔
865
        r.from.object_type == VcsObjectType::MooObject
×
866
            && r.to.object_type == VcsObjectType::MooObject
×
867
    }) {
×
868
        let from_name = obj_id_to_object_name(
×
869
            &renamed.from.name,
×
870
            object_names.get(&renamed.from.name).map(|s| s.as_str()),
×
871
        );
872
        let to_name = obj_id_to_object_name(
×
873
            &renamed.to.name,
×
874
            object_names.get(&renamed.to.name).map(|s| s.as_str()),
×
875
        );
876
        // Reverse the rename direction for undo
877
        undo_delta.add_object_renamed(to_name, from_name);
×
878
    }
879

880
    // Process modified objects - get detailed changes and invert them (filter to only MooObject types)
881
    for modified_obj in change
45✔
882
        .modified_objects
45✔
883
        .iter()
45✔
884
        .filter(|o| o.object_type == VcsObjectType::MooObject)
45✔
885
    {
886
        let object_name = obj_id_to_object_name(
4✔
887
            &modified_obj.name,
4✔
888
            object_names.get(&modified_obj.name).map(|s| s.as_str()),
4✔
889
        );
890
        undo_delta.add_object_modified(object_name.clone());
4✔
891

892
        // Get the detailed changes by comparing versions
893
        let object_change = compare_object_versions(
4✔
894
            database,
4✔
895
            &object_name,
4✔
896
            modified_obj.version,
4✔
897
            verb_hints_ref,
4✔
898
            prop_hints_ref,
4✔
899
        )?;
×
900
        
901
        // INVERT the change to get the undo operations
902
        // If a verb was added in the change, we need to delete it to undo
903
        // If a verb was deleted in the change, we need to add it back to undo
904
        let inverted_change = object_change.invert();
4✔
905
        undo_delta.add_object_change(inverted_change);
4✔
906
    }
907

908
    Ok(undo_delta)
45✔
909
}
45✔
910

911
/// Get object names for the change objects to improve display names
912
/// This is a simplified implementation - in practice you'd want to
913
/// query the actual object names from the MOO database
914
pub fn get_object_names_for_change(change: &Change) -> HashMap<String, String> {
45✔
915
    let mut object_names = HashMap::new();
45✔
916

917
    // Try to get object names from workspace provider (filter to only MooObject types)
918
    for obj_info in change
45✔
919
        .added_objects
45✔
920
        .iter()
45✔
921
        .chain(change.modified_objects.iter())
45✔
922
        .chain(change.deleted_objects.iter())
45✔
923
        .filter(|o| o.object_type == VcsObjectType::MooObject)
45✔
924
    {
38✔
925
        // For now, we'll just use the object name as the name
38✔
926
        // In a real implementation, you'd query the actual object names
38✔
927
        object_names.insert(obj_info.name.clone(), obj_info.name.clone());
38✔
928
    }
38✔
929

930
    for renamed in change.renamed_objects.iter().filter(|r| {
45✔
931
        r.from.object_type == VcsObjectType::MooObject
×
932
            && r.to.object_type == VcsObjectType::MooObject
×
933
    }) {
×
934
        object_names.insert(renamed.from.name.clone(), renamed.from.name.clone());
×
935
        object_names.insert(renamed.to.name.clone(), renamed.to.name.clone());
×
936
    }
×
937

938
    object_names
45✔
939
}
45✔
940

941
#[cfg(test)]
942
mod tests {
943
    use super::*;
944

945
    #[test]
946
    fn test_object_change_to_moo_var() {
2✔
947
        let mut change = ObjectChange::new("TestObject".to_string());
2✔
948
        change.verbs_added.insert("new_verb".to_string());
2✔
949
        change.props_modified.insert("existing_prop".to_string());
2✔
950

951
        let moo_var = change.to_moo_var();
2✔
952

953
        // Verify it's a map
954
        assert!(matches!(moo_var.variant(), moor_var::Variant::Map(_)));
2✔
955
    }
2✔
956

957
    #[test]
958
    fn test_object_diff_model_to_moo_var() {
2✔
959
        let mut model = ObjectDiffModel::new();
2✔
960
        model.add_object_added("NewObject".to_string());
2✔
961
        model.add_object_deleted("OldObject".to_string());
2✔
962

963
        let moo_var = model.to_moo_var();
2✔
964

965
        // Verify it's a map
966
        assert!(matches!(moo_var.variant(), moor_var::Variant::Map(_)));
2✔
967
    }
2✔
968

969
    #[test]
970
    fn test_obj_id_to_object_name() {
2✔
971
        assert_eq!(obj_id_to_object_name("#4", Some("foobar")), "Foobar");
2✔
972
        assert_eq!(obj_id_to_object_name("#4", Some("#4")), "#4");
2✔
973
        assert_eq!(obj_id_to_object_name("#4", None), "#4");
2✔
974
        assert_eq!(
2✔
975
            obj_id_to_object_name("TestObject", Some("TestObject")),
2✔
976
            "TestObject"
977
        );
978
    }
2✔
979

980
    #[test]
981
    fn test_merge_object_diff_models() {
2✔
982
        let mut model1 = ObjectDiffModel::new();
2✔
983
        model1.add_object_added("Object1".to_string());
2✔
984

985
        let mut model2 = ObjectDiffModel::new();
2✔
986
        model2.add_object_added("Object2".to_string());
2✔
987
        model2.add_object_deleted("Object3".to_string());
2✔
988

989
        model1.merge(model2);
2✔
990

991
        assert!(model1.objects_added.contains("Object1"));
2✔
992
        assert!(model1.objects_added.contains("Object2"));
2✔
993
        assert!(model1.objects_deleted.contains("Object3"));
2✔
994
    }
2✔
995
}
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