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

biscuitWizard / moor-hackercore / 18602197582

17 Oct 2025 07:00PM UTC coverage: 80.772% (+0.2%) from 80.621%
18602197582

push

github

biscuitWizard
Fixing meta only changes

42 of 54 new or added lines in 1 file covered. (77.78%)

155 existing lines in 3 files now uncovered.

8473 of 10490 relevant lines covered (80.77%)

379.62 hits per line

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

82.95
/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
    /// Meta: Properties that became ignored in this change
34
    pub meta_ignored_properties: HashSet<String>,
35
    /// Meta: Verbs that became ignored in this change
36
    pub meta_ignored_verbs: HashSet<String>,
37
    /// Meta: Properties that were unignored in this change
38
    pub meta_unignored_properties: HashSet<String>,
39
    /// Meta: Verbs that were unignored in this change
40
    pub meta_unignored_verbs: HashSet<String>,
41
}
42

43
/// Represents a complete set of object changes/deltas for communication to MOO
44
#[derive(Debug, Clone, Serialize, Deserialize)]
45
pub struct ObjectDiffModel {
46
    /// Objects that were renamed (from_obj_id -> to_obj_id mapping)
47
    pub objects_renamed: HashMap<String, String>,
48
    /// Objects that were deleted
49
    pub objects_deleted: HashSet<String>,
50
    /// Objects that were added
51
    pub objects_added: HashSet<String>,
52
    /// Objects that were modified
53
    pub objects_modified: HashSet<String>,
54
    /// Detailed list of changes for each modified object
55
    pub changes: Vec<ObjectChange>,
56
}
57

58
impl ObjectChange {
59
    /// Create a new empty ObjectChange
60
    pub fn new(obj_id: String) -> Self {
110✔
61
        Self {
110✔
62
            obj_id,
110✔
63
            verbs_modified: HashSet::new(),
110✔
64
            verbs_added: HashSet::new(),
110✔
65
            verbs_renamed: HashMap::new(),
110✔
66
            verbs_deleted: HashSet::new(),
110✔
67
            props_modified: HashSet::new(),
110✔
68
            props_added: HashSet::new(),
110✔
69
            props_renamed: HashMap::new(),
110✔
70
            props_deleted: HashSet::new(),
110✔
71
            meta_ignored_properties: HashSet::new(),
110✔
72
            meta_ignored_verbs: HashSet::new(),
110✔
73
            meta_unignored_properties: HashSet::new(),
110✔
74
            meta_unignored_verbs: HashSet::new(),
110✔
75
        }
110✔
76
    }
110✔
77

78
    /// Invert this ObjectChange to create the reverse operations needed to undo it
79
    /// 
80
    /// This is used when abandoning a change - we need to return the inverse operations
81
    /// so the MOO database can undo the changes. For example:
82
    /// - verbs_added becomes verbs_deleted (to undo an addition, we delete)
83
    /// - verbs_deleted becomes verbs_added (to undo a deletion, we add back)
84
    /// - verbs_renamed is reversed (old->new becomes new->old)
85
    /// - verbs_modified stays the same (modifications need to be reverted)
86
    pub fn invert(&self) -> ObjectChange {
39✔
87
        ObjectChange {
88
            obj_id: self.obj_id.clone(),
39✔
89
            // Modifications are symmetric - still need to modify to revert
90
            verbs_modified: self.verbs_modified.clone(),
39✔
91
            // Swap added ↔ deleted to invert the operation
92
            verbs_added: self.verbs_deleted.clone(),
39✔
93
            verbs_deleted: self.verbs_added.clone(),
39✔
94
            // Reverse rename direction (old->new becomes new->old)
95
            verbs_renamed: self
39✔
96
                .verbs_renamed
39✔
97
                .iter()
39✔
98
                .map(|(k, v)| (v.clone(), k.clone()))
39✔
99
                .collect(),
39✔
100
            // Properties follow the same pattern
101
            props_modified: self.props_modified.clone(),
39✔
102
            props_added: self.props_deleted.clone(),
39✔
103
            props_deleted: self.props_added.clone(),
39✔
104
            props_renamed: self
39✔
105
                .props_renamed
39✔
106
                .iter()
39✔
107
                .map(|(k, v)| (v.clone(), k.clone()))
39✔
108
                .collect(),
39✔
109
            // Meta changes invert: ignored ↔ unignored
110
            meta_ignored_properties: self.meta_unignored_properties.clone(),
39✔
111
            meta_ignored_verbs: self.meta_unignored_verbs.clone(),
39✔
112
            meta_unignored_properties: self.meta_ignored_properties.clone(),
39✔
113
            meta_unignored_verbs: self.meta_ignored_verbs.clone(),
39✔
114
        }
115
    }
39✔
116

117
    /// Convert this ObjectChange to a MOO v_map
118
    pub fn to_moo_var(&self) -> Var {
90✔
119
        let mut pairs = Vec::new();
90✔
120

121
        // obj_id - use helper to convert to v_obj if it's a numeric ID
122
        pairs.push((v_str("obj_id"), object_id_to_var(&self.obj_id)));
90✔
123

124
        // verbs_modified
125
        let verbs_modified_list: Vec<Var> = self.verbs_modified.iter().map(|v| v_str(v)).collect();
90✔
126
        pairs.push((
90✔
127
            v_str("verbs_modified"),
90✔
128
            moor_var::v_list(&verbs_modified_list),
90✔
129
        ));
90✔
130

131
        // verbs_added
132
        let verbs_added_list: Vec<Var> = self.verbs_added.iter().map(|v| v_str(v)).collect();
90✔
133
        pairs.push((v_str("verbs_added"), moor_var::v_list(&verbs_added_list)));
90✔
134

135
        // verbs_renamed
136
        let verbs_renamed_map: Vec<(Var, Var)> = self
90✔
137
            .verbs_renamed
90✔
138
            .iter()
90✔
139
            .map(|(k, v)| (v_str(k), v_str(v)))
90✔
140
            .collect();
90✔
141
        pairs.push((v_str("verbs_renamed"), v_map(&verbs_renamed_map)));
90✔
142

143
        // verbs_deleted
144
        let verbs_deleted_list: Vec<Var> = self.verbs_deleted.iter().map(|v| v_str(v)).collect();
90✔
145
        pairs.push((
90✔
146
            v_str("verbs_deleted"),
90✔
147
            moor_var::v_list(&verbs_deleted_list),
90✔
148
        ));
90✔
149

150
        // props_modified
151
        let props_modified_list: Vec<Var> = self.props_modified.iter().map(|v| v_str(v)).collect();
90✔
152
        pairs.push((
90✔
153
            v_str("props_modified"),
90✔
154
            moor_var::v_list(&props_modified_list),
90✔
155
        ));
90✔
156

157
        // props_added
158
        let props_added_list: Vec<Var> = self.props_added.iter().map(|v| v_str(v)).collect();
90✔
159
        pairs.push((v_str("props_added"), moor_var::v_list(&props_added_list)));
90✔
160

161
        // props_renamed
162
        let props_renamed_map: Vec<(Var, Var)> = self
90✔
163
            .props_renamed
90✔
164
            .iter()
90✔
165
            .map(|(k, v)| (v_str(k), v_str(v)))
90✔
166
            .collect();
90✔
167
        pairs.push((v_str("props_renamed"), v_map(&props_renamed_map)));
90✔
168

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

176
        // Build meta map if any meta changes exist
177
        if !self.meta_ignored_properties.is_empty()
90✔
178
            || !self.meta_ignored_verbs.is_empty()
87✔
179
            || !self.meta_unignored_properties.is_empty()
85✔
180
            || !self.meta_unignored_verbs.is_empty()
85✔
181
        {
182
            let mut meta_pairs = Vec::new();
6✔
183

184
            // meta_ignored_properties
185
            let meta_ignored_props_list: Vec<Var> = self
6✔
186
                .meta_ignored_properties
6✔
187
                .iter()
6✔
188
                .map(|v| v_str(v))
6✔
189
                .collect();
6✔
190
            meta_pairs.push((
6✔
191
                v_str("ignored_properties"),
6✔
192
                moor_var::v_list(&meta_ignored_props_list),
6✔
193
            ));
6✔
194

195
            // meta_ignored_verbs
196
            let meta_ignored_verbs_list: Vec<Var> = self
6✔
197
                .meta_ignored_verbs
6✔
198
                .iter()
6✔
199
                .map(|v| v_str(v))
6✔
200
                .collect();
6✔
201
            meta_pairs.push((
6✔
202
                v_str("ignored_verbs"),
6✔
203
                moor_var::v_list(&meta_ignored_verbs_list),
6✔
204
            ));
6✔
205

206
            // meta_unignored_properties
207
            let meta_unignored_props_list: Vec<Var> = self
6✔
208
                .meta_unignored_properties
6✔
209
                .iter()
6✔
210
                .map(|v| v_str(v))
6✔
211
                .collect();
6✔
212
            meta_pairs.push((
6✔
213
                v_str("unignored_properties"),
6✔
214
                moor_var::v_list(&meta_unignored_props_list),
6✔
215
            ));
6✔
216

217
            // meta_unignored_verbs
218
            let meta_unignored_verbs_list: Vec<Var> = self
6✔
219
                .meta_unignored_verbs
6✔
220
                .iter()
6✔
221
                .map(|v| v_str(v))
6✔
222
                .collect();
6✔
223
            meta_pairs.push((
6✔
224
                v_str("unignored_verbs"),
6✔
225
                moor_var::v_list(&meta_unignored_verbs_list),
6✔
226
            ));
6✔
227

228
            pairs.push((v_str("meta"), v_map(&meta_pairs)));
6✔
229
        }
84✔
230

231
        v_map(&pairs)
90✔
232
    }
90✔
233
}
234

