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

biscuitWizard / moor-hackercore / 18436414552

12 Oct 2025 12:05AM UTC coverage: 78.951% (-4.1%) from 83.054%
18436414552

push

github

biscuitWizard
Formatted repository with cargo fmt.

2204 of 3071 new or added lines in 48 files covered. (71.77%)

32 existing lines in 12 files now uncovered.

6324 of 8010 relevant lines covered (78.95%)

253.98 hits per line

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

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

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

70
        // obj_id
71
        pairs.push((v_str("obj_id"), v_str(&self.obj_id)));
17✔
72

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

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

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

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

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

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

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

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

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

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

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

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

153
        // objects_deleted
154
        let objects_deleted_list: Vec<Var> =
114✔
155
            self.objects_deleted.iter().map(|v| v_str(v)).collect();
114✔
156
        pairs.push((
114✔
157
            v_str("objects_deleted"),
114✔
158
            moor_var::v_list(&objects_deleted_list),
114✔
159
        ));
114✔
160

161
        // objects_added
162
        let objects_added_list: Vec<Var> = self.objects_added.iter().map(|v| v_str(v)).collect();
114✔
163
        pairs.push((
114✔
164
            v_str("objects_added"),
114✔
165
            moor_var::v_list(&objects_added_list),
114✔
166
        ));
114✔
167

168
        // objects_modified
169
        let objects_modified_list: Vec<Var> =
114✔
170
            self.objects_modified.iter().map(|v| v_str(v)).collect();
114✔
171
        pairs.push((
114✔
172
            v_str("objects_modified"),
114✔
173
            moor_var::v_list(&objects_modified_list),
114✔
174
        ));
114✔
175

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

184
        v_map(&pairs)
114✔
185
    }
114✔
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) {
19✔
199
        self.objects_added.insert(obj_id);
19✔
200
    }
19✔
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) {
28✔
209
        // Remove any existing change for this object
210
        self.changes.retain(|c| c.obj_id != change.obj_id);
28✔
211
        self.changes.push(change);
28✔
212
    }
28✔
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 to an object name
250
/// Returns the object name if it's different from the OID, otherwise returns the OID
251
pub fn obj_id_to_object_name(obj_id: &str, object_name: Option<&str>) -> String {
52✔
252
    match object_name {
50✔
253
        Some(name) if name != obj_id => {
50✔
254
            // Capitalize first letter if it's a name
255
            if let Some(first_char) = name.chars().next() {
2✔
256
                let mut result = String::with_capacity(name.len());
2✔
257
                result.push(first_char.to_uppercase().next().unwrap_or(first_char));
2✔
258
                result.push_str(&name[1..]);
2✔
259
                result
2✔
260
            } else {
261
                name.to_string()
×
262
            }
263
        }
264
        _ => obj_id.to_string(),
50✔
265
    }
266
}
52✔
267

