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 {
141 out.firstMissingId = action.id;
142 return out;
143 }
144
145 staged.emplace_back( actionIdx, it->second );
146 }
147
148 for( const auto& [actionIdx, kind] : staged )
149 aPlan.actions[actionIdx].kind = kind;
150
151 aPlan.unresolved.clear();
153 out.appliedCount = staged.size();
154 return out;
155}
156
157
158std::vector<CONFLICT_LIST_ENTRY> BuildConflictList( const MERGE_PLAN& aPlan )
159{
160 std::vector<CONFLICT_LIST_ENTRY> out;
161
162 std::set<KIID_PATH> unresolvedSet( aPlan.unresolved.begin(), aPlan.unresolved.end() );
163
164 for( std::size_t i = 0; i < aPlan.actions.size(); ++i )
165 {
166 const ITEM_RESOLUTION& action = aPlan.actions[i];
167
168 if( !unresolvedSet.count( action.id ) )
169 continue;
170
171 wxString label = action.id.AsString();
172
173 // Long KIID_PATH strings (multi-segment, on-disk-style) overflow the
174 // dialog's list control — keep the right side of the path because
175 // that's where the disambiguating segment lives.
176 if( label.Length() > 40 )
177 label = wxS( "…" ) + label.Right( 39 );
178
179 out.push_back( { i, label, action.id } );
180 }
181
182 return out;
183}
184
185
186std::vector<KIID_PATH>
188 const std::vector<std::size_t>& aConflictActionIndices )
189{
190 std::vector<KIID_PATH> out;
191
192 for( std::size_t actionIdx : aConflictActionIndices )
193 {
194 // Bounds-check the index — the function is public and callers can
195 // pass arbitrary indices. The dialog's own conflict-action list is
196 // built from plan.actions so this never fires for it, but a future
197 // mergetool consumer could feed in something stale.
198 if( actionIdx >= aPlan.actions.size() )
199 continue;
200
201 const ITEM_RESOLUTION& a = aPlan.actions[actionIdx];
202
205 {
206 out.push_back( a.id );
207 }
208 }
209
210 return out;
211}
212
213
214std::optional<BOX2I>
216 const std::map<KIID_PATH, BOX2I>& aPrimary,
217 const std::map<KIID_PATH, BOX2I>& aOurs,
218 const std::map<KIID_PATH, BOX2I>& aTheirs,
219 const std::map<KIID_PATH, BOX2I>& aAncestor )
220{
221 auto find = []( const std::map<KIID_PATH, BOX2I>& aMap,
222 const KIID_PATH& aLookup ) -> std::optional<BOX2I>
223 {
224 auto it = aMap.find( aLookup );
225
226 if( it == aMap.end() )
227 return std::nullopt;
228
229 // Treat a zero-size bbox (regardless of origin) as absence so a
230 // side with a placeholder entry falls through to the next side.
231 // A 1xN sliver is still usable — the highlight rectangle just
232 // gets narrow.
233 if( it->second.GetWidth() <= 0 && it->second.GetHeight() <= 0 )
234 return std::nullopt;
235
236 return it->second;
237 };
238
239 if( auto b = find( aPrimary, aId ); b.has_value() ) return b;
240 if( auto b = find( aOurs, aId ); b.has_value() ) return b;
241 if( auto b = find( aTheirs, aId ); b.has_value() ) return b;
242 if( auto b = find( aAncestor, aId ); b.has_value() ) return b;
243
244 return std::nullopt;
245}
246
247
248wxString BuildConflictDetailText( const ITEM_RESOLUTION& aResolution )
249{
250 wxString text;
251 text << _( "Item id: " ) << aResolution.id.AsString() << wxS( "\n" );
252 text << _( "Current resolution: " );
253
254 switch( aResolution.kind )
255 {
256 case ITEM_RES::TAKE_OURS: text << _( "Take ours" ); break;
257 case ITEM_RES::TAKE_THEIRS: text << _( "Take theirs" ); break;
258 case ITEM_RES::TAKE_ANCESTOR: text << _( "Take ancestor" ); break;
259 case ITEM_RES::MERGE_PROPS: text << _( "Property-level merge" ); break;
260 case ITEM_RES::DELETE_ITEM: text << _( "Delete" ); break;
261 case ITEM_RES::KEEP: text << _( "Keep (default)" ); break;
262 }
263
264 text << wxS( "\n\n" );
265 text << wxString::Format( _( "%zu property edit(s) in this resolution.\n" ),
266 aResolution.props.size() );
267
268 return text;
269}
270
271} // namespace KICAD_DIFF
wxString AsString() const
Definition kiid.cpp:393
#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 subset of conflict actions whose kind is NOT one of the concrete TAKE_OURS / TAKE_THEIRS /...
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