235
impl ObjectDiffModel {
236
    /// Create a new empty ObjectDiffModel
237
    pub fn new() -> Self {
252✔
238
        Self {
252✔
239
            objects_renamed: HashMap::new(),
252✔
240
            objects_deleted: HashSet::new(),
252✔
241
            objects_added: HashSet::new(),
252✔
242
            objects_modified: HashSet::new(),
252✔
243
            changes: Vec::new(),
252✔
244
        }
252✔
245
    }
252✔
246

247
    /// Convert this ObjectDiffModel to a MOO v_map
248
    pub fn to_moo_var(&self) -> Var {
220✔
249
        let mut pairs = Vec::new();
220✔
250

251
        // objects_renamed - use helper to convert object IDs to v_obj if they're numeric
252
        let objects_renamed_map: Vec<(Var, Var)> = self
220✔
253
            .objects_renamed
220✔
254
            .iter()
220✔
255
            .map(|(k, v)| (object_id_to_var(k), object_id_to_var(v)))
220✔
256
            .collect();
220✔
257
        pairs.push((v_str("objects_renamed"), v_map(&objects_renamed_map)));
220✔
258

259
        // objects_deleted - use helper to convert object IDs to v_obj if they're numeric
260
        let objects_deleted_list: Vec<Var> =
220✔
261
            self.objects_deleted.iter().map(|v| object_id_to_var(v)).collect();
220✔
262
        pairs.push((
220✔
263
            v_str("objects_deleted"),
220✔
264
            moor_var::v_list(&objects_deleted_list),
220✔
265
        ));
220✔
266

267
        // objects_added - use helper to convert object IDs to v_obj if they're numeric
268
        let objects_added_list: Vec<Var> = self.objects_added.iter().map(|v| object_id_to_var(v)).collect();
220✔
269
        pairs.push((
220✔
270
            v_str("objects_added"),
220✔
271
            moor_var::v_list(&objects_added_list),
220✔
272
        ));
220✔
273

274
        // objects_modified - use helper to convert object IDs to v_obj if they're numeric
275
        let objects_modified_list: Vec<Var> =
220✔
276
            self.objects_modified.iter().map(|v| object_id_to_var(v)).collect();
220✔
277
        pairs.push((
220✔
278
            v_str("objects_modified"),
220✔
279
            moor_var::v_list(&objects_modified_list),
220✔
280
        ));
220✔
281

282
        // changes
283
        let changes_list: Vec<Var> = self
220✔
284
            .changes
220✔
285
            .iter()
220✔
286
            .map(|change| change.to_moo_var())
220✔
287
            .collect();
220✔
288
        pairs.push((v_str("changes"), moor_var::v_list(&changes_list)));
220✔
289

290
        v_map(&pairs)
220✔
291
    }
220✔
292

293
    /// Add a renamed object to the model
UNCOV
294
    pub fn add_object_renamed(&mut self, from: String, to: String) {
×
UNCOV
295
        self.objects_renamed.insert(from, to);
×
UNCOV
296
    }
×
297

298
    /// Add a deleted object to the model
299
    pub fn add_object_deleted(&mut self, obj_id: String) {
43✔
300
        self.objects_deleted.insert(obj_id);
43✔
301
    }
43✔
302

303
    /// Add an added object to the model
304
    pub fn add_object_added(&mut self, obj_id: String) {
38✔
305
        self.objects_added.insert(obj_id);
38✔
306
    }
38✔
307

308
    /// Add a modified object to the model
309
    pub fn add_object_modified(&mut self, obj_id: String) {
11✔
310
        self.objects_modified.insert(obj_id);
11✔
311
    }
11✔
312

313
    /// Add or update an object change in the model
314
    pub fn add_object_change(&mut self, change: ObjectChange) {
107✔
315
        // Remove any existing change for this object
316
        self.changes.retain(|c| c.obj_id != change.obj_id);
107✔
317
        self.changes.push(change);
107✔
318
    }
107✔
319

320
    /// Merge another ObjectDiffModel into this one
321
    pub fn merge(&mut self, other: ObjectDiffModel) {
22✔
322
        // Merge renamed objects
323
        for (from, to) in other.objects_renamed {
22✔
UNCOV
324
            self.objects_renamed.insert(from, to);
×
UNCOV
325
        }
×
326

327
        // Merge deleted objects
328
        for obj_id in other.objects_deleted {
31✔
329
            self.objects_deleted.insert(obj_id);
9✔
330
        }
9✔
331

332
        // Merge added objects
333
        for obj_id in other.objects_added {
38✔
334
            self.objects_added.insert(obj_id);
16✔
335
        }
16✔
336

337
        // Merge modified objects
338
        for obj_id in other.objects_modified {
23✔
339
            self.objects_modified.insert(obj_id);
1✔
340
        }
1✔
341

342
        // Merge changes
343
        for change in other.changes {
43✔
344
            self.add_object_change(change);
21✔
345
        }
21✔
346
    }
22✔
347
}
348

