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

biscuitWizard / moor-hackercore / 18444615293

12 Oct 2025 01:27PM UTC coverage: 80.154% (+0.3%) from 79.829%
18444615293

push

github

biscuitWizard
Fixing race condition in tests

15 of 15 new or added lines in 1 file covered. (100.0%)

305 existing lines in 7 files now uncovered.

7298 of 9105 relevant lines covered (80.15%)

293.96 hits per line

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

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

66
    /// Convert this ObjectChange to a MOO v_map
67
    pub fn to_moo_var(&self) -> Var {
37✔
68
        let mut pairs = Vec::new();
37✔
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)));
37✔
72

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

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

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

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

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

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

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

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

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

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

141
    /// Convert this ObjectDiffModel to a MOO v_map
142
    pub fn to_moo_var(&self) -> Var {
161✔
143
        let mut pairs = Vec::new();
161✔
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
161✔
147
            .objects_renamed
161✔
148
            .iter()
161✔
149
            .map(|(k, v)| (object_id_to_var(k), object_id_to_var(v)))
161✔
150
            .collect();
161✔
151
        pairs.push((v_str("objects_renamed"), v_map(&objects_renamed_map)));
161✔
152

153
        // objects_deleted - use helper to convert object IDs to v_obj if they're numeric
154
        let objects_deleted_list: Vec<Var> =
161✔
155
            self.objects_deleted.iter().map(|v| object_id_to_var(v)).collect();
161✔
156
        pairs.push((
161✔
157
            v_str("objects_deleted"),
161✔
158
            moor_var::v_list(&objects_deleted_list),
161✔
159
        ));
161✔
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();
161✔
163
        pairs.push((
161✔
164
            v_str("objects_added"),
161✔
165
            moor_var::v_list(&objects_added_list),
161✔
166
        ));
161✔
167

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

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

184
        v_map(&pairs)
161✔
185
    }
161✔
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) {
10✔
204
        self.objects_modified.insert(obj_id);
10✔
205
    }
10✔
206

207
    /// Add or update an object change in the model
208
    pub fn add_object_change(&mut self, change: ObjectChange) {
40✔
209
        // Remove any existing change for this object
210
        self.changes.retain(|c| c.obj_id != change.obj_id);
40✔
211
        self.changes.push(change);
40✔
212
    }
40✔
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 {
94✔
253
    // Check if the string starts with '#' and the rest is a valid number
254
    if let Some(stripped) = obj_id.strip_prefix('#') {
94✔
255
        if let Ok(num) = stripped.parse::<i32>() {
20✔
256
            // This is a numeric object ID like "#73", return as v_obj
257
            return v_objid(num);
20✔
UNCOV
258
        }
×
259
    }
74✔
260
    // Otherwise, return as a string
261
    v_str(obj_id)
74✔
262
}
94✔
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 {
64✔
267
    match object_name {
62✔
268
        Some(name) if name != obj_id => {
62✔
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 {
UNCOV
276
                name.to_string()
×
277
            }
278
        }
279
        _ => obj_id.to_string(),
62✔
280
    }
281
}
64✔
282

283
/// Compare object versions to determine detailed changes
284
pub fn compare_object_versions(
38✔
285
    database: &DatabaseRef,
38✔
286
    obj_name: &str,
38✔
287
    local_version: u64,
38✔
288
) -> Result<ObjectChange, ObjectsTreeError> {
38✔
289
    let mut object_change = ObjectChange::new(obj_name.to_string());
38✔
290

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

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

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

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

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

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

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

369
    Ok(object_change)
38✔
370
}
38✔
371

372
/// Compare two ObjectDefinitions and populate the ObjectChange with detailed differences
373
/// If database and obj_name are provided, ignored properties/verbs from meta are excluded from deleted lists
374
#[allow(dead_code)]
UNCOV
375
pub fn compare_object_definitions(
×
UNCOV
376
    baseline: &ObjectDefinition,
×
UNCOV
377
    local: &ObjectDefinition,
×
UNCOV
378
    object_change: &mut ObjectChange,
×
UNCOV
379
) {
×
UNCOV
380
    compare_object_definitions_with_meta(baseline, local, object_change, None, None);
×
381
}
×
382

