KiCad PCB EDA Suite
Loading...
Searching...
No Matches
kicad_merge_engine.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 3
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, you may find one here:
18 * http://www.gnu.org/licenses/gpl-3.0.html
19 */
20
22
23#include <json_conversions.h>
24
25#include <nlohmann/json.hpp>
26
27#include <map>
28#include <set>
29#include <stdexcept>
30
31
32namespace KICAD_DIFF
33{
34
35const char* PropResToString( PROP_RES aRes )
36{
37 switch( aRes )
38 {
39 case PROP_RES::OURS: return "ours";
40 case PROP_RES::THEIRS: return "theirs";
41 case PROP_RES::ANCESTOR: return "ancestor";
42 case PROP_RES::CUSTOM: return "custom";
43 }
44
45 return "ours";
46}
47
48
49PROP_RES PropResFromString( const std::string& aStr )
50{
51 if( aStr == "ours" ) return PROP_RES::OURS;
52 if( aStr == "theirs" ) return PROP_RES::THEIRS;
53 if( aStr == "ancestor" ) return PROP_RES::ANCESTOR;
54 if( aStr == "custom" ) return PROP_RES::CUSTOM;
55
56 throw std::invalid_argument( "Unknown PROP_RES: " + aStr );
57}
58
59
60const char* ItemResToString( ITEM_RES aRes )
61{
62 switch( aRes )
63 {
64 case ITEM_RES::TAKE_OURS: return "take_ours";
65 case ITEM_RES::TAKE_THEIRS: return "take_theirs";
66 case ITEM_RES::TAKE_ANCESTOR: return "take_ancestor";
67 case ITEM_RES::MERGE_PROPS: return "merge_props";
68 case ITEM_RES::DELETE_ITEM: return "delete";
69 case ITEM_RES::KEEP: return "keep";
70 }
71
72 return "keep";
73}
74
75
76ITEM_RES ItemResFromString( const std::string& aStr )
77{
78 if( aStr == "take_ours" ) return ITEM_RES::TAKE_OURS;
79 if( aStr == "take_theirs" ) return ITEM_RES::TAKE_THEIRS;
80 if( aStr == "take_ancestor" ) return ITEM_RES::TAKE_ANCESTOR;
81 if( aStr == "merge_props" ) return ITEM_RES::MERGE_PROPS;
82 if( aStr == "delete" )
84 if( aStr == "keep" ) return ITEM_RES::KEEP;
85
86 throw std::invalid_argument( "Unknown ITEM_RES: " + aStr );
87}
88
89
90namespace
91{
92
93void indexChangeTree( const ITEM_CHANGE& aChange,
94 std::map<KIID_PATH, const ITEM_CHANGE*>& aIndex )
95{
96 aIndex[aChange.id] = &aChange;
97
98 for( const ITEM_CHANGE& child : aChange.children )
99 indexChangeTree( child, aIndex );
100}
101
102} // namespace
103
104
105std::map<KIID_PATH, const ITEM_CHANGE*> IndexChangesByKiid( const DOCUMENT_DIFF& aDiff )
106{
107 std::map<KIID_PATH, const ITEM_CHANGE*> index;
108
109 for( const ITEM_CHANGE& c : aDiff.changes )
110 indexChangeTree( c, index );
111
112 return index;
113}
114
115
116std::map<wxString, const PROPERTY_DELTA*> IndexPropertiesByName( const ITEM_CHANGE& aChange )
117{
118 std::map<wxString, const PROPERTY_DELTA*> index;
119
120 for( const PROPERTY_DELTA& d : aChange.properties )
121 index[d.name] = &d;
122
123 return index;
124}
125
126
128 const PROPERTY_DELTA* aTheirs,
129 const KICAD_MERGE_ENGINE::OPTIONS& aOptions )
130{
132
133 if( aOurs && !aTheirs )
134 {
135 out.kind = PROP_RES::OURS;
136 return out;
137 }
138
139 if( !aOurs && aTheirs )
140 {
142 return out;
143 }
144
145 // Both sides null is unreachable from PlanMerge (allNames is the union of
146 // present keys), but defend the deref-of-aOurs/aTheirs below: assert in
147 // debug builds so a broken caller fails QA via wxAssertThrower, and
148 // return a deterministic OURS in release builds so production doesn't
149 // crash on a contract violation.
150 wxASSERT( aOurs || aTheirs );
151
152 if( !aOurs && !aTheirs )
153 {
154 out.kind = PROP_RES::OURS;
155 return out;
156 }
157
158 // Branch order is load-bearing. autoResolveEqualValues runs FIRST so
159 // converging-edits (both sides reach the same end value) auto-merge under
160 // either option flag, even when ours' delta is a no-op against a stale
161 // baseline. The two no-op detectors that follow are gated on matching
162 // before values so a stale-baseline no-op doesn't silently override a
163 // real edit on the other side.
164
165 if( aOptions.autoResolveEqualValues && aOurs->after == aTheirs->after )
166 {
167 out.kind = PROP_RES::OURS; // same end value; pick either
168 return out;
169 }
170
171 // No-op detection requires both sides to share the same `before` value
172 // (the 3-way merge contract). A stale baseline on either side disables
173 // the auto-merge and falls through to the conflict path.
174 const bool baselinesMatch = aOurs->before == aTheirs->before;
175
176 if( aOptions.preferAutoMerge && baselinesMatch && aOurs->after == aOurs->before )
177 {
178 out.kind = PROP_RES::THEIRS; // ours didn't really change
179 return out;
180 }
181
182 if( aOptions.preferAutoMerge && baselinesMatch && aTheirs->after == aTheirs->before )
183 {
184 out.kind = PROP_RES::OURS; // theirs didn't really change
185 return out;
186 }
187
188 // True conflict — default to OURS but flag for user.
189 out.kind = PROP_RES::OURS;
190 out.isUnresolved = true;
191 return out;
192}
193
194
196{
197 return name == aOther.name && kind == aOther.kind && customValue == aOther.customValue;
198}
199
200
201nlohmann::json PROPERTY_RESOLUTION::ToJson() const
202{
203 nlohmann::json j;
204 j["name"] = name;
205 j["kind"] = PropResToString( kind );
206
207 if( kind == PROP_RES::CUSTOM )
208 j["custom"] = customValue.ToJson();
209
210 return j;
211}
212
213
215{
217 r.name = aJson.at( "name" ).get<wxString>();
218 r.kind = PropResFromString( aJson.at( "kind" ).get<std::string>() );
219
220 if( aJson.contains( "custom" ) )
221 r.customValue = DIFF_VALUE::FromJson( aJson.at( "custom" ) );
222
223 return r;
224}
225
226
228{
229 return id == aOther.id && kind == aOther.kind && props == aOther.props;
230}
231
232
233nlohmann::json ITEM_RESOLUTION::ToJson() const
234{
235 nlohmann::json j;
236 j["id"] = id.AsString();
237 j["kind"] = ItemResToString( kind );
238
239 nlohmann::json arr = nlohmann::json::array();
240
241 for( const PROPERTY_RESOLUTION& p : props )
242 arr.push_back( p.ToJson() );
243
244 j["props"] = std::move( arr );
245 return j;
246}
247
248
249ITEM_RESOLUTION ITEM_RESOLUTION::FromJson( const nlohmann::json& aJson )
250{
252 r.id = KIID_PATH( aJson.at( "id" ).get<wxString>() );
253 r.kind = ItemResFromString( aJson.at( "kind" ).get<std::string>() );
254
255 for( const auto& p : aJson.at( "props" ) )
256 r.props.push_back( PROPERTY_RESOLUTION::FromJson( p ) );
257
258 return r;
259}
260
261
262nlohmann::json MERGE_PLAN::ToJson() const
263{
264 nlohmann::json j;
265
266 nlohmann::json arr = nlohmann::json::array();
267
268 for( const ITEM_RESOLUTION& a : actions )
269 arr.push_back( a.ToJson() );
270
271 j["actions"] = std::move( arr );
272
273 nlohmann::json unr = nlohmann::json::array();
274
275 for( const KIID_PATH& u : unresolved )
276 unr.push_back( u.AsString() );
277
278 j["unresolved"] = std::move( unr );
279 j["requiresZoneRefill"] = requiresZoneRefill;
280 j["requiresConnectivityRebuild"] = requiresConnectivityRebuild;
281 return j;
282}
283
284
285MERGE_PLAN MERGE_PLAN::FromJson( const nlohmann::json& aJson )
286{
287 MERGE_PLAN p;
288
289 for( const auto& a : aJson.at( "actions" ) )
290 p.actions.push_back( ITEM_RESOLUTION::FromJson( a ) );
291
292 for( const auto& u : aJson.at( "unresolved" ) )
293 p.unresolved.push_back( KIID_PATH( u.get<wxString>() ) );
294
295 p.requiresZoneRefill = aJson.value( "requiresZoneRefill", false );
296 p.requiresConnectivityRebuild = aJson.value( "requiresConnectivityRebuild", false );
297 return p;
298}
299
300
302 const DOCUMENT_DIFF& aAncestorTheirs ) const
303{
304 MERGE_PLAN plan;
305
306 auto ourIndex = IndexChangesByKiid( aAncestorOurs );
307 auto theirIndex = IndexChangesByKiid( aAncestorTheirs );
308
309 std::set<KIID_PATH> processed;
310
311 auto noteSideEffects = [&]( const ITEM_CHANGE& aChange )
312 {
313 if( ChangeInvalidatesZone( aChange ) )
314 plan.requiresZoneRefill = true;
315
316 if( ChangeRequiresConnectivityRebuild( aChange ) )
317 plan.requiresConnectivityRebuild = true;
318 };
319
320 // Pass 1: items touched on both sides.
321 for( const auto& [id, ourChange] : ourIndex )
322 {
323 auto it = theirIndex.find( id );
324
325 if( it == theirIndex.end() )
326 continue; // only ours touched it — Pass 2 handles those
327
328 const ITEM_CHANGE* theirChange = it->second;
329 processed.insert( id );
330 noteSideEffects( *ourChange );
331 noteSideEffects( *theirChange );
332
333 // Both deleted: take the deletion.
334 if( ourChange->kind == CHANGE_KIND::REMOVED
335 && theirChange->kind == CHANGE_KIND::REMOVED )
336 {
338 r.id = id;
340 plan.actions.push_back( std::move( r ) );
341 continue;
342 }
343
344 // One side deleted, the other modified: conflict (data loss risk).
345 if( ourChange->kind == CHANGE_KIND::REMOVED
346 && theirChange->kind == CHANGE_KIND::MODIFIED )
347 {
348 plan.unresolved.push_back( id );
350 r.id = id;
351 r.kind = ITEM_RES::KEEP; // safe default; user must resolve
352 plan.actions.push_back( std::move( r ) );
353 continue;
354 }
355
356 if( ourChange->kind == CHANGE_KIND::MODIFIED
357 && theirChange->kind == CHANGE_KIND::REMOVED )
358 {
359 plan.unresolved.push_back( id );
361 r.id = id;
363 plan.actions.push_back( std::move( r ) );
364 continue;
365 }
366
367 // Both added with the same id: explicit collision. Keep the default
368 // as KEEP so a downstream `kind == TAKE_*` check still reflects an
369 // explicit user choice; applier's KEEP path falls back to
370 // ancestor-or-ours.
371 if( ourChange->kind == CHANGE_KIND::ADDED
372 && theirChange->kind == CHANGE_KIND::ADDED )
373 {
374 plan.unresolved.push_back( id );
376 r.id = id;
378 plan.actions.push_back( std::move( r ) );
379 continue;
380 }
381
382 // Both modified: property-level merge.
383 if( ourChange->kind == CHANGE_KIND::MODIFIED
384 && theirChange->kind == CHANGE_KIND::MODIFIED )
385 {
386 // preferAutoMerge=false: every item touched on both sides
387 // conflicts, even if the property edits are orthogonal.
388 if( !m_options.preferAutoMerge )
389 {
390 plan.unresolved.push_back( id );
392 r.id = id;
394 plan.actions.push_back( std::move( r ) );
395 continue;
396 }
397
398 // A coarse MODIFIED record with no property deltas and no child
399 // edits means the change was caught by semantic operator== alone
400 // — something PROPERTY_MANAGER cannot serialize (e.g., raw cache
401 // state). Resolving as MERGE_PROPS with zero props would silently
402 // lose that change; flag a conflict instead.
403 if( ourChange->properties.empty() && theirChange->properties.empty()
404 && ourChange->children.empty() && theirChange->children.empty() )
405 {
406 plan.unresolved.push_back( id );
408 r.id = id;
410 plan.actions.push_back( std::move( r ) );
411 continue;
412 }
413
415 r.id = id;
417
418 auto ourProps = IndexPropertiesByName( *ourChange );
419 auto theirProps = IndexPropertiesByName( *theirChange );
420
421 // Union of property names touched on either side.
422 std::set<wxString> allNames;
423
424 for( const auto& [n, _] : ourProps ) allNames.insert( n );
425 for( const auto& [n, _] : theirProps ) allNames.insert( n );
426
427 bool hasUnresolvedProp = false;
428
429 for( const wxString& name : allNames )
430 {
431 auto ourIt = ourProps.find( name );
432 auto theirIt = theirProps.find( name );
433
434 const PROPERTY_DELTA* ourDelta = ourIt != ourProps.end() ? ourIt->second : nullptr;
435 const PROPERTY_DELTA* theirDelta = theirIt != theirProps.end() ? theirIt->second : nullptr;
436
437 const auto outcome = ResolvePropertyConflict( ourDelta, theirDelta, m_options );
438
440 p.name = name;
441 p.kind = outcome.kind;
442 r.props.push_back( std::move( p ) );
443
444 if( outcome.isUnresolved )
445 hasUnresolvedProp = true;
446 }
447
448 if( hasUnresolvedProp )
449 plan.unresolved.push_back( id );
450
451 plan.actions.push_back( std::move( r ) );
452 continue;
453 }
454
455 // Any remaining combination is conservative-conflict.
456 plan.unresolved.push_back( id );
458 r.id = id;
460 plan.actions.push_back( std::move( r ) );
461 }
462
463 // Pass 2: items touched only on one side — auto-take that side.
464 for( const auto& [id, ourChange] : ourIndex )
465 {
466 if( processed.count( id ) )
467 continue;
468
469 noteSideEffects( *ourChange );
470
472 r.id = id;
473
474 switch( ourChange->kind )
475 {
479 break;
480
482
486 plan.unresolved.push_back( id );
487 break;
488 }
489
490 plan.actions.push_back( std::move( r ) );
491 }
492
493 for( const auto& [id, theirChange] : theirIndex )
494 {
495 if( processed.count( id ) || ourIndex.count( id ) )
496 continue;
497
498 noteSideEffects( *theirChange );
499
501 r.id = id;
502
503 switch( theirChange->kind )
504 {
508 break;
509
511
515 plan.unresolved.push_back( id );
516 break;
517 }
518
519 plan.actions.push_back( std::move( r ) );
520 }
521
522 return plan;
523}
524
525} // namespace KICAD_DIFF
int index
const char * name
static DIFF_VALUE FromJson(const nlohmann::json &aJson)
MERGE_PLAN Plan(const DOCUMENT_DIFF &aAncestorOurs, const DOCUMENT_DIFF &aAncestorTheirs) const
Plan the merge given the canonical pair of diffs.
#define _(s)
bool ChangeInvalidatesZone(const ITEM_CHANGE &aChange)
Whether a change to an item of the given type invalidates any overlapping filled zones.
ITEM_RES
Resolution kind for a whole item.
const char * PropResToString(PROP_RES aRes)
Canonical lower-case spellings for PROP_RES used inside the JSON serialization of PROPERTY_RESOLUTION...
PROP_RES PropResFromString(const std::string &aStr)
PROPERTY_RESOLUTION_OUTCOME ResolvePropertyConflict(const PROPERTY_DELTA *aOurs, const PROPERTY_DELTA *aTheirs, const KICAD_MERGE_ENGINE::OPTIONS &aOptions)
Decide how to resolve a single property edit between two sides.
ITEM_RES ItemResFromString(const std::string &aStr)
PROP_RES
Resolution kind for a single property of a single item.
bool ChangeRequiresConnectivityRebuild(const ITEM_CHANGE &aChange)
Whether a change to an item of the given type requires the connectivity graph to be rebuilt.
std::map< KIID_PATH, const ITEM_CHANGE * > IndexChangesByKiid(const DOCUMENT_DIFF &aDiff)
Flatten a DOCUMENT_DIFF's ITEM_CHANGE tree into a KIID_PATH -> ITEM_CHANGE* map, recursing into child...
const char * ItemResToString(ITEM_RES aRes)
Canonical snake_case spellings used in MERGE_PLAN JSON serialization (take_ours / take_theirs / take_...
std::map< wxString, const PROPERTY_DELTA * > IndexPropertiesByName(const ITEM_CHANGE &aChange)
Index property deltas inside one ITEM_CHANGE by property name.
The full set of changes between two parsed documents of one type.
std::vector< ITEM_CHANGE > changes
One change record on a single item.
std::vector< PROPERTY_DELTA > properties
std::vector< ITEM_CHANGE > children
std::vector< PROPERTY_RESOLUTION > props
static ITEM_RESOLUTION FromJson(const nlohmann::json &aJson)
bool operator==(const ITEM_RESOLUTION &aOther) const
bool preferAutoMerge
When true, property-orthogonal edits auto-merge silently.
bool autoResolveEqualValues
When true and a property edit conflicts but the side values are equal, the resolution is automaticall...
Result of planning a 3-way merge.
std::vector< ITEM_RESOLUTION > actions
nlohmann::json ToJson() const
std::vector< KIID_PATH > unresolved
static MERGE_PLAN FromJson(const nlohmann::json &aJson)
Single (name, before, after) triple for one mutated property on an item.
Per-property merge decision result.
static PROPERTY_RESOLUTION FromJson(const nlohmann::json &aJson)
bool operator==(const PROPERTY_RESOLUTION &aOther) const