349
impl Default for ObjectDiffModel {
UNCOV
350
    fn default() -> Self {
×
UNCOV
351
        Self::new()
×
UNCOV
352
    }
×
353
}
354

355
/// Helper function to convert an object ID string to a MOO Var
356
/// If the string is in the format "#<number>", returns a v_obj (object reference)
357
/// Otherwise, returns a v_str (string)
358
pub fn object_id_to_var(obj_id: &str) -> Var {
173✔
359
    // Check if the string starts with '#' and the rest is a valid number
360
    if let Some(stripped) = obj_id.strip_prefix('#') {
173✔
361
        if let Ok(num) = stripped.parse::<i32>() {
10✔
362
            // This is a numeric object ID like "#73", return as v_obj
363
            return v_objid(num);
10✔
UNCOV
364
        }
×
365
    }
163✔
366
    // Otherwise, return as a string
367
    v_str(obj_id)
163✔
368
}
173✔
369

370
/// Helper function to convert an object ID to an object name
371
/// Returns the object name if it's different from the OID, otherwise returns the OID
372
pub fn obj_id_to_object_name(obj_id: &str, object_name: Option<&str>) -> String {
92✔
373
    match object_name {
90✔
374
        Some(name) if name != obj_id => {
90✔
375
            // Capitalize first letter if it's a name
376
            if let Some(first_char) = name.chars().next() {
2✔
377
                let mut result = String::with_capacity(name.len());
2✔
378
                result.push(first_char.to_uppercase().next().unwrap_or(first_char));
2✔
379
                result.push_str(&name[1..]);
2✔
380
                result
2✔
381
            } else {
UNCOV
382
                name.to_string()
×
383
            }
384
        }
385
        _ => obj_id.to_string(),
90✔
386
    }
387
}
92✔
388

389
/// Compare object versions to determine detailed changes
390
pub fn compare_object_versions(
90✔
391
    database: &DatabaseRef,
90✔
392
    obj_name: &str,
90✔
393
    local_version: u64,
90✔
394
    verb_hints: Option<&[crate::types::VerbRenameHint]>,
90✔
395
    prop_hints: Option<&[crate::types::PropertyRenameHint]>,
90✔
396
) -> Result<ObjectChange, ObjectsTreeError> {
90✔
397
    let mut object_change = ObjectChange::new(obj_name.to_string());
90✔
398

399
    // Get the local version content
400
    let local_sha256 = database
90✔
401
        .refs()
90✔
402
        .get_ref(VcsObjectType::MooObject, obj_name, Some(local_version))
90✔
403
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?
90✔
404
        .ok_or_else(|| {
90✔
UNCOV
405
            ObjectsTreeError::SerializationError(format!(
×
UNCOV
406
                "Local version {local_version} of object '{obj_name}' not found"
×
UNCOV
407
            ))
×
UNCOV
408
        })?;
×
409

410
    let local_content = database
90✔
411
        .objects()
90✔
412
        .get(&local_sha256)
90✔
413
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?
90✔
414
        .ok_or_else(|| {
90✔
UNCOV
415
            ObjectsTreeError::SerializationError(format!(
×
UNCOV
416
                "Object content for SHA256 '{local_sha256}' not found"
×
UNCOV
417
            ))
×
UNCOV
418
        })?;
×
419

420
    // Parse local object definition
421
    let local_def = database
90✔
422
        .objects()
90✔
423
        .parse_object_dump(&local_content)
90✔
424
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?;
90✔
425

426
    // Get the baseline version (previous version)
427
    // For version 1 (new object), there is no baseline (version 0 doesn't exist)
428
    // For version 2+, the baseline is the previous version
429
    let baseline_version = local_version.saturating_sub(1);
90✔
430
    let baseline_sha256 = database
90✔
431
        .refs()
90✔
432
        .get_ref(VcsObjectType::MooObject, obj_name, Some(baseline_version))
90✔
433
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?;
90✔
434

435
    if let Some(baseline_sha256) = baseline_sha256 {
90✔
436
        // Get baseline content and parse it
437
        let baseline_content = database
19✔
438
            .objects()
19✔
439
            .get(&baseline_sha256)
19✔
440
            .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?
19✔
441
            .ok_or_else(|| {
19✔
442
                ObjectsTreeError::SerializationError(format!(
×
443
                    "Baseline object content for SHA256 '{baseline_sha256}' not found"
×
444
                ))
×
UNCOV
445
            })?;
×
446

447
        let baseline_def = database
19✔
448
            .objects()
19✔
449
            .parse_object_dump(&baseline_content)
19✔
450
            .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?;
19✔
451

452
        // Compare the two object definitions with meta filtering and hints
453
        compare_object_definitions_with_meta(
19✔
454
            &baseline_def,
19✔
455
            &local_def,
19✔
456
            &mut object_change,
19✔
457
            Some(database),
19✔
458
            Some(obj_name),
19✔
459
            verb_hints,
19✔
460
            prop_hints,
19✔
461
        );
462
    } else {
463
        // No baseline version - this is a new object, mark all as added
464
        for verb in &local_def.verbs {
72✔
465
            for verb_name in &verb.names {
2✔
466
                object_change.verbs_added.insert(verb_name.as_string());
1✔
467
            }
1✔
468
        }
469
        for prop_def in &local_def.property_definitions {
73✔
470
            object_change.props_added.insert(prop_def.name.as_string());
2✔
471
        }
2✔
472
        for prop_override in &local_def.property_overrides {
71✔
UNCOV
473
            object_change
×
UNCOV
474
                .props_added
×
UNCOV
475
                .insert(prop_override.name.as_string());
×
UNCOV
476
        }
×
477
    }
478

479
    // Add meta changes to the object change
480
    compare_meta_versions(database, obj_name, &mut object_change)?;
90✔
481

482
    Ok(object_change)
90✔
483
}
90✔
484