383
/// Compare two ObjectDefinitions with optional meta filtering
384
pub fn compare_object_definitions_with_meta(
13✔
385
    baseline: &ObjectDefinition,
13✔
386
    local: &ObjectDefinition,
13✔
387
    object_change: &mut ObjectChange,
13✔
388
    database: Option<&DatabaseRef>,
13✔
389
    obj_name: Option<&str>,
13✔
390
) {
13✔
391
    // Load meta if database and obj_name are provided
392
    let meta = if let (Some(db), Some(name)) = (database, obj_name) {
13✔
393
        match db.refs().get_ref(VcsObjectType::MooMetaObject, name, None) {
13✔
394
            Ok(Some(meta_sha256)) => match db.objects().get(&meta_sha256) {
5✔
395
                Ok(Some(yaml)) => db.objects().parse_meta_dump(&yaml).ok(),
5✔
UNCOV
396
                _ => None,
×
397
            },
398
            _ => None,
8✔
399
        }
400
    } else {
UNCOV
401
        None
×
402
    };
403
    // Compare verbs
404
    let baseline_verbs: HashMap<String, &moor_compiler::ObjVerbDef> = baseline
13✔
405
        .verbs
13✔
406
        .iter()
13✔
407
        .flat_map(|v| v.names.iter().map(move |name| (name.as_string(), v)))
15✔
408
        .collect();
13✔
409

410
    let local_verbs: HashMap<String, &moor_compiler::ObjVerbDef> = local
13✔
411
        .verbs
13✔
412
        .iter()
13✔
413
        .flat_map(|v| v.names.iter().map(move |name| (name.as_string(), v)))
13✔
414
        .collect();
13✔
415

416
    // Find added, modified, and deleted verbs
417
    for (verb_name, local_verb) in &local_verbs {
26✔
418
        if let Some(baseline_verb) = baseline_verbs.get(verb_name) {
13✔
419
            // Verb exists in both - check if it's modified
420
            if verbs_differ(baseline_verb, local_verb) {
10✔
421
                object_change.verbs_modified.insert(verb_name.clone());
1✔
422
            }
9✔
423
        } else {
3✔
424
            // Verb is new
3✔
425
            object_change.verbs_added.insert(verb_name.clone());
3✔
426
        }
3✔
427
    }
428

429
    for verb_name in baseline_verbs.keys() {
15✔
430
        if !local_verbs.contains_key(verb_name) {
15✔
431
            // Verb is missing - check if it's ignored before marking as deleted
432
            let is_ignored = meta
5✔
433
                .as_ref()
5✔
434
                .map(|m| m.ignored_verbs.contains(verb_name))
5✔
435
                .unwrap_or(false);
5✔
436

437
            if !is_ignored {
5✔
438
                // Verb was actually deleted (not just ignored)
3✔
439
                object_change.verbs_deleted.insert(verb_name.clone());
3✔
440
            } else {
3✔
441
                tracing::debug!(
2✔
UNCOV
442
                    "Verb '{}' is missing but ignored in meta, not marking as deleted",
×
443
                    verb_name
444
                );
445
            }
446
        }
10✔
447
    }
448

449
    // Detect verb renames: if a verb was deleted and another was added with the same content,
450
    // it's likely a rename rather than a delete+add
451
    detect_verb_renames(&baseline_verbs, &local_verbs, object_change);
13✔
452

453
    // Compare property definitions
454
    let baseline_props: HashMap<String, &moor_compiler::ObjPropDef> = baseline
13✔
455
        .property_definitions
13✔
456
        .iter()
13✔
457
        .map(|p| (p.name.as_string(), p))
13✔
458
        .collect();
13✔
459

460
    let local_props: HashMap<String, &moor_compiler::ObjPropDef> = local
13✔
461
        .property_definitions
13✔
462
        .iter()
13✔
463
        .map(|p| (p.name.as_string(), p))
13✔
464
        .collect();
13✔
465

466
    // Find added, modified, and deleted property definitions
467
    for (prop_name, local_prop) in &local_props {
22✔
468
        if let Some(baseline_prop) = baseline_props.get(prop_name) {
9✔
469
            // Property exists in both - check if it's modified
470
            if property_definitions_differ(baseline_prop, local_prop) {
6✔
UNCOV
471
                object_change.props_modified.insert(prop_name.clone());
×
472
            }
6✔
473
        } else {
3✔
474
            // Property is new
3✔
475
            object_change.props_added.insert(prop_name.clone());
3✔
476
        }
3✔
477
    }
478

479
    for prop_name in baseline_props.keys() {
13✔
480
        if !local_props.contains_key(prop_name) {
13✔
481
            // Property is missing - check if it's ignored before marking as deleted
482
            let is_ignored = meta
7✔
483
                .as_ref()
7✔
484
                .map(|m| m.ignored_properties.contains(prop_name))
7✔
485
                .unwrap_or(false);
7✔
486

487
            if !is_ignored {
7✔
488
                // Property was actually deleted (not just ignored)
4✔
489
                object_change.props_deleted.insert(prop_name.clone());
4✔
490
            } else {
4✔
491
                tracing::debug!(
3✔
UNCOV
492
                    "Property '{}' is missing but ignored in meta, not marking as deleted",
×
493
                    prop_name
494
                );
495
            }
496
        }
6✔
497
    }
498

499
    // Compare property overrides
500
    let baseline_overrides: HashMap<String, &moor_compiler::ObjPropOverride> = baseline
13✔
501
        .property_overrides
13✔
502
        .iter()
13✔
503
        .map(|p| (p.name.as_string(), p))
13✔
504
        .collect();
13✔
505

506
    let local_overrides: HashMap<String, &moor_compiler::ObjPropOverride> = local
13✔
507
        .property_overrides
13✔
508
        .iter()
13✔
509
        .map(|p| (p.name.as_string(), p))
13✔
510
        .collect();
13✔
511

512
    // Find added, modified, and deleted property overrides
513
    for (prop_name, local_override) in &local_overrides {
13✔
514
        if let Some(baseline_override) = baseline_overrides.get(prop_name) {
×
515
            // Override exists in both - check if it's modified
516
            if property_overrides_differ(baseline_override, local_override) {
×
517
                object_change.props_modified.insert(prop_name.clone());
×
518
            }
×
519
        } else {
×
UNCOV
520
            // Override is new
×
UNCOV
521
            object_change.props_added.insert(prop_name.clone());
×
UNCOV
522
        }
×
523
    }
524

525
    for prop_name in baseline_overrides.keys() {
13✔
UNCOV
526
        if !local_overrides.contains_key(prop_name) {
×
527
            // Override is missing - check if it's ignored before marking as deleted
UNCOV
528
            let is_ignored = meta
×
UNCOV
529
                .as_ref()
×
UNCOV
530
                .map(|m| m.ignored_properties.contains(prop_name))
×
UNCOV
531
                .unwrap_or(false);
×
532

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

545
    // Detect property renames: if a property was deleted and another was added with the same content,
546
    // it's likely a rename rather than a delete+add
547
    detect_property_renames(&baseline_props, &local_props, object_change);
13✔
548
    detect_property_override_renames(&baseline_overrides, &local_overrides, object_change);
13✔
549
}
13✔
550

551
/// Detect verb renames by finding deleted verbs that match added verbs in content
552
/// Also detects renames when verb names have overlapping aliases (space-delimited)
553
fn detect_verb_renames(
13✔
554
    baseline_verbs: &HashMap<String, &moor_compiler::ObjVerbDef>,
13✔
555
    local_verbs: &HashMap<String, &moor_compiler::ObjVerbDef>,
13✔
556
    object_change: &mut ObjectChange,
13✔
557
) {
13✔
558
    // Find potential renames: for each deleted verb, check if there's a matching added verb
559
    let mut renames_to_apply = Vec::new();
13✔
560

561
    for deleted_name in &object_change.verbs_deleted.clone() {
13✔
562
        if let Some(baseline_verb) = baseline_verbs.get(deleted_name) {
3✔
563
            // Check if there's an added verb with the same content
564
            for added_name in &object_change.verbs_added.clone() {
3✔
565
                if let Some(local_verb) = local_verbs.get(added_name) {
3✔
566
                    // Check if the verbs have the same content (everything except the name)
567
                    if !verbs_differ(baseline_verb, local_verb) {
3✔
568
                        // Check if the names have any overlapping elements (e.g. "look examine" vs "look inspect")
569
                        // This helps confirm it's a rename vs coincidental identical code
570
                        let has_overlap = verb_names_overlap(deleted_name, added_name);
3✔
571
                        
572
                        // Accept the rename if:
573
                        // 1. Names have overlapping aliases, OR
574
                        // 2. Names are similar enough (could add Levenshtein distance check here)
575
                        // For now, we'll accept any exact content match as a rename
576
                        if has_overlap || true {
3✔
577
                            // This is a rename!
578
                            renames_to_apply.push((deleted_name.clone(), added_name.clone()));
3✔
579
                            break; // Each deleted verb can only match one added verb
3✔
UNCOV
580
                        }
×
UNCOV
581
                    }
×
UNCOV
582
                }
×
583
            }
UNCOV
584
        }
×
585
    }
586

587
    // Apply the renames: remove from added/deleted, add to renamed
588
    for (old_name, new_name) in renames_to_apply {
16✔
589
        object_change.verbs_deleted.remove(&old_name);
3✔
590
        object_change.verbs_added.remove(&new_name);
3✔
591
        object_change.verbs_renamed.insert(old_name, new_name);
3✔
592
    }
3✔
593
}
13✔
594

595
/// Check if two verb names have any overlapping elements when split by spaces
596
/// For example, "look examine" and "look inspect" overlap on "look"
597
fn verb_names_overlap(name1: &str, name2: &str) -> bool {
3✔
598
    // Split both names by spaces and check for any common elements
599
    let names1: HashSet<&str> = name1.split_whitespace().collect();
3✔
600
    let names2: HashSet<&str> = name2.split_whitespace().collect();
3✔
601
    
602
    // If either name has no elements after splitting, fall back to exact comparison
603
    if names1.is_empty() || names2.is_empty() {
3✔
UNCOV
604
        return name1 == name2;
×
605
    }
3✔
606
    
607
    // Check if there's any intersection
608
    !names1.is_disjoint(&names2)
3✔
609
}
3✔
610

611
/// Detect property definition renames by finding deleted properties that match added properties in content
612
/// Skip rename detection for properties with falsy values (empty strings, lists, maps) to avoid false positives
613
fn detect_property_renames(
13✔
614
    baseline_props: &HashMap<String, &moor_compiler::ObjPropDef>,
13✔
615
    local_props: &HashMap<String, &moor_compiler::ObjPropDef>,
13✔
616
    object_change: &mut ObjectChange,
13✔
617
) {
13✔
618
    let mut renames_to_apply = Vec::new();
13✔
619

620
    for deleted_name in &object_change.props_deleted.clone() {
13✔
621
        if let Some(baseline_prop) = baseline_props.get(deleted_name) {
4✔
622
            // Skip rename detection if the property value is falsy (empty/default)
623
            if let Some(value) = &baseline_prop.value {
4✔
624
                if is_property_value_falsy(value) {
4✔
625
                    continue;
2✔
626
                }
2✔
UNCOV
627
            }
×
628
            
629
            // Check if there's an added property with the same content
630
            for added_name in &object_change.props_added.clone() {
2✔
631
                if let Some(local_prop) = local_props.get(added_name) {
1✔
632
                    // Skip if the new property value is also falsy
633
                    if let Some(value) = &local_prop.value {
1✔
634
                        if is_property_value_falsy(value) {
1✔
UNCOV
635
                            continue;
×
636
                        }
1✔
UNCOV
637
                    }
×
638
                    
639
                    // Check if the properties have the same content (everything except the name)
640
                    if !property_definitions_differ(baseline_prop, local_prop) {
1✔
641
                        // This is a rename!
642
                        renames_to_apply.push((deleted_name.clone(), added_name.clone()));
1✔
643
                        break;
1✔
UNCOV
644
                    }
×
UNCOV
645
                }
×
646
            }
UNCOV
647
        }
×
648
    }
649

650
    // Apply the renames
651
    for (old_name, new_name) in renames_to_apply {
14✔
652
        object_change.props_deleted.remove(&old_name);
1✔
653
        object_change.props_added.remove(&new_name);
1✔
654
        object_change.props_renamed.insert(old_name, new_name);
1✔
655
    }
1✔
656
}
13✔
657

658
/// Detect property override renames by finding deleted overrides that match added overrides in content
659
/// Skip rename detection for properties with falsy values (empty strings, lists, maps) to avoid false positives
660
fn detect_property_override_renames(
13✔
661
    baseline_overrides: &HashMap<String, &moor_compiler::ObjPropOverride>,
13✔
662
    local_overrides: &HashMap<String, &moor_compiler::ObjPropOverride>,
13✔
663
    object_change: &mut ObjectChange,
13✔
664
) {
13✔
665
    let mut renames_to_apply = Vec::new();
13✔
666

667
    for deleted_name in &object_change.props_deleted.clone() {
13✔
668
        if let Some(baseline_override) = baseline_overrides.get(deleted_name) {
3✔
669
            // Skip rename detection if the property value is falsy (empty/default)
UNCOV
670
            if let Some(value) = &baseline_override.value {
×
UNCOV
671
                if is_property_value_falsy(value) {
×
UNCOV
672
                    continue;
×
UNCOV
673
                }
×
UNCOV
674
            }
×
675
            
676
            // Check if there's an added property override with the same content
UNCOV
677
            for added_name in &object_change.props_added.clone() {
×
UNCOV
678
                if let Some(local_override) = local_overrides.get(added_name) {
×
679
                    // Skip if the new property value is also falsy
UNCOV
680
                    if let Some(value) = &local_override.value {
×
681
                        if is_property_value_falsy(value) {
×
682
                            continue;
×
683
                        }
×
UNCOV
684
                    }
×
685
                    
686
                    // Check if the property overrides have the same content (everything except the name)
UNCOV
687
                    if !property_overrides_differ(baseline_override, local_override) {
×
688
                        // This is a rename!
UNCOV
689
                        renames_to_apply.push((deleted_name.clone(), added_name.clone()));
×
690
                        break;
×
691
                    }
×
692
                }
×
693
            }
694
        }
3✔
695
    }
696

697
    // Apply the renames
698
    for (old_name, new_name) in renames_to_apply {
13✔
699
        object_change.props_deleted.remove(&old_name);
×
UNCOV
700
        object_change.props_added.remove(&new_name);
×
701
        object_change.props_renamed.insert(old_name, new_name);
×
UNCOV
702
    }
×
703
}
13✔
704

705
/// Check if a property value is "falsy" (empty/default) to avoid false positives in rename detection
706
fn is_property_value_falsy(value: &moor_var::Var) -> bool {
5✔
707
    use moor_var::Variant;
708
    
709
    match value.variant() {
5✔
710
        Variant::Str(s) => s.is_empty(),
4✔
711
        Variant::List(l) => l.is_empty(),
×
712
        Variant::Map(m) => m.is_empty(),
×
713
        Variant::Int(i) => *i == 0,
1✔
UNCOV
714
        Variant::Float(f) => *f == 0.0,
×
715
        Variant::None => true,
×
UNCOV
716
        _ => false,
×
717
    }
718
}
5✔
719

720
/// Check if two verb definitions differ
721
pub fn verbs_differ(
13✔
722
    baseline: &moor_compiler::ObjVerbDef,
13✔
723
    local: &moor_compiler::ObjVerbDef,
13✔
724
) -> bool {
13✔
725
    baseline.argspec != local.argspec
13✔
726
        || baseline.owner != local.owner
13✔
727
        || baseline.flags != local.flags
13✔
728
        || baseline.program != local.program
13✔
729
}
13✔
730

731
/// Check if two property definitions differ
732
pub fn property_definitions_differ(
7✔
733
    baseline: &moor_compiler::ObjPropDef,
7✔
734
    local: &moor_compiler::ObjPropDef,
7✔
735
) -> bool {
7✔
736
    baseline.perms != local.perms || baseline.value != local.value
7✔
737
}
7✔
738

739
/// Check if two property overrides differ
UNCOV
740
pub fn property_overrides_differ(
×
UNCOV
741
    baseline: &moor_compiler::ObjPropOverride,
×
UNCOV
742
    local: &moor_compiler::ObjPropOverride,
×
UNCOV
743
) -> bool {
×
UNCOV
744
    baseline.value != local.value || baseline.perms_update != local.perms_update
×
UNCOV
745
}
×
746

747
/// Build an ObjectDiffModel by comparing a change against the compiled state
748
/// This is the shared logic used by approve and status operations
749
pub fn build_object_diff_from_change(
28✔
750
    database: &DatabaseRef,
28✔
751
    change: &Change,
28✔
752
) -> Result<ObjectDiffModel, ObjectsTreeError> {
28✔
753
    let mut diff_model = ObjectDiffModel::new();
28✔
754

755
    // Get the complete object list from the index state (excluding the local change)
756
    let complete_object_list = database
28✔
757
        .index()
28✔
758
        .compute_complete_object_list()
28✔
759
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?;
28✔
760

761
    tracing::info!(
28✔
UNCOV
762
        "Using complete object list with {} objects as baseline for change '{}'",
×
UNCOV
763
        complete_object_list.len(),
×
764
        change.name
765
    );
766

767
    // Process the change to build the diff
768
    process_change_for_diff(database, &mut diff_model, change)?;
28✔
769

770
    Ok(diff_model)
28✔
771
}
28✔
772

773
/// Process a single change and add its modifications to the diff model
774
/// This is the shared logic used by approve and status operations
775
pub fn process_change_for_diff(
28✔
776
    database: &DatabaseRef,
28✔
777
    diff_model: &mut ObjectDiffModel,
28✔
778
    change: &Change,
28✔
779
) -> Result<(), ObjectsTreeError> {
28✔
780
    // Process added objects (filter to only MooObject types)
781
    for obj_info in change
28✔
782
        .added_objects
28✔
783
        .iter()
28✔
784
        .filter(|o| o.object_type == VcsObjectType::MooObject)
28✔
785
    {
786
        let obj_name = obj_id_to_object_name(&obj_info.name, Some(&obj_info.name));
20✔
787
        diff_model.add_object_added(obj_name.clone());
20✔
788

789
        // Get detailed object changes by comparing local vs baseline (which will be empty for new objects)
790
        let object_change = compare_object_versions(database, &obj_name, obj_info.version)?;
20✔
791
        diff_model.add_object_change(object_change);
20✔
792
    }
793

794
    // Process deleted objects (filter to only MooObject types)
795
    for obj_info in change
28✔
796
        .deleted_objects
28✔
797
        .iter()
28✔
798
        .filter(|o| o.object_type == VcsObjectType::MooObject)
28✔
UNCOV
799
    {
×
UNCOV
800
        let obj_name = obj_id_to_object_name(&obj_info.name, Some(&obj_info.name));
×
UNCOV
801
        diff_model.add_object_deleted(obj_name);
×
UNCOV
802
    }
×
803

804
    // Process renamed objects (filter to only MooObject types)
805
    for renamed in change.renamed_objects.iter().filter(|r| {
28✔
UNCOV
806
        r.from.object_type == VcsObjectType::MooObject
×
UNCOV
807
            && r.to.object_type == VcsObjectType::MooObject
×
UNCOV
808
    }) {
×
UNCOV
809
        let from_name = obj_id_to_object_name(&renamed.from.name, Some(&renamed.from.name));
×
UNCOV
810
        let to_name = obj_id_to_object_name(&renamed.to.name, Some(&renamed.to.name));
×
UNCOV
811
        diff_model.add_object_renamed(from_name, to_name);
×
UNCOV
812
    }
×
813

814
    // Process modified objects with detailed comparison (filter to only MooObject types)
815
    for obj_info in change
28✔
816
        .modified_objects
28✔
817
        .iter()
28✔
818
        .filter(|o| o.object_type == VcsObjectType::MooObject)
28✔
819
    {
820
        let obj_name = obj_id_to_object_name(&obj_info.name, Some(&obj_info.name));
10✔
821
        diff_model.add_object_modified(obj_name.clone());
10✔
822

823
        // Get detailed object changes by comparing local vs baseline
824
        let object_change = compare_object_versions(database, &obj_name, obj_info.version)?;
10✔
825
        diff_model.add_object_change(object_change);
10✔
826
    }
827

828
    Ok(())
28✔
829
}
28✔
830

831
/// Build an ObjectDiffModel for abandoning a change (undo operations)
832
/// This creates the reverse operations needed to undo the change
833
pub fn build_abandon_diff_from_change(
27✔
834
    database: &DatabaseRef,
27✔
835
    change: &Change,
27✔
836
) -> Result<ObjectDiffModel, ObjectsTreeError> {
27✔
837
    // Get the complete object list from the index state for comparison
838
    let complete_object_list = database
27✔
839
        .index()
27✔
840
        .compute_complete_object_list()
27✔
841
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?;
27✔
842

843
    tracing::info!(
27✔
UNCOV
844
        "Using complete object list with {} objects as baseline for abandoning change '{}'",
×
UNCOV
845
        complete_object_list.len(),
×
846
        change.name
847
    );
848

849
    // Create a delta model showing what needs to be undone
850
    let mut undo_delta = ObjectDiffModel::new();
27✔
851

852
    // Get object name mappings for better display names
853
    let object_names = get_object_names_for_change(change);
27✔
854

855
    // Process added objects - to undo, we need to delete them (filter to only MooObject types)
856
    for added_obj in change
27✔
857
        .added_objects
27✔
858
        .iter()
27✔
859
        .filter(|o| o.object_type == VcsObjectType::MooObject)
27✔
860
    {
861
        let object_name = obj_id_to_object_name(
26✔
862
            &added_obj.name,
26✔
863
            object_names.get(&added_obj.name).map(|s| s.as_str()),
26✔
864
        );
865
        undo_delta.add_object_deleted(object_name);
26✔
866
    }
867

868
    // Process deleted objects - to undo, we need to add them back (filter to only MooObject types)
869
    for deleted_obj in change
27✔
870
        .deleted_objects
27✔
871
        .iter()
27✔
872
        .filter(|o| o.object_type == VcsObjectType::MooObject)
27✔
873
    {
UNCOV
874
        let object_name = obj_id_to_object_name(
×
UNCOV
875
            &deleted_obj.name,
×
UNCOV
876
            object_names.get(&deleted_obj.name).map(|s| s.as_str()),
×
877
        );
UNCOV
878
        undo_delta.add_object_added(object_name);
×
879
    }
880

881
    // Process renamed objects - to undo, we need to rename them back (filter to only MooObject types)
882
    for renamed in change.renamed_objects.iter().filter(|r| {
27✔
UNCOV
883
        r.from.object_type == VcsObjectType::MooObject
×
UNCOV
884
            && r.to.object_type == VcsObjectType::MooObject
×
UNCOV
885
    }) {
×
UNCOV
886
        let from_name = obj_id_to_object_name(
×
UNCOV
887
            &renamed.from.name,
×
UNCOV
888
            object_names.get(&renamed.from.name).map(|s| s.as_str()),
×
889
        );
UNCOV
890
        let to_name = obj_id_to_object_name(
×
UNCOV
891
            &renamed.to.name,
×
UNCOV
892
            object_names.get(&renamed.to.name).map(|s| s.as_str()),
×
893
        );
UNCOV
894
        undo_delta.add_object_renamed(to_name, from_name);
×
895
    }
896

897
    // Process modified objects - to undo, we need to mark them as modified
898
    // and create basic ObjectChange entries (filter to only MooObject types)
899
    for modified_obj in change
27✔
900
        .modified_objects
27✔
901
        .iter()
27✔
902
        .filter(|o| o.object_type == VcsObjectType::MooObject)
27✔
903
    {
UNCOV
904
        let object_name = obj_id_to_object_name(
×
UNCOV
905
            &modified_obj.name,
×
UNCOV
906
            object_names.get(&modified_obj.name).map(|s| s.as_str()),
×
907
        );
UNCOV
908
        undo_delta.add_object_modified(object_name.clone());
×
909

910
        // Create a basic ObjectChange for modified objects
911
        // In a real implementation, you'd want to track what specifically changed
UNCOV
912
        let mut object_change = ObjectChange::new(object_name);
×
UNCOV
913
        object_change.props_modified.insert("content".to_string());
×
UNCOV
914
        undo_delta.add_object_change(object_change);
×
915
    }
916

917
    Ok(undo_delta)
27✔
918
}
27✔
919

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

926
    // Try to get object names from workspace provider (filter to only MooObject types)
927
    for obj_info in change
27✔
928
        .added_objects
27✔
929
        .iter()
27✔
930
        .chain(change.modified_objects.iter())
27✔
931
        .chain(change.deleted_objects.iter())
27✔
932
        .filter(|o| o.object_type == VcsObjectType::MooObject)
27✔
933
    {
26✔
934
        // For now, we'll just use the object name as the name
26✔
935
        // In a real implementation, you'd query the actual object names
26✔
936
        object_names.insert(obj_info.name.clone(), obj_info.name.clone());
26✔
937
    }
26✔
938

939
    for renamed in change.renamed_objects.iter().filter(|r| {
27✔
UNCOV
940
        r.from.object_type == VcsObjectType::MooObject
×
UNCOV
941
            && r.to.object_type == VcsObjectType::MooObject
×
UNCOV
942
    }) {
×
UNCOV
943
        object_names.insert(renamed.from.name.clone(), renamed.from.name.clone());
×
UNCOV
944
        object_names.insert(renamed.to.name.clone(), renamed.to.name.clone());
×
UNCOV
945
    }
×
946

947
    object_names
27✔
948
}
27✔
949

950
#[cfg(test)]
951
mod tests {
952
    use super::*;
953

954
    #[test]
955
    fn test_object_change_to_moo_var() {
2✔
956
        let mut change = ObjectChange::new("TestObject".to_string());
2✔
957
        change.verbs_added.insert("new_verb".to_string());
2✔
958
        change.props_modified.insert("existing_prop".to_string());
2✔
959

960
        let moo_var = change.to_moo_var();
2✔
961

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

966
    #[test]
967
    fn test_object_diff_model_to_moo_var() {
2✔
968
        let mut model = ObjectDiffModel::new();
2✔
969
        model.add_object_added("NewObject".to_string());
2✔
970
        model.add_object_deleted("OldObject".to_string());
2✔
971

972
        let moo_var = model.to_moo_var();
2✔
973

974
        // Verify it's a map
975
        assert!(matches!(moo_var.variant(), moor_var::Variant::Map(_)));
2✔
976
    }
2✔
977

978
    #[test]
979
    fn test_obj_id_to_object_name() {
2✔
980
        assert_eq!(obj_id_to_object_name("#4", Some("foobar")), "Foobar");
2✔
981
        assert_eq!(obj_id_to_object_name("#4", Some("#4")), "#4");
2✔
982
        assert_eq!(obj_id_to_object_name("#4", None), "#4");
2✔
983
        assert_eq!(
2✔
984
            obj_id_to_object_name("TestObject", Some("TestObject")),
2✔
985
            "TestObject"
986
        );
987
    }
2✔
988

989
    #[test]
990
    fn test_merge_object_diff_models() {
2✔
991
        let mut model1 = ObjectDiffModel::new();
2✔
992
        model1.add_object_added("Object1".to_string());
2✔
993

994
        let mut model2 = ObjectDiffModel::new();
2✔
995
        model2.add_object_added("Object2".to_string());
2✔
996
        model2.add_object_deleted("Object3".to_string());
2✔
997

998
        model1.merge(model2);
2✔
999

1000
        assert!(model1.objects_added.contains("Object1"));
2✔
1001
        assert!(model1.objects_added.contains("Object2"));
2✔
1002
        assert!(model1.objects_deleted.contains("Object3"));
2✔
1003
    }
2✔
1004
}
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