268
/// Compare object versions to determine detailed changes
269
pub fn compare_object_versions(
18✔
270
    database: &DatabaseRef,
18✔
271
    obj_name: &str,
18✔
272
    local_version: u64,
18✔
273
) -> Result<ObjectChange, ObjectsTreeError> {
18✔
274
    let mut object_change = ObjectChange::new(obj_name.to_string());
18✔
275

276
    // Get the local version content
277
    let local_sha256 = database
18✔
278
        .refs()
18✔
279
        .get_ref(VcsObjectType::MooObject, obj_name, Some(local_version))
18✔
280
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?
18✔
281
        .ok_or_else(|| {
18✔
NEW
282
            ObjectsTreeError::SerializationError(format!(
×
NEW
283
                "Local version {local_version} of object '{obj_name}' not found"
×
NEW
284
            ))
×
NEW
285
        })?;
×
286

287
    let local_content = database
18✔
288
        .objects()
18✔
289
        .get(&local_sha256)
18✔
290
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?
18✔
291
        .ok_or_else(|| {
18✔
NEW
292
            ObjectsTreeError::SerializationError(format!(
×
NEW
293
                "Object content for SHA256 '{local_sha256}' not found"
×
NEW
294
            ))
×
NEW
295
        })?;
×
296

297
    // Parse local object definition
298
    let local_def = database
18✔
299
        .objects()
18✔
300
        .parse_object_dump(&local_content)
18✔
301
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?;
18✔
302

303
    // Get the baseline version (previous version)
304
    // For version 1 (new object), there is no baseline (version 0 doesn't exist)
305
    // For version 2+, the baseline is the previous version
306
    let baseline_version = local_version.saturating_sub(1);
18✔
307
    let baseline_sha256 = database
18✔
308
        .refs()
18✔
309
        .get_ref(VcsObjectType::MooObject, obj_name, Some(baseline_version))
18✔
310
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?;
18✔
311

312
    if let Some(baseline_sha256) = baseline_sha256 {
18✔
313
        // Get baseline content and parse it
314
        let baseline_content = database
5✔
315
            .objects()
5✔
316
            .get(&baseline_sha256)
5✔
317
            .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?
5✔
318
            .ok_or_else(|| {
5✔
NEW
319
                ObjectsTreeError::SerializationError(format!(
×
NEW
320
                    "Baseline object content for SHA256 '{baseline_sha256}' not found"
×
NEW
321
                ))
×
NEW
322
            })?;
×
323

324
        let baseline_def = database
5✔
325
            .objects()
5✔
326
            .parse_object_dump(&baseline_content)
5✔
327
            .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?;
5✔
328

329
        // Compare the two object definitions with meta filtering
330
        compare_object_definitions_with_meta(
5✔
331
            &baseline_def,
5✔
332
            &local_def,
5✔
333
            &mut object_change,
5✔
334
            Some(database),
5✔
335
            Some(obj_name),
5✔
336
        );
337
    } else {
338
        // No baseline version - this is a new object, mark all as added
339
        for verb in &local_def.verbs {
13✔
340
            for verb_name in &verb.names {
×
341
                object_change.verbs_added.insert(verb_name.as_string());
×
342
            }
×
343
        }
344
        for prop_def in &local_def.property_definitions {
13✔
345
            object_change.props_added.insert(prop_def.name.as_string());
×
346
        }
×
347
        for prop_override in &local_def.property_overrides {
13✔
NEW
348
            object_change
×
NEW
349
                .props_added
×
NEW
350
                .insert(prop_override.name.as_string());
×
UNCOV
351
        }
×
352
    }
353

354
    Ok(object_change)
18✔
355
}
18✔
356

357
/// Compare two ObjectDefinitions and populate the ObjectChange with detailed differences
358
/// If database and obj_name are provided, ignored properties/verbs from meta are excluded from deleted lists
359
#[allow(dead_code)]
NEW
360
pub fn compare_object_definitions(
×
NEW
361
    baseline: &ObjectDefinition,
×
NEW
362
    local: &ObjectDefinition,
×
NEW
363
    object_change: &mut ObjectChange,
×
NEW
364
) {
×
365
    compare_object_definitions_with_meta(baseline, local, object_change, None, None);
×
366
}
×
367