485
/// Compare meta object versions to determine what meta changes occurred
486
/// Updates the object_change with meta_ignored_* and meta_unignored_* fields
487
pub fn compare_meta_versions(
97✔
488
    database: &DatabaseRef,
97✔
489
    obj_name: &str,
97✔
490
    object_change: &mut ObjectChange,
97✔
491
) -> Result<(), ObjectsTreeError> {
97✔
492
    // Get current meta version
493
    let current_meta = if let Some(meta_sha256) = database
97✔
494
        .refs()
97✔
495
        .get_ref(VcsObjectType::MooMetaObject, obj_name, None)
97✔
496
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?
97✔
497
    {
498
        let yaml = database
14✔
499
            .objects()
14✔
500
            .get(&meta_sha256)
14✔
501
            .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?
14✔
502
            .ok_or_else(|| {
14✔
UNCOV
503
                ObjectsTreeError::SerializationError(format!(
×
UNCOV
504
                    "Meta SHA256 '{meta_sha256}' not found"
×
UNCOV
505
                ))
×
UNCOV
506
            })?;
×
507
        Some(
508
            database
14✔
509
                .objects()
14✔
510
                .parse_meta_dump(&yaml)
14✔
511
                .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?,
14✔
512
        )
513
    } else {
514
        None
83✔
515
    };
516

517
    // Get baseline meta version by finding the previous meta version
518
    // We need to find what the meta looked like before the local change
519
    let baseline_meta = {
97✔
520
        // Get the current version of the meta
521
        let current_meta_version = database
97✔
522
            .refs()
97✔
523
            .get_current_version(VcsObjectType::MooMetaObject, obj_name)
97✔
524
            .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?;
97✔
525

526
        if let Some(current_version) = current_meta_version {
97✔
527
            // Get the previous version
528
            let baseline_version = current_version.saturating_sub(1);
14✔
529
            if baseline_version > 0 {
14✔
530
                if let Some(baseline_sha256) = database
1✔
531
                    .refs()
1✔
532
                    .get_ref(
1✔
533
                        VcsObjectType::MooMetaObject,
1✔
534
                        obj_name,
1✔
535
                        Some(baseline_version),
1✔
536
                    )
1✔
537
                    .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?
1✔
538
                {
539
                    let yaml = database
1✔
540
                        .objects()
1✔
541
                        .get(&baseline_sha256)
1✔
542
                        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?
1✔
543
                        .ok_or_else(|| {
1✔
UNCOV
544
                            ObjectsTreeError::SerializationError(format!(
×
UNCOV
545
                                "Baseline meta SHA256 '{baseline_sha256}' not found"
×
UNCOV
546
                            ))
×
UNCOV
547
                        })?;
×
548
                    Some(
549
                        database
1✔
550
                            .objects()
1✔
551
                            .parse_meta_dump(&yaml)
1✔
552
                            .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?,
1✔
553
                    )
554
                } else {
555
                    None
×
556
                }
557
            } else {
558
                None
13✔
559
            }
560
        } else {
561
            None
83✔
562
        }
563
    };
564

565
    // Compare the two meta objects
566
    let current_meta = current_meta.unwrap_or_default();
97✔
567
    let baseline_meta = baseline_meta.unwrap_or_default();
97✔
568

569
    // Find properties that became ignored
570
    for prop in &current_meta.ignored_properties {
105✔
571
        if !baseline_meta.ignored_properties.contains(prop) {
8✔
572
            object_change.meta_ignored_properties.insert(prop.clone());
8✔
573
        }
8✔
574
    }
575

576
    // Find properties that became unignored
577
    for prop in &baseline_meta.ignored_properties {
98✔
578
        if !current_meta.ignored_properties.contains(prop) {
1✔
579
            object_change
1✔
580
                .meta_unignored_properties
1✔
581
                .insert(prop.clone());
1✔
582
        }
1✔
583
    }
584

585
    // Find verbs that became ignored
586
    for verb in &current_meta.ignored_verbs {
104✔
587
        if !baseline_meta.ignored_verbs.contains(verb) {
7✔
588
            object_change.meta_ignored_verbs.insert(verb.clone());
7✔
589
        }
7✔
590
    }
591

592
    // Find verbs that became unignored
593
    for verb in &baseline_meta.ignored_verbs {
97✔
UNCOV
594
        if !current_meta.ignored_verbs.contains(verb) {
×
595
            object_change.meta_unignored_verbs.insert(verb.clone());
×
596
        }
×
597
    }
598

599
    Ok(())
97✔
600
}
97✔
601

602
/// Apply hints to convert added/deleted verbs/properties to renames
603
fn apply_hints_to_object_change(
19✔
604
    object_change: &mut ObjectChange,
19✔
605
    obj_name: &str,
19✔
606
    verb_hints: &[crate::types::VerbRenameHint],
19✔
607
    prop_hints: &[crate::types::PropertyRenameHint],
19✔
608
) {
19✔
609
    // Apply verb rename hints
610
    for hint in verb_hints {
19✔
UNCOV
611
        if hint.object_name != obj_name {
×
612
            continue; // Skip hints for other objects
×
613
        }
×
614

615
        // Check if both from_verb and to_verb are in the expected sets
616
        let from_in_deleted = object_change.verbs_deleted.contains(&hint.from_verb);
×
617
        let to_in_added = object_change.verbs_added.contains(&hint.to_verb);
×
618

UNCOV
619
        if from_in_deleted && to_in_added {
×
620
            // This is a valid rename hint - apply it
621
            object_change.verbs_deleted.remove(&hint.from_verb);
×
UNCOV
622
            object_change.verbs_added.remove(&hint.to_verb);
×
UNCOV
623
            object_change.verbs_renamed.insert(hint.from_verb.clone(), hint.to_verb.clone());
×
624
            
UNCOV
625
            tracing::debug!(
×
UNCOV
626
                "Applied verb rename hint for object '{}': '{}' -> '{}'",
×
627
                obj_name, hint.from_verb, hint.to_verb
628
            );
629
        }
×
630
    }
631

632
    // Apply property rename hints
633
    for hint in prop_hints {
19✔
UNCOV
634
        if hint.object_name != obj_name {
×
UNCOV
635
            continue; // Skip hints for other objects
×
UNCOV
636
        }
×
637

638
        // Check if both from_prop and to_prop are in the expected sets
UNCOV
639
        let from_in_deleted = object_change.props_deleted.contains(&hint.from_prop);
×
UNCOV
640
        let to_in_added = object_change.props_added.contains(&hint.to_prop);
×
641

UNCOV
642
        if from_in_deleted && to_in_added {
×
643
            // This is a valid rename hint - apply it
UNCOV
644
            object_change.props_deleted.remove(&hint.from_prop);
×
UNCOV
645
            object_change.props_added.remove(&hint.to_prop);
×
UNCOV
646
            object_change.props_renamed.insert(hint.from_prop.clone(), hint.to_prop.clone());
×
647
            
UNCOV
648
            tracing::debug!(
×
UNCOV
649
                "Applied property rename hint for object '{}': '{}' -> '{}'",
×
650
                obj_name, hint.from_prop, hint.to_prop
651
            );
652
        }
×
653
    }
654
}
19✔
655

