KiCad PCB EDA Suite
Loading...
Searching...
No Matches
dialog_kicad_merge_3way.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
28#include <trace_helpers.h>
30
31#include <wx/ffile.h>
32#include <wx/log.h>
33#include <wx/msgdlg.h>
34#include <wx/sizer.h>
35#include <wx/utils.h>
36
37
38namespace
39{
40
44const KIGFX::COLOR4D& highlightColor()
45{
46 static const KICAD_DIFF::DIFF_COLOR_THEME theme;
47 return theme.conflict;
48}
49
50
55enum class AUTO_MODE
56{
57 NOT_SET, // KICAD_MERGETOOL_AUTO env var absent — use the dialog
58 LOADED, // env set, file valid, aOut populated
59 LOAD_FAILED // env set but file missing / unparseable / malformed
60};
61
62
66AUTO_MODE loadAutoResolutionFile( std::map<wxString, KICAD_DIFF::ITEM_RES>& aOut )
67{
68 wxString path;
69
70 if( !wxGetEnv( wxT( "KICAD_MERGETOOL_AUTO" ), &path ) || path.IsEmpty() )
71 return AUTO_MODE::NOT_SET;
72
73 wxFFile file( path, wxT( "rb" ) );
74 wxString contents;
75
76 if( !file.IsOpened() || !file.ReadAll( &contents, wxConvUTF8 ) )
77 {
78 wxLogTrace( traceDiffMerge,
79 wxT( "KICAD_MERGETOOL_AUTO points at unreadable path '%s'" ), path );
80 return AUTO_MODE::LOAD_FAILED;
81 }
82
85 std::string( contents.utf8_str().data(), contents.utf8_str().length() ) );
86
88 {
89 wxLogTrace( traceDiffMerge,
90 wxT( "KICAD_MERGETOOL_AUTO parse failed (status=%d, context=%s)" ),
91 static_cast<int>( parsed.status ), parsed.errorContext );
92 return AUTO_MODE::LOAD_FAILED;
93 }
94
95 aOut = std::move( parsed.resolutions );
96 return AUTO_MODE::LOADED;
97}
98
99} // namespace
100
101
103 const KICAD_DIFF::MERGE_PLAN& aPlan,
104 CONFLICT_CONTEXT aContext ) :
106 m_plan( aPlan ),
107 m_context( std::move( aContext ) )
108{
109 // Splice a GAL-backed conflict viewer into the resolution panel's sizer
110 // when one is present. The original .fbp emits a text-only layout; we
111 // augment it programmatically here rather than re-authoring the .fbp.
112 if( wxSizer* resolutionSizer = m_panelResolution->GetSizer() )
113 {
114 m_canvas = new WIDGET_DIFF_CANVAS( m_panelResolution );
115 m_canvas->SetMinSize( wxSize( -1, 200 ) );
116
117 // Insert just above the text detail (which sits at index 1 after
118 // the label). Keep the existing layout otherwise intact.
119 const size_t insertAt = std::min<size_t>( 1, resolutionSizer->GetItemCount() );
120 resolutionSizer->Insert( insertAt, m_canvas,
121 wxSizerFlags( 1 ).Expand().Border( wxALL, 4 ) );
122 m_panelResolution->Layout();
123 }
124
125 buildList();
126
127 if( m_conflictActionIndex.empty() )
128 {
129 m_labelDetail->SetLabel( _( "No conflicts to resolve." ) );
130 m_sdbSizerApply->Enable( true );
131 }
132 else
133 {
134 m_labelDetail->SetLabel( wxString::Format( _( "%zu conflict(s) require resolution." ),
135 m_conflictActionIndex.size() ) );
136 }
137
138 Layout();
139
140 // Headless / scripted mode: if KICAD_MERGETOOL_AUTO is set, apply the
141 // pre-recorded resolutions and close the dialog immediately. The
142 // EndModal is deferred via CallAfter so it runs after ShowModal has
143 // started its event loop. Used by CI test runners that exercise the
144 // full git mergetool → kicad-cli → kicad --mergetool → applier flow
145 // without a human at the keyboard.
146 //
147 // Bad-env paths (missing file, parse error, partial coverage, unknown
148 // resolution kind) explicitly EndModal( wxID_CANCEL ) so the caller's
149 // exit code is non-zero. Silently falling back to the interactive
150 // dialog would hang a CI run forever.
151 std::map<wxString, KICAD_DIFF::ITEM_RES> autoResolutions;
152
153 switch( loadAutoResolutionFile( autoResolutions ) )
154 {
155 case AUTO_MODE::NOT_SET:
156 break;
157
158 case AUTO_MODE::LOAD_FAILED:
159 wxLogTrace( traceDiffMerge,
160 wxT( "KICAD_MERGETOOL_AUTO load failed; closing dialog as cancelled" ) );
161 CallAfter( [this]() { EndModal( wxID_CANCEL ); } );
162 break;
163
164 case AUTO_MODE::LOADED:
165 {
168 autoResolutions );
169
170 switch( applied.status )
171 {
173 wxLogTrace( traceDiffMerge,
174 wxT( "KICAD_MERGETOOL_AUTO missing resolution for %s" ),
175 applied.firstMissingId.AsString() );
176 CallAfter( [this]() { EndModal( wxID_CANCEL ); } );
177 break;
178
181 CallAfter( [this]() { EndModal( wxID_APPLY ); } );
182 wxLogTrace( traceDiffMerge,
183 wxT( "KICAD_MERGETOOL_AUTO applied %zu auto-resolutions" ),
184 applied.appliedCount );
185 break;
186 }
187
188 break;
189 }
190 }
191}
192
193
194void DIALOG_KICAD_MERGE_3WAY::OnClose( wxCloseEvent& aEvent )
195{
196 EndModal( wxID_CANCEL );
197}
198
199
200void DIALOG_KICAD_MERGE_3WAY::OnCancel( wxCommandEvent& aEvent )
201{
202 EndModal( wxID_CANCEL );
203}
204
205
206void DIALOG_KICAD_MERGE_3WAY::OnApply( wxCommandEvent& aEvent )
207{
208 // Only mark resolved if every conflict has a concrete TAKE_* choice.
209 // KEEP / MERGE_PROPS at this point mean the user hasn't actually picked
210 // a side; without this guard the dialog would silently accept the
211 // engine's default resolution and lose user intent.
212 const std::vector<KIID_PATH> stillUnresolved =
214
215 if( !stillUnresolved.empty() )
216 {
217 wxMessageBox( wxString::Format( _( "%zu conflict(s) still need a resolution. "
218 "Pick Ours, Theirs, or Ancestor for each one." ),
219 stillUnresolved.size() ),
220 _( "Resolve Merge Conflicts" ),
221 wxOK | wxICON_INFORMATION, this );
222 return;
223 }
224
225 m_plan.unresolved.clear();
226 EndModal( wxID_APPLY );
227}
228
229
231{
232 showConflict( m_listConflicts->GetSelection() );
233}
234
235
237{
238 if( m_currentConflict < 0
239 || m_currentConflict >= static_cast<int>( m_conflictActionIndex.size() ) )
240 {
241 return;
242 }
243
244 std::size_t actionIdx = m_conflictActionIndex[m_currentConflict];
245 KICAD_DIFF::ITEM_RESOLUTION& action = m_plan.actions[actionIdx];
246
247 if( m_radioOurs->GetValue() ) action.kind = KICAD_DIFF::ITEM_RES::TAKE_OURS;
248 else if( m_radioTheirs->GetValue() ) action.kind = KICAD_DIFF::ITEM_RES::TAKE_THEIRS;
249 else if( m_radioAncestor->GetValue() ) action.kind = KICAD_DIFF::ITEM_RES::TAKE_ANCESTOR;
250
251 // Side selection also drives which geometry the canvas displays so the
252 // user can preview each candidate's context.
254}
255
256
258{
259 if( m_radioTheirs->GetValue() ) return SIDE::THEIRS;
260 if( m_radioAncestor->GetValue() ) return SIDE::ANCESTOR;
261 return SIDE::OURS;
262}
263
264
266{
267 if( !m_canvas )
268 return;
269
271 const std::map<KIID_PATH, BOX2I>* primaryBBoxes = nullptr;
272
273 switch( activeSide() )
274 {
275 case SIDE::OURS:
276 scene.referenceGeometry = m_context.oursGeometry;
277 primaryBBoxes = &m_context.oursBBoxes;
278 break;
279 case SIDE::THEIRS:
280 scene.referenceGeometry = m_context.theirsGeometry;
281 primaryBBoxes = &m_context.theirsBBoxes;
282 break;
283 case SIDE::ANCESTOR:
284 scene.referenceGeometry = m_context.ancestorGeometry;
285 primaryBBoxes = &m_context.ancestorBBoxes;
286 break;
287 }
288
289 static const std::map<KIID_PATH, BOX2I> emptyBBoxMap;
290
291 BOX2I conflictBBox;
292
293 if( m_currentConflict >= 0
294 && m_currentConflict < static_cast<int>( m_conflictActionIndex.size() ) )
295 {
296 const KICAD_DIFF::ITEM_RESOLUTION& action =
298
299 const std::optional<BOX2I> resolved =
301 action.id,
302 primaryBBoxes ? *primaryBBoxes : emptyBBoxMap,
303 m_context.oursBBoxes, m_context.theirsBBoxes,
304 m_context.ancestorBBoxes );
305
306 if( resolved.has_value() )
307 {
308 conflictBBox = *resolved;
309
311 shape.bbox = conflictBBox;
312 shape.color = highlightColor();
313 shape.changeId = action.id;
314 scene.conflictShapes.push_back( shape );
315 }
316 }
317
318 // documentBBox covers the side's geometry so the user can pan/zoom out
319 // to see context; the explicit ZoomToBBox below focuses the initial view
320 // on the conflict so it isn't lost in the wide-area auto-fit.
322
323 const std::optional<KIID_PATH> highlight =
325 ? std::optional<KIID_PATH>(
327 : std::nullopt;
328
329 m_canvas->SetScene( std::move( scene ) );
330 m_canvas->HighlightChange( highlight );
331
332 if( conflictBBox.GetWidth() > 0 || conflictBBox.GetHeight() > 0 )
333 {
334 // Pad the focus a bit so the highlight isn't flush with the edges
335 // and the user sees some surrounding context.
336 BOX2I focus = conflictBBox;
337 focus.Inflate( std::max( conflictBBox.GetWidth(), conflictBBox.GetHeight() ) );
338 m_canvas->ZoomToBBox( focus );
339 }
340}
341
342
344{
345 m_listConflicts->Clear();
346 m_conflictActionIndex.clear();
347
348 for( const KICAD_DIFF::CONFLICT_LIST_ENTRY& entry
350 {
351 m_listConflicts->Append( entry.label );
352 m_conflictActionIndex.push_back( entry.actionIndex );
353 }
354}
355
356
358{
359 m_currentConflict = aIndex;
360
361 if( aIndex < 0 || aIndex >= static_cast<int>( m_conflictActionIndex.size() ) )
362 {
363 m_labelDetail->SetLabel( _( "Select a conflict on the left to see details." ) );
364 m_textDetail->SetValue( wxEmptyString );
365 return;
366 }
367
368 std::size_t actionIdx = m_conflictActionIndex[aIndex];
369 const KICAD_DIFF::ITEM_RESOLUTION& action = m_plan.actions[actionIdx];
370
371 m_labelDetail->SetLabel( wxString::Format( _( "Conflict on %s" ), action.id.AsString() ) );
372
374
375 // Sync the radio buttons to the current resolution.
376 m_radioOurs->SetValue( action.kind == KICAD_DIFF::ITEM_RES::TAKE_OURS );
379
381}
BOX2< VECTOR2I > BOX2I
Definition box2.h:918
constexpr BOX2< Vec > & Inflate(coord_type dx, coord_type dy)
Inflates the rectangle horizontally by dx and vertically by dy.
Definition box2.h:554
constexpr size_type GetWidth() const
Definition box2.h:210
constexpr size_type GetHeight() const
Definition box2.h:211
DIALOG_KICAD_MERGE_3WAY_BASE(wxWindow *parent, wxWindowID id=wxID_ANY, const wxString &title=_("Resolve Merge Conflicts"), const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxSize(950, 700), long style=wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER)
void OnConflictSelected(wxCommandEvent &aEvent) override
DIALOG_KICAD_MERGE_3WAY(wxWindow *aParent, const KICAD_DIFF::MERGE_PLAN &aPlan, CONFLICT_CONTEXT aContext={})
void OnApply(wxCommandEvent &aEvent) override
void OnCancel(wxCommandEvent &aEvent) override
void rebuildCanvas()
Rebuild the canvas scene from the current side (per radio) and the active conflict's bbox.
void OnClose(wxCloseEvent &aEvent) override
WIDGET_DIFF_CANVAS * m_canvas
GAL-backed conflict viewer injected into the resolution panel.
KICAD_DIFF::MERGE_PLAN m_plan
int m_currentConflict
Currently-displayed conflict (index into m_conflictActionIndex), -1 if none.
void showConflict(int aIndex)
Update the detail panel for the selected conflict.
void buildList()
Populate the conflict list from the plan's unresolved items.
SIDE
Which side's geometry the canvas should currently display.
void OnResolutionChanged(wxCommandEvent &aEvent) override
std::vector< std::size_t > m_conflictActionIndex
Indices into m_plan.actions matching the order of m_listConflicts.
A color representation with 4 components: red, green, blue, alpha.
Definition color4d.h:101
wxString AsString() const
Definition kiid.cpp:393
#define _(s)
const wxChar *const traceDiffMerge
Flag to enable diff/merge engine and renderer debugging output.
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...
@ OK
parsed cleanly; resolutions is populated
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.
void ExpandBBoxToGeometry(DIFF_SCENE &aScene)
Grow the scene's documentBBox to also include the extent of any background geometry.
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 /...
STL namespace.
@ LOAD_FAILED
Phase 8 context for the conflict canvas.
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
Build the list of conflict-action indices the dialog walks (i.e.
DOCUMENT_GEOMETRY referenceGeometry
Background geometry from the two source documents.
Definition diff_scene.h:239
std::vector< SCENE_SHAPE > conflictShapes
Definition diff_scene.h:233
Result of planning a 3-way merge.
Shared rendering model consumed by both the GAL renderer (interactive widget) and the plotter rendere...
Definition diff_scene.h:90
KIGFX::COLOR4D color
Definition diff_scene.h:92
KIID_PATH changeId
Stable identifier of the ITEM_CHANGE that produced this shape.
Definition diff_scene.h:99
std::string path
wxLogTrace helper definitions.