368
/// Compare two ObjectDefinitions with optional meta filtering
369
pub fn compare_object_definitions_with_meta(
5✔
370
    baseline: &ObjectDefinition,
5✔
371
    local: &ObjectDefinition,
5✔
372
    object_change: &mut ObjectChange,
5✔
373
    database: Option<&DatabaseRef>,
5✔
374
    obj_name: Option<&str>,
5✔
375
) {
5✔
376
    // Load meta if database and obj_name are provided
377
    let meta = if let (Some(db), Some(name)) = (database, obj_name) {
5✔
378
        match db.refs().get_ref(VcsObjectType::MooMetaObject, name, None) {
5✔
379
            Ok(Some(meta_sha256)) => match db.objects().get(&meta_sha256) {
5✔
380
                Ok(Some(yaml)) => db.objects().parse_meta_dump(&yaml).ok(),
5✔
NEW
381
                _ => None,
×
382
            },
UNCOV
383
            _ => None,
×
384
        }
385
    } else {
386
        None
×
387
    };
388
    // Compare verbs
389
    let baseline_verbs: HashMap<String, &moor_compiler::ObjVerbDef> = baseline
5✔
390
        .verbs
5✔
391
        .iter()
5✔
392
        .flat_map(|v| v.names.iter().map(move |name| (name.as_string(), v)))
10✔
393
        .collect();
5✔
394

395
    let local_verbs: HashMap<String, &moor_compiler::ObjVerbDef> = local
5✔
396
        .verbs
5✔
397
        .iter()
5✔
398
        .flat_map(|v| v.names.iter().map(move |name| (name.as_string(), v)))
8✔
399
        .collect();
5✔
400

401
    // Find added, modified, and deleted verbs
402
    for (verb_name, local_verb) in &local_verbs {
13✔
403
        if let Some(baseline_verb) = baseline_verbs.get(verb_name) {
8✔
404
            // Verb exists in both - check if it's modified
405
            if verbs_differ(baseline_verb, local_verb) {
8✔
406
                object_change.verbs_modified.insert(verb_name.clone());
×
407
            }
8✔
408
        } else {
×
409
            // Verb is new
×
410
            object_change.verbs_added.insert(verb_name.clone());
×
411
        }
×
412
    }
413

414
    for verb_name in baseline_verbs.keys() {
10✔
415
        if !local_verbs.contains_key(verb_name) {
10✔
416
            // Verb is missing - check if it's ignored before marking as deleted
417
            let is_ignored = meta
2✔
418
                .as_ref()
2✔
419
                .map(|m| m.ignored_verbs.contains(verb_name))
2✔
420
                .unwrap_or(false);
2✔
421

422
            if !is_ignored {
2✔
423
                // Verb was actually deleted (not just ignored)
×
424
                object_change.verbs_deleted.insert(verb_name.clone());
×
425
            } else {
×
426
                tracing::debug!(
2✔
NEW
427
                    "Verb '{}' is missing but ignored in meta, not marking as deleted",
×
428
                    verb_name
429
                );
430
            }
431
        }
8✔
432
    }
433

434
    // Compare property definitions
435
    let baseline_props: HashMap<String, &moor_compiler::ObjPropDef> = baseline
5✔
436
        .property_definitions
5✔
437
        .iter()
5✔
438
        .map(|p| (p.name.as_string(), p))
10✔
439
        .collect();
5✔
440

441
    let local_props: HashMap<String, &moor_compiler::ObjPropDef> = local
5✔
442
        .property_definitions
5✔
443
        .iter()
5✔
444
        .map(|p| (p.name.as_string(), p))
6✔
445
        .collect();
5✔
446

447
    // Find added, modified, and deleted property definitions
448
    for (prop_name, local_prop) in &local_props {
11✔
449
        if let Some(baseline_prop) = baseline_props.get(prop_name) {
6✔
450
            // Property exists in both - check if it's modified
451
            if property_definitions_differ(baseline_prop, local_prop) {
6✔
452
                object_change.props_modified.insert(prop_name.clone());
×
453
            }
6✔
454
        } else {
×
455
            // Property is new
×
456
            object_change.props_added.insert(prop_name.clone());
×
457
        }
×
458
    }
459

460
    for prop_name in baseline_props.keys() {
10✔
461
        if !local_props.contains_key(prop_name) {
10✔
462
            // Property is missing - check if it's ignored before marking as deleted
463
            let is_ignored = meta
4✔
464
                .as_ref()
4✔
465
                .map(|m| m.ignored_properties.contains(prop_name))
4✔
466
                .unwrap_or(false);
4✔
467

468
            if !is_ignored {
4✔
469
                // Property was actually deleted (not just ignored)
1✔
470
                object_change.props_deleted.insert(prop_name.clone());
1✔
471
            } else {
1✔
472
                tracing::debug!(
3✔
NEW
473
                    "Property '{}' is missing but ignored in meta, not marking as deleted",
×
474
                    prop_name
475
                );
476
            }
477
        }
6✔
478
    }
479

480
    // Compare property overrides
481
    let baseline_overrides: HashMap<String, &moor_compiler::ObjPropOverride> = baseline
5✔
482
        .property_overrides
5✔
483
        .iter()
5✔
484
        .map(|p| (p.name.as_string(), p))
5✔
485
        .collect();
5✔
486

487
    let local_overrides: HashMap<String, &moor_compiler::ObjPropOverride> = local
5✔
488
        .property_overrides
5✔
489
        .iter()
5✔
490
        .map(|p| (p.name.as_string(), p))
5✔
491
        .collect();
5✔
492

493
    // Find added, modified, and deleted property overrides
494
    for (prop_name, local_override) in &local_overrides {
5✔
495
        if let Some(baseline_override) = baseline_overrides.get(prop_name) {
×
496
            // Override exists in both - check if it's modified
497
            if property_overrides_differ(baseline_override, local_override) {
×
498
                object_change.props_modified.insert(prop_name.clone());
×
499
            }
×
500
        } else {
×
501
            // Override is new
×
502
            object_change.props_added.insert(prop_name.clone());
×
503
        }
×
504
    }
505

506
    for prop_name in baseline_overrides.keys() {
5✔
507
        if !local_overrides.contains_key(prop_name) {
×
508
            // Override is missing - check if it's ignored before marking as deleted
NEW
509
            let is_ignored = meta
×
NEW
510
                .as_ref()
×
511
                .map(|m| m.ignored_properties.contains(prop_name))
×
512
                .unwrap_or(false);
×
513

514
            if !is_ignored {
×
515
                // Override was actually deleted (not just ignored)
×
516
                object_change.props_deleted.insert(prop_name.clone());
×
517
            } else {
×
NEW
518
                tracing::debug!(
×
NEW
519
                    "Property override '{}' is missing but ignored in meta, not marking as deleted",
×
520
                    prop_name
521
                );
522
            }
523
        }
×
524
    }
525
}
5✔
526