656
/// Compare two ObjectDefinitions with optional meta filtering and rename hints
657
pub fn compare_object_definitions_with_meta(
25✔
658
    baseline: &ObjectDefinition,
25✔
659
    local: &ObjectDefinition,
25✔
660
    object_change: &mut ObjectChange,
25✔
661
    database: Option<&DatabaseRef>,
25✔
662
    obj_name: Option<&str>,
25✔
663
    verb_hints: Option<&[crate::types::VerbRenameHint]>,
25✔
664
    prop_hints: Option<&[crate::types::PropertyRenameHint]>,
25✔
665
) {
25✔
666
    // Load meta if database and obj_name are provided
667
    let meta = if let (Some(db), Some(name)) = (database, obj_name) {
25✔
668
        match db.refs().get_ref(VcsObjectType::MooMetaObject, name, None) {
19✔
669
            Ok(Some(meta_sha256)) => match db.objects().get(&meta_sha256) {
5✔
670
                Ok(Some(yaml)) => db.objects().parse_meta_dump(&yaml).ok(),
5✔
UNCOV
671
                _ => None,
×
672
            },
673
            _ => None,
14✔
674
        }
675
    } else {
676
        None
6✔
677
    };
678
    // Compare verbs
679
    // Use a map from first verb name to the verb definition to track each verb uniquely
680
    let baseline_verbs: HashMap<String, &moor_compiler::ObjVerbDef> = baseline
25✔
681
        .verbs
25✔
682
        .iter()
25✔
683
        .filter_map(|v| {
25✔
684
            // Use the first name as the identifier for this verb
685
            v.names.first().map(|name| (name.as_string(), v))
24✔
686
        })
24✔
687
        .collect();
25✔
688

689
    let local_verbs: HashMap<String, &moor_compiler::ObjVerbDef> = local
25✔
690
        .verbs
25✔
691
        .iter()
25✔
692
        .filter_map(|v| {
25✔
693
            // Use the first name as the identifier for this verb
694
            v.names.first().map(|name| (name.as_string(), v))
25✔
695
        })
25✔
696
        .collect();
25✔
697

698
    // Find added, modified, and deleted verbs
699
    // We track which baseline verbs have been matched to avoid marking them as deleted
700
    let mut matched_baseline_verbs: HashSet<String> = HashSet::new();
25✔
701

702
    for (first_name, local_verb) in &local_verbs {
50✔
703
        // Check if this verb exists in baseline by looking for any matching name
704
        let baseline_match = baseline_verbs.iter().find(|(_, baseline_verb)| {
27✔
705
            // Check if any name from baseline_verb matches any name from local_verb
706
            baseline_verb.names.iter().any(|bn| 
27✔
707
                local_verb.names.iter().any(|ln| bn.as_string() == ln.as_string())
40✔
708
            )
709
        });
27✔
710

711
        if let Some((baseline_first_name, baseline_verb)) = baseline_match {
25✔
712
            matched_baseline_verbs.insert(baseline_first_name.clone());
18✔
713
            
714
            // Verb exists in both - check if it's modified
715
            if verbs_differ(baseline_verb, local_verb) {
18✔
716
                object_change.verbs_modified.insert(first_name.clone());
8✔
717
            }
10✔
718
        } else {
7✔
719
            // Verb is new (no matching names in baseline)
7✔
720
            object_change.verbs_added.insert(first_name.clone());
7✔
721
        }
7✔
722
    }
723

724
    // Check for deleted verbs (those in baseline but not matched in local)
725
    for (baseline_first_name, baseline_verb) in &baseline_verbs {
49✔
726
        if !matched_baseline_verbs.contains(baseline_first_name) {
24✔
727
            // Check if any name from this verb is ignored
728
            let is_ignored = meta.as_ref().map(|m| {
6✔
729
                baseline_verb.names.iter().any(|name| m.ignored_verbs.contains(&name.as_string()))
2✔
730
            }).unwrap_or(false);
6✔
731

732
            if !is_ignored {
6✔
733
                // Verb was actually deleted (not just ignored)
4✔
734
                object_change.verbs_deleted.insert(baseline_first_name.clone());
4✔
735
            } else {
4✔
736
                tracing::debug!(
2✔
UNCOV
737
                    "Verb '{}' is missing but ignored in meta, not marking as deleted",
×
738
                    baseline_first_name
739
                );
740
            }
741
        }
18✔
742
    }
743

744
    // Compare property definitions
745
    let baseline_props: HashMap<String, &moor_compiler::ObjPropDef> = baseline
25✔
746
        .property_definitions
25✔
747
        .iter()
25✔
748
        .map(|p| (p.name.as_string(), p))
25✔
749
        .collect();
25✔
750

751
    let local_props: HashMap<String, &moor_compiler::ObjPropDef> = local
25✔
752
        .property_definitions
25✔
753
        .iter()
25✔
754
        .map(|p| (p.name.as_string(), p))
25✔
755
        .collect();
25✔
756

757
    // Find added, modified, and deleted property definitions
758
    for (prop_name, local_prop) in &local_props {
33✔
759
        if let Some(baseline_prop) = baseline_props.get(prop_name) {
8✔
760
            // Property exists in both - check if it's modified
761
            if property_definitions_differ(baseline_prop, local_prop) {
6✔
UNCOV
762
                object_change.props_modified.insert(prop_name.clone());
×
763
            }
6✔
764
        } else {
2✔
765
            // Property is new
2✔
766
            object_change.props_added.insert(prop_name.clone());
2✔
767
        }
2✔
768
    }
769

770
    for prop_name in baseline_props.keys() {
25✔
771
        if !local_props.contains_key(prop_name) {
10✔
772
            // Property is missing - check if it's ignored before marking as deleted
773
            let is_ignored = meta
4✔
774
                .as_ref()
4✔
775
                .map(|m| m.ignored_properties.contains(prop_name))
4✔
776
                .unwrap_or(false);
4✔
777

778
            if !is_ignored {
4✔
779
                // Property was actually deleted (not just ignored)
1✔
780
                object_change.props_deleted.insert(prop_name.clone());
1✔
781
            } else {
1✔
782
                tracing::debug!(
3✔
UNCOV
783
                    "Property '{}' is missing but ignored in meta, not marking as deleted",
×
784
                    prop_name
785
                );
786
            }
787
        }
6✔
788
    }
789

790
    // Compare property overrides
791
    let baseline_overrides: HashMap<String, &moor_compiler::ObjPropOverride> = baseline
25✔
792
        .property_overrides
25✔
793
        .iter()
25✔
794
        .map(|p| (p.name.as_string(), p))
25✔
795
        .collect();
25✔
796

797
    let local_overrides: HashMap<String, &moor_compiler::ObjPropOverride> = local
25✔
798
        .property_overrides
25✔
799
        .iter()
25✔
800
        .map(|p| (p.name.as_string(), p))
25✔
801
        .collect();
25✔
802

803
    // Find added, modified, and deleted property overrides
804
    for (prop_name, local_override) in &local_overrides {
25✔
UNCOV
805
        if let Some(baseline_override) = baseline_overrides.get(prop_name) {
×
806
            // Override exists in both - check if it's modified
807
            if property_overrides_differ(baseline_override, local_override) {
×
UNCOV
808
                object_change.props_modified.insert(prop_name.clone());
×
UNCOV
809
            }
×
UNCOV
810
        } else {
×
UNCOV
811
            // Override is new
×
UNCOV
812
            object_change.props_added.insert(prop_name.clone());
×
UNCOV
813
        }
×
814
    }
815

816
    for prop_name in baseline_overrides.keys() {
25✔
UNCOV
817
        if !local_overrides.contains_key(prop_name) {
×
818
            // Override is missing - check if it's ignored before marking as deleted
UNCOV
819
            let is_ignored = meta
×
UNCOV
820
                .as_ref()
×
UNCOV
821
                .map(|m| m.ignored_properties.contains(prop_name))
×
UNCOV
822
                .unwrap_or(false);
×
823

UNCOV
824
            if !is_ignored {
×
UNCOV
825
                // Override was actually deleted (not just ignored)
×
UNCOV
826
                object_change.props_deleted.insert(prop_name.clone());
×
UNCOV
827
            } else {
×
UNCOV
828
                tracing::debug!(
×
UNCOV
829
                    "Property override '{}' is missing but ignored in meta, not marking as deleted",
×
830
                    prop_name
831
                );
832
            }
UNCOV
833
        }
×
834
    }
835

836
    // Apply hints if provided
837
    if let Some(name) = obj_name {
25✔
838
        if let (Some(v_hints), Some(p_hints)) = (verb_hints, prop_hints) {
19✔
839
            apply_hints_to_object_change(object_change, name, v_hints, p_hints);
19✔
840
        }
19✔
841
    }
6✔
842
}
25✔
843

