KiCad PCB EDA Suite
Loading...
Searching...
No Matches
auto_resolution.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 * or you may search the http://www.gnu.org website for the version 3 license,
20 * or you may write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 */
23
25
26#include <nlohmann/json.hpp>
27
28#include <wx/intl.h>
29
30#include <set>
31
32
33namespace KICAD_DIFF
34{
35
36AUTO_RESOLUTION_PARSE_RESULT ParseAutoResolutionJson( const std::string& aJsonContent )
37{
39
40 nlohmann::json j;
41
42 try
43 {
44 j = nlohmann::json::parse( aJsonContent );
45 }
46 catch( const nlohmann::json::exception& e )
47 {
49 out.errorContext = wxString::FromUTF8( e.what() );
50 return out;
51 }
52
53 if( !j.is_object() )
54 {
56 return out;
57 }
58
59 for( const auto& [key, val] : j.items() )
60 {
61 const wxString keyStr = wxString::FromUTF8( key );
62
63 if( !val.is_string() )
64 {
66 out.errorContext = keyStr;
67 out.resolutions.clear(); // contract: populated only on OK
68 return out;
69 }
70
72
73 try
74 {
75 kind = ItemResFromString( val.get<std::string>() );
76 }
77 catch( const std::invalid_argument& )
78 {
80 out.errorContext = keyStr;
81 out.resolutions.clear();
82 return out;
83 }
84
85 // Engine-internal kinds (KEEP / DELETE / MERGE_PROPS) make no sense
86 // as a scripted resolution — only the TAKE_* family represents a
87 // user-equivalent choice of side.
88 if( kind != ITEM_RES::TAKE_OURS && kind != ITEM_RES::TAKE_THEIRS
89 && kind != ITEM_RES::TAKE_ANCESTOR )
90 {
92 out.errorContext = keyStr;
93 out.resolutions.clear();
94 return out;
95 }
96
97 out.resolutions.emplace( keyStr, kind );
98 }
99
101 return out;
102}
103
104
105APPLY_AUTO_RESOLUTIONS_RESULT
107 const std::vector<std::size_t>& aConflictActionIndices,
108 const std::map<wxString, ITEM_RES>& aResolutions )
109{
111
112 if( aConflictActionIndices.empty() )
113 {
115 return out;
116 }
117
118 // Stage the writes so a partial-coverage failure leaves the plan
119 // untouched — the dialog promises that on PARTIAL the caller can
120 // still observe the original plan, e.g. for fallback reporting.
121 std::vector<std::pair<std::size_t, ITEM_RES>> staged;
122 staged.reserve( aConflictActionIndices.size() );
123
124 for( std::size_t actionIdx : aConflictActionIndices )
125 {
126 if( actionIdx >= aPlan.actions.size() )
127 {
128 // Stale/bad index — treat as PARTIAL so the caller bails out
129 // rather than silently dropping a conflict.
131 return out;
132 }
133
134 const ITEM_RESOLUTION& action = aPlan.actions[actionIdx];
135
136 auto it = aResolutions.find( action.id.AsString() );
137
138 if( it == aResolutions.end() )
139 {
140 if( action.kind == ITEM_RES::MERGE_PROPS )
141 continue;
142
144 out.firstMissingId = action.id;
145 return out;
146 }
147
148 staged.emplace_back( actionIdx, it->second );
149 }
150
151 for( const auto& [actionIdx, kind] : staged )
152 aPlan.actions[actionIdx].kind = kind;
153
154 aPlan.unresolved.clear();
156 out.appliedCount = staged.size();
157 return out;
158}
159
160
161std::vector<CONFLICT_LIST_ENTRY> BuildConflictList( const MERGE_PLAN& aPlan )
162{
163 std::vector<CONFLICT_LIST_ENTRY> out;
164
165 std::set<KIID_PATH> unresolvedSet( aPlan.unresolved.begin(), aPlan.unresolved.end() );
166
167 for( std::size_t i = 0; i < aPlan.actions.size(); ++i )
168 {
169 const ITEM_RESOLUTION& action = aPlan.actions[i];
170
171 if( !unresolvedSet.count( action.id ) && action.kind != ITEM_RES::MERGE_PROPS )
172 continue;
173
174 wxString label = action.id.AsString();
175
176 // Long KIID_PATH strings (multi-segment, on-disk-style) overflow the
177 // dialog's list control — keep the right side of the path because
178 // that's where the disambiguating segment lives.
179 if( label.Length() > 40 )
180 label = wxS( "…" ) + label.Right( 39 );
181
182 out.push_back( { i, label, action.id } );
183 }
184
185 return out;
186}
187
188
189std::vector<KIID_PATH>
191 const std::vector<std::size_t>& aConflictActionIndices )
192{
193 std::vector<KIID_PATH> out;
194
195 for( std::size_t actionIdx : aConflictActionIndices )
196 {
197 // Bounds-check the index — the function is public and callers can
198 // pass arbitrary indices. The dialog's own conflict-action list is
199 // built from plan.actions so this never fires for it, but a future
200 // mergetool consumer could feed in something stale.
201 if( actionIdx >= aPlan.actions.size() )
202 continue;
203
204 const ITEM_RESOLUTION& a = aPlan.actions[actionIdx];
205
208 {
209 out.push_back( a.id );
210 }
211 }
212
213 return out;
214}
215
216
217std::optional<BOX2I>
219 const std::map<KIID_PATH, BOX2I>& aPrimary,
220 const std::map<KIID_PATH, BOX2I>& aOurs,
221 const std::map<KIID_PATH, BOX2I>& aTheirs,
222 const std::map<KIID_PATH, BOX2I>& aAncestor )
223{
224 auto find = []( const std::map<KIID_PATH, BOX2I>& aMap,
225 const KIID_PATH& aLookup ) -> std::optional<BOX2I>
226 {
227 auto it = aMap.find( aLookup );
228
229 if( it == aMap.end() )
230 return std::nullopt;
231
232 // Treat a zero-size bbox (regardless of origin) as absence so a
233 // side with a placeholder entry falls through to the next side.
234 // A 1xN sliver is still usable — the highlight rectangle just
235 // gets narrow.
236 if( it->second.GetWidth() <= 0 && it->second.GetHeight() <= 0 )
237 return std::nullopt;
238
239 return it->second;
240 };
241
242 if( auto b = find( aPrimary, aId ); b.has_value() ) return b;
243 if( auto b = find( aOurs, aId ); b.has_value() ) return b;
244 if( auto b = find( aTheirs, aId ); b.has_value() ) return b;
245 if( auto b = find( aAncestor, aId ); b.has_value() ) return b;
246
247 return std::nullopt;
248}
249
250
251wxString BuildConflictDetailText( const ITEM_RESOLUTION& aResolution )
252{
253 wxString text;
254 text << _( "Item id: " ) << aResolution.id.AsString() << wxS( "\n" );
255 text << _( "Current resolution: " );
256
257 switch( aResolution.kind )
258 {
259 case ITEM_RES::TAKE_OURS: text << _( "Take ours" ); break;
260 case ITEM_RES::TAKE_THEIRS: text << _( "Take theirs" ); break;
261 case ITEM_RES::TAKE_ANCESTOR: text << _( "Take ancestor" ); break;
262 case ITEM_RES::MERGE_PROPS: text << _( "Property-level merge" ); break;
263 case ITEM_RES::DELETE_ITEM: text << _( "Delete" ); break;
264 case ITEM_RES::KEEP: text << _( "Keep (default)" ); break;
265 }
266
267 text << wxS( "\n\n" );
268 text << wxString::Format( wxPLURAL( "%zu property edit in this resolution.\n",
269 "%zu property edits in this resolution.\n", aResolution.props.size() ),
270 aResolution.props.size() );
271
272 return text;
273}
274
275} // namespace KICAD_DIFF
wxString AsString() const
Definition kiid.cpp:423
#define _(s)
wxString BuildConflictDetailText(const ITEM_RESOLUTION &aResolution)
Build the human-readable detail text shown in the merge dialog's resolution panel for a given ITEM_RE...
AUTO_RESOLUTION_PARSE_RESULT ParseAutoResolutionJson(const std::string &aJsonContent)
Parse a KICAD_MERGETOOL_AUTO-format JSON object mapping item IDs (KIID_PATH strings) to ITEM_RES spel...
@ BAD_VALUE
at least one entry's value wasn't a string
@ NOT_OBJECT
JSON parsed but the root was not an object.
@ INVALID_JSON
input could not be parsed as JSON
@ UNKNOWN_KIND
at least one entry's string wasn't a known ITEM_RES
@ OK
parsed cleanly; resolutions is populated
@ ENGINE_INTERNAL_KIND
at least one entry asked for an engine-internal kind (KEEP / DELETE / MERGE_PROPS); only the TAKE_* f...
ITEM_RES
Resolution kind for a whole item.
std::vector< CONFLICT_LIST_ENTRY > BuildConflictList(const MERGE_PLAN &aPlan)
std::optional< BOX2I > ResolveConflictBBox(const KIID_PATH &aId, const std::map< KIID_PATH, BOX2I > &aPrimary, const std::map< KIID_PATH, BOX2I > &aOurs, const std::map< KIID_PATH, BOX2I > &aTheirs, const std::map< KIID_PATH, BOX2I > &aAncestor)
Resolve the best bounding box for a conflicted item across the three sides of a 3-way merge.
APPLY_AUTO_RESOLUTIONS_RESULT ApplyAutoResolutions(MERGE_PLAN &aPlan, const std::vector< std::size_t > &aConflictActionIndices, const std::map< wxString, ITEM_RES > &aResolutions)
Apply a parsed auto-resolution map to a MERGE_PLAN's conflicts.
ITEM_RES ItemResFromString(const std::string &aStr)
std::vector< KIID_PATH > CollectUnresolvedConflicts(const MERGE_PLAN &aPlan, const std::vector< std::size_t > &aConflictActionIndices)
Return the conflict actions that still have no decision.
APPLY_AUTO_RESOLUTIONS_STATUS status
KIID_PATH firstMissingId
Set only when status == PARTIAL AND the partial result was caused by a missing entry in the auto-reso...
Result of ParseAutoResolutionJson.
std::map< wxString, ITEM_RES > resolutions
std::vector< PROPERTY_RESOLUTION > props
Result of planning a 3-way merge.
std::vector< ITEM_RESOLUTION > actions
std::vector< KIID_PATH > unresolved