527
/// Check if two verb definitions differ
528
pub fn verbs_differ(
8✔
529
    baseline: &moor_compiler::ObjVerbDef,
8✔
530
    local: &moor_compiler::ObjVerbDef,
8✔
531
) -> bool {
8✔
532
    baseline.argspec != local.argspec
8✔
533
        || baseline.owner != local.owner
8✔
534
        || baseline.flags != local.flags
8✔
535
        || baseline.program != local.program
8✔
536
}
8✔
537

538
/// Check if two property definitions differ
539
pub fn property_definitions_differ(
6✔
540
    baseline: &moor_compiler::ObjPropDef,
6✔
541
    local: &moor_compiler::ObjPropDef,
6✔
542
) -> bool {
6✔
543
    baseline.perms != local.perms || baseline.value != local.value
6✔
544
}
6✔
545

546
/// Check if two property overrides differ
NEW
547
pub fn property_overrides_differ(
×
NEW
548
    baseline: &moor_compiler::ObjPropOverride,
×
NEW
549
    local: &moor_compiler::ObjPropOverride,
×
NEW
550
) -> bool {
×
NEW
551
    baseline.value != local.value || baseline.perms_update != local.perms_update
×
UNCOV
552
}
×
553

554
/// Build an ObjectDiffModel by comparing a change against the compiled state
555
/// This is the shared logic used by approve and status operations
556
pub fn build_object_diff_from_change(
18✔
557
    database: &DatabaseRef,
18✔
558
    change: &Change,
18✔
559
) -> Result<ObjectDiffModel, ObjectsTreeError> {
18✔
560
    let mut diff_model = ObjectDiffModel::new();
18✔
561

562
    // Get the complete object list from the index state (excluding the local change)
563
    let complete_object_list = database
18✔
564
        .index()
18✔
565
        .compute_complete_object_list()
18✔
566
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?;
18✔
567

568
    tracing::info!(
18✔
NEW
569
        "Using complete object list with {} objects as baseline for change '{}'",
×
NEW
570
        complete_object_list.len(),
×
571
        change.name
572
    );
573

574
    // Process the change to build the diff
575
    process_change_for_diff(database, &mut diff_model, change)?;
18✔
576

577
    Ok(diff_model)
18✔
578
}
18✔
579

580
/// Process a single change and add its modifications to the diff model
581
/// This is the shared logic used by approve and status operations
582
pub fn process_change_for_diff(
18✔
583
    database: &DatabaseRef,
18✔
584
    diff_model: &mut ObjectDiffModel,
18✔
585
    change: &Change,
18✔
586
) -> Result<(), ObjectsTreeError> {
18✔
587
    // Process added objects (filter to only MooObject types)
588
    for obj_info in change
18✔
589
        .added_objects
18✔
590
        .iter()
18✔
591
        .filter(|o| o.object_type == VcsObjectType::MooObject)
18✔
592
    {
593
        let obj_name = obj_id_to_object_name(&obj_info.name, Some(&obj_info.name));
13✔
594
        diff_model.add_object_added(obj_name.clone());
13✔
595

596
        // Get detailed object changes by comparing local vs baseline (which will be empty for new objects)
597
        let object_change = compare_object_versions(database, &obj_name, obj_info.version)?;
13✔
598
        diff_model.add_object_change(object_change);
13✔
599
    }
600

601
    // Process deleted objects (filter to only MooObject types)
602
    for obj_info in change
18✔
603
        .deleted_objects
18✔
604
        .iter()
18✔
605
        .filter(|o| o.object_type == VcsObjectType::MooObject)
18✔
NEW
606
    {
×
607
        let obj_name = obj_id_to_object_name(&obj_info.name, Some(&obj_info.name));
×
608
        diff_model.add_object_deleted(obj_name);
×
609
    }
×
610

611
    // Process renamed objects (filter to only MooObject types)
612
    for renamed in change.renamed_objects.iter().filter(|r| {
18✔
NEW
613
        r.from.object_type == VcsObjectType::MooObject
×
NEW
614
            && r.to.object_type == VcsObjectType::MooObject
×
NEW
615
    }) {
×
616
        let from_name = obj_id_to_object_name(&renamed.from.name, Some(&renamed.from.name));
×
617
        let to_name = obj_id_to_object_name(&renamed.to.name, Some(&renamed.to.name));
×
618
        diff_model.add_object_renamed(from_name, to_name);
×
619
    }
×
620

621
    // Process modified objects with detailed comparison (filter to only MooObject types)
622
    for obj_info in change
18✔
623
        .modified_objects
18✔
624
        .iter()
18✔
625
        .filter(|o| o.object_type == VcsObjectType::MooObject)
18✔
626
    {
627
        let obj_name = obj_id_to_object_name(&obj_info.name, Some(&obj_info.name));
5✔
628
        diff_model.add_object_modified(obj_name.clone());
5✔
629

630
        // Get detailed object changes by comparing local vs baseline
631
        let object_change = compare_object_versions(database, &obj_name, obj_info.version)?;
5✔
632
        diff_model.add_object_change(object_change);
5✔
633
    }
634

635
    Ok(())
18✔
636
}
18✔
637