844
/// Check if two verb definitions differ
845
pub fn verbs_differ(
18✔
846
    baseline: &moor_compiler::ObjVerbDef,
18✔
847
    local: &moor_compiler::ObjVerbDef,
18✔
848
) -> bool {
18✔
849
    baseline.argspec != local.argspec
18✔
850
        || baseline.owner != local.owner
18✔
851
        || baseline.flags != local.flags
16✔
852
        || baseline.program != local.program
16✔
853
}
18✔
854

855
/// Check if two property definitions differ
856
pub fn property_definitions_differ(
6✔
857
    baseline: &moor_compiler::ObjPropDef,
6✔
858
    local: &moor_compiler::ObjPropDef,
6✔
859
) -> bool {
6✔
860
    baseline.perms != local.perms || baseline.value != local.value
6✔
861
}
6✔
862

863
/// Check if two property overrides differ
UNCOV
864
pub fn property_overrides_differ(
×
865
    baseline: &moor_compiler::ObjPropOverride,
×
866
    local: &moor_compiler::ObjPropOverride,
×
867
) -> bool {
×
868
    baseline.value != local.value || baseline.perms_update != local.perms_update
×
869
}
×
870

871
/// Build an ObjectDiffModel by comparing a change against the compiled state
872
/// This is the shared logic used by approve and status operations
873
pub fn build_object_diff_from_change(
41✔
874
    database: &DatabaseRef,
41✔
875
    change: &Change,
41✔
876
) -> Result<ObjectDiffModel, ObjectsTreeError> {
41✔
877
    let mut diff_model = ObjectDiffModel::new();
41✔
878

879
    // Get the complete object list from the index state (excluding the local change)
880
    let complete_object_list = database
41✔
881
        .index()
41✔
882
        .compute_complete_object_list()
41✔
883
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?;
41✔
884

885
    tracing::info!(
41✔
UNCOV
886
        "Using complete object list with {} objects as baseline for change '{}'",
×
UNCOV
887
        complete_object_list.len(),
×
888
        change.name
889
    );
890

891
    // Process the change to build the diff
892
    process_change_for_diff(database, &mut diff_model, change)?;
41✔
893

894
    Ok(diff_model)
41✔
895
}
41✔
896

897
/// Process a single change and add its modifications to the diff model
898
/// This is the shared logic used by approve and status operations
899
pub fn process_change_for_diff(
41✔
900
    database: &DatabaseRef,
41✔
901
    diff_model: &mut ObjectDiffModel,
41✔
902
    change: &Change,
41✔
903
) -> Result<(), ObjectsTreeError> {
41✔
904
    // Use hints from the change (they're kept permanently now)
905
    let verb_hints_ref = Some(change.verb_rename_hints.as_slice());
41✔
906
    let prop_hints_ref = Some(change.property_rename_hints.as_slice());
41✔
907

908
    // Process added objects (filter to only MooObject types)
909
    for obj_info in change
41✔
910
        .added_objects
41✔
911
        .iter()
41✔
912
        .filter(|o| o.object_type == VcsObjectType::MooObject)
41✔
913
    {
914
        let obj_name = obj_id_to_object_name(&obj_info.name, Some(&obj_info.name));
31✔
915
        diff_model.add_object_added(obj_name.clone());
31✔
916

917
        // Get detailed object changes by comparing local vs baseline (which will be empty for new objects)
918
        let object_change = compare_object_versions(
31✔
919
            database,
31✔
920
            &obj_name,
31✔
921
            obj_info.version,
31✔
922
            verb_hints_ref,
31✔
923
            prop_hints_ref,
31✔
UNCOV
924
        )?;
×
925
        diff_model.add_object_change(object_change);
31✔
926
    }
927

928
    // Process deleted objects (filter to only MooObject types)
929
    for obj_info in change
41✔
930
        .deleted_objects
41✔
931
        .iter()
41✔
932
        .filter(|o| o.object_type == VcsObjectType::MooObject)
41✔
933
    {
1✔
934
        let obj_name = obj_id_to_object_name(&obj_info.name, Some(&obj_info.name));
1✔
935
        diff_model.add_object_deleted(obj_name);
1✔
936
    }
1✔
937

938
    // Process renamed objects (filter to only MooObject types)
939
    for renamed in change.renamed_objects.iter().filter(|r| {
41✔
UNCOV
940
        r.from.object_type == VcsObjectType::MooObject
×
UNCOV
941
            && r.to.object_type == VcsObjectType::MooObject
×
UNCOV
942
    }) {
×
UNCOV
943
        let from_name = obj_id_to_object_name(&renamed.from.name, Some(&renamed.from.name));
×
UNCOV
944
        let to_name = obj_id_to_object_name(&renamed.to.name, Some(&renamed.to.name));
×
UNCOV
945
        diff_model.add_object_renamed(from_name, to_name);
×
UNCOV
946
    }
×
947

948
    // Process modified objects with detailed comparison (filter to only MooObject types)
949
    for obj_info in change
41✔
950
        .modified_objects
41✔
951
        .iter()
41✔
952
        .filter(|o| o.object_type == VcsObjectType::MooObject)
41✔
953
    {
954
        let obj_name = obj_id_to_object_name(&obj_info.name, Some(&obj_info.name));
6✔
955
        diff_model.add_object_modified(obj_name.clone());
6✔
956

957
        // Get detailed object changes by comparing local vs baseline
958
        let object_change = compare_object_versions(
6✔
959
            database,
6✔
960
            &obj_name,
6✔
961
            obj_info.version,
6✔
962
            verb_hints_ref,
6✔
963
            prop_hints_ref,
6✔
UNCOV
964
        )?;
×
965
        diff_model.add_object_change(object_change);
6✔
966
    }
967

968
    // Process meta-only changes (meta objects that were added/modified without MOO object changes)
969
    // We need to check for meta objects that have changes but their corresponding MOO object
970
    // wasn't in the added/modified lists
971
    let mut processed_objects = std::collections::HashSet::new();
41✔
972
    
973
    // Collect all MOO object names we've already processed
974
    for obj_info in change.added_objects.iter()
41✔
975
        .chain(change.modified_objects.iter())
41✔
976
        .filter(|o| o.object_type == VcsObjectType::MooObject)
44✔
977
    {
37✔
978
        processed_objects.insert(obj_info.name.clone());
37✔
979
    }
37✔
980

981
    // Now look for meta objects whose MOO objects weren't processed
982
    for obj_info in change
41✔
983
        .added_objects
41✔
984
        .iter()
41✔
985
        .chain(change.modified_objects.iter())
41✔
986
        .filter(|o| o.object_type == VcsObjectType::MooMetaObject)
44✔
987
    {
988
        // Only process if the corresponding MOO object wasn't already processed
989
        if !processed_objects.contains(&obj_info.name) {
7✔
990
            let obj_name = obj_id_to_object_name(&obj_info.name, Some(&obj_info.name));
6✔
991
            
992
            // Create an ObjectChange with just meta tracking
993
            let mut object_change = ObjectChange::new(obj_name);
6✔
994
            
995
            // Compare meta versions to populate meta fields
996
            if let Err(e) = compare_meta_versions(database, &obj_info.name, &mut object_change) {
6✔
UNCOV
997
                tracing::warn!(
×
UNCOV
998
                    "Failed to compare meta versions for '{}': {}",
×
999
                    obj_info.name,
1000
                    e
1001
                );
1002
            }
6✔
1003
            
1004
            // Only add if there are actual meta changes
1005
            if !object_change.meta_ignored_properties.is_empty()
6✔
1006
                || !object_change.meta_ignored_verbs.is_empty()
3✔
1007
                || !object_change.meta_unignored_properties.is_empty()
2✔
1008
                || !object_change.meta_unignored_verbs.is_empty()
1✔
1009
            {
5✔
1010
                diff_model.add_object_change(object_change);
5✔
1011
            }
5✔
1012
        }
1✔
1013
    }
1014

1015
    Ok(())
41✔
1016
}
41✔
1017

1018
/// Build an ObjectDiffModel for abandoning a change (undo operations)
1019
/// This creates the reverse operations needed to undo the change
1020
pub fn build_abandon_diff_from_change(
47✔
1021
    database: &DatabaseRef,
47✔
1022
    change: &Change,
47✔
1023
) -> Result<ObjectDiffModel, ObjectsTreeError> {
47✔
1024
    // Get the complete object list from the index state for comparison
1025
    let complete_object_list = database
47✔
1026
        .index()
47✔
1027
        .compute_complete_object_list()
47✔
1028
        .map_err(|e| ObjectsTreeError::SerializationError(e.to_string()))?;
47✔
1029

1030
    tracing::info!(
47✔
UNCOV
1031
        "Using complete object list with {} objects as baseline for abandoning change '{}'",
×
UNCOV
1032
        complete_object_list.len(),
×
1033
        change.name
1034
    );
1035

1036
    // Create a delta model showing what needs to be undone
1037
    let mut undo_delta = ObjectDiffModel::new();
47✔
1038

1039
    // Get object name mappings for better display names
1040
    let object_names = get_object_names_for_change(change);
47✔
1041

1042
    // Use hints from the change for proper rename tracking
1043
    let verb_hints_ref = Some(change.verb_rename_hints.as_slice());
47✔
1044
    let prop_hints_ref = Some(change.property_rename_hints.as_slice());
47✔
1045

1046
    // Process added objects - to undo, we need to delete them (filter to only MooObject types)
1047
    for added_obj in change
47✔
1048
        .added_objects
47✔
1049
        .iter()
47✔
1050
        .filter(|o| o.object_type == VcsObjectType::MooObject)
47✔
1051
    {
1052
        let object_name = obj_id_to_object_name(
35✔
1053
            &added_obj.name,
35✔
1054
            object_names.get(&added_obj.name).map(|s| s.as_str()),
35✔
1055
        );
1056
        undo_delta.add_object_deleted(object_name.clone());
35✔
1057

1058
        // Get the detailed changes for this added object, then invert them
1059
        // This gives us the verb/property details for the deletion
1060
        let object_change = compare_object_versions(
35✔
1061
            database,
35✔
1062
            &object_name,
35✔
1063
            added_obj.version,
35✔
1064
            verb_hints_ref,
35✔
1065
            prop_hints_ref,
35✔
UNCOV
1066
        )?;
×
1067
        
1068
        // Invert the change to show what needs to be deleted/undone
1069
        let inverted_change = object_change.invert();
35✔
1070
        undo_delta.add_object_change(inverted_change);
35✔
1071
    }
1072

1073
    // Process deleted objects - to undo, we need to add them back (filter to only MooObject types)
1074
    for deleted_obj in change
47✔
1075
        .deleted_objects
47✔
1076
        .iter()
47✔
1077
        .filter(|o| o.object_type == VcsObjectType::MooObject)
47✔
1078
    {
1079
        let object_name = obj_id_to_object_name(
1✔
1080
            &deleted_obj.name,
1✔
1081
            object_names.get(&deleted_obj.name).map(|s| s.as_str()),
1✔
1082
        );
1083
        undo_delta.add_object_added(object_name.clone());
1✔
1084

1085
        // For deleted objects, we need to get the baseline version (the version before deletion)
1086
        // and show what needs to be added back
1087
        // The deleted_obj.version represents the last version before deletion
1088
        let baseline_version = deleted_obj.version;
1✔
1089
        
1090
        // Get the baseline object to see what needs to be re-added
1091
        if let Ok(Some(baseline_sha256)) = database.refs().get_ref(
1✔
1092
            VcsObjectType::MooObject,
1✔
1093
            &deleted_obj.name,
1✔
1094
            Some(baseline_version),
1✔
1095
        ) {
1096
            if let Ok(Some(baseline_content)) = database.objects().get(&baseline_sha256) {
1✔
1097
                if let Ok(baseline_def) = database.objects().parse_object_dump(&baseline_content) {
1✔
1098
                    // Create an ObjectChange showing what needs to be added back
1099
                    let mut object_change = ObjectChange::new(object_name);
1✔
1100
                    
1101
                    // Mark all verbs as needing to be added back
1102
                    for verb in &baseline_def.verbs {
1✔
UNCOV
1103
                        for verb_name in &verb.names {
×
UNCOV
1104
                            object_change.verbs_added.insert(verb_name.as_string());
×
UNCOV
1105
                        }
×
1106
                    }
1107
                    
1108
                    // Mark all properties as needing to be added back
1109
                    for prop_def in &baseline_def.property_definitions {
1✔
UNCOV
1110
                        object_change.props_added.insert(prop_def.name.as_string());
×
UNCOV
1111
                    }
×
1112
                    for prop_override in &baseline_def.property_overrides {
1✔
UNCOV
1113
                        object_change.props_added.insert(prop_override.name.as_string());
×
UNCOV
1114
                    }
×
1115
                    
1116
                    undo_delta.add_object_change(object_change);
1✔
UNCOV
1117
                }
×
UNCOV
1118
            }
×
UNCOV
1119
        }
×
1120
    }
1121

1122
    // Process renamed objects - to undo, we need to rename them back (filter to only MooObject types)
1123
    for renamed in change.renamed_objects.iter().filter(|r| {
47✔
UNCOV
1124
        r.from.object_type == VcsObjectType::MooObject
×
UNCOV
1125
            && r.to.object_type == VcsObjectType::MooObject
×
UNCOV
1126
    }) {
×
UNCOV
1127
        let from_name = obj_id_to_object_name(
×
UNCOV
1128
            &renamed.from.name,
×
UNCOV
1129
            object_names.get(&renamed.from.name).map(|s| s.as_str()),
×
1130
        );
UNCOV
1131
        let to_name = obj_id_to_object_name(
×
UNCOV
1132
            &renamed.to.name,
×
UNCOV
1133
            object_names.get(&renamed.to.name).map(|s| s.as_str()),
×
1134
        );
1135
        // Reverse the rename direction for undo
UNCOV
1136
        undo_delta.add_object_renamed(to_name, from_name);
×
1137
    }
1138

1139
    // Process modified objects - get detailed changes and invert them (filter to only MooObject types)
1140
    for modified_obj in change
47✔
1141
        .modified_objects
47✔
1142
        .iter()
47✔
1143
        .filter(|o| o.object_type == VcsObjectType::MooObject)
47✔
1144
    {
1145
        let object_name = obj_id_to_object_name(
4✔
1146
            &modified_obj.name,
4✔
1147
            object_names.get(&modified_obj.name).map(|s| s.as_str()),
4✔
1148
        );
1149
        undo_delta.add_object_modified(object_name.clone());
4✔
1150

1151
        // Get the detailed changes by comparing versions
1152
        let object_change = compare_object_versions(
4✔
1153
            database,
4✔
1154
            &object_name,
4✔
1155
            modified_obj.version,
4✔
1156
            verb_hints_ref,
4✔
1157
            prop_hints_ref,
4✔
UNCOV
1158
        )?;
×
1159
        
1160
        // INVERT the change to get the undo operations
1161
        // If a verb was added in the change, we need to delete it to undo
1162
        // If a verb was deleted in the change, we need to add it back to undo
1163
        let inverted_change = object_change.invert();
4✔
1164
        undo_delta.add_object_change(inverted_change);
4✔
1165
    }
1166

1167
    Ok(undo_delta)
47✔
1168
}
47✔
1169