638
/// Build an ObjectDiffModel for abandoning a change (undo operations)
639
/// This creates the reverse operations needed to undo the change
640
pub fn build_abandon_diff_from_change(
27✔
641
    database: &DatabaseRef,
27✔
642
    change: &Change,
27✔
643
) -> Result<ObjectDiffModel, ObjectsTreeError> {
27✔
644
    // Get the complete object list from the index state for comparison
645
    let complete_object_list = database
27✔
646
        .index()
27✔
647
        .compute_complete_object_list()
27✔
648
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?;
27✔
649

650
    tracing::info!(
27✔
NEW
651
        "Using complete object list with {} objects as baseline for abandoning change '{}'",
×
NEW
652
        complete_object_list.len(),
×
653
        change.name
654
    );
655

656
    // Create a delta model showing what needs to be undone
657
    let mut undo_delta = ObjectDiffModel::new();
27✔
658

659
    // Get object name mappings for better display names
660
    let object_names = get_object_names_for_change(change);
27✔
661

662
    // Process added objects - to undo, we need to delete them (filter to only MooObject types)
663
    for added_obj in change
27✔
664
        .added_objects
27✔
665
        .iter()
27✔
666
        .filter(|o| o.object_type == VcsObjectType::MooObject)
27✔
667
    {
668
        let object_name = obj_id_to_object_name(
26✔
669
            &added_obj.name,
26✔
670
            object_names.get(&added_obj.name).map(|s| s.as_str()),
26✔
671
        );
672
        undo_delta.add_object_deleted(object_name);
26✔
673
    }
674

675
    // Process deleted objects - to undo, we need to add them back (filter to only MooObject types)
676
    for deleted_obj in change
27✔
677
        .deleted_objects
27✔
678
        .iter()
27✔
679
        .filter(|o| o.object_type == VcsObjectType::MooObject)
27✔
680
    {
NEW
681
        let object_name = obj_id_to_object_name(
×
NEW
682
            &deleted_obj.name,
×
NEW
683
            object_names.get(&deleted_obj.name).map(|s| s.as_str()),
×
684
        );
UNCOV
685
        undo_delta.add_object_added(object_name);
×
686
    }
687

688
    // Process renamed objects - to undo, we need to rename them back (filter to only MooObject types)
689
    for renamed in change.renamed_objects.iter().filter(|r| {
27✔
NEW
690
        r.from.object_type == VcsObjectType::MooObject
×
NEW
691
            && r.to.object_type == VcsObjectType::MooObject
×
NEW
692
    }) {
×
NEW
693
        let from_name = obj_id_to_object_name(
×
NEW
694
            &renamed.from.name,
×
NEW
695
            object_names.get(&renamed.from.name).map(|s| s.as_str()),
×
696
        );
NEW
697
        let to_name = obj_id_to_object_name(
×
NEW
698
            &renamed.to.name,
×
NEW
699
            object_names.get(&renamed.to.name).map(|s| s.as_str()),
×
700
        );
UNCOV
701
        undo_delta.add_object_renamed(to_name, from_name);
×
702
    }
703

704
    // Process modified objects - to undo, we need to mark them as modified
705
    // and create basic ObjectChange entries (filter to only MooObject types)
706
    for modified_obj in change
27✔
707
        .modified_objects
27✔
708
        .iter()
27✔
709
        .filter(|o| o.object_type == VcsObjectType::MooObject)
27✔
710
    {
NEW
711
        let object_name = obj_id_to_object_name(
×
NEW
712
            &modified_obj.name,
×
NEW
713
            object_names.get(&modified_obj.name).map(|s| s.as_str()),
×
714
        );
UNCOV
715
        undo_delta.add_object_modified(object_name.clone());
×
716

717
        // Create a basic ObjectChange for modified objects
718
        // In a real implementation, you'd want to track what specifically changed
719
        let mut object_change = ObjectChange::new(object_name);
×
720
        object_change.props_modified.insert("content".to_string());
×
721
        undo_delta.add_object_change(object_change);
×
722
    }
723

724
    Ok(undo_delta)
27✔
725
}
27✔
726

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