1170
/// Get object names for the change objects to improve display names
1171
/// This is a simplified implementation - in practice you'd want to
1172
/// query the actual object names from the MOO database
1173
pub fn get_object_names_for_change(change: &Change) -> HashMap<String, String> {
47✔
1174
    let mut object_names = HashMap::new();
47✔
1175

1176
    // Try to get object names from workspace provider (filter to only MooObject types)
1177
    for obj_info in change
47✔
1178
        .added_objects
47✔
1179
        .iter()
47✔
1180
        .chain(change.modified_objects.iter())
47✔
1181
        .chain(change.deleted_objects.iter())
47✔
1182
        .filter(|o| o.object_type == VcsObjectType::MooObject)
47✔
1183
    {
40✔
1184
        // For now, we'll just use the object name as the name
40✔
1185
        // In a real implementation, you'd query the actual object names
40✔
1186
        object_names.insert(obj_info.name.clone(), obj_info.name.clone());
40✔
1187
    }
40✔
1188

1189
    for renamed in change.renamed_objects.iter().filter(|r| {
47✔
UNCOV
1190
        r.from.object_type == VcsObjectType::MooObject
×
UNCOV
1191
            && r.to.object_type == VcsObjectType::MooObject
×
UNCOV
1192
    }) {
×
UNCOV
1193
        object_names.insert(renamed.from.name.clone(), renamed.from.name.clone());
×
UNCOV
1194
        object_names.insert(renamed.to.name.clone(), renamed.to.name.clone());
×
UNCOV
1195
    }
×
1196

1197
    object_names
47✔
1198
}
47✔
1199

1200
#[cfg(test)]
1201
mod tests {
1202
    use super::*;
1203

1204
    #[test]
1205
    fn test_object_change_to_moo_var() {
2✔
1206
        let mut change = ObjectChange::new("TestObject".to_string());
2✔
1207
        change.verbs_added.insert("new_verb".to_string());
2✔
1208
        change.props_modified.insert("existing_prop".to_string());
2✔
1209

1210
        let moo_var = change.to_moo_var();
2✔
1211

1212
        // Verify it's a map
1213
        assert!(matches!(moo_var.variant(), moor_var::Variant::Map(_)));
2✔
1214
    }
2✔
1215

1216
    #[test]
1217
    fn test_object_diff_model_to_moo_var() {
2✔
1218
        let mut model = ObjectDiffModel::new();
2✔
1219
        model.add_object_added("NewObject".to_string());
2✔
1220
        model.add_object_deleted("OldObject".to_string());
2✔
1221

1222
        let moo_var = model.to_moo_var();
2✔
1223

1224
        // Verify it's a map
1225
        assert!(matches!(moo_var.variant(), moor_var::Variant::Map(_)));
2✔
1226
    }
2✔
1227

1228
    #[test]
1229
    fn test_obj_id_to_object_name() {
2✔
1230
        assert_eq!(obj_id_to_object_name("#4", Some("foobar")), "Foobar");
2✔
1231
        assert_eq!(obj_id_to_object_name("#4", Some("#4")), "#4");
2✔
1232
        assert_eq!(obj_id_to_object_name("#4", None), "#4");
2✔
1233
        assert_eq!(
2✔
1234
            obj_id_to_object_name("TestObject", Some("TestObject")),
2✔
1235
            "TestObject"
1236
        );
1237
    }
2✔
1238

1239
    #[test]
1240
    fn test_merge_object_diff_models() {
2✔
1241
        let mut model1 = ObjectDiffModel::new();
2✔
1242
        model1.add_object_added("Object1".to_string());
2✔
1243

1244
        let mut model2 = ObjectDiffModel::new();
2✔
1245
        model2.add_object_added("Object2".to_string());
2✔
1246
        model2.add_object_deleted("Object3".to_string());
2✔
1247

1248
        model1.merge(model2);
2✔
1249

1250
        assert!(model1.objects_added.contains("Object1"));
2✔
1251
        assert!(model1.objects_added.contains("Object2"));
2✔
1252
        assert!(model1.objects_deleted.contains("Object3"));
2✔
1253
    }
2✔
1254
}
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