733
    // Try to get object names from workspace provider (filter to only MooObject types)
734
    for obj_info in change
27✔
735
        .added_objects
27✔
736
        .iter()
27✔
737
        .chain(change.modified_objects.iter())
27✔
738
        .chain(change.deleted_objects.iter())
27✔
739
        .filter(|o| o.object_type == VcsObjectType::MooObject)
27✔
740
    {
26✔
741
        // For now, we'll just use the object name as the name
26✔
742
        // In a real implementation, you'd query the actual object names
26✔
743
        object_names.insert(obj_info.name.clone(), obj_info.name.clone());
26✔
744
    }
26✔
745

746
    for renamed in change.renamed_objects.iter().filter(|r| {
27✔
NEW
747
        r.from.object_type == VcsObjectType::MooObject
×
NEW
748
            && r.to.object_type == VcsObjectType::MooObject
×
NEW
749
    }) {
×
750
        object_names.insert(renamed.from.name.clone(), renamed.from.name.clone());
×
751
        object_names.insert(renamed.to.name.clone(), renamed.to.name.clone());
×
752
    }
×
753

754
    object_names
27✔
755
}
27✔
756

757
#[cfg(test)]
758
mod tests {
759
    use super::*;
760

761
    #[test]
762
    fn test_object_change_to_moo_var() {
2✔
763
        let mut change = ObjectChange::new("TestObject".to_string());
2✔
764
        change.verbs_added.insert("new_verb".to_string());
2✔
765
        change.props_modified.insert("existing_prop".to_string());
2✔
766

767
        let moo_var = change.to_moo_var();
2✔
768

769
        // Verify it's a map
770
        assert!(matches!(moo_var.variant(), moor_var::Variant::Map(_)));
2✔
771
    }
2✔
772

773
    #[test]
774
    fn test_object_diff_model_to_moo_var() {
2✔
775
        let mut model = ObjectDiffModel::new();
2✔
776
        model.add_object_added("NewObject".to_string());
2✔
777
        model.add_object_deleted("OldObject".to_string());
2✔
778

779
        let moo_var = model.to_moo_var();
2✔
780

781
        // Verify it's a map
782
        assert!(matches!(moo_var.variant(), moor_var::Variant::Map(_)));
2✔
783
    }
2✔
784

785
    #[test]
786
    fn test_obj_id_to_object_name() {
2✔
787
        assert_eq!(obj_id_to_object_name("#4", Some("foobar")), "Foobar");
2✔
788
        assert_eq!(obj_id_to_object_name("#4", Some("#4")), "#4");
2✔
789
        assert_eq!(obj_id_to_object_name("#4", None), "#4");
2✔
790
        assert_eq!(
2✔
791
            obj_id_to_object_name("TestObject", Some("TestObject")),
2✔
792
            "TestObject"
793
        );
794
    }
2✔
795

796
    #[test]
797
    fn test_merge_object_diff_models() {
2✔
798
        let mut model1 = ObjectDiffModel::new();
2✔
799
        model1.add_object_added("Object1".to_string());
2✔
800

801
        let mut model2 = ObjectDiffModel::new();
2✔
802
        model2.add_object_added("Object2".to_string());
2✔
803
        model2.add_object_deleted("Object3".to_string());
2✔
804

805
        model1.merge(model2);
2✔
806

807
        assert!(model1.objects_added.contains("Object1"));
2✔
808
        assert!(model1.objects_added.contains("Object2"));
2✔
809
        assert!(model1.objects_deleted.contains("Object3"));
2✔
810
    }
2✔